【Java Nio Netty】基于TCP的簡單Netty自定義協議實現(萬字,全篇例子)

基于TCP的簡單Netty自定義協議實現(萬字,全篇例子)

前言

有一陣子沒寫博客了,最近在學習Netty寫一個實時聊天軟件,一個高性能異步事件驅動的網絡應用框架,我們常用的SpringBoot一般基于Http協議,而Netty是沒有十分明確的協議的,不過它內置了一些常用的通信協議,當然你也可以自定義協議。

一、要求

接下來的內容默認你已經有了最基本的JavaNettyNio知識,如果還沒有這方面的知識的話,可以先去小破站找個視頻學習學習。

二、通信協議

* 本文提到的通信協議都是指基于TCP的應用層通信協議,請勿理解錯誤。

1、協議基本單位

當數據在兩臺計算機上傳輸時,傳輸的數據以比特(Bit)為單位,就像01010100010010101...這種,但是以比特作為傳輸單位太過精細、太過底層,所以封裝一下它,將8bit封裝成一個單位,就成了字節(Byte),所以一個協議的基本單位是字節Byte。同樣的,因為字節是其他大多數高級數據類型的基本組成,所以通信協議的基本單位是字節。例如一串字節流可以被解析為視頻、圖片、字符串等等,它是通用的。

也就是說,我們要自定義一個通信協議,就必須得自己解析字節。在SpringBoot框架中,我們在Controller中能夠直接得到字符串、對象的原因是框架已經幫我們將字節解析好了,我們直接用就行,但是如果我們要自定義協議,就必須自力更生,自己定義格式并解析它。

2、協議格式

協議的格式不是固定的,協議只能是一個約定而不是強制要求。

舉個例子,假如你在晚自習上睡覺,你提前和同桌約定好,老師來了他就敲兩下桌子,班長來了他就敲三下桌子,那么這種約定就可以認定為是一個通信協議,但其并不是固定的,因為明晚、后晚…你可以約定其他方式,例如敲一下變成老師來了,敲兩下變成班長來了,踢你一下表示老師來了,踢你兩下表示班長來了。并不是固定的。

基于這種思想,我們可以定義一個簡單的通信協議,版本號為V1

請求地址  客戶端IP  請求正文

基于這個協議,假如我們有一個請求,它請求服務器的/test地址,客戶端IP是192.168.1.2,請求正文是hello,那么這個協議看起來就像:

/test192.168.1.2hello

將它轉為字節流就是(沒有空格,空格只是為了方便查看加的):

47 116 101 115 116 49 57 50 46 49 54 56 46 49 46 50 104 101 108 108 111

服務器在解析時,就可以解析[0,5]個字符串為請求地址[/test],解析[6,11]個字符串為客戶端IP[192.168.1.2],解析剩下的所有字符串為請求正文。

當然,為了形象一點舉了一個不太恰當的簡單例子,解析的不是字符串而是字節。

3、TCP的粘包半包

Ⅰ、問題描述

這個問題可能我一時半會解釋不清楚,導致粘包半包的原因很多,感興趣的可以去找找資料。

你只用知道,基于TCP時,數據并不是一次性達到的,而是分段到達的,例如我們上面舉的例子,那個協議數據:/test192.168.1.2hello,服務器在接收這些數據時它就有可能:

第一次收到:/test19
第二次收到:2.168.
第三次收到:1.2hello
...

它可能不會一次收全,可能要好幾次,所以我們上面定義的簡單的協議就有一個問題:它沒有消息邊界,就是當客戶端多次發送數據時,服務器無法知道哪些數據是哪次請求的。還是剛才的例子:

第一次收到:/test192.168.1.2he
第二次收到:llo/haha192.168.1.2hi

在這兩次數據中,客戶端分別發送了兩次請求:/test192.168.1.2hello/haha192.168.1.2hi,但是因為粘包半包的問題,服務器不知道哪條是哪條了,就會導致解析出錯。

Ⅱ、如何解決

解決這個問題有很多種方法,常見的方法有分隔符、標識請求長度等等。兩種方法我都舉個例子,你也可以自己想一個方法來解決,都是靈活的,解決方法不是固定的。

分隔符的方法也很簡單:我們在每次請求結束時,都添加一個特殊符號,用于標識這個請求結束了,服務器在解析時,遇到這個特殊符號,就知道這個請求結束了,后面的數據是新請求的了。例如我們以$為分隔符,服務器:

第一次收到:/test192.168.1.2
第二次收到:hello$/haha192.168.1.2hi

