这是我参与8月更文挑战的第4天,活动详情查看:8月更文挑战
一、 源码入口
上篇文章我们讲述了几个典型的内置解码器的的源码,本节课我们将讲述关于数据通信过程中数据写入的过程.
我们以之前的一个例子的代码为例:
这段代码相信大家都记得,我们直接进入主题,分析以下他的源码:
ctx.writeAndFlush(byteBuf);
@Override
public ChannelFuture writeAndFlush(Object msg) {
return writeAndFlush(msg, newPromise());
}
@Override
public ChannelFuture writeAndFlush(Object msg, ChannelPromise promise) {
write(msg, true, promise);
return promise;
}
这里注意个小细节,write(msg, true, promise); 的第二个参数是true,为什么是true呢?因为我们现在调用的是writeAndFlush方法,是存在flush的,所以为true,不带flush为false!
private void write(Object msg, boolean flush, ChannelPromise promise) {
...............忽略..............
//寻找对应的handler 根据掩码
final AbstractChannelHandlerContext next = findContextOutbound(flush ?
(MASK_WRITE | MASK_FLUSH) : MASK_WRITE);
final Object m = pipeline.touch(msg, next);
EventExecutor executor = next.executor();
if (executor.inEventLoop()) {
//是否调用了 writeAndFlush
if (flush) {
next.invokeWriteAndFlush(m, promise);
} else {
//是否调用了 write
next.invokeWrite(m, promise);
}
} else {
...........忽略............
}
}
这里为了方便分析,我们还是按照同步的方式进行分析:
final AbstractChannelHandlerContext next = findContextOutbound(flush ? (MASK_WRITE | MASK_FLUSH) : MASK_WRITE);
从当前的节点开始向上寻找实现了 write方法和flush方法的,默认是HeadContext来实现的,所以这里找到的必定是HeadContext!
二、源码分析
我们调用的是writeAndflush方法,所以flush为true,所以会进入到if逻辑里面:
next.invokeWriteAndFlush(m, promise);
void invokeWriteAndFlush(Object msg, ChannelPromise promise) {
if (invokeHandler()) {
//加入写队列
invokeWrite0(msg, promise);
//刷新缓冲区
invokeFlush0();
} else {
writeAndFlush(msg, promise);
}
}
invokeHandler() 一般返回为true, 所以我们进入到 invokeWrite0
:
1. 追加写队列
private void invokeWrite0(Object msg, ChannelPromise promise) {
try {
((ChannelOutboundHandler) handler()).write(this, msg, promise);
} catch (Throwable t) {
notifyOutboundHandlerException(t, promise);
}
}
我们前面分析过,这个handler必定是HeadContext的,所以我们进入到 io.netty.channel.DefaultChannelPipeline.HeadContext#write:
@Override
public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) {
unsafe.write(msg, promise);
}
@Override
public final void write(Object msg, ChannelPromise promise) {
assertEventLoop();
ChannelOutboundBuffer outboundBuffer = this.outboundBuffer;
..........忽略................
int size;
try {
//转换bytebuf为堆外内存
//io.netty.channel.nio.AbstractNioByteChannel.filterOutboundMessage
msg = filterOutboundMessage(msg);
size = pipeline.estimatorHandle().size(msg);
if (size < 0) {
size = 0;
}
} catch (Throwable t) {
safeSetFailure(promise, t);
ReferenceCountUtil.release(msg);
return;
}
//将数据插入到写队列
outboundBuffer.addMessage(msg, size, promise);
}
-
Netty默认的就是使用堆外内存,即使你创建的是一个堆内内存Netty也会强行转换为堆外内存(代码如下):
msg = filterOutboundMessage(msg);
@Override protected final Object filterOutboundMessage(Object msg) { if (msg instanceof ByteBuf) { ByteBuf buf = (ByteBuf) msg; //判断是不是堆外内存 if (buf.isDirect()) { //堆外内存无需转换直接返回 return msg; } //创建一个堆外内存,将数据写进堆外内存 return newDirectBuffer(buf); } .................................................... }
-
判断可读字节数:
size = pipeline.estimatorHandle().size(msg);
-
开始向写队列追加数据
//将数据插入到写队列 outboundBuffer.addMessage(msg, size, promise);
public void addMessage(Object msg, int size, ChannelPromise promise) { //将BuytBuf封装为Entity Entry entry = Entry.newInstance(msg, size, total(msg), promise); //当tailEntity为Null的时候 if (tailEntry == null) { //初始化 flushedEntry flushedEntry = null; } else { Entry tail = tailEntry; //将当前的尾节点指向新节点 tail.next = entry; } //把当前的新节点赋值为尾节点 tailEntry = entry; if (unflushedEntry == null) { //unflushedEntry也指向entry新节点 unflushedEntry = entry; } // 将消息添加到未刷新的数组后,增加待处理字节。 // See https://github.com/netty/netty/issues/1619 incrementPendingOutboundBytes(entry.pendingSize, false); }
首先进行节点的追加,该次追加是为刷新节点的追加,我们看一动图的演示:
事实上说白了就是,数据在这一步并不会被写进通道,而是被追加到了Netty设计的一个链表上,只要不调用flush方法,该链表的数据就一致被追加;
-
tailEntry永远指向最新的一个数据,unflushedEnrty永远指向头节点!
-
每次追加一个节点,这个节点都会被追加到上一个节点next的引用!
每次追加数据完毕后,记录以下当前待刷新的字节的数量:
incrementPendingOutboundBytes(entry.pendingSize, false);
private void incrementPendingOutboundBytes(long size, boolean invokeLater) { if (size == 0) { return; } //当前缓冲区有多少代写字节 TOTAL_PENDING_SIZE_UPDATER long newWriteBufferSize = TOTAL_PENDING_SIZE_UPDATER.addAndGet(this, size); //待写字节 > 64 * 1024的话 if (newWriteBufferSize > channel.config().getWriteBufferHighWaterMark()) { setUnwritable(invokeLater); } }
这里会判断当前的代写字节是否超过了 64 * 1024
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) { //传播 ChannelWritabilityChanged(invokeLater) 事件 fireChannelWritabilityChanged(invokeLater); } break; } } }
主要逻辑就是当待写入的字节数量大于 64K的时候们就会传播一个
ChannelWritabilityChanged
事件! -
2. 追加到刷新队列
我们回到任务的主线: io.netty.channel.AbstractChannelHandlerContext#invokeWriteAndFlush
invokeFlush0();
private void invokeFlush0() {
try {
((ChannelOutboundHandler) handler()).flush(this);
} catch (Throwable t) {
notifyHandlerException(t);
}
}
同样同上面一样,这里的handler同样是 HeadContext,我们进入到:io.netty.channel.DefaultChannelPipeline.HeadContext#flush:
@Override
public final void flush() {
assertEventLoop();
ChannelOutboundBuffer outboundBuffer = this.outboundBuffer;
if (outboundBuffer == null) {
return;
}
//添加到刷新缓冲区
outboundBuffer.addFlush();
//开始刷新到管道
flush0();
}
这里做了两件事:
- 将写队列的数据转移到刷新队列
- 将刷新队列的数据写到Socket通道
I. 将写队列的数据转移到刷新队列
//添加到刷新缓冲区
outboundBuffer.addFlush();
public void addFlush() {
//取出第一个 等待刷新队列的节点
Entry entry = unflushedEntry;
if (entry != null) {
//当第一次添加
if (flushedEntry == null) {
// 还没有flushedEntry,所以从条目开始
//将刷新队列等于当前的待刷新的节点
flushedEntry = entry;
}
do {
//自增
flushed ++;
if (!entry.promise.setUncancellable()) {
// 已取消,因此请确保我们释放内存并通知释放的字节
int pending = entry.cancel();
//减少将要写入的待处理字节。
decrementPendingOutboundBytes(pending, false, true);
}
//不断将数据处理完毕
entry = entry.next;
} while (entry != null);
// 所有已刷新,因此重置为未刷新
unflushedEntry = null;
}
}
这个逻辑,起始细看来并不难,我们之前写队列的结构如下:
我们看这个逻辑:
Entry entry = unflushedEntry;
if (entry != null) {
//当第一次添加
if (flushedEntry == null) {
// 还没有flushedEntry,所以从条目开始
//将刷新队列等于当前的待刷新的节点
flushedEntry = entry;
}
根据上图,flushedEntry确实为Null,就会进入到if逻辑,使 flushedEntry指向entry, 与是乎,结构变成了下图这样:
开始循环消费带待刷新队列:
do {
//自增
flushed ++;
if (!entry.promise.setUncancellable()) {
// 已取消,因此请确保我们释放内存并通知释放的字节
int pending = entry.cancel();
//减少将要写入的待处理字节。
decrementPendingOutboundBytes(pending, false, true);
}
//不断将数据处理完毕
entry = entry.next;
} while (entry != null);
-
这里会不断的消费 ,等到数据量小于32K的时候,同样会触发
ChannelWritabilityChanged
-
decrementPendingOutboundBytes(pending, false, true);
-
private void decrementPendingOutboundBytes(long size, boolean invokeLater, boolean notifyWritability) { if (size == 0) { return; } long newWriteBufferSize = TOTAL_PENDING_SIZE_UPDATER.addAndGet(this, -size); //待处理的节点 小于 32*1024 if (notifyWritability && newWriteBufferSize < channel.config().getWriteBufferLowWaterMark()) { //ChannelWritabilityChanged(invokeLater); setWritable(invokeLater); } }
-
这个源码可以自己跟一下试试,和上面的基本一致,一个是+一个是-
// 所有已刷新,因此重置为未刷新
unflushedEntry = null;
此时的结构图变为:
总体的刷新队列如下:
II. 将数据写进刷新队列
//开始刷新到管道
flush0();
@Override
protected final void flush0() {
if (!isFlushPending()) {
super.flush0();
}
}
protected void flush0() {
.................忽略..................
try {
//开始写入
doWrite(outboundBuffer);
} catch (Throwable t) {
.................忽略..................
}
}
这里我们重点关注:doWrite(outboundBuffer);
@Override
protected void doWrite(ChannelOutboundBuffer in) throws Exception {
int writeSpinCount = config().getWriteSpinCount();
do {
//获取当前刷新队列的头数据
Object msg = in.current();
..................................
//不断循环写入
//开始向jdk管道写入数据
writeSpinCount -= doWriteInternal(in, msg);
} while (writeSpinCount > 0);
incompleteWrite(writeSpinCount < 0);
}
这里开始循环消费之前的flush队列,调用 doWriteInternal写入:
private int doWriteInternal(ChannelOutboundBuffer in, Object msg) throws Exception {
if (msg instanceof ByteBuf) {
ByteBuf buf = (ByteBuf) msg;
...................................
//开始写入
final int localFlushedAmount = doWriteBytes(buf);
...................................
//磁盘写入
} else if (msg instanceof FileRegion) {
...................................
}
return WRITE_STATUS_SNDBUF_FULL;
}
我们进入到:doWriteBytes(buf);方法:
@Override
protected int doWriteBytes(ByteBuf buf) throws Exception {
//返回可读字节
final int expectedWrittenBytes = buf.readableBytes();
//将 buf写入管道
return buf.readBytes(javaChannel(), expectedWrittenBytes);
}
获取当前的可读字节数,然后通过 javaChannel()返回的channel对象写入通道,至此一个entity的数据就被写入到Socket通道里面了!
三、总结
- 先将数据写进write队列
- 将write队列的Entity转移到flush队列
- 将flush队列里面的Entity里面的ByteBuf通过JDK底层的Channel写进Socket通道!