概述

一个问题

Netty 源码深度解析(九) - 编码_网络

Netty 源码深度解析(九) - 编码_源码_02

编码器实现了​​ChannelOutboundHandler​​,并将出站数据从 一种格式转换为另一种格式,和我们方才学习的解码器的功能正好相反。Netty 提供了一组类, 用于帮助你编写具有以下功能的编码器:


  • 将消息编码为字节
  • 将消息编码为消息
    我们将首先从抽象基类 MessageToByteEncoder 开始来对这些类进行考察

1 抽象类 MessageToByteEncoder

Netty 源码深度解析(九) - 编码_Java_03

解码器通常需要在​​Channel​​关闭之后产生最后一个消息(因此也就有了 ​​decodeLast()​​方法)

这显然不适于编码器的场景——在连接被关闭之后仍然产生一个消息是毫无意义的

1.1 ShortToByteEncoder

其接受一​​Short​​ 型实例作为消息,编码为​​Short​​的原子类型值,并写入​​ByteBuf​​,随后转发给​​ChannelPipeline​​中的下一个 ​​ChannelOutboundHandler​

每个传出的 Short 值都将会占用 ByteBuf 中的 2 字节

Netty 源码深度解析(九) - 编码_源码_04

Netty 源码深度解析(九) - 编码_网络_05

1.2 Encoder

Netty 源码深度解析(九) - 编码_面试_06

Netty 提供了一些专门化的 ​​MessageToByteEncoder​​,可基于此实现自己的编码器

​WebSocket08FrameEncoder​​类提供了一个很好的实例

Netty 源码深度解析(九) - 编码_网络_07

2 抽象类 MessageToMessageEncoder

你已经看到了如何将入站数据从一种消息格式解码为另一种

为了完善这幅图,将展示 对于出站数据将如何从一种消息编码为另一种。​​MessageToMessageEncoder​​类的 ​​encode()​​方法提供了这种能力

Netty 源码深度解析(九) - 编码_Netty_08

为了演示,使用​​IntegerToStringEncoder​​ 扩展了 ​​MessageToMessageEncoder​

  • 编码器将每个出站 Integer 的 String 表示添加到了该 List 中
    Netty 源码深度解析(九) - 编码_源码_09
    Netty 源码深度解析(九) - 编码_Java_10

关于有趣的 MessageToMessageEncoder 的专业用法,请查看 ​​io.netty.handler. codec.protobuf.ProtobufEncoder​​类,它处理了由 Google 的 Protocol Buffers 规范所定义 的数据格式。

一个java对象最后是如何转变成字节流,写到socket缓冲区中去的

writeAndFlush


  • 从tail节点开始往前传播
  • 逐个调用channelHandler#write
  • 逐个调用channelHandler#flush

java对象编码过程

write:写队列

flush:刷新写队列

writeAndFlush: 写队列并刷新

pipeline中的标准链表结构

Netty 源码深度解析(九) - 编码_面试_11

数据从head节点流入,先拆包,然后解码成业务对象,最后经过业务​​Handler​​处理,调用​​write​​,将结果对象写出去

而写的过程先通过​​tail​​节点,然后通过​​encoder​​节点将对象编码成​​ByteBuf​​,最后将该​​ByteBuf​​对象传递到​​head​​节点,调用底层的Unsafe写到JDK底层管道

Java对象编码过程

为什么我们在pipeline中添加了encoder节点,java对象就转换成netty可以处理的ByteBuf,写到管道里?

我们先看下调用write的code

Netty 源码深度解析(九) - 编码_Netty_12

业务处理器接受到请求之后,做一些业务处理,返回一个​​user​



然后,user在pipeline中传递
Netty 源码深度解析(九) - 编码_网络_13
Netty 源码深度解析(九) - 编码_网络_14
Netty 源码深度解析(九) - 编码_Netty_15
Netty 源码深度解析(九) - 编码_Java_16



情形一
Netty 源码深度解析(九) - 编码_面试_17
Netty 源码深度解析(九) - 编码_Java_18



情形二
Netty 源码深度解析(九) - 编码_网络_19
Netty 源码深度解析(九) - 编码_源码_20
Netty 源码深度解析(九) - 编码_源码_21
Netty 源码深度解析(九) - 编码_Java_22
handler 如果不覆盖 flush 方法,就会一直向前传递直到 head 节点
Netty 源码深度解析(九) - 编码_源码_23



落到 ​​Encoder​​节点,下面是 ​​Encoder​​ 的处理流程

Netty 源码深度解析(九) - 编码_源码_24