服務器在解析到$符時,就知道/test請求已經結束了,后面的數據是屬于/haha請求的了。但是這么做的話,有一個缺點,就是之后傳輸的正文數據中不能含有$符,不然解析依舊出錯,你也可以定義復雜一點的符號,例如幾個符號拼接也行:@$&...。不過我要說的是,其實你還可以用標識請求長度的方式解決。

標識請求長度就是客戶端在傳輸請求之前,先計算好整個請求有多少個字符(為了不復雜先說成字符吧,其實是字節),再傳輸數據,服務器在接收到數據后,會去讀取這個字段,查看整個請求有多少個字符,然后再根據這個數字讀取多少個字符。那這就需要一個字段用來專門存儲長度了。

基于這個需求,我們上面定義的協議就得小小的升級一下,變成V2

請求長度  請求地址  客戶端IP  請求正文

以后,服務器會先讀取開頭的長度,再根據長度讀取后面的數據,例如我們還是剛才的/test請求,那么它將會變成:

21/test192.168.1.2hello

因為 /test192.168.1.2hello 總共是21個字符,所以一開始就變為了21,服務器一讀取到開頭的數字21,就往下讀取21個字符,讀完后,就默認這個請求已經結束了,再往下的就是其他請求了。

當然,你也可以將長度字段包含在內,那就是:

23/test192.168.1.2hello

這個長度可以出現在整個請求體的任何地方(除了正文),只要你在服務器/客戶端解析的時候對應解析就行了。

暫時就介紹這個兩個簡單的方法,其他的方法你可以自己想,想出來了可以自己實現,原則是能解決問題就是好辦法。

三、創建協議

1、改正上面的說法

在上面的各個例子中,我為了例子不復雜說的是解析字符,其實解析的是字節(Byte)

字符是字符,字節是字節,它們不一樣, 是一個字符,你好 是一個字符串,而 -28(十進制) 它是一個字節,-28-67-96 它們三個字節組成了一個字符

***UTF8編碼下,常見的中文字符一般由3個字節組成,不常見的一般是4個字節組成。

***UTF8編碼下,英文字符一般由1個字節組成。

*** 數字的情況稍微復雜:
1、8位的數字一般占用1字節,范圍從 -128 到 127
2、16位的數字一般占2字節,范圍從 -32,768 到 32,767
3、32位數字一般占4字節,范圍從 -2,147,483,648 到 2,147,483,647
4、64位數字一般占8字節,范圍從 -9,223,372,036,854,775,808 到 9,223,372,036,854,775,807
5、128位數字一般占16字節,范圍很大,不寫了。
例如在Rust中,i324字節,它對應的Java數字類型是inti648字節,對應的Java類型是long,以此類推。
JavaScriptnumber類型是64位的,占8字節,所以js要想表達64位以下的就有點麻煩了。

2、SP協議

解釋了上面的錯誤后,可以開始正式自定義協議了,給這個協議取個名字,就叫SP協議吧,Simple Protocol,譯為簡單的協議。

Ⅰ、報文長度

首先,粘包半包的問題用長度字段解決,4個字節表示的32位數字就夠用了,它的范圍是-2,147,483,648 到 2,147,483,647,負的20億到正的20億,用來表示數據的話(不算負數):2147483648 / (1024 * 1024) = 2048 MB,也就是說32位數字所表示的數字范圍(正數)用來表示數據大小的話,可以表示2GB的數據,一個請求根本不可能達到這么大,所以32位的數字夠用。因為2147483648個字節就是2GB

那么協議開頭就是:

長度4字節
Ⅱ、魔數

在協議中添加一個魔數,用來標識這個報文是屬于SP協議的,服務器在網絡中讀取字節流時,如果在長度字節后沒有找到這個魔數,就證明該字節流不是SP協議的,就可以停止讀取接下來的數據了,可以做關閉連接、丟棄數據等操作,就好像,你去坐火車去北京,火車進站時你看第二節車廂上有沒有寫目的地北京,如果寫了,那么就是你要坐的火車,如果沒寫,那就證明不是你要坐的火車,你可以等下一趟。其實就是為整個協議打一個標記。

魔數用幾個字節都行,為了不重復,建議使用4字節的32位數字,那么協議的第二部分應該是:

長度4字節 魔數4字節
Ⅲ、客戶端身份

在多個客戶端連接時,服務器需要為每個客戶端頒發一個標識,用來區分不同的客戶端的請求,用幾個字節都行,為了不重復,建議使用32字節的uuid作為客戶端唯一標識。

