什么是nio?
nio 本质就是多路复用,内核告诉你那些句柄可读可写
jdk的selector就是对操作系统的io多路复用的一个封装,在linux中就是对epoll的封装;epoll的本质就是让内核直接处理句柄,不需要再复制到用户空间,这个要整理不能简单介绍
================>传送门(后续整理)
非nio的io复用的使用:
客户端:
打开socketchannel 设置 socketchannel为非阻塞 异步链接server ,向reactor注册多路复用器,reactor线程创建selector启动线程,轮询就绪的key 异步读请求到bytebuffer (异步写请求到socketchannel)
服务端:
打开serversocketchannel 绑定端口,Reactor创建selector 将serversocketchannel注册到selector selector轮询key。判断是否是新接入,新接入设置为accept。不是新接入的可读状态 异步请求到byuteBuffer(异步写butebuffer到socketchannel)
如果是netty不用这么麻烦(首先启动bootstrapt 注入两个线程组,创建channel为nioServerSocketChannel,然后配置nioserverSocketChannel的tcp参数,设置为backlog,最后绑定iohandler 处理类(包含处理类,编解码,记录日志等),上面配置完成以后,绑定监听端口即可)
tcp拆包粘包:
产生原因:1.进行了mss大小的tcp分段 2.超mtu进行ip分段 3.程序写的数据超过缓冲期大小
解决策略:
1.定长消息
2.在包尾部进行分割符
换行:LineBasedFrameDecoder
分隔符:DelimiterBasedFrameDecoder
定长(固定长度):FixLengthFrameFecoder
3.在表头表名消息的总长度
netty解决粘包的第一种方法:LineBasedFrameDecoder解决:
(通过addLast()添加编解码器)
(服务端)
一:bootstrap.group(boss,work).channel(NioserversocketChannel.class).OPTION( tcp参数).childHandler(子处理器)
二:子处理器(pipeline).addLast(LineBasedFrameDecoder编解码).addLast(StringDecoder).addLast(真正的业务处理器handler)
三:真正的处理器子处理器handler需要继承ChannelHandlerAdaptr父类实现 channelActive、channelRead,exceptionCaught方法
(客户端)
与服务端类似:bootstrap.group().channel().option().handler(new ChannerlInitializer(){
实现initChannel(socketChannel)方法{
ch.pipeline.addLast(new LineBasedFrameDecoder()).addLast(StringDecoder()); addLast(客户端处理类)}
})
:如此解决的问题:不会出现粘包的问题
为什么可以解决该问题:
原理很简单LineBasedFrameDecoder的作用是依次遍历bytebuf的可读字节,读到/N /R/N 换行符的时候结束,从可读索引到结束位置的区间字节组成一行
而StringDecoder将接受的对象转成字符串然后调用后来的handler,这两个东西就是做按行切换的的文本解码器
另外的处理方法:DelimiterBasedFrameDecoder 和 FixLengthFrameFecoder 这两种的实现第一个是自动完成以分隔符做结束标志的消息的解码
第二个是自动完成对定长消息的解码
什么是netty的编解码?
当远程调用的时候,需要把本地的java对象编码为字节数组或者bytebuffer对象,当远程读取到bytebuffer对象或者字节数组的时候,需要将其解码位发送时的java对象
java序列化:仅仅是java编解码技术的一种
为什么rpc框架很少使用jdk的serializable序列化:第一个缺点:因为rpc通常用到的是调用非java对象,而jdk的serializable只是适用于java语言;第二个缺点:因为序列化的后码流很大,占用网络传输空间大;第三个缺点,序列化的性能比二进制编码的性能差
编解码技术二:google的protbuf
特点:结构化数据存储格式(xml json) ;高校的编解码技术;语言无关;支持java.c python
使用二进制编码有益于网络传输
Thrift:
弱势:必须先确定好对象的数据结构。当数据结构发生变化了,需要重新定义idl(接口描述)文件
优点:适用于大型数据的交换及存储的工具。性能远优于json和xml
thrift主要由5部分组成:
1.语言系统及idl编译器,由用户给定额idl文件生成相应的接口代码
2.Tprotocol:Rpc的协议层,选择多种不同的序列化方式。如json和binary
3.TTransport:rpc的传输层,可以选择不同的传输层实现。如socket。NIO。MemoryBuffer
4.Tprocessor:作为协议层和用户提供的服务实现之间的纽带,负责调用服务实现的接口
5.TServer:聚合上面的2.3.4
Thrift支持三种典型的编解码:通用的二进制编解码。压缩的二进制编解码。优化的可选字段压缩编解码
Netty使用Protobuf作为编解码框架实现
优点:跨语言,编解码后消息更小,编解码性能高,支持定义可选和必选字段
特点:支持数据结构化一次到处使用,包括跨语言使用
netty实现protobuf:
前面配置一样,bootstrap.group.channel.option.childHandler(pipeline.addlast(protobuVarine32FrameDecoder(半包处理)).addLast(new protobufEncoder()).addLast(new SubReqServerHandler()))
因为使用了Decoder所以消息直接自动解码,收到的消息可以直接使用;由于使用了Encoder我们做响应的时候不需要手动进行编码
客户端:类似,pipeline.addLast(new protobufVarint32FrameDecoder).addLast(new ProtobufDecoder()).addLast(ProtobufVarint32LengthFieldPrepender).addLast(new ProtobufEncoder).addLast(handler)
支持http场景
netty天生的异步事件驱动框架,因此,niotcp协议栈开发的http协议也是异步非阻塞的
WEBSOCKET协议开发
websocket的出现解决了http的弊端,1.http是个半双工的协议,数据只能单一方向传递
2.http消息荣誉繁杂,包含消息头,消息体,换行符等,相比于其余二进制冗杂
照着写一份代码试一下懒得解释了
netty的基本组件及源码学习
ByteBuf:
首先我们数据传输用到缓冲区,使用的最多主要是bytebuffer,8中基本数据类型都用各自的bytebuffer,这个是jdknio类库提供使用的
缺点是
bytebuffer的长度是固定死的,出现了编码的pojo大于bytebuffer的时候就会出现索引越界的异常
2.bytebuffer只有一个标志位置的指针position 读写的时候需要手动调用filp()和rewind()等要正确调用否则会出现异常
光明出现了:Netty提供了新的缓冲区bytebuf数组缓冲区
提供一下几个基本功能:
1.支持7种剧本类型byte数组,bytebuffer等的读写
2.缓冲区自身的copy和slice
3.设置网络字节序
4.构造缓冲区实例
5.操作位置指针等方法
BYTEBUFFER的实现:如果用的是jdk的bytebuffer,要正确使用filp,如果写入数据后不调用flip的话。读取的内容不正确
所以得出结论 : flip()做的是将position与初始位置交换,limit移动到position的位置
bytebuf的实现
采用了readIndex和writeIndex两个指针
clear以后数据都两个指针都回到0位置
Bytebuffer进行扩容,他不能自动扩容,put数据的时候会判断剩余空间是否足够,如果不够的话会进行重新开辟新的空间,将老数据移入,并清理老的bytebuffer对象;而netty提供的新的bytebuf会自动扩容不需要人为进行手动扩容
clear:将读写指针移会到初始位置
Mark+Reset:
bytebuffer用的是这两个,主要的用处是mark将当前指针记录,reset将指针恢复为备份在mark中的值
bytebuf因为有两个指针,所以对应的mark和reset有四个,分别是读mark读reset写mark写reset
已知:nio的socketchannel进行网络读写。操作的对象是bytebuffer。
但是:netty所用的是bytebuf他不能操作
所以:要在接口层进行两者互换,
详解bytebuffer与bytebuf转换:
1.nioBuffer()将bytebuf转换成bytebuffer,两者公用一个缓冲区内容,对bytebuffer的读写操作,不会修改原bytebuf的动态拓展操作
2.niobuffer(int index , int length)也是将bytebuf转换成buffer,是在起始位置为index,长度为length进行转换
支持随机读写(set / get)
但是问题在set和write的区别是set必须确认长度大小是否符合可写入的大小,不能支持动态扩容,而write是支持动态扩容的
Bytebuf分类
按照内存分配的角度,可以分为两类:堆内存和直接内存
堆内存:
分配及回收速度快,可以被jvm自动回收,但是缺点是io读写的时候要额外做一次内存复制,将堆内存的缓冲区复制到内核的channel中
直接内存
非堆内存,在堆外进行内存分配,他的分配和回收会比堆内存比较慢,但是io操作的时候可以直接从socketchannel中读取
最佳实践:
io通信线程读写缓冲区使用直接内存。后端业务编解码模块使用堆内存
按照内存回收的角度区分bytebuf可以以分为两类,基于对象池的bytebuf 普通的bytebuf
区别在于对象池的bytebuf可以复用bytebuf对象,可以在netty的高负载大并发的冲击下使得gc更加的稳定
Channel是个啥
是jdk的NIO的组成部分,netty重新实现
主要的api还是进行网络io
channel在netty中是网络操作抽象类、他聚合了一组功能,包含但不限于网络的读写客户端发起链接主动关闭链接,获取通信双方饿网络地址等;同时也可以获取eventloop,获取缓冲分配器,和pipeline等
netty用自己的channel主要是要能够跟netty的整体架构融合, 例如io模型,基于channelpipeline的定制模型,以及基于元数据描述配置化的tcp参数等
channel的一些常用api
1.channel read:从channel中读取数据到第一个inbound缓冲区,读取完以后会调用channelReadComplete这样决定是否需要继续读取数据
2.channelFuture write:将数据通过channelpipeline写入到channel中,该操作只是将消息发送到环形数组中,只有当调用到flush才会真正的发送数据
3.channelfuture writeandflush 将write和flush组合
4.channel flush将之前写入到发送环形数组的消息全部写入channel 并发送给通信方
5.channelfure connect 指定服务器地址发起链接请求
6.channelfuture bind 绑定指定的本地socket地址
7.is open:判断是否channel已经打开;isRegistered:判断channel是否注册到evenloop上
8.eventLoop():channel需要注册到evenloop的多路复用器;通过该方法,可以获取到channel注册的evenloop (evenloop本质上是吃力网络读写的reactor线程)
netty网络传播的本质是,当有网络io的操作时候会发生channelPipeline中对应的事件方法;channel在io操作中产生的io事件,驱动事件在channelpieline中传播。由对应的channelhandler进行事件处理。不关心的事件直接忽略。功能等价于aop像切面一样
两个重要的channel介绍一下 nioServerSocketChannel 和 nioSocketChannel
nioServerSocketChannel
对于nioserversocketchannel来说,他的读取操作就是接受客户端的链接,然后创建niosocketChannel对象
nioSocketChannel
1.链接操作 socket.bind + socketchannel.connect;如果链接success将niosocketChannel中的selectionKey设置为OP_CONNECT
什么是ChannelPipeline 和 ChannelHandler
就是一种责任链模式
Netty将channel的数据管道抽象为channelpipeline ,消息在channelPipeline中传递。channelHandler为事件拦截器,被channelpipeline持有(pipeline是handler的容器),用户可以通过新增和删除channelhandler进行不同的业务处理,不需要对已有的handler进行修改。
本章三个概念:channelpipeline channelhandler channelhandlerContext
pipeline的流程图详解
左边一列:数据读取的流程,底层的socketchannel 调用read()读取bytebuf -----> 触发io线程eventloop调用channelpipeline的fireChannelRead的方法。将消息(bytebuf)传输到channelPipeline中 --------->依次被channelhandler(1.2.3.4)一直到tailHandler(任何一个channelhandler都可以拦截和处理当前的消息结束消息传递
右边一列:调用channelContext的write方法,从tailHandler开始,途径channelHandler(N...1)将消息发送到缓冲区中等待刷新,也可以中断流程如编码失败的时候,需要中断消息构造future返回
Netty的事件:inbound事件。outbound事件(参看上面的流程图)这两个事件只是netty根据事件在pipeline中流向抽象出来的术语
inbound事件:(流程图左边)(都是执行的channelHandlerContext的 方法)(从io线程流向用户业务的handler)
1.channel注册 》 channelHandlerContext.fireChannelRegistered()
2.TCP链路建立成功,Channel激活事件
3.读事件
4.读完之后的通知事件
5.异常通知事件
6.Channel的可写状态变化通知事件
7.TCP关闭
outbound事件(流程图右边)(都是执行的channelHandlerContext的 方法)(由用户线程或者代码发起的io操作)
1.bind 绑定本地地址事件
2.链接服务端事件
3.write发送事件
4.flush刷新事件
5.read读事件
6.关闭当前channel事件
pipeline的构成>使用ServerBootStrap 或者 bootStrap启动服务端和客户端 netty为每一个channel都会单独的创建一个独立的pipeline
channelhandler是线程不安全的,channelpipeline是线程安全的,channelhandler支持动态的添加和删除,:使用场景在业务高峰期的时候要对系统进行阻塞保护的时候,就可以动态的增减阻塞的channelhandler
channelpipeline维护了channlerhandler 名和 channelHandlerContext实例的映射关系
ChannelHandler支持注解:
1.sharable 多个channelPipeline公用一个channlehandler
2.skip被注释的方法,直接跳过
解码的处理流程:
messageToMessageDecoder 》 byteTomessageDecoder(都是实现channelHandler接口)
NETTY 提供的半包解码器:
1.lineBaseFrameDecoder
2.DelimiterBasedFrameDecoder
3.LengthFieldBasedFrameDecoder :通过长度区分 与其对应的是LengthFieldPrepender将消息编码为 : 长度字段+原消息
如何区分整包:固定长度。回车换行符。分隔符。指定长度
到此为止前面的问题到底是啥?知道了pipeline 和 channelhandler的联系(容器),但是channelhandlerContext和他们的联系是啥呢?????
以下是通过资料查询这三者的关系:
pipeline是handler的通道,context表示ChannelHandler和ChannelPipeline之间的关系
注意:一个ChannelHandler可以属于多个ChannelPipeline,它也可以绑定多个ChannelHandlerContext实例,但是要使用注解sharable
注意二:handler线程不安全的所以要注意
什么是EventLoop和EventLoopGroup?
netty我们会创建boss和woker线程组带着问题看一下?
netty框架主要的线程就是IO线程
Netty线程模型本质上遵循了Reactor的基础线程模型,但是实现与Reactor存在差异.首先我们回顾一下Reactor的线程模型,这地方我应该整理 传送=======>
https://www.jianshu.com/writer#/notebooks/48158437/notes/79347965
回顾完以后看下netty
虽然netty官方的demo是主从reactor模式但是也是支持netty用单或者多线程的模型
Netty的最佳实践
1.创建两个NioEventLoopGroup用于隔离nio Acceptor 和 nio 的io线程 (理解为一个是链接线程一个是读数据的线程)
2.不要在ChannelHandler中启动用户线程,可以启用线程可以将解码后的消息发送到业务线程
3.解码操作要在nio的解码handler中操作,不要切换到用户线程中完成消息解码
4.如果业务处理简单可以直接在nio线程上完成业务逻辑编排
5.如果业务复杂,不要在nio线程上完成业务,要将解码的消息封装成task交付给业务线程中执行,尽快将nio线程释放,
推荐线程数量计算公式
线程数量 = (线程总时间/瓶颈资源时间) * 瓶颈资源的线程并行数
NioEventLoop:不单单是一个io线程,除了负责io的读写,可以实现定时任务,可以处理系统task。(当io线程和用户线程都要操作网络资源的时候,为防止并发操作导致的锁竞争,将用户线程的操作task放到队列中,有io线程同一处理,实现局部无锁化)
Netty解决了空轮询
空轮询的bug:正常情况下selector的select操作是阻塞的,只有被监听的fd有读写的时候才会被唤醒,但是空轮询的bug是没有 读写的时候也会被唤醒,所以会出现空轮询的异常,
产生的原因:linux2.6内核中, poll和epoll突然中断连接的socket的场景下,会将exenset集合设置为pollup。或者是poerr 。这样就造成了evenset事件集合发生了变化。将selector被唤醒就出现了空轮询
代码如下:
Netty解决空轮询的方式:
1.对selector的select的操作周期进行统计
2.每次出现空轮询都记录数
3.如果再某个周期内出现了n次空轮询则证明出现了空轮询的bug
Netty会通过重建selector进行系统恢复
ChannelFuture是异步获取io操作结果的,分为两种状态uncompleted 和 completed 。再其刚创建的时候处于第一种状态,在其完成之后会处于第二种状态,第三种状态也分为操作成功,失败,取消三种
Netty的架构剖析
Netty的行业应用
多线程编程在netty中的应用
拓展知识:
主流的操作系统提供了线程的实现,主要有三种
内核线程:这种线程有内核来完成线程的切换,内核通过线程调度器来对线程进行调度,并负责将线程任务映射到不同的处理器上
用户线程实现:完全建立在用户空间的线程库上,用户线程的创建,启动,运行完全在用户态中完成,不需要内核的帮助,因此执行性能更改
混合实现:顾名思义
sun-jdk:在内核线程实现
solaris-jdk:可以设置参数选择在哪里实现
Netty的逻辑架构
又上向下
service
|
pipeline
|
Reactor
解释:
Reactor通信调度层:包含ReactornioeventLoop。niosocketChannel , bytebuf,主要负责监听网络的读写和链接操作,负责将网络层数据读取到内存中,例如创建连接,读写事件,并将事件触发到pipeline等
ChannelPipeline职责链:负责动态的编排职责连。职责链选择监听和处理自己关心的事件,可以将外部的消息转换成自己内部的pojo对象,这样上层业务只关心业务处理即可
service业务编排层,对于业务开发人只需要关心业务逻辑制定即可,这种分层架构设计理念实现了nio框架各层之间的解耦,便于上层协议栈的开发和业务逻辑定制
NETTY优化的地方:
1.非阻塞的io库,基于reactor实现,解决同步阻塞io模式下,服务端无法平滑增长客户端的问题
2.tcp接收发送缓冲区用直接内存,避免内存复制
3.通过内存池方式循环利用bytebuf,避免频繁常见和销毁bytebuf
4.环形数组缓冲区实现无锁化并发编程
5.关键资源的单线程串行处理,避免多线程并发带来的所竞争和额外的cpu资源消耗
6.通过引用计数器及时释放不在引用的对象,锁粒度的内存管理降低了gc的频率
netty的可靠性
检测链路的有效性,由于长链接每次发送消息都要创建链路,性能相比于短连接性能更高,netty提供了两种心跳检测(空闲时检测机制)
读空闲超时机制:
当连续T周期没有消息可读,触发超时handler,并发送心跳消息,进行链路检测,当n个周期没有收到心跳则关闭链路
写空闲超时机制:
当有T个周期没有数据要发的情况下,用户可以基于写空闲超时发送心跳消息,进行链路检测,如果连续n个周期没有收到对方心跳,主动关闭
netty的可制定性
1.责任链模式,channelpipeline基于责任链开发,便于业务逻辑的拦截,定制和拓展
2.基于接口的开发:关键的类库都提供了接口或者抽象类,如果netty无法满足用户需求,可以由用户自定义实现类
3.提供了工厂类,通过重载这些工厂类可以按需创建出用户实现的对象
4.提供大量系统参数,供用户按需设置,增强系统的场景定制性
Dubbo与Netty
流程图:
DUBBO的RPC框架,通过dubbo encode方式将pojo对象编码为dubbo协议的二进制字节流,通过netty client发送给服务提供者,服务提供者的netty server从niosocketChannel读取二进制码流,将bytebuf解码为dubbo请求消息并调用服务提供者,处理完后返回
netty位dubbo提供的了高性能nio框架,主要职责在:
1.提供了异步高性能的nio框架
2.nio客户端和服务端
3.心跳检测能力
4.断链重试机制
5.流量控制
6.dubbo协议的编解码handler
NETTY CLIENT 的主要实现:
nettyhandler 持有了dubbo的自定义的channelhandler ,通过channelhandler 回调dubbo的filter,实现rpc的服务调用
学习高性能
https://www.cnblogs.com/flgb/p/13122281.html