寫數據是NIO Channel實現的另一個比較複雜的功能。每一個channel都有一個outboundBuffer,這是一個輸出緩衝區。當調用channel的write方法寫數據時,這個數據被一系列ChannelOutboundHandler處理之後,它被放進這個緩衝區中,並沒有真正把數據寫到socket channel中。然後再調用channel的flush方法,flush會把outboundBuffer中數據真正寫到socket channel。正常情況下flush之後,數據已經真正寫完了。但使用Selector加非阻塞socket的方式寫數據,讓寫操作變得複雜了。操作系統爲每個socket維護了一個數據發送緩衝區,它的長度SO_SNDBUF, 每次發送數據,先把數據寫到這個緩衝區中,操作系統負責把這個發送緩衝區中的數據發送出去,並清理這個緩衝區。當向緩衝區寫的速率大於系統的發送速率時,它會被填滿,在非阻塞模式下的表現爲: 調用socket的write方法寫入長度爲n數據,實際寫入的數據長度m的範圍是:0=<m<n。這個時候還剩下長度爲n-m的數據沒有寫入到socket,而數據必須以正確的順序完整地寫入到socket中。 outboundBuffer正是爲解決這個問題而設計的,沒寫進socket的剩餘數據會以正確的順序保存在outboundBuffer中,當發送緩衝區中有空間可以寫時,可以從outboundBuffer中取出剩餘的數據繼續寫入到socket中。
Channel write實現: 把數據寫到outboundBuffer中
write調用棧:
1 io.netty.channel.AbstractChannel#write(java.lang.Object)
2 io.netty.channel.DefaultChannelPipeline#write(java.lang.Object)
3 io.netty.channel.AbstractChannelHandlerContext#write(java.lang.Object)
4 io.netty.channel.AbstractChannelHandlerContext#write(java.lang.Object, io.netty.channel.ChannelPromise)
5 io.netty.channel.AbstractChannelHandlerContext#write(java.lang.Object, boolean, io.netty.channel.ChannelPromise)
6 io.netty.channel.AbstractChannelHandlerContext#invokeWrite
7 io.netty.channel.DefaultChannelPipeline.HeadContext#write
8 io.netty.channel.AbstractChannel.AbstractUnsafe#write
write的主要邏輯在io.netty.channel.AbstractChannel.AbstractUnsafe#write中實現,這個方法把要寫的數據msg對象放到outboundBuffer中。在執行close時,netty不希望有希望寫新的數據,避免引起不可預料的錯誤,因此會把outboundBuffer置爲null。這裏在向outboundBuffer寫數據之前會把對它進行檢查,如果是null就拋出錯誤。下面是這個write方法的實現。
1 @Override
2 public final void write(Object msg, ChannelPromise promise) {
3 assertEventLoop();
4
5 ChannelOutboundBuffer outboundBuffer = this.outboundBuffer;
6 if (outboundBuffer == null) {
7 safeSetFailure(promise, WRITE_CLOSED_CHANNEL_EXCEPTION);
8 ReferenceCountUtil.release(msg);
9 return;
10 }
11
12 int size;
13 try {
14 msg = filterOutboundMessage(msg);
15 size = pipeline.estimatorHandle().size(msg);
16 if (size < 0) {
17 size = 0;
18 }
19 } catch (Throwable t) {
20 safeSetFailure(promise, t);
21 ReferenceCountUtil.release(msg);
22 return;
23 }
24
25 outboundBuffer.addMessage(msg, size, promise);
26 }
第5-9行,對outboudBuffer進行檢查,如果是null拋出錯誤。這個裏有個小細節,用一個局部變量引用outboundBuffer,避免由其他線程對this.outboundBuffer置空引發錯誤。
14行,調用filterOutboundMessage對msg進行過濾。這是一個protected方法,默認實現是什麼都沒做,返回輸入的msg參數。子類可以覆蓋這個方法,把msg轉換成期望的類型。
15行,計算msg的長度。
25行,把放入到outboundBuffer中。
Channel flush實現:把數據真正寫到channel
flush調用棧:
1 io.netty.channel.AbstractChannel#flush
2 io.netty.channel.DefaultChannelPipeline#flush
3 io.netty.channel.AbstractChannelHandlerContext#flush
4 io.netty.channel.AbstractChannelHandlerContext#invokeFlush
5 io.netty.channel.DefaultChannelPipeline.HeadContext#flush
6 io.netty.channel.AbstractChannel.AbstractUnsafe#flush
7 io.netty.channel.AbstractChannel.AbstractUnsafe#flush0
8 io.netty.channel.socket.nio.NioSocketChannel#doWrite
9 io.netty.channel.nio.AbstractNioByteChannel#doWrite
10 io.netty.channel.socket.nio.NioSocketChannel#doWriteBytes
以上是io.netty.channel.socket.nio.NioSocketChannel的flush調用棧,對於io.netty.channel.socket.nio.NioDatagramChannel來說,從第8行開始變得不同:
7 io.netty.channel.AbstractChannel.AbstractUnsafe#flush0
8 io.netty.channel.nio.AbstractNioMessageChannel#doWrite
9 io.netty.channel.socket.nio.NioDatagramChannel#doWriteMessage
把Byte數據流寫入channel
io.netty.channel.socket.nio.NioSocketChannel#doWrite是Byte數據流的寫邏輯,io.netty.channel.nio.AbstractNioByteChannel#doWrite也是,這兩者不同的地方在於前者是在outboundBuffer可以轉換成java.nio.ByteBuffer的情況下執行,後者是在outboundBuffer中的msg是ByteBuf或FileRegin類型時執行。除此之外其他邏輯都一樣:
- 儘量把outboundBuffer中的數據寫到channel中。
- 如果channel無法寫入數據,在channel的SelectionKey上註冊OP_WRITE事件,等channel可寫的時候再繼續寫入。
- 如寫入次數超過限制,把flush操作包裝成task放到eventLoop排隊,等待再次執行。
下面來看看io.netty.channel.socket.nio.NioSocketChannel#doWrite的實現代碼:
1 @Override
2 protected void doWrite(ChannelOutboundBuffer in) throws Exception {
3 for (;;) {
4 int size = in.size();
5 if (size == 0) {
6 // All written so clear OP_WRITE
7 clearOpWrite();
8 break;
9 }
10 long writtenBytes = 0;
11 boolean done = false;
12 boolean setOpWrite = false;
13
14 // Ensure the pending writes are made of ByteBufs only.
15 ByteBuffer[] nioBuffers = in.nioBuffers();
16 int nioBufferCnt = in.nioBufferCount();
17 long expectedWrittenBytes = in.nioBufferSize();
18 SocketChannel ch = javaChannel();
19
20 // Always us nioBuffers() to workaround data-corruption.
21 // See https://github.com/netty/netty/issues/2761
22 switch (nioBufferCnt) {
23 case 0:
24 // We have something else beside ByteBuffers to write so fallback to normal writes.
25 super.doWrite(in);
26 return;
27 case 1:
28 // Only one ByteBuf so use non-gathering write
29 ByteBuffer nioBuffer = nioBuffers[0];
30 for (int i = config().getWriteSpinCount() - 1; i >= 0; i --) {
31 final int localWrittenBytes = ch.write(nioBuffer);
32 if (localWrittenBytes == 0) {
33 setOpWrite = true;
34 break;
35 }
36 expectedWrittenBytes -= localWrittenBytes;
37 writtenBytes += localWrittenBytes;
38 if (expectedWrittenBytes == 0) {
39 done = true;
40 break;
41 }
42 }
43 break;
44 default:
45 for (int i = config().getWriteSpinCount() - 1; i >= 0; i --) {
46 final long localWrittenBytes = ch.write(nioBuffers, 0, nioBufferCnt);
47 if (localWrittenBytes == 0) {
48 setOpWrite = true;
49 break;
50 }
51 expectedWrittenBytes -= localWrittenBytes;
52 writtenBytes += localWrittenBytes;
53 if (expectedWrittenBytes == 0) {
54 done = true;
55 break;
56 }
57 }
58 break;
59 }
60
61 // Release the fully written buffers, and update the indexes of the partially written buffer.
62 in.removeBytes(writtenBytes);
63
64 if (!done) {
65 // Did not write all buffers completely.
66 incompleteWrite(setOpWrite);
67 break;
68 }
69 }
70 }
第5-7行,如果outboundBuffer中已經沒有數據了,調用clearOpWrite方法清除channel SelectionKey上的OP_WRITE事件。
第15-17行,把outboundBuffer轉換成ByteBuffer類型,並得到數據長度。
25行,outboundBuffer不能轉換成ByteBuffer, 調用io.netty.channel.nio.AbstractNioByteChannel#doWrite執行寫操作。
29-42,45-57的邏輯基本已經,都是儘量把ByteBuffer中的數據寫到channel中,滿足下列條件中的任意一個時,結束本次寫操作:
1. ByteBuffer中的數據已經寫完,正常結束。
2. channel已經不能寫入數據,需要在channel可以寫是繼續執行寫操作。
3. 者超過channel config中寫入次數限制,需要選擇合適的實際繼續執行寫操作。
62行,把已經寫入到channel的數據從outboundBuffer中刪除。
64-66行, 如果數據沒寫完,調用incompleteWrite處理沒寫完的情況。當setOpWrite==true時,在channel的SelectionKey上設置OP_WRITE事件,等eventLoop觸發這個事件時再繼續執行flush操作。否則,把flush包裝成task放到eventLoop中排隊執行。
當NioEventLoop檢測到OP_WRITE事件時,會調用processSelectedKey方法處理:
if ((readyOps & SelectionKey.OP_WRITE) != 0) {
ch.unsafe().forceFlush();
}
forceFlush的調用棧如下:
1 io.netty.channel.nio.AbstractNioChannel.AbstractNioUnsafe#forceFlush
2 io.netty.channel.AbstractChannel.AbstractUnsafe#flush0
3 io.netty.channel.socket.nio.NioSocketChannel#doWrite
4 io.netty.channel.nio.AbstractNioByteChannel#doWrite
5 io.netty.channel.socket.nio.NioSocketChannel#doWriteBytes
把數據寫入UDP類型的channel
io.netty.channel.nio.AbstractNioMessageChannel#doWrite是數據報的寫邏輯。相較於Byte流類型的數據,數據報的寫邏輯簡單一些。它只是把outboundBuffer中的數據報依次寫入到channel中,如果channel寫滿了,在channel的SelectionKey上設置OP_WRITE事件隨後退出,其後OP_WRITE事件處理邏輯和Byte流寫邏輯一樣。 真正的寫操作在io.netty.channel.socket.nio.NioDatagramChannel#doWriteMessage中實現,這個方法的實現如下:
1 @Override
2 protected boolean doWriteMessage(Object msg, ChannelOutboundBuffer in) throws Exception {
3 final SocketAddress remoteAddress;
4 final ByteBuf data;
5 if (msg instanceof AddressedEnvelope) {
6 @SuppressWarnings("unchecked")
7 AddressedEnvelope<ByteBuf, SocketAddress> envelope = (AddressedEnvelope<ByteBuf, SocketAddress>) msg;
8 remoteAddress = envelope.recipient();
9 data = envelope.content();
10 } else {
11 data = (ByteBuf) msg;
12 remoteAddress = null;
13 }
14
15 final int dataLen = data.readableBytes();
16 if (dataLen == 0) {
17 return true;
18 }
19
20 final ByteBuffer nioData = data.internalNioBuffer(data.readerIndex(), dataLen);
21 final int writtenBytes;
22 if (remoteAddress != null) {
23 writtenBytes = javaChannel().send(nioData, remoteAddress);
24 } else {
25 writtenBytes = javaChannel().write(nioData);
26 }
27 return writtenBytes > 0;
28 }
5-9行,處理AddressedEnvelope類型的數據報,得到數據報的遠程地址和數據。
10-12行,發送的是一個ByteBuf。沒有指定遠程地址。這種情況下需要先調用channel的connect方法。
20-26行,分別針對兩種情況發送數據報. 23行指定了遠程地址,25行沒有指定遠程地址,但調用過了connect方法。