netty 解決粘包 和 分包的問題
netty 解決粘包 和 分包的問題
更多幹貨
概述
netty和tcp協議的關係
netty → Java Runtime Socket (io、nio、nio2) → OS Socket → TCP (當然也可以是UDP、SCTP);
作系統層的Socket都必須做三次握手(僅對TCP而言),Netty當然無法跳過,只不過它對使用者遮蔽了三次握手(當然還有四次揮手)的部分細節。
粘包現象
客戶端在一個for迴圈內連續傳送1000個hello給Netty伺服器端
Socket socket = new Socket("127.0.0.1", 10101);
for(int i = 0; i < 1000; i++){
socket.getOutputStream().write(“hello”.getBytes());
}
socket.close();
而在伺服器端接受到的資訊並不是預期的1000個獨立的Hello字串.
實際上是無序的hello字串混合在一起, 如圖所示. 這種現象我們稱之為粘包.
為什麼會出現這種現象呢? TCP是個”流”協議,流其實就是沒有界限的一串資料。
TCP底層中並不瞭解上層業務資料的具體含義,它會根據TCP緩衝區的實際情況進行包劃分,
所以在TCP中就有可能一個完整地包會被TCP拆分成多個包,也有可能吧多個小的包封裝成一個大的資料包傳送。
分包處理
顧名思義, 我們要對傳輸的資料進行分包. 一個簡單的處理邏輯是在傳送資料包之前, 先用四個位元組佔位, 表示資料包的長度.
資料包結構為:
Socket socket = new Socket("127.0.0.1", 10101);
String message = "hello";
byte[] bytes = message.getBytes();
ByteBuffer buffer = ByteBuffer.allocate(4 + bytes.length);
buffer.putInt(bytes.length);
buffer.put(bytes);
byte[] array = buffer.array();
for(int i=0; i<1000; i++){
socket.getOutputStream().write(array);
}
socket.close();
伺服器端程式碼, 我們需要藉助於FrameDecoder類來分包.
if(buffer.readableBytes() > 4){
if(buffer.readableBytes() > 2048){
buffer.skipBytes(buffer.readableBytes());
}
//標記
buffer.markReaderIndex();
//長度
int length = buffer.readInt();
if(buffer.readableBytes() < length){
buffer.resetReaderIndex();
//快取當前剩餘的buffer資料,等待剩下資料包到來
return null;
}
//讀資料
byte[] bytes = new byte[length];
buffer.readBytes(bytes);
//往下傳遞物件
return new String(bytes);
}
//快取當前剩餘的buffer資料,等待剩下資料包到來
return null;
這邊可能有個疑問, 為什麼MyDecoder中資料沒有讀取完畢, 需要return null,
正常的pipeline在資料處理完都是要sendUpstream, 給下一個pipeline的.
這個需要看下FrameDecoder.messageReceived 的原始碼. 他在其中快取了一個cumulation物件,
如果return了null, 他會繼續往快取裡寫資料來實現分包
public void messageReceived(ChannelHandlerContext ctx, MessageEvent e) throws Exception {
Object m = e.getMessage();
if (!(m instanceof ChannelBuffer)) {
// 資料讀完了, 轉下一個pipeline
ctx.sendUpstream(e);
} else {
ChannelBuffer input = (ChannelBuffer)m;
if (input.readable()) {
if (this.cumulation == null) {
try {
this.callDecode(ctx, e.getChannel(), input, e.getRemoteAddress());
} finally {
this.updateCumulation(ctx, input);
}
} else {
// 快取上一次沒讀完整的資料
input = this.appendToCumulation(input);
try {
this.callDecode(ctx, e.getChannel(), input, e.getRemoteAddress());
} finally {
this.updateCumulation(ctx, input);
}
}
}
}
}
Socket位元組流攻擊
在上述程式碼中, 我們會在伺服器端為客戶端傳送的資料包長度, 預先分配byte陣列.
如果遇到惡意攻擊, 傳入的資料長度與內容 不匹配. 例如宣告資料長度為Integer.MAX_VALUE.
這樣會消耗大量的伺服器資源生成byte[], 顯然是不合理的.
因此我們還要加個最大長度限制.
if(buffer.readableBytes() > 2048){
buffer.skipBytes(buffer.readableBytes());
}
新的麻煩也隨之而來, 雖然可以跳過指定長度, 但是資料包本身就亂掉了.
因為長度和內容不匹配, 跳過一個長度後, 不知道下一段資料的開頭在哪裡了.
因此我們自定義資料包裡面, 不僅要引入資料包長度, 還要引入一個包頭來劃分各個包的範圍.
包頭用任意一段特殊字元標記即可, 例如$$$.
// 防止socket位元組流攻擊
if(buffer.readableBytes() > 2048){
buffer.skipBytes(buffer.readableBytes());
}
// 記錄包頭開始的index
int beginReader = buffer.readerIndex();
while(true) {
if(buffer.readInt() == ConstantValue.FLAG) {
break;
}
}
新的資料包結構為:
| 包頭(4位元組) | 長度(4位元組) | 資料 |
Netty自帶拆包類
自己實現拆包雖然可以細粒度控制, 但是也會有些不方便, 可以直接呼叫Netty提供的一些內建拆包類.
FixedLengthFrameDecoder 按照特定長度組包
DelimiterBasedFrameDecoder 按照指定分隔符組包, 例如本文中的$$$
LineBasedFrameDecoder 按照換行符進行組包, \r \n等等
程式碼
Server
package com.server;
import java.net.InetSocketAddress;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import org.jboss.netty.bootstrap.ServerBootstrap;
import org.jboss.netty.channel.ChannelPipeline;
import org.jboss.netty.channel.ChannelPipelineFactory;
import org.jboss.netty.channel.Channels;
import org.jboss.netty.channel.socket.nio.NioServerSocketChannelFactory;
import org.jboss.netty.handler.codec.string.StringDecoder;
import org.jboss.netty.handler.codec.string.StringEncoder;
public class Server {
public static void main(String[] args) {
//服務類
ServerBootstrap bootstrap = new ServerBootstrap();
//boss執行緒監聽埠,worker執行緒負責資料讀寫
ExecutorService boss = Executors.newCachedThreadPool();
ExecutorService worker = Executors.newCachedThreadPool();
//設定niosocket工廠
bootstrap.setFactory(new NioServerSocketChannelFactory(boss, worker));
//設定管道的工廠
bootstrap.setPipelineFactory(new ChannelPipelineFactory() {
@Override
public ChannelPipeline getPipeline() throws Exception {
ChannelPipeline pipeline = Channels.pipeline();
pipeline.addLast("decoder", new MyDecoder());
pipeline.addLast("handler1", new HelloHandler());
return pipeline;
}
});
bootstrap.bind(new InetSocketAddress(10101));
System.out.println("start!!!");
}
}
Client
package com.server;
import java.net.Socket;
import java.nio.ByteBuffer;
public class Client {
public static void main(String[] args) throws Exception {
Socket socket = new Socket("127.0.0.1", 10101);
String message = "hello";
byte[] bytes = message.getBytes();
ByteBuffer buffer = ByteBuffer.allocate(4 + bytes.length);
buffer.putInt(bytes.length);
buffer.put(bytes);
byte[] array = buffer.array();
for(int i=0; i<1000; i++){
socket.getOutputStream().write(array);
}
socket.close();
}
}
HelloHandler
package com.server;
import org.jboss.netty.channel.ChannelHandlerContext;
import org.jboss.netty.channel.MessageEvent;
import org.jboss.netty.channel.SimpleChannelHandler;
public class HelloHandler extends SimpleChannelHandler {
private int count = 1;
@Override
public void messageReceived(ChannelHandlerContext ctx, MessageEvent e) throws Exception {
System.out.println(e.getMessage() + " " +count);
count++;
}
}
MyDecoder
package com.server;
import org.jboss.netty.buffer.ChannelBuffer;
import org.jboss.netty.channel.Channel;
import org.jboss.netty.channel.ChannelHandlerContext;
import org.jboss.netty.handler.codec.frame.FrameDecoder;
public class MyDecoder extends FrameDecoder {
@Override
protected Object decode(ChannelHandlerContext ctx, Channel channel, ChannelBuffer buffer) throws Exception {
if(buffer.readableBytes() > 4){
if(buffer.readableBytes() > 2048){
buffer.skipBytes(buffer.readableBytes());
}
//標記
buffer.markReaderIndex();
//長度
int length = buffer.readInt();
if(buffer.readableBytes() < length){
buffer.resetReaderIndex();
//快取當前剩餘的buffer資料,等待剩下資料包到來
return null;
}
//讀資料
byte[] bytes = new byte[length];
buffer.readBytes(bytes);
//往下傳遞物件
return new String(bytes);
}
//快取當前剩餘的buffer資料,等待剩下資料包到來
return null;
}
}
相關文章
- Socket 粘包和分包問題
- Netty--粘包與分包Netty
- Netty解決粘包和拆包問題的四種方案Netty
- Netty入門系列(2) --使用Netty解決粘包和拆包問題Netty
- socket的半包,粘包與分包的問題
- Netty解決半包(TCP粘包/拆包導致)讀寫問題NettyTCP
- Netty粘包&半包解決方案Netty
- Netty中使用MessagePack時的TCP粘包問題與解決方案NettyTCP
- Netty原始碼學習6——netty編碼解碼器&粘包半包問題的解決Netty原始碼
- 深入學習Netty(5)——Netty是如何解決TCP粘包/拆包問題的?NettyTCP
- Netty2:粘包/拆包問題與使用LineBasedFrameDecoder的解決方案Netty
- java nio解決半包 粘包問題Java
- TCP 粘包 - 拆包問題及解決方案TCP
- Netty 中的粘包和拆包Netty
- Netty拾遺(七)——粘包與拆包問題Netty
- Netty如何解決粘包拆包?(二)Netty
- 粘包問題
- 粘包問題原因和解決方法
- Netty - 粘包與拆包Netty
- TCP粘包拆包問題TCP
- 結合RPC框架通訊談 netty如何解決TCP粘包問題RPC框架NettyTCP
- TCP協議粘包問題詳解TCP協議
- Go TCP 粘包問題GoTCP
- 粘包拆包及解決方案
- Netty Protobuf處理粘包分析Netty
- Netty(三) 什麼是 TCP 拆、粘包?如何解決?NettyTCP
- 25. Socket與粘包問題
- 訊息粘包 和 訊息不完整 問題
- 從零開始netty學習筆記之TCP粘包和拆包Netty筆記TCP
- 粘包問題、socketserver模組實現併發Server
- 01揹包問題的解決
- java nio訊息半包、粘包解決方案Java
- 計算機網路 - TCP粘包、拆包以及解決方案計算機網路TCP
- 修改labelme原始碼,解決粘連mask分離問題原始碼
- 關於Android檔案數過大,分包問題的解決辦法Android
- 【Socket】解決UDP丟包問題UDP
- 詳說tcp粘包和半包TCP
- 通過大量實戰案例分解Netty中是如何解決拆包黏包問題的?Netty