编码器的执行时机

首先, 我们想通过服务端,往客户端发送数据, 通常我们会调用ctx.writeAndFlush(数据)的方式, 入参位置的数据可能是基本数据类型,也可能对象

其次,编码器同样属于handler,只不过他是特化的专门用于编码作用的handler, 在我们的消息真正写入jdk底层的ByteBuffer时前,数据需要经过编码处理, 不是说不进行编码就发送不出去,而是不经过编码,客户端可能接受到的是乱码

然后,我们知道,ctx.writeAndFlush(数据)它其实是出站处理器特有的行为,因此注定了它需要在pipeline中进行传递,从哪里进行传递呢? 从tail节点开始,一直传播到header之前的我们自己添加的自定义的解码器

WriteAndFlush()的逻辑

我们跟进源码WriteAndFlush()相对于Write(),它的flush字段是true

private void write(Object msg, boolean flush, ChannelPromise promise) {
AbstractChannelHandlerContext next = findContextOutbound();
final Object m = pipeline.touch(msg, next);
EventExecutor executor = next.executor();
if (executor.inEventLoop()) {
if (flush) {
//todo 因为flush 为 true
next.invokeWriteAndFlush(m, promise);
} else {
next.invokeWrite(m, promise);
}

于是就会这样

  • 逐个调用handler的write()
  • 逐个调用handler的flush()

知道这一点很重要,这意味这我们知道了,事件传播分成两波进行, 一波write,一波flush, 这两波事件传播的大体流程我写在这里, 在下面

write

  • 将ByteBuf 转换成DirctBuffer
  • 将消息(DirctBuffer)封装进entry 插入写队列
  • 设置写状态

flush

  • 刷新标志,设置写状态
  • 变量buffer队列,过滤Buffer
  • 调用jdk底层的api,把ByteBuf写入jdk原生的ByteBuffer

自定义一个简单的编码器

/**
* @Author: Changwu
* @Date: 2019/7/21 20:49
*/
public class MyPersonEncoder extends MessageToByteEncoder<PersonProtocol> { // todo write动作会传播到 MyPersonEncoder的write方法, 但是我们没有重写, 于是就执行 父类 MessageToByteEncoder的write, 我们进去看
@Override
protected void encode(ChannelHandlerContext ctx, PersonProtocol msg, ByteBuf out) throws Exception {
System.out.println("MyPersonEncoder....");
// 消息头 长度
out.writeInt(msg.getLength());
// 消息体
out.writeBytes(msg.getContent());
}
}

选择继承MessageToByteEncoder<T> 从消息到字节的编码器

继续跟进

ok,现在来到了我们自定义的 解码器MyPersonEncoder ,

但是,并没看到正在传播的writeAndFlush(),没关系, 我们自己的解码器继承了MessageToByteEncoder,这个父类中实现了writeAndFlush(),源码如下:解析写在源码后面

// todo 看他的write方法
@Override
public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) throws Exception {
ByteBuf buf = null;
try {
if (acceptOutboundMessage(msg)) {// todo 1 判断当前是否可以处理这个对象
@SuppressWarnings("unchecked")
I cast = (I) msg;
// todo 2 内存分配
buf = allocateBuffer(ctx, cast, preferDirect);
try {
// todo 3 调用本类的encode(), 这个方法就是我们自己实现的方法
encode(ctx, cast, buf);
} finally {
// todo 4 释放
ReferenceCountUtil.release(cast);
} if (buf.isReadable()) {
// todo 5. 往前传递
ctx.write(buf, promise);
} else {
buf.release();
ctx.write(Unpooled.EMPTY_BUFFER, promise);
}
buf = null;
} else {
ctx.write(msg, promise);
}
} catch (EncoderException e) {
throw e;
} catch (Throwable e) {
throw new EncoderException(e);
} finally {
if (buf != null) {
// todo 释放
buf.release();
}
}
  • 将我们发送的消息msg,封装进了 ByteBuf 中
  • 编码: 执行encode()方法,这是个抽象方法,由我们自定义的编码器实现
    • 我们的实现很简单,分别往Buf里面写入下面两次数据

      • int类型的消息的长度
      • 消息体
  • 将msg释放
  • 继续向前传递 write()事件
  • 最终,释放第一步创建的ByteBuf

小结

到这里为止,编码器的执行流程已经完成了,我们可以看到,和解码器的架构逻辑相似,类似于模板设计模式,对我们来说,只不过是做了个填空题


其实到上面的最后一步 释放第一步创建的ByteBuf之前 ,消息已经被写到jdk底层的 ByteBuffer 中了,怎么做的呢? 别忘了它的上一步, 继续向前传递write()事件,再往前其实就是HeaderContext了,和HeaderContext直接关联的就是unsafe类, 这并不奇怪,我们都知道,netty中无论是客户端还是服务端channel底层的数据读写,都依赖unsafe

下面开始分析,WriteAndFlush()底层的两波任务细节

第一波事件传递 write()

我们跟进HenderContext的write() ,而HenderContext的中依赖的是unsafe.wirte()所以直接去 AbstractChannel的Unsafe 源码如下:

@Override
public final void write(Object msg, ChannelPromise promise) {
assertEventLoop();
ChannelOutboundBuffer outboundBuffer = this.outboundBuffer;
if (outboundBuffer == null) { // todo 缓存 写进来的 buffer
ReferenceCountUtil.release(msg);
return;
} int size;
try {
// todo buffer Dirct化 , (我们查看 AbstractNioByteBuf的实现)
msg = filterOutboundMessage(msg); size = pipeline.estimatorHandle().size(msg);
if (size < 0) {
size = 0;
}
} catch (Throwable t) {
safeSetFailure(promise, t);
ReferenceCountUtil.release(msg);
return;
}
// todo 插入写队列 将 msg 插入到 outboundBuffer
// todo outboundBuffer 这个对象是 ChannelOutBoundBuf类型的,它的作用就是起到一个容器的作用
// todo 下面看, 是如何将 msg 添加进 ChannelOutBoundBuf中的
outboundBuffer.addMessage(msg, size, promise);
}

参数位置的msg,就是经过我们自定义解码器的父类进行包装了的ByteBuf类型消息

这个方法主要做了三件事

  • 第一: filterOutboundMessage(msg); 将ByteBuf转换成DirctByteBuf

当我们进入查看他的实现时,idea会提示,它的子类重写了这个方法, 是谁重写的呢? 是AbstractNioByteChannel 这个类其实是属于客户端阵营的类,和服务端的AbstractNioMessageChannel相提并论

源码如下:

protected final Object filterOutboundMessage(Object msg) {
if (msg instanceof ByteBuf) {
ByteBuf buf = (ByteBuf) msg;
if (buf.isDirect()) {
return msg;
} return newDirectBuffer(buf);
} if (msg instanceof FileRegion) {
return msg;
} throw new UnsupportedOperationException(
"unsupported message type: " + StringUtil.simpleClassName(msg) + EXPECTED_TYPES);
}
  • 第二件事: 将转换后的DirectBuffer插入到写队列中

什么是写队列 ? 作用是啥?

它其实就是一个netty自定义的容器,使用的单向链表的结构,为什么要有这个容器呢? 回想一下,服务端需要向客户端发送消息,消息进而被封装进ByteBuf,但是呢, 往客户端写的方法有两个

  • write()
  • writeAndFlush()

这个方法的区别是有的,前者只是进行了写,(写到了ByteBuf) 却没有将内容刷新到ByteBuffer,没有刷新到缓存中,就没办法进一步把它写入jdk原生的ByteBuffer中, 而 writeAndFlush()就比较方便,先把msg写入ByteBuf,然后直接刷进socket,一套带走,打完收工

但是如果客户端偏偏就是不使用writeAndFlush(),而使用前者,那么盛放消息的ByteBuf被传递到handler的最开始的位置,怎么办? unsafe也无法把它写给客户端, 难道丢弃不成?

于是写队列就解决了这个问题,它以链表当做数据结构,新传播过来的ByteBuf就会被他封装成一个一个的节点(entry)进行维护,为了区分这个链表中,哪个节点是被使用过的,哪个节点是没有使用过的,他就用三个标记指针进行标记,如下:

  • flushedEntry 被刷新过的entry
  • tailEntry 尾节点
  • unflushedEntry 未被刷的entry

下面我们看一下,它如何将一个新的节点,添加到写队列

addMessage(Object msg, int size, ChannelPromise promise) 添加写队列

public void addMessage(Object msg, int size, ChannelPromise promise) {
// todo 将上面的三者封装成实体
// todo 调用工厂方法, 创建 Entry , 在 当前的ChannelOutboundBuffer 中每一个单位都是一个 Entry, 用它进一步包装 msg
Entry entry = Entry.newInstance(msg, size, total(msg), promise); // todo 调整三个指针, 去上面查看这三个指针的定义
if (tailEntry == null) {
flushedEntry = null;
tailEntry = entry;
} else {
Entry tail = tailEntry;
tail.next = entry;
tailEntry = entry;
}
if (unflushedEntry == null) {
unflushedEntry = entry;
} // increment pending bytes after adding message to the unflushed arrays.
// See https://github.com/netty/netty/issues/1619
// todo 跟进这个方法
incrementPendingOutboundBytes(entry.pendingSize, false);
}

看他的源码,其实就是简单的针对链表进行插入的操作,尾插入法, 一直往最后的位置插入,链表的头被标记成unflushedEntry 这两个节点之间entry,表示是可以被flush的节点

在每次添加新的 节点后都调用incrementPendingOutboundBytes(entry.pendingSize, false)方法, 这个方法的作用是设置写状态, 设置怎样的状态呢? 我们看它的源码, 可以看到,它会记录下累计的ByteBuf的容量,一旦超出了阈值,就会传播channel不可写的事件

  • 这也是write()的第三件事
private void incrementPendingOutboundBytes(long size, boolean invokeLater) {
if (size == 0) {
return;
}
// todo TOTAL_PENDING_SIZE_UPDATER 当前缓存中 存在的代写的 字节
// todo 累加
long newWriteBufferSize = TOTAL_PENDING_SIZE_UPDATER.addAndGet(this, size);
// todo 判断 新的将被写的 buffer的容量不能超过 getWriteBufferHighWaterMark() 默认是 64*1024 64字节
if (newWriteBufferSize > channel.config().getWriteBufferHighWaterMark()) {
// todo 超过64 字节,进入这个方法
setUnwritable(invokeLater);
}
}

小结:

到目前为止,第一波write()事件已经完成了,我们可以看到了,这个事件的功能就是使用ChannelOutBoundBuf将write事件传播过去的单个ByteBuf维护起来,等待 flush事件的传播

第二波事件传递 flush()

我们重新回到,AbstractChannel中,看他的第二波flush事件的传播状态, 源码如下:它也是主要做了下面的三件事

  • 添加刷新标志,设置写状态
  • 遍历buffer队列,过滤可以flush的buffer
  • 调用jdk底层的api,进行自旋写
// todo 最终传递到 这里
@Override
public final void flush() {
assertEventLoop(); ChannelOutboundBuffer outboundBuffer = this.outboundBuffer;
if (outboundBuffer == null) {
return;
}
// todo 添加刷新标志, 设置写状态
outboundBuffer.addFlush(); // todo 遍历buffer队列, 过滤byteBuf
flush0();
}

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

什么是添加刷新标志呢? 其实就是更改链表中的指针位置,三个指针之间的可以完美的把entry划分出曾经flush过的和未flush节点

ok,继续

下面看一下如何设置状态,addflush() 源码如下:

 * todo 给 ChannelOutboundBuffer 添加缓存, 这意味着, 原来添加进 ChannelOutboundBuffer 中的所有 Entry, 全部会被标记为 flushed 过
*/
public void addFlush() {
// todo 默认让 entry 指向了 unflushedEntry ==> 其实链表中的最左边的 未被使用过的 entry
// todo
Entry entry = unflushedEntry; if (entry != null) {
if (flushedEntry == null) {
// there is no flushedEntry yet, so start with the entry
flushedEntry = entry;
}
do {
flushed ++;
if (!entry.promise.setUncancellable()) {
// Was cancelled so make sure we free up memory and notify about the freed bytes
int pending = entry.cancel();
// todo 跟进这个方法
decrementPendingOutboundBytes(pending, false, true);
}
entry = entry.next;
} while (entry != null); // All flushed so reset unflushedEntry
unflushedEntry = null;
}
}

目标是移动指针,改变每一个节点的状态, 哪一个指针呢? 是 flushedEntry , 它指向读被flush的节点,也就是说,它左边的,都被处理过了

下面的代码,是选出一开始位置, 因为, 如果flushedEntry == null,说明没有任何一个曾经被flush过的节点,于是就将开始的位置定位到最左边开始,

if (flushedEntry == null) {
// there is no flushedEntry yet, so start with the entry
flushedEntry = entry;
}

紧接着一个do-while循环,从最后一个被flushedEntry的地方,到尾部,挨个遍历每一个节点, 因为这些节点要被flush进缓存,我们需要把write时累加的他们的容量减掉, 源码如下

private void decrementPendingOutboundBytes(long size, boolean invokeLater, boolean notifyWritability) {
if (size == 0) {
return;
}
// todo 每次 减去 -size
long newWriteBufferSize = TOTAL_PENDING_SIZE_UPDATER.addAndGet(this, -size);
// todo 默认 getWriteBufferLowWaterMark() -32kb
// todo newWriteBufferSize<32 就把不可写状态改为可写状态
if (notifyWritability && newWriteBufferSize < channel.config().getWriteBufferLowWaterMark()) {
setWritable(invokeLater);
}
}

同样是使用原子类做到的这件事, 此外,经过减少的容量,如果小于了32kb就会传播 channel可写的事件

遍历buffer队列, 过滤byteBuf

这是flush的重头戏,它实现了将数据写入socket的操作

我们跟进它的源码,doWrite(ChannelOutboundBuffer in) 这是本类AbstractChannel的抽象方法, 写如的逻辑方法,被设计成抽象的,具体往那个channel写,和具体的实现有关, 当前我们想往客户端写, 它的实现是AbstractNioByteChannel,我们进入它的实现,源码如下

 boolean setOpWrite = false;
// todo 整体是无限循环, 过滤ByteBuf
for (;;) {
// todo 获取第一个 flushedEntity, 这个entity中 有我们需要的 byteBuf
Object msg = in.current();
if (msg == null) {
// Wrote all messages.
clearOpWrite();
// Directly return here so incompleteWrite(...) is not called.
return;
} if (msg instanceof ByteBuf) {
// todo 第三部分,jdk底层, 进行自旋的写
ByteBuf buf = (ByteBuf) msg;
int readableBytes = buf.readableBytes();
if (readableBytes == 0) {
// todo 当前的 ByteBuf 中,没有可写的, 直接remove掉
in.remove();
continue;
} boolean done = false;
long flushedAmount = 0;
if (writeSpinCount == -1) {
// todo 获取自旋锁, netty使用它进行
writeSpinCount = config().getWriteSpinCount();
}
// todo 这个for循环是在自旋尝试往 jdk底层的 ByteBuf写入数据
for (int i = writeSpinCount - 1; i >= 0; i --) { // todo 把 对应的 buf , 写到socket中
// todo localFlushedAmount就是 本次 往jdk底层的 ByteBuffer 中写入了多少字节
int localFlushedAmount = doWriteBytes(buf); if (localFlushedAmount == 0) {
setOpWrite = true;
break;
}
// todo 累加一共写了多少字节
flushedAmount += localFlushedAmount;
// todo 如果buf中的数据全部写完了, 设置完成的状态, 退出循环
if (!buf.isReadable()) {
done = true;
break;
}
} in.progress(flushedAmount); // todo 自旋结束,写完了 done = true
if (done) {
// todo 跟进去
in.remove();
} else {
// Break the loop and so incompleteWrite(...) is called.
break;
}
....

这一段代码也是非常长, 它的主要逻辑如下:

通过一个无限循环,保证可以拿到所有的节点上的ByteBuf,通过这个函数获取节点, Object msg = in.current();

我们进一步看它的实现,如下,它只会取出我们标记的节点

 public Object current() {
Entry entry = flushedEntry;
if (entry == null) {
return null;
} return entry.msg;
}

下一步, 使用jdk的自旋锁,循环16次,尝试往jdk底层的ByteBuffer中写数据, 调用函数doWriteBytes(buf);他是本类的抽象方法, 具体的实现是,客户端chanel的封装类NioSocketChannel实现的源码如下:

// todo
@Override
protected int doWriteBytes(ByteBuf buf) throws Exception {
final int expectedWrittenBytes = buf.readableBytes();
// todo 将字节数据, 写入到 java 原生的 channel中
return buf.readBytes(javaChannel(), expectedWrittenBytes);
}

这个readBytes()依然是抽象方法,因为前面我们曾经把从ByteBuf转化成了Dirct类型的, 所以它的实现类是PooledDirctByteBuf 继续跟进如下: 终于见到了亲切的一幕

 // todo
@Override
public int readBytes(GatheringByteChannel out, int length) throws IOException {
checkReadableBytes(length);
//todo 关键的就是 getBytes() 跟进去
int readBytes = getBytes(readerIndex, out, length, true);
readerIndex += readBytes;
return readBytes;
} 跟进getBytes(){
index = idx(index);
// todo 将netty 的 ByteBuf 塞进 jdk的 ByteBuffer tmpBuf;
tmpBuf.clear().position(index).limit(index + length);
// todo 调用jdk的write()方法
return out.write(tmpBuf);
}

此外,被使用过的节点会被remove()掉, 源码如下, 也是针对链表的操作

private void removeEntry(Entry e) {
if (-- flushed == 0) { // todo 如果是最后一个节点, 把所有的指针全部设为 null
// processed everything
flushedEntry = null;
if (e == tailEntry) {
tailEntry = null;
unflushedEntry = null;
}
} else { //todo 如果 不是最后一个节点, 把当前节点,移动到最后的 节点
flushedEntry = e.next;
}
}

小结

到这里, 第二波任务的传播就完成了

write

  • 将buffer 转换成DirctBuffer
  • 将消息entry 插入写队列
  • 设置写状态

flush

  • 刷新标志,设置写状态
  • 变量buffer队列,过滤Buffer
  • 调用jdk底层的api,把ByteBuf写入jdk原生的ByteBuffer

Netty编码流程及WriteAndFlush()的实现的更多相关文章

  1. 一个低级错误引发Netty编码解码中文异常

    前言 最近在调研Netty的使用,在编写编码解码模块的时候遇到了一个中文字符串编码和解码异常的情况,后来发现是笔者犯了个低级错误.这里做一个小小的回顾. 错误重现 在设计Netty的自定义协议的时候, ...

  2. Netty执行流程分析与重要组件介绍

    一.环境搭建 创建工程,引入Netty依赖 二.基于Netty的请求响应Demo 1.TestHttpServerHandle  处理器.读取客户端发送过来的请求,并且向客户端返回hello worl ...

  3. Netty源码分析第1章(Netty启动流程)---->第3节: 服务端channel初始化

    Netty源码分析第一章:Netty启动流程   第三节:服务端channel初始化 回顾上一小节的initAndRegister()方法: final ChannelFuture initAndRe ...

  4. Netty源码分析第1章(Netty启动流程)---->第4节: 注册多路复用

    Netty源码分析第一章:Netty启动流程   第四节:注册多路复用 回顾下以上的小节, 我们知道了channel的的创建和初始化过程, 那么channel是如何注册到selector中的呢?我们继 ...

  5. X264编码流程详解(转)

    http://blog.csdn.net/xingyu19871124/article/details/7671634 对H.264编码标准一直停留在理解原理的基础上,对于一个实际投入使用的编码器是如 ...

  6. Netty启动流程剖析

    编者注:Netty是Java领域有名的开源网络库,特点是高性能和高扩展性,因此很多流行的框架都是基于它来构建的,比如我们熟知的Dubbo.Rocketmq.Hadoop等,针对高性能RPC,一般都是基 ...

  7. netty 服务器端流程调度Flow笔记

    create NioEventLoopGroup Instance 一.NioServerSocketChannel init note:Initializing ChannelConfig crea ...

  8. 架构师养成记--21.netty编码解码

    背景 作为网络传输框架,免不了哟啊传输对象,对象在传输之前就要序列化,这个序列化的过程就是编码过程.接收到编码后的数据就需要解码,还原传输的数据. 代码 工厂类 import io.netty.han ...

  9. Netty编码的艺术

    Netty 编码器原理和数据输出: Netty 默认提供了丰富的编解码框架供用户集成使用,我们只对较常用的Java 序列化编码器进行讲解.其它的编码器,实现方式大同小异.其实编码器和解码器比较类似, ...

随机推荐

  1. ML:吴恩达 机器学习 课程笔记(Week1~2)

    吴恩达(Andrew Ng)机器学习课程:课程主页 由于博客编辑器有些不顺手,所有的课程笔记将全部以手写照片形式上传.有机会将在之后上传课程中各个ML算法实现的Octave版本. Linear Reg ...

  2. 基于ASP.NET的新闻管理系统(一)

    1. 项目简介 1.1设计内容 (1)可以在首页查看各类新闻,可以点击新闻查看具体内容:可以查看不同类型的新闻,并了解热点新闻,可以在搜索框里输入要查找的内容. (2)在后台界面中,管理员可以修改密码 ...

  3. C语言实现常用数据结构——二叉树

    #include<stdio.h> #include<stdlib.h> #define SIZE 10 typedef struct Tree { int data; str ...

  4. apache虚拟主机防止php网页木马vhost.conf文件配置

    <VirtualHost *> DocumentRoot "/www/www.abc.com" ServerName www.abc.com ServerAlias a ...

  5. SpringBoot从入门到精通二(SpringBoot整合myBatis的两种方式)

    前言 通过上一章的学习,我们已经对SpringBoot有简单的入门,接下来我们深入学习一下SpringBoot,我们知道任何一个网站的数据大多数都是动态的,也就是说数据是从数据库提取出来的,而非静态数 ...

  6. 文件识别浅谈(含office文件区分)

    前言 本文主要根据后台接口识别Office文件类型这一话题做一些分享,主要方向还是放在不能获取到文件名情况下的Office文件识别. 可获取到文件名 如果后端接口可以获取到完成的文件名称,则整个过程会 ...

  7. Scala 学习之路(十二)—— 类型参数

    一.泛型 Scala支持类型参数化,使得我们能够编写泛型程序. 1.1 泛型类 Java中使用<>符号来包含定义的类型参数,Scala则使用[]. class Pair[T, S](val ...

  8. Demo小细节

    (1) 程序如下: public class Example { static int i = 1, j = 2; static { display(i); i = i + j; } static v ...

  9. Google浏览器插件之闪存过滤器

    一件很有意思的事情引发的无聊尝试. 博客园有个很有趣的功能,就是闪存,翻阅到07年园长对闪存的定义:      记录一闪而过的想法,高兴或者不高兴都可以发一下.我用这个一直以来的想法就是,想到点啥发点 ...

  10. wireshark数据包分析实战 第一章

    1,数据包分析工具:tcpdump.wireshark.前者是命令行的,后者是图形界面的. 分析过程:收集数据.转换数据(二进制数据转换为可读形式).分析数据.tcpdump不提供分析数据,只将最原始 ...