1、問題背景
傳輸層除了有TCP協議外還有UDP協議。
首先,UDP不會發生粘包或拆包現象。因爲UDP是基於報文發送的,從UDP的幀結構可以看出,在UDP首部採用了16bit來指示UDP數據報文的長度,因此在應用層能很好的將不同的數據報文區分開,從而避免粘包和拆包的問題。
但TCP是基於字節流的,在基於流的傳輸裏(如TCP/IP),接收到的數據會先被存儲到一個socket接收緩衝裏。不幸的是,基於流的傳輸並不是一個數據包隊列,而是一個字節隊列。TCP底層並不瞭解上層業務數據的具體含義,它會根據TCP緩衝區的實際情況進行化包的劃分,所以在業務上認爲,一個完整的包可能會被TCP拆成多個包進行發送,也有多個小的包封裝成一個大的數據包發送,這就是所謂的TCP粘包和拆包問題。粘包拆包現象發生如圖所示:
2、發生粘包或拆包的直接原因
原因與解決方式,網上一查有很多,服務端 和 客戶端 都會造成粘包、半包問題,以下列出常見原因。
- 服務端:
- 要發送的數據大於TCP發送緩衝區剩餘空間大小,將會發生拆包。
- 待發送數據大於MSS(最大報文長度),TCP在傳輸前將進行拆包。
- 要發送的數據小於TCP發送緩衝區的大小,TCP將多次寫入緩衝區的數據一次發送出去,將會發生粘包。
- 接收端:
- 接收數據端的應用層沒有及時讀取接收緩衝區中的數據,將發生粘包。
3、粘包問題的解決策略
TCP以流的方式進行數據傳輸,由於底層TCP無法理解上層的業務數據,所以在底層是無法保證數據包不被拆分和重組的,這個問題只能通過上層的應用協議棧設計來解決,上層的應用協議爲了對消息進行區分,業界主流的解決方案歸納如下:
- 固定消息長度,累計讀取到長度總和爲定長LEN的報文後,就認爲讀取到了一個完整的消息,如果不夠,空位補空格;將計數器置位,重新開始讀取下一個數據。
- 將回車換行符作爲消息結束符,例如FTP協議,這種方式在文本協議中應該比較廣泛。
- 將特殊的分隔符作爲消息結束標誌,回車換行符就是一種特殊的結束分隔符。
- 通過在消息頭中定義長度字段來標識消息的總長度。通常設計思路爲消息頭的第一個字段使用int32來表示消息的總長度。
- 更復雜的應用層協議。
4、netty提供的處理方式
Netty對上述前四種應用做了統一抽象,提供了Decoder來解決對應的問題,使用起來非常方便。只要能夠熟練的掌握這些類庫的使用,TCP粘包問題就可以變得非常容易,這個就是其他NIO框架和JDK原生的NIO API無法匹敵的。有了這些Decoder,用戶不需要對自己讀取的報文進行人工解碼,也不需要考慮TCP的粘包和拆包。
4.1、Decoder API
- LineBasedFrameDecoder(消息以回車換行符結尾的處理)
- StringDecoder
- DelimiterBasedFrameDecoder(消息以自定義分隔符結尾的處理)
- FixedLengthFrameDecoder
4.2、上述API使用方式及原理
4.2.1、LineBasedFrameDecoder 和 StringDecoder示例
- 服務端:
@Override
public void run() {
EventLoopGroup bossGroup = new NioEventLoopGroup();
EventLoopGroup workerGroup = new NioEventLoopGroup();
try {
ServerBootstrap b = new ServerBootstrap();
b.group(bossGroup, workerGroup)
.channel(NioServerSocketChannel.class)
.option(ChannelOption.SO_BACKLOG, 100)
.handler(new LoggingHandler(LogLevel.INFO))
.childHandler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel socketChannel) throws Exception {
//add decoder
socketChannel.pipeline().addLast(new LineBasedFrameDecoder(MAX_LEN));
socketChannel.pipeline().addLast(new StringDecoder());
}
});
//綁定端口
ChannelFuture future = b.bind(port).sync();
//等待服務端監聽端口關閉
logger.debug("server start!");
future.channel().closeFuture().sync();
logger.debug("server end");
} catch (InterruptedException e) {
e.printStackTrace();
}finally {
bossGroup.shutdownGracefully();
workerGroup.shutdownGracefully();
}
}
- 客戶端:
@Override
public void run() {
EventLoopGroup group = new NioEventLoopGroup();
Bootstrap b = new Bootstrap();
b.group(group)
.channel(NioSocketChannel.class)
.remoteAddress(new InetSocketAddress(host, port))
.handler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel socketChannel) throws Exception {
//add decoder
socketChannel.pipeline().addLast(new LineBasedFrameDecoder(MAX_LEN));
socketChannel.pipeline().addLast(new StringDecoder());
}
});
try {
ChannelFuture f = b.connect().sync();
f.channel().closeFuture().sync();
} catch (InterruptedException e) {
e.printStackTrace();
}finally {
try {
group.shutdownGracefully().sync();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
- 原理分析:
LineBasedFrameDecoder + StringDecoder組合就是按行切換的文本解碼器,它被設計用來支持TCP的粘包和拆包。其工作原理是它依次遍歷ByteBuf中的可讀字節,判斷是否有”\n” 或者”\r\n”, 如果有,就以此位置爲結束位置,從可讀索引到結束位置區間的字節就組成了一行。它是以換行符爲結束標誌的解碼器, 支持攜帶結束符或者不攜帶結束符兩種解碼方式,同時支持配置單行的最大長度。回車換行解碼器實際上是一種特殊的 DelimiterBasedFrameDecoder 解碼器
如果連續讀取到最大長度之後,仍然沒有發現換行符就會拋出異常,同時忽略掉之前讀到的異常碼流。StringDecoder的功能非常簡單,就是將接收到的對象換成字符串,然後繼續調用後面的Handller。
4.2.2、DelimiterBasedFrameDecoder+StringDecoder示例
選定特殊字符作爲收發雙方發送數據包的分隔符,注:此方式需確定數據包中不含該分隔符,否則數據包會被拆包。若服務器與客戶端之間相互發送的數據均含特定分隔符"_#",則C、S兩端initChannel方法中都需添加該解碼器。
- 服務端:
@Override
protected void initChannel(SocketChannel ch) throws Exception {
// 告訴DelimiterBasedFrameDecoder以_#作爲分隔符
ByteBuf delimiter = Unpooled.copiedBuffer("_#".getBytes());
ChannelPipeline pipeline = ch.pipeline();
//1024表示單條消息的最大長度,當達到該長度還沒有找到分隔符,則拋出TooLongFrameException
pipeline.addLast(new DelimiterBasedFrameDecoder(1024,delimiter));
pipeline.addLast(new StringDecoder());
}
- 客戶端:
@Override
protected void initChannel(SocketChannel ch) throws Exception {
ByteBuf delimiter = Unpooled.copiedBuffer("_#".getBytes());
ChannelPipeline pipeline = ch.pipeline();
pipeline.addLast(new DelimiterBasedFrameDecoder(1024,delimiter));
pipeline.addLast(new StringDecoder());
}
注:消息尾需加上"_#" ,即ctx.writeAndFlush (yourMessage+"_#");
DelimiterBasedFrameDecoder 原理分析:解碼時,判斷當前已經讀取的 ByteBuf 中是否包含分隔符 ByteBuf,如果包含,則截取對應的 ByteBuf 返回。
4.2.3、定義消息長度字段編解碼示例
- 服務端
public class SocketServer {
public static void main(String[] args) throws InterruptedException {
EventLoopGroup bossGroup = new NioEventLoopGroup();
EventLoopGroup workerGroup = new NioEventLoopGroup();
try {
ServerBootstrap serverBootstrap = new ServerBootstrap();
serverBootstrap.group(bossGroup, workerGroup)
.channel(NioServerSocketChannel.class)
.handler(new LoggingHandler(LogLevel.INFO))
.childHandler(new ChannelInitializer<SocketChannel>(){
@Override
protected void initChannel(SocketChannel ch) throws Exception {
ChannelPipeline pipeline = ch.pipeline();
//添加自定義解碼處理器
pipeline.addLast(new SelfDefineDecodeHandler());
pipeline.addLast(new ServerBusinessHandler());
}
});
ChannelFuture channelFuture = serverBootstrap.bind(8090).sync();
channelFuture.channel().closeFuture().sync();
}
finally {
bossGroup.shutdownGracefully();
workerGroup.shutdownGracefully();
}
}
}
自定義解碼處理器:
/**
* 定長消息數據格式
*
* | length | msg | 頭部length用4字節存儲,存儲的長度爲消息體msg的總長度
*
*
* */
public class SelfDefineDecodeHandler extends ByteToMessageDecoder {
@Override
protected void decode(ChannelHandlerContext ctx, ByteBuf bufferIn, List<Object> out) throws Exception {
if (bufferIn.readableBytes() < 4) {
return;
}
//返回當前buff中readerIndex索引
int beginIndex = bufferIn.readerIndex();
//在當前readerIndex基礎上讀取4字節並返回,同時增加readIndex
int length = bufferIn.readInt();
/**
* 1.當可讀數據小於length,說明包還沒有接收完全
* 2.開始可讀爲beginindex,此時讀完readInt後需要重置readerindex
* 3.重置readerindex後繼續等待下一個讀事件到來
* */
if (bufferIn.readableBytes() < length) {
//重置當前的readerindex爲beginindex
bufferIn.readerIndex(beginIndex);
return;
}
//4字節存放length,這裏整個消息長度爲4+length,跳過當前消息,增大bufferIn的readindex,bufferIn中數組可複用
bufferIn.readerIndex(beginIndex + 4 + length);
//Returns a slice of this buffer's sub-region.
//取出當前的整條消息並存入otherByteBufRef中
ByteBuf otherByteBufRef = bufferIn.slice(beginIndex, 4 + length);
/**
* 1.每一個bytebuf都有一個計數器,每次調用計數器減1,當計數器爲0時則不可用。
* 2.當前bytebuf中數據包含多條消息,本條信息會通過out返回被繼續封裝成一個新的bytebuf返回下一個hander處理
* 3.retain方法是將當前的bytebuf計數器加1
* */
otherByteBufRef.retain();
out.add(otherByteBufRef);
}
}
服務端業務處理器:
public class ServerBusinessHandler extends ChannelInboundHandlerAdapter {
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
/**
* 1.讀數據,這裏收到的是一個完整的消息數據,從上述decoder方法中的out傳遞到當前邏輯
* 2.對消息進一步的解碼
* */
ByteBuf buf = (ByteBuf)msg;
int length = buf.readInt();
assert length == (8);
byte[] head = new byte[4];
buf.readBytes(head);
String headString = new String(head);
assert "head".equals(headString);
byte[] body = new byte[4];
buf.readBytes(body);
String bodyString = new String(body);
assert "body".equals(bodyString);
}
}
- 客戶端
public class SocketClient {
public static void main(String[] args) throws InterruptedException {
EventLoopGroup eventLoopGroup = new NioEventLoopGroup();
try {
Bootstrap bootstrap = new Bootstrap();
bootstrap.group(eventLoopGroup)
.channel(NioSocketChannel.class)
.handler(new LoggingHandler(LogLevel.INFO))
.handler(new ChannelInitializer<SocketChannel>(){
@Override
protected void initChannel(SocketChannel ch) throws Exception {
ChannelPipeline pipeline = ch.pipeline();
pipeline.addLast(new SocketClientHandler());
}});
ChannelFuture channelFuture = bootstrap.connect("localhost", 8090).sync();
channelFuture.channel().closeFuture().sync();
}
finally {
eventLoopGroup.shutdownGracefully();
}
}
}
客戶端消息處理器
public class SocketClientHandler extends ChannelInboundHandlerAdapter {
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
//暫不處理
}
/**
* 1.寫數據的邏輯,先寫入消息的總長度
* 2.分別寫入消息體的內容
* 3.以bytebuf的方式發送數據
* */
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
UnpooledByteBufAllocator allocator = new UnpooledByteBufAllocator(false);
ByteBuf buffer = allocator.buffer(20);
buffer.writeInt(8);
buffer.writeBytes("head".getBytes());
buffer.writeBytes("body".getBytes());
ctx.writeAndFlush(buffer);
}
}
此外,本人覺得https://www.jianshu.com/p/adc2de3691c7基於netty實現了後臺簡易聊天程序, 可加深初學者理解,供參考。
另,netty相關分析可參考資料:
零拷貝:https://www.cnblogs.com/xys1228/p/6088805.html
channelHandler:https://www.cnblogs.com/krcys/p/9297092.html
https://www.jianshu.com/p/a9bcd89553f5
心跳保活處理:https://blog.csdn.net/u013967175/article/details/78591810
編解碼處理:https://www.cnblogs.com/itdragon/p/8384014.html
本文參考以下大神博客:
http://www.importnew.com/26577.html