抽絲剝繭NIO

原文地址: https://zybuluo.com/zhangever/note/972295

 

從JDK NIO文檔可以看到,Java將其劃分成了三大塊:Channel,Buffer以及Selector。 
此處輸入圖片的描述 
Channel抽象了IO設備(如網絡/文件);Buffer封裝了對數據的緩衝存儲,Selector則是提供了一種可以以單線程非阻塞的方式,來處理多個連接。 
下面我們圍繞這幾個核心概念,來認識一下NIO.

一 基礎概念

要搞清楚網絡IO,我們需要理解幾個概念。這裏以linux爲例。

1.1 緩衝區

內核緩衝區:

OS會爲每個網絡連接(channel)分配一個發送緩衝區以及接收緩衝區(緩衝區大小通過tcp內核參數指定,詳見下文)。所以一臺服務器能支持多少個連接,必要前提就是內存必須夠大。

網卡緩衝區:

類似內核緩衝區,硬件設備也有緩衝區. 對於網卡來說,也分爲兩個:發送緩衝區以及接收緩衝區。

發送數據的時候,應用通過系統調用把數據寫入到內核緩衝區,驅動程序再把數據從內核緩衝區寫到網卡緩衝區,最後網卡把數據從網卡緩衝區發到網絡上去。在tcp協議棧中,當內核收到對方的ack後,纔會把數據從內核緩衝區中刪除 
接收數據的時候,網卡把數據先放入網卡的緩衝區,然後通過驅動程序把網卡緩衝區的數據copy到內核緩衝區,同時通過中斷通知應用讀取數據。

1.2 數據流

一條典型的數據流如下:

app bufferos bufferether buffernetwork


 
  1. 1. 應用構建一條消息,通過系統調用write,寫入到內核緩衝區
  2. 2. 網卡驅動程序把內核緩衝區的數據copy到網卡緩衝區中
  3. 3. 最後網卡把網卡緩衝區的消息發到網絡上

對於基於jvm的應用來說,如果應用是在堆中產生的消息,還會額外多一次堆內內存到堆外內存的copy

app heap bufferapp heap-off bufferos bufferether buffernetwork

1.3 關鍵的tcp內核參數

SO_BACKLOG:

服務端TCP棧在處理客戶端connect請求的過程中,會維護A/B兩個隊列. 服務端在跟客戶端進行連接握手的時候, 
1. 首先會收到客戶端的SYN時(第一次握手),然後向客戶端發送SYN ACK確認(第二次握手),TCP內核模塊把客戶端連接加入到A隊列中, 
2. 然後服務器接收到客戶端發送的ACK時(第三次握手),TCP內核模塊把客戶端連接從A隊列移動到B隊列,連接完成, 
3. 這時,應用程序的accept調用會返回。也就是說accept從B隊列中取出完成了三次握手的連接。 
B隊列的長度就是SO_BACKLOG,當B隊列長度已經等於SO_BACKLOG時,新的連接將會被TCP內核拒絕。 
需要注意的是, 有一個內核參數net.core.somaxconn是B隊列長度的最大值(默認長度爲128),可見實際的B隊列長度應該=min(SO_BACKLOG, somaxconn) 
所以,如果backlog過小,可能會出現accept速度跟不上,A、B隊列滿了,導致新的客戶端無法連接。要注意的是,backlog對應用支持的連接數並無影響,backlog影響的只是還沒有被accept取出的連接(連接數很大程度上取決於服務器的內存大小) 
服務端處理客戶端連接建立的過程, 可參考佔小狼的博文

緩衝區參數:

TCP內核爲每個連接分配的緩衝區大小默認分別爲這兩個內核參數值:net.ipv4.tcp_rmem/net.ipv4.tcp_wmem.

在Linux下,我們可以通過如下指令查看:


 
  1. # cat /proc/sys/net/ipv4/tcp_rmem
  2. 4096 87380 6291456

以上三個值分別爲最小值/默認值/最大值. 
其中默認值以及最大值又分別給net.core.rmem_default以及net.core.rmem_max覆蓋. 
應用程序可通過SO_RCVBUF/SO_SNDBUF來分別修改接收/發送緩衝區大小(但不能超過內核指定的最大值以及最小值)

