從零開始學習Netty - 學習筆記 -Netty入門【半包,黏包】

Netty進階

1.黏包半包

1.1.黏包

服務端代碼

public class HelloWorldServer {private static final Logger logger = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());public static void main(String[] args) {NioEventLoopGroup bossGroup = new NioEventLoopGroup();NioEventLoopGroup workerGroup = new NioEventLoopGroup();try {ServerBootstrap serverBootstrap = new ServerBootstrap();serverBootstrap.channel(NioServerSocketChannel.class);serverBootstrap.group(bossGroup, workerGroup);serverBootstrap.childHandler(new ChannelInitializer<SocketChannel>() {@Overrideprotected void initChannel(SocketChannel channel) throws Exception {channel.pipeline().addLast(new LoggingHandler(LogLevel.DEBUG));}});ChannelFuture channelFuture = serverBootstrap.bind(8080).sync();channelFuture.channel().closeFuture().sync();} catch (Exception e) {logger.error("server error !", e);} finally {bossGroup.shutdownGracefully();workerGroup.shutdownGracefully();}}
}

客戶端代碼

public class HelloWorldClient {private static final Logger logger = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());public static void main(String[] args) {NioEventLoopGroup worker = new NioEventLoopGroup();try {Bootstrap bootstrap = new Bootstrap();bootstrap.channel(NioSocketChannel.class);bootstrap.group(worker);bootstrap.handler(new ChannelInitializer<SocketChannel>() {@Overrideprotected void initChannel(SocketChannel channel) throws Exception {channel.pipeline().addLast(new ChannelInboundHandlerAdapter() {// 會在連接 channel 成功后,觸發active 事件@Overridepublic void channelActive(ChannelHandlerContext ctx) throws Exception {super.channelActive(ctx);// 連接建立后,模擬發送數據,每次發送 16個字節 一共發送 10 次for (int i = 0; i < 10; i++) {ByteBuf buffer = ctx.alloc().buffer(16);buffer.writeBytes(new byte[]{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15});// 寫入channelchannel.writeAndFlush(buffer);}}});}});ChannelFuture channelFuture = bootstrap.connect("127.0.0.1", 8080).sync();channelFuture.channel().closeFuture().sync();} catch (Exception e) {logger.error("client error!");} finally {worker.shutdownGracefully();}}
}

image-20240302100630626

半包

需要對服務端 和客戶端 的代碼稍微修改下

// 設置每次接收緩沖區的大小,所以但是客戶端每次發送的是16個字節 所以可以模擬半包情況
serverBootstrap.option(ChannelOption.SO_RCVBUF,10);// 注意 如果不生效的話,建議服務端也設置響應的緩沖區大小
// 設置發送方緩沖區大小
bootstrap.option(ChannelOption.SO_SNDBUF, 10);

image-20240302102147457

1.2.滑動窗口

TCP以一個段(segment)為單位,每次發送一個段就需要進行一次確認應答(ACK),為了保證消息傳輸過程的穩定性,但是這樣做的缺點就是會導致包的往返時間越長,性能就越差。

  • 為了解決這個問題,引入窗口的概念,窗口的大小決定了無需等待應答而可以繼續發送數據的最大值

  • 窗口實際就起到一個緩沖區的作用,同時也能起到流量控制的作用

    • 圖中深色的部門表示即將要發送的數據,高亮的部分就是窗口
    • 窗口內的數據才允許被發送,當應答未到達前,窗口必須停止滑動
    • 如果1001 - 2000 這個段的數據ACK回來了,窗口就可以向前滑動
    • 接收方也會維護一個窗口,只有落在窗口內的數據才允許接收

1.3.黏包半包現象分析

  1. 黏包
    • 現象
      • 發送 abc def 接收 abdcef
    • 原因
      • 應用層:接收方ByteBuf設置太大(Netty默認1024)
      • 滑動窗口:假設發送方 256 bytes 表示一個完整報文,但是由于接收方處理不及時,且窗口大小足夠大,這256 bytes 字節就會緩沖在接收方的滑動窗口中,當滑動窗口緩沖了多個報文就會黏包
      • Nagle算法:會造成黏包
  2. 半包
    • 現象:發送 abcefg 接收方 abc efg
    • 原因
      • 應用層:接收方ByteBuf 設置容量大小,小于實際發送的數據量
      • 滑動窗口:假設接收方的窗口只剩下了,128byte,發送方的報文大小是 256 byte,這時就會放不下,只能先發送 128 byte數據,然后等待ack確認后,才能發送剩下的部門,這時就造成了半包。
      • MSS限制:當發送的數據超過了MSS的限制后,會將數據切割,然后分批發送,就會造成半包
        • 為什么在數據傳輸截斷存在數據分割呢?一個TCP報文的有效數據(凈荷數據)是有大小容量限制的,這個報文有效數據的大小就被稱為**MSS(Mixinum Segment Size) 最大報文字段長度**。具體MSS的值會在三次握手階段進行協商,但是最大長度不會超過**1460**個字節

出現黏包半包的主要原因就是 TCP的消息沒有邊界

1.4.黏包半包解決

1.4.1.短鏈接(解決黏包)

客戶端發送完后立馬進行斷開

短鏈接并不能半包問題

短鏈接雖然能解決黏包問題,但是缺點也是很明顯的

  • 連接建立開銷高,因為需要進行握手等操作。
  • 頻繁的連接管理會增加服務器負擔。
  • 可能導致資源浪費,如 TCP 連接的建立和釋放。
  • 存在網絡擁塞風險,特別是在高并發情況下。
  • 難以維護狀態,增加開發和維護的復雜性。
public class HelloWorldClient {private static final Logger logger = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());public static void main(String[] args) {// 短鏈接發發送for (int i = 0; i < 10; i++) {shortLinkedSend();}}/*** 短鏈接發送 測試*/private static void shortLinkedSend() {NioEventLoopGroup worker = new NioEventLoopGroup();try {Bootstrap bootstrap = new Bootstrap();bootstrap.channel(NioSocketChannel.class);// 設置發送方緩沖區大小bootstrap.option(ChannelOption.SO_SNDBUF, 10);bootstrap.group(worker);bootstrap.handler(new ChannelInitializer<SocketChannel>() {@Overrideprotected void initChannel(SocketChannel channel) throws Exception {channel.pipeline().addLast(new ChannelInboundHandlerAdapter() {// 會在連接 channel 成功后,觸發active 事件@Overridepublic void channelActive(ChannelHandlerContext ctx) throws Exception {super.channelActive(ctx);// 連接建立后,模擬發送數據ByteBuf buffer = ctx.alloc().buffer(16);buffer.writeBytes(new byte[]{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15});// 發送數據ctx.writeAndFlush(buffer);// 主動斷開鏈接ctx.channel().close();}});}});ChannelFuture channelFuture = bootstrap.connect("127.0.0.1", 8080).sync();channelFuture.channel().closeFuture().sync();} catch (Exception e) {logger.error("client error!");} finally {worker.shutdownGracefully();}}}

image-20240302135845886

image-20240302140556134

1.4.2.定長解碼器
  • 固定長度限制:消息長度必須是固定的,這限制了處理可變長度消息的能力。
  • 資源浪費:對于短消息,會浪費網絡帶寬和系統資源。
  • 消息邊界問題:無法處理不符合固定長度的消息,可能導致解碼器阻塞或消息邊界錯誤。
  • 不適用于多種消息類型:無法處理多種長度不同的消息類型。
  • 性能影響:對于長消息,可能會影響性能。

客戶端代碼

	public static void main(String[] args) {fixedLengthDecoder();}/*** 定長解碼器 測試*/private static void fixedLengthDecoder () {NioEventLoopGroup worker = new NioEventLoopGroup();try {Bootstrap bootstrap = new Bootstrap();bootstrap.channel(NioSocketChannel.class);// 設置發送方緩沖區大小bootstrap.option(ChannelOption.SO_SNDBUF, 10);bootstrap.group(worker);bootstrap.handler(new ChannelInitializer<SocketChannel>() {@Overrideprotected void initChannel(SocketChannel channel) throws Exception {channel.pipeline().addLast(new ChannelInboundHandlerAdapter() {// 會在連接 channel 成功后,觸發active 事件@Overridepublic void channelActive(ChannelHandlerContext ctx) throws Exception {super.channelActive(ctx);// 連接建立后,模擬發送數據ByteBuf buffer = ctx.alloc().buffer(16);for (int i = 0; i < 10; i++) {String s = "hello," + new Random().nextInt(100000000);logger.error("send data:{}", s);buffer.writeBytes(fillString(16, s));}// 發送數據ctx.writeAndFlush(buffer);}});}});ChannelFuture channelFuture = bootstrap.connect("127.0.0.1", 8080).sync();channelFuture.channel().closeFuture().sync();} catch (Exception e) {logger.error("client error!");} finally {worker.shutdownGracefully();}}/*** 編寫要給方法 給定一個長度,和數值,* 例如長度 16  數值 abc 剩下的填充**/private static byte[] fillString(int length, String value) {if (value.length() > length) {return value.substring(0, length).getBytes();}StringBuilder sb = new StringBuilder(value);for (int i = 0; i < length - value.length(); i++) {sb.append("*");}return sb.toString().getBytes();}

服務端

服務端的代碼沒有太大改動

@Override
protected void initChannel(SocketChannel channel) throws Exception {// 在打印日志前添加了定長解碼器// 添加定長解碼器 16  消息長度必須發送方 和 接收方一致// 注意順序,必須要先解碼,然后才能打印日志channel.pipeline().addLast(new FixedLengthFrameDecoder(16));channel.pipeline().addLast(new LoggingHandler(LogLevel.DEBUG));
}

image-20240302142715768

image-20240302142842684

1.4.3.行解碼器(分隔符)

\r \r\n

客戶端

這里的客戶端 代碼 和上面一致,我們只針對客戶端消息代碼進行修改

// 每次發送消息的結尾加上換行符
String s = "hello," + new Random().nextInt(100000000) + "\n";

服務端

用的不多

// 添加行解碼器,設置每次接收的數據大小
// 注意順序,必須要先解碼,然后才能打印日志
channel.pipeline().addLast(new LineBasedFrameDecoder(1024));

image-20240302151110375

1.4.4.LTC解碼器

LengthFieldBasedFrameDecoder方法的工作原理以及各個參數的含義:

  1. maxFrameLength(最大幀長度):這個參數指定了一個幀的最大長度。當接收到的幀長度超過這個限制時,解碼器會拋出一個異常。設置一個適當的最大幀長度可以防止你的應用程序受到惡意或錯誤消息的影響。
  2. lengthFieldOffset(長度字段偏移量):這個參數表示長度字段的偏移量,也就是在接收到的字節流中,長度字段從哪里開始的位置。通常,這個偏移量是相對于字節流的起始位置而言的。
  3. lengthFieldLength(長度字段長度):這個參數指定了長度字段本身所占用的字節數。在接收到的字節流中,長度字段通常是一個固定長度的整數,用來表示消息的長度。
  4. lengthAdjustment(長度調整值):在某些情況下,長度字段可能包括了消息頭的長度,而不是整個消息的長度。這個參數允許你進行一些調整,以便準確地計算出消息的實際長度。
  5. initialBytesToStrip(要剝離的初始字節數):在解碼器將幀傳遞給處理器之前,會先從幀中剝離一些字節。通常,這些字節是長度字段本身,因為處理器只需要處理消息的有效負載部分。這個參數告訴解碼器要剝離的初始字節數。
Client 客戶端 LengthFieldBasedFrameDecoder NextHandler 下一個處理器 Network 網絡 發送字節流 接收字節流 讀取長度字段 解析長度字段來確定消息的長度 返回等待更多數據 讀取完整消息 傳遞完整消息給下一個處理器 alt [消息長度不足] [消息長度足夠] loop [消息解析過程] 處理完整的消息 Client 客戶端 LengthFieldBasedFrameDecoder NextHandler 下一個處理器 Network 網絡

假設有一個網絡協議,它的消息格式如下:

  • 消息長度字段占據前4個字節。
  • 長度字段之后是實際的消息內容。

現在假設你收到了一個包含以上格式的字節流。你希望用Netty的LengthFieldBasedFrameDecoder來解碼這個消息。

在這種情況下,你需要設置以下參數:

  • lengthFieldOffset: 偏移量為0,因為長度字段從消息的開頭開始。
  • lengthFieldLength: 長度字段本身是4個字節。
  • lengthAdjustment: 在這種情況下,長度字段表示的是消息內容的長度,不包括長度字段本身,所以這個值是0。
  • initialBytesToStrip: 需要剝離長度字段本身,也就是4個字節。(因為用4個字節表示了字段的長度)

假設你收到的字節流如下:

[消息長度字段] [消息內容]
[0, 0, 0, 5] [72, 101, 108, 108, 111]
  • 長度字段 [0, 0, 0, 5] 表示消息長度為5個字節。
  • 后面的5個字節 [72, 101, 108, 108, 111] 則是實際的消息內容,代表著 “Hello”。

LengthFieldBasedFrameDecoder 將會將這個字節流解析成一條消息,其中包含了 “Hello” 這個字符串。

測試

public class TestLengthFiledDecoder {private static final Logger logger = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());public static void main(String[] args) {// 創建一個 EmbeddedChannel 并添加一個 LengthFieldBasedFrameDecoder// 該解碼器會根據長度字段的值來解碼數據// EmbeddedChannel 是一個用于測試的 Channel 實現EmbeddedChannel channel = new EmbeddedChannel(/** maxFrameLength: 最大幀長度* lengthFieldOffset: 長度字段的偏移量* lengthFieldLength: 長度字段的長度* lengthAdjustment: 長度字段的值表示的長度與整個幀的長度之間的差值(如果消息后面再加上一個長度字段,那么這個字段的值就是lengthAdjustment*  sendInfo("Netty",buffer);后面再加上一個長度字段,那么這個字段的值就是lengthAdjustment) 不加會報錯* initialBytesToStrip: 解碼后的數據需要跳過的字節數*/new LengthFieldBasedFrameDecoder(1024, 0, 4, 0, 4),new LoggingHandler(LogLevel.DEBUG));ByteBuf buffer = ByteBufAllocator.DEFAULT.buffer();// 4 個字節內容的長度 實際內容sendInfo("Hello,World111111111111111111111111111111111", buffer);sendInfo("Hello", buffer);sendInfo("Netty",buffer);// 模擬寫入數據channel.writeInbound(buffer);}private static void sendInfo(String s, ByteBuf buffer) {byte[] bytes = s.getBytes();// 寫入內容 大端模式 寫入長度 4 個字節int length = bytes.length;buffer.writeInt(length);buffer.writeBytes(bytes);}
}

image-20240302170706970

image-20240302191215747

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/news/718073.shtml
繁體地址,請注明出處:http://hk.pswp.cn/news/718073.shtml
英文地址,請注明出處:http://en.pswp.cn/news/718073.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

Ubuntu上Jenkins自動化部署Gitee上VUE項目

文章目錄 1.安裝NodeJS插件2.配置全局工具配置-NodeJS環境變量3.新建自由風格的軟件項目任務4.配置General配置丟棄舊的構建配置參數化構建過程 5.配置源碼管理6.構建觸發器7.設置構建環境8.配置構建步驟9.配置構建后操作10測試構建 前文鏈接&#xff1a; Ubuntu上Jenkins自動…

java常用應用程序編程接口(API)——Instant,DateTimeFormatter,Period,Duration概述

前言&#xff1a; 整理下學習心得。打好基礎&#xff0c;daydayup&#xff01; Instant Instant是時間線上的某個時刻/時間戳&#xff0c;通過獲取Instant的對象可以拿到此刻的時間&#xff0c;該時間由兩部分組成&#xff1a;1&#xff0c;從1970年1月1日00:00:00開始走到此刻…

前端開發 VSCode 插件推薦

1、Chinese (Simplified) (簡體中文) Language Pack for Visual Studio Code VS Code 的中文&#xff08;簡體&#xff09;語言包&#xff0c;此中文&#xff08;簡體&#xff09;語言包為 VS Code 提供本地化界面。 下載地址&#xff1a;Chinese (Simplified) (簡體中文) La…

D*算法超詳解 (D星算法 / Dynamic A*算法/ Dstar算法)(死循環解決--跟其他資料不一樣奧)

所需先驗知識&#xff08;沒有先驗知識可能會有大礙&#xff0c;了解的話會對D*的理解有幫助&#xff09;&#xff1a;A*算法/ Dijkstra算法 何為D*算法 Dijkstra算法是無啟發的尋找圖中兩節點的最短連接路徑的算法&#xff0c;A*算法則是在Dijkstra算法的基礎上加入了啟發函數…

[JavaWeb玩耍日記]HTML+CSS+JS快速使用

目錄 一.標簽 二.指定css 三.css選擇器 四.超鏈接 五.視頻與排版 六.布局測試 七.布局居中 八.表格 九.表單 十.表單項 十一.JS引入與輸出 十二.JS變量&#xff0c;循環&#xff0c;函數 十三.Array與字符串方法 十四.自定義對象與JSON 十五.BOM對象 十六.獲取…

Network LSA 結構簡述

Network LSA主要用于描述一個區域內的網絡拓撲結構&#xff0c;包括網絡中的路由器和連接到這些路由器的網絡。它記錄了每個路由器的鄰居關系、連接狀態以及連接的度量值&#xff08;如帶寬、延遲等&#xff09;&#xff0c;以便計算最短路徑和構建路由表。display ospf lsdb n…

網關kong記錄接口處理請求和響應插件 tcp-log-with-body的安裝

tcp-log-with-body 介紹 Kong的tcp-log-with-body插件是一個高效的工具&#xff0c;它能夠轉發Kong處理的請求和響應。這個插件非常適用于需要詳細記錄API請求和響應信息的情景&#xff0c;尤其是在調試和排查問題時。 軟件環境說明 kong version 2.1.4 - 2.8.3 [可用親測]C…

二、數據結構——單鏈表,雙鏈表,棧,隊列,單調棧,單調隊列,KMP,Trie,并查集,堆,哈希表等內容。

對于鏈表來說&#xff0c;由于new操作時間太長&#xff0c;因此&#xff0c;算法題中一般使用靜態鏈表。 1.單鏈表 采用數組實現單鏈表&#xff0c;可以直接開兩個數據&#xff0c;一個數組存放數值&#xff0c;另外一個數據存放下一個元素&#xff08;指針&#xff09;。 示…

JavaScript“基本語法”筆記(自學第一天)!

一、JavaScript的用途和應用領域 JavaScript的應用領域非常廣泛&#xff0c;主要包括以下幾個方面&#xff1a; 網頁交互性: JavaScript最初是為了增強網頁的交互性而開發的&#xff0c;它可以控制網頁的行為、樣式和內容&#xff0c;使用戶能夠與網頁進行實時交互&#xff0c…

一個教材上的CMS網站源碼在Linux服務器上登錄時驗證碼正常,但在windows下不能正常顯示

一個教材上的CMS網站源碼在Linux服務器上登錄時驗證碼正常&#xff0c;但在windows下不能正常顯示。 在linux服務器上能正常顯示。顯示界面如下所示&#xff1a;

蜻蜓FM語音下載(mediadown)

一、介紹 蜻蜓FM語音下載&#xff08;mediadown&#xff09;&#xff0c;能夠幫助你下載蜻蜓FM音頻節目。如果你是蜻蜓FM會員&#xff0c;它還能幫你下載會員節目。 二、下載地址 本站下載&#xff1a;蜻蜓FM語音下載&#xff08;mediadown&#xff09; 百度網盤下載&#…

Web 應用防火墻(WAF):功能、應用場景和未來發展方向

Web 應用防火墻&#xff08;WAF&#xff09;是一種用于保護 Web 應用程序免受各種網絡攻擊的安全工具。WAF 可以檢測并阻止對 Web 應用程序的惡意攻擊&#xff0c;如SQL 注入、跨站腳本&#xff08;XSS&#xff09;和跨站請求偽造&#xff08;CSRF&#xff09;等。它通過檢查 H…

【Redis 主從復制】

文章目錄 1 :peach:環境配置:peach:1.1 :apple:三種配置方式:apple:1.2 :apple:驗證:apple:1.3 :apple:斷開復制和切主:apple:1.4 :apple:安全性:apple:1.5 :apple:只讀:apple:1.6 :apple:傳輸延遲:apple: 2 :peach:拓撲結構:peach:2.1 :apple:?主?從結構:apple:2.2 :apple:?…

【MetaGPT】配置教程

MetaGPT配置教程&#xff08;使用智譜AI的GLM-4&#xff09; 文章目錄 MetaGPT配置教程&#xff08;使用智譜AI的GLM-4&#xff09;零、為什么要學MetaGPT一、配置環境二、克隆代碼倉庫三、設置智譜AI配置四、 示例demo&#xff08;狼羊對決&#xff09;五、參考鏈接 零、為什么…

大數據技術(一)

大數據技術概述 大數據技術層面及其功能 數據采集與預處理 利用ETL(extract-transform-load)工具將分布的、異構數據源中的數據&#xff0c;如關系數據、平面數據文件等&#xff0c;抽取到臨時中間層后進行清洗、轉換、集成&#xff0c;最后加載到數據倉庫或數據集市中&…

C語言什么是循環嵌套?

一、問題 分?結構是可以進?嵌套的&#xff0c;循環結構同樣也?持嵌套&#xff0c;那什么是循環嵌套呢&#xff1f; 二、解答 ?個循環體內?包含另?個完整的循環結構&#xff0c;就稱之為循環嵌套。內嵌的循環中還可以嵌套循環&#xff0c;這就是多層循環&#xff0c;也叫…

類與對象詳解 C++ (1)

1.struct和class 與C語言不同的是&#xff0c;C中struct和class可以定義成員變量和成員函數。更偏好用class。 2.類的定義 格式如下&#xff1a; class 為 定義類的 關鍵字&#xff0c; ClassName 為類的名字&#xff0c; {} 中為類的主體&#xff0c;注意 類定義結束時后面…

前端canvas項目實戰——簡歷制作網站(五):右側屬性欄(字體、字號、行間距)

目錄 前言一、效果展示二、實現步驟1. 優化代碼&#xff0c;提取常量2. 實現3個編輯模塊3. 實現updateFontProperty方法4. 一個常見的用法&#xff1a;僅更新當前選中文字的樣式 三、Show u the code后記 前言 上一篇博文中&#xff0c;我們擴充了線條對象&#xff08;fabric.…

springboot 整合oauth2

1、EnableOAuth2Client&#xff1a;客戶端&#xff0c;提供OAuth2RestTemplate&#xff0c;用于客戶端訪問資源服務。 簡要步驟&#xff1a;客戶端訪問資源->客戶端發現沒有資源訪問token->客戶端根據授權類型生成跳轉url->瀏覽器 302 到認證授權服務進行認證、授權。…

Dockerfile構建過程詳解

Dockerfile介紹 docker是用來構建docker鏡像的文件&#xff01;命令參數腳本&#xff01; 構建步驟&#xff1a; 1、編寫一個dockerfile文件 2、docker build構建成為一個鏡像 3、docker run 運行鏡像 …