引言
在前面關(guān)于《Netty入門篇》的文章中黔宛,咱們已經(jīng)初步對Netty
這個著名的網(wǎng)絡(luò)框架有了認知近刘,本章的目的則是承接上文,再對Netty
中的一些進階知識進行闡述臀晃,畢竟前面的內(nèi)容中觉渴,僅闡述了一些Netty
的核心組件,想要真正掌握Netty
框架徽惋,對于它我們應(yīng)該具備更為全面的認知案淋。
一、Netty中的粘包半包問題
實際上粘包险绘、半包問題踢京,并不僅僅只在Netty
中存在,但凡基于TCP
協(xié)議構(gòu)建的網(wǎng)絡(luò)組件宦棺,基本都需要面臨這兩個問題瓣距,對于粘包問題,在之前關(guān)于《計算機網(wǎng)絡(luò)與協(xié)議簇-TCP沾包》中也曾講到過:
[圖片上傳失敗...(image-da6ecd-1671678429584)]
但當(dāng)時我寫成了沾包代咸,但實際上專業(yè)的術(shù)語解釋為:粘包蹈丸,這里我糾正一下,接著再簡單說清楚粘包和半包的問題:
粘包:這種現(xiàn)象就如同其名,指通信雙方中的一端發(fā)送了多個數(shù)據(jù)包逻杖,但在另一端則被讀取成了一個數(shù)據(jù)包奋岁,比如客戶端發(fā)送
123、ABC
兩個數(shù)據(jù)包弧腥,但服務(wù)端卻收成的卻是123ABC
這一個數(shù)據(jù)包厦取。造成這個問題的本質(zhì)原因,在前面TCP
的章節(jié)中講過管搪,這主要是因為TPC
為了優(yōu)化傳輸效率虾攻,將多個小包合并成一個大包發(fā)送,同時多個小包之間沒有界限分割造成的更鲁。
半包:指通信雙方中的一端發(fā)送一個大的數(shù)據(jù)包霎箍,但在另一端被讀取成了多個數(shù)據(jù)包,例如客戶端向服務(wù)端發(fā)送了一個數(shù)據(jù)包:
ABCDEFGXYZ
澡为,而服務(wù)端則讀取成了ABCEFG漂坏、XYZ
兩個包,這兩個包實際上都是一個數(shù)據(jù)包中的一部分媒至,這個現(xiàn)象則被稱之為半包問題(產(chǎn)生這種現(xiàn)象的原因在于:接收方的數(shù)據(jù)接收緩沖區(qū)過小導(dǎo)致的)顶别。
上述提到的這兩種網(wǎng)絡(luò)通信的問題具體該如何解決,這點咱們放到后面再細說拒啰,先來看看Netty
中的沾包和半包問題驯绎。
1.1、Netty的粘包谋旦、半包問題演示
這里也就不多說廢話了剩失,結(jié)合《Netty入門篇》的知識,快速搭建出一個服務(wù)端册着、客戶端的通信案例拴孤,如下:
// 演示數(shù)據(jù)粘包問題的服務(wù)端
public class AdhesivePackageServer {
public static void main(String[] args) {
EventLoopGroup group = new NioEventLoopGroup();
ServerBootstrap server = new ServerBootstrap();
server.group(group);
server.channel(NioServerSocketChannel.class);
server.childHandler(new ServerInitializer());
server.bind("127.0.0.1",8888);
}
}
// 演示粘包、半包問題的通用初始化器
public class ServerInitializer extends ChannelInitializer<SocketChannel> {
@Override
protected void initChannel(SocketChannel socketChannel) throws Exception {
socketChannel.pipeline().addLast(new LoggingHandler(LogLevel.DEBUG));
socketChannel.pipeline().addLast(new ChannelInboundHandlerAdapter() {
// 數(shù)據(jù)就緒事件:當(dāng)收到客戶端數(shù)據(jù)時會讀取通道內(nèi)的數(shù)據(jù)
@Override
public void channelReadComplete(ChannelHandlerContext ctx)
throws Exception {
// 在這里直接輸出通道內(nèi)的數(shù)據(jù)信息
System.out.println(ctx.channel());
super.channelReadComplete(ctx);
}
});
}
}
// 演示數(shù)據(jù)粘包問題的客戶端
public class AdhesivePackageClient {
public static void main(String[] args) {
EventLoopGroup worker = new NioEventLoopGroup();
Bootstrap client = new Bootstrap();
try {
client.group(worker);
client.channel(NioSocketChannel.class);
client.handler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel ch) throws Exception {
ch.pipeline().addLast(new ChannelInboundHandlerAdapter() {
// 在通道準備就緒后會觸發(fā)的事件
@Override
public void channelActive(ChannelHandlerContext ctx)
throws Exception {
// 向服務(wù)端發(fā)送十次數(shù)據(jù)甲捏,每次發(fā)送一個字節(jié)演熟!
for (int i = 0; i < 10; i++) {
System.out.println("正在向服務(wù)端發(fā)送第"+
i +"次數(shù)據(jù)......");
ByteBuf buffer = ctx.alloc().buffer(1);
buffer.writeBytes(new byte[]{(byte) i});
ctx.writeAndFlush(buffer);
}
}
});
}
});
client.connect("127.0.0.1", 8888).sync();
} catch (Exception e){
e.printStackTrace();
} finally {
worker.shutdownGracefully();
}
}
}
復(fù)制代碼
這個案例中的代碼也并不難理解,客戶端的代碼中摊鸡,會向服務(wù)端發(fā)送十次數(shù)據(jù)绽媒,而服務(wù)端僅僅只做了數(shù)據(jù)讀取的動作而已,接著來看看運行結(jié)果:
[圖片上傳失敗...(image-c1d075-1671678429582)]
從運行結(jié)果中可明顯觀測到免猾,客戶端發(fā)送的十個1Bytes
的數(shù)據(jù)包是辕,在服務(wù)端直接被合并成了一個10Bytes
的數(shù)據(jù)包,這顯然就是粘包的現(xiàn)象猎提,接著再來看看半包的問題获三,代碼如下:
// 演示半包問題的服務(wù)端
public class HalfPackageServer {
public static void main(String[] args) {
EventLoopGroup group = new NioEventLoopGroup();
ServerBootstrap server = new ServerBootstrap();
server.group(group);
server.channel(NioServerSocketChannel.class);
// 調(diào)整服務(wù)端的接收窗口大小為四字節(jié)
server.option(ChannelOption.SO_RCVBUF,4);
server.childHandler(new ServerInitializer());
server.bind("127.0.0.1",8888);
}
}
// 演示半包問題的客戶端
public class HalfPackageClient {
public static void main(String[] args) {
EventLoopGroup worker = new NioEventLoopGroup();
Bootstrap client = new Bootstrap();
try {
client.group(worker);
client.channel(NioSocketChannel.class);
client.handler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel ch) throws Exception {
ch.pipeline().addLast(new ChannelInboundHandlerAdapter() {
// 在通道準備就緒后會觸發(fā)的事件
@Override
public void channelActive(ChannelHandlerContext ctx)
throws Exception {
// 向服務(wù)端發(fā)送十次數(shù)據(jù)旁蔼,每次發(fā)送十個字節(jié)!
for (int i = 0; i < 10; i++) {
ByteBuf buffer = ctx.alloc().buffer();
buffer.writeBytes(new byte[]
{'a','b','c','d','e','f','g','x','y','z'});
ctx.writeAndFlush(buffer);
}
}
});
}
});
client.connect("127.0.0.1", 8888).sync();
} catch (Exception e){
e.printStackTrace();
} finally {
worker.shutdownGracefully();
}
}
}-
復(fù)制代碼
上面的代碼中疙教,客戶端向服務(wù)端發(fā)送了十次數(shù)據(jù)棺聊,每次數(shù)據(jù)會發(fā)送10
個字節(jié),而在服務(wù)端多加了下述這行代碼:
server.option(ChannelOption.SO_RCVBUF,4);
復(fù)制代碼
這行代碼的作用是調(diào)整服務(wù)端的接收窗口大小為四字節(jié)贞谓,因為默認的接收窗口較大限佩,客戶端需要一次性發(fā)送大量數(shù)據(jù)才能演示出半包現(xiàn)象,這里為了便于演示裸弦,因此將接收窗口調(diào)小祟同,運行結(jié)果如下:
[圖片上傳失敗...(image-dc21e5-1671678429582)]
從上述運行結(jié)果中,也能夠明顯觀察到半包現(xiàn)象理疙,客戶端發(fā)送的十個數(shù)據(jù)包晕城,每個包中的數(shù)據(jù)都為10
字節(jié),但服務(wù)端中窖贤,接收到的數(shù)據(jù)顯然并不符合預(yù)期砖顷,尤其是第三個數(shù)據(jù)包,是一個不折不扣的半包現(xiàn)象赃梧。
1.2滤蝠、粘包、半包問題的產(chǎn)生原因
前面簡單聊了一下粘包授嘀、半包問題几睛,但這些問題究竟是什么原因?qū)е碌哪兀繉τ谶@點前面并未深入探討粤攒,這里來做統(tǒng)一講解,想要弄明白粘包囱持、半包問題的產(chǎn)生原因夯接,這還得說回TCP
協(xié)議,大家還記得之前說過的TCP-滑動窗口嘛纷妆?
[圖片上傳失敗...(image-91c0e5-1671678429582)]
1.2.1盔几、TCP協(xié)議的滑動窗口
由于TCP
是一種可靠性傳輸協(xié)議,所以在網(wǎng)絡(luò)通信過程中掩幢,會采用一問一答的形式逊拍,也就是一端發(fā)送數(shù)據(jù)后,必須得到另一端返回ACK
響應(yīng)后际邻,才會繼續(xù)發(fā)送后續(xù)的數(shù)據(jù)芯丧。但這種一問一答的同步方式,顯然會十分影響數(shù)據(jù)的傳輸效率世曾。
TCP
協(xié)議為了解決傳輸效率的問題缨恒,引入了一種名為滑動窗口的技術(shù),也就是在發(fā)送方和接收方上各有一個緩沖區(qū),這個緩沖區(qū)被稱為“窗口”骗露,假設(shè)發(fā)送方的窗口大小為100KB
岭佳,那么發(fā)送端的前100KB
數(shù)據(jù),無需等待接收端返回ACK
萧锉,可以一直發(fā)送珊随,直到發(fā)滿100KB
數(shù)據(jù)為止。
如果發(fā)送端在發(fā)送前
100KB
數(shù)據(jù)時柿隙,接收端返回了某個數(shù)據(jù)包的ACK
叶洞,那此時發(fā)送端的窗口會一直向下滑動,比如最初窗口范圍是0~100KB
优俘,收到ACK
后會滑動到20~120KB京办、120~220KB....
(實際上窗口的大小、范圍帆焕,TCP
會根據(jù)網(wǎng)絡(luò)擁塞程度惭婿、ACK
響應(yīng)時間等情況來自動調(diào)整)。
同時叶雹,除開發(fā)送方有窗口外财饥,接收方也會有一個窗口,接收方只會讀取窗口范圍之內(nèi)的數(shù)據(jù)折晦,如果超出窗口范圍的數(shù)據(jù)并不會讀取钥星,這也就意味著不會對窗口之外的數(shù)據(jù)包返回ACK
,所以發(fā)送方在未收到ACK
時满着,對應(yīng)的窗口會停止向后滑動谦炒,并在一定時間后對未返回ACK
的數(shù)據(jù)進行重發(fā)。
對于TCP
的滑動窗口风喇,發(fā)送方的窗口起到優(yōu)化傳輸效率的作用宁改,而接收端的窗口起到流量控制的作用。
1.2.2魂莫、傳輸層的MSS與鏈路層的MTU
理解了滑動窗口的概念后还蹲,接著來說說MSS、MTU
這兩個概念耙考,MSS
是傳輸層的最大報文長度限制谜喊,而MTU
則是鏈路層的最大數(shù)據(jù)包大小限制,一般MTU
會限制MSS
倦始,比如MTU=1500
斗遏,那么MSS
最大只能為1500
減去報文頭長度,以TCP
協(xié)議為例楣号,MSS
最大為1500-40=1460
最易。
為什么需要這個限制呢怒坯?這是由于網(wǎng)絡(luò)設(shè)備硬件導(dǎo)致的,比如任意類型的網(wǎng)卡藻懒,不可能讓一個數(shù)據(jù)包無限增長剔猿,因為網(wǎng)卡會有帶寬限制,比如一次性傳輸一個1GB
的數(shù)據(jù)包嬉荆,如果不限制大小直接發(fā)送归敬,這會導(dǎo)致網(wǎng)絡(luò)出現(xiàn)堵塞,并且超出網(wǎng)絡(luò)硬件設(shè)備單次傳輸?shù)淖畲笙拗啤?/p>
所以當(dāng)一個數(shù)據(jù)包鄙早,超出
MSS
大小時汪茧,TCP
協(xié)議會自動切割這個數(shù)據(jù)包舒帮,將該數(shù)據(jù)包拆分成一個個的小包魔招,然后分批次進行傳輸,從而實現(xiàn)大文件的傳輸粱栖。
1.2.3弥虐、TCP協(xié)議的Nagle算法
基于MSS
最大報文限制扩灯,可以實現(xiàn)大文件的切割并分批發(fā)送,但在網(wǎng)絡(luò)通信中霜瘪,還有另一種特殊情況珠插,即是極小的數(shù)據(jù)包傳輸,因為TCP
的報文頭默認會有40
個字節(jié)颖对,如果數(shù)據(jù)只有1
字節(jié)捻撑,那加上報文頭依舊會產(chǎn)生一個41
字節(jié)的數(shù)據(jù)包。
如果這種體積較小的數(shù)據(jù)包在傳輸中經(jīng)常出現(xiàn)缤底,這定然會導(dǎo)致網(wǎng)絡(luò)資源的浪費顾患,畢竟數(shù)據(jù)包中只有
1
字節(jié)是數(shù)據(jù),另外40
個字節(jié)是報文頭个唧,如果出現(xiàn)1W
個這樣的數(shù)據(jù)包描验,也就意味著會產(chǎn)生400MB
的報文頭,但實際數(shù)據(jù)只占10MB
坑鱼,這顯然是不妥當(dāng)?shù)摹?/p>
正是由于上述原因,因此TCP
協(xié)議中引入了一種名為Nagle
的算法絮缅,如若連續(xù)幾次發(fā)送的數(shù)據(jù)都很小鲁沥,TCP
會根據(jù)算法把多個數(shù)據(jù)合并成一個包發(fā)出,從而優(yōu)化網(wǎng)絡(luò)傳輸?shù)男矢牵⑶覝p少對資源的占用画恰。
1.2.4、應(yīng)用層的接收緩沖區(qū)和發(fā)送緩沖區(qū)
對于操作系統(tǒng)的IO
函數(shù)而言吸奴,網(wǎng)絡(luò)數(shù)據(jù)不管是發(fā)送也好允扇,還是接收也罷缠局,并不會采用“復(fù)制”的方式工作,比如現(xiàn)在想要傳輸一個10MB
的數(shù)據(jù)考润,不可能直接將這個數(shù)據(jù)一次性拷貝到緩沖區(qū)內(nèi)狭园,而是一個一個字節(jié)進行傳輸,舉個例子:
假設(shè)現(xiàn)在要發(fā)送
ABCDEFGXYZ....
這組數(shù)據(jù)糊治,IO
函數(shù)會挨個將每個字節(jié)放到發(fā)送緩沖區(qū)中唱矛,會呈現(xiàn)A、B井辜、C绎谦、D、E粥脚、F....
這個順序挨個寫入窃肠,而接收方依舊如此,讀取數(shù)據(jù)時也會一個個字節(jié)讀取刷允,以A冤留、B、C恃锉、D搀菩、E、F....
這個順序讀取一個數(shù)據(jù)包中的數(shù)據(jù)(實際情況會復(fù)雜一些破托,可能會按一定單位操作數(shù)據(jù)肪跋,而并不是以單個字節(jié)作為單位)。
而應(yīng)用程序為了發(fā)送/接收數(shù)據(jù)土砂,通常都需要具備兩個緩沖區(qū)州既,即所說的接收緩沖區(qū)和發(fā)送緩沖區(qū),一個用來暫存要發(fā)送的數(shù)據(jù)萝映,另一個則用來暫存接收到的數(shù)據(jù)吴叶,同時這兩個緩沖區(qū)的大小,可自行調(diào)整其大行虮邸(Netty
默認的接收/發(fā)送緩沖區(qū)大小為1024KB
)蚌卤。
1.2.5、粘包奥秆、半包問題的產(chǎn)生原因
理解了上述幾個概念后逊彭,接著再來看看粘包和半包就容易很多了,粘包和半包問題构订,可能會由多方面因素導(dǎo)致侮叮,如下:
- 粘包:發(fā)送
12345、ABCDE
兩個數(shù)據(jù)包悼瘾,被接收成12345ABCDE
一個數(shù)據(jù)包囊榜,多個包粘在一起审胸。- 應(yīng)用層:接收方的接收緩沖區(qū)太大,導(dǎo)致讀取多個數(shù)據(jù)包一起輸出卸勺。
-
TCP
滑動窗口:接收方窗口較大砂沛,導(dǎo)致發(fā)送方發(fā)出多個數(shù)據(jù)包,處理不及時造成粘包孔庭。 -
Nagle
算法:由于發(fā)送方的數(shù)據(jù)包體積過小尺上,導(dǎo)致多個數(shù)據(jù)包合并成一個包發(fā)送。
- 半包:發(fā)送
12345ABCDE
一個數(shù)據(jù)包圆到,被接收成12345怎抛、ABCDE
兩個數(shù)據(jù)包,一個包拆成多個芽淡。- 應(yīng)用層:接收方緩沖區(qū)太小马绝,無法存方發(fā)送方的單個數(shù)據(jù)包,因此拆開讀取挣菲。
- 滑動窗口:接收方的窗口太小富稻,無法一次性放下完整數(shù)據(jù)包,只能讀取其中一部分白胀。
-
MSS
限制:發(fā)送方的數(shù)據(jù)包超過MSS
限制椭赋,被拆分為多個數(shù)據(jù)包發(fā)送。
上述即是出現(xiàn)粘包或杠、半包問題的根本原因哪怔,更多的是由于TCP
協(xié)議造成的,所以想要解決這兩個問題向抢,就得自己重寫底層的TCP
協(xié)議认境,這對于咱們而言并不現(xiàn)實,畢竟TCP/IP
協(xié)議棧挟鸠,基本涵蓋各式各樣的網(wǎng)絡(luò)設(shè)備叉信,想要從根源上解決粘包、半包問題艘希,重寫協(xié)議后還得替換掉所有網(wǎng)絡(luò)設(shè)備內(nèi)部的TCP
實現(xiàn)硼身,目前世界上沒有任何一個組織、企業(yè)覆享、個人具備這樣的影響力鸠姨。
1.3、粘包淹真、半包問題的解決方案
既然無法在底層從根源上解決問題,那此時可以換個思路连茧,也就是從應(yīng)用層出發(fā)核蘸,粘包巍糯、半包問題都是由于數(shù)據(jù)包與包之間,沒有邊界分割導(dǎo)致的客扎,那想要解決這樣的問題祟峦,發(fā)送方可以在每個數(shù)據(jù)包的尾部,自己拼接一個特殊分隔符徙鱼,接收方讀取到數(shù)據(jù)時宅楞,再根據(jù)對應(yīng)的分隔符讀取數(shù)據(jù)即可。
對于其他的一些網(wǎng)絡(luò)編程的技術(shù)棧袱吆,咱們不做過多延伸厌衙,重點來聊一聊Netty
中的粘包、半包問題該如何解決呢绞绒?其實這也并不需要自己動手解決婶希,因為Netty
內(nèi)部早已內(nèi)置了相關(guān)實現(xiàn),畢竟我們能想到的問題蓬衡,框架的設(shè)計者也早已料到喻杈,接著一起來看看Netty
的解決方案吧。
1.3.1狰晚、使用短連接解決粘包問題
對于短連接大家應(yīng)該都不陌生筒饰,HTTP/1.0
版本中,默認使用的就是TCP
短連接壁晒,這是指客戶端在發(fā)送一次數(shù)據(jù)后瓷们,就會立馬斷開與服務(wù)端的網(wǎng)絡(luò)連接,在客戶端斷開連接后讨衣,服務(wù)端會收到一個-1
的狀態(tài)碼换棚,而咱們可以用這個作為消息(數(shù)據(jù))的邊界,以此區(qū)分不同的數(shù)據(jù)包反镇,如下:
// 演示通過短連接解決粘包問題的服務(wù)端
public class AdhesivePackageServer {
public static void main(String[] args) {
EventLoopGroup group = new NioEventLoopGroup();
ServerBootstrap server = new ServerBootstrap();
server.group(group);
server.channel(NioServerSocketChannel.class);
server.childHandler(new ServerInitializer());
server.bind("127.0.0.1",8888);
}
}
// 演示通過短連接解決粘包問題的客戶端
public class Client {
public static void main(String[] args) {
for (int i = 0; i < 3; i++) {
sendData();
}
}
private static void sendData(){
EventLoopGroup worker = new NioEventLoopGroup();
Bootstrap client = new Bootstrap();
try {
client.group(worker);
client.channel(NioSocketChannel.class);
client.handler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel ch) throws Exception {
ch.pipeline().addLast(new ChannelInboundHandlerAdapter() {
// 在通道準備就緒后會觸發(fā)的事件
@Override
public void channelActive(ChannelHandlerContext ctx)
throws Exception {
// 向服務(wù)端發(fā)送一個20字節(jié)的數(shù)據(jù)包固蚤,然后斷開連接
ByteBuf buffer = ctx.alloc().buffer(1);
buffer.writeBytes(new byte[]
{'0','1','2','3','4',
'5','6','7','8','9',
'A','B','C','D','E',
'M','N','X','Y','Z'});
ctx.writeAndFlush(buffer);
ctx.channel().close();
}
});
}
});
client.connect("127.0.0.1", 8888).sync();
} catch (Exception e){
e.printStackTrace();
} finally {
worker.shutdownGracefully();
}
}
}
復(fù)制代碼
服務(wù)端的代碼,依舊用之前演示粘包問題的AdhesivePackageServer
歹茶,上述只對客戶端的代碼進行了改造夕玩,主要是將創(chuàng)建客戶端連接、發(fā)送數(shù)據(jù)的代碼抽象成了一個方法惊豺,然后在循環(huán)內(nèi)部調(diào)用該方法燎孟,運行結(jié)果如下:
[圖片上傳失敗...(image-a39c54-1671678429582)]
從運行結(jié)果中可以看出,發(fā)送的3
個數(shù)據(jù)包尸昧,都未出現(xiàn)粘包問題揩页,每個數(shù)據(jù)包之間都是獨立分割的。但這種方式解決粘包問題烹俗,實際上屬于一種“投機取巧”的方案爆侣,畢竟每個數(shù)據(jù)包都采用新的連接發(fā)送萍程,在操作系統(tǒng)級別來看,每個數(shù)據(jù)包都源自于不同的網(wǎng)絡(luò)套接字兔仰,自然會分開讀取茫负。
但這種方式無法解決半包問題,例如這里咱們將服務(wù)端的接收緩沖區(qū)調(diào)泻醺啊:
// 演示半包問題的服務(wù)端
public class HalfPackageServer {
public static void main(String[] args) {
EventLoopGroup group = new NioEventLoopGroup();
ServerBootstrap server = new ServerBootstrap();
server.group(group);
server.channel(NioServerSocketChannel.class);
// 調(diào)整服務(wù)端的接收緩沖區(qū)大小為16字節(jié)(最小為16忍法,無法設(shè)置更小)
server.childOption(ChannelOption.RCVBUF_ALLOCATOR,
new AdaptiveRecvByteBufAllocator(16,16,16));
server.childHandler(new ServerInitializer());
server.bind("127.0.0.1",8888);
}
}
復(fù)制代碼
然后再啟動這個服務(wù)端榕吼,接著再啟動前面的客戶端饿序,效果如下:
[圖片上傳失敗...(image-b7ca1d-1671678429582)]
從結(jié)果中依舊會發(fā)現(xiàn),多個數(shù)據(jù)包之間還是發(fā)生了半包問題友题,因為服務(wù)端的接收緩沖區(qū)一次性最大只能存下16Bytes
數(shù)據(jù)嗤堰,所以客戶端每次發(fā)送20Bytes
數(shù)據(jù),無法全部存入緩沖區(qū)度宦,最終就出現(xiàn)了一個數(shù)據(jù)包被拆成多個包讀取踢匣。
正由于短連接這種方式,無法很好的解決半包問題戈抄,所以一般線上除開特殊場景外离唬,否則不會使用短連接這種形式來單獨解決粘包問題,接著看看Netty
中提供的一些解決方案划鸽。
1.3.2输莺、定長幀解碼器
前面聊到的短連接方式,解決粘包問題的思路屬于投機取巧行為裸诽,同時也需要頻繁的建立/斷開連接嫂用,這無論是從資源利用率、還是程序執(zhí)行的效率上來說丈冬,都并不妥當(dāng)嘱函,而Netty
中提供了一系列解決粘包、半包問題的實現(xiàn)類埂蕊,即Netty
的幀解碼器往弓,先來看看定長幀解碼器,案例如下:
// 通過定長幀解碼器解決粘包蓄氧、半包問題的演示類
public class FixedLengthFrameDecoderDemo {
public static void main(String[] args) {
// 通過Netty提供的測試通道來代替服務(wù)端函似、客戶端
EmbeddedChannel channel = new EmbeddedChannel(
// 添加一個定長幀解碼器(每條數(shù)據(jù)以8字節(jié)為單位拆包)
new FixedLengthFrameDecoder(8),
new LoggingHandler(LogLevel.DEBUG)
);
// 調(diào)用三次發(fā)送數(shù)據(jù)的方法(等價于向服務(wù)端發(fā)送三次數(shù)據(jù))
sendData(channel,"ABCDEGF",8);
sendData(channel,"XYZ",8);
sendData(channel,"12345678",8);
}
private static void sendData(EmbeddedChannel channel, String data, int len){
// 獲取發(fā)送數(shù)據(jù)的字節(jié)長度
byte[] bytes = data.getBytes();
int dataLength = bytes.length;
// 根據(jù)固定長度補齊要發(fā)送的數(shù)據(jù)
String alignString = "";
if (dataLength < len){
int alignLength = len - bytes.length;
for (int i = 1; i <= alignLength; i++) {
alignString = alignString + "*";
}
}
// 拼接上補齊字符,得到最終要發(fā)送的消息數(shù)據(jù)
String msg = data + alignString;
byte[] msgBytes = msg.getBytes();
// 構(gòu)建緩沖區(qū)喉童,通過channel發(fā)送數(shù)據(jù)
ByteBuf buffer = ByteBufAllocator.DEFAULT.buffer(8);
buffer.writeBytes(msgBytes);
channel.writeInbound(buffer);
}
}
復(fù)制代碼
注意看上述這個案例撇寞,在其中就并未搭建服務(wù)端、客戶端了,而是采用EmbeddedChannel
對象來測試蔑担,這個通道是Netty
提供的測試通道露氮,可以基于它來快速搭建測試用例,上述中的:
new EmbeddedChannel(
new FixedLengthFrameDecoder(8),
new LoggingHandler(LogLevel.DEBUG)
);
復(fù)制代碼
這段代碼钟沛,就類似于之前在服務(wù)端的pipeline
添加處理器的過程,等價于下述這段代碼:
socketChannel.pipeline().addLast(new FixedLengthFrameDecoder(8));
socketChannel.pipeline().addLast(new LoggingHandler(LogLevel.DEBUG));
復(fù)制代碼
理解了EmbeddedChannel
后局扶,接著先來看看運行結(jié)果恨统,如下:
[圖片上傳失敗...(image-6ad4ef-1671678429581)]
注意看上述結(jié)果,在該案例中三妈,服務(wù)端會以8Bytes
為單位畜埋,然后對數(shù)據(jù)進行分包處理,平均每讀取8Bytes
數(shù)據(jù)畴蒲,就會將其當(dāng)作一個數(shù)據(jù)包悠鞍。如果客戶端發(fā)送的一條數(shù)據(jù),長度沒有8
個字節(jié)模燥,在sendData()
方法中則會以*
號補齊咖祭。比如上圖中,發(fā)送了一條XYZ
數(shù)據(jù)蔫骂,因為長度只有3
字節(jié)么翰,所以會再拼接五個*
號補齊八字節(jié)的長度。
這種采用固定長度解析數(shù)據(jù)的方式辽旋,的確能夠有效避免粘包浩嫌、半包問題的出現(xiàn),因為每個數(shù)據(jù)包之間补胚,會以八個字節(jié)的長度作為界限码耐,然后分割數(shù)據(jù)。但這種方式也存在三個致命缺陷:
- ①只適用于傳輸固定長度范圍內(nèi)的數(shù)據(jù)場景溶其,而且客戶端在發(fā)送數(shù)據(jù)前骚腥,還需自己根據(jù)長度補齊數(shù)據(jù)。
- ②如果發(fā)送的數(shù)據(jù)超出固定長度握联,服務(wù)端依舊會按固定長度分包桦沉,所以仍然會存在半包問題。
- ③對于未達到固定長度的數(shù)據(jù)金闽,還需要額外傳輸補齊的
*
號字符纯露,會占用不必要的網(wǎng)絡(luò)資源。
1.3.3代芜、行幀解碼器
上面說到的定長幀解碼器埠褪,由于使用時存在些許限制,使用它來解析數(shù)據(jù)就并不那么靈活,尤其是針對于一些數(shù)據(jù)長度可變的場景钞速,顯得就有些許乏力贷掖,因此Netty
中還提供了行幀解碼器,案例如下:
// 通過行幀解碼器解決粘包渴语、半包問題的演示類
public class LineFrameDecoderDemo {
public static void main(String[] args) {
// 通過Netty提供的測試通道來代替服務(wù)端苹威、客戶端
EmbeddedChannel channel = new EmbeddedChannel(
// 添加一個行幀解碼器(在超出1024后還未檢測到換行符,就會停止讀燃菪住)
new LineBasedFrameDecoder(1024),
new LoggingHandler(LogLevel.DEBUG)
);
// 調(diào)用三次發(fā)送數(shù)據(jù)的方法(等價于向服務(wù)端發(fā)送三次數(shù)據(jù))
sendData(channel,"ABCDEGF");
sendData(channel,"XYZ");
sendData(channel,"12345678");
}
private static void sendData(EmbeddedChannel channel, String data){
// 在要發(fā)送的數(shù)據(jù)結(jié)尾牙甫,拼接上一個\n換行符(\r\n也可以)
String msg = data + "\n";
// 獲取發(fā)送數(shù)據(jù)的字節(jié)長度
byte[] msgBytes = msg.getBytes();
// 構(gòu)建緩沖區(qū),通過channel發(fā)送數(shù)據(jù)
ByteBuf buffer = ByteBufAllocator.DEFAULT.buffer(8);
buffer.writeBytes(msgBytes);
channel.writeInbound(buffer);
}
}
復(fù)制代碼
在上述案例中调违,咱們給服務(wù)端添加了一個LineBasedFrameDecoder(1024)
行解碼器窟哺,其中有個1024
的數(shù)字,這是啥意思呢技肩?這個是數(shù)據(jù)的最大長度限制且轨,畢竟在網(wǎng)絡(luò)接收過程中,如果一直沒有讀取到換行符虚婿,總不能一直接收下去旋奢,所以當(dāng)數(shù)據(jù)的長度超出該值后,Netty
會默認將前面讀到的數(shù)據(jù)分成一個數(shù)據(jù)包雳锋。
同時在發(fā)送數(shù)據(jù)的sendData()
方法中黄绩,這回就無需咱們自己補齊數(shù)據(jù)了,只需在每個要發(fā)送的數(shù)據(jù)末尾玷过,手動拼接上一個\n
或\r\n
換行符即可爽丹,服務(wù)端在讀取數(shù)據(jù)時,會按換行符來作為界限分割辛蚊,運行結(jié)果如下:
[圖片上傳失敗...(image-67deb2-1671678429581)]
從結(jié)果中能夠看出粤蝎,每個數(shù)據(jù)包都是按客戶端發(fā)送的格式做了解析,并未出現(xiàn)粘包袋马、半包現(xiàn)象初澎。
1.3.4、分隔符幀解碼器
上面聊了以換行符作為分隔符的解碼器虑凛,但Netty
中還提供了自定義分隔符的解碼器碑宴,使用這種解碼器,能讓諸位隨心所欲的定義自己的分隔符桑谍,案例如下:
public class DelimiterFrameDecoderDemo {
public static void main(String[] args) {
// 自定義一個分隔符(記得要用ByteBuf對象來包裝)
ByteBuf delimiter = ByteBufAllocator.DEFAULT.buffer(1);
delimiter.writeByte('*');
// 通過Netty提供的測試通道來代替服務(wù)端延柠、客戶端
EmbeddedChannel channel = new EmbeddedChannel(
// 添加一個分隔符幀解碼器(傳入自定義的分隔符)
new DelimiterBasedFrameDecoder(1024,delimiter),
new LoggingHandler(LogLevel.DEBUG)
);
// 調(diào)用三次發(fā)送數(shù)據(jù)的方法(等價于向服務(wù)端發(fā)送三次數(shù)據(jù))
sendData(channel,"ABCDEGF");
sendData(channel,"XYZ");
sendData(channel,"12345678");
}
private static void sendData(EmbeddedChannel channel, String data){
// 在要發(fā)送的數(shù)據(jù)結(jié)尾,拼接上一個*號(因為前面自定義的分隔符為*號)
String msg = data + "*";
// 獲取發(fā)送數(shù)據(jù)的字節(jié)長度
byte[] msgBytes = msg.getBytes();
// 構(gòu)建緩沖區(qū)锣披,通過channel發(fā)送數(shù)據(jù)
ByteBuf buffer = ByteBufAllocator.DEFAULT.buffer(8);
buffer.writeBytes(msgBytes);
channel.writeInbound(buffer);
}
}
復(fù)制代碼
這個案例的運行結(jié)果與上一個完全相同贞间,不同點則在于換了一個解碼器贿条,換成了:
new DelimiterBasedFrameDecoder(1024,delimiter)
復(fù)制代碼
而后發(fā)送數(shù)據(jù)的時候,對每個數(shù)據(jù)的結(jié)尾增热,手動拼接一個*
號作為分隔符即可整以。
相較于原本的定長解碼器,行解碼器峻仇、自定義分隔符解碼器顯然更加靈活公黑,因為支持可變長度的數(shù)據(jù),但這兩種解碼器摄咆,依舊存在些許缺點:
- ①對于每一個讀取到的字節(jié)都需要判斷一下:是否為結(jié)尾的分隔符帆调,這會影響整體性能。
- ②依舊存在最大長度限制豆同,當(dāng)數(shù)據(jù)超出最大長度后,會自動將其分包含鳞,在數(shù)據(jù)傳輸量較大的情況下影锈,依舊會導(dǎo)致半包現(xiàn)象出現(xiàn)。
1.3.5蝉绷、LTC幀解碼器
前面聊過的多個解碼器中鸭廷,無論是哪個,都多多少少會存在些許不完美熔吗,因此Netty
最終提供了一款LTC
解碼器辆床,這個解碼器也屬于實際Netty
開發(fā)中,應(yīng)用最為廣泛的一種桅狠,但理解起來略微有些復(fù)雜讼载,先來看看它的構(gòu)造方法:
public class LengthFieldBasedFrameDecoder extends ByteToMessageDecoder {
public LengthFieldBasedFrameDecoder(
int maxFrameLength,
int lengthFieldOffset,
int lengthFieldLength,
int lengthAdjustment,
int initialBytesToStrip) {
this(maxFrameLength,
lengthFieldOffset,
lengthFieldLength,
lengthAdjustment,
initialBytesToStrip, true);
}
// 暫時省略其他參數(shù)的構(gòu)造方法......
}
復(fù)制代碼
從上述構(gòu)造器中可明顯看出,LTC
中存在五個參數(shù)中跌,看起來都比較長咨堤,接著簡單解釋一下:
-
maxFrameLength
:數(shù)據(jù)最大長度,允許單個數(shù)據(jù)包的最大長度漩符,超出長度后會自動分包一喘。 -
lengthFieldOffset
:長度字段偏移量,表示描述數(shù)據(jù)長度的信息從第幾個字段開始嗜暴。 -
lengthFieldLength
:長度字段的占位大小凸克,表示數(shù)據(jù)中的使用了幾個字節(jié)描述正文長度。 -
lengthAdjustment
:長度調(diào)整數(shù)闷沥,表示在長度字段的N
個字節(jié)后才是正文數(shù)據(jù)的開始萎战。 -
initialBytesToStrip
:頭部剝離字節(jié)數(shù),表示先將數(shù)據(jù)去掉N
個字節(jié)后狐赡,再開始讀取數(shù)據(jù)撞鹉。
上述這種方式描述五個參數(shù)疟丙,大家估計理解起來有些困難,那么下面結(jié)合Netty
源碼中的注釋鸟雏,先把這幾個參數(shù)徹底搞明白再說享郊,先來看個案例:
[圖片上傳失敗...(image-997b10-1671678429581)]
比如上述這組數(shù)據(jù),對應(yīng)的參數(shù)如下:
lengthFieldOffset = 0
lengthFieldLength = 4
lengthAdjustment = 0
initialBytesToStrip = 0
復(fù)制代碼
這組參數(shù)表示啥意思呢孝鹊?表示目前這條數(shù)據(jù)炊琉,長度字段從第0
個字節(jié)開始,使用4
個字節(jié)來描述數(shù)據(jù)長度又活,這時服務(wù)端會讀取數(shù)據(jù)的前4
個字節(jié)苔咪,得到正文數(shù)據(jù)的長度,從而得知:在第四個字節(jié)之后柳骄,再往后讀十個字節(jié)团赏,是一條完整的數(shù)據(jù),最終向后讀取10
個字節(jié)耐薯,最終就會讀到Hi, ZhuZi.
這條數(shù)據(jù)舔清。
但上述這種方式對數(shù)據(jù)解碼之后,讀取時依舊會顯示長度字段曲初,也就是前四個用來描述長度的字節(jié)也會被讀到体谒,因此最終會顯示出10Hi, ZhuZi.
這樣的格式,那如果想要去掉前面的長度字段怎么辦呢臼婆?這需要用到initialBytesToStrip
參數(shù)抒痒,如下:
lengthFieldOffset = 0
lengthFieldLength = 4
lengthAdjustment = 0
initialBytesToStrip = 4
復(fù)制代碼
[圖片上傳失敗...(image-f92bd5-1671678429581)]
這組參數(shù)又是啥意思呢?其實和前面那一組數(shù)據(jù)沒太大的變化颁褂,只是用initialBytesToStrip
聲明要剝離掉前4
個字節(jié)故响,所以數(shù)據(jù)經(jīng)過解碼后,最終會去掉前面描述長度的四個字節(jié)颁独,僅顯示Hi, ZhuZi.
這十個字節(jié)的數(shù)據(jù)被去。
上述這種形式抗俄,其實就是預(yù)設(shè)了一個長度字段挫酿,服務(wù)端、客戶端之間約定使用N
個字節(jié)來描述數(shù)據(jù)長度剂桥,接著在讀取數(shù)據(jù)時丰捷,讀取指定個字節(jié)坯墨,得到本次數(shù)據(jù)的長度,最終能夠正常解碼數(shù)據(jù)病往。但這種方式只能滿足最基本的數(shù)據(jù)傳輸捣染,如果在數(shù)據(jù)中還需要添加一些正文信息,比如附加數(shù)據(jù)頭信息停巷、版本號的情況耍攘,又該如何處理呢榕栏?如下:
lengthFieldOffset = 8
lengthFieldLength = 4
lengthAdjustment = 0
initialBytesToStrip = 0
復(fù)制代碼
[圖片上傳失敗...(image-1d27fb-1671678429581)]
上述這個示例中,假設(shè)附加信息占8Bytes
蕾各,這里就需要用到lengthFieldOffset
參數(shù)扒磁,以此來表示長度字段偏移量是8
,這意味著讀取數(shù)據(jù)時式曲,要從第九個字節(jié)開始妨托,往后讀四個字節(jié)的數(shù)據(jù),才能夠得到描述數(shù)據(jù)長度的字段吝羞,然后解析得到10
兰伤,最終再往后讀取十個字節(jié)的數(shù)據(jù),讀到一條完整的數(shù)據(jù)钧排。
當(dāng)然敦腔,如果只想要讀到正文數(shù)據(jù)怎么辦?如下:
lengthFieldOffset = 8
lengthFieldLength = 4
lengthAdjustment = 0
initialBytesToStrip = 12
復(fù)制代碼
[圖片上傳失敗...(image-4a5f46-1671678429581)]
依舊只需要通過initialBytesToStrip
參數(shù)恨溜,從頭部剝離掉前12
個字節(jié)即可会烙,這里的12
個字節(jié),由八字節(jié)的附加信息筒捺、四字節(jié)的長度描述組成,去掉這兩部分纸厉,自然就得到了正文數(shù)據(jù)系吭。
OK,再來看另一種情況颗品,假如長度字段在最前面肯尺,附加信息在中間,但我只想要讀取正文數(shù)據(jù)怎么辦呢躯枢?
lengthFieldOffset = 0
lengthFieldLength = 4
lengthAdjustment = 8
initialBytesToStrip = 12
復(fù)制代碼
[圖片上傳失敗...(image-fd4f1f-1671678429581)]
在這里咱們又用到了lengthAdjustment
這個參數(shù)则吟,這個參數(shù)是長度調(diào)整數(shù)的意思,上面的示例中賦值為8
锄蹂,即表示從長度字段后開始氓仲,跳過8
個字節(jié)后,才是正文數(shù)據(jù)的開始得糜。接收方在解碼數(shù)據(jù)時敬扛,首先會從0
開始讀取四個字節(jié),得到正文數(shù)據(jù)的長度為10
朝抖,接著會根據(jù)lengthAdjustment
參數(shù)啥箭,跳過中間8
個的字節(jié),最后再往后讀10
個字節(jié)數(shù)據(jù)治宣,從而得到最終的正文數(shù)據(jù)急侥。
OK~砌滞,經(jīng)過上述幾個示例的講解后,相信大家對給出的幾個參數(shù)都有所了解坏怪,如若覺得有些暈乎贝润,可回頭再多仔細閱讀幾遍,這樣有助于加深對各個參數(shù)的印象陕悬。但本質(zhì)上來說题暖,LTC
解碼器,就是基于這些參數(shù)捉超,來確定一條數(shù)據(jù)的長度胧卤、位置,從而讀取到精確的數(shù)據(jù)拼岳,避免粘包枝誊、半包的現(xiàn)象產(chǎn)生,接下來上個Demo
理解:
// 通過LTC幀解碼器解決粘包惜纸、半包問題的演示類
public class LTCDecoderDemo {
public static void main(String[] args) {
// 通過Netty提供的測試通道來代替服務(wù)端叶撒、客戶端
EmbeddedChannel channel = new EmbeddedChannel(
// 添加一個行幀解碼器(在超出1024后還未檢測到換行符,就會停止讀饶桶妗)
new LengthFieldBasedFrameDecoder(1024,0,4,0,0),
new LoggingHandler(LogLevel.DEBUG)
);
// 調(diào)用三次發(fā)送數(shù)據(jù)的方法(等價于向服務(wù)端發(fā)送三次數(shù)據(jù))
sendData(channel,"Hi, ZhuZi.");
}
private static void sendData(EmbeddedChannel channel, String data){
// 獲取要發(fā)送的數(shù)據(jù)字節(jié)以及長度
byte[] dataBytes = data.getBytes();
int dataLength = dataBytes.length;
// 先將數(shù)據(jù)長度寫入到緩沖區(qū)祠够、再將正文數(shù)據(jù)寫入到緩沖區(qū)
ByteBuf buffer = ByteBufAllocator.DEFAULT.buffer();
buffer.writeInt(dataLength);
buffer.writeBytes(dataBytes);
// 發(fā)送最終組裝好的數(shù)據(jù)
channel.writeInbound(buffer);
}
}
復(fù)制代碼
上述案例中創(chuàng)建了一個LTC
解碼器,對應(yīng)的參數(shù)值為1024,0,4,0,0
粪牲,這分別對應(yīng)前面的五個參數(shù)古瓤,如下:
maxFrameLength = 1024
lengthFieldOffset = 0
lengthFieldLength = 4
lengthAdjustment = 0
initialBytesToStrip = 0
復(fù)制代碼
這組值意思為:數(shù)據(jù)的第0~4
個字節(jié)是長度字段,用來描述正文數(shù)據(jù)的長度腺阳,運行結(jié)果如下:
[圖片上傳失敗...(image-da4342-1671678429581)]
效果十分明顯落君,既沒有產(chǎn)生粘包、半包問題亭引,而且無需逐個字節(jié)判斷是否為分割符绎速,這對比之前的幾種解碼器而言,這種方式的效率顯然好上特別特別多焙蚓。當(dāng)然纹冤,上述結(jié)果中,如果想要去掉前面的四個.
购公,就只需要將initialBytesToStrip = 4
即可赵哲,從頭部剝離掉四個字節(jié)再讀取。
1.3.6君丁、粘包枫夺、半包解決方案小結(jié)
前面介紹了短連接、定長解碼器绘闷、行解碼器橡庞、分隔符解碼器以及LTC
解碼器這五種方案较坛,其中咱們需要牢記的是最后一種,因為其他的方案多少存在一些性能問題扒最,而通過LTC
解碼器這種方式處理粘包丑勤、半包問題的效率最好,因為無需逐個字節(jié)判斷消息邊界吧趣。
但實際
Netty
開發(fā)中法竞,如果其他解碼器更符合業(yè)務(wù)需求,也不必死死追求使用LTC
解碼器强挫,畢竟技術(shù)為業(yè)務(wù)提供服務(wù)岔霸,適合自己業(yè)務(wù)的,才是最好的俯渤!
二呆细、Netty的長連接與心跳機制
對于長連接、短連接八匠,這個概念在前面稍有提及絮爷,所謂的短連接就是每次讀寫數(shù)據(jù)完成后,立馬斷開客戶端與服務(wù)端的網(wǎng)絡(luò)連接梨树。而長連接則是相反的意思坑夯,一次數(shù)據(jù)交互完成后,服務(wù)端和客戶端之間繼續(xù)保持連接抡四,當(dāng)后續(xù)需再次收/發(fā)數(shù)據(jù)時柜蜈,可直接復(fù)用原有的網(wǎng)絡(luò)連接。
長連接這種模式床嫌,在并發(fā)較高的情況下能夠帶來額外的性能收益,因為
Netty
服務(wù)端胸私、客戶端綁定IP
端口厌处,搭建Channel
通道的過程,放到底層實際上就是TCP
三次握手的過程岁疼,同理阔涉,客戶端、服務(wù)端斷開連接的過程捷绒,即對應(yīng)著TCP
的四次揮手瑰排。
大家都知道,TCP
三次握手/四次揮手暖侨,這個過程無疑是比較“重量級”的椭住,并發(fā)情況下,頻繁創(chuàng)建字逗、銷毀網(wǎng)絡(luò)連接京郑,其資源開銷宅广、性能開銷會比較大,所以使用長連接的方案些举,能夠有效減少創(chuàng)建和銷毀網(wǎng)絡(luò)連接的動作跟狱。
那如何讓Netty
開啟長連接支持呢?這需要涉及到之前用過的ChannelOption
這個類户魏,接著來詳細講講它驶臊。
2.1、Netty調(diào)整網(wǎng)絡(luò)參數(shù)(ChannelOption)
ChannelOption
是Netty
提供的參數(shù)調(diào)整類叼丑,該類中提供了很多常量关翎,分別對應(yīng)著底層TCP、UDP幢码、
計算機網(wǎng)絡(luò)的一些參數(shù)笤休,在創(chuàng)建服務(wù)端、客戶端時症副,我們可以通過ChannelOption
類來調(diào)整網(wǎng)絡(luò)參數(shù)店雅,以此滿足不同的業(yè)務(wù)需求,該類中提供的常量列表如下:
-
ALLOCATOR
:ByteBuf
緩沖區(qū)的分配器贞铣,默認值為ByteBufAllocator.DEFAULT
闹啦。 -
RCVBUF_ALLOCATOR
:通道接收數(shù)據(jù)的ByteBuf
分配器,默認為AdaptiveRecvByteBufAllocator.DEFAULT
辕坝。 -
MESSAGE_SIZE_ESTIMATOR
:消息大小估算器窍奋,默認為DefaultMessageSizeEstimator.DEFAULT
。 -
CONNECT_TIMEOUT_MILLIS
:設(shè)置客戶端的連接超時時間酱畅,默認為3000ms
琳袄,超出會斷開連接。 -
MAX_MESSAGES_PER_READ
:一次Loop
最大讀取的消息數(shù)纺酸。-
ServerChannel/NioChannel
默認16
窖逗,其他類型的Channel
默認為1
。
-
-
WRITE_SPIN_COUNT
:一次Loop
最大寫入的消息數(shù)餐蔬,默認為16
碎紊。- 一個數(shù)據(jù)
16
次還未寫完,需要提交一個新的任務(wù)給EventLoop
樊诺,防止數(shù)據(jù)量較大的場景阻塞系統(tǒng)仗考。
- 一個數(shù)據(jù)
-
WRITE_BUFFER_HIGH_WATER_MARK
:寫高水位標記,默認為64K
词爬,超出時Channel.isWritable()
返回Flase
秃嗜。 -
WRITE_BUFFER_LOW_WATER_MARK
:寫低水位標記,默認為32K
,超出高水位又下降到低水位時痪寻,isWritable()
返回True
螺句。 -
WRITE_BUFFER_WATER_MARK
:寫水位標記,如果寫的數(shù)據(jù)量也超出該值橡类,依舊返回Flase
蛇尚。 -
ALLOW_HALF_CLOSURE
:一個遠程連接關(guān)閉時,是否半關(guān)本地連接顾画,默認為Flase
取劫。-
Flase
表示自動關(guān)閉本地連接,為True
會觸發(fā)入站處理器的userEventTriggered()
方法研侣。
-
-
AUTO_READ
:自動讀取機制谱邪,默認為True
,通道上有數(shù)據(jù)時庶诡,自動調(diào)用channel.read()
讀取數(shù)據(jù)惦银。 -
AUTO_CLOSE
:自動關(guān)閉機制,默認為Flase
末誓,發(fā)生錯誤時不會斷開與某個通道的連接扯俱。 -
SO_BROADCAST
:設(shè)置廣播機制,默認為Flase
喇澡,為True
時會開啟Socket
的廣播消息迅栅。 -
SO_KEEPALIVE
:開啟長連接機制,一次數(shù)據(jù)交互完后不會立馬斷開連接晴玖。 -
SO_SNDBUF
:發(fā)送緩沖區(qū)读存,用于保存要發(fā)送的數(shù)據(jù),未收到接收數(shù)據(jù)的ACK
之前呕屎,數(shù)據(jù)會存在這里让簿。 -
SO_RCVBUF
:接受緩沖區(qū),用戶保存要接受的數(shù)據(jù)秀睛。 -
SO_REUSEADDR
:是否復(fù)用IP
地址與端口號尔当,開啟后可重復(fù)綁定同一個地址。 -
SO_LINGER
:設(shè)置延遲關(guān)閉琅催,默認為-1
居凶。-
-1
:表示禁用該功能虫给,當(dāng)調(diào)用close()
方法后會立即返回藤抡,底層會先處理完數(shù)據(jù)。 -
0
:表示禁用該功能抹估,調(diào)用后立即返回缠黍,底層會直接放棄正在處理的數(shù)據(jù)。 - 大于
0
的正整數(shù):關(guān)閉時等待n
秒药蜻,或數(shù)據(jù)處理完成才正式關(guān)閉瓷式。
-
-
SO_BACKLOG
:指定服務(wù)端的連接隊列長度替饿,當(dāng)連接數(shù)達到該值時,會拒絕新的連接請求贸典。 -
SO_TIMEOUT
:設(shè)置接受數(shù)據(jù)時等待的超時時間视卢,默認為0
,表示無限等待廊驼。 -
IP_TOS
: -
IP_MULTICAST_ADDR
:設(shè)置IP
頭的Type-of-Service
字段据过,描述IP
包的優(yōu)先級和QoS
選項。 -
IP_MULTICAST_IF
:對應(yīng)IP
參數(shù)IP_MULTICAST_IF
妒挎,設(shè)置對應(yīng)地址的網(wǎng)卡為多播模式绳锅。 -
IP_MULTICAST_TTL
:對應(yīng)IP
參數(shù)IP_MULTICAST_IF2
,同上但支持IPv6
酝掩。 -
IP_MULTICAST_LOOP_DISABLED
:對應(yīng)IP
參數(shù)IP_MULTICAST_LOOP
鳞芙,設(shè)置本地回環(huán)地址的多播模式。 -
TCP_NODELAY
:開啟TCP
的Nagle
算法期虾,會將多個小包合并成一個大包發(fā)送原朝。 -
DATAGRAM_CHANNEL_ACTIVE_ON_REGISTRATION
:DatagramChannel
注冊的EventLoop
即表示已激活。 -
SINGLE_EVENTEXECUTOR_PER_GROUP
:Pipeline
是否由單線程執(zhí)行彻消,默認為True
竿拆,所有處理器由一條線程執(zhí)行,無需經(jīng)過線程上下文切換宾尚。
上面列出了ChannelOption
類中提供的參數(shù)丙笋,其中涵蓋了網(wǎng)絡(luò)通用的參數(shù)、TCP
協(xié)議煌贴、UDP
協(xié)議以及IP
協(xié)議的參數(shù)御板,其他的咱們無需過多關(guān)心,這里重點注意TCP
協(xié)議的兩個參數(shù):
-
TCP_NODELAY
:開啟TCP
的Nagle
算法牛郑,會將多個小包合并成一個大包發(fā)送怠肋。 -
SO_KEEPALIVE
:開啟長連接機制,一次數(shù)據(jù)交互完后不會立馬斷開連接淹朋。
第一個參數(shù)就是之前聊到的Nagle
算法笙各,而關(guān)于現(xiàn)在要聊的長連接,就是SO_KEEPALIVE
這個參數(shù)础芍,想要讓這些參數(shù)生效杈抢,需要將其裝載到對應(yīng)的服務(wù)端/客戶端上,Netty
中提供了兩個裝載參數(shù)的方法:
-
option()
:發(fā)生在連接初始化階段仑性,也就是程序初始化時惶楼,就會裝載該方法配置的參數(shù)。 -
childOption()
:發(fā)生在連接建立之后,這些參數(shù)只有等連接建立后才會被裝載歼捐。
其實也可以這樣理解何陆,option()
方法配置的參數(shù)是對全局生效的,而childOption()
配置的參數(shù)豹储,是針對于連接生效的贷盲,而想要開啟長連接配置,只需稍微改造一下服務(wù)端/客戶端代碼即可:
// 服務(wù)端代碼
server.childOption(ChannelOption.SO_KEEPALIVE, true);
// 客戶端代碼
client.option(ChannelOption.SO_KEEPALIVE, true);
復(fù)制代碼
通過上述的方式開啟長連接之后剥扣,TCP
默認每兩小時會發(fā)送一次心跳檢測晃洒,查看對端是否還存活,如果對端由于網(wǎng)絡(luò)故障導(dǎo)致下線朦乏,TCP
會自動斷開與對方的連接球及。
2.2、Netty的心跳機制
前面聊到了Netty
的長連接呻疹,其實本質(zhì)上并不是Netty
提供的長連接實現(xiàn)吃引,而是通過調(diào)整參數(shù),借助傳輸層TCP
協(xié)議提供的長連接機制刽锤,從而實現(xiàn)服務(wù)端與客戶端的長連接支持镊尺。不過TCP
雖然提供了長連接支持,但其心跳機制并不夠完善并思,Why
庐氮?其實答案很簡單,因為心跳檢測的間隔時間太長了宋彼,每隔兩小時才檢測一次弄砍!
也許有人會說:兩小時就兩小時,這有什么問題嗎输涕?其實問題有些大音婶,因為兩小時太長了,無法有效檢測到機房斷電莱坎、機器重啟衣式、網(wǎng)線拔出、防火墻更新等情況檐什,假設(shè)一次心跳結(jié)束后碴卧,對端就出現(xiàn)了這些故障,依靠
TCP
自身的心跳頻率乃正,需要等到兩小時之后才能檢測到問題住册。而這些已經(jīng)失效的連接應(yīng)當(dāng)及時剔除,否則會長時間占用服務(wù)端資源烫葬,畢竟服務(wù)端的可用連接數(shù)是有限的界弧。
所以,光依靠TCP
的心跳機制搭综,這無法保障咱們的應(yīng)用穩(wěn)健性垢箕,因此一般開發(fā)中間件也好、通信程序也罷兑巾、亦或是RPC
框架等条获,都會在應(yīng)用層再自實現(xiàn)一次心跳機制,而所謂的心跳機制蒋歌,也并不是特別高大上的東西帅掘,實現(xiàn)的思路有兩種:
- 服務(wù)端主動探測:每間隔一定時間后,向所有客戶端發(fā)送一個檢測信號堂油,過程如下:
- 假設(shè)目前有三個節(jié)點修档,
A
為服務(wù)端,B府框、C
都為客戶端吱窝。-
A
:你們還活著嗎? -
B
:我還活著迫靖! -
C
:.....(假設(shè)掛掉了院峡,無響應(yīng))
-
-
A
收到了B
的響應(yīng),但C
卻未給出響應(yīng)系宜,很有可能掛了照激,A
中斷與C
的連接。
- 假設(shè)目前有三個節(jié)點修档,
- 客戶端主動告知:每間隔一定時間后盹牧,客戶端向服務(wù)端發(fā)送一個心跳包俩垃,過程如下:
- 依舊是上述那三個節(jié)點。
-
B
:我還活著汰寓,不要開除我吆寨! -
C
:....(假設(shè)掛掉了,不發(fā)送心跳包) -
A
:收到B
的心跳包踩寇,但未收到C
的心跳包啄清,將C
的網(wǎng)絡(luò)連接斷開。
一般來說俺孙,一套健全的心跳機制辣卒,都會結(jié)合上述兩種方案一起實現(xiàn),也就是客戶端定時向服務(wù)端發(fā)送心跳包睛榄,當(dāng)服務(wù)端未收到某個客戶端心跳包的情況下荣茫,再主動向客戶端發(fā)起探測包,這一步主要是做二次確認场靴,防止由于網(wǎng)絡(luò)擁塞或其他問題啡莉,導(dǎo)致原本客戶端發(fā)出的心跳包丟失港准。
2.2.1、心跳機制的實現(xiàn)思路分析
前面叨叨絮絮說了很多咧欣,那么在Netty
中該如何實現(xiàn)呢浅缸?其實在Netty
中提供了一個名為IdleStateHandler
的類,它可以對一個通道上的讀魄咕、寫衩椒、讀/寫操作設(shè)置定時器,其中主要提供了三種類型的心跳檢測:
// 當(dāng)一個Channel(Socket)在指定時間后未觸發(fā)讀事件哮兰,會觸發(fā)這個事件
public static final IdleStateEvent READER_IDLE_STATE_EVENT;
// 當(dāng)一個Channel(Socket)在指定時間后未觸發(fā)寫事件毛萌,會觸發(fā)這個事件
public static final IdleStateEvent WRITER_IDLE_STATE_EVENT;
// 上述讀、寫等待事件的結(jié)合體
public static final IdleStateEvent ALL_IDLE_STATE_EVENT;
復(fù)制代碼
在Netty
中喝滞,當(dāng)一個已建立連接的通道阁将,超出指定時間后還沒有出現(xiàn)數(shù)據(jù)交互,對應(yīng)的Channel
就會進入閑置Idle
狀態(tài)右遭,根據(jù)不同的Socket/Channel
事件冀痕,會進入不同的閑置狀態(tài),而不同的閑置狀態(tài)又會觸發(fā)不同的閑置事件狸演,也就是上述提到的三種閑置事件言蛇,在Netty
中用IdleStateEvent
事件類來表示。
OK宵距,正是由于
Netty
提供了IdleStateEvent
閑置事件類腊尚,所以咱們可以基于它來實現(xiàn)心跳機制,但這里還需要用到《Netty入門篇-入站處理器》中聊到的一個方法:userEventTriggered()
满哪,這個鉤子方法婿斥,會在通道觸發(fā)任意事件后被調(diào)用,這也就意味著:只要通道上觸發(fā)了事件哨鸭,都會觸發(fā)該方法執(zhí)行民宿,閑置事件也不例外!
有了IdleState像鸡、userEventTriggered()
這兩個基礎(chǔ)后活鹰,咱們就可基于這兩個玩意兒,去實現(xiàn)一個簡單的心跳機制只估,最基本的功能實現(xiàn)如下:
- 客戶端:在閑置一定時間后志群,能夠主動給服務(wù)端發(fā)送心跳包。
- 服務(wù)端:能夠主動檢測到未發(fā)送數(shù)據(jù)包的閑置連接蛔钙,并中斷連接锌云。
2.2.2、帶有心跳機制的客戶端實現(xiàn)
上述這兩點功能實現(xiàn)起來并不難吁脱,咱們首先寫一下客戶端的實現(xiàn)桑涎,如下:
// 心跳機制的客戶端處理器
public class HeartbeatClientHandler extends ChannelInboundHandlerAdapter {
// 通用的心跳包數(shù)據(jù)
private static final ByteBuf HEARTBEAT_DATA =
Unpooled.unreleasableBuffer(Unpooled.copiedBuffer("I am Alive", CharsetUtil.UTF_8));
@Override
public void userEventTriggered(ChannelHandlerContext ctx, Object event) throws Exception {
// 如果當(dāng)前觸發(fā)的事件是閑置事件
if (event instanceof IdleStateEvent) {
IdleStateEvent idleEvent = (IdleStateEvent) event;
// 如果當(dāng)前通道觸發(fā)了寫閑置事件
if (idleEvent.state() == IdleState.WRITER_IDLE){
// 表示當(dāng)前客戶端有一段時間未向服務(wù)端發(fā)送數(shù)據(jù)了彬向,
// 為了防止服務(wù)端關(guān)閉當(dāng)前連接,手動發(fā)送一個心跳包
ctx.channel().writeAndFlush(HEARTBEAT_DATA.duplicate());
System.out.println("成功向服務(wù)端發(fā)送心跳包....");
} else {
super.userEventTriggered(ctx, event);
}
}
}
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
System.out.println("正在與服務(wù)端建立連接....");
// 建立連接成功之后攻冷,先向服務(wù)端發(fā)送一條數(shù)據(jù)
ctx.channel().writeAndFlush("我是會發(fā)心跳包的客戶端-A娃胆!");
super.channelActive(ctx);
}
@Override
public void channelInactive(ChannelHandlerContext ctx) throws Exception {
System.out.println("服務(wù)端主動關(guān)閉了連接....");
super.channelInactive(ctx);
}
}
復(fù)制代碼
因為要借助userEventTriggered()
方法來實現(xiàn)事件監(jiān)聽,所以咱們需要定義一個類繼承入站處理器讲衫,接著在其中做了一個判斷,如果當(dāng)前觸發(fā)了IdleStateEvent
閑置事件孵班,這也就意味著目前沒有向服務(wù)端發(fā)送數(shù)據(jù)了涉兽,因此需要發(fā)送一個心跳包,告知服務(wù)端自己還活著,接著需要將這個處理器加在客戶端上面,如下:
// 演示心跳機制的客戶端(會發(fā)送心跳包)
public class ClientA {
public static void main(String[] args) {
EventLoopGroup worker = new NioEventLoopGroup();
Bootstrap client = new Bootstrap();
try {
client.group(worker);
client.channel(NioSocketChannel.class);
// 打開長連接配置
client.option(ChannelOption.SO_KEEPALIVE, true);
// 指定一個自定義的初始化器
client.handler(new ClientInitializer());
client.connect("127.0.0.1", 8888).sync();
} catch (Exception e){
e.printStackTrace();
}
}
}
// 客戶端的初始化器
public class ClientInitializer extends ChannelInitializer<SocketChannel> {
@Override
protected void initChannel(SocketChannel socketChannel) throws Exception {
ChannelPipeline pipeline = socketChannel.pipeline();
// 配置如果3s內(nèi)未觸發(fā)寫事件蝙泼,就會觸發(fā)寫閑置事件
pipeline.addLast("IdleStateHandler",
new IdleStateHandler(0,3,0,TimeUnit.SECONDS));
pipeline.addLast("Encoder",new StringEncoder(CharsetUtil.UTF_8));
pipeline.addLast("Decoder",new StringDecoder(CharsetUtil.UTF_8));
// 裝載自定義的客戶端心跳處理器
pipeline.addLast("HeartbeatHandler",new HeartbeatClientHandler());
}
}
復(fù)制代碼
客戶端的代碼基本上和之前的案例差異不大孵睬,重點看ClientInitializer
這個初始化器,里面首先加入了一個IdleStateHandler
比默,參數(shù)為0、3、0
渴肉,單位是秒,這是啥意思呢爽冕?點進源碼看看構(gòu)造函數(shù)仇祭,如下:
public IdleStateHandler(long readerIdleTime,
long writerIdleTime,
long allIdleTime,
TimeUnit unit) {
this(false, readerIdleTime, writerIdleTime, allIdleTime, unit);
}
復(fù)制代碼
沒錯,其實賦值的三個參數(shù)颈畸,也就分別對應(yīng)著讀操作的閑置事件乌奇、寫操作的閑置事件、讀寫操作的閑置事件眯娱,如果賦值為0
礁苗,表示這些閑置事件不需要關(guān)心,在前面的賦值中徙缴,第二個參數(shù)writerIdleTime
被咱們賦值成了3
试伙,這表示如果客戶端通道在三秒內(nèi),未觸發(fā)寫事件于样,就會觸發(fā)寫閑置事件迁霎,而后會調(diào)用HeartbeatClientHandler.userEventTriggered()
方法,從而向服務(wù)端發(fā)送一個心跳包百宇。
2.2.3考廉、帶有心跳機制的服務(wù)端實現(xiàn)
接著再來看看服務(wù)端的代碼實現(xiàn),同樣需要有一個心跳處理器携御,如下:
// 心跳機制的服務(wù)端處理器
public class HeartbeatServerHandler extends ChannelInboundHandlerAdapter {
@Override
public void userEventTriggered(ChannelHandlerContext ctx, Object event) throws Exception {
// 如果當(dāng)前觸發(fā)的事件是閑置事件
if (event instanceof IdleStateEvent) {
IdleStateEvent idleEvent = (IdleStateEvent) event;
// 如果對應(yīng)的Channel通道觸發(fā)了讀閑置事件
if (idleEvent.state() == IdleState.READER_IDLE){
// 表示對應(yīng)的客戶端沒有發(fā)送心跳包昌粤,則關(guān)閉對應(yīng)的網(wǎng)絡(luò)連接
// (心跳包也是一種特殊的數(shù)據(jù)既绕,會觸發(fā)讀事件,有心跳就不會進這步)
ctx.channel().close();
System.out.println("關(guān)閉了未發(fā)送心跳包的連接....");
} else {
super.userEventTriggered(ctx, event);
}
}
}
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
// 如果收到的是心跳包涮坐,則給客戶端做出一個回復(fù)
if ("I am Alive".equals(msg)){
ctx.channel().writeAndFlush("I know");
}
System.out.println("收到客戶端消息:" + msg);
super.channelRead(ctx, msg);
}
}
復(fù)制代碼
在Server
端的心跳處理器中凄贩,同樣監(jiān)聽了閑置事件,但這里監(jiān)聽的是讀閑置事件袱讹,因為一個通道如果長時間沒有觸發(fā)讀事件疲扎,這表示對應(yīng)的客戶端已經(jīng)很長事件沒有發(fā)數(shù)據(jù)了,所以需要關(guān)閉對應(yīng)的客戶端連接捷雕。
有小伙伴或許會疑惑:為什么一個客戶端通道長時間未發(fā)送數(shù)據(jù)就需要關(guān)閉連接呀椒丧?這不是違背了長連接的初衷嗎?答案并非如此救巷,因為前面在咱們的客戶端中壶熏,在通道長時間未觸發(fā)寫事件的情況下,會主動向服務(wù)端發(fā)送心跳包浦译,而心跳包也是一種特殊的數(shù)據(jù)包棒假,依舊會觸發(fā)服務(wù)端上的讀事件,所以但凡正常發(fā)送心跳包的連接精盅,都不會被服務(wù)端主動關(guān)閉帽哑。
OK,接著來看看服務(wù)端的實現(xiàn)叹俏,其實和前面的客戶端差不多:
// 演示心跳機制的服務(wù)端
public class Server {
public static void main(String[] args) {
EventLoopGroup group = new NioEventLoopGroup();
ServerBootstrap server = new ServerBootstrap();
server.group(group);
server.channel(NioServerSocketChannel.class);
// 在這里開啟了長連接配置祝拯,以及配置了自定義的初始化器
server.childOption(ChannelOption.SO_KEEPALIVE, true);
server.childHandler(new ServerInitializer());
server.bind("127.0.0.1",8888);
}
}
// 服務(wù)端的初始化器
public class ServerInitializer extends ChannelInitializer<SocketChannel> {
@Override
protected void initChannel(SocketChannel socketChannel) throws Exception {
ChannelPipeline pipeline = socketChannel.pipeline();
// 配置如果5s內(nèi)未觸發(fā)讀事件,就會觸發(fā)讀閑置事件
pipeline.addLast("IdleStateHandler",
new IdleStateHandler(5,0,0,TimeUnit.SECONDS));
pipeline.addLast("Encoder",new StringEncoder(CharsetUtil.UTF_8));
pipeline.addLast("Decoder",new StringDecoder(CharsetUtil.UTF_8));
// 裝載自定義的服務(wù)端心跳處理器
pipeline.addLast("HeartbeatHandler",new HeartbeatServerHandler());
}
}
復(fù)制代碼
重點注意看:在服務(wù)端配置的是讀閑置事件她肯,如果在5s
內(nèi)未觸發(fā)讀事件佳头,就會觸發(fā)對應(yīng)通道的讀閑置事件,但這里是5s
晴氨,為何不配置成客戶端的3s
呢康嘉?因為如果兩端的閑置超時時間配置成一樣,就會造成客戶端正在發(fā)心跳包籽前、服務(wù)端正在關(guān)閉連接的這種情況出現(xiàn)亭珍,最終導(dǎo)致心跳機制無法正常工作,對于這點大家也可以自行演示枝哄。
2.2.4肄梨、普通的客戶端實現(xiàn)
最后,為了方便觀看效果挠锥,這里咱們再創(chuàng)建一個不會發(fā)送心跳包的客戶端B
众羡,同樣打開它的長連接選項,然后來對比測試效果蓖租,如下:
// 演示心跳機制的客戶端(不會發(fā)送心跳包)
public class ClientB {
public static void main(String[] args) {
EventLoopGroup worker = new NioEventLoopGroup();
Bootstrap client = new Bootstrap();
try {
client.group(worker);
client.channel(NioSocketChannel.class);
client.option(ChannelOption.SO_KEEPALIVE, true);
client.handler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel socketChannel)
throws Exception {
ChannelPipeline pipeline = socketChannel.pipeline();
pipeline.addLast("Encoder",new StringEncoder(CharsetUtil.UTF_8));
pipeline.addLast("Decoder",new StringDecoder(CharsetUtil.UTF_8));
pipeline.addLast(new ChannelInboundHandlerAdapter(){
@Override
public void channelActive(ChannelHandlerContext ctx)
throws Exception {
// 建立連接成功之后粱侣,先向服務(wù)端發(fā)送一條數(shù)據(jù)
ctx.channel().writeAndFlush("我是不會發(fā)心跳包的客戶端-B羊壹!");
}
@Override
public void channelInactive(ChannelHandlerContext ctx)
throws Exception {
System.out.println("因為沒發(fā)送心跳包,俺被開除啦齐婴!");
// 當(dāng)通道被關(guān)閉時油猫,停止前面啟動的線程池
worker.shutdownGracefully();
}
});
}
});
client.connect("127.0.0.1", 8888).sync();
} catch (Exception e){
e.printStackTrace();
}
}
}
復(fù)制代碼
上述這段代碼中,僅構(gòu)建出了一個最基本的客戶端柠偶,其中主要干了兩件事情:
- ①在連接建立成功之后情妖,先向服務(wù)端發(fā)送一條數(shù)據(jù)。
- ②在連接(通道)被關(guān)閉時诱担,輸出一句“俺被開除啦毡证!”的信息,并優(yōu)雅停止線程池该肴。
除此之外情竹,該客戶端并未裝載自己實現(xiàn)的客戶端心跳處理器藐不,這也就意味著:客戶端B
并不會主動給服務(wù)端發(fā)送心跳包匀哄。
2.2.5、Netty心跳機制測試
接著分別啟動服務(wù)端雏蛮、客戶端A
涎嚼、客戶端B
,然后查看控制臺的日志挑秉,如下:
[圖片上傳失敗...(image-bccdea-1671678429579)]
從上圖的運行結(jié)果來看法梯,在三方啟動之后,整體過程如下:
-
ClientA
:先與服務(wù)端建立連接犀概,并且在建立連接之后發(fā)送一條數(shù)據(jù)立哑,后續(xù)持續(xù)發(fā)送心跳包。 -
ClientB
:先與服務(wù)端建立連接姻灶,然后在建立連接成功后發(fā)送一條數(shù)據(jù)铛绰,后續(xù)不會再發(fā)數(shù)據(jù)。 -
Server
:與ClientA产喉、B
保持連接捂掰,然后定期檢測閑置連接,關(guān)閉未發(fā)送心跳包的連接曾沈。
在上述這個過程中这嚣,由于ClientB
建立連接后,未主動向服務(wù)端發(fā)送心跳包塞俱,所以在一段時間之后姐帚,服務(wù)端主動將ClientB
的連接(通道)關(guān)閉了,有人會問:明明ClientB
還活著呀障涯,這樣做合理嗎卧土?
其實這個問題是合理的惫皱,因為這里只是模擬線上環(huán)境測試,所以
ClientB
沒有主動發(fā)送數(shù)據(jù)包尤莺,但在線上環(huán)境旅敷,每個客戶端都會定期向服務(wù)端發(fā)送心跳包,都會為每個客戶端配置心跳處理器颤霎。在都配置了心跳處理器的情況下媳谁,如果一個客戶端長時間沒發(fā)送心跳包,這意味著這個客戶端十有八九涼涼了友酱,所以自然需要將其關(guān)閉晴音,防止這類“廢棄連接”占用服務(wù)端資源。
不過上述的心跳機制僅實現(xiàn)了最基礎(chǔ)的版本缔杉,還未徹底將其完善锤躁,但我這里就不繼續(xù)往下實現(xiàn)了,畢竟主干已經(jīng)搭建好了或详,剩下的只是一些細枝末節(jié)系羞,我這里提幾點完善思路:
- ①在檢測到某個客戶端未發(fā)送心跳包的情況下,服務(wù)端應(yīng)當(dāng)主動再發(fā)起一個探測包霸琴,二次確認客戶端是否真的掛了椒振,這樣做的好處在于:能夠有效避免網(wǎng)絡(luò)抖動造成的“客戶端假死”現(xiàn)象。
- ②客戶端梧乘、服務(wù)端之間交互的數(shù)據(jù)包澎迎,應(yīng)當(dāng)采用統(tǒng)一的格式進行封裝,也就是都遵守同一規(guī)范包裝數(shù)據(jù)选调,例如
{msgType:"Heartbeat", msgContent:"...", ...}
夹供。 - ③在客戶端被關(guān)閉的情況下,但凡不是因為物理因素仁堪,如機房斷電哮洽、網(wǎng)線被拔、機器宕機等情況造成的客戶端下線枝笨,客戶端都必須具備斷線重連功能袁铐。
將上述三條完善后,才能夠被稱為是一套相對健全的心跳檢測機制横浑,所以大家感興趣的情況下剔桨,可基于前面給出的源碼接著實現(xiàn)~