1.4 問題

  1. 在Java IO操作中,爲何使用堆外內存(heap-off)會比堆內存高效?

    一般來說,申請堆內存比申請堆外內存更快。 
    但是,如上所述,當發生IO操作的時候,數據需從堆內複製到堆外,再把數據從用戶態複製到核心態,相當於做了兩次copy;而使用堆外內存的話,就只需要做一次copy(從用戶態到核心態的copy). 
    具體代碼可參看java.net.SocketOutputStream對應的native代碼

  2. 爲何數據在經過IO的時候,需要兩次copy?不能直接把堆內存數據直接傳到內核麼?

    簡單來說,read/write等系統調用,需要傳入buffer的地址。然而heapBuffer的話,由於GC的存在,地址會發生移動而heap-off不會. 更詳細解釋可參考知乎R大的解釋.

  3. 服務端對客戶端的連接,設置接收緩衝區大小爲10(socketChannel.setOption(StandardSocketOptions.SO_RCVBUF, 10)),然後客戶端發送長度爲100的字符串過來,結果會如何?

    服務端將會正常的接收到所有字符. 
    這個設置實際上並沒有產生預期結果. 
    在上述語句執行前, 該channel的接收緩衝區大小爲net.core.rmem_default(此值優先)或者net.ipv4.tcp_rmem的第二個值; 
    執行後,由於設置的值小於內核允許的最小值(net.ipv4.tcp_rmem的第一個值),最終該channel的接收緩衝區大小設置爲內核允許的最小值.在本人機器上,該值爲1024.

  4. 續4, 客戶端發送長度爲1500的字符串過來,已經超過了設置的接收緩衝區大小,結果會如何?會有消息丟失嗎?

    服務端會觸發2次可讀事件,第一次讀了1024個字符,第二次讀了476個字符,且消息不會丟失. 
    前面我們在說緩衝區的時候說到,除了有內核緩衝區,還會有一個硬件設備的緩衝區(這裏是網卡的緩衝區).

問題3/4可在3.1小節的NIO簡單模式中驗證.

二 NIO的底層實現機制

高性能的非阻塞IO的實現,依賴一個叫Selector的牛逼貨,該組件也稱IO多路複用器,多個網絡連接可共用一個selector,從而實現單線程處理多個客戶端連接的目的(傳統的阻塞式IO,通常是一個連接需要一個線程去處理).

selector的機制如下:

  1. 系統在內核中創建一個selector, 對於不同客戶端的連接(也就是NIO中的socketChannel),都可以被註冊到同一個selector上. selector通過數組或者鏈表結構維護這衆多的channel(在內核中表現爲文件句柄).
  2. 當channel狀態發生變化的時候(例如接收緩衝區收到數據會觸發可讀事件,發送緩衝區空閒會觸發可寫事件等),該channel會被selector打上ready的標記.
  3. 當應用程序調用selector.select()方法的時候,selector會輪詢其所管轄的channel,把就緒的channel放到selectedKeys中.
  4. 應用遍歷selectedKeys,並對每個channel的事件進行處理.

selector的底層實現有三種方式,分別使用select/poll/epoll系統調用.在內核2.6+的Linux上, Java使用的是epoll. 
下面是三種實現的對比

實現 說明 性能
select 需要在用戶態跟核心態之間相互copy大量文件句柄,文件句柄採用數組結構,數組大小跟內核參數有關 一般來說,少於1024個句柄下,性能優異;但由於採用盲輪詢,句柄越多性能越差
poll 跟select沒有本質差別,不同之處在於,文件句柄採用的是鏈表結構,理論上沒限制數量 跟select一致
epoll 採用事件回調機制,只copy有效的文件句柄 性能優越,但如果偵聽的連接數不多的話(例如少於1024),性能反而沒有select/poll高

下面給三段僞代碼說明上述三種機制,摘自知乎藍形參的回覆:

1 簡單粗暴模式(非阻塞忙輪詢)
所謂的忙,就是說這個機制永遠在盲目的輪詢


 
  1. while true {
  2. for channel in channels[] {
  3. if channel has data
  4. handle(channel)
  5. }
  6. }

2 select/poll模式
首先通過select進入阻塞.當有IO事件的時候,從阻塞態中醒來. 
是有目的的輪詢,複雜度爲O(n)


 
  1. while true {
  2. select(channels[])
  3. for channel in channels[] {
  4. if channel has data
  5. handle(channel)
  6. }
  7. }

