1 概述
在Netty中,发送报文和读取报文都是通过Unsafe
处理的,但是说到底发送和读取报文都是从java.nio.channels.SelectableChannel
读取或者向其写报文,而Netty自定义Channel
是对java.nio.channels.SelectableChannel
的封装增强,所以Unsafe
在发送和读取报文最终调用的还是Channel
相关的方法。
我们在Handler中处理报文向Channel
或者AbstractChannelHandlerContext
写数据时(关于Handler
、AbstractChannelHandlerContext
可以参考笔者这篇文章),其实并没有实际向SelectableChannel
中写,也就没有实际写入socket发送缓冲区,而是写入Unsafe
中的缓存ChannelOutboundBuffer
中,只有在调用flush
方法或者注册了OP_WRITE
事件后才会实际向socket缓冲中进行写入操作,这里要注意在NIO编程时一般不会向Channel
注册OP_WRITE
事件,Netty也一样,只会在因网络缓存满写半包之后剩下数据要及时写出去,才会注册OP_WRITE
事件,让Channel
在准备好可以写之后告诉自己继续写剩下的数据(关于OP_WRITE
事件的理解可以参考笔者这篇文章)。
write
和flush
属于Outbound事件,所在最终调用pipeline中head节点(也就是HeadContext
)的write
和flush
方法,HeadContext
的write
和flush
方法如下(有关Outbound事件HeadContext
可以参考笔者这篇文章):
//HeadContext
@Override
public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) throws Exception {
unsafe.write(msg, promise);
}
@Override
public void flush(ChannelHandlerContext ctx) throws Exception {
unsafe.flush();
}
可见最终调用的还是Unsafe
的write
和flush
方法,所以前面的调用我们不再赘述,后面直接介绍Unsafe
的write
和flush
方法。
2 相关类介绍
这里我们主要介绍Unsafe
中的缓存ChannelOutboundBuffer
类定义,ChannelOutboundBuffer
使用单链表组织待写出的数据,链表节点为其内部类Entry
,这里有一个知识点Entry
使用了Netty的对象池技术(可参考笔者这篇文章),下面首先看ChannelOutboundBuffer
的重要域:
//ChannelOutboundBuffer
//ChannelOutboundBuffer采用单链表组织要写出的ByteBuf,
//tailEntry用于指向最后一个加入到链表的节点
// The Entry which represents the tail of the buffer
private Entry tailEntry;
//每次调用addMessage方法向OutboundBuffer追加节点时
//如果unflushedEntry为空则会被置为最后追加的节点
//addMessage是需要判断unflushedEntry为空才置为最后追加的节点,
//而unflushedEntry会在flush方法调用后会置为空
//所以unflushedEntry不是始终指向尾节点,准确的说应该是指向最靠前
//的一个未flushed节点
// The Entry which is the first unflushed in the linked-list structure
private Entry unflushedEntry;
//每次调用flush方法时,都会尝试将当前所有节点写入socket缓存中,
//缓冲满时,则会注册OP_WRITE方法,在channel就绪时继续写,
//但是不管能够一次性写出去,调用flush方法都会将从头结点开始
//到unflushedEntry的所有节点表示为flush状态,flushedEntry置为
//最后未写出的unflushedEntry节点,unflushedEntry则置为空
// The Entry that is the first in the linked-list structure that was flushed
private Entry flushedEntry;
//每次调用flush方法时都可能会将多个节点尝试写入socket
//flushed则记录最后一个调用flush累计打算写的节点个数
// The number of flushed entries that are not written yet
private int flushed;
看完重要域之后,下面再看重要的方法:
//ChannelOutboundBuffer
/**
* Add given message to this {@link ChannelOutboundBuffer}. The given {@link ChannelPromise} will be notified once
* the message was written.
*/
//向链表中添加节点
public void addMessage(Object msg, int size, ChannelPromise promise) {
//新建一个Entry节点实例
Entry entry = Entry.newInstance(msg, size, total(msg), promise);
//如果tailEntry为空,则表示是第一个节点
if (tailEntry == null) {
flushedEntry = null;
tailEntry = entry;
} else {
//否则将新节点追加的链表最后,并更新tailEntry为新的尾节点
Entry tail = tailEntry;
tail.next = entry;
tailEntry = entry;
}
//如果unflushedEntry为空,则更新其为最新的尾节点
if (unflushedEntry == null) {
unflushedEntry = entry;
}
//累积所有缓冲未写出的字节数
// increment pending bytes after adding message to the unflushed arrays.
// See https://github.com/netty/netty/issues/1619
incrementPendingOutboundBytes(entry.pendingSize, false);
}
/**
* Add a flush to this {@link ChannelOutboundBuffer}. This means all previous added messages are marked as flushed
* and so you will be able to handle them.
*/
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;
//判断unflushedEntry是否为空,放置节点个数没有发生变化,
//但是多次调用addFlush方法
if (entry != null) {
//flushedEntry表示当前没有刷新任务,将flushedEntry节点
//赋值为entry
if (flushedEntry == null) {
// there is no flushedEntry yet, so start with the entry
flushedEntry = entry;
}
do {
//递增待刷新节点个数
flushed ++;
//设置该节点对应的promise为不可取消的
//这里使用AtomicReferenceFieldUpdater更新
//如果更新失败,表示已经取消了
if (!entry.promise.setUncancellable()) {
// Was cancelled so make sure we free up memory and notify about the freed bytes
//如果设置失败,则表示在刷新之前已经取消对该节点的
//写操作,所以这里进行取消,然后从所有待写入字节数
//中减去该节点字节数
int pending = entry.cancel();
decrementPendingOutboundBytes(pending, false, true);
}
//获取链表下一个元素
entry = entry.next;
} while (entry != null);
// All flushed so reset unflushedEntry
//因为所有节点都会被flush,所以设置unflushedEntry为空
unflushedEntry = null;
}
}
通过上面的ChannelOutboundBuffer.addFlush
方法实现可知,在写入数据放入ChannelOutboundBuffer
缓存之后如果想取消发送,则必须在addFlush
方法调用之前取消,否则该消息会被设置为不可取消状态。
3 Unsafe.write
如上面的介绍Unsafe.write
没有直接向socket缓冲写入数据,而是将数据写入Unsafe
中的缓存ChannelOutboundBuffer
,直接看AbstractUnsafe.write
源码:
//AbstractUnsafe
//ChannelOutboundBuffer是AbstractUnsafe的一个域,在声明时初始化如下
private volatile ChannelOutboundBuffer outboundBuffer = new ChannelOutboundBuffer(AbstractChannel.this);
//写方法
@Override
public final void write(Object msg, ChannelPromise promise) {
assertEventLoop();
ChannelOutboundBuffer outboundBuffer = this.outboundBuffer;
//outboundBuffer为空,表示初始化失败,这里抛异常
if (outboundBuffer == null) {
// If the outboundBuffer is null we know the channel was closed and so
// need to fail the future right away. If it is not null the handling of the rest
// will be done in flush0()
// See https://github.com/netty/netty/issues/2362
safeSetFailure(promise, WRITE_CLOSED_CHANNEL_EXCEPTION);
// release message now to prevent resource-leak
ReferenceCountUtil.release(msg);
return;
}
int size;
try {
//过滤写出的报文,默认实现实际没有做什么操作,不展开介绍
msg = filterOutboundMessage(msg);
//得出报文的大小,这个后面介绍
size = pipeline.estimatorHandle().size(msg);
//如果小于0的话,赋值为0
if (size < 0) {
size = 0;
}
} catch (Throwable t) {
safeSetFailure(promise, t);
ReferenceCountUtil.release(msg);
return;
}
//将消息添加到缓冲中
outboundBuffer.addMessage(msg, size, promise);
}
上面pipeline.estimatorHandle().size(msg)
默认实现如下源码所示:
//DefaultMessageSizeEstimator.HandleImpl
private static final class HandleImpl implements Handle {
private final int unknownSize;
private HandleImpl(int unknownSize) {
this.unknownSize = unknownSize;
}
//size方法返回的就是ByteBuf的可读字节数
@Override
public int size(Object msg) {
if (msg instanceof ByteBuf) {
return ((ByteBuf) msg).readableBytes();
}
if (msg instanceof ByteBufHolder) {
return ((ByteBufHolder) msg).content().readableBytes();
}
if (msg instanceof FileRegion) {
return 0;
}
return unknownSize;
}
}
4 Unsafe.flush
Unsafe.flush
在AbstractUnsafe
中实现如下:
//AbstractUnsafe
@Override
public final void flush() {
assertEventLoop();
ChannelOutboundBuffer outboundBuffer = this.outboundBuffer;
if (outboundBuffer == null) {
return;
}
//这个会将outboundBuffer所有待写入节点修改为flushing状态
//具体实现在上面第二节已经介绍过
outboundBuffer.addFlush();
flush0();
}
@SuppressWarnings("deprecation")
protected void flush0() {
//inFlush0在进入时被置为true,结束一次flush
//后变为false,这里判断为true直接返回,避免
//正在刷新中又触发调用
if (inFlush0) {
// Avoid re-entrance
return;
}
final ChannelOutboundBuffer outboundBuffer = this.outboundBuffer;
if (outboundBuffer == null || outboundBuffer.isEmpty()) {
return;
}
inFlush0 = true;
//AbstractUnsafe是AbstractChannel的内部类,这个方法是
//AbstractChannel的方法,保证刷新时通道还是有效状态
// Mark all pending write requests as failure if the channel is inactive.
if (!isActive()) {
try {
//同样判断通道状态是否为打开状态
if (isOpen()) {
outboundBuffer.failFlushed(FLUSH0_NOT_YET_CONNECTED_EXCEPTION, true);
} else {
// Do not trigger channelWritabilityChanged because the channel is closed already.
outboundBuffer.failFlushed(FLUSH0_CLOSED_CHANNEL_EXCEPTION, false);
}
} finally {
inFlush0 = false;
}
return;
}
try {
//进行向通道的实际写操作,这里的doWrite也是Channel的方法
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 {
try {
shutdownOutput(voidPromise(), t);
} catch (Throwable t2) {
close(voidPromise(), t2, FLUSH0_CLOSED_CHANNEL_EXCEPTION, false);
}
}
} finally {
inFlush0 = false;
}
}
AbstractUnsafe.flush0
方法中调用的doWrite
其实是外部类AbstractChannel
定义的方法,我们直接看其子类AbstractNioByteChannel
中的实现:
//AbstractNioByteChannel
@Override
protected void doWrite(ChannelOutboundBuffer in) throws Exception {
//可以看下getWriteSpinCount方法的注释
//因为每次写入可能会因为socket缓冲满而写不了数据
//导致写入字节数为0,writeSpinCount就是控制如果
//写入字节数为0,尝试进行多少次写入
//也就是注释所说的:向channel写入返回直到返回非0值
//尝试次数,默认为16,可用使用WRITE_SPIN_COUNT进行配置
//上面的getWriteSpinCount方法注释的含义,但是看代码,每次写成功
//一些数据(写入字节数大于0)节点会递减writeSpinCount的值
//所以writeSpinCount也表示一次flush操作尝试写的次数
int writeSpinCount = config().getWriteSpinCount();
do {
//获取outboundBuffer当前的头结点,进行写入
Object msg = in.current();
//如果返回为空,表示outboundBuffer所有节点
//的数据已经全部发送
if (msg == null) {
//所有数据都已发送,所以取消OP_WRITE事件的注册,
//避免select因write事件返回,但是程序实际上无
//数据要写
// Wrote all messages.
clearOpWrite();
// Directly return here so incompleteWrite(...) is not called.
return;
}
//这里进行实际写操作,如果当前节点in.current写成功
//会从outboundBuffer中删除该节点,并将in.current
//置为后面一个节点
writeSpinCount -= doWriteInternal(in, msg);
//while如果writeSpinCount小于0,则不再进行写入
} while (writeSpinCount > 0);
//如果writeSpinCount<0,则表示没有写入完成,还有数据未写,
//所以会注册OP_WRITE,让channel在可写时主动告诉程序,
//应用会再次进行写尝试
incompleteWrite(writeSpinCount < 0);
}
private int doWriteInternal(ChannelOutboundBuffer in, Object msg) throws Exception {
//如果消息类型为ByteBuf,进入如下分支
if (msg instanceof ByteBuf) {
ByteBuf buf = (ByteBuf) msg;
//该ByteBuf已经没有数据可读,直接返回
//没有进行实际的写操作,所以writeSpinCount不递减
if (!buf.isReadable()) {
in.remove();
return 0;
}
//进行实际的写操作,这里我们后面会列一下NioSocketChannel
//的实现
final int localFlushedAmount = doWriteBytes(buf);
//如果返回值大于0,表示已经写出一部分字节
if (localFlushedAmount > 0) {
//更新写出进度
in.progress(localFlushedAmount);
//如果该ByteBuf没有可读数据,表示该ByteBuf已经全部写完
//把它从outboundBuffer中移除,
//这会将outboundBuffer.current节点指向当前节点后面一个节点
if (!buf.isReadable()) {
in.remove();
}
//进行了实际写操作,所以writeSpinCount递减一
return 1;
}
} else if (msg instanceof FileRegion) {
//和写ByteBuf一样的逻辑,不再介绍
FileRegion region = (FileRegion) msg;
if (region.transferred() >= region.count()) {
in.remove();
return 0;
}
long localFlushedAmount = doWriteFileRegion(region);
if (localFlushedAmount > 0) {
in.progress(localFlushedAmount);
if (region.transferred() >= region.count()) {
in.remove();
}
return 1;
}
} else {
// Should not reach here.
throw new Error();
}
//到这里表示实际写操作返回值小于等于0,可能socket缓冲区已满
//所以这里返回一个特别大的数(2147483647),令doWrite直接
//跳出while循环
return WRITE_STATUS_SNDBUF_FULL;
}
//doWrite中如果writeSpinCount<0,则表示没有写入完成,还有数据未写,
//这里传入true
protected final void incompleteWrite(boolean setOpWrite) {
// Did not write completely.
//如果传入ture,表示上次写入没有完成,还有数据待写,所以注册
//OP_WRITE事件,让通道在准备好写入时告诉自己进行写入
if (setOpWrite) {
setOpWrite();
} else {
// It is possible that we have set the write OP, woken up by NIO because the socket is writable, and then
// use our write quantum. In this case we no longer want to set the write OP because the socket is still
// writable (as far as we know). We will find out next time we attempt to write if the socket is writable
// and set the write OP if necessary.
//表示数据写入完毕,所以取消对OP_WRITE的注册
clearOpWrite();
// Schedule flush again later so other tasks can be picked up in the meantime
//向channel加入一个flushTask,该任务会调用unsafe.flush0方法
eventLoop().execute(flushTask);
}
}
//注册OP_WRITE事件
protected final void setOpWrite() {
final SelectionKey key = selectionKey();
// Check first if the key is still valid as it may be canceled as part of the deregistration
// from the EventLoop
// See https://github.com/netty/netty/issues/2104
if (!key.isValid()) {
return;
}
final int interestOps = key.interestOps();
if ((interestOps & SelectionKey.OP_WRITE) == 0) {
key.interestOps(interestOps | SelectionKey.OP_WRITE);
}
}
//取消注册的OP_WRITE事件
protected final void clearOpWrite() {
final SelectionKey key = selectionKey();
// Check first if the key is still valid as it may be canceled as part of the deregistration
// from the EventLoop
// See https://github.com/netty/netty/issues/2104
if (!key.isValid()) {
return;
}
final int interestOps = key.interestOps();
if ((interestOps & SelectionKey.OP_WRITE) != 0) {
key.interestOps(interestOps & ~SelectionKey.OP_WRITE);
}
}
private final Runnable flushTask = new Runnable() {
@Override
public void run() {
// Calling flush0 directly to ensure we not try to flush messages that were added via write(...) in the
// meantime.
((AbstractNioUnsafe) unsafe()).flush0();
}
};
下面AbstractNioByteChannel.doWriteBytes(ByteBuf buf)我们看下
NioSocketChannel.doWriteBytes(ByteBuf buf)`的实现:
//NioSocketChannel
//可见就是向java.nio.channels.SocketChannel写数据
@Override
protected int doWriteBytes(ByteBuf buf) throws Exception {
final int expectedWrittenBytes = buf.readableBytes();
return buf.readBytes(javaChannel(), expectedWrittenBytes);
}
如果向通道注册了OP_WRITE
事件可以在NioEventLoop.run
方法select
返回后,在processSelectedKey
处理返回的SelectionKey
的代码中会调用Unsafe.forceFlush
private void processSelectedKey(SelectionKey k, AbstractNioChannel ch) {
...
try {
...
// Process OP_WRITE first as we may be able to write some queued buffers and so free memory.
if ((readyOps & SelectionKey.OP_WRITE) != 0) {
// Call forceFlush which will also take care of clear the OP_WRITE once there is nothing left to write
//forceFlush最终会调用Unsafe.flush0方法
ch.unsafe().forceFlush();
}
...
} catch (CancelledKeyException ignored) {
unsafe.close(unsafe.voidPromise());
}
}
最后总结一下,我们在向通道进行写入时,调用write
方法只是将待写入的数据放入ChannelOutboundBuffer
缓冲中,实际向socket进行写出有两个地方触发,第一个就是调用Unsafe.flush
方法;另一个就是发生了部分写,注册OP_WRITE
之后,Selector.select
返回写就绪,也会触发实际socket写操作。