那么協議第三部分是:

長度4字節 魔數4字節 客戶端標識32位
Ⅳ、請求路徑

請求路徑這塊比較靈活,你可以使用1字節的8位數字表示,也就是-128 到 127個數字。例如,你可以規定1就是登錄,2就是注冊等等。

我使用的是英文字符串的方式,也就是一個字符一個字節,但是路徑長度不是不變的,它會變化。例如 /test5個字節,但是 /hi3個字節,不能像剛才一樣用固定的長度來標識,那么就需要一個固定的路徑長度字段,用來表示后續路徑的長度。

于是協議的第四部分就是:

長度4字節 魔數4字節 客戶端標識32位 路徑長度4字節 路徑N字節
Ⅴ、請求正文

到這步后這個簡單的協議就基本完成了,后續的正文長度是不定的,但是我們有開頭的長度字段表示整個報文的長度,所以這個協議第五部分就是:

長度4字節 魔數4字節 客戶端標識32位 路徑長度4字節 路徑N字節 正文N字節

3、完整協議

協議定義到這后基本完成了,但是這只是一個簡單的例子,實際應用中肯定要復雜許多。

基于該協議,模擬一個請求,它請求/test路徑,使用Java字節碼文件同款的魔數0xCAFEBABE,請求正文是hello,那么這個協議組裝完成應該是這樣的:

| 54 | 0xCAFEBABE | 32位的UUID | 5 | /test | hello |

解釋一下,首先魔數占了4字節,UUID占了32字節,路徑長度占了4字節,路徑占了5字節,正文占了5字節,報文長度字段不計算在內,所以總長度是:4 + 32 + 4 + 5 + 5 = 54字節,這就是開頭54的由來。

路徑 /test 前的 5 就是表示 /test 所占的 5 字節。

至此,協議定義完成,任何只要遵守了這個協議的請求都能夠被Netty服務器識別。

四、服務器代碼實現

協議定義好了,該寫服務器代碼實現這個協議了。

1、Netty服務器啟動流程

首先得先來復習一下Netty的啟動流程,我們才知道如何實現這個協議。

快速啟動一個Netty服務器代碼:

public static void main(String[] args) {NioEventLoopGroup boss = new NioEventLoopGroup(1);// 處理連接NioEventLoopGroup worker = new NioEventLoopGroup();// 處理業務try {ChannelFuture channelFuture = new ServerBootstrap().group(boss, worker) // 設置線程組.channel(NioServerSocketChannel.class) // 使用NIO通信模式.childHandler(new ChannelInitializer<SocketChannel>() {@Overrideprotected void initChannel(SocketChannel socketChannel) throws Exception {// 在這里添加自定義的處理器}}).bind(8080).sync();// 綁定端口并啟動服務器System.out.println("Netty Server is starting...");channelFuture.channel().closeFuture().sync();// 監聽關閉} catch (InterruptedException e) {throw new RuntimeException(e);}finally {// 優雅的關閉線程組boss.shutdownGracefully();worker.shutdownGracefully();}
}

要想自定義一個協議,我們的重點在 initChannel() 方法上,它可以為Netty添加處理器,在TCP收到的數據傳過來的時候,處理原始的字節流數據

2、添加自定義處理器

Ⅰ、解釋ChannelInitializer的作用

為了啟動看起來清爽,我們可以將childHandler()所需的參數抽取出來:

public class CustomHandler extends ChannelInitializer<SocketChannel> {@Overrideprotected void initChannel(SocketChannel socketChannel) throws Exception {}
}

childHandler()中傳遞.childHandler(new CustomHandler())

原始的字節流數據在達到Netty的時候,Netty內部會在我們自定義的處理器之前先做一些處理,比如說將字節流數據封裝成ByteBuf對象等等,就像SprinigBoot我們添加自定義攔截器一樣,在我們添加的攔截器之前,SpringBoot就已經添加了許多內部的攔截器先一步處理過數據了。

也就是說,我們自定義處理器接收到的數據,其實是經過ByteBuf封裝過的字節流緩沖對象,ByteBuf對象其實就是對Java.NioByteBuffer的進一步封裝升級。

畫個簡陋的圖,自定義處理器處理數據的整個流程看起來像這樣:

在這里插入圖片描述

我們剛剛自定義的處理器初始化器就是這部分:

在這里插入圖片描述