3 epoll模式
首先也是通過select進入阻塞.當有IO事件的時候,從阻塞態中醒來,並返回有IO事件發生的channel. 
複雜度爲O(1)


 
  1. while true {
  2. selected_channels[] = select(channels[])
  3. for channel in selected_channels[] {
  4. handle(channel)
  5. }
  6. }

最後再提一下epoll的LT模式以及ET模式

LT模式:level-trigger,水平觸發模式,當某channel處於某種狀態下(例如可讀或者可寫),如果關注了該事件,那麼每次調用select都會返回該channel. 
ET模式:edge-trigger,邊緣觸發模式,某channel只有在發生狀態變化的時候,纔會在select的時候返回該channel. 
JDK的nio使用LT模式,而netty使用ET模式. 
所以, 在JDK的原生NIO接口中, 我們可以在響應讀事件的時候讀一次就跑,反正如果沒讀完下次select會繼續讀.但是在netty中, 響應讀事件的時候必須要一次讀光緩衝區的內容.

三 經典的NIO使用模式

Talk is cheap, show the code. 
下面從簡單到複雜介紹兩種nio的使用模式.

3.1 簡單模式

下面的代碼展示了一個使用了NIO的服務端應用. 目的是瞭解一下NIO的相關API.


 
  1. package nio.demo;
  2.  
  3. import java.io.IOException;
  4. import java.net.InetSocketAddress;
  5. import java.net.SocketOptions;
  6. import java.net.StandardSocketOptions;
  7. import java.nio.ByteBuffer;
  8. import java.nio.channels.*;
  9. import java.util.Iterator;
  10.  
  11. /**
  12. * Created by ever on 2017/11/24.
  13. */
  14. public class ServerDemo {
  15. private static final int BUF_SIZE = 1024;
  16. private static final int PORT = 8070;
  17. private static final int TIMEOUT = 3000;
  18.  
  19. public static void main(String[] args) throws IOException, InterruptedException {
  20. selector();
  21. }
  22.  
  23. /**
  24. * 初始化selector以及ServerSocketChannel
  25. * @throws IOException
  26. * @throws InterruptedException
  27. */
  28. private static void selector() throws IOException, InterruptedException {
  29. ServerSocketChannel ssc = ServerSocketChannel.open();
  30. Selector selector = Selector.open();
  31.  
  32. ssc.socket().bind(new InetSocketAddress(PORT));
  33. ssc.configureBlocking(false);
  34.  
  35. ssc.register(selector, SelectionKey.OP_ACCEPT);
  36.  
  37. // 開始輪詢直至天荒地老
  38. while (true) {
  39. if (selector.select(TIMEOUT) == 0) {
  40. System.out.println("No io events found");
  41. continue;
  42. }
  43.  
  44. Iterator<SelectionKey> iter = selector.selectedKeys().iterator();
  45. while (iter.hasNext()) {
  46. SelectionKey key = iter.next();
  47.  
  48. if (key.isAcceptable()) {
  49. handleAccept(key);
  50. }
  51. if (key.isWritable() && key.isValid()) {
  52. handleWrite(key);
  53. }
  54. if (key.isReadable()) {
  55. handleRead(key);
  56. }
  57. iter.remove();
  58. }
  59. }
  60.  
  61. }
  62.  
  63. private static void handleAccept(SelectionKey key) throws IOException {
  64. System.out.println("Handle accept event");
  65. ServerSocketChannel ssc = (ServerSocketChannel) key.channel();
  66. SocketChannel sc = ssc.accept();
  67. sc.configureBlocking(false);
  68. //設置輸入緩衝區的大小, 參看1.4小節問題3/4
  69. sc.setOption(StandardSocketOptions.SO_RCVBUF, 10);
  70. sc.register(key.selector(), SelectionKey.OP_READ);
  71. }
  72.  
  73. /**
  74. * @param key
  75. * @throws IOException
  76. * @throws InterruptedException
  77. */
  78. private static void handleWrite(SelectionKey key) throws IOException, InterruptedException {
  79. System.out.println("Handle write event");
  80. SocketChannel sc = (SocketChannel) key.channel();
  81. ByteBuffer buffer = (ByteBuffer)key.attachment();
  82. if (buffer == null) {
  83. ByteBuffer.allocate(BUF_SIZE);
  84. buffer.put("return from server".getBytes());
  85. buffer.flip();
  86. key.attach(buffer);
  87. }
  88.  
  89. int writes = sc.write(buffer);
  90. System.out.println("write bytes:" + writes);
  91. System.out.println("remain:" + buffer.remaining());
  92. if (buffer.remaining() == 0) {
  93. key.attach(null);
  94. key.interestOps(SelectionKey.OP_READ);
  95. }
  96. }
  97.  
  98. private static void handleRead(SelectionKey key) throws IOException {
  99. System.out.println("Handle read event");
  100. SocketChannel sc = (SocketChannel) key.channel();
  101. ByteBuffer buffer = ByteBuffer.allocate(BUF_SIZE);
  102. long bytesRead = sc.read(buffer);
  103. System.out.println("read:" + bytesRead);
  104. if (bytesRead == -1) {
  105. System.out.println("Peer closed");
  106. sc.close();
  107. System.exit(0);
  108. }
  109. // 輸出內容
  110. if (bytesRead > 0) {
  111. buffer.flip();
  112. while (buffer.hasRemaining()) {
  113. System.out.print((char) buffer.get());
  114. }
  115. }
  116.  
  117. // won't have any effect this time, and will take effect after next select() invoked
  118. key.interestOps(SelectionKey.OP_WRITE|SelectionKey.OP_READ);
  119. }
  120. }

