目录
一、 背景
二、源码
一、背景
writeAndFlush,顾名思义,就是写入(发送缓冲区)并且刷新,熟悉netty编码的同学对这个方法一定不会感到陌生, 这方法既能将数据写到发送缓存中, 也能刷新到channel中。
二、源码
以在动态长度解码器那节中使用的例子为入口,我们进入到writeAndFlush看看其执行逻辑。
public class LengthFieldBasedFrameDecoderTestClient {
public static void main(String[] args) throws Exception {
EventLoopGroup workerGroup = new NioEventLoopGroup();
try {
Bootstrap b = new Bootstrap();
b.group(workerGroup)
.channel(NioSocketChannel.class)
.option(ChannelOption.SO_KEEPALIVE, true)
.handler(new ChannelInitializer<SocketChannel>() {
@Override
public void initChannel(SocketChannel ch) {
ch.pipeline()
.addLast(new ChannelInboundHandlerAdapter() {
public void channelActive(ChannelHandlerContext ctx) {
ByteBuf byteBuf = Unpooled.copiedBuffer("hello world".getBytes());
ctx.writeAndFlush(byteBuf);
}
});
}
});
ChannelFuture f = b.connect("127.0.0.1", 9000).sync();
System.out.println("Started LengthFieldBasedFrameDecoderTestClient...");
f.channel().closeFuture().sync();
} finally {
workerGroup.shutdownGracefully();
}
}
}
进入到ChannelHandlerContext的实现类AbstractChannelHandlerContext中。
@Override
public ChannelFuture writeAndFlush(Object msg) {
return writeAndFlush(msg, newPromise());
}
newPromise()是为了写成功或者失败后的异步回调通知(DefaultChannelPromise)。其中会注册listener监听器,对处理事件进行监听。
继续进入到实现了ChannelOutboundInvoker的writeAndFlush方法。
@Override
public ChannelFuture writeAndFlush(Object msg, ChannelPromise promise) {
if (msg == null) {
throw new NullPointerException("msg");
}
if (!validatePromise(promise, true)) {
ReferenceCountUtil.release(msg);
// cancelled
return promise;
}
write(msg, true, promise);
return promise;
}
此时的msg就是发送的消息载体。promise是刚才创建的DefaultChannelPromise。
在这个writeAndFlush方法中首先对msg进行非空判断,然后对promise进行校验,核心逻辑为write方法,直接进入。
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) {
next.invokeWriteAndFlush(m, promise);
} else {
next.invokeWrite(m, promise);
}
} else {
AbstractWriteTask task;
if (flush) {
task = WriteAndFlushTask.newInstance(next, m, promise);
} else {
task = WriteTask.newInstance(next, m, promise);
}
safeExecute(executor, task, promise, m);
}
}
findContextOutbound()用于以当前ctx为入口一直往前找,直到找到为出站的一个ctx为止,返回找到的ctx(此时为HeadContext,也就是第一个节点了),然后获取执行器NIOEventLoop,flush此时为true,直接进入invokeWriteAndFlush()方法。
private void invokeWriteAndFlush(Object msg, ChannelPromise promise) {
if (invokeHandler()) {
//将消息放入输出缓冲区
invokeWrite0(msg, promise);
//将输出缓冲区中的数据通过socket发送到网络中
invokeFlush0();
} else {
writeAndFlush(msg, promise);
}
}
到这里就很明显了,如果handler准备好了,依次调用invokeWrite0、invokeFlush0写入数据输出缓冲区中(ChannelOutboundBuffer)并且flush刷新缓冲区发送到网络中。
直接进入invokeWrite0方法。
此时将HeadContext转成出站Handler-ChannelOutboundHandler并且调用其write方法。
private void invokeWrite0(Object msg, ChannelPromise promise) {
try {
((ChannelOutboundHandler) handler()).write(this, msg, promise);
} catch (Throwable t) {
notifyOutboundHandlerException(t, promise);
}
}
再往下走就看到了熟悉的unsafe方法,netty所有io操作,底层都是依赖unsafe进行处理的。
@Override
public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) throws Exception {
unsafe.write(msg, promise);
}
此时的unsafe为NioSocketChannelUnsafe。
进入到其write方法逻辑中。
@Override
public final void write(Object msg, ChannelPromise promise) {
assertEventLoop();
ChannelOutboundBuffer outboundBuffer = this.outboundBuffer;
if (outboundBuffer == null) {
//如果outboundBuffer为null,则该通道已关闭,因此需要立即使将来失效。
safeSetFailure(promise, WRITE_CLOSED_CHANNEL_EXCEPTION);
// 立即释放消息以防止资源泄漏
ReferenceCountUtil.release(msg);
return;
}
int size;
try {
//过滤待发送的消息,只允许发送ByteBuf和FileRegion
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);
}
首先获得ChannelOutboundBuffer,如果outboundBuffer为null,说明通道已经关闭,立即释放消息以防止资源泄漏,然后直接return返回。
filterOutboundMessage(msg),过滤待发送的消息,只允许发送ByteBuf和FileRegion。
@Override
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);
}
在这里会对msg类型进行判断,只有ByteBuf以及 FileRegion可以进行网络传输,其他类型的数据是不支持的,如果是非ByteBuf或者FileRegion,直接抛出异常,消息类型是不支持的。
pipeline.estimatorHandle().size(msg)对待发送的消息大小进行预估,如果消息大小小于零,直接重置为零,然后最后通过addMessage(msg, size, promise)将消息添加到buffer中。
进入到addMessage(msg, size, promise)方法。
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;
}
// increment pending bytes after adding message to the unflushed arrays.
// See https://github.com/netty/netty/issues/1619
incrementPendingOutboundBytes(size, false);
}
首先构造一个Entry实例entry,这个Entry是ChannelOutboundBuffer中自定义的一个静态内部类,内部封装了一个Entry结点信息,核心属性如下:
private final Handle<Entry> handle;
Entry next;
//原始消息
Object msg;
ByteBuffer[] bufs;
ByteBuffer buf;
//异步回调通知成功或者失败
ChannelPromise promise;
long progress;
//对数据本身大小的记录
long total;
//msg+bufs+buf待发送数据总大小
int pendingSize;
int count = -1;
boolean cancelled;
ChannelOutboundBuffer中几个核心属性如下:
// flush操作时进行刷新的ByteBuf
private Entry flushedEntry;
// 还未刷新的ByteBuf
private Entry unflushedEntry;
// 始终指向最后一个Entry
private Entry tailEntry;
// 还未写入的刷新Entry对象数量
private int flushed;
此时尾部结点tailEntry是为空的,所以,新创建的entry就放到了尾部作为tailEntry。unflushedEntry结点为entry。
然后incrementPendingOutboundBytes调整buffer的大小size。
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);
}
}
至此,write写入一个信息就完成了,其实核心就是构造一个entry结点添加到buffer单向链表尾部。
然后继续回到之前invokeWriteAndFlush的invokeFlush0方法,invokeFlush0方法主要功能就两个:
①、将消息标记为待发送。
②、刷新发送缓冲区发送消息
直接进入方法内部。
来到unsafe操作。
@Override
public void flush(ChannelHandlerContext ctx) throws Exception {
unsafe.flush();
}
接着往下走。
@Override
public final void flush() {
assertEventLoop();
ChannelOutboundBuffer outboundBuffer = this.outboundBuffer;
if (outboundBuffer == null) {
return;
}
//消息标记为flushed
outboundBuffer.addFlush();
flush0();
}
进入addFlush方法。
public void addFlush() {
// There is no need to process all entries if there was already a flush before and no new messages
// where added in the meantime.
//
// See https://github.com/netty/netty/issues/2577
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()) {
// 已取消,因此请确保我们释放内存并通知释放的字节
int pending = entry.cancel();
decrementPendingOutboundBytes(pending, false, true);
}
entry = entry.next;
} while (entry != null);
// 所有已刷新,因此重置unflushedEntry
unflushedEntry = null;
}
}
将Entry entry = unflushedEntry意思是如果之前已经有刷新并且在此期间没有添加新消息,则无需处理所有entry,然后通过一个循环flushed记录还未刷新的entry数量。所有已刷新,因此重置unflushedEntry。
然后返回flush0,进入方法内部。
protected void flush0() {
if (inFlush0) {
// 避免重复进入
return;
}
// 没有数据,直接返回
final ChannelOutboundBuffer outboundBuffer = this.outboundBuffer;
if (outboundBuffer == null || outboundBuffer.isEmpty()) {
return;
}
//正在刷新数据
inFlush0 = true;
//如果通道处于非活动状态,则将所有请求标记为失败。
if (!isActive()) {
try {
//Channel已经打开
if (isOpen()) {
outboundBuffer.failFlushed(FLUSH0_NOT_YET_CONNECTED_EXCEPTION, true);
} else {
// 不要触发channelWritabilityChanged,因为通道已经关闭
outboundBuffer.failFlushed(FLUSH0_CLOSED_CHANNEL_EXCEPTION, false);
}
} finally {
inFlush0 = false;
}
return;
}
try {
//将输出缓冲区中的数据通过socket传输
doWrite(outboundBuffer);
} catch (Throwable t) {
if (t instanceof IOException && config().isAutoClose()) {
/**
* Just call {@link #close(ChannelPromise, Throwable, boolean)} here which will take care of
* failing all flushed messages and also ensure the actual close of the underlying transport
* will happen before the promises are notified.
*
* This is needed as otherwise {@link #isActive()} , {@link #isOpen()} and {@link #isWritable()}
* may still return {@code true} even if the channel should be closed as result of the exception.
*/
close(voidPromise(), t, FLUSH0_CLOSED_CHANNEL_EXCEPTION, false);
} else {
outboundBuffer.failFlushed(t, true);
}
} finally {
inFlush0 = false;
}
}
进入到核心逻辑doWrite(outboundBuffer)。
@Override
protected void doWrite(ChannelOutboundBuffer in) throws Exception {
for (;;) {
int size = in.size();
if (size == 0) {
// All written so clear OP_WRITE
clearOpWrite();
break;
}
long writtenBytes = 0;
boolean done = false;
boolean setOpWrite = false;
ByteBuffer[] nioBuffers = in.nioBuffers();
int nioBufferCnt = in.nioBufferCount();
long expectedWrittenBytes = in.nioBufferSize();
SocketChannel ch = javaChannel();
// Always us nioBuffers() to workaround data-corruption.
// See https://github.com/netty/netty/issues/2761
switch (nioBufferCnt) {
case 0:
// 除了ByteBuffers之外,其他东西
super.doWrite(in);
return;
case 1:
// 只有一个ByteBuf,非聚集写入
ByteBuffer nioBuffer = nioBuffers[0];
for (int i = config().getWriteSpinCount() - 1; i >= 0; i --) {
final int localWrittenBytes = ch.write(nioBuffer);
if (localWrittenBytes == 0) {
setOpWrite = true;
break;
}
expectedWrittenBytes -= localWrittenBytes;
writtenBytes += localWrittenBytes;
if (expectedWrittenBytes == 0) {
done = true;
break;
}
}
break;
default:
// 多个ByteBuf,因此使用聚集写入 config().getWriteSpinCount()=16
for (int i = config().getWriteSpinCount() - 1; i >= 0; i --) {
final long localWrittenBytes = ch.write(nioBuffers, 0, nioBufferCnt);
if (localWrittenBytes == 0) {
setOpWrite = true;
break;
}
expectedWrittenBytes -= localWrittenBytes;
writtenBytes += localWrittenBytes;
//如果16次写入之后还有数据没有写入,那么done就位false
if (expectedWrittenBytes == 0) {
done = true;
break;
}
}
break;
}
// 释放完全写入的缓冲区,并更新部分写入的缓冲区的索引。
in.removeBytes(writtenBytes);
if (!done) {
//没有完全写入所有缓冲区。
incompleteWrite(setOpWrite);
break;
}
}
}
首先size方法(flushed的大小)获取到ChannelOutboundBuffer的未刷新的entry数量,如果为空,说明所有的都应刷新,那么就清空写事件clearOpWrite。writtenBytes为本次循环已经写出的字节数,done标识本次循环是否写出了所有待写出的数据,setOpWrite标识是否需要设置SelectionKey.OP_WRITE事件,in.nioBuffers确保待写的写操作仅有ByteBufs
public ByteBuffer[] nioBuffers() {
long nioBufferSize = 0;
int nioBufferCount = 0;
final InternalThreadLocalMap threadLocalMap = InternalThreadLocalMap.get();
ByteBuffer[] nioBuffers = NIO_BUFFERS.get(threadLocalMap);
Entry entry = flushedEntry;
while (isFlushedEntry(entry) && entry.msg instanceof ByteBuf) {
if (!entry.cancelled) {
ByteBuf buf = (ByteBuf) entry.msg;
final int readerIndex = buf.readerIndex();
final int readableBytes = buf.writerIndex() - readerIndex;
if (readableBytes > 0) {
if (Integer.MAX_VALUE - readableBytes < nioBufferSize) {
// If the nioBufferSize + readableBytes will overflow an Integer we stop populate the
// ByteBuffer array. This is done as bsd/osx don't allow to write more bytes then
// Integer.MAX_VALUE with one writev(...) call and so will return 'EINVAL', which will
// raise an IOException. On Linux it may work depending on the
// architecture and kernel but to be safe we also enforce the limit here.
// This said writing more the Integer.MAX_VALUE is not a good idea anyway.
//
// See also:
// - https://www.freebsd.org/cgi/man.cgi?query=write&sektion=2
// - http://linux.die.net/man/2/writev
break;
}
nioBufferSize += readableBytes;
int count = entry.count;
if (count == -1) {
//noinspection ConstantValueVariableUse
entry.count = count = buf.nioBufferCount();
}
int neededSpace = nioBufferCount + count;
if (neededSpace > nioBuffers.length) {
nioBuffers = expandNioBufferArray(nioBuffers, neededSpace, nioBufferCount);
NIO_BUFFERS.set(threadLocalMap, nioBuffers);
}
if (count == 1) {
ByteBuffer nioBuf = entry.buf;
if (nioBuf == null) {
// cache ByteBuffer as it may need to create a new ByteBuffer instance if its a
// derived buffer
entry.buf = nioBuf = buf.internalNioBuffer(readerIndex, readableBytes);
}
nioBuffers[nioBufferCount ++] = nioBuf;
} else {
ByteBuffer[] nioBufs = entry.bufs;
if (nioBufs == null) {
// cached ByteBuffers as they may be expensive to create in terms
// of Object allocation
entry.bufs = nioBufs = buf.nioBuffers();
}
nioBufferCount = fillBufferArray(nioBufs, nioBuffers, nioBufferCount);
}
}
}
entry = entry.next;
}
this.nioBufferCount = nioBufferCount;
this.nioBufferSize = nioBufferSize;
return nioBuffers;
}
此方法获取所有待写出的ByteBuf,然后构造出一个个ByteBuffer,因为底层还是利用jdk的ByteBuffer进行网络传输的,最终汇总成一个ByteBuffer数组进行返回。nioBufferCount为本次循环需要写出的ByteBuffer个数,然后对nioBufferCnt进行匹配。
如果为零,除了ByteBuffer,我们还有其他东西要写,所以执行doWrite(in)去写。
@Override
protected void doWrite(ChannelOutboundBuffer in) throws Exception {
int writeSpinCount = -1;
boolean setOpWrite = false;
for (;;) {
Object msg = in.current();
if (msg == null) {
// Wrote all messages.
clearOpWrite();
// Directly return here so incompleteWrite(...) is not called.
return;
}
if (msg instanceof ByteBuf) {
ByteBuf buf = (ByteBuf) msg;
int readableBytes = buf.readableBytes();
if (readableBytes == 0) {
in.remove();
continue;
}
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 the loop and so incompleteWrite(...) is called.
break;
}
} else if (msg instanceof FileRegion) {
FileRegion region = (FileRegion) msg;
boolean done = region.transferred() >= region.count();
if (!done) {
long flushedAmount = 0;
if (writeSpinCount == -1) {
writeSpinCount = config().getWriteSpinCount();
}
for (int i = writeSpinCount - 1; i >= 0; i--) {
long localFlushedAmount = doWriteFileRegion(region);
if (localFlushedAmount == 0) {
setOpWrite = true;
break;
}
flushedAmount += localFlushedAmount;
if (region.transferred() >= region.count()) {
done = true;
break;
}
}
in.progress(flushedAmount);
}
if (done) {
in.remove();
} else {
// Break the loop and so incompleteWrite(...) is called.
break;
}
} else {
// Should not reach here.
throw new Error();
}
}
incompleteWrite(setOpWrite);
}
在doWrite中核心逻辑我们看doWriteBytes(buf)和doWriteFileRegion(region)两种方式,进入到方法内部会发现其实底层还是利用了JavaChannel进行数据写入。
@Override
protected int doWriteBytes(ByteBuf buf) throws Exception {
final int expectedWrittenBytes = buf.readableBytes();
return buf.readBytes(javaChannel(), expectedWrittenBytes);
}
@Override
protected long doWriteFileRegion(FileRegion region) throws Exception {
final long position = region.transferred();
return region.transferTo(javaChannel(), position);
}
返回来如果nioBufferCnt为1,那么就使用费非聚集写入,如果大于1,就使用循环进行写入,最终释放完全写入的缓冲区,并更新部分写入的缓冲区的索引。
最终如果经过循环之后还有数据剩余,那么就包装成task交给eventLoop进行处理。
protected final void incompleteWrite(boolean setOpWrite) {
// Did not write completely.
if (setOpWrite) {
setOpWrite();
} else {
// Schedule flush again later so other tasks can be picked up in the meantime
Runnable flushTask = this.flushTask;
if (flushTask == null) {
flushTask = this.flushTask = new Runnable() {
@Override
public void run() {
flush();
}
};
}
eventLoop().execute(flushTask);
}
}
个人才疏学浅、信手涂鸦,netty框架更多模块解读相关源码持续更新中,感兴趣的朋友请移步至个人公众号,谢谢支持😜😜......
公众号:wenyixicodedog