它的作用就是往處理器鏈中添加一個個的自定義處理器,在ChannelInitializer中添加處理器也很簡單,繼承ChannelInitializer并實現它的initChannel方法,再通過initChannel的形參SocketChannel獲取到ChannelPipeline就可以添加了,代碼像這樣:

@Override
protected void initChannel(SocketChannel channel) throws Exception {ChannelPipeline pipeline = channel.pipeline();pipeline.addLast(處理器對象);// 添加一個個的處理器pipeline.addLast(處理器對象);// 添加一個個的處理器...
}
Ⅱ、出站(Outbound)和入站(Inbound)

我沒打錯字,是出站入站,不是出棧入棧,說白了其實就是數據進入Netty和數據從Netty發出,進入Netty的行為叫入站Netty往外發送數據的行為叫出站

所以處理器可以分為三種:入站處理器出站處理器入站出站處理器入站處理器專門處理進入Netty的數據,出站處理器專門處理從Netty發送的數據,而入站出站處理器則兩者都可以。

這些處理器看起來像這樣:

在這里插入圖片描述

*** 注意,出站處理器的順序是與入站相反的,出站是從尾巴上為第1個處理器,頭為最后一個處理器,處理數據時會按照順序一個一個進行。

有一個比喻可以很好理解它們之間的關系:
處理器鏈pipeline就像兩條相反的流水線,pipeline.addLast();方法就像在流水線上安排一個工人,調用一次就安排一個工人,只不過一些工人專門處理過來的貨物,一些工人專門處理過去的貨物。

好了,接下來我們開始代碼實現處理器了。

Ⅲ、處理器實現
①、處理長度

報文長度字段是我們自定義協議SP協議的第一個字段,所以第一個處理器我們先處理長度。

首先,這個處理器肯定是入站處理器,因為是客戶端發送來的數據,我們要解析。而入站處理器怎么寫呢?

其實Netty為我們提供了入站出站處理器的多個模板,我們需要繼承并寫上自己的實現就行了。

最簡單的入站處理器是SimpleChannelInboundHandler,源代碼我就不講了,不然又要講半天。我們新建一個類繼承它,這個類就叫CustomLengthHandler吧:

public class CustomLengthHandler extends SimpleChannelInboundHandler<ByteBuf> {@Overrideprotected void channelRead0(ChannelHandlerContext channelHandlerContext, ByteBuf byteBuf) throws Exception {}
}

為什么SimpleChannelInboundHandler的泛型是ByteBuf?其實這里不一定是固定的(不是第一個處理器的情況),你想是什么都可以,取決于上一個處理器傳遞給當前處理器什么東西,還記得我們上面的那個流程圖嗎?:

在這里插入圖片描述

一個一個的處理器處理完數據后,可以繼續往下傳遞數據,傳遞的數據就是自定義的。例如我從上一個處理器得到ByteBuf對象,我將其解析完后,封裝成一個對象MyObject,那么我可以往下傳遞這個MyObject對象,下一個處理器就不用再處理一遍ByteBuf原始數據了,下一個處理器直接處理MyBoject封裝好數據的對象就行了。類比一下,就好像上一個處理器給我當前處理器傳遞一個JSON字符串,我當前處理器處理JSON字符串,將其序列化為對象,并往下傳遞這個對象,那么下一個處理器就不用再處理原始的JSON字符串了,就這么個意思。

所以SimpleChannelInboundHandler的泛型就是上一個處理器,傳遞給當前處理器的數據的類型,剛才解釋過了,它并不是固定的,上面的CustomLengthHandler也可以這么寫:

public class CustomLengthHandler extends SimpleChannelInboundHandler<String> {@Overrideprotected void channelRead0(ChannelHandlerContext ctx, String str) throws Exception {// 上一個處理器給我傳遞了一個字符串}
}

也可以:

public class CustomLengthHandler extends SimpleChannelInboundHandler<Integer> {@Overrideprotected void channelRead0(ChannelHandlerContext ctx, Integer itg) throws Exception {// 上一個處理器給我傳遞了一個數字}
}

并不是固定的。

好了,不說廢話了,開始代碼實現:

因為我們是第一個入站處理器,上面我們也提到過,Netty內部會將數據封裝成ByteBuf,所以我們從上一個處理器接收到的數據其實是一個ByteBuf對象,所以第一個處理器的泛型必需為ByteBuf

public class CustomLengthHandler extends SimpleChannelInboundHandler<ByteBuf> {@Overrideprotected void channelRead0(ChannelHandlerContext ctx, ByteBuf buf) throws Exception {}
}

ByteBuf是一個字節緩沖區,我們可以從它讀取到字節數據,例如:

protected void channelRead0(ChannelHandlerContext ctx, ByteBuf buf) throws Exception {byte b = buf.readByte();// 讀取1個字節int i = buf.readInt();// 讀取4個字節,因為我們之前說了Java的int是4字節組成的buf.readShort();// 依次類推,讀取2字節buf.readLong();String str = buf.readBytes(5).toString(StandardCharsets.UTF_8);// 讀取5個字節并轉為字符串,注意編碼為UTF8
}

還記得嗎,在我們的SP協議中,我們定義前四個字節是報文長度,所以一開始我們先讀取4字節:

protected void channelRead0(ChannelHandlerContext ctx, ByteBuf buf) throws Exception {int msgLength = buf.readInt();// 報文長度
}

在得到這個報文長度字段后,我們需要對ByteBuf的長度做一下判斷,如果它的長度小于報文長度,那就說明數據還未全部到達,那我們先不做處理,等完全到達后再做處理,代碼像這樣:

protected void channelRead0(ChannelHandlerContext ctx, ByteBuf buf) throws Exception {int msgLength = buf.readInt();if (buf.readableBytes() < msgLength){ // 緩沖區中的數據不足 msgLength 個,暫不處理return;}// 讀取 msgLength 個字節,也就是整個報文長度的字節,它得到的就是整個報文的完整字節緩沖區ByteBuf bufNew = buf.readBytes(msgLength);// 讀取 msgLength 個字節,不包含 msgLength 占用的4字節// 為了效率也可以寫為:// ByteBuf bufNew = buf.readSlice(msgLength);ctx.fireChannelRead(bufNew);// 傳遞給下一個處理器
}

為什么要這樣寫?還記得一開始我提到的TCP粘包半包嗎?因為數據并不是一次完整到達的,所以我們必需處理數據部分達到的情況。ByteBuf就像一個蓄水池,從管道中一開始流進來一些水,但是這些水沒有達到蓄水池該有的蓄水量,所以不管它,等它滿足了蓄水量,我們再處理。

buf.readBytes(msgLength);就是一次性從蓄水池(ByteBuf)中獲取msgLength量的水(字節),并將它放到一個新的水池(ByteBuf bufNew)中,這個新的水池,包含了完整的水量(報文所有字節),接著往下傳遞這個新的水池ctx.fireChannelRead(bufNew);

定義完處理器后,還需要將它添加進處理器鏈中,還記得我們上面一開始定義的public class CustomHandler extends ChannelInitializer<SocketChannel>嗎?在其中添加:

public class CustomHandler extends ChannelInitializer<SocketChannel> {@Overrideprotected void initChannel(SocketChannel channel) throws Exception {ChannelPipeline pipeline = channel.pipeline();pipeline.addLast(new CustomLengthHandler());// 我們自定義的第一個長度處理器,它也是入站處理器1}
}

到此為止,這個超級簡單的報文長度處理器就寫完了,當然,這個處理器有很多的問題,它只作為演示,實際使用會有很多Bug,因為實際使用中要處理的情況有點復雜,好在Netty給我們提供了一個開箱即用的報文長度處理器,這也是為什么我寫得這么簡單的原因,因為只需了解簡單的原理而不需要深入探索,Netty有現成的。

這個處理器就是 LengthFieldBasedFrameDecoder,它的構造函數常用且重要的有5個參數,類型都是int,我們一個一個來看:
1、第一個參數maxFrameLength,是整個報文最大長度,說白了就是限制報文大小的,你的報文不可能無限大。
2、第二個參數lengthFieldOffset,是你的長度字段是從第幾個字節開始的,我們的SP協議定義了一開始就是長度字段,所以這個參數我們可以填0。
3、第三個參數lengthFieldLength,是你的長度字段占幾個字節,我們定義的SP協議指明了長度字段占4個字節,所以填4就行。
4、第四個參數lengthAdjustment,有點繞,是指沒有計算進長度,但是在報文中存在的數據的長度。例如你有數據:5ab,因為長度字段5占用4個字節,b占用1個字節,但是沒有把a占用的1個字節算進來,所以這個例子中,lengthAdjustment就得填1,如果是6ab,那么lengthAdjustment就得填0,因為你將a占用的1字節算進來了。
5、第五個參數initialBytesToStrip,是指最終得到的數據要跳過幾個字節,在我們的SP協議中,如果接下來的數據你不想要長度字段,那就可以跳過長度字段的4字節,initialBytesToStrip就可以填4,那么得到的數據中就不包含長度了。

基于我們的SP協議,最終得到的處理器應該是:

public class CustomHandler extends ChannelInitializer<SocketChannel> {@Overrideprotected void initChannel(SocketChannel channel) throws Exception {ChannelPipeline pipeline = channel.pipeline();// 長度處理器,它也是入站處理器1pipeline.addLast(new LengthFieldBasedFrameDecoder((1024 * 1024) * 50, // 限制最大報文長度為50MB0, 4, 0, 0));// 長度是從0開始的,長度字段4字節,偏移量為0,不跳過字節}
}
②、魔數校驗

長度處理完了,現在TCP粘包半包所帶來的問題我們解決了,接下來就是校驗魔數,新增一個入站處理器:

public class CustomMagicNumberHandler extends SimpleChannelInboundHandler<ByteBuf> {@Overrideprotected void channelRead0(ChannelHandlerContext channelHandlerContext, ByteBuf byteBuf) throws Exception {}
}

LengthFieldBasedFrameDecoder中傳遞過來的數據依舊是ByteBuf,所以泛型我們依舊寫成ByteBuf,到達這里的數據,其實還是原始的報文數據,只不過經過前面的處理它一定是完整的。

做一下簡單的魔數校驗:

public class CustomMagicNumberHandler extends SimpleChannelInboundHandler<ByteBuf> {@Overrideprotected void channelRead0(ChannelHandlerContext ctx, ByteBuf buf) throws Exception {buf.readInt();// 跳過開頭的4字節長度字段int magicNumber = buf.readInt();if (magicNumber != 0xCAFEBABE){ctx.close();// 魔數不正確,直接關閉連接}ctx.fireChannelRead(buf);}
}

將處理器添加進處理器鏈:

public class CustomHandler extends ChannelInitializer<SocketChannel> {@Overrideprotected void initChannel(SocketChannel channel) throws Exception {ChannelPipeline pipeline = channel.pipeline();// 長度處理器,它也是入站處理器1pipeline.addLast(new LengthFieldBasedFrameDecoder((1024 * 1024) * 50, // 限制最大報文長度為50MB0, 4,0, 0));// 長度是從0開始的,長度字段4字節,偏移量為0,不跳過字節pipeline.addLast(new CustomMagicNumberHandler());// 魔數處理器,入站處理器2}
}
③、為客戶端生成唯一值UUID或校驗客戶端的UUID是否存在

這里我就不寫了,其實就是簡單的頒發身份證明和校驗身份證明而已,生成一個唯一值,然后存儲到服務器上,這里判斷UUID是否存在在報文中,如果不存在為其生成一個UUID并存儲,如果存在,從服務器存儲的UUID中找看能不能找得到。
后面的代碼可以根據協議定義的規則解析。

④、其他規則實現

Ⅳ、需要注意的點
①、ByteBuf的讀取

ByteBuf在讀取的時候是不可回退的,就像迭代器,迭代到下一個就不能再回去讀上一個了,要想回去重新讀,必需得重置讀取:

buf.resetReaderIndex();

然后又從最開頭開始讀取。ByteBuf中數據的基本單位是字節,readInt()readLong()等方法實際上讀取的都是字節,只不過封裝了一下,將多個字節轉為對應Java類型了。

②、字符編碼

注意,解析協議時,客戶端與服務器都要使用相同的字符編碼,否則解析字節會對不上,因為有些字符編碼使用的字節數可能不太一樣。

③、業務邏輯處理

協議解析完后,將數據傳遞到業務邏輯時,可以使用Netty服務器啟動時的:

NioEventLoopGroup worker = new NioEventLoopGroup();

worker來處理業務邏輯,worker的本質其實是一個線程池。

其他的注意事項我想起來了后續會加,有什么問題可以評論區留言,看到會回復。

五、簡單封裝的框架

根據以上代碼的思路,我封裝了一個簡單的開源框架,主要處理SP協議的加強版,它包含了長度處理魔數客戶端標識路徑處理數據加密等操作(暫未做數據驗證)。

源代碼鏈接是:simple-netty-core,丟在gitee上了,為什么不是GitHub?因為我的電腦不科學上網的話,始終訪問不到GitHub,即使修改了host文件也訪問不到,所以干脆就將源代碼丟在gitee上了。

這個框架是我學習Netty時寫的,比較簡單,基本能使用,感興趣的可以參考一下,也歡迎貢獻。

寫在最后

最后疊個甲吧:以上內容是我個人理解,不保證全部正確,如有遺漏、錯誤等后續我會回來更新這篇博客,歡迎評論區指正。

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

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

相關文章

【2025最新計算機畢業設計】基于SSM校園歌手賽事管理系統【提供源碼+答辯PPT+文檔+項目部署】

作者簡介&#xff1a;?CSDN新星計劃導師、Java領域優質創作者、掘金/華為云/阿里云/InfoQ等平臺優質作者、專注于Java技術領域和學生畢業項目實戰,高校老師/講師/同行前輩交流。? 主要內容&#xff1a;&#x1f31f;Java項目、Python項目、前端項目、PHP、ASP.NET、人工智能…

Visual Studio 使用 GitHub Copilot 協助調試

&#x1f380;&#x1f380;&#x1f380;【AI輔助編程系列】&#x1f380;&#x1f380;&#x1f380; Visual Studio 使用 GitHub Copilot 與 IntelliCode 輔助編碼Visual Studio 安裝和管理 GitHub CopilotVisual Studio 使用 GitHub Copilot 擴展Visual Studio 使用 GitHu…

了解ARM的千兆以太網——RK3588

1. 簡介 本文并不重點講解調試內容&#xff0c;重點了解以太網在ARM設計中的框架以及在設備樹以及驅動的一個整體框架。了解作為一個驅動開發人員當拿到一款未開發過的ARM板卡應該怎么去把網卡配置使用起來。 2. 基礎知識介紹 在嵌入式ARM中實現以太網的解決方案通常有以下兩種…

Springboot家政服務管理系統

摘 要 科技進步的飛速發展引起人們日常生活的巨大變化&#xff0c;電子信息技術的飛速發展使得電子信息技術的各個領域的應用水平得到普及和應用。信息時代的到來已成為不可阻擋的時尚潮流&#xff0c;人類發展的歷史正進入一個新時代。在現實運用中&#xff0c;應用軟件的工作…

DC-9筆記

靶機信息 官網:DC: 9 ~ VulnHub 只有一個flag,官網上沒給其他提示 信息收集 nmap 192.168.66.2-254nmap 192.168.66.146 -A -p-開放了80端口,22端口是filtered的,被過濾? NMAP 六種端口狀態解讀_nmap filtered-CSDN博客 那來看看http服務吧 http(80) 頁腳是空白的,插件也…

STM32-筆記3-驅動蜂鳴器

1、復制03項目&#xff0c;重命名為04項目 打開04項目的Drivers/BSP/led文件夾&#xff0c;把led文件夾更改為beep文件夾&#xff0c;改文件夾內部的.c和.h文件更改為beep.c和beep.h文件&#xff0c;如下圖所示。 2、打開工程文件 出現彈窗&#xff0c;顯示找不到xx文件&#…

PHP開發日志 ━━ 基礎知識:四種不同的變量返回方式該如何調用

最近在給框架升級&#xff0c;其中涉及到古早的緩存系統升級&#xff0c;現在準備區分類型為混合、變量和普通文件&#xff0c;那么變量用什么形式存儲到緩存才能給后續開發帶來便利和通用性呢&#xff1f;于是就涉及到了本文的php基礎知識。 好吧&#xff0c;又是一個無用的知…

概率論得學習和整理30: 用EXCEL 描述泊松分布 poisson distribution

目錄 1 泊松分布的基本內容 1.1 泊松分布的關鍵點 1.1.1 屬于離散分布 1.1.2 泊松分布的特點&#xff1a;每個子區間內概率相等 &#xff0c; λ就是平均概率 1.2 核心參數 1.3 pmf公式 1.4 期望和方差 2 例1&#xff1a;用EXCEL計算泊松分布的概率 3 比較λ不同值時…

Java中的垃圾收集器

文章目錄 1. 理解不同類型的垃圾收集器1.1 Serial 收集器1.2 Parallel (吞吐量) 收集器1.3 CMS (Concurrent Mark-Sweep) 收集器1.4 G1 (Garbage First) 收集器1.5 ZGC 和 Shenandoah GC1.6 Epsilon GC1.7 ParNew 收集器1.8 Zing (Azul Systems) 2. 優化垃圾收集器的選擇和配置…

測試工程師八股文05|功能測試、業務測試

一、基礎概念 1、軟件測試分類 1??按照軟件產生的階段劃分 單元測試&#xff1a;針對程序源代碼進行測試【開發自測】集成測試&#xff1a;針對模塊之間功能交互進行測試系統測試&#xff1a;對整個系統&#xff08;功能、非功能&#xff09;進行全面測試驗收測試&#xff…

圖(dfs與bfs)算法2

進度&#xff1a;15/100 原題1&#xff1a; 給你一棵二叉樹的根節點 root &#xff0c;翻轉這棵二叉樹&#xff0c;并返回其根節點。 &#xff08;力扣的圖&#xff09; 原題2&#xff1a; 給定二叉樹的根節點 root &#xff0c;返回所有左葉子之和。 原題3&#xff1a; 給…

《鴻蒙開發-答案之書》字符串占位符格式化

《鴻蒙開發-答案之書》字符串占位符格式化 先在string.json定義&#xff1a; {"name":"message_arrive","value":"We will arrive at %s."}使用&#xff0c;它有兩種使用方式&#xff1a; 方式一&#xff1a; Text($r(app.string.…

Redis bitmaps 使用

應用場景&#xff1a; 記錄id為 1 的用戶&#xff0c;2024年12月簽到情況&#xff0c;并統計&#xff1b; 記錄 1號簽到 zxys-redis:0>setbit 1:202412 1 1 記錄 2號簽到 zxys-redis:0>setbit 1:202412 2 1 記錄 3號未簽到 zxys-redis:0>setbit 1:202412 3 0 …

【微服務】SpringBoot 整合Redis Stack 構建本地向量數據庫相似性查詢

目錄 一、前言 二、向量數據庫介紹 2.1 什么是向量數據庫 2.2 向量數據庫特點 2.3 向量數據庫使用場景 三、常用的向量數據庫解決方案 3.1 Milvus 3.1.1 Milvus是什么 3.1.2 Milvus主要特點 3.2 Faiss 3.2.1 Faiss是什么 3.2.2 Faiss主要特點 3.3 Pinecone 3.3.1 …

【數據庫】大二數據庫復習范圍 (快速版)幫助你快速復習數據庫

第一章 1. 信息=數據+語義 2:數據庫管理系統(database management system, DBMS) 3. 數據庫系統(database system, DBS)由數據庫、數據庫用戶、計算機硬件系統和計算機軟件系統等幾部分組成 4. 數據模型按應用層次可分為概念模型、邏輯模型和物理模型。 5.每個二維表…

FMIKit-Simulink 常見問題解決方案

將解壓后的文件夾添加到 MATLAB 路徑中&#xff1a; addpath(fullfile(pwd, FMIKit-Simulink-3.1));初始化 FMIKit&#xff1a; FMIKit.initialize(); 設置求解器rtwsfcnfmi.tlc、或grtfmi.tlc再CtrlB即可。 幫助文檔可查看導出FUM和導入FMU。 FMIKit-Simulink-3.1\html\index…

UE UMG 多級彈出菜單踩坑

多級彈出菜單 https://www.bilibili.com/video/BV1ub411J7nA 運行時添加 widget 的方法 create widget 然后 add child 到某個組件&#xff0c;比如 canvas 運行時修改 widget 位置的方法 set widget slot position 用起來沒效果 懷疑是因為我沒有傳入 slot 但是暫時不知…

sunset: midnight

https://www.vulnhub.com/entry/sunset-midnight,517/ 主機發現端口掃描 探測存活主機&#xff0c;8是靶機 nmap -sP 192.168.56.0/24 Starting Nmap 7.94SVN ( https://nmap.org ) at 2024-12-05 16:49 CST Nmap scan report for 192.168.56.1 …

【PyTorch】動態調整學習率 torch.optim.lr_scheduler.StepLR 調度器

文章目錄 1. torch.optim.lr_scheduler.StepLR 官方文檔詳解2. 使用示例2.1 官方提供使用示例2.2 自己寫代碼測試方法2.2.1 get_last_lr() 方法2.2.2 state_dict() 方法2.2.3 load_state_dict() 保存和加載調度器 3. 思考3.1 為什么需要state_dict()3.2 get_lr() 與 get_last_l…

伊克羅德與九科信息共同發布RPA+AI智能機器人解決方案

12月12日&#xff0c;伊克羅德信息在上海舉辦“創見AI&#xff0c;邁進智能化未來——科技賦能零售電商”活動&#xff0c;與九科信息、亞馬遜云科技共同探討與分享&#xff0c;融合生成式AI技術和智能自動化&#xff08;RPA,Robotic Process Automation&#xff09;在電商零售…