3.1.1 問題

  1. 假設在handleWrite方法中往客戶端寫入超過內核輸出緩衝區大小的字節(假設內核的輸出緩衝區大小爲1024),會有何現象?

    這時會觸發多次OP_WRITE事件,直至buffer中的數據全部寫入內核緩衝區.

  2. 如果在handleAccept中,爲新連接註冊OP_CONNECT事件, 會有何後果?

    服務端將會陷入空轉,select方法的timeout參數看似失效, 馬上返回0,從而進入第40行,不斷輸出"===", 同時CPU飆升至100%. 
    隔壁家老司機老王分析後認爲是jdk跟內核關於OP_CONNECT事件的翻譯機制出了問題.內核認爲OP_CONNECT是可寫事件,所以每次調用select都會馬上返回;然而jdk這時候只認OP_CONNECT. 
    可以理解爲JDK的一個bug, 在netty中對該事件會做特殊處理:High CPU usage with NioEventLoop,相關處理代碼

  3. 如果註釋掉49行,也就是不處理accept事件,那麼這個客戶端的連接處於什麼狀態呢?能正常讀寫麼?

    由於已經觸發了OP_ACCEPT事件,說明TCP的三次握手已經完成,連接已經建立(見1.3 SO_BACKLOG內核參數的解釋).但是由於服務端對這個連接沒有註冊讀寫事件,實際上並不能有效跟客戶端通信. 
    同時,由於沒有完成accept操作,這個連接始終留在b隊列中,從而反覆觸發OP_ACCEPT事件.

3.2 Reactor模式

這裏主要參考Doug Lee在講解NIO API時關於Reactor模型的描述. 
事實上這個模式也應用於很多開源框架中,例如Netty

3.2.1 單線程Reactor模式

