8 Netty编码和writeAndFlush()
如果调用writeAndFlush(),且有对应的编码器,那么Netty就能将对象变成字节流,最终写到socket底层
总过程图:涉及两个重要的自定义handler:Encoder、BizHandler;最后传到head,通过底层unsafe写到socket
BizHandler:
public class BizHandler extends ChannelInboundHandlerAdapter {
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
//...:这里可能会有一些对象的数据,这里简化,直接new
User user = new User(19, "zhangsan");
ctx.channel().writeAndFlush(user);
}
}
Encoder:
/**
* ---------------------
*| 4 | 4 | ? |
* ---------------------
*| length | age | name |
* ---------------------
*/
//可以使用基于长度域解码器对out进行解码
public class Encoder extends MessageToByteEncoder<User> {
@Override
protected void encode(ChannelHandlerContext ctx, User user, ByteBuf out) throws Exception {
byte[] bytes = user.getName().getBytes();
out.writeInt(4 + bytes.length);
out.writeInt(user.getAge());
out.writeBytes(bytes);
}
}
问题:
1.out是如何传播到Encoder的?
通过write传:MessageToByteEncoder#write方法会调用encode,这也是我们重写此方法的原因
MessageToByteEncoder的write方法会调用encoder方法,传入ByteBuf,重写后就能拿到
2.将对象编码进out之后,是如何把out写到socket底层的?
自旋写
8.1 writeAndFlush()抽象步骤
writeAndFlush拆分为writer和flush方法,这样可以单独调用
- 流程
1.从tail节点开始往前向outbound传播
2.先逐个调用channelHandler的write方法直到Head,表明将对象写到缓存(自定义encoder覆盖write方法)
3.write结束后,再逐个调用channelHandler的flush方法直到Head,表明对象真的要写到socket底层去了(此方法不用覆盖)
- 解析
在逐个调用channelHandler的write方法时,需要进行“当前线程是否对应NioEventLoop”的判断逻辑
8.2 抽象编码器MessageToByteEncoder处理逻辑
MessageToByteEncoder是outboundHandler,write方法的msg其实就是要编码的对象
- 流程
1.匹配对象(acceptOutboundMessage(0)
2.如果匹配成功,则分配内存(ByteBuf buf = allocateBuffer())
3.编码实现(encode()),自定义编码器需要重写此方法
4.释放对象
5.传播数据
6.如果出了异常,则释放buf内存
- 解析
使用TypeParameterMatcher来判断当前handler是否能处理此msg
分配出来的内存作用:给第三步用来写入对象数据用
如果对象是ByteBuf对象,则需要释放,如果是自定义的就不用
传播到head节点,由此节点负责将数据写入socket
8.3 Head节点的write、flush
write():Head的write调用Unsafe的write,这一步称为写buffer队列
8.3.1 write()-写buffer队列
- 流程
1.direct化ByteBuf(AbstractNioByteChannel#newDirectBuffer()),即将内存转为堆外内存,因为要通过Unsafe来操作
2.将ByteBuf封装为一个entry,然后插入写队列,通过指针来标识ByteBuf的一些状态(addMessage)
3.设置写状态,统计当前需要写出的字节,这样如果内存不足了,那么就能知道不能将新的ByteBuf插入队列,会抛出异常(64字节最大)
- 解析
指针:tailEntry、flushedEntry、unflushedEntry
// Entry(flushedEntry) --> ... Entry(unflushedEntry) --> ... Entry(tailEntry)
//链表结构中的第一个被刷新过的entry,即Entry(flushedEntry) --> ... 这段都是被write掉的了
private Entry flushedEntry;
//链表结构中第一个未刷新的entry
private Entry unflushedEntry;
//表示缓冲区的尾部
private Entry tailEntry;
第一次调用write()时三个指针的状态:
第二次调用write()时三个指针的状态:
第n次调用write()时三个指针的状态:
8.3.2 flush-刷新buffer队列
flush():Head的flush调用Unsafe的flush,这一步称为刷新buffer队列
- 流程
1.添加刷新标志(即三个指针)并设置写状态(将刷新到socket的字节数减掉,这样就可以继续write了)
2.遍历buffer队列,过滤ByteBuf
3.调用jdk底层api进行自旋写(默认尝试16次,性能点)
- 解析
如下图的两个写状态,如果小于64,表明不可写了,如果小于32,就把不可写改为可写(设计巧妙)
第一次过程后三个指针的状态:unflushedEntry为null,说明缓冲区已经没有可以flush到flushedEntry到TailEntry这一段的Bytebuf了;而flushedEntry到TailEntry这一段都是可以flush的
每次写完都会移除当前entry,即将flushedEntry往前移动一个位置,最后指向null
8. 总结
- 如何把对象变成字节流,最终写到socket底层?
8.1的抽象步骤具体看就是