(十)Netty進階篇:漫談網(wǎng)絡(luò)粘包逝她、半包問題浇坐、解碼器與長連接、心跳機制實戰(zhàn)

引言

在前面關(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)

ChannelOptionNetty提供的參數(shù)調(diào)整類叼丑,該類中提供了很多常量关翎,分別對應(yīng)著底層TCP、UDP幢码、計算機網(wǎng)絡(luò)的一些參數(shù)笤休,在創(chuàng)建服務(wù)端、客戶端時症副,我們可以通過ChannelOption類來調(diào)整網(wǎng)絡(luò)參數(shù)店雅,以此滿足不同的業(yè)務(wù)需求,該類中提供的常量列表如下:

  • ALLOCATORByteBuf緩沖區(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)仗考。
  • 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:開啟TCPNagle算法期虾,會將多個小包合并成一個大包發(fā)送原朝。
  • DATAGRAM_CHANNEL_ACTIVE_ON_REGISTRATIONDatagramChannel注冊的EventLoop即表示已激活。
  • SINGLE_EVENTEXECUTOR_PER_GROUPPipeline是否由單線程執(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:開啟TCPNagle算法牛郑,會將多個小包合并成一個大包發(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的連接。
  • 客戶端主動告知:每間隔一定時間后盹牧,客戶端向服務(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)~

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市徙融,隨后出現(xiàn)的幾起案子洒缀,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 206,214評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件树绩,死亡現(xiàn)場離奇詭異萨脑,居然都是意外死亡,警方通過查閱死者的電腦和手機饺饭,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,307評論 2 382
  • 文/潘曉璐 我一進店門渤早,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人瘫俊,你說我怎么就攤上這事鹊杖。” “怎么了扛芽?”我有些...
    開封第一講書人閱讀 152,543評論 0 341
  • 文/不壞的土叔 我叫張陵骂蓖,是天一觀的道長。 經(jīng)常有香客問我川尖,道長登下,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 55,221評論 1 279
  • 正文 為了忘掉前任叮喳,我火速辦了婚禮被芳,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘嘲更。我一直安慰自己筐钟,他們只是感情好揩瞪,可當(dāng)我...
    茶點故事閱讀 64,224評論 5 371
  • 文/花漫 我一把揭開白布赋朦。 她就那樣靜靜地躺著,像睡著了一般李破。 火紅的嫁衣襯著肌膚如雪宠哄。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,007評論 1 284
  • 那天嗤攻,我揣著相機與錄音毛嫉,去河邊找鬼。 笑死妇菱,一個胖子當(dāng)著我的面吹牛承粤,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播闯团,決...
    沈念sama閱讀 38,313評論 3 399
  • 文/蒼蘭香墨 我猛地睜開眼辛臊,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了房交?” 一聲冷哼從身側(cè)響起彻舰,我...
    開封第一講書人閱讀 36,956評論 0 259
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后刃唤,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體隔心,經(jīng)...
    沈念sama閱讀 43,441評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 35,925評論 2 323
  • 正文 我和宋清朗相戀三年尚胞,在試婚紗的時候發(fā)現(xiàn)自己被綠了硬霍。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 38,018評論 1 333
  • 序言:一個原本活蹦亂跳的男人離奇死亡笼裳,死狀恐怖须尚,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情侍咱,我是刑警寧澤耐床,帶...
    沈念sama閱讀 33,685評論 4 322
  • 正文 年R本政府宣布,位于F島的核電站楔脯,受9級特大地震影響撩轰,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜昧廷,卻給世界環(huán)境...
    茶點故事閱讀 39,234評論 3 307
  • 文/蒙蒙 一堪嫂、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧木柬,春花似錦皆串、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,240評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至速挑,卻和暖如春谤牡,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背姥宝。 一陣腳步聲響...
    開封第一講書人閱讀 31,464評論 1 261
  • 我被黑心中介騙來泰國打工翅萤, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人腊满。 一個月前我還...
    沈念sama閱讀 45,467評論 2 352
  • 正文 我出身青樓套么,卻偏偏與公主長得像,于是被迫代替她去往敵國和親碳蛋。 傳聞我的和親對象是個殘疾皇子胚泌,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 42,762評論 2 345

推薦閱讀更多精彩內(nèi)容