用單條線程來處理IO事件以及業務邏輯.


 
  1. public class Reactor implements Runnable {
  2. final Selector selector;
  3. final ServerSocketChannel serverSocket;
  4.  
  5. Reactor(int port) throws IOException {
  6. selector = Selector.open();
  7. serverSocket = ServerSocketChannel.open();
  8. serverSocket.socket().bind(
  9. new InetSocketAddress(port));
  10. serverSocket.configureBlocking(false);
  11. SelectionKey sk =
  12. serverSocket.register(selector,
  13. SelectionKey.OP_ACCEPT);
  14. sk.attach(new Acceptor());
  15. }
  16.  
  17. public void run() { // normally in a new Thread
  18. try {
  19. while (!Thread.interrupted()) {
  20. selector.select();
  21. Set selected = selector.selectedKeys();
  22. Iterator it = selected.iterator();
  23. while (it.hasNext())
  24. dispatch((SelectionKey) (it.next()));
  25. selected.clear();
  26. }
  27. } catch (IOException ex) { /* ... */ }
  28. }
  29.  
  30. void dispatch(SelectionKey k) {
  31. Runnable r = (Runnable) (k.attachment());
  32. if (r != null)
  33. r.run();
  34. }
  35.  
  36. class Acceptor implements Runnable { // inner
  37. public void run() {
  38. try {
  39. SocketChannel channel = serverSocket.accept();
  40. if (channel != null)
  41. new Handler(selector, channel);
  42. } catch (IOException ex) { /* ... */ }
  43. }
  44. }
  45. }
  46.  
  47. final class Handler implements Runnable {
  48. private static final int MAXIN = 1024;
  49. private static final int MAXOUT = 1024;
  50. final SocketChannel socket;
  51. final SelectionKey sk;
  52. ByteBuffer input = ByteBuffer.allocate(MAXIN);
  53. ByteBuffer output = ByteBuffer.allocate(MAXOUT);
  54. static final int READING = 0, SENDING = 1;
  55. int state = READING;
  56.  
  57. Handler(Selector sel, SocketChannel c)
  58. throws IOException {
  59. socket = c;
  60. c.configureBlocking(false);
  61. // Optionally try first read now
  62. sk = socket.register(sel, 0);
  63. sk.attach(this);
  64. sk.interestOps(SelectionKey.OP_READ);
  65. sel.wakeup();
  66. }
  67.  
  68. boolean inputIsComplete() { /* ... */
  69. return true;
  70. }
  71.  
  72. boolean outputIsComplete() { /* ... */
  73. return true;
  74. }
  75.  
  76. void process() { /* ... */ }
  77.  
  78. public void run() {
  79. try {
  80. if (state == READING) read();
  81. else if (state == SENDING) send();
  82. } catch (IOException ex) { /* ... */ }
  83. }
  84.  
  85. void read() throws IOException {
  86. socket.read(input);
  87. if (inputIsComplete()) {
  88. process();
  89. state = SENDING;
  90. // Normally also do first write now
  91. sk.interestOps(SelectionKey.OP_WRITE);
  92. }
  93. }
  94.  
  95. void send() throws IOException {
  96. socket.write(output);
  97. if (outputIsComplete()) sk.cancel();
  98. }
  99. }

單線程Reactor模型示意圖如下: 
此處輸入圖片的描述
主要流程: 
1. Reactor啓動ServerSocket ss以及Selector, 並把ss對應的selectionKey綁定Acceptor作爲ss上事件的處理器. 
2. Reactor在run方法裏面進入無限循環,輪詢Selector上的IO事件. 當有事件發生的時候,就通過dispatch分發出去 
3. Acceptor負責處理ss的accept事件,併產生跟客戶端的socket連接 s,同時把s註冊到Selector上(關注READ事件,同時也關聯了一個處理讀寫的Handler)

3.2.2 多線程Reactor模型

單線程模式下, IO線程既要處理IO事件也要處理業務邏輯. 當業務邏輯比較複雜序號比較大的耗時的時候,就會嚴重影響系統的吞吐量. 
因而,IO線程跟業務邏輯線程分離是一個自然而然的設計. 
此處輸入圖片的描述
如上圖, 這裏是用一個線程處理IO,然後通過線程池來處理業務邏輯.

3.2.3 主從Reactor模型

主從Reactor模型是一種非常流行的高性能NIO模型.在多線程Reactor模型的基礎上, 分裂成2個Reactor,其中mainReactor主要用於處理客戶端連接事件,subReactor主要處理已建立連接後的客戶端讀寫事件. 
此處輸入圖片的描述

四 Netty的IO以及線程模型

Netty的線程模型比較靈活,通過配置可以實現第三節所說的單線程/多線程模型,而官方推薦的是3.2.3中描述的主從Reactor模型,但又有所區別: 
此處輸入圖片的描述
其中, mainReactor叫Boss, subReactor叫Worker.

下圖從更貼近代碼的角度看這個IO模型(原諒我做一次伸手黨,下面是來自佔小狼簡書的配圖): 
此處輸入圖片的描述

這塊佔小狼對Netty的相關分析相當到位,自感無法超越,大家請移玉步.

