Netty之向外发送核心ChannelOutboundBuffer源码解析
本文参考自 https://www.jianshu.com/p/311425d1c72f
(Transport implementors only) an internal data structure used by {@link AbstractChannel} to store its pending
* outbound write requests.
它有三个重要的成员变量
private Entry flushedEntry; // 即将被消费的开始节点 private Entry unflushedEntry;// 被添加的开始节点,但没有准备好被消费。 private Entry tailEntry;// 最后一个节点
其内部逻辑就是链表

调用 addMessage 方法的时候,创建一个 Entry ,将这个 Entry 追加到 TailEntry 节点后面,调用 addFlush 的时候,将 unflushedEntry 的引用赋给 flushedEntry,然后将 unflushedEntry 置为 null。
当数据被写进 Socket 后,从 flushedEntry(current) 节点开始,循环将每个节点删除。
二 addMessage
public void addMessage(Object msg, int size, ChannelPromise promise) { Entry entry = Entry.newInstance(msg, size, total(msg), promise); if (tailEntry == null) { flushedEntry = null; tailEntry = entry; } else { Entry tail = tailEntry; tail.next = entry; tailEntry = entry; } if (unflushedEntry == null) { unflushedEntry = entry; } incrementPendingOutboundBytes(entry.pendingSize, false); }
其实就是链表的写法
- 根据 ByteBuf 相互属性和 promise 创建一个 Entry 节点。
- 将新的节点追加到 tailEntry 节点上。如果考虑之前的全部被清空了话,则新节点就是唯一节点,unflushedEntry 属性就是新的节点。可对照上面的图来看。
- 使用 CAS 将 totalPendingSize(总的数据大小) 属性增加 Entry 实例的大小(96 字节) + 真实数据的大小。
三 addFlush
答:因为 Netty 提供了 promise,这个对象可以做取消操作,例如,不发送这个 ByteBuf 了,所以,在 write 之后,flush 之前需要告诉 promise 不能做取消操作了。
public void addFlush() { Entry entry = unflushedEntry; if (entry != null) { if (flushedEntry == null) { flushedEntry = entry; } do { flushed ++; if (!entry.promise.setUncancellable()) { int pending = entry.cancel(); decrementPendingOutboundBytes(pending, false, true); } entry = entry.next; } while (entry != null); unflushedEntry = null; } }
- 首先拿到未刷新的头节点。
- 判 null 之后,将这个 unflushedEntry 赋值给 flushedEntry,而这里的判 null 是做什么呢?防止多次调用 flush 。
- 循环尝试设置这些节点,告诉他们不能做取消操作了,如果尝试失败了,就将这个节点取消,在调用 nioBuffers 方法的时候,这个节点会被忽略。同时将 totalPendingSize 相应的减小。
四 flush0
最终调用的是 doWrite(outboundBuffer);SocketChannel ch = javaChannel(); // 获取自旋的次数,默认16 int writeSpinCount = config().getWriteSpinCount(); // 获取设置的每个 ByteBuf 的最大字节数,这个数字来自操作系统的 so_sndbuf 定义 int maxBytesPerGatheringWrite = ((NioSocketChannelConfig) config).getMaxBytesPerGatheringWrite(); // 调用 ChannelOutboundBuffer 的 nioBuffers 方法获取 ByteBuffer 数组,从flushedEntry开始,循环获取 ByteBuffer[] nioBuffers = in.nioBuffers(1024, maxBytesPerGatheringWrite); // ByteBuffer 的数量 int nioBufferCnt = in.nioBufferCount(); // 使用 NIO 写入 Socket ch.write(buffer); // 调整最大字节数 adjustMaxBytesPerGatheringWrite(attemptedBytes, localWrittenBytes, maxBytesPerGatheringWrite); // 删除 ChannelOutboundBuffer 中的 Entry in.removeBytes(localWrittenBytes); // 自旋减一,直到自旋小于0停止循环,当然如果 ChannelOutboundBuffer 空了,也会停止。 --writeSpinCount; // 如果自旋16次还没有完成 flush,则创建一个任务放进mpsc 队列中执行。 incompleteWrite(writeSpinCount < 0);
上面的代码示例是展示写一个ByteBuffer的例子,如果多个ByteBuffer也是直接利用NIO的方法
public abstract long write(ByteBuffer[] srcs, int offset, int length)
五 缓冲区防溢出策略
写缓冲区是一个大小无限的链表,所以注定不能让这个链表无限制的扩大,那么当缓冲区太大了该怎么办呢?
addMessage的最后一行有一个方法
incrementPendingOutboundBytes(entry.pendingSize, false); 注意这个false
private void incrementPendingOutboundBytes(long size, boolean invokeLater) { if (size == 0) { return; } long newWriteBufferSize = TOTAL_PENDING_SIZE_UPDATER.addAndGet(this, size); if (newWriteBufferSize > channel.config().getWriteBufferHighWaterMark()) { setUnwritable(invokeLater); } }
如果当前的大小大于了高水位就会调用 setUnwritable,上面知道入参是false
那么高水位是多少呢 ? private static final int DEFAULT_HIGH_WATER_MARK = 64 * 1024; 64KB
继续看 setUnwritable
private void setUnwritable(boolean invokeLater) { for (;;) { final int oldValue = unwritable; final int newValue = oldValue | 1; if (UNWRITABLE_UPDATER.compareAndSet(this, oldValue, newValue)) { if (oldValue == 0 && newValue != 0) { fireChannelWritabilityChanged(invokeLater); } break; } } }
继续看 ,注意入参还是false
private void fireChannelWritabilityChanged(boolean invokeLater) { final ChannelPipeline pipeline = channel.pipeline(); if (invokeLater) { Runnable task = fireChannelWritabilityChangedTask; if (task == null) { fireChannelWritabilityChangedTask = task = new Runnable() { @Override public void run() { pipeline.fireChannelWritabilityChanged(); } }; } channel.eventLoop().execute(task); } else { pipeline.fireChannelWritabilityChanged(); } }
所以调用的是 pipeline.fireChannelWritabilityChanged();
public final ChannelPipeline fireChannelWritabilityChanged() { AbstractChannelHandlerContext.invokeChannelWritabilityChanged(head);//注意这个head return this; }
省掉中间过程
private void invokeChannelWritabilityChanged() { if (invokeHandler()) { try { ((ChannelInboundHandler) handler()).channelWritabilityChanged(this); } catch (Throwable t) { notifyHandlerException(t); } } else { fireChannelWritabilityChanged(); } }
调用的是channelHandler的 channelWritabilityChanged
我看了下Netty中没有方法对此有实现,所以实现是交给了用户,目的是让当前的pipeline写的慢一点,比如把在调用写入时,睡眠一段时间
浙公网安备 33010602011771号