• LEK分布式日志系统搭建


    1:日志方案介绍

       通常日志被分散存储到不同的设备上,如果你管理多台服务器,你可能还在使用依次登陆每台机器的做法来查看日志,这样效率比较低下。当务之急我们使用集中化的日志管理,开源实时日志分析ELK平台能够完美的解决我们上述的问题。具体流程如下图1。

    图1  ELK方案

    2:ELK介绍

    ELK由ElasticSearch、Logstash和Kiabana三个开源工具组成。

    Elasticsearch是个开源分布式搜索引擎,它的特点有:分布式,零配置,自动发现,索引自动分片,索引副本机制,restful风格接口,多数据源,自动搜索负载等。

    Logstash是一个完全开源的工具,他可以对你的日志进行收集、过滤,并将其存储供以后使用(如,搜索)。

    Kibana 也是一个开源和免费的工具,它Kibana可以为 Logstash 和 ElasticSearch 提供的日志分析友好的 Web 界面,可以帮助您汇总、分析和搜索重要数据日志。

    画了一个ELK工作的原理,如下图2:

    图2 ELK工作原理图

    如图:Logstash收集AppServer产生的Log,并存放到ElasticSearch集群中,而Kibana则从ES集群中查询数据生成图表,再返回给Browser。

    3:单机器Linux搭建ELK

    官网下载安装包 https://www.elastic.co/downloads。

    ElasticSearch: 2.4.1

    Logstash: 2.4.0

    Kibana: 4.6.0

    Java:  version  "1.7.0_65"

    注:

    a:由于Logstash的运行依赖于Java环境, 而Logstash 1.5以上版本不低于java 1.7,因此推荐使用最新版本的Java。因为我们只需要Java的运行环境,请自行搜索安装。

    3.1:安装Elasticsearch

    在安装Elasticsearch前,需要创建一个非ROOT用户,因为Elasticsearch只能由非root用户来进行操作。具体步骤如下:

    1:解压:tar -zxvf elasticsearch-2.4.1.tar.gz

    将解压后的整个文件授权给非root用户

    2:修改配置文件:vi elasticsearch-2.4.1/config/elasticsearch.yml 

    cluster.name: my-application (集群名字,随意)

    node.name : node-0  (节点名字 随意)

    network.host: 192.168.56.101  (自己IP)

    http.port: 9200       (端口不需要修改)

    3:插件安装

    在bin目录下。安装head插件,主要是用来观看节点状态以及索引等等。

    联网状态下:

    head插件:./bin/plugin install mobz/elasticsearch-head

       bigdesk插件:./bin/plugin install AIsaac08/bigdesk (只适合2.0以上版本)

          

           不联网可以直接从svn上下载

    http://10.112.1.225:7080/svn/Speed4J/company_doc/学习资料/ELK/ELK_linux软件

            然后新建一个plugins目录。放进去就可以了。

    4: 启动检测 (必须用非root用户启动)

    启动 ./bin/elasticsearch &  (&代表了后台启动)

    a:正常启动(9200端口必须开通)

    b:head效果

    ip:9200/_plugin/head

    c:bigdesk效果

    ip:9200/_plugin/bigdesk

     5:注意事项

    对配置文件修改时,建议开头不要保留空格,有时候会出现奇怪错误。

    3.2:安装logstash

    Logstash安装比较简单:

    1:解压,。tar -zxvf logstash-2.4.0 tar.gz

    2:编写配置文件conf( 文件名和存放位置随意)

       在这里我存放在新建的conf文件夹下,名字为001.conf

    3:进行配置

    目前是在单机版本上部署,简单说明下各个配置,file代表的是文件输入,path代表的是文件路径。Type代表了自定义的文件类型,可以修改。start_position 代表了从哪里开始读取文件。Elasticserach暂时照抄这个配置,只要把hosts代表的ip修改为自己的就好。

    input {

        file {

            path =>"E:/SOFTALL/ELKoldlogs/filelog.txt"

            type =>"logs"

            start_position =>"beginning"

        }

    }

    filter {

    }

    output {

    elasticsearch {

            hosts =>"192.168.56.101"  (换成自己的ip)

            index =>"logstash-%{type}-%{+YYYY.MM.dd}"

            document_type =>"%{type}"

            workers => 10

            template_overwrite => true

        }

    }

    4 启动测试

    ./bin/logstash agent -f config/001.conf  --configtest

    出现ok字样,就说明你的配置文件的基本格式没错误,但具体语法还无法检测。

    ./bin/logstash agent -f config/001.conf  &  后台启动

    3.3 安装Kibana

    Kibana安装和logstash有点类似,具体步骤:

    1:解压压缩包 tar -zxvf kibana-4.6.1-linux-x86_64.tar.gz 

    2:修改配置文件

    vi config/kibana.yml 
           server.port: 5601 
           server.host: “192.168.56.101” 
           elasticsearch.url: http://192.168.56.101:9200 
           kibana.index: “.kibana” 

    3:结果展示:

    出现这个画面后,点击create,然后再点击Discover:

    4:注意事项

    在elasticsearch.url: 这里,冒号后面必须存在空格。非常重要

    对配置进行修改时,开头不能存在空格。

    到此,单机器版安装部署ELK已经成功了。

    关闭kibana操作  fuser -n tcp 5601  然后杀掉对应端口就好了。

    4:logstash详解

    Logstash主要是用来读取日志,解析日志,输出日志。其中还有对日志的编码,格式等一些设置。

    下图3是logstash工作原理图。

                          图3 logtsahs工作流程

    4.1:input讲解

    在介绍input配置前,我们可以在logstash目录下输入这样一串命令

    ./logstahs -e ‘’

    然后在窗口下手动输入hello world。等下

    就会出现下面类似效果

    刚才的./logstahs -e ‘’其实完整命令就是

    logtshash -e 'input{stdin{}} output {stdout{ codec => rubydebug }}'

    4.1.1 文件输入 file

    在本次平台建设中,只使用了file作为input的输入源。特对部分参数进行详细介绍。

    l  codec =>  #可选项,codec,默认是plain,

    可通过这个参数设置编码方式

    l  discover_interval =>  #可选项,number,

    logstash 每隔多久去检查一次被监听的 path 下是否有新文件。默认值是 15 秒。

    l   exclude =>...  #可选项,array

    不想被监听的文件可以排除出去,这里跟 path 一样支持 glob 展开。

    l   sincedb_path =>...  #可选项,string,

    如果你不想用默认的$HOME/.sincedb(Windows平台上在C:WindowsSystem32configsystemprofile.sincedb),可以通过这个配置定义 sincedb 文件到其他位置。

    l  sincedb_write_interval => #可选项, number,

    logstash 每隔多久写一次 sincedb 文件,默认是 15 秒。

    l  stat_interval => #可选项, number,

    logstash 每隔多久检查一次被监听文件状态(是否有更新),默认是 1 秒。

    start_position => #可选项string

    logstash 从什么位置开始读取文件数据,默认是结束位置,也就是说 logstash 进程会以类似 tail -F 的形式运行。如果你是要导入原有数据,把这个设定改成 “beginning”,logstash 进程就从头开始读取,有点类似cat,但是读到最后一行不会终止,而是继续变成 tail –F;默认值end

    path => # 必选项, array

    定需处理的文件路径, 可以定义多个路径

    tags => # 可选项, array

    在数据处理过程中,由具体的插件来添加或者删除的标记

     type =># 可选项, string

    自定义将要处理事件类型,可以自己随便定义,比如处理的是linux的系统日志,可以定义为"syslog"

    参考配置:

     file{

             codec => plain{ charset =>"UTF-8"    } 编码格式

             type =>"cluster"   文件类型

             path =>"/home/logstash-2.4.0/logtest/1025/004.log"  文件路径

             start_position =>"beginning" 从什么地方读取

        }

    4.1.2 multiline多行事件编码

    在日志记录中,有的时候会出现报错日志例如java的异常日志,这个时候我们可以把报错部分当作一个整体,合并为一行来进行输出。此时就涉及到multiline的具体用法。

    参考配置:

     codec => multiline{

                            charset =>"UTF-8"

                            pattern =>"^d{4}-d{2}"

                            negate => true 

                            what =>"previous"

             }

    Charset :指定编码

    pattern =>...    # string 类型,设置匹配的正则表达式 ,必选   

    negate =>...    # boolean类型,设置正向匹配还是反向匹配,默认是false,可选

    what =>...   # 设置未匹配的内容是向前合并还是向后合并,previous, next 两个值选择,必选

    previous是指这段异常文字和前面的合并为一行输出了。Next是指与后面的合并为一行输出了。

    4.1.3 add_field介绍

    add_field 就是添加一个字段,这个是个很有用的。使用上比较简单

    add_field=>{

                         "append"=>"runtime-cop"

                         }     

    具体效果就是增加了一个append的字段,它的值是runtime-cop。

    4.2 filter讲解 ★★★★★

    Logstash的强大能力的突出一点就在filter中。它扩展了进入过滤器的原始数

    据,进行复杂的逻辑处理,甚至可以无中生有的添加新的 logstash 事件到后续的流程中去。后续学习,可以去参考官方文档,根据具体业务需求,增加不同的解析手段:https://www.elastic.co/guide/en/logstash/2.4/input-plugins.html。

    4.2.1 Grok讲解

    Grok是logstash里面自带的正则语法。是一种非常强大的解析各种非结构化的日志数据的工具,官方罗列出来的有120多种,极大程度上方便了开发人员或运维人员来对特定的日志进行解析,具体网址如下:https://github.com/logstash-plugins/logstash-patterns-core/tree/master/patterns

    USERNAME [a-zA-Z0-9._-]+

    USER %{USERNAME}

    INT (?:[+-]?(?:[0-9]+))

    BASE10NUM (?<![0-9.+-])(?>[+-]?(?:(?:[0-9]+(?:.[0-9]+)?)|(?:.[0-9]+)))

    NUMBER (?:%{BASE10NUM})

    BASE16NUM (?<![0-9A-Fa-f])(?:[+-]?(?:0x)?(?:[0-9A-Fa-f]+))

    BASE16FLOAT (?<![0-9A-Fa-f.])(?:[+-]?(?:0x)?(?:(?:[0-9A-Fa-f]+(?:.[0-9A-Fa-f]*)?)|(?:.[0-9A-Fa-f]+)))

    用法: %{USERNAME :name}

    这个name就是自定义的随意单词,用来匹配符合这个 [a-zA-Z0-9._-]+

    例子:

    55.3.244.1 GET /index.html 15824 0.043

    %{IP:ip}%{WORD:method}%{URIPATHPARAM:request}%{NUMBER:bytes}%{NUMBER:duration}

    在解析日志的过程中,一般采用官方的正则都可以来满足解析要求。当然也能进行自定义正则。

    (?<queue_id>[0-9A-F]{10,11})  (?<className>[[sS]*]) 这个就是自定义的正则,<className>里面的className 是自定义正则名字  [[sS]*] 是格式

    2016-09-29 10:26:31,652 [cn.xulei.elk.ElkDemo02]

    %{TIMESTAMP_ISO8601:logdate}s?(?<className>[[sS]*])?"] 

    当然,也能采用%{}这样的用法,这时候就需要把自定义的正则放在一个文件中该文件名随意,地址随意,

    参考配置:

    2016-09-29 10:26:31,652 [cn.xulei.elk.ElkDemo01][140] [main] - Info log [20000].

     

    grok{

           patterns_dir => ["./patterns/test01.txt"]

    match=>["message","%{TIMESTAMP_ISO8601:logdate}s?[%{className:class}]s?[%{classLine:line}]s?[%{threadName:thread}]s?-s?%{LOGLEVEL:logLevel}s?(?<sss>[sS]*)?"] 

    }

    上面的patterns_dir是指规则路径。可以使用相对路径。

    分别匹配:

    2016-09-29 10:26:31,652   %{TIMESTAMP_ISO8601:logdate}

    [cn.xulei.elk.ElkDemo01]   [%{className:class}]

    [140]    [%{classLine:line}]

    [main]   [%{threadName:thread}]

    Info  %{LOGLEVEL:logLevel}

    log [20000]. (?<sss>[sS]*)?

    4.2.2 条件判断    

    ==(等于), !=(不等于), <(小于), >(大于), <=(小于等于), >=(大于等于)

     

    =~(匹配正则), !~(不匹配正则)

     

    in(包含), not in(不包含)

     

    and(与), or(或), nand(非与), xor(非或)

     

    ():复合表达式,    !():对复合表达式结果取反

    参考配置:

    if [append] =="runtime-cop" {

    grok{

    patterns_dir => ["/home/logstash-2.4.0/patterns/test01.txt"]

    match=> ["message","%{TIMESTAMP_ISO8601:logdate}s?[%{moduleName:moduleName}]s?[%{threadName:thread}]s?[%{tradeNo:tradeNo}]s?[%{LOGLEVEL:logLevel}]s?(?<sss>[sS]*)?"  ]            

    }    

    }else if [append] =="runtime-framework" {

    grok{

    patterns_dir => ["/home/logstash-2.4.0/patterns/test01.txt"]

    match=> ["message","%{TIMESTAMP_ISO8601:logdate}s?[%{className:className}]s?[%{threadName:thread}]s?[%{tradeNo:tradeNo}]s?[%{LOGLEVEL:logLevel}]s?(?<sss>[sS]*)?"  ]           

    }    

    }

    4.3 output讲解

    Output作为输出,可以有不同的输出源,例如file,es,tcp等等.本次运行的数据全部输出到ES中.就以ES来作为讲解

    elasticsearch {

                 

               hosts =>"192.168.56.102"

               index =>"logstash-logs-%{+YYYY.MM.dd}"

               document_type =>"%{type}"

               workers => 10

             }

    1:hosts 代表了es所在的机器IP。

    里面可以为数组。[“192.168.56.102:9200”,”192.168.56.101:9200”],

    这代表了一个logstash发送到了2个es中。

    2:index =>"logstash-logs-%{+YYYY.MM.dd}"

    代表了这个创建的索引,是根据日期来创建的,其中的logs其实就是自己随便写的,

    其余的都不是必须的,可以照抄

    5:Elasticsearch 详解

    5.1 Elasticsearch数据存储分片原理介绍

        对传入的每一条数据,es都会采用一定的计算规则来处理,最后获取到一个routing参数,默认情况下,就使用其_id值。将其_id值计算哈希后,对索引的主分片数取余,就是数据实际应该存储到的分片 ID。由于取余这个计算,完全依赖于分母,所以导致 ES 索引有一个限制,索引的主分片数,不可以随意修改。因为一旦主分片数不一样,所以数据的存储位置计算结果都会发生改变,索引数据就完全不可读了。我们只需要默认配置不修改就好了。

       作为一个分布式存储日志的工具,创建数据副本是必需的,ES 数据写入流程,自然也涉及到副本。在有副本配置的情况下,数据从发向 ES 节点,到接到 ES 节点响应返回,流程图如下:

    1. 客户端请求发送给 Node 1 节点,注意图中 Node 1 是 Master 节点,实际完全可以不是。

       2. Node 1 用数据的 _id 取余计算得到应该讲数据存储到 shard 0 上。通过cluster state 信息发现 shard 0 的主分片已经分配到了 Node 3 上。Node 1 转

    发请求数据给 Node 3。

       3. Node 3 完成请求数据的索引过程,存入主分片 0。然后并行转发数据给分配有shard 0 的副本分片的 Node 1 和 Node 2。当收到任一节点汇报副本分片数据写入成功,Node 3 即返回给初始的接收节点 Node 1,宣布数据写入成功。

    Node 1 返回成功响应给客户端。

    elasticsearch的config文件夹里面有两个配置文件:elasticsearch.yml和logging.yml 配置集群主要是看elasticsearch.yml的配置。

    cluster.name:elasticsearch

    配置es的集群名称,默认是elasticsearch,es会自动发现在同一网段下的es,如果在同一网段下有多个集群,就可以用这个属性来区分不同的集群。

     

    node.name:"FranzKafka"

    节点名,默认随机指定一个name列表中名字,

    node.master:true

    指定该节点是否有资格被选举成为node,默认是true,es是默认集群中的第一台机器为master,如果这台机挂了就会重新选举master。

     

    node.data:true

    指定该节点是否存储索引数据,默认为true。

    1:master:true  data:false  只为主节点 但不存储数据

    2:master: true data:true  这个是默认配置 就是自主选择主节点,都存储数据

     

    network.bind_host:192.168.0.1

    设置绑定的ip地址,可以是ipv4或ipv6的,默认为0.0.0.0。

     

    http.port:9200

    设置对外服务的http端口,默认为9200。

     

    transport.tcp.port:9300

    设置节点间交互的tcp端口,默认是9300。这个是多个node节点之间的通信端口

     

    5.2 主节点不存储数据

    此时。Node-17是主节点,不存储数据,

    对应配置为

    node.master:true 

    node.data:false

    其他节点配置为:

    node.master:false 

    node.data:true

    下面是主节点不存储数据的head插件图。

     访问主节点17的head页面

     

    从图可知:

    1:每个索引都被分为了5个分片

    2:每个分片都有一个副本。

    3:5个分片都随机分配到2个数据节点上。

    注意分片的边框(border)有粗有细,具体区别是:

    粗边框代表:primary(true)细边框 就是false。一个代表了副分片,一个是主分片

    提前介绍点集群,在页面左边上,有个集群健康值

    这个值有3种表现形式:

    1:green 路灯 所有分片都正常运行,

    2:yellow 黄灯 所有主分片都正常运行,但是副分片有缺失,这意味着当前的es是可以运行的,但是有一定的风险。

    3:red红灯,有主分片缺少,这部分数据不可用。

    5.3 自主选择主节点

    默认配置

    这个就是自主选择主节点。Fox和fox2都可以随便变为主节点,

    配置是默认配置

    node.master : true

    node.data: true

    5.4 Elasticsearch集群配置

    在elasticsearch中,集群配置比较简单

    discovery.zen.ping.unicast.hosts: ["192.168.56.101:9300","192.168.56.102:9300"]

    这句话是说设置集群中master节点的初始列表,可以通过这些节点来自动发现新加入集群的节点,好,到此,你就成功了。

    5.4.1集群unicast连接

    ES 是一个 P2P 类型(使用 gossip 协议)的分布式系统,除了集群状态管理以外,其他所有的请求都可以发送到集群内任意一台节点上,这个节点可以自己找到需要转发给哪些节点,并且直接跟这些节点通信。从配置上来看,很简单,只需要写同一个cluster.name。那么就会自动归属到同一个集群中。

    ES从2.0开始。从以前的自动发现方式改变为支持一种unicast模式(单播方式),在这里只需要配置几个具体的节点值。Es就能够把它视为处入整个集群传播途径中,通过他们来组建集群。所以处入unicast模式下的集群,只需要配置相同的几个节点就可以了。

    参考配置1

    network.host: "192.168.0.2"

    discovery.zen.ping.timeout: 100s  

    discovery.zen.fd.ping_timeout: 100s

    discovery.zen.ping.unicast.hosts: ["10.19.0.97","10.19.0.98","10

    .19.0.99","10.19.0.100"]

    discovery.zen.ping.timeout 参数仅在加入或者选举 master 主节点的时候才起

    作用;

    discovery.zen.fd.ping_timeout 参数则在稳定运行的集群中,master 检测所有

    节点,以及节点检测 master 是否畅通时长期有用。

        当ES集群发展到一定规模的时候,可能会出现多个集群,随之而来的是数据分配到多个集群中,但数据还是需要一起使用,为了解决这个问题,我们需要使用到一个配置tribe节点。目前,我们的需求还没达到,感兴趣的同学可以去自己研究研究。

    全部配置:

    # ---------------------------------- Cluster -----------------------------------

    # Use a descriptive name for your cluster:

    cluster.name: linux

    # ------------------------------------ Node ------------------------------------

    # Use a descriptive name for the node:

    node.name: node-17   节点名字

    node.master: true       主节点配置

    node.data: false        数据节点配置

    # Add custom attributes to the node:

    #

    # node.rack: r1

    # ----------------------------------- Paths ------------------------------------

    # Path to directory where to store the data (separate multiple locations by comma):

    #

    # path.data: /path/to/data  

    设置配置文件的存储路径,默认是es根目录下的config文件夹。

    #

    # Path to log files:

    #

    # path.logs: /path/to/logs

    设置日志文件的存储路径,默认是es根目录下的logs文件夹

    # ----------------------------------- Memory -----------------------------------

    # Lock the memory on startup:  是否锁定内存。

    # bootstrap.memory_lock: true

    # Make sure that the `ES_HEAP_SIZE` environment variable is set to about half the memory

    # available on the system and that the owner of the process is allowed to use this limit.

    # Elasticsearch performs poorly when the system is swapping the memory.

    # ---------------------------------- Network -----------------------------------

    # Set the bind address to a specific IP (IPv4 or IPv6):

    #

    network.host: 192.168.10.17    设置绑定的ip地址,

    #

    # Set a custom port for HTTP:

    http.port: 9200    设置对外服务的http端口,默认为9200。

    # For more information, see the documentation at:

    <http://www.elastic.co/guide/en/elasticsearch/reference/current/modules-network.html>

    # --------------------------------- Discovery ----------------------------------

    # Pass an initial list of hosts to perform discovery when new node is started:

    # The default list of hosts is ["127.0.0.1", "[::1]"]

    discovery.zen.ping.unicast.hosts:["192.168.10.17:9300","192.168.10.23:9300","192.168.10.10:9300"]

    #

    # Prevent the "split brain" by configuring the majority of nodes (total number of nodes / 2 + 1):

    #

    # discovery.zen.minimum_master_nodes: 3

    #

    # For more information, see the documentation at:

    # <http://www.elastic.co/guide/en/elasticsearch/reference/current/modules-discovery.html>

    #

    # ---------------------------------- Gateway -----------------------------------

    #

    # Block initial recovery after a full cluster restart until N nodes are started:

    #

    # gateway.recover_after_nodes: 3

    #gateway.recover_after_nodes:1

    设置集群中N个节点启动时进行数据恢复,默认为1。

    #

    # For more information, see the documentation at:

    #

    # ---------------------------------- Various -----------------------------------

    #

    # Disable starting multiple nodes on a single system:

    #

    # node.max_local_storage_nodes: 1

    #

    # Require explicit names when deleting indices:

    #

    # action.destructive_requires_name: true

    注意:

           在单独的服务器上,建议锁定内存,这样可以提高效率,具体配置是

    bootstrap.memory_lock: true。

    同时在elasticsearch的bin目录下,修改elasticseach.in.sh的ES_MIN_MEM=

    ES_MAX_MEM=具体内存大小

    6 kibana介绍

    Kibana可以理解为一个展示ES数据的简单页面。

    6.1 添加自定义字段解析数据查找数据

    当对数据进行解析后,可以重新刷新来建立索引,操作流程

    1:打开主页 http://ip:5601

    2:点击Setting  然后再点击creat。

    3:

    6.1.1怎么查询指定交易号的记录

    JSON数据展示

    6.2 kibana普通查询语法

    1:按页面左侧显示的字段搜索

    例如左边有message  host等等字段。

    限定字段全文搜索:message:value

    精确搜索:关键字加上双引号 message:"value"

    如果要根据id来插叙   _id : 123456  等等

    2:通配符模式

    ? 匹配单个字符

    * 匹配0到多个字符

     例如 *cn  前面的有47条记录 。或者是cn*这个就有70多条记录

    3:时间定位

    有quick Relative Absolute 等,等级越来越高。可以具体到几分几秒

    点击右边

    出现quick  Relative  Absolute

    3:多条件组合搜索

    多个检索条件的组合:- 代表了不能含有 +代表了必须含有

    + Checking 代表必须还有Checking  - Checking:不能含有此项

     And 表示和  or表示或者    Checking And to

    4:近似搜索:

    " Checking  if "~2 表示 Checking 和 if 中间隔着2个单词以内

    在查询的过程中,记得写空格。

    6.3 kibana对应集群配置介绍

        Kibana就是一个网页应用,数据是在es中,kibana上的操作不会存在损坏数据的问题,但是有延迟等缺点。

    6.3.1:es采用自主选择主节点方式

    A:只采用一个kibana情况

    这个可以使用一个kibana对应2个es。前提是2个es必须都启动起来

    但是出问题后坏掉。

    B:采用2个kibana来读取es

    每个kibana都对应一个es。配置好就可以了。

    这个情况下,数据都不会被丢失,

        实际查看的时候,如果2个es都是好的,那随便打开一个就可以。

    如果一个坏了,那就只能打开es状态好的。

    6.3.2 es采用单主节点模式

      很简单,就是用一个kibana只连接到es的主节点上。

  • 相关阅读:
    【数据挖掘导论】——绪论
    Debian Customer PPA RFC (by quqi99)
    uva 11248 Frequency Hopping (最大流)
    非常easy的JAVA反射教程
    【Spark】RDD操作具体解释4——Action算子
    NHibernate剖析:Mapping篇之Mapping-By-Code(1):概览
    eclipse中文凝视字体太小解决方法
    cocos2d-x-3.x bringToFront &amp; sendToBack实现
    POJ 1018 Communication System 题解
    监听器和 利 用观察者设计模式设计一个程序
  • 原文地址:https://www.cnblogs.com/zbhdsfdx-2015/p/8631064.html
Copyright © 2020-2023  润新知