ChannelHandler和Codec
现在开始介绍仅次于EventLoop的另一个核心组件,ChannelHandler,可插拔的处理流程。
ChannelHandler类层次.png
1, 有没有看到有几个Adapter结尾的类,传说中的(适配器设计模式)
2, 有没有看到Encoder结尾的类,没错,他们就是转码用的,codec只是ChannelHandler中一类有着特殊任务的家伙而已。
3, 要说ChannelHandler必须先从它的容器pipeline开始。
pipeline只有一个实现类DefaultChannelPipeline,应该把里面的每一行代码看一遍,核心数据结构是AbstractChannelHandlerContext构成的双链表。
这个链表有默认的头和为,就是上面截图里的HeadContext和TailContext。
这两个家伙有非常重要的作用哦。
整个pipeline的处理流程是:ChannelInboundHandler定义的输入事件触发时从head向tail传播,ChannelOutboundHandler定义的输出事件触发时从tail向head传播。现在就清晰了,HeadContext即是ChannelInboundHandler起向后传播事件的作用,又是ChannelOutboundHandler,在outbound的最后一环调用channel完成操作。tail是inbound的最后一环,如果pipeline处理过程中调用了write,事件会从当前handler context的下一个开始传播,如果一直没调用write,最后就会走到tail,tail会记录一个log,提示当前请求没有被处理。
然后就是各种把handler添加到pipeline的方法了
channel context.png
从newContext就可以看出来啦,handler context起着关联pipeline和handler的作用,同时可以指定处理当前handler事件回调的EventLoop,如果不指定,就使用当前的pipeline关联的channel注册到的那个EventLoop。
正是由于handler context关联着pipeline,所以handler有了动态修改pipeline的能力。Bootstrap的方法handler只能指定一个handler,这个handler就起着初始化整个pipeline的作用啦。这个特殊的channel handler就是ChannelInitializer,它自己被添加成功后就会触发handlerAdded方法,在这里最终调用抽象方法initChannel初始化整个pipeline,然后功成身退毁灭自己。
一个请求经过pipeline之后,标准的处理流程是这样的:首先原始请求数据已经被EventLoop读到了ByteBuf里,然后经过pipeline的一个ByteToMessageDecoder,将字节流解码为message,message一般是各种java对象或者是各种协议里的一帧。然后再经过n个MessageToMessageDecoder,转成业务队列,其实就是反序列化的概念,加下来就是具体的业务逻辑了,业务处理过程中,如果已经生成了部分响应信息,可以先调用write*方法写入ChannelOutboundBuffer,在这个过程中可以先设置n个MessageToMessageDecoder,再加一个MessageToByteDecoder,最后转成字节流,其实就是序列化的概念,等全部处理完之后,调用flush方法把响应信息返回client。
codec系列一共有6个抽象类。按请求处理流程的顺序分别介绍。
ByteToMessageDecoder:顾名思义。tcp是分包发送的,一个请求可能分成了n包,所以呢,再收到一个完整的请求之前必须先把之前的请求都保留下来,有两种策略,一个是使用CompositeByteBuf,一个是使用单个ByteBuf,动态扩展。
默认使用单个动态扩展的ByteBuf。每次收到数据都会触发channelRead事件,啥时候才能知道一个完整的请求读完了呢,留了一个抽象方法decode,不同的协议有不同的实现机制,只要decode成功,就说明一个请求收完了。每次decode有些协议可能decode出多个message,每个message都会触发一次channelRead事件的传播,最后channel读完所有数据之后,触发channelReadComplete。ByteToMessageDecoder有两个状态变量:discardAfterReads和numReads,后者记录channelRead触发的次数,前者记录多少次read之后调用一次ByteBuf的discardSomeReadBytes方法,因为readIndex之前的数据是不可用的,ByteBuf的容量是有限的,所以需要经常的回收空间。discardSomeReadBytes的作用就是平移readIndex,writeIndex以及他们之间的数据到ByteBuf开头。每次channelRead结束都会判断numReads是否超过了discardAfterReads,每次channelReadComplete也会回收一次空间。
以RedisDecoder为例说明decode方法的实现机制:
decode 状态机.png
这让我想起了在飞天时给mandiri银行做的电子钱包应用,也是这么玩的。
解析一个完整的请求可能需要n个步骤,每个步骤对应一个状态,每走完一个步骤就修改成对应的状态,下次根据当前状态就知道该走那个步骤啦。
MessageToMessageDecoder:这个就没啥新鲜的了,有一个地方很神奇,一直以为java的泛形在运行时会擦除,运行时无法获取泛形的类型信息。netty在这里用了一种奇妙的方法通过反射获得了泛形的类型,每次decode之前都会先调用acceptInboundMessage方法判断当前的message类型是否符合MessageToMessageDecoder的泛形参数的类型。不符合就跳过。我要把string转成int,你给我一个Object,我也不知道怎么转啊。所以判断类型兼容是很重要的。怎么获得泛形参数的呢:虽然运行时泛形被擦除,但是通过反射可以获得一个类定义时的泛形参数名,直接上图
获取泛形参数类型.png
那类型擦除到底是咋回事,有时间之后详细分析。
MessageToMessageEncoder:跟MessageToMessageDecoder是完全一样的套路了。
MessageToByteEncoder:套路同上。
还有两个编码解码融为一体的家伙:
ByteToMessageCodec:内部的所有操作都委托给ByteToMessageDecoder和MessageToByteEncoder了,传输中的代理模式。
MessageToMessageCodec:套路同上
本节完。。。。。