Flume速览
Flume是一个分布式的、可靠的、高可用的海量日志采集、聚合和传输的系统。Java实现,插件丰富,模块分明。
数据流模型:Source-Channel-Sink
事务机制保证了消息传递的可靠性
一、基本组件
Event:消息的基本单位,有header和body组成。header是键值对的形式,body是字节数组,存储具体数据
Agent:JVM进程,负责将一端外部来源产生的消息转发到另一端外部的目的地
- Source:从外部来源读取event,并写入channel
- Channel:event暂存组件,source写入后,event将会一直保存,直到被sink成功消费
- Sink:从channel读入event,并写入目的地
二、Flume常见数据流
简单数据流
复杂数据流1
复杂数据流 2
三、完整的传输处理流程
一个event在一个agent中的传输处理流程如下:
source--interceptor--selector-->channel-->sink processor-->sink中心存储/下一级agent
除了source、channel和sink三个基本组件外,还有可选组件Intercptor、Selector和SinkProcessor
source从数据源中获取数据后封装为event,event包含header头信息和body
source收集到event需要发送到channel里临时存储
在发送到channel之前,如果配置了拦截器和选择器,
首先经过拦截器,拦截器可以处理event中的header信息,可以在header中添加消息处理的时间戳,还可以添加主机名称,一些静态值等等。也可以根据正则表达式对event进行过滤,决定哪些event可以继续向后进行传输,哪些删除终止。可以配置多个拦截器,多个拦截器顺序执行。
然后传给选择器,可以决定用哪种方式把event写入到channel当中
event存入sink之后,可以用SinkProcessor,根据配置可以选择故障转移处理器和负载均衡处理器,sink把event发送到中心存储或后续的agent
四、Source组件
对接各种外部数据源,将收集到的event发送到Channel中,一个source可以向多个channel发送event,Flume内置非常丰富的Source,同时用户可以自定义Source
1.Avro Source
支持Avro协议,接收RPC事件请求。Avro Source通过监听Avro端口接收外部Avro客户端流事件(event),在Flume的多层架构中经常被使用接收上游Avro Sink发送的event
关键参数说明:
- type:类型名称avro
- bind:绑定的IP
- port:监听的端口
- threads:接收请求的线程数,当需要接收多个avro客户端的数据流时要设置合适的线程数,否则会造成avro客户端数据流积压。
- compression-type:是否使用压缩,如果使用压缩则设置为”deflate“,avro source一般用于多个Agent组成的数据流,接收来自avro sink的event,如果avro source设置了压缩,那么上一阶段的avro sink也要设置压缩,默认值none
- channels:Source对接的Channel名称
2.Exec Source
支持Linux命令,收集标准输出数据或者通过tail -f file的方式监听指定文件
可以实现实时的消息传递,但是它并不记录已经读取文件的位置,不支持断电续传,当Exec Source重启或者挂掉都会造成后续增加的消息丢失,一般在测试环境使用
关键参数:
- type:source类型为exec
- command:Linux命令
- channels:Source对接的Channel名称
在flume的conf目录中,建立source目录
创建avrosource.conf
avroagent.sources = r1
avroagent.channels = c1
avroagent.sinks = k1
avroagent.sources.r1.type = avro
avroagent.sources.r1.bind = 192.168.99.151
avroagent.sources.r1.port = 8888
avroagent.sources.r1.threads= 3
avroagent.sources.r1.channels = c1
avroagent.channels.c1.type = memory
avroagent.channels.c1.capacity = 10000
avroagent.channels.c1.transactionCapacity = 1000
avroagent.sinks.k1.type = logger
avroagent.sinks.k1.channel = c1
创建execsource.conf
execagent.sources = r1
execagent.channels = c1
execagent.sinks = k1
execagent.sources.r1.type = exec
execagent.sources.r1.command = tail -F /bigdata/log/exec/exectest.log
execagent.sources.r1.channels = c1
execagent.channels.c1.type = memory
execagent.channels.c1.capacity = 10000
execagent.channels.c1.transactionCapacity = 1000
execagent.sinks.k1.type = avro
execagent.sinks.k1.channel = c1
execagent.sinks.k1.hostname = 192.168.99.151
execagent.sinks.k1.port = 8888
## 1.exec和avro source演示
#两个agent组成的数据采集流程
execagent -> avroagent 启动一个avrosource的agent
bin/flume-ng agent --conf conf --conf-file conf/source/avrosource.conf --name avroagent -Dflume.root.logger=INFO,console 启动一个execsource的agent,向avroagent以rpc的方式发送收集的数据
bin/flume-ng agent --conf conf --conf-file conf/source/execsource.conf --name execagent 监听/bigdata/log/exec/exectest.log文件
3.Spooling Directory Source
监听一个文件夹,收集文件夹下文件数据,收集完文件数据会将文件名称的后缀改为.COMPLETED
缺点不支持已存在文件新增数据的收集
关键参数:
- spoolDtype:source类型为spooldir
- spooDir:source监听的文件夹
- fileHeader:是否添加文件的绝对路径到event的header中,默认值false
- fileHeaderKey:添加到event header中文件绝对路径的键值,默认值false
- fileSuffix:收集完新的文件数据结合文件添加的后缀名称,默认值:.COMPLETED
- channels:Source对接的Channel名称
4.Taildir Source
监听一个文件夹或者文件,通过正则表达式匹配需要监听的数据源文件,Taildir Source通过将监听的文件位置写入到文件中来实现断点续传,并且能够保证没有重复数据的读取
关键参数:
type:source类型TAILDIR
positionFile:保存监听文件读取位置的文件(文件名和已经采集的位置)路径
idleTimeout:关闭空闲文件延迟时间,如果有新的记录添加到已关闭的空闲文件taildir source,将继续打开该空闲文件,默认值120000毫秒(2分钟)
writePosInterval:向保存数据位置文件中写入读取文件位置的时间间隔,默认值3000毫秒
batchSize:批量写入channel最大event数,默认值100
maxBackoffSleep:每次最后一次尝试没有获取到监听文件最新数据的最大延迟时间,默认值5000毫秒
cachePatternMatching:对应监听的文件夹下,通过正则表达式匹配的文件可能数量会很多,将匹配成功的监听文件列表、读取文件的顺序都提那就到缓存中,可以提高性能,默认值true
fileHeader:是否添加文件的绝对路径到event的header中,默认值false
flleHeaderKey:添加到vent header中文件绝对路径的键值,默认值是false
filegroups:监听的文件组列表,taildirsource通过文件组监听多个目录或文件
filegroups.<filegroupName>:使用正则表达式标识监听文件的路径
channels:Source对接的Channel名称
创建taildirsource.conf
taildiragent.sources = r1
taildiragent.channels = c1
taildiragent.sinks = k1
taildiragent.sources.r1.type = TAILDIR
taildiragent.sources.r1.positionFile = /bigdata/flume/taildir/position/taildir_position.json
taildiragent.sources.r1.filegroups = f1 f2
taildiragent.sources.r1.filegroups.f1 = /bigdata/taildir_log/test1/test.log
taildiragent.sources.r1.filegroups.f2 = /bigdata/taildir_log/test2/.*\\.log
taildiragent.sources.r1.channels = c1
taildiragent.channels.c1.type = memory
taildiragent.channels.c1.capacity = 10000
taildiragent.channels.c1.transactionCapacity = 1000
taildiragent.sinks.k1.type = logger
taildiragent.sinks.k1.channel = c1
运行
## 2.taildir source演示 bin/flume-ng agent --conf conf --conf-file conf/source/taildirsource.conf --name taildiragent -Dflume.root.logger=INFO,console
5.Kafka Source
对接分布式消息队列Kafka,作为Kafka的消费者持续从Kafka中拉取数据,如果多个Kafka source同时消费Kafka中同一个主题(topic),则Kafka source的Kafka.consumer.group.id应该设置成相同的组id,多个Kafka source之不会消费重复的数据,每一个source都会拉取topic下的不同数据
关键参数:
type:类型设置为kafkasource的类路径,org.apache.flume.source.kafka.KafkaSource
channels:Source对接的Channel名称
kafka.bootstrap.servers:Kafka broker列表,格式为ip1:port1,ip2:port2...,建议配置多个值提高容错能力,多个值之间用逗号隔开
kafka.topics:消费的topic名称
kafka.consumer.group.id:Kafka source所属组id,默认值flume
batchSize:批量写入channel的最大消息数,默认值1000
batchDurationMillis:等待批量写入channel的最长时间,这个参数和batchSize两个参数只有一个先满足都会触发批量写入channel操作,默认值1000毫秒
创建 kafkasource.conf
kafkasourceagent.sources = r1
kafkasourceagent.channels = c1
kafkasourceagent.sinks = k1
kafkasourceagent.sources.r1.type = org.apache.flume.source.kafka.KafkaSource
kafkasourceagent.sources.r1.channels = c1
kafkasourceagent.sources.r1.kafka.bootstrap.servers = 192.168.99.151:9092,192.168.99.152:9092,192.168.99.153:9092
kafkasourceagent.sources.r1.kafka.topics = flumetopictest1
kafkasourceagent.sources.r1.kafka.consumer.group.id = flumecg
kafkasourceagent.channels.c1.type = memory
kafkasourceagent.sinks.k1.type = logger
kafkasourceagent.sinks.k1.channel = c1
运行
## 3.kafka source演示
在kafka中创建主题
bin/kafka-topics.sh --create --zookeeper 192.168.99.151:2181 --replication-factor 1 --partitions 3 --topic flumetopictest
查看主题
bin/kafka-topics.sh --list --zookeeper 192.168.99.151:2181
向flumetopictest1发送消息
bin/kafka-console-producer.sh --broker-list 192.168.99.151:9092,192.168.99.152:9092,192.168.99.153:9092 --topic flumetopictest #启动flume agent
bin/flume-ng agent --conf conf --conf-file conf/source/kafkasource.conf --name kafkasourceagent -Dflume.root.logger=INFO,console
五、Channel组件
channel被设计为event中转暂存区,存储Source收集并且没有被Sink消费的event,为了平衡source收集和sink读取数据的速度,可视为Flume内部的消息队列
channel是线程安全的并且具有事务性,支持source写失败重复写和sink读失败重复读等操作
常用的Channel类型:
Memory Channel
File Channel
Kafka Channel等
1.Memory Channel
使用内存作为Channel,Memory Channel读写速度快,但是存储数据量小,Flume进程挂掉、服务器停机或者重启都会导致数据丢失。部属Flume Agent的线上服务器内存资源充足、不关心数据丢失的场景下可以使用
参数说明:
2.File Channel
将event写入到磁盘文件中,与Memory Channel相比存储容量大,无数据丢失风险
Flume将Event顺序写入到File Channel文件的末尾,在配置文件中通过设置maxFileSize参数设置数据文件大小上限
当一个已关闭的只读数据文件中的Event被完全读取完成,并且Sink已经提交读取完成的事务,则Flume将删除存储该数据文件
参数说明:
创建channel目录,创建filechannel.conf
a1.sources = r1
a1.channels = c1
a1.sinks = k1
a1.sources.r1.type = netcat
a1.sources.r1.bind = localhost
a1.sources.r1.port = 44444
a1.sources.r1.channels = c1
a1.channels.c1.type = file
a1.channels.c1.dataDirs = /bigdata/flume/filechannel/data
a1.channels.c1.checkpointDir = /bigdata/flume/filechannel/checkpoint
a1.channels.c1.useDualCheckpoints = true
a1.channels.c1.backupCheckpointDir = /bigdata/flume/filechannel/backup
a1.sinks.k1.type = logger
a1.sinks.k1.channel = c1
运行
## 1.file channel演示
在/bigdata/flume/filechannel目录手动创建backup、checkpoint、data文件夹 bin/flume-ng agent --conf conf --conf-file conf/channel/filechannle.conf --name a1 -Dflume.root.logger=INFO,console 使用telnet发送数据
telnet localhost 44444
3.Kafka Channel
将分布式消息队列Kafka作为channel
相比于Memory Channel和File Channel存储容量更大、容错能力更强,弥补了其他两种Channel的短板,如果合理利用Kafka的性能,能够达到事半功倍的效果
参数说明:
创建kafkachannel.conf
a1.sources = r1
a1.channels = c1
a1.sinks = k1
a1.sources.r1.type = netcat
a1.sources.r1.bind = localhost
a1.sources.r1.port = 44444
a1.sources.r1.channels = c1
a1.channels.c1.type = org.apache.flume.channel.kafka.KafkaChannel
a1.channels.c1.kafka.bootstrap.servers = 192.168.99.151:9092,192.168.99.152:9092,192.168.99.153:9092
a1.channels.c1.kafka.topic = flumechannel1
a1.channels.c1.kafka.consumer.group.id = flumecg1
a1.sinks.k1.type = logger
a1.sinks.k1.channel = c1
运行
## 2.kafka channel演示 bin/kafka-topics.sh --create --zookeeper 192.168.99.151:2181 --replication-factor 1 --partitions 3 --topic kafkachannel
查看主题
bin/kafka-topics.sh --list --zookeeper 192.168.99.151:2181 bin/flume-ng agent --conf conf --conf-file conf/channel/kafkachannel.conf --name a1 -Dflume.root.logger=INFO,console 使用telnet发送数据
telnet localhost 44444
六、Interceptor拦截器
Source经event写入到Channel之前调用拦截器
Source和Channel之间可以有多个拦截器,不同的拦截器使用不同的规则处理Event
可选、轻量级、可插拔的插件
通过实现Interceptor接口实现自定义的拦截器
内置拦截器:Timestamp Interceptor、Host Interceptor、UUID Interceptor、Static Interceptor、Regex Filtering Interceptor等
1.Timestmap Interceptor
Flume使用时间戳拦截器在event头信息中添加时间戳信息,Key为timestamp,Value为拦截器拦截Event时的时间戳
头信息时间戳的作用,比如HDFS存储的数据采用时间分区存储,Sink可以根据Event头信息中的时间戳将Event按照时间分区写入到HDFS
参数说明:
2.Host Intercepor
Flume使用主机拦截器在Event头信息中添加主机名称或者IP
主机拦截器的作用:比如Source将Event按照主机名称写入到不同的Channel中便于后续的Sink对不同Channel中的数据分开处理
参数说明:
3.Static Intercepor
Flume使用static interceptor静态拦截器在event头信息添加静态信息
参数说明:
七、Sink组件
从Channel消费event,输出到外部存储,或者输出到下一个阶段的agent
一个Sink只能从一个Channel中消费event
当Sink写出event成功后,会向Channel提交事务。Sink事务提交成功,处理完成的event将会被Channel删除。否则Channel会等待Sink重新消费处理失败的event
Flume提供了丰富的Sink组件,如Avro Sink、HDFS Sink、Kafka Sink、File Roll Sink、HTTP Sink等
1.Avro Sink
常用于对接下一层的Avro Source,通过发送RPC请求将Event发送到下一层的Avro Source
为了减少Event传输占用大量的网络资源,Avro Sink提供了端到端的批量压缩数据传输
参数说明:
2.HDFS Sink
将Event写入到HDFS中持久化存储
提供了强大的时间戳转义功能,根据Event头信息中的timestamp时间戳信息转义成日期格式,在HDFS中以日期目录分层存储
参数说明:
创建HDFS目录,创建hdfssink.conf
a1.sources = r1
a1.channels = c1
a1.sinks = k1
a1.sources.r1.type = netcat
a1.sources.r1.bind = localhost
a1.sources.r1.port = 44444
a1.sources.r1.interceptors = i1
a1.sources.r1.interceptors.i1.type = timestamp
a1.sources.r1.channels = c1
a1.channels.c1.type = memory
a1.sinks.k1.type = hdfs
a1.sinks.k1.channel = c1
a1.sinks.k1.hdfs.path = /data/flume/%Y%m%d/%H%M
a1.sinks.k1.hdfs.filePrefix = hdfssink-
a1.sinks.k1.hdfs.fileSuffix = .log
a1.sinks.k1.hdfs.fileType = DataStream
a1.sinks.k1.hdfs.writeFormat = Text
a1.sinks.k1.hdfs.round = true
a1.sinks.k1.hdfs.roundValue = 2
a1.sinks.k1.hdfs.roundUnit = minute
a1.sinks.k1.hdfs.rollInterval = 30
a1.sinks.k1.hdfs.rollSize = 0
a1.sinks.k1.hdfs.rollCount = 0
运行
## 1.hdfs sink演示 bin/flume-ng agent --conf conf --conf-file conf/sink/hdfssink.conf --name a1 -Dflume.root.logger=INFO,console 使用telnet发送数据
telnet localhost 44444
3.Kafka Sink
Flume通过KafkaSink将Event写入到Kafka指定的主题中
参数说明
创建kafkasink.conf
a1.sources = r1
a1.channels = c1
a1.sinks = k1
a1.sources.r1.type = netcat
a1.sources.r1.bind = localhost
a1.sources.r1.port = 44444
a1.sources.r1.channels = c1
a1.channels.c1.type = memory
a1.sinks.k1.type = org.apache.flume.sink.kafka.KafkaSink
a1.sinks.k1.channel = c1
a1.sinks.k1.kafka.topic = kafkasink
a1.sinks.k1.kafka.bootstrap.servers = 192.168.99.151:9092,192.168.99.152:9092,192.168.99.153:9092
a1.sinks.k1.kafka.flumeBatchSize = 100
a1.sinks.k1.kafka.producer.acks = 1
运行
## 2.kafka sink演示
创建主题FlumeKafkaSinkTopic1
bin/kafka-topics.sh --create --zookeeper 192.168.99.151:2181 --replication-factor 1 --partitions 3 --topic kafkasink
查看主题
bin/kafka-topics.sh --list --zookeeper 192.168.99.151:2181 bin/flume-ng agent --conf conf --conf-file conf/sink/kafkasink.conf --name a1 >/dev/null 2>&1 & bin/kafka-console-consumer.sh --zookeeper 192.168.99.151:2181 --from-beginning --topic kafkasink 使用telnet发送数据
telnet localhost 44444
八、Selector选择器
Source将event写入到Channel之前调用拦截器,如果配置了Interceptor拦截器,则Selector在拦截器全部处理完之后调用。通过selector决定event写入Channel的方式
内置Replicating Channel Selector复制Channel选择器、Multiplexing Channel Selector复用Channel选择器
1.Replicating Channel Selector
如果Channel选择器没有指定,默认值。即一个Source以复制的方式将一个event同时写入到多个Channel中,不同的Sink可以从不同的Channel中获取相同的event
参数说明:
创建replicating_selector目录,创建replicating_selector.conf
a1.sources = r1
a1.channels = c1 c2
a1.sinks = k1 k2
#定义source
a1.sources.r1.type = netcat
a1.sources.r1.bind = localhost
a1.sources.r1.port = 44444
#设置复制选择器
a1.sources.r1.selector.type = replicating
#设置required channel
a1.sources.r1.channels = c1 c2
#设置channel c1
a1.channels.c1.type = memory
#设置channel c2
a1.channels.c2.type = memory
#设置kafka sink
a1.sinks.k1.channel = c1
a1.sinks.k1.type = org.apache.flume.sink.kafka.KafkaSink
a1.sinks.k1.kafka.topic = flumeselector
a1.sinks.k1.kafka.bootstrap.servers = 192.168.99.151:9092,192.168.99.152:9092
a1.sinks.k1.kafka.flumeBatchSize = 10
a1.sinks.k1.kafka.producer.acks = 1
#设置file sink
a1.sinks.k2.channel = c2
a1.sinks.k2.type = file_roll
a1.sinks.k2.sink.directory = /bigdata/flume/selector
a1.sinks.k2.sink.rollInterval = 60
运行
## 1.replicating selector演示 一个source将一个event拷贝到多个channel,通过不同的sink消费不同的channel,将相同的event输出到不同的地方
配置文件:replicating_selector.conf
分别写入到kafka和文件中 创建主题flumeselector
bin/kafka-topics.sh --create --zookeeper 192.168.99.151:2181 --replication-factor 1 --partitions 3 --topic flumeselector 启动flume agent
bin/flume-ng agent --conf conf --conf-file conf/replicating_selector/replicating_selector.conf --name a1 查看kafka FlumeSelectorTopic1主题数据
bin/kafka-console-consumer.sh --zookeeper 192.168.99.151:2181 --from-beginning --topic flumeselector 使用telnet发送数据
telnet localhost 44444 查看/bigdata/flume/selector路径下的数据
2.Multiplexing Channle Selector
多路复用选择器根据evetn的头信息中不同键值数据来判断Event应该被写入到哪个Channel中。
参数说明:
创建multiplexing_selector目录
创建avro_sink1.conf
agent1.sources = r1
agent1.channels = c1
agent1.sinks = k1
agent1.sources.r1.type = netcat
agent1.sources.r1.bind = localhost
agent1.sources.r1.port = 44444
agent1.sources.r1.interceptors = i1 // 拦截器
agent1.sources.r1.interceptors.i1.type = static // 动态
agent1.sources.r1.interceptors.i1.key = logtype // event头信息中添加静态信息,key是logtype
agent1.sources.r1.interceptors.i1.value = ad // 静态信息的value是ad
agent1.sources.r1.channels = c1
agent1.channels.c1.type = memory
agent1.sinks.k1.type = avro
agent1.sinks.k1.channel = c1
agent1.sinks.k1.hostname = 192.168.99.151 // 发送目标avrosource的IP
agent1.sinks.k1.port = 8888
创建avro_sink2.conf
agent2.sources = r1
agent2.channels = c1
agent2.sinks = k1
agent2.sources.r1.type = netcat
agent2.sources.r1.bind = localhost
agent2.sources.r1.port = 44445
agent2.sources.r1.interceptors = i1
agent2.sources.r1.interceptors.i1.type = static
agent2.sources.r1.interceptors.i1.key = logtype
agent2.sources.r1.interceptors.i1.value = search
agent2.sources.r1.channels = c1
agent2.channels.c1.type = memory
agent2.sinks.k1.type = avro
agent2.sinks.k1.channel = c1
agent2.sinks.k1.hostname = 192.168.99.151
agent2.sinks.k1.port = 8888
创建avro_sink3.conf
agent3.sources = r1
agent3.channels = c1
agent3.sinks = k1
agent3.sources.r1.type = netcat
agent3.sources.r1.bind = localhost
agent3.sources.r1.port = 44446
agent3.sources.r1.interceptors = i1
agent3.sources.r1.interceptors.i1.type = static
agent3.sources.r1.interceptors.i1.key = logtype
agent3.sources.r1.interceptors.i1.value = other
agent3.sources.r1.channels = c1
agent3.channels.c1.type = memory
agent3.sinks.k1.type = avro
agent3.sinks.k1.channel = c1
agent3.sinks.k1.hostname = 192.168.99.151
agent3.sinks.k1.port = 8888
创建multiplexing_selector.conf
a3.sources = r1
a3.channels = c1 c2 c3
a3.sinks = k1 k2 k3
a3.sources.r1.type = avro
a3.sources.r1.bind = 192.168.99.151
a3.sources.r1.port = 8888
a3.sources.r1.threads= 3
#设置multiplexing selector
a3.sources.r1.selector.type = multiplexing
a3.sources.r1.selector.header = logtype // 设置header为logtype,在发送的event的头信息里,应该有logtype对应的键,选择器会从event头信息里找到logtype的键对应的值
#通过header中logtype键对应的值来选择不同的sink
a3.sources.r1.selector.mapping.ad = c1 // 当logtype对应的值是ad时,放在c1中
a3.sources.r1.selector.mapping.search = c2 // 当logtype是search的时候,放在c2中
a3.sources.r1.selector.default = c3
a3.sources.r1.channels = c1 c2 c3
a3.channels.c1.type = memory
a3.channels.c2.type = memory
a3.channels.c3.type = memory
#分别设置三个sink的不同输出
a3.sinks.k1.type = file_roll // 磁盘里滚动生成文件
a3.sinks.k1.channel = c1
a3.sinks.k1.sink.directory = /bigdata/flume/multiplexing/k1
a3.sinks.k1.sink.rollInterval = 60 // 滚动生成文件的时间是60秒
a3.sinks.k2.channel = c2
a3.sinks.k2.type = file_roll
a3.sinks.k2.sink.directory = /bigdata/flume/multiplexing/k2
a3.sinks.k2.sink.rollInterval = 60
a3.sinks.k3.channel = c3
a3.sinks.k3.type = file_roll
a3.sinks.k3.sink.directory = /bigdata/flume/multiplexing/k3
a3.sinks.k3.sink.rollInterval = 60
运行
## 2.multiplexing selector演示 配置文件multiplexing_selector.conf、avro_sink1.conf、avro_sink2.conf、avro_sink3.conf
向不同的avro_sink对应的配置文件的agent发送数据,不同的avro_sink配置文件通过static interceptor在event头信息中写入不同的静态数据
multiplexing_selector根据event头信息中不同的静态数据类型分别发送到不同的目的地 在/bigdata/flume/multiplexing目录下分别创建看k1 k2 k3目录 bin/flume-ng agent --conf conf --conf-file conf/multiplexing_selector/multiplexing_selector.conf --name a3 -Dflume.root.logger=INFO,console bin/flume-ng agent --conf conf --conf-file conf/multiplexing_selector/avro_sink1.conf --name agent1 >/dev/null 2>&1 &
bin/flume-ng agent --conf conf --conf-file conf/multiplexing_selector/avro_sink2.conf --name agent2 >/dev/null 2>&1 &
bin/flume-ng agent --conf conf --conf-file conf/multiplexing_selector/avro_sink3.conf --name agent3 >/dev/null 2>&1 & 使用telnet发送数据
telnet localhost 44444
telnet localhost 44445
telnet localhost 44446
telnet 是source --> 根据不同端口发送给三个agent1、2、3 -->发送给a3
九、Sink Processor
协调多个sink间进行load balance和failover
1.Load-Balance Sink Processor
负载均衡处理器
参数说明:
2.Failover Sink Processor
容错处理器,可定义一个sink优先级列表,根据优先级选择使用的sink
参数说明:
创建failover目录
创建collector1.conf
collector1.sources = r1
collector1.channels = c1
collector1.sinks = k1
collector1.sources.r1.type = avro
collector1.sources.r1.bind = 192.168.99.151
collector1.sources.r1.port = 8888
collector1.sources.r1.threads= 3
collector1.sources.r1.channels = c1
collector1.channels.c1.type = memory
collector1.sinks.k1.type = logger
collector1.sinks.k1.channel = c1
创建collector2.conf
collector2.sources = r1
collector2.channels = c1
collector2.sinks = k1
collector2.sources.r1.type = avro
collector2.sources.r1.bind = 192.168.99.151
collector2.sources.r1.port = 8889
collector2.sources.r1.threads= 3
collector2.sources.r1.channels = c1
collector2.channels.c1.type = memory
collector2.sinks.k1.type = logger
collector2.sinks.k1.channel = c1
创建failover.conf
agent1.sources = r1
agent1.channels = c1
agent1.sinks = k1 k2
agent1.sources.r1.type = netcat
agent1.sources.r1.bind = localhost
agent1.sources.r1.port = 44444
agent1.sources.r1.channels = c1
agent1.channels.c1.type = memory
agent1.sinkgroups = g1
agent1.sinkgroups.g1.sinks = k1 k2
agent1.sinkgroups.g1.processor.type = failover
agent1.sinkgroups.g1.processor.priority.k1 = 10
agent1.sinkgroups.g1.processor.priority.k2 = 5
agent1.sinks.k1.type = avro
agent1.sinks.k1.channel = c1
agent1.sinks.k1.hostname = 192.168.99.151
agent1.sinks.k1.port = 8888
agent1.sinks.k2.type = avro
agent1.sinks.k2.channel = c1
agent1.sinks.k2.hostname = 192.168.99.151
agent1.sinks.k2.port = 8889
运行
## 1.failover processor演示
分别启动两个collector agent
bin/flume-ng agent --conf conf --conf-file conf/failover/collector1.conf --name collector1 -Dflume.root.logger=INFO,console bin/flume-ng agent --conf conf --conf-file conf/failover/collector2.conf --name collector2 -Dflume.root.logger=INFO,console agent1通过failover方式,配置sink优先级列表,k1发送到collector1,k2发送到collector2。k1的优先级高于k2,所以在启动的时候
source将event发送到collector1,杀掉collector1,agent1会切换发送到collector2,实现高可用
bin/flume-ng agent --conf conf --conf-file conf/failover/failover.conf --name agent1 -Dflume.root.logger=INFO,console
agent1连接到collector1和collector2,collector1优先级高,先接收消息,当1崩了,collector2才接收消息,恢复collector1,会重新接收消息
3.Failover应用场景
分布式日志收集场景
任何一个collector挂掉,会转发到另一个collector上
十、数据收集系统实现
单层日志收集架构
分层日志收集架构
案例flume+kafka
Producer
kafka0.10.2版本官方文档地址:http://kafka.apache.org/0102/documentation.html
1.在Maven项目的pom.xml文件中添加kafka客户端相关依赖,本项目使用的是kafka0.10版本的API
<properties>
<kafka-version>0.10.2.0</kafka-version>
</properties>
<dependencies>
<dependency>
<groupId>org.apache.kafka</groupId>
<artifactId>kafka-clients</artifactId>
<version>${kafka-version}</version>
</dependency>
</dependencies>
2.Producer生产逻辑
1)配置Producer相关参数
2)创建Producer实例
3)构建待发送的消息
4)发送消息
5)关闭Producer实例
3.Producer相关参数说明
Producer相关配置文档:http://kafka.apache.org/0102/documentation.html#producerconfigs
bootstrap.servers(必选项):通过该参数设置producer连接kafka集群所需的broker地址列表。
格式:host1:port1,host2:post2,...可以设置一个或者多个地址,中间用逗号隔开,该参数默认值是""。
注意:该参数值建议设置两个及以上broker地址,这样可以保障任意一个broker宕机不可用,producer还可以连接到kafka。可以不设置全部的broker地址,因为producer可以从给定的broker里查找其它broker信息。
key.serializer和value.serializer(必选项):这两个参数用于设置key和value的序列化器,producer需要通过序列化器将key和value序列化成字节数组通过网络发送到kafka。
kafka客户端内置的序列化器:String / Double / Long / Integer / ByteArray / ByteBuffer / Bytes
注意:在配置序列化器的时候,要写序列化器的全路径,如:org.apache.kafka.common.serialization.StringSerializer
client.id(可选项):用户设置客户端id,如果不设置,kafka客户端会自动生成一个非空字符串作为客户端id,自动生成的客户端id形式“producer-1”
acks(可选项):默认是值是1,可选值范围[all, -1, 0, 1]。用于设置Producer发送消息到Borker是否等待接收Broker返回成功送达信号。
0:表示Producer发送消息到Broker之后不需要等待Broker返回成功送达的信号,这种方式吞吐量高,但是存在数据丢失的风险,“retries”参数配置的发送消息失败重试次数将失效。
1:表示Broker接收到消息成功写入本地log文件后,向Producer返回成功接收的信号,不需要等待所有的Follower全部同步完消息后再做回应,这种方式在数据丢失风险和吞吐量之间做了平衡。
-1(或者all):表示Broker接收到Producer的消息成功写入本地log并且等待所有的Follower成功写入本地log后,向Producer返回成功接收的信号,这种方式能够保证消息不丢失,但是性能最差。
retries:Producer发送消息失败后的重试次数。默认值是0,即发送消息出现异常不做任何重试。
retry.backoff.ms:用来设置两次重试之间的时间间隔,默认值是100ms。
4.ProducerConfig工具类
注意:在平时的开发过程中,由于kafka的参数非常多,为了防止由于人为书写错误,导致参数名出错,kafka客户端提供了ProducerConfig工具类,在ProducerConfig工具类中定义个kafka相关的各种参数。
ProducerConfig的定义如下:
5.kafka客户端代码请参考本课时资料中的ProducerClient.java文件
在ProducerClient类中定义了4个客户端发送消息的方法,每个方法实现了一种发送消息的方法,可以参考ProducerClient类的具体实现,学习kafka Producer Java API的使用。
package bigdata.kafka; import org.apache.kafka.clients.producer.*;
import org.apache.kafka.common.serialization.StringSerializer; import java.util.Properties;
import java.util.concurrent.ExecutionException; public class ProducerClient {
//broker列表
public static final String brokers = "192.168.196.130:9092, 192.168.196.131:9092, 192.168.196.132:9092";
//producer要发送的主题名称
public static final String topic = "topic_producer_client1"; /**
* 初始化配置kafka producer客户端相关参数
* @return
* Properties对象
*/
public static Properties initConfig(){
Properties props = new Properties();
//设置bootstrap.servers参数值,producer客户端连接的borker地址列表
props.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, brokers); //防止参数的键和值由于人为原因写错,可以使用kafka客户端内置的工具类进行优化 //设置key和value使用String类型的序列化器 //props.put("key.serializer", "org.apache.kafka.common.serialization.StringSerializer");
//下一行代码是等价设置
props.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, StringSerializer.class.getName()); //props.put("value.serializer", "org.apache.kafka.common.serialization.StringSerializer");
//下一行代码是等价设置
props.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, StringSerializer.class.getName()); //(可选项)设置客户端等待接收Broker返回成功送达信号的方式
//props.put("acks","1");
//下一行代码是等价设置
props.put(ProducerConfig.ACKS_CONFIG,"1"); //(可选项)设置客户端发送消息失败后的重试次数
props.put(ProducerConfig.RETRIES_CONFIG,"3"); //(可选项)设置producer客户端id
props.put(ProducerConfig.CLIENT_ID_CONFIG,"producer.client.id.test1"); return props;
} /**
* key为空的情况,producer客户端向kafka指定主题发送value
* 客户端发送消息方式:异步,无回调函数
* @param topic :主题名称
* @param value :发送的value值
*/
public void sendMessage(String topic,String value){
//1)配置Producer相关参数
Properties props = initConfig();
//2)创建Producer实例
KafkaProducer<String, String> producer = new KafkaProducer<String,String>(props);
//3)构建待发送的消息ProducerRecord,ProducerRecord<String, String>泛型中的两个String类型指的是Key和Value的类型
ProducerRecord record = new ProducerRecord<String, String>(topic,value);
//4)发送消息
producer.send(record);//send方法默认是异步发送消息
//5)关闭Producer实例
producer.close();
} /**
* key为空的情况,producer客户端向kafka指定主题发送value
* 客户端发送消息方式:异步,有回调函数
* @param topic :主题名称
* @param value :发送的value值
*/
public void sendMessageCallback(String topic,String value){
//1)配置Producer相关参数
Properties props = initConfig();
//2)创建Producer实例
KafkaProducer<String, String> producer = new KafkaProducer<String,String>(props);
//3)构建待发送的消息ProducerRecord,ProducerRecord<String, String>泛型中的两个String类型指的是Key和Value的类型
ProducerRecord record = new ProducerRecord<String, String>(topic,value);
//4)发送消息
//异步发送消息,通过自定义回调函数处理消息发送后等待服务端响应
producer.send(record, new Callback() {
@Override
public void onCompletion(RecordMetadata recordMetadata, Exception e) {
if(e != null){
//如果发送消息发生异常,打印出异常信息
e.printStackTrace();
}else{
//打印发送成功的消息的元数据信息
System.out.println("topic:" + recordMetadata.topic());
System.out.println("partition:" + recordMetadata.partition());//消息被发送到的分区号
System.out.println("offset:" + recordMetadata.offset());//消息在分区中的偏移量
}
}
}); //5)关闭Producer实例
producer.close();
} /**
* key不为空的情况,producer客户端向kafka指定主题发送value
* 客户端发送消息方式:异步
* @param topic :主题名称
* @param key :发送的key值
* @param value :发送的value值
*/
public void sendMessage(String topic,String key,String value){
Properties props = initConfig();
KafkaProducer<String, String> producer = new KafkaProducer<String,String>(props);
ProducerRecord record = new ProducerRecord<String, String>(topic,key,value);
producer.send(record);
producer.close();
} /**
* key不为空的情况,producer客户端向kafka指定主题发送value
* 客户端发送消息方式:同步
* @param topic :主题名称
* @param key :发送的key值
* @param value :发送的value值
*/
public void sendMessageSync(String topic,String key,String value){
Properties props = initConfig();
KafkaProducer<String, String> producer = new KafkaProducer<String,String>(props);
ProducerRecord record = new ProducerRecord<String, String>(topic,key,value);
try {
//在调用完send方法后链式地调用get方法,会同步阻塞,等待消息发送成功或者发生异常
producer.send(record).get();
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
}
producer.close();
}
public static void main(String[] args) {
ProducerClient producer = new ProducerClient(); //调用不需要传key的发送消息的方法
producer.sendMessage(topic,"hello kafka 1"); //异步发送消息,没有设置回调函数
producer.sendMessageCallback(topic,"hello kafka callback1"); //异步发送消息,并设置回调函数 //调用传入自定义key值的发送消息的方法
producer.sendMessage(topic,"key2","hello kafka 2");//异步发送消息
producer.sendMessageSync(topic,"key3","hello kafka sync 2");//同步发送消息
}
}
Consumer
package bigdata.kafka; import org.apache.kafka.clients.consumer.ConsumerConfig;
import org.apache.kafka.clients.consumer.ConsumerRecord;
import org.apache.kafka.clients.consumer.ConsumerRecords;
import org.apache.kafka.clients.consumer.KafkaConsumer;
import org.apache.kafka.clients.producer.ProducerConfig;
import org.apache.kafka.common.TopicPartition;
import org.apache.kafka.common.serialization.StringDeserializer; import java.util.Arrays;
import java.util.Properties;
import java.util.concurrent.atomic.AtomicBoolean; public class ConsumerClient {
public static final String brokers = "192.168.196.130:9092,192.168.196.131:9092,192.168.196.132:9092";
public static final String groupId = "cg1";
public static final AtomicBoolean isRunning = new AtomicBoolean(true); /**
* 配置consumer相关参数
* @return
*/
public static Properties initConfig(){
Properties props = new Properties();
//设置bootstrap.servers参数值,consumer客户端连接的borker地址列表
props.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, brokers); //防止参数的键和值由于人为原因写错,可以使用kafka客户端内置的工具类进行优化 //设置key和value使用String类型的反序列化器 //props.put("key.deserializer", "org.apache.kafka.common.serialization.StringDeserializer");
//下一行代码是等价设置
props.put(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class.getName()); //props.put("value.deserializer", "org.apache.kafka.common.serialization.StringDeserializer");
//下一行代码是等价设置
props.put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class.getName()); //consumer组id
props.put(ConsumerConfig.GROUP_ID_CONFIG,groupId); return props;
} /**
* 从订阅的主题中拉取消息消费,自动提交offset
* @param topic 订阅的主题
*/
public void pollMessage(String topic){
//1)配置consumer相关参数
Properties props = initConfig(); //2)创建Consumer实例
KafkaConsumer<String,String> consumer = new KafkaConsumer<String, String>(props); //3)订阅主题
consumer.subscribe(Arrays.asList(topic)); //4)拉取消息并消费
//通过重复轮询的方式,consumer客户端通过poll方法从订阅的topic中拉取消息
while(isRunning.get()){
//在poll方法中需要传入一个timeout超时时间(单位毫秒),timeout超时时间用来控制poll方法的阻塞时间
ConsumerRecords<String,String> records = consumer.poll(500);
System.out.println("拉取到的消息个数:" + records.count()); for(ConsumerRecord<String,String> record : records){
System.out.println("topic:" + record.topic());
System.out.println("partition:" + record.partition());
System.out.println("offset:" + record.offset());
System.out.println("value:" + record.value());
}
} //5)提交消费的消息偏移量(offset),默认consumer客户端每隔5秒自动提交offset //6)关闭消费者实例
consumer.close();
} /**
* 按照分区处理消息,自动提交offset
* @param topic 订阅的主题
*/
public void pollMessageDealByPartition(String topic){
//1)配置consumer相关参数
Properties props = initConfig(); //2)创建Consumer实例
KafkaConsumer<String,String> consumer = new KafkaConsumer<String, String>(props); //3)订阅主题
consumer.subscribe(Arrays.asList(topic)); //4)拉取消息并消费
while(isRunning.get()){
ConsumerRecords<String,String> records = consumer.poll(500);
System.out.println("拉取到的消息个数:" + records.count());
//按照分区处理消息
for(TopicPartition partition : records.partitions()){
System.out.println(">>>>>>>>> 正在处理的partition = " + partition.partition());
//处理每个分区里的消息
for(ConsumerRecord<String,String> record : records.records(partition)){
System.out.println("topic:" + record.topic());
System.out.println("partition:" + record.partition());
System.out.println("offset:" + record.offset());
System.out.println("value:" + record.value());
}
}
} //5)提交消费的消息偏移量(offset),默认consumer客户端每隔5秒自动提交offset //6)关闭消费者实例
consumer.close();
} public static void main(String[] args) {
String topic = "topic_client1";
ConsumerClient consumer = new ConsumerClient();
// 测试1:从订阅的主题中拉取消息消费,自动提交offset
// consumer.pollMessage(topic); // 测试2:从订阅的主题中拉取消息,按照分区处理消息,自动提交offset
// consumer.pollMessageDealByPartition(topic);
}
}
Consumer客户端消费逻辑步骤
1)配置Consumer相关参数
2)创建Consumer实例
3)订阅主题
4)拉取消息并消费
5)提交已消费的消息的偏移量
6)关闭Consumer实例
Consumer相关参数说明
1)bootstrap.servers
(必选项)通过该参数设置producer连接kafka集群所需的broker地址列表。
格式:host1:port1,host2:post2,...可以设置一个或者多个地址,中间用逗号隔开,该参数默认值是""。
2)group.id
(必选项)消费者所属的消费者组id,默认值为“”。
3)key.deserializer和value.deserializer
(必选项)这两个参数用于设置key和value的反序列化器,consumer需要通过反序列化器将key和value反序列化成原对象格式。
注意在配置序列化器的时候,要写序列化器的全路径,如:org.apache.kafka.common.serialization.StringDeserializer
4)auto.offset.reset
当一个新的消费者组被建立,之前没有这个新的消费者组消费的offset记录。或者之前存储的某个Consumer已消费的offset被删除了,查询不到该Consumer已消费的offset。诸如上述的两种情况,消费者找不到已消费的offset记录时,就会根据客户端参数auto.offset.reset参数的配置来决定从哪里开始消费。
该参数默认值是“latest”,表示从分区的末尾开始消费消息。
如果设置值为“earliest”,表示从从分区的最早的一条消息开始消费。
如果设置值为“none”,表示从如果查询不到该消费者已消费的offset,就抛出异常。
Consumer客户端消费消息的方式
Kafka Consumer采用从指定topic拉取消息的方式消费数据,并不是kafka服务器主动将消息推送给Consumer。Consumer通过不断地轮询,反复调用poll方法从订阅的主题的分区中拉取消息。
poll方法定义:public ConsumerRecords<K, V> poll(long timeout)
调用poll方法时需要传入long类型的timeout超时时间,单位毫秒,返回值是拉取到的消息的集合ConsumerRecords。timeout超时时间用来控制poll方法的阻塞时间,当poll方法拉取到消息时,会直接将拉取到的数据集返回;当使用poll方法没有拉取到消息时,则poll方法阻塞等待订阅的主题中有新的消息可以消费,直到到达超时时间。
消息偏移量offset
在kafka的分区中,每条消息都有一个唯一的offset,用来表示消息在分区中的位置。消费者使用offset来表示消费到分区中的某个消息所在的位置,并且消费者要保存已消费消息的offset,消费者下次拉取新消息会从已消费的offset之后拉取。
Kafka Consumer默认是由客户端自动提交offset,在kafka老的版本中将Consumer已消费的offset保存在Zookeeper中,在Kafka0.10版本之后将已消费的消息的offset保存在kafka的__consumer_offsets中。Consumer客户端通过enable.auto.commit参数设置是否自动提交offset,默认值是true自动提交。自动提交的时间间隔由auto.commit.interval.ms参数控制,默认值是5秒。所以Consumer客户端默认情况下是每隔5秒钟提交一次已消费的消息的offset。
Flume速览的更多相关文章
- .NET平台开源项目速览(17)FluentConsole让你的控制台酷起来
从该系列的第一篇文章 .NET平台开源项目速览(1)SharpConfig配置文件读写组件 开始,不知不觉已经到第17篇了.每一次我们都是介绍一个小巧甚至微不足道的.NET平台的开源软件,或者学习,或 ...
- .NET平台开源项目速览(15)文档数据库RavenDB-介绍与初体验
不知不觉,“.NET平台开源项目速览“系列文章已经15篇了,每一篇都非常受欢迎,可能技术水平不高,但足够入门了.虽然工作很忙,但还是会抽空把自己知道的,已经平时遇到的好的开源项目分享出来.今天就给大家 ...
- .NET平台开源项目速览(13)机器学习组件Accord.NET框架功能介绍
Accord.NET Framework是在AForge.NET项目的基础上封装和进一步开发而来.因为AForge.NET更注重与一些底层和广度,而Accord.NET Framework更注重与机器 ...
- .NET平台开源项目速览(1)SharpConfig配置文件读写组件
在.NET平台日常开发中,读取配置文件是一个很常见的需求.以前都是使用System.Configuration.ConfigurationSettings来操作,这个说实话,搞起来比较费劲.不知道大家 ...
- .NET平台开源项目速览(12)哈希算法集合类库HashLib
.NET的System.Security.Cryptography命名空间本身是提供加密服务,散列函数,对称与非对称加密算法等功能.实际上,大部分情况下已经满足了需求,而且.NET实现的都是目前国际上 ...
- .NET平台开源项目速览(11)KwCombinatorics排列组合使用案例(1)
今年上半年,我在KwCombinatorics系列文章中,重点介绍了KwCombinatorics组件的使用情况,其实这个组件我5年前就开始用了,非常方便,麻雀虽小五脏俱全.所以一直非常喜欢,才写了几 ...
- .NET平台开源项目速览(10)FluentValidation验证组件深入使用(二)
在上一篇文章:.NET平台开源项目速览(6)FluentValidation验证组件介绍与入门(一) 中,给大家初步介绍了一下FluentValidation验证组件的使用情况.文章从构建间的验证器开 ...
- .NET平台开源项目速览(9)软件序列号生成组件SoftwareProtector介绍与使用
在文章:这些.NET开源项目你知道吗?让.NET开源来得更加猛烈些吧!(第二辑)中,给大家初步介绍了一下Software Protector序列号生成组件.今天就通过一篇简单的文章来预览一下其强大的功 ...
- .NET平台开源项目速览(8)Expression Evaluator表达式计算组件使用
在文章:这些.NET开源项目你知道吗?让.NET开源来得更加猛烈些吧!(第二辑)中,给大家初步介绍了一下Expression Evaluator验证组件.那里只是概述了一下,并没有对其使用和强大功能做 ...
随机推荐
- 力扣 -- 寻找两个有序数组的中位数 Median of Two Sorted Arrays python实现
题目描述: 中文: 给定两个大小为 m 和 n 的有序数组 nums1 和 nums2. 请你找出这两个有序数组的中位数,并且要求算法的时间复杂度为 O(log(m + n)). 你可以假设 nums ...
- Requests使用
Requests 简介 Requests库 requests是一个很实用的Python HTTP客户端库,编写爬虫和测试服务器响应数据时经常会用到.可以说,**Requests 完全满足如今网络的需求 ...
- OpenCV常用基本处理函数(5)图像模糊
2D卷积操作 cv.filter2D() 可以让我们对一幅图像进行卷积操作, 图像模糊(图像平滑)使用低通滤波器可以达到图像模糊的目的.这对与去除噪音很有帮助.其实就是去除图像中的高频成分(比如:噪音 ...
- 小鱼易连 for mac如何使用?小鱼易连 mac版使用教程
小鱼易连 for mac如何使用?小鱼易连 mac版是参加远程会议人士的首选,高效极致,简单流畅,视频流畅,语音清晰,无需专用网络的功能深受用户的喜欢,它提供的文件和电脑的共享,让你的会议更加高效.下 ...
- SpringBoot搭建基于Apache Shiro+Redis的分布式Session共享功能
我们在上一遍文档中已经完成了Shiro验证功能.(http://www.cnblogs.com/nbfujx/p/7773789.html),在此基础上我们将完成分布式Session共享功能. Red ...
- linux编译php
./configure --prefix=/usr/local/php --with-config-file-path=/usr/local/php/etc --with-mysql=/usr/loc ...
- Spring学习笔记第一篇——初识Spring
1.简单介绍 spring的ioc底层是先配置xml文件,接着创建工厂,利用dom4j解析配置文件,最后通过反射完成.大概步骤差不多这样,这些具体代码spring帮你完成了.现在我们只需要配置xml和 ...
- Codechef March Cook-Off 2018. Maximum Tree Path
目录 题意 解析 AC_code @(Codechef March Cook-Off 2018. Maximum Tree Path) 题意 给你一颗\(n(1e5)\)个点有边权有点权的树,\(Mi ...
- PHP使用引用变量foreach时,切记其他循环不要使用同一个名字的变量
foreach ($log['data'] as $k => &$value) { if ($value['token'] != 0) { $value['change_num'] = ...
- HTML5: HTML5 WebSocket
ylbtech-HTML5: HTML5 WebSocket 1.返回顶部 1. HTML5 WebSocket WebSocket是HTML5开始提供的一种在单个 TCP 连接上进行全双工通讯的协议 ...