原文地址: 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. 應用構建一條消息,通過系統調用write,寫入到內核緩衝區
2. 網卡驅動程序把內核緩衝區的數據copy到網卡緩衝區中
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下,我們可以通過如下指令查看:
# cat /proc/sys/net/ipv4/tcp_rmem
4096 87380 6291456
以上三個值分別爲最小值/默認值/最大值.
其中默認值以及最大值又分別給net.core.rmem_default
以及net.core.rmem_max
覆蓋.
應用程序可通過SO_RCVBUF/SO_SNDBUF來分別修改接收/發送緩衝區大小(但不能超過內核指定的最大值以及最小值)
1.4 問題
-
在Java IO操作中,爲何使用堆外內存(heap-off)會比堆內存高效?
一般來說,申請堆內存比申請堆外內存更快。
但是,如上所述,當發生IO操作的時候,數據需從堆內複製到堆外,再把數據從用戶態複製到核心態,相當於做了兩次copy;而使用堆外內存的話,就只需要做一次copy(從用戶態到核心態的copy).
具體代碼可參看java.net.SocketOutputStream對應的native代碼 -
爲何數據在經過IO的時候,需要兩次copy?不能直接把堆內存數據直接傳到內核麼?
簡單來說,read/write等系統調用,需要傳入buffer的地址。然而heapBuffer的話,由於GC的存在,地址會發生移動而heap-off不會. 更詳細解釋可參考知乎R大的解釋.
-
服務端對客戶端的連接,設置接收緩衝區大小爲10(
socketChannel.setOption(StandardSocketOptions.SO_RCVBUF, 10)
),然後客戶端發送長度爲100的字符串過來,結果會如何?服務端將會正常的接收到所有字符.
這個設置實際上並沒有產生預期結果.
在上述語句執行前, 該channel的接收緩衝區大小爲net.core.rmem_default
(此值優先)或者net.ipv4.tcp_rmem
的第二個值;
執行後,由於設置的值小於內核允許的最小值(net.ipv4.tcp_rmem
的第一個值),最終該channel的接收緩衝區大小設置爲內核允許的最小值.在本人機器上,該值爲1024. -
續4, 客戶端發送長度爲1500的字符串過來,已經超過了設置的接收緩衝區大小,結果會如何?會有消息丟失嗎?
服務端會觸發2次可讀事件,第一次讀了1024個字符,第二次讀了476個字符,且消息不會丟失.
前面我們在說緩衝區的時候說到,除了有內核緩衝區,還會有一個硬件設備的緩衝區(這裏是網卡的緩衝區).
問題3/4可在3.1小節的NIO簡單模式中驗證.
二 NIO的底層實現機制
高性能的非阻塞IO的實現,依賴一個叫Selector的牛逼貨,該組件也稱IO多路複用器,多個網絡連接可共用一個selector,從而實現單線程處理多個客戶端連接的目的(傳統的阻塞式IO,通常是一個連接需要一個線程去處理).
selector的機制如下:
- 系統在內核中創建一個selector, 對於不同客戶端的連接(也就是NIO中的socketChannel),都可以被註冊到同一個selector上. selector通過數組或者鏈表結構維護這衆多的channel(在內核中表現爲文件句柄).
- 當channel狀態發生變化的時候(例如接收緩衝區收到數據會觸發可讀事件,發送緩衝區空閒會觸發可寫事件等),該channel會被selector打上ready的標記.
- 當應用程序調用selector.select()方法的時候,selector會輪詢其所管轄的channel,把就緒的channel放到selectedKeys中.
- 應用遍歷selectedKeys,並對每個channel的事件進行處理.
selector的底層實現有三種方式,分別使用select/poll/epoll系統調用.在內核2.6+的Linux上, Java使用的是epoll.
下面是三種實現的對比
實現 | 說明 | 性能 |
---|---|---|
select | 需要在用戶態跟核心態之間相互copy大量文件句柄,文件句柄採用數組結構,數組大小跟內核參數有關 | 一般來說,少於1024個句柄下,性能優異;但由於採用盲輪詢,句柄越多性能越差 |
poll | 跟select沒有本質差別,不同之處在於,文件句柄採用的是鏈表結構,理論上沒限制數量 | 跟select一致 |
epoll | 採用事件回調機制,只copy有效的文件句柄 | 性能優越,但如果偵聽的連接數不多的話(例如少於1024),性能反而沒有select/poll高 |
下面給三段僞代碼說明上述三種機制,摘自知乎藍形參的回覆:
1 簡單粗暴模式(非阻塞忙輪詢):
所謂的忙,就是說這個機制永遠在盲目的輪詢
while true {
for channel in channels[] {
if channel has data
handle(channel)
}
}
2 select/poll模式:
首先通過select進入阻塞.當有IO事件的時候,從阻塞態中醒來.
是有目的的輪詢,複雜度爲O(n)
while true {
select(channels[])
for channel in channels[] {
if channel has data
handle(channel)
}
}
3 epoll模式:
首先也是通過select進入阻塞.當有IO事件的時候,從阻塞態中醒來,並返回有IO事件發生的channel.
複雜度爲O(1)
while true {
selected_channels[] = select(channels[])
for channel in selected_channels[] {
handle(channel)
}
}
最後再提一下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.
package nio.demo;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.net.SocketOptions;
import java.net.StandardSocketOptions;
import java.nio.ByteBuffer;
import java.nio.channels.*;
import java.util.Iterator;
/**
* Created by ever on 2017/11/24.
*/
public class ServerDemo {
private static final int BUF_SIZE = 1024;
private static final int PORT = 8070;
private static final int TIMEOUT = 3000;
public static void main(String[] args) throws IOException, InterruptedException {
selector();
}
/**
* 初始化selector以及ServerSocketChannel
* @throws IOException
* @throws InterruptedException
*/
private static void selector() throws IOException, InterruptedException {
ServerSocketChannel ssc = ServerSocketChannel.open();
Selector selector = Selector.open();
ssc.socket().bind(new InetSocketAddress(PORT));
ssc.configureBlocking(false);
ssc.register(selector, SelectionKey.OP_ACCEPT);
// 開始輪詢直至天荒地老
while (true) {
if (selector.select(TIMEOUT) == 0) {
System.out.println("No io events found");
continue;
}
Iterator<SelectionKey> iter = selector.selectedKeys().iterator();
while (iter.hasNext()) {
SelectionKey key = iter.next();
if (key.isAcceptable()) {
handleAccept(key);
}
if (key.isWritable() && key.isValid()) {
handleWrite(key);
}
if (key.isReadable()) {
handleRead(key);
}
iter.remove();
}
}
}
private static void handleAccept(SelectionKey key) throws IOException {
System.out.println("Handle accept event");
ServerSocketChannel ssc = (ServerSocketChannel) key.channel();
SocketChannel sc = ssc.accept();
sc.configureBlocking(false);
//設置輸入緩衝區的大小, 參看1.4小節問題3/4
sc.setOption(StandardSocketOptions.SO_RCVBUF, 10);
sc.register(key.selector(), SelectionKey.OP_READ);
}
/**
* @param key
* @throws IOException
* @throws InterruptedException
*/
private static void handleWrite(SelectionKey key) throws IOException, InterruptedException {
System.out.println("Handle write event");
SocketChannel sc = (SocketChannel) key.channel();
ByteBuffer buffer = (ByteBuffer)key.attachment();
if (buffer == null) {
ByteBuffer.allocate(BUF_SIZE);
buffer.put("return from server".getBytes());
buffer.flip();
key.attach(buffer);
}
int writes = sc.write(buffer);
System.out.println("write bytes:" + writes);
System.out.println("remain:" + buffer.remaining());
if (buffer.remaining() == 0) {
key.attach(null);
key.interestOps(SelectionKey.OP_READ);
}
}
private static void handleRead(SelectionKey key) throws IOException {
System.out.println("Handle read event");
SocketChannel sc = (SocketChannel) key.channel();
ByteBuffer buffer = ByteBuffer.allocate(BUF_SIZE);
long bytesRead = sc.read(buffer);
System.out.println("read:" + bytesRead);
if (bytesRead == -1) {
System.out.println("Peer closed");
sc.close();
System.exit(0);
}
// 輸出內容
if (bytesRead > 0) {
buffer.flip();
while (buffer.hasRemaining()) {
System.out.print((char) buffer.get());
}
}
// won't have any effect this time, and will take effect after next select() invoked
key.interestOps(SelectionKey.OP_WRITE|SelectionKey.OP_READ);
}
}
3.1.1 問題
-
假設在handleWrite方法中往客戶端寫入超過內核輸出緩衝區大小的字節(假設內核的輸出緩衝區大小爲1024),會有何現象?
這時會觸發多次OP_WRITE事件,直至buffer中的數據全部寫入內核緩衝區.
-
如果在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,相關處理代碼 -
如果註釋掉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事件以及業務邏輯.
public class Reactor implements Runnable {
final Selector selector;
final ServerSocketChannel serverSocket;
Reactor(int port) throws IOException {
selector = Selector.open();
serverSocket = ServerSocketChannel.open();
serverSocket.socket().bind(
new InetSocketAddress(port));
serverSocket.configureBlocking(false);
SelectionKey sk =
serverSocket.register(selector,
SelectionKey.OP_ACCEPT);
sk.attach(new Acceptor());
}
public void run() { // normally in a new Thread
try {
while (!Thread.interrupted()) {
selector.select();
Set selected = selector.selectedKeys();
Iterator it = selected.iterator();
while (it.hasNext())
dispatch((SelectionKey) (it.next()));
selected.clear();
}
} catch (IOException ex) { /* ... */ }
}
void dispatch(SelectionKey k) {
Runnable r = (Runnable) (k.attachment());
if (r != null)
r.run();
}
class Acceptor implements Runnable { // inner
public void run() {
try {
SocketChannel channel = serverSocket.accept();
if (channel != null)
new Handler(selector, channel);
} catch (IOException ex) { /* ... */ }
}
}
}
final class Handler implements Runnable {
private static final int MAXIN = 1024;
private static final int MAXOUT = 1024;
final SocketChannel socket;
final SelectionKey sk;
ByteBuffer input = ByteBuffer.allocate(MAXIN);
ByteBuffer output = ByteBuffer.allocate(MAXOUT);
static final int READING = 0, SENDING = 1;
int state = READING;
Handler(Selector sel, SocketChannel c)
throws IOException {
socket = c;
c.configureBlocking(false);
// Optionally try first read now
sk = socket.register(sel, 0);
sk.attach(this);
sk.interestOps(SelectionKey.OP_READ);
sel.wakeup();
}
boolean inputIsComplete() { /* ... */
return true;
}
boolean outputIsComplete() { /* ... */
return true;
}
void process() { /* ... */ }
public void run() {
try {
if (state == READING) read();
else if (state == SENDING) send();
} catch (IOException ex) { /* ... */ }
}
void read() throws IOException {
socket.read(input);
if (inputIsComplete()) {
process();
state = SENDING;
// Normally also do first write now
sk.interestOps(SelectionKey.OP_WRITE);
}
}
void send() throws IOException {
socket.write(output);
if (outputIsComplete()) sk.cancel();
}
}
單線程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時的思考問題爲主.
首先,給出一個經典的服務端代碼:
public class EchoServer {
private final int port;
public EchoServer(int port) {
this.port = port;
}
public void run() throws Exception {
// Configure the server.
// 1. 創建EventLoopGroup, 每個group有若干個EventLoop,每個EventLoop有一個selector
EventLoopGroup bossGroup = new NioEventLoopGroup(); // (1)
EventLoopGroup workerGroup = new NioEventLoopGroup();
try {
ServerBootstrap b = new ServerBootstrap(); // (2)
b.group(bossGroup, workerGroup)
.channel(NioServerSocketChannel.class) // (3)
.option(ChannelOption.SO_BACKLOG, 100)
.handler(new LoggingHandler(LogLevel.INFO))
.childHandler(new ChannelInitializer<SocketChannel>() { // (4)
@Override
public void initChannel(SocketChannel ch) throws Exception {
ch.pipeline().addLast(
new EchoServerHandler());
}
});
// Start the server.
ChannelFuture f = b.bind(port).sync(); // (5)
// Wait until the server socket is closed.
f.channel().closeFuture().sync();
} finally {
// Shut down all event loops to terminate all threads.
bossGroup.shutdownGracefully();
workerGroup.shutdownGracefully();
}
}
public static void main(String[] args) throws Exception {
int port;
if (args.length > 0) {
port = Integer.parseInt(args[0]);
} else {
port = 8090;
}
new EchoServer(port).run();
}
}
class EchoServerHandler extends ChannelInboundHandlerAdapter {
private static final Logger logger = Logger.getLogger(
EchoServerHandler.class.getName());
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
logger.log(Level.WARNING, "channelRead:" + msg);
ctx.writeAndFlush(msg);
}
@Override
public void channelReadComplete(ChannelHandlerContext ctx) throws Exception {
logger.log(Level.WARNING, "channelReadComplete");
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
// Close the connection when an exception is raised.
logger.log(Level.WARNING, "Unexpected exception from downstream.", cause);
ctx.close();
}
}
問題:
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)
private void write(Object msg, boolean flush, ChannelPromise promise) {
AbstractChannelHandlerContext next = findContextOutbound();
EventExecutor executor = next.executor();
if (executor.inEventLoop()) {
next.invokeWrite(msg, promise);
if (flush) {
next.invokeFlush();
}
} else {
AbstractWriteTask task;
if (flush) {
task = WriteAndFlushTask.newInstance(next, msg, promise);
} else {
task = WriteTask.newInstance(next, msg, promise);
}
safeExecute(executor, task, promise, msg);
}
}
7.Netty如何處理TCP粘包/拆包的問題的?
一般可通過三種方式:
3.1 固定長度. 通訊雙方約定好每個消息的長度.
3.2 分隔符.例如回車換行或者約定好的特定分隔符.
3.3 消息頭+消息體,其中消息頭指定消息的長度.
Netty對於上述的三種方案都有現成的編解碼器. 而實際中應用最廣泛的是第三種.
五 Netty最佳實踐
1. 避免在業務線程中觸碰netty
netty應該作爲一個框架級的基礎通訊組件, 在業務層中不應該出現netty的相關操作.比如寫操作, 因爲IO線程是異步的, 你往channel寫的東西,很可能在真正序列化之前,已經給業務線程又修改的面目全非了.
六 結束語
本文在快塑網CTO王在祥(江湖人稱老王)指導下完成,謹表感激. 在寫作本文的過程中也多次跟唯品會架構部的樑哥交流討教, 同時在拜讀唯品會另一大神白衣大大的文章中獲益良多,佔小狼簡書上的源碼分析也很到位, 一併感謝.