按照简单自定义协议,将Java对象 User 写到传入的参数 out中,这个out到底是什么?

需知​​User​​对象,从​​BizHandler​​传入到 ​​MessageToByteEncoder​​时,首先传到 ​​write​

Netty 源码深度解析(九) - 编码_源码_25

1. 判断当前Handelr是否能处理写入的消息(匹配对象)

Netty 源码深度解析(九) - 编码_Netty_26

Netty 源码深度解析(九) - 编码_Java_27

Netty 源码深度解析(九) - 编码_网络_28

  • 判断该对象是否是该类型参数匹配器实例可匹配到的类型
    Netty 源码深度解析(九) - 编码_源码_29
    Netty 源码深度解析(九) - 编码_面试_30

2 分配内存

Netty 源码深度解析(九) - 编码_Java_31

Netty 源码深度解析(九) - 编码_源码_32

3 编码实现


  • 调用​​encode​​,这里就调回到 ​​Encoder​​ 这个​​Handler​​中
    Netty 源码深度解析(九) - 编码_Java_33
  • 其为抽象方法,因此自定义实现类实现编码方法
    Netty 源码深度解析(九) - 编码_源码_34
    Netty 源码深度解析(九) - 编码_源码_35

4 释放对象

  • 既然自定义Java对象转换成​​ByteBuf​​​了,那么这个对象就已经无用,释放掉 (当传入的​​msg​​​类型是​​ByteBuf​​​时,就不需要自己手动释放了)
    Netty 源码深度解析(九) - 编码_网络_36
    Netty 源码深度解析(九) - 编码_Java_37

5 传播数据

//112 如果buf中写入了数据,就把buf传到下一个节点,直到 header 节点

Netty 源码深度解析(九) - 编码_源码_38

6 释放内存

//115 否则,释放buf,将空数据传到下一个节点

// 120 如果当前节点不能处理传入的对象,直接扔给下一个节点处理

// 127 当buf在pipeline中处理完之后,释放

Netty 源码深度解析(九) - 编码_面试_39

Encoder处理传入的Java对象


  • 判断当前​​Handler​​是否能处理写入的消息

  • 如果能处理,进入下面的流程
  • 否则,直接扔给下一个节点处理

  • 将对象强制转换成​​Encoder​​​ 可以处理的 ​​Response​​对象
  • 分配一个​​ByteBuf​
  • 调用​​encoder​​,即进入到 Encoder 的 encode方法,该方法是用户代码,用户将数据写入ByteBuf
  • 既然自定义Java对象转换成ByteBuf了,那么这个对象就已经无用了,释放掉(当传入的msg类型是ByteBuf时,无需自己手动释放)
  • 如果buf中写入了数据,就把buf传到下一个节点,否则,释放buf,将空数据传到下一个节点
  • 最后,当buf在pipeline中处理完之后,释放节点

总结就是,​​Encoder​​​节点分配一个​​ByteBuf​​​,调用​​encode​​方法,将Java对象根据自定义协议写入到ByteBuf,然后再把ByteBuf传入到下一个节点,在我们的例子中,最终会传入到head节点

public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) throws Exception {
unsafe.write(msg, promise);
}

这里的msg就是前面在Encoder节点中,载有java对象数据的自定义ByteBuf对象

write - 写buffer队列

Netty 源码深度解析(九) - 编码_Netty_40

Netty 源码深度解析(九) - 编码_源码_41

Netty 源码深度解析(九) - 编码_Java_42

Netty 源码深度解析(九) - 编码_源码_43

Netty 源码深度解析(九) - 编码_面试_44

Netty 源码深度解析(九) - 编码_源码_45

Netty 源码深度解析(九) - 编码_Netty_46

Netty 源码深度解析(九) - 编码_Java_47

Netty 源码深度解析(九) - 编码_Java_48

以下过程分三步讲解

Netty 源码深度解析(九) - 编码_网络_49

direct ByteBuf

Netty 源码深度解析(九) - 编码_Java_50

Netty 源码深度解析(九) - 编码_网络_51


  • 首先,调用​​assertEventLoop​​确保该方法的调用是在​​reactor​​线程中
  • 然后,调用 ​​filterOutboundMessage()​​,将待写入的对象过滤,把非​​ByteBuf​​对象和​​FileRegion​​过滤,把所有的非直接内存转换成直接内存​​DirectBuffer​Netty 源码深度解析(九) - 编码_网络_52
    Netty 源码深度解析(九) - 编码_Netty_53

