不管是服務端仍是客戶端,當咱們讀取或者發送消息的時候,都須要考慮TCP底層的粘包/拆包機制。html
TCP是個「流」協議,所謂流,就是沒有界限的一串數據。你們能夠想一想河裏的流水,是連成一片的,其間並無分界線。TCP底層並不瞭解上層業務數據的具體含義,它會根據TCP緩衝區的實際狀況進行包的劃分,因此在業務上認爲,一個完整的包可能會被TCP拆分紅多個包進行發送,也有可能把多個小的包封裝成一個大的數據包發送,這就是所謂的TCP粘包和拆包問題。java
假設客戶端分別發送了兩個數據包D1和D2給服務端,因爲服務端一次讀取到的字節數是不肯定的,故可能存在如下4種狀況。bootstrap
(1)服務端分兩次讀取到了兩個獨立的數據包,分別是D1和D2,沒有粘包和拆包;服務器
(2)服務端一次接收到了兩個數據包,D1和D2粘合在一塊兒,被稱爲TCP粘包;框架
(3)服務端分兩次讀取到了兩個數據包,第一次讀取到了完整的D1包和D2包的部份內容,第二次讀取到了D2包的剩餘內容,這被稱爲TCP拆包;異步
(4)服務端分兩次讀取到了兩個數據包,第一次讀取到了D1包的部份內容D1_1,第二次讀取到了D1包的剩餘內容D1_2和D2包的整包。socket
若是此時服務端TCP接收滑窗很是小,而數據包D1和D2比較大,頗有可能會發生第五種可能,即服務端分屢次才能將D1和D2包接收徹底,期間發生屢次拆包。ide
問題產生的緣由有三個,分別以下。oop
(1)應用程序write寫入的字節大小大於套接口發送緩衝區大小;post
(2)進行MSS大小的TCP分段;
(3)以太網幀的payload大於MTU進行IP分片。
因爲底層的TCP沒法理解上層的業務數據,因此在底層是沒法保證數據包不被拆分和重組的,這個問題只能經過上層的應用協議棧設計來解決,根據業界的主流協議的解決方案,能夠概括以下。
(1)消息定長,例如每一個報文的大小爲固定長度200字節,若是不夠,空位補空格;
(2)在包尾增長回車換行符進行分割,例如FTP協議;
(3)將消息分爲消息頭和消息體,消息頭中包含表示消息總長度(或者消息體長度)的字段,一般設計思路爲消息頭的第一個字段使用int32來表示消息的總長度;
(4)更復雜的應用層協議。
在前面的時間服務器例程中,咱們屢次強調並無考慮讀半包問題,這在功能測試時每每沒有問題,可是一旦壓力上來,或者發送大報文以後,就會存在粘包/拆包問題。若是代碼沒有考慮,每每就會出現解碼錯位或者錯誤,致使程序不能正常工做。以Netty 入門示例爲例。
import io.netty.buffer.ByteBuf; import io.netty.buffer.Unpooled; import io.netty.channel.ChannelHandlerAdapter; import io.netty.channel.ChannelHandlerContext; public class TimeServerHandler extends ChannelHandlerAdapter { private int counter; @Override public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception { ByteBuf buf = (ByteBuf) msg; byte[] req = new byte[buf.readableBytes()]; buf.readBytes(req); String body = new String(req, "UTF-8").substring(0, req.length - System.getProperty("line.separator").length()); System.out.println("The time server receive order : " + body + " ; the counter is : " + ++counter); String currentTime = "QUERY TIME ORDER".equalsIgnoreCase(body) ? new java.util.Date( System.currentTimeMillis()).toString() : "BAD ORDER"; currentTime = currentTime + System.getProperty("line.separator"); ByteBuf resp = Unpooled.copiedBuffer(currentTime.getBytes()); ctx.writeAndFlush(resp); } @Override public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) { ctx.close(); } }
每讀到一條消息後,就計一次數,而後發送應答消息給客戶端。按照設計,服務端接收到的消息總數應該跟客戶端發送的消息總數相同,並且請求消息刪除回車換行符後應該爲"QUERY TIME ORDER"。
import io.netty.buffer.ByteBuf; import io.netty.buffer.Unpooled; import io.netty.channel.ChannelHandlerAdapter; import io.netty.channel.ChannelHandlerContext; public class TimeClientHandler extends ChannelHandlerAdapter { private int counter; private byte[] req; public TimeClientHandler() { req = ("QUERY TIME ORDER" + System.getProperty("line.separator")).getBytes(); } @Override public void channelActive(ChannelHandlerContext ctx) { ByteBuf message = null; for (int i = 0; i < 100; i++) { message = Unpooled.buffer(req.length); message.writeBytes(req); ctx.writeAndFlush(message); } } @Override public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception { ByteBuf buf = (ByteBuf) msg; byte[] req = new byte[buf.readableBytes()]; buf.readBytes(req); String body = new String(req, "UTF-8"); System.out.println("Now is : " + body + " ; the counter is : " + ++counter); } @Override public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) { // 釋放資源 ctx.close(); } }
客戶端跟服務端鏈路創建成功以後,循環發送100條消息,每發送一條就刷新一次,保證每條消息都會被寫入Channel中。按照咱們的設計,服務端應該接收到100條查詢時間指令的請求消息。客戶端每接收到服務端一條應答消息以後,就打印一次計數器。按照設計初衷,客戶端應該打印100次服務端的系統時間。
運行結果:
服務端運行結果以下。
The time server receive order : QUERY TIME ORDER
QUERY TIME ORDER
......................
QUERY TIME ORDER ; the counter is : 1
The time server receive order :
QUERY TIME ORDER
............
QUERY TIME ORDER ; the counter is : 2
服務端運行結果代表它只接收到了兩條消息,第一條包含57條「QUERY TIME ORDER」指令,第二條包含了43條「QUERY TIME ORDER」指令,總數正好是100條。咱們期待的是收到100條消息,每條包含一條「QUERY TIME ORDER」指令。這說明發生了TCP粘包。
客戶端運行結果以下。
Now is : BAD ORDER
BAD ORDER
; the counter is : 1
按照設計初衷,客戶端應該收到100條當前系統時間的消息,但實際上只收到了一條。這不難理解,由於服務端只收到了2條請求消息,因此實際服務端只發送了2條應答,因爲請求消息不知足查詢條件,因此返回了2條「BAD ORDER」應答消息。可是實際上客戶端只收到了一條包含2條「BAD ORDER」指令的消息,說明服務端返回的應答消息也發生了粘包。因爲上面的例程沒有考慮TCP的粘包/拆包,因此當發生TCP粘包時,咱們的程序就不能正常工做。
爲了解決TCP粘包/拆包致使的半包讀寫問題,Netty默認提供了多種編解碼器用於處理半包,只要能熟練掌握這些類庫的使用,TCP粘包問題今後會變得很是容易,你甚至不須要關心它們,這也是其餘NIO框架和JDK原生的NIO API所沒法匹敵的。
服務端代碼:
import io.netty.bootstrap.ServerBootstrap; import io.netty.channel.*; import io.netty.channel.nio.NioEventLoopGroup; import io.netty.channel.socket.nio.NioServerSocketChannel; import io.netty.handler.codec.LineBasedFrameDecoder; import io.netty.handler.codec.string.StringDecoder; public class TimeServer { public void bind(int port) throws Exception { // 配置服務端的NIO線程組 EventLoopGroup bossGroup = new NioEventLoopGroup(); EventLoopGroup workerGroup = new NioEventLoopGroup(); try { ServerBootstrap b = new ServerBootstrap(); b.group(bossGroup, workerGroup) .channel(NioServerSocketChannel.class) .option(ChannelOption.SO_BACKLOG, 1024) .childHandler(new ChildChannelHandler()); // 綁定端口,同步等待成功 ChannelFuture f = b.bind(port).sync(); // 等待服務端監聽端口關閉 f.channel().closeFuture().sync(); } finally { // 優雅退出,釋放線程池資源 bossGroup.shutdownGracefully(); workerGroup.shutdownGracefully(); } } private class ChildChannelHandler extends ChannelInitializer { @Override protected void initChannel(Channel arg0) throws Exception { arg0.pipeline().addLast(new LineBasedFrameDecoder(1024)); arg0.pipeline().addLast(new StringDecoder()); arg0.pipeline().addLast(new TimeServerHandler()); } } public static void main(String[] args) throws Exception { int port = 8080; if (args != null && args.length > 0) { try { port = Integer.valueOf(args[0]); } catch (NumberFormatException e) { // 採用默認值 } } new TimeServer().bind(port); } } import io.netty.buffer.ByteBuf; import io.netty.buffer.Unpooled; import io.netty.channel.ChannelHandlerAdapter; import io.netty.channel.ChannelHandlerContext; public class TimeServerHandler extends ChannelHandlerAdapter { private int counter; @Override public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception { String body = (String) msg; System.out.println("The time server receive order : " + body + " ; the counter is : " + ++counter); String currentTime = "QUERY TIME ORDER".equalsIgnoreCase(body) ? new java.util.Date(System.currentTimeMillis()).toString() : "BAD ORDER"; currentTime = currentTime + System.getProperty("line.separator"); ByteBuf resp = Unpooled.copiedBuffer(currentTime.getBytes()); ctx.writeAndFlush(resp); } @Override public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) { ctx.close(); } }
客戶端代碼:
import io.netty.bootstrap.Bootstrap; import io.netty.channel.*; import io.netty.channel.nio.NioEventLoopGroup; import io.netty.channel.socket.nio.NioSocketChannel; import io.netty.handler.codec.LineBasedFrameDecoder; import io.netty.handler.codec.string.StringDecoder; public class TimeClient { public void connect(int port, String host) throws Exception { // 配置客戶端NIO線程組 EventLoopGroup group = new NioEventLoopGroup(); try { Bootstrap b = new Bootstrap(); b.group(group).channel(NioSocketChannel.class) .option(ChannelOption.TCP_NODELAY, true) .handler(new ChannelInitializer() { @Override public void initChannel(Channel ch) throws Exception { ch.pipeline().addLast(new LineBasedFrameDecoder(1024)); ch.pipeline().addLast(new StringDecoder()); ch.pipeline().addLast(new TimeClientHandler()); } }); // 發起異步鏈接操做 ChannelFuture f = b.connect(host, port).sync(); // 等待客戶端鏈路關閉 f.channel().closeFuture().sync(); } finally { // 優雅退出,釋放NIO線程組 group.shutdownGracefully(); } } public static void main(String[] args) throws Exception { int port = 8080; if (args != null && args.length > 0) { try { port = Integer.valueOf(args[0]); } catch (NumberFormatException e) { // 採用默認值 } } new TimeClient().connect(port, "127.0.0.1"); } } import io.netty.buffer.ByteBuf; import io.netty.buffer.Unpooled; import io.netty.channel.ChannelHandlerAdapter; import io.netty.channel.ChannelHandlerContext; public class TimeClientHandler extends ChannelHandlerAdapter { private int counter; private byte[] req; public TimeClientHandler() { req = ("QUERY TIME ORDER" + System.getProperty("line.separator")) .getBytes(); } @Override public void channelActive(ChannelHandlerContext ctx) { ByteBuf message = null; for (int i = 0; i < 100; i++) { message = Unpooled.buffer(req.length); message.writeBytes(req); ctx.writeAndFlush(message); } } @Override public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception { String body = (String) msg; System.out.println("Now is : " + body + " ; the counter is : " + ++counter); } @Override public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) { // 釋放資源 ctx.close(); } }
兩個變化:
運行結果:
服務端執行結果以下。
The time server receive order : QUERY TIME ORDER ; the counter is : 1
.....................................
The time server receive order : QUERY TIME ORDER ; the counter is : 100
客戶端運行結果以下。
Now is : Thu Feb 20 00:00:14 CST 2014 ; the counter is : 1
......................................
Now is : Thu Feb 20 00:00:14 CST 2014 ; the counter is : 100
程序的運行結果徹底符合預期,說明經過使用LineBasedFrameDecoder和StringDecoder成功解決了TCP粘包致使的讀半包問題。對於使用者來講,只要將支持半包解碼的handler添加到ChannelPipeline中便可,不須要寫額外的代碼,用戶使用起來很是簡單。
LineBasedFrameDecoder的工做原理是它依次遍歷ByteBuf中的可讀字節,判斷看是否有「\n」或者「\r\n」,若是有,就以此位置爲結束位置,從可讀索引到結束位置區間的字節就組成了一行。它是以換行符爲結束標誌的解碼器,支持攜帶結束符或者不攜帶結束符兩種解碼方式,同時支持配置單行的最大長度。若是連續讀取到最大長度後仍然沒有發現換行符,就會拋出異常,同時忽略掉以前讀到的異常碼流。
StringDecoder的功能很是簡單,就是將接收到的對象轉換成字符串,而後繼續調用後面的handler。LineBasedFrameDecoder + StringDecoder組合就是按行切換的文本解碼器,它被設計用來支持TCP的粘包和拆包。
若是發送的消息不是以換行符結束的該怎麼辦呢?或者沒有回車換行符,靠消息頭中的長度字段來分包怎麼辦?是否是須要本身寫半包解碼器?答案是否認的,Netty提供了多種支持TCP粘包/拆包的解碼器,用來知足用戶的不一樣訴求。