netty的基本组件
- EventLoopGroup
- ServerBootstrap
- NioServerSocketChannel
- ChannelFuture
- 各种ChannelHandler
- ChannelPipeline
- ChannelHandlerContext
- byteBuf
ChannelPipeline
-
本质是职责链模式的变形
- 类似于servlet,servlet专门用来拼凑页面来相应的,中间会有各种filter,完全不影响servlet功能的实现
-
把channel的管道抽象成ChannelPipeline,消息在channelPipeline上传递,传递给handler进行处理,并且在channelPipeline可以很方便的进行handler的增加和删除
-
在channelPipeline分为两种事件,出站和入站
- 入站:从网络到应用程序
- 出站:从应用程序到网络,不仅是数据,还有操作的通知,比如入站事件有channelRead,但是在出站事件还有一个read,意思为读取更多数据,也是一个出站操作,不是read就一定是入站,出站事件还有绑定、发起连接、重连关闭等
-
ChannelPipeline是netty自动创建的,有一个channel连接,就会有一个独立的channelPipeline,并不是全服务器只有一个
-
channelPipeline是线程安全的,往pipeline加handler,或者删除handler都是线程安全的,做了并发控制,但是对于每一个handler自己本身,如果出现跨线程访问时,是不能保证线程安全的,这种情况发生在handler打上了sharable注解
具体的方法
- 很多都是管理handler的方法
- 让事件和数据在pipeline上传递和流动的相关方法
- pipeline是一个借口,channelPipeline定义了各种行为
DefaultChannelPipeline
-
channelPipeline的缺省实现,需要对所有的定义方法提供实现
-
handler加入pipile的过程,就是链表的增删过程,而pipile的真正节点是ctx
-
addFirst
- 链表操作
-
所有的add和remove操作完成后都会执行callHandlerAdded0方法
-
callHandlerAdded0
1.ctx.handler().handlerAdded(ctx);
是一个空实现,由子类去覆盖这个方法
-
-
所有以fire开头的方法,都是跟入站相关的方法,包括链路的建立、从链路上读取相关数据等等,都会产生一个事件,这个事件会在pipile中处理,入站事件是有限的,所以netty就对这些事件做了抽象
-
包括像read、发起绑定、write这些方法是出站方法,在pipeline中有实现,但是注意pipeline本身是不做io操作的,只是handler的容器,这些出站操作最终交给channel或者channel相关的unsafe类做处理
-
例如write方法
@Override public final ChannelFuture write(Object msg, ChannelPromise promise) { return tail.write(msg, promise); }
1.tail是AbstractChannelHandlerContext类
2.在DefaultChannelPipeline构造方法中,发现head和tail构成了一个双向链表
3.tail总是最后一个,所以channel.write是从尾巴上面开始写的,出站也就会流经所有的handler
-
ChannelHandlerContext
- 代表了handler和pipeline之间的关系
具体方法
- 和那些channel、pipeline进行了挂钩
- channel()
- pipeline()
- 支持哪些事件的传播
- fire开头的方法
AbstractChannelHandlerContext
-
ChannelHandlerContext接口的抽象类
-
与网络传播的各种方法调用,比如说context、fire、invoke
-
例如fireChannelRead方法
-
@Override public ChannelHandlerContext fireChannelRead(final Object msg) { invokeChannelRead(findContextInbound(), msg); return this; }
-
static void invokeChannelRead(final AbstractChannelHandlerContext next, Object msg) { final Object m = next.pipeline.touch(ObjectUtil.checkNotNull(msg, "msg"), next); EventExecutor executor = next.executor(); if (executor.inEventLoop()) { next.invokeChannelRead(m); } else { executor.execute(new Runnable() { @Override public void run() { next.invokeChannelRead(m); } }); } }
1.首先取出下一个handler
2.交给下一个handler执行
3.下面代码块判断当前执行invokeChannelRead方法的是不是当前线程
if (executor.inEventLoop()) { next.invokeChannelRead(m); } else { executor.execute(new Runnable() { @Override public void run() { next.invokeChannelRead(m); } }); }
?
因为eventloop和一个channel挂钩的,一个channel生命周期内所有事情都由这个eventloop进行负责,如果出现跨线程调用,netty会进行判断,是不是当前channel的同一个线程,是就直接执行,不是就投放到一个队列中去执行,投放到哪个队列?投放到当前channel所属线程里面去异步执行
-
-
private AbstractChannelHandlerContext findContextInbound() { AbstractChannelHandlerContext ctx = this; do { ctx = ctx.next; } while (!ctx.inbound); return ctx; } private AbstractChannelHandlerContext findContextOutbound() { AbstractChannelHandlerContext ctx = this; do { ctx = ctx.prev; } while (!ctx.outbound); return ctx; }
怎么去找下一个入站或者怎么去栈前一个出站?
循环遍历,直到找到为止。所以入站和出站之间的相对顺序不能乱,但是入站和出站的顺序可以乱,这是因为netty对handler进行了入站和出站的标记
HeadContext
-
既是ctx,又是handler
-
同时,既可以处理入站,又可以处理出站
-
入站基本没有处理,只是进行简单的触发并往后面传递,因为对于入站的头一个,根本不知道怎么处理网络数据,只需要往后面传递即可
- channelInactive
- channelRead
- channelActive
- channelRegistered
-
出站,也没有做处理,而是交给了unsafe类去做的处理,因为是出站的最后一个,直接往网络上发送即可,真正读写网络的是靠unsafe类,这是netty中的unsafe,不是jdk中的unsafe
- bind
- connect
- disconnect
- close
- deregister
- read
- write
- flush
TailContext
-
本身是一个ctx,同时只是一个入站的最后一个handler
-
大部分方法都是空实现
-
有代码实现的,基本也是什么都不做,执行了onUnhandledInboundMessage方法
- 而这个方法执行了ReferenceCountUtil.release(msg),做了资源的释放,避免造成内存泄漏
DefaultChannelHandlerContext
- 大部分方法都已经由AbstractChannelHandlerContext实现了,所以这个方法很简单
ChannelHandler
-
负责对io事件做拦截和处理
-
在handler有选择的进行拦截和处理,也可以透传
-
支持注解的
- sharable注解,在多个channelPipeline之间共享
- skip注解,跳过这个方法的注解
ChannelHandler接口的继承关系
-
ChannelInboundHandler接口
-
ChannelOutboundHandler接口
-
ChannelHandlerAdapter抽象类
-
ChannelInboundHandlerAdapter实体类
-
ChannelOutboundHandlerAdapter实体类
-
为什么在接口之外还要提供一个adapter类?
-
不用全量实现
-
一些平庸实现,比如ChannelInboundHandlerAdapter的channelRegistered和channelUnregistered等等很多方法,实现都是透传(fire),当需要实现额外功能时,只需要重写这个类即可,不需要增加新的方法
-
ChannelOutboundHandlerAdapter不是透传,而是直接调用ctx下面的方法
-
ChannelHandlerAdapter也是,要么是空实现,要么是透传
-
ChannelHandler子类
- 系统级handler
- HeaderHandler
- TelnetHandler
- 编解码类handler ,ByteToByte,ByteToMessage,MessageToByte,…
- 功能性handler,包括流量整型 Handler、读写超时 Handler、日志 Handler等
- 协议编解码handler,ssl、http、websocket协议相关的
ByteToMessageDecoder
-
decode方法是一个抽象方法,需要自行实现
-
在decode里面,把ByteBuf解码,可以是javaBean,转换完成后,继续在handler上传递
-
因为是解码,一定是入站,所以关注的是channelRead方法
-
为什么只要实现了decode,就能进行解码了?
-
channelRead方法
1.首先判断读进来的是不是ByteBuf,如果是进行一系列处理,不是则透传
2.如果是,创建一个CodeOutputList,往后面handler进行传递
3.TCP协议天生就有粘包半包问题,所以引入了一个cumulation的变量,用来保存上次传输没有传输完的数据,而且专门有一个内部接口Cumulator,专门来进行合并
4.callDecode进行实际的解码,放在一个while里面进行解码,因为一个tcp报文里面包含多个应用程序报文,所以在while循环里面调用decodeRemovalReentryProtection方法,而这个方法里面又调用我们定义的decode方法,凡是解出来的都放在CodeOutputList中
5.只要list里面解码出来的size大于0,就往后面传递
-
注意,这里不是直接处理粘包半包问题,解决粘包半包的一种方法FixedLengthFrameDecoder就是派生于这个类,粘包半包处理的handler总是第一个
-
为什么要三次握手,三次握手会传递序列号,告诉对端下次传递是多少,如果不是就扔掉了,然后操作系统底层会对所有的包进行组包
-
MessageToByteEncoder
-
派生自outbound,我们要实现encode这个抽象方法
-
最关键的是write方法
-
1.首先进行收到的msg的类型进行判定,是不是定义的message的类型,这个判定比较复杂,用了jdk里面的反射(match方法)
-
2.进行强制转型
-
3.交给定义好的encode方法处理
-
4.如果编码成功,buf里面写入了数据,交给ctx继续往外面写
-
5.如果失败,把buf释放掉,把一个null的buf往对端写
-
编码完成后,为什么要对cast进行内存释放?
把Message理解成javaBean,Byte理解成ByteBuf,转成ByteBuf后,应该把Message释放掉,因为已经没用了
-
MessageToMessageEncoder
- 编码完成后,也要对cast进行内存释放,因为转换前的javaBean已经没用了,需要释放掉
- 如果抛出IllegalReferenceCountException: refcnt:0, increment: 1,就要考虑这个buffer是不是已经被释放掉了
- 要么别用这种Encode了,直接用OutBoundAdapter那个实体类
Future和Promise源码分析
- 在写netty的应用程序时,所以的操作都是异步的,如果要获取结果,就是通过future来获取结果
juc并发包下的Future
- get方法
- cancel方法
netty下的future接口,继承于juc下的future
-
除了获取结果,还加入了等待、同步、同步转异步
-
实际过程中也是使用它的子类ChannelFuture,是io操作相关的
ChannelFuture
-
支持增加监听器,当io操作完成后,会调用这个监听器里面的方法,可以通过监听器获取io操作的结果
-
会有两种状态
- 要么是完成,要么是未完成
- 注意操作完成不代表成功,完成有三种,操作成功、操作失败、操作取消
-
为什么不直接通过get方法去拿,而是通过监听器去拿?
-
get方法会阻塞,netty里面都是异步操作,完成时间是无法预测的,如果不设置超时时间,当前线程就会在get方法上阻塞,可能会长时间挂死,netty异步操作里面线程是很忙的,一个io线程要处理很多channel,阻塞在一个channel上了,其他的channel就没法处理的,虽然也可以使用超时,但是不知道合适的超时时间,通过异步回调机制,只要有结果来了,netty会在调用注册的监听器,执行操作完成的相关方法,通过里面的future去拿结果
-
后者性能更优,前者有很大的性能损失
-
AbstractFuture
-
实现了两个get方法
-
第一个get
1.调用await方法进行阻塞
2.当io操作完成,会调用notify,唤醒阻塞的线程
3.对获得的结果进行相应的判定
-
第二个超时实现的get
await方法中带入了超时时长
-
future总结
- 对操作结果的获取,但是有个问题,future是获取结果,不管是哪一级的future,都没有写结果的地方,而Promise是一个可写的future,设置io操作的结果,也基础于Future
Promise
- setSuccess
- setFailure
DefaultPromise
-
主要的实现类
-
主要的工作是进行各种设置,比如当操作成功了,setSuccess方法,该方法首先设置结果,然后再唤醒相关的线程
-
setSuccess0-----设置结果的方法
1.setValue0用了cas操作来保证线程安全
2.checkNotifyWaiters来唤醒所有等待的线程
3.notifyliteners来唤醒监听器
执行监听器的operationComplete通过future来设置回调结果
-
-
channel中的await方法、sync方法最终都是调用的Object.wait方法
-
b.bind方法,主线程进入一种wait的状态,当有绑定完成的时候,会调用相关的notify方法把相关线程唤醒,如果不加sync()方法,很快就会跑完,不能保证主线程执行完,绑定就完成了
-
用netty实现的rpc中(rpc-netty-server)RpcServerFrame,其中的bind方法,其中主线程不会很快跑完退出应用程序?