插入写队列

  • 接下来,估算出需要写入的ByteBuf的size

Netty 源码深度解析(九) - 编码_面试_54

  • 最后,调用 ChannelOutboundBuffer 的addMessage(msg, size, promise) 方法,所以,接下来,我们需要重点看一下这个方法干了什么事情
    Netty 源码深度解析(九) - 编码_网络_55

想要理解上面这段代码,须掌握写缓存中的几个消息指针

Netty 源码深度解析(九) - 编码_面试_56

ChannelOutboundBuffer 里面的数据结构是一个单链表结构,每个节点是一个 Entry,Entry 里面包含了待写出ByteBuf 以及消息回调 promise下面分别是

三个指针的作用


  • flushedEntry
    表第一个被写到OS Socket缓冲区中的节点
    Netty 源码深度解析(九) - 编码_Java_57
  • unFlushedEntry
    表第一个未被写入到OS Socket缓冲区中的节点
    Netty 源码深度解析(九) - 编码_源码_58
  • tailEntry
    表​​ChannelOutboundBuffer​​缓冲区的最后一个节点
    Netty 源码深度解析(九) - 编码_面试_59

图解过程



初次调用write 即 ​​addMessage​​ 后
Netty 源码深度解析(九) - 编码_网络_60
​fushedEntry​​指向空,​​unFushedEntry​​和 ​​tailEntry​​都指向新加入节点



第二次调用 ​​addMessage​​后
Netty 源码深度解析(九) - 编码_Netty_61



第n次调用 ​​addMessage​​后
Netty 源码深度解析(九) - 编码_Java_62



可得,调用n次​​addMessage​​后


  • ​flushedEntry​​​指针一直指向​​null​​,表此时尚未有节点需写到Socket缓冲区
  • ​unFushedEntry​​后有n个节点,表当前还有n个节点尚未写到Socket缓冲区

设置写状态

Netty 源码深度解析(九) - 编码_网络_63



统计当前有多少字节需要需要被写出
Netty 源码深度解析(九) - 编码_Java_64



当前缓冲区中有多少待写字节
Netty 源码深度解析(九) - 编码_Netty_65



Netty 源码深度解析(九) - 编码_Java_66

Netty 源码深度解析(九) - 编码_源码_67

Netty 源码深度解析(九) - 编码_网络_68

Netty 源码深度解析(九) - 编码_Java_69

  • 所以默认不能超过64k
    Netty 源码深度解析(九) - 编码_Java_70

Netty 源码深度解析(九) - 编码_面试_71

  • 自旋锁+CAS 操作,通过 pipeline 将事件传播到channelhandler 中监控
    Netty 源码深度解析(九) - 编码_网络_72

flush:刷新buffer队列

添加刷新标志并设置写状态



不管调用​​channel.flush()​​,还是​​ctx.flush()​​,最终都会落地到​​pipeline​​中的​​head​​节点
Netty 源码深度解析(九) - 编码_Java_73



之后进入到​​AbstractUnsafe​Netty 源码深度解析(九) - 编码_网络_74



flush方法中,先调用
Netty 源码深度解析(九) - 编码_面试_75
Netty 源码深度解析(九) - 编码_Java_76
Netty 源码深度解析(九) - 编码_面试_77
Netty 源码深度解析(九) - 编码_Java_78



结合前面的图来看,上述过程即
首先拿到 ​​unflushedEntry​​ 指针,然后将​​flushedEntry​​指向​​unflushedEntry​​所指向的节点,调用完毕后
Netty 源码深度解析(九) - 编码_源码_79



遍历 buffer 队列,过滤bytebuf



接下来,调用 ​​flush0()​Netty 源码深度解析(九) - 编码_面试_80



发现这里的核心代码就一个 ​​doWrite​Netty 源码深度解析(九) - 编码_网络_81



AbstractNioByteChannel

  • 继续跟
protected void doWrite(ChannelOutboundBuffer in) throws Exception {
int writeSpinCount = -1;

boolean setOpWrite = false;
for (;;) {
// 拿到第一个需要flush的节点的数据
Object msg = in.current();

if (msg instanceof ByteBuf) {
boolean done = false;
long flushedAmount = 0;
// 拿到自旋锁迭代次数
if (writeSpinCount == -1) {
writeSpinCount = config().getWriteSpinCount();
}
// 自旋,将当前节点写出
for (int i = writeSpinCount - 1; i >= 0; i --) {
int localFlushedAmount = doWriteBytes(buf);
if (localFlushedAmount == 0) {
setOpWrite = true;
break;
}

flushedAmount += localFlushedAmount;
if (!buf.isReadable()) {
done = true;
break;
}
}

in.progress(flushedAmount);

// 写完之后,将当前节点删除
if (done) {
in.remove();
} else {
break;
}
}
}
}


