上一節我們基於netty簡單構建了一個客戶端服務器,相互發送消息,但是前提是不考慮粘包和拆包的情況,今天我們來分析下粘包和拆包
什麼是粘包,什麼是拆包
熟悉TCP的都知道,TCP是一個"流協議",所謂的流,就是沒有界限的一串數據,連成一片的,比如我們客戶端發送2個數據包A、B給服務器,而每次發送包的多少具體會根據TCP緩衝區的實際情況進行包的劃分,所以A、B發送到服務器就會出現如下問題
UDP就像郵寄包裹一樣,雖然一次也是運輸多個,但是每個包裹都有界限,一個一個簽收,所以不會出現粘包、半包問題
- 服務端分兩次讀取到兩個獨立的數據包,分別是A和B,沒有粘包和拆包
- 服務端一次接收到兩個數據包,A和B粘和在一起,發生了TCP粘包
- 服務端分兩次讀取到了兩個數據包,第一次讀取到了A包的全部和B部分包,第二次讀取到了B包的剩下包內容,發生了TCP拆包
- 服務端分兩次讀取到了兩個數據包,第一次讀取到了A包的部分,第二次讀取到了A包剩下的部分+B包的全部
- 服務器TCP接受滑窗非常小,A和B數據包比較大,需要分很多次才能將A和B包完全接受,期間發生多次拆包
解決此類問題的根本手段:找出消息的邊界
方法 | 尋找消息邊界的方式 | 優點 | 缺點 | 推薦度 |
---|---|---|---|---|
TCP連接改爲短連接,一個請求一個短連接 | 建立連接釋放連接之間的信息即爲傳輸的信息 | 簡單 | 效率低下 | 不推薦 |
固定長度 | 消息統一滿足固定長度,不足補零或者其他 | 簡單 | 浪費空間 | 不推薦 |
分隔符 | 分隔符之間 | 空間不浪費,也比較簡單 | 內容本身出現分隔符需要轉義,需要掃描全部內容 | 推薦 |
固定長度字段存內容的長度信息 | 先解析固定長度的字段獲取長度,然後讀取後續內容 | 精確定位數據,內容不用轉義 | 長度理論上有限制,需提前預支可能的最大長度從而定義長度佔用字節 | 推薦 |
其他方式 | 例如JSON以{}是否成對出現 |
代碼實現
這裏是基於按行分隔符切換的文本解析碼: LineBasedFrameDecoder + StringDecoder
代碼實現:
服務器端
NettyServer
@Slf4j
public class NettyServer {
public static void main(String[] args) {
int port = 8080;
// 創建鏈各個Reactor線程租,一個用於服務端接受客戶端連接
// 一個用於SocketChannel的網絡讀寫
EventLoopGroup bossGroup = new NioEventLoopGroup();
EventLoopGroup workerGroup = new NioEventLoopGroup();
try {
// netty 用於啓動NIO服務端的輔助啓動類,不低降低服務端開發難度
ServerBootstrap b = new ServerBootstrap();
b.group(bossGroup, workerGroup)
.channel(NioServerSocketChannel.class)
.option(ChannelOption.SO_BACKLOG, 1024)//配置NioServerSocketChannel 的TCP 參數
.childHandler(new ChildChannelHeandler());// 綁定I/O事件處理類 ChildChannelHeandler
log.info("服務器啓動" + "端口 {}", port);
// 綁定端口,調用同步阻塞等待成功
ChannelFuture f = b.bind(port).sync();
//等待服務端監聽端口關閉
f.channel().closeFuture().sync();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
bossGroup.shutdownGracefully();
workerGroup.shutdownGracefully();
}
}
}
class ChildChannelHeandler extends ChannelInitializer<SocketChannel> {
@Override
protected void initChannel(SocketChannel socketChannel) throws Exception {
socketChannel.pipeline().addLast(new LineBasedFrameDecoder(1024));
socketChannel.pipeline().addLast(new StringDecoder());
socketChannel.pipeline().addLast(new TimeServerHandler());
//添加解碼器 StringDecoder 和 LineBasedFrameDecoder
}
}
相比之前添加了 LineBasedFrameDecoder 和 StringDecoder解析器
TimeServerHandler
/**
* @author WH
* @version 1.0
* @date 2020/5/24 21:48
* @Description 繼承ChannelHandlerAdapter 對網絡事件進行讀寫
*/
@Slf4j
public class TimeServerHandler extends ChannelHandlerAdapter {
private int count;
//客戶端返回結果調用
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception{
System.out.println("返回消息");
String body = (String) msg;
//請求信息
log.info("客戶端請求參數boyd: {}", body);
// 以空格爲消息分割符號來進行包的拆分
String message = "服務端收到了你的消息:" + body + System.getProperty("line.separator");
log.info("服務端收到的消息總數count: {}", ++count);
ByteBuf resp = Unpooled.copiedBuffer(message.getBytes());
ctx.writeAndFlush(resp);
}
// 客戶端斷開連接監聽
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
log.info("客戶端斷開了連接");
ctx.close();
}
}
消息直接獲取 返回消息添加空格 System.getProperty(“line.separator”)
客戶端
NettyClient
public class NettyClient {
public static void main(String[] args) {
//配置客戶端NIO線程租
EventLoopGroup group = new NioEventLoopGroup();
try {
Bootstrap b = new Bootstrap();
b.group(group).channel(NioSocketChannel.class)
.option(ChannelOption.TCP_NODELAY, true)
.handler(new ChannelInitializer<SocketChannel>() {
@Override
public void initChannel(SocketChannel ch) throws Exception{
ch.pipeline().addLast(new LineBasedFrameDecoder(1024));
ch.pipeline().addLast(new StringDecoder());
ch.pipeline().addLast(new TimeClinetHandler());
//添加解碼器
}
});
//發起異步連接操作
ChannelFuture f = b.connect("127.0.0.1", 8080).sync();
//等待客戶端鏈路關閉
f.channel().closeFuture().sync();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
//優雅退出,釋放NIO線程租
group.shutdownGracefully();
}
}
}
相比之前添加了 LineBasedFrameDecoder 和 StringDecoder解析器
TimeClinetHandler
@Slf4j
public class TimeClinetHandler extends ChannelHandlerAdapter {
private byte[] req;
private int count;
public TimeClinetHandler() {
// 以空格爲消息分割符號來進行包的拆分
String meg = "你好服務器" + System.getProperty("line.separator");
req = meg.getBytes();
log.info("發送消息 req: {}", new String(req));
}
//當服務器TCP鏈路建立成功後,調用 channelActive 方法
@Override
public void channelActive(ChannelHandlerContext ctx) {
ByteBuf firstMessage = null;
log.info("開始發送消息");
for (int i = 0; i < 100; i++) {
firstMessage = Unpooled.buffer(req.length);
firstMessage.writeBytes(req);
ctx.writeAndFlush(firstMessage);
}
}
//當服務器返回應答消息時,調用 channelRead 方法
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception{
//直接獲取解碼的字符串
String body = (String) msg;
log.info("服務器返回消息爲:{},消息總數爲{}",body,++count);
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
log.warn("發生異常" + cause.getMessage());
ctx.close();
}
}
消息直接獲取 返回消息添加空格 System.getProperty(“line.separator”)
運行
ps 如果不使用解析器會發現服務器接受到的消息就只要兩條,這裏是正常的