TCP黏包拆包
TCP是一個流協(xié)議,就是沒有界限的一長串二進制數(shù)據(jù)。TCP作為傳輸層協(xié)議并不不了解上層業(yè)務數(shù)據(jù)的具體含義,它會根據(jù)TCP緩沖區(qū)的實際情況進行數(shù)據(jù)包的劃分,所以在業(yè)務上認為是一個完整的包,可能會被TCP拆分成多個包進行發(fā)送,也有可能把多個小的包封裝成一個大的數(shù)據(jù)包發(fā)送,這就是所謂的TCP粘包和拆包問題。
怎么解決?
- • 消息定長度,傳輸?shù)臄?shù)據(jù)大小固定長度,例如每段的長度固定為100字節(jié),如果不夠空位補空格
- • 在數(shù)據(jù)包尾部添加特殊分隔符,比如下劃線,中劃線等
- • 將消息分為消息頭和消息體,消息頭中包含表示信息的總長度
Netty提供了多個解碼器,可以進行分包的操作,分別是:
- • LineBasedFrameDecoder (回車換行分包)
- • DelimiterBasedFrameDecoder(特殊分隔符分包)
- • FixedLengthFrameDecoder(固定長度報文來分包)
- • LengthFieldBasedFrameDecoder(自定義長度來分包)
制造粘包和拆包問題
為了驗證我們的解碼器能夠解決這種粘包和拆包帶來的問題,首先我們就制造一個這樣的問題,以此用來做對比。
服務端:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
|
public static void main(String[] args) { EventLoopGroup bossGroup = new NioEventLoopGroup(); EventLoopGroup workerGroup = new NioEventLoopGroup(); ServerBootstrap bootstrap = new ServerBootstrap(); bootstrap.group(bossGroup, workerGroup) .channel(NioServerSocketChannel. class ) .childHandler( new ChannelInitializer<SocketChannel>() { @Override public void initChannel(SocketChannel ch) throws Exception { ch.pipeline().addLast( "decoder" , new StringDecoder()); ch.pipeline().addLast( "encoder" , new StringEncoder()); ch.pipeline().addLast( new ChannelInboundHandlerAdapter() { @Override public void channelRead(ChannelHandlerContext ctx, Object msg) { System.err.println( "server:" + msg.toString()); ctx.writeAndFlush(msg.toString() + "你好" ); } }); } }) .option(ChannelOption.SO_BACKLOG, 128 ) .childOption(ChannelOption.SO_KEEPALIVE, true ); try { ChannelFuture f = bootstrap.bind( 2222 ).sync(); f.channel().closeFuture().sync(); } catch (InterruptedException e) { e.printStackTrace(); } finally { workerGroup.shutdownGracefully(); bossGroup.shutdownGracefully(); } } |
客戶端我們發(fā)送一個比較長的字符串,如果服務端收到的消息是一條,那么就是對的,如果是多條,那么就有問題了。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
|
public static void main(String[] args) { EventLoopGroup workerGroup = new NioEventLoopGroup(); Channel channel = null ; try { Bootstrap b = new Bootstrap(); b.group(workerGroup); b.channel(NioSocketChannel. class ); b.option(ChannelOption.SO_KEEPALIVE, true ); b.handler( new ChannelInitializer<SocketChannel>() { @Override public void initChannel(SocketChannel ch) throws Exception { ch.pipeline().addLast( "decoder" , new StringDecoder()); ch.pipeline().addLast( "encoder" , new StringEncoder()); ch.pipeline().addLast( new ChannelInboundHandlerAdapter() { @Override public void channelRead(ChannelHandlerContext ctx, Object msg) { System.err.println( "client:" + msg.toString()); } }); } }); ChannelFuture f = b.connect( "127.0.0.1" , 2222 ).sync(); channel = f.channel(); StringBuilder msg = new StringBuilder(); for ( int i = 0 ; i < 100 ; i++) { msg.append( "hello yinjihuan" ); } channel.writeAndFlush(msg); } catch (Exception e) { e.printStackTrace(); } } |
首先啟動服務端,然后再啟動客戶端,通過控制臺可以看到服務接收的數(shù)據(jù)分成了2次,這就是我們要解決的問題。
server:hello yinjihuanhello....
server:o yinjihuanhello...
LineBasedFrameDecoder
用LineBasedFrameDecoder 來解決需要在發(fā)送的數(shù)據(jù)結尾加上回車換行符,這樣LineBasedFrameDecoder 才知道這段數(shù)據(jù)有沒有讀取完整。
改造服務端代碼,只需加上LineBasedFrameDecoder 解碼器即可,構造函數(shù)的參數(shù)是數(shù)據(jù)包的最大長度。
1
2
3
4
5
6
7
8
9
10
11
12
|
public void initChannel(SocketChannel ch) throws Exception { ch.pipeline().addLast( new LineBasedFrameDecoder( 10240 )); ch.pipeline().addLast( "decoder" , new StringDecoder()); ch.pipeline().addLast( "encoder" , new StringEncoder()); ch.pipeline().addLast( new ChannelInboundHandlerAdapter() { @Override public void channelRead(ChannelHandlerContext ctx, Object msg) { System.err.println( "server:" + msg.toString()); ctx.writeAndFlush(msg.toString() + "你好" ); } }); } |
改造客戶端發(fā)送代碼,再數(shù)據(jù)后面加上回車換行符
1
2
3
4
5
6
7
|
ChannelFuture f = b.connect( "127.0.0.1" , 2222 ).sync(); channel = f.channel(); StringBuilder msg = new StringBuilder(); for ( int i = 0 ; i < 100 ; i++) { msg.append( "hello yinjihuan" ); } channel.writeAndFlush(msg + System.getProperty( "line.separator" )); |
DelimiterBasedFrameDecoder
DelimiterBasedFrameDecoder和LineBasedFrameDecoder差不多,DelimiterBasedFrameDecoder可以自己定義需要分割的符號,比如下劃線,中劃線等等。
改造服務端代碼,只需加上DelimiterBasedFrameDecoder解碼器即可,構造函數(shù)的參數(shù)是數(shù)據(jù)包的最大長度。我們用下劃線來分割。
1
2
3
4
5
6
7
8
9
10
11
12
|
public void initChannel(SocketChannel ch) throws Exception { ch.pipeline().addLast( new DelimiterBasedFrameDecoder( 10240 , Unpooled.copiedBuffer( "_" .getBytes()))); ch.pipeline().addLast( "decoder" , new StringDecoder()); ch.pipeline().addLast( "encoder" , new StringEncoder()); ch.pipeline().addLast( new ChannelInboundHandlerAdapter() { @Override public void channelRead(ChannelHandlerContext ctx, Object msg) { System.err.println( "server:" + msg.toString()); ctx.writeAndFlush(msg.toString() + "你好" ); } }); } |
改造客戶端發(fā)送代碼,再數(shù)據(jù)后面加上下劃線
1
2
3
4
5
6
7
|
ChannelFuture f = b.connect( "127.0.0.1" , 2222 ).sync(); channel = f.channel(); StringBuilder msg = new StringBuilder(); for ( int i = 0 ; i < 100 ; i++) { msg.append( "hello yinjihuan" ); } channel.writeAndFlush(msg + "_" ); |
FixedLengthFrameDecoder
FixedLengthFrameDecoder是按固定的數(shù)據(jù)長度來進行解碼的,也就是說你客戶端發(fā)送的每條消息的長度是固定的,下面我們看看怎么使用。
服務端還是一樣,增加FixedLengthFrameDecoder解碼器即可。
1
2
3
4
5
6
7
8
9
10
11
12
|
public void initChannel(SocketChannel ch) throws Exception { ch.pipeline().addLast( new FixedLengthFrameDecoder( 1500 )); ch.pipeline().addLast( "decoder" , new StringDecoder()); ch.pipeline().addLast( "encoder" , new StringEncoder()); ch.pipeline().addLast( new ChannelInboundHandlerAdapter() { @Override public void channelRead(ChannelHandlerContext ctx, Object msg) { System.err.println( "server:" + msg.toString()); ctx.writeAndFlush(msg.toString() + "你好" ); } }); } |
客戶端,msg輸出的長度就是1500
1
2
3
4
5
6
7
8
|
ChannelFuture f = b.connect( "127.0.0.1" , 2222 ).sync(); channel = f.channel(); StringBuilder msg = new StringBuilder(); for ( int i = 0 ; i < 100 ; i++) { msg.append( "hello yinjihuan" ); } System.out.println(msg.length()); channel.writeAndFlush(msg); |
服務端代碼:
1
2
3
4
5
6
7
8
9
10
11
12
13
|
public void initChannel(SocketChannel ch) throws Exception { ch.pipeline().addLast( "frameDecoder" , new LengthFieldBasedFrameDecoder(Integer.MAX_VALUE, 0 , 4 , 0 , 4 )); ch.pipeline().addLast( "frameEncoder" , new LengthFieldPrepender( 4 )); ch.pipeline().addLast( "decoder" , new StringDecoder()); ch.pipeline().addLast( "encoder" , new StringEncoder()); ch.pipeline().addLast( new ChannelInboundHandlerAdapter() { @Override public void channelRead(ChannelHandlerContext ctx, Object msg) { System.err.println( "server:" + msg.toString()); ctx.writeAndFlush(msg.toString() + "你好" ); } }); } |
客戶端,直接發(fā)送就行
1
2
3
4
5
6
7
|
ChannelFuture f = b.connect( "127.0.0.1" , 2222 ).sync(); channel = f.channel(); StringBuilder msg = new StringBuilder(); for ( int i = 0 ; i < 100 ; i++) { msg.append( "hello yinjihuan" ); } channel.writeAndFlush(msg); |
源碼參考:https://github.com/yinjihuan/netty-im
以上就是本文的全部內(nèi)容,希望對大家的學習有所幫助,也希望大家多多支持服務器之家。
原文鏈接:https://blog.51cto.com/14888386/2516865