第一步,调用​​current()​​先拿到第一个需要​​flush​​的节点的数据
Netty 源码深度解析(九) - 编码_Netty_82



第二步,拿到自旋锁的迭代次数
Netty 源码深度解析(九) - 编码_面试_83



第三步 调用 JDK 底层 API 进行自旋写
自旋的方式将​​ByteBuf​​写到JDK NIO的​​Channel​​ 强转为ByteBuf,若发现没有数据可读,直接删除该节点
Netty 源码深度解析(九) - 编码_源码_84



拿到自旋锁迭代次数



Netty 源码深度解析(九) - 编码_Java_85



在并发编程中使用自旋锁可以提高内存使用率和写的吞吐量,默认值为16
Netty 源码深度解析(九) - 编码_面试_86



继续看源码
Netty 源码深度解析(九) - 编码_网络_87
Netty 源码深度解析(九) - 编码_面试_88



​javaChannel()​​,表明 JDK NIO Channel 已介入此次事件
Netty 源码深度解析(九) - 编码_面试_89
Netty 源码深度解析(九) - 编码_Netty_90



得到向JDK 底层已经写了多少字节
Netty 源码深度解析(九) - 编码_Java_91
Netty 源码深度解析(九) - 编码_网络_92



从 Netty 的 bytebuf 写到 JDK 底层的 bytebuffer
Netty 源码深度解析(九) - 编码_网络_93
Netty 源码深度解析(九) - 编码_Netty_94



第四步,删除该节点
节点的数据已经写入完毕,接下来就需要删除该节点
Netty 源码深度解析(九) - 编码_网络_95
首先拿到当前被​​flush​​掉的节点(​​flushedEntry​​所指)
然后拿到该节点的回调对象 ​​ChannelPromise​​, 调用 ​​removeEntry()​​移除该节点
Netty 源码深度解析(九) - 编码_Java_96
这里是逻辑移除,只是将flushedEntry指针移到下个节点,调用后
Netty 源码深度解析(九) - 编码_网络_97



随后,释放该节点数据的内存,调用​​safeSuccess​​回调,用户代码可以在回调里面做一些记录,下面是一段Example

Netty 源码深度解析(九) - 编码_面试_98

最后,调用 ​​recycle​​,将当前节点回收

writeAndFlush - 写队列并刷新

writeAndFlush​在某个​​Handler​​中被调用后,最终会落到 ​​TailContext​​节点

Netty 源码深度解析(九) - 编码_面试_99

Netty 源码深度解析(九) - 编码_网络_100

Netty 源码深度解析(九) - 编码_源码_101

Netty 源码深度解析(九) - 编码_源码_102

通过一个boolean变量flush,表明调用​​invokeWriteAndFlush​​ or ​​invokeWrite​​,​​invokeWrite​​便是我们上文中的write过程。

Netty 源码深度解析(九) - 编码_Netty_103

可以看到,最终调用的底层方法和单独调用​​write​​和​​flush​​一样的

Netty 源码深度解析(九) - 编码_Netty_104

Netty 源码深度解析(九) - 编码_Java_105

由此看来,​​invokeWriteAndFlush​​基本等价于​​write​​之后再来一次​​flush​​。

总结


  • 调用​​write​​​并没有将数据写到Socket缓冲区中,而是写到了一个单向链表的数据结构中,​​flush​​才是真正的写出
  • ​writeAndFlush​​等价于先将数据写到netty的缓冲区,再将netty缓冲区中的数据写到Socket缓冲区中,写的过程与并发编程类似,用自旋锁保证写成功
  • netty中的缓冲区中的ByteBuf为DirectByteBuf

如何把对象变成字节流,最终写到socket底层?

当 BizHandler 通过 writeAndFlush 方法将自定义对象往前传播时,其实可以拆分成两个过程


  • 通过 pipeline逐渐往前传播,传播到其中的一个 encode 节点后,其负责重写 write 方法将自定义的对象转化为 ByteBuf,接着继续调用 write 向前传播
  • pipeline中的编码器原理是创建一个​​ByteBuf​​​,将Java对象转换为​​ByteBuf​​​,然后再把​​ByteBuf​​继续向前传递,若没有再重写了,最终会传播到 head 节点,其中缓冲区列表拿到缓存写到 JDK 底层 ByteBuffer