這裏主要以學習Netty時的思考問題爲主. 
首先,給出一個經典的服務端代碼:


 
  1. public class EchoServer {
  2. private final int port;
  3.  
  4. public EchoServer(int port) {
  5. this.port = port;
  6. }
  7.  
  8. public void run() throws Exception {
  9. // Configure the server.
  10. // 1. 創建EventLoopGroup, 每個group有若干個EventLoop,每個EventLoop有一個selector
  11. EventLoopGroup bossGroup = new NioEventLoopGroup(); // (1)
  12. EventLoopGroup workerGroup = new NioEventLoopGroup();
  13. try {
  14. ServerBootstrap b = new ServerBootstrap(); // (2)
  15. b.group(bossGroup, workerGroup)
  16. .channel(NioServerSocketChannel.class) // (3)
  17. .option(ChannelOption.SO_BACKLOG, 100)
  18. .handler(new LoggingHandler(LogLevel.INFO))
  19. .childHandler(new ChannelInitializer<SocketChannel>() { // (4)
  20. @Override
  21. public void initChannel(SocketChannel ch) throws Exception {
  22. ch.pipeline().addLast(
  23. new EchoServerHandler());
  24. }
  25. });
  26.  
  27. // Start the server.
  28. ChannelFuture f = b.bind(port).sync(); // (5)
  29.  
  30. // Wait until the server socket is closed.
  31. f.channel().closeFuture().sync();
  32. } finally {
  33. // Shut down all event loops to terminate all threads.
  34. bossGroup.shutdownGracefully();
  35. workerGroup.shutdownGracefully();
  36. }
  37. }
  38.  
  39. public static void main(String[] args) throws Exception {
  40. int port;
  41. if (args.length > 0) {
  42. port = Integer.parseInt(args[0]);
  43. } else {
  44. port = 8090;
  45. }
  46. new EchoServer(port).run();
  47. }
  48. }
  49.  
  50. class EchoServerHandler extends ChannelInboundHandlerAdapter {
  51.  
  52. private static final Logger logger = Logger.getLogger(
  53. EchoServerHandler.class.getName());
  54.  
  55. @Override
  56. public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
  57. logger.log(Level.WARNING, "channelRead:" + msg);
  58. ctx.writeAndFlush(msg);
  59. }
  60.  
  61. @Override
  62. public void channelReadComplete(ChannelHandlerContext ctx) throws Exception {
  63. logger.log(Level.WARNING, "channelReadComplete");
  64. }
  65.  
  66. @Override
  67. public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
  68. // Close the connection when an exception is raised.
  69. logger.log(Level.WARNING, "Unexpected exception from downstream.", cause);
  70. ctx.close();
  71. }
  72. }

問題: 
1.BossGroup跟WorkerGroup都是同一種類型(NioEventLoopGroup),那他們的角色分別是啥? 有啥講究呢?

前者用於處理新客戶端連接事件,後者用於處理建立好連接後的channel的IO事件. 
默認情況下,一個NioEventLoopGroup會創建cpu核心數*2個EventLoop, 而我們知道每個EventLoop就有一個Selector. 
如果你的服務端應用只綁定一個ip以及一個端口的話(99%的應用都是這種情況吧?),那麼BossGroup裏面只有一個EventLoop/Selector是起作用的.所以,建議BossGroup創建的時候指定只需要1個EventLoop:EventLoopGroup bossGroup = new NioEventLoopGroup(1);

2.上面的代碼都看不到NIO的Selector,Channel,它們在哪?

每個NioEventLoop包括一個Selector,一個taskQueue(tq),而它本身又繼承了Executor接口(從而擁有execute方法,但要注意一個NioEventLoop只有一條worker線程),同時還持有一個Executor成員e以及一個Thread成員t(這就是所謂的worker線程). 
在ServerBootStrap的bind方法中,會創建一條NioServerSocketChannel並註冊到bossGroup的一個EventLoop的Selector上.

3.什麼時候關注OP_ACCEPT事件的呢?

4.1 ServerBootStrap.bind會通過initAndRegister創建一條NIOServerSocketChannel(在構造函數中會初始化其pipeline), 並初始化該channel(通過ChannelInitializer創建一個ServerBootstrapAcceptor) 
4.2 然後是selector的註冊. 在boss eventLoopGroup中找到一個EventLoop, 執行register操作, 並最終在unsafe對象中生成registerTask. 
4.3 registerTask通過eventLoop.execute提交到任務列表中並執行. 這時候eventLoop的線程t會創建並進入到無限循環的run方法中. 
4.4 在registerTask中, 給channel註冊到eventLoop中,但op位爲0: 
selectionKey = javaChannel().register(eventLoop().selector, 0, this)
之後, 
pipeline.fireChannelRegistered(); 
pipeline.fireChannelActive();
 
第一句觸發ChannelInitializer.channelRegistered方法,最終把ServerBootstrapAcceptor加入到pipeline中. 
第二句觸發handler.read方法, 最終在doBeginRead中完成對OP_ACCEPT事件的關注.

4.某個連接的多次數據接收,是否總是在同一個線程中執行?

上面說到,某個連接是註冊到workerGroup中的某個EventLoop的Selector的,該Selector的所有io事件都由該EventLoop的線程池處理,而在netty 4.x中, 這個線程池只有一個線程(SingleThreadEventLoop). 
故某連接的所有io事件,都由同一個線程處理.

5.write&Flush 的線程處理模式?

netty中ctx.write有兩種情況: 
1. io線程(且要是該socket對應的EventLoop的io線程)直接調用ctx.write,這時候會直接寫到ChannelOutBoundBuffer裏面. 
2. 如果是非IO線程調用ctx.write,那麼會把返回消息封裝成一個task扔到EventLoop的taskQueue裏面.然後在後面的循環中, 返回消息會給io線程寫到ChannelOutBoundBuffer裏面. 
如果是writeAndFlush, 那麼相當於write完後再調用一次flush. flush的作用是把ChannelOutBoundBuffer裏面的數據真正寫入到Tcp內核緩衝區並通過網卡發送出去.

6.多個業務線程同時往客戶端進行寫操作,是否會出現包纏繞?例如線程A,B,C都同時寫1個1M的返回包,是否會出現客戶端收到 a1 b1 a2 b2 c1 ...

由上面第五個問題可知, 不會. 
A,B,C這些業務線程實際上在寫數據的時候,是各自把數據封裝成一個task扔到eventLoop的任務隊列裏面, 然後後面的循環中(不一定是下一次哦), io線程依次取出每一個任務(串行)寫到channelOutboundBuffer中, 然後有flush調用的時候返回給客戶端. 
儘管1M的包有點大, 但它們拆包的時候也是同一個包按順序發送, 不至於出現亂序, 這個是由TCP的機制來保證的.

下面的代碼是netty對write操作的處理(AbstractChannelHandlerContext)


 
  1. private void write(Object msg, boolean flush, ChannelPromise promise) {
  2. AbstractChannelHandlerContext next = findContextOutbound();
  3. EventExecutor executor = next.executor();
  4. if (executor.inEventLoop()) {
  5. next.invokeWrite(msg, promise);
  6. if (flush) {
  7. next.invokeFlush();
  8. }
  9. } else {
  10. AbstractWriteTask task;
  11. if (flush) {
  12. task = WriteAndFlushTask.newInstance(next, msg, promise);
  13. } else {
  14. task = WriteTask.newInstance(next, msg, promise);
  15. }
  16. safeExecute(executor, task, promise, msg);
  17. }
  18. }

7.Netty如何處理TCP粘包/拆包的問題的?

一般可通過三種方式: 
3.1 固定長度. 通訊雙方約定好每個消息的長度. 
3.2 分隔符.例如回車換行或者約定好的特定分隔符. 
3.3 消息頭+消息體,其中消息頭指定消息的長度. 
Netty對於上述的三種方案都有現成的編解碼器. 而實際中應用最廣泛的是第三種.

五 Netty最佳實踐

1. 避免在業務線程中觸碰netty

netty應該作爲一個框架級的基礎通訊組件, 在業務層中不應該出現netty的相關操作.比如寫操作, 因爲IO線程是異步的, 你往channel寫的東西,很可能在真正序列化之前,已經給業務線程又修改的面目全非了.

六 結束語

本文在快塑網CTO王在祥(江湖人稱老王)指導下完成,謹表感激. 在寫作本文的過程中也多次跟唯品會架構部的樑哥交流討教, 同時在拜讀唯品會另一大神白衣大大的文章中獲益良多,佔小狼簡書上的源碼分析也很到位, 一併感謝.

​​​​​​​

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章