2023-11-30跟著源碼學(xué)IM(十二):基于Netty打造一款高性能的IM即時(shí)通訊程序

本文由竹子愛熊貓分享论寨,原題“(十一)Netty實(shí)戰(zhàn)篇:基于Netty框架打造一款高性能的IM即時(shí)通訊程序”卤恳,本文有修訂和改動(dòng)萄喳。

1芬骄、引言

關(guān)于Netty網(wǎng)絡(luò)框架的內(nèi)容猾愿,前面已經(jīng)講了兩個(gè)章節(jié),但總歸來說難以真正掌握账阻,畢竟只是對(duì)其中一個(gè)個(gè)組件進(jìn)行講解蒂秘,很難讓諸位將其串起來形成一條線,所以本章中則會(huì)結(jié)合實(shí)戰(zhàn)案例淘太,對(duì)Netty進(jìn)行更深層次的學(xué)習(xí)與掌握材彪,實(shí)戰(zhàn)案例也并不難,一個(gè)非常樸素的IM聊天程序琴儿。

原本打算做個(gè)多人斗地主練習(xí)程序,但那需要織入過多的業(yè)務(wù)邏輯嘁捷,因此一方面會(huì)帶來不必要的理解難度造成,讓案例更為復(fù)雜化,另一方面代碼量也會(huì)偏多雄嚣,所以最終依舊選擇實(shí)現(xiàn)基本的IM聊天程序晒屎,既簡(jiǎn)單喘蟆,又能加深對(duì)Netty的理解。

2鼓鲁、配套源碼

本文配套源碼的開源托管地址是:

1)主地址:https://github.com/liuhaijieAdmin/springboot-netty

2)備地址:https://github.com/52im/springboot-netty

3蕴轨、知識(shí)準(zhǔn)備

關(guān)于 Netty 是什么,這里簡(jiǎn)單介紹下:

Netty 是一個(gè) Java 開源框架骇吭。Netty 提供異步的橙弱、事件驅(qū)動(dòng)的網(wǎng)絡(luò)應(yīng)用程序框架和工具,用以快速開發(fā)高性能燥狰、高可靠性的網(wǎng)絡(luò)服務(wù)器和客戶端程序棘脐。

也就是說,Netty 是一個(gè)基于 NIO 的客戶龙致、服務(wù)器端編程框架蛀缝,使用Netty 可以確保你快速和簡(jiǎn)單的開發(fā)出一個(gè)網(wǎng)絡(luò)應(yīng)用,例如實(shí)現(xiàn)了某種協(xié)議的客戶目代,服務(wù)端應(yīng)用屈梁。

Netty 相當(dāng)簡(jiǎn)化和流線化了網(wǎng)絡(luò)應(yīng)用的編程開發(fā)過程,例如榛了,TCP 和 UDP 的 Socket 服務(wù)開發(fā)在讶。

有關(guān)Netty的入門文章:

1)新手入門:目前為止最透徹的的Netty高性能原理和框架架構(gòu)解析

2)寫給初學(xué)者:Java高性能NIO框架Netty的學(xué)習(xí)方法和進(jìn)階策略

3)史上最通俗Netty框架入門長(zhǎng)文:基本介紹、環(huán)境搭建忽冻、動(dòng)手實(shí)戰(zhàn)

如果你連Java NIO都不知道真朗,下面的文章建議優(yōu)先讀:

1)少啰嗦!一分鐘帶你讀懂Java的NIO和經(jīng)典IO的區(qū)別

2)史上最強(qiáng)Java NIO入門:擔(dān)心從入門到放棄的僧诚,請(qǐng)讀這篇遮婶!

3)Java的BIO和NIO很難懂?用代碼實(shí)踐給你看湖笨,再不懂我轉(zhuǎn)行旗扑!

Netty源碼和API 在線查閱地址:

1)Netty-4.1.x 完整源碼(在線閱讀版)

2)Netty-4.1.x API文檔(在線版)

4、基于Netty設(shè)計(jì)通信協(xié)議

協(xié)議慈省,這玩意兒相信大家肯定不陌生了臀防,簡(jiǎn)單回顧一下協(xié)議的概念:網(wǎng)絡(luò)協(xié)議是指一種通信雙方都必須遵守的約定,兩個(gè)不同的端边败,按照一定的格式對(duì)數(shù)據(jù)進(jìn)行“編碼”袱衷,同時(shí)按照相同的規(guī)則進(jìn)行“解碼”,從而實(shí)現(xiàn)兩者之間的數(shù)據(jù)傳輸與通信笑窜。

當(dāng)自己想要打造一款I(lǐng)M通信程序時(shí)致燥,對(duì)于消息的封裝、拆分也同樣需要設(shè)計(jì)一個(gè)協(xié)議排截,通信的兩端都必須遵守該協(xié)議工作嫌蚤,這也是實(shí)現(xiàn)通信程序的前提辐益。

但為什么需要通信協(xié)議呢?

因?yàn)門CP/IP中是基于流的方式傳輸消息脱吱,消息與消息之間沒有邊界智政,而協(xié)議的目的則在于約定消息的樣式、邊界等箱蝠。

5续捂、Redis通信的RESP協(xié)議參考學(xué)習(xí)

不知大家是否還記得之前我聊到的RESP客戶端協(xié)議,這是Redis提供的一種客戶端通信協(xié)議抡锈。如果想要操作Redis疾忍,就必須遵守該協(xié)議的格式發(fā)送數(shù)據(jù)。

這個(gè)協(xié)議特別簡(jiǎn)單床三,如下:

1)首先要求所有命令一罩,都以*開頭,后面跟著具體的子命令數(shù)量撇簿,接著用換行符分割聂渊;

2)接著需要先用$符號(hào)聲明每個(gè)子命令的長(zhǎng)度,然后再用換行符分割四瘫;

3)最后再拼接上具體的子命令汉嗽,同樣用換行符分割。

這樣描述有些令人難懂找蜜,那就直接看個(gè)案例饼暑,例如一條簡(jiǎn)單set命令。

如下:

客戶端命令:

????setname ZhuZi

轉(zhuǎn)變?yōu)镽ESP指令:

????*3

????$3

????set

????$4

????name

????$5

????ZhuZi

按照Redis的規(guī)定洗做,但凡滿足RESP協(xié)議的客戶端弓叛,都可以直接連接并操作Redis服務(wù)端,這也就意味著咱們可以直接通過Netty來手寫一個(gè)Redis客戶端诚纸。

代碼如下:

// 基于Netty撰筷、RESP協(xié)議實(shí)現(xiàn)的Redis客戶端

publicclassRedisClient {

????// 換行符的ASCII碼

????staticfinalbyte[] LINE = {13, 10};

????publicstaticvoidmain(String[] args) {

????????EventLoopGroup worker = newNioEventLoopGroup();

????????Bootstrap client = newBootstrap();

????????try{

????????????client.group(worker);

????????????client.channel(NioSocketChannel.class);

????????????client.handler(newChannelInitializer<SocketChannel>() {

????????????????@Override

????????????????protectedvoidinitChannel(SocketChannel socketChannel)

????????????????????????????????????????????????????????throwsException {

????????????????????ChannelPipeline pipeline = socketChannel.pipeline();

????????????????????pipeline.addLast(newChannelInboundHandlerAdapter(){

????????????????????????// 通道建立成功后調(diào)用:向Redis發(fā)送一條set命令

????????????????????????@Override

????????????????????????publicvoidchannelActive(ChannelHandlerContext ctx)

????????????????????????????????????????????????????????????throwsException {

????????????????????????????String command = "set name ZhuZi";

????????????????????????????ByteBuf buffer = respCommand(command);

????????????????????????????ctx.channel().writeAndFlush(buffer);

????????????????????????}

????????????????????????// Redis響應(yīng)數(shù)據(jù)時(shí)觸發(fā):打印Redis的響應(yīng)結(jié)果

????????????????????????@Override

????????????????????????publicvoidchannelRead(ChannelHandlerContext ctx,

????????????????????????????????????????????????Object msg) throwsException {

????????????????????????????// 接受Redis服務(wù)端執(zhí)行指令后的結(jié)果

????????????????????????????ByteBuf buffer = (ByteBuf) msg;

????????????????????????????System.out.println(buffer.toString(CharsetUtil.UTF_8));

????????????????????????}

????????????????????});

????????????????}

????????????});

????????????// 根據(jù)IP、端口連接Redis服務(wù)端

????????????client.connect("192.168.12.129", 6379).sync();

????????} catch(Exception e){

????????????e.printStackTrace();

????????}

????}

????privatestaticByteBuf respCommand(String command){

????????// 先對(duì)傳入的命令以空格進(jìn)行分割

????????String[] commands = command.split(" ");

????????ByteBuf buffer = ByteBufAllocator.DEFAULT.buffer();

????????// 遵循RESP協(xié)議:先寫入指令的個(gè)數(shù)

????????buffer.writeBytes(("*"+ commands.length).getBytes());

????????buffer.writeBytes(LINE);

????????// 接著分別寫入每個(gè)指令的長(zhǎng)度以及具體值

????????for(String s : commands) {

????????????buffer.writeBytes(("$"+ s.length()).getBytes());

????????????buffer.writeBytes(LINE);

????????????buffer.writeBytes(s.getBytes());

????????????buffer.writeBytes(LINE);

????????}

????????// 把轉(zhuǎn)換成RESP格式的命令返回

????????returnbuffer;

????}

}

在上述這個(gè)案例中畦徘,也僅僅只是通過respCommand()這個(gè)方法毕籽,對(duì)用戶輸入的指令進(jìn)行了轉(zhuǎn)換。同時(shí)在上面通過Netty井辆,與Redis的地址关筒、端口建立了連接。在連接建立成功后杯缺,就會(huì)向Redis發(fā)送一條轉(zhuǎn)換成RESP指令的set命令平委。接著等待Redis的響應(yīng)結(jié)果并輸出,如下:

+OK

因?yàn)檫@是一條寫指令夺谁,所以當(dāng)Redis收到執(zhí)行完成后廉赔,最終就會(huì)返回一個(gè)OK,大家也可直接去Redis中查詢匾鸥,也依舊能夠查詢到剛剛寫入的name這個(gè)鍵值蜡塌。

6、HTTP超文本傳輸協(xié)議參考學(xué)習(xí)

前面咱們自己針對(duì)于Redis的RESP協(xié)議勿负,對(duì)用戶指令進(jìn)行了封裝馏艾,然后發(fā)往Redis執(zhí)行。

但對(duì)于這些常用的協(xié)議奴愉,Netty早已提供好了現(xiàn)成的處理器琅摩,想要使用時(shí)無需從頭開發(fā),可以直接使用現(xiàn)成的處理器來實(shí)現(xiàn)锭硼。

比如現(xiàn)在咱們可以基于Netty提供的處理器房资,實(shí)現(xiàn)一個(gè)簡(jiǎn)單的HTTP服務(wù)器。

代碼如下:

// 基于Netty提供的處理器實(shí)現(xiàn)HTTP服務(wù)器

publicclassHttpServer {

????publicstaticvoidmain(String[] args) throwsInterruptedException {

????????EventLoopGroup boss = newNioEventLoopGroup();

????????EventLoopGroup worker = newNioEventLoopGroup();

????????ServerBootstrap server = newServerBootstrap();

????????server

????????????.group(boss,worker)

????????????.channel(NioServerSocketChannel.class)

????????????.childHandler(newChannelInitializer<NioSocketChannel>() {

????????????????@Override

????????????????protectedvoidinitChannel(NioSocketChannel ch) {

????????????????????ChannelPipeline pipeline = ch.pipeline();

????????????????????// 添加一個(gè)Netty提供的HTTP處理器

????????????????????pipeline.addLast(newHttpServerCodec());

????????????????????pipeline.addLast(newChannelInboundHandlerAdapter() {

????????????????????????@Override

????????????????????????publicvoidchannelRead(ChannelHandlerContext ctx,

????????????????????????????????????????????????Object msg) throwsException {

????????????????????????????// 在這里輸出一下消息的類型

????????????????????????????System.out.println("消息類型:"+ msg.getClass());

????????????????????????????super.channelRead(ctx, msg);

????????????????????????}

????????????????????});

????????????????????pipeline.addLast(newSimpleChannelInboundHandler<HttpRequest>() {

????????????????????????@Override

????????????????????????protectedvoidchannelRead0(ChannelHandlerContext ctx,

????????????????????????????????????????????????????HttpRequest msg) throwsException {

????????????????????????????System.out.println("客戶端的請(qǐng)求路徑:"+ msg.uri());

????????????????????????????// 創(chuàng)建一個(gè)響應(yīng)對(duì)象檀头,版本號(hào)與客戶端保持一致轰异,狀態(tài)碼為OK/200

????????????????????????????DefaultFullHttpResponse response =

????????????????????????????????????newDefaultFullHttpResponse(

????????????????????????????????????????????msg.protocolVersion(),

????????????????????????????????????????????HttpResponseStatus.OK);

????????????????????????????// 構(gòu)造響應(yīng)內(nèi)容

????????????????????????????byte[] content = "<h1>Hi, ZhuZi!</h1>".getBytes();

????????????????????????????// 設(shè)置響應(yīng)頭:告訴客戶端本次響應(yīng)的數(shù)據(jù)長(zhǎng)度

????????????????????????????response.headers().setInt(

????????????????????????????????HttpHeaderNames.CONTENT_LENGTH,content.length);

????????????????????????????// 設(shè)置響應(yīng)主體

????????????????????????????response.content().writeBytes(content);

????????????????????????????// 向客戶端寫入響應(yīng)數(shù)據(jù)

????????????????????????????ctx.writeAndFlush(response);

????????????????????????}

????????????????????});

????????????????}

????????????})

????????????.bind("127.0.0.1",8888)

????????????.sync();

????}

}

在該案例中,咱們就未曾手動(dòng)對(duì)HTTP的數(shù)據(jù)包進(jìn)行拆包處理了暑始,而是在服務(wù)端的pipeline上添加了一個(gè)HttpServerCodec處理器搭独,這個(gè)處理器是Netty官方提供的。

其類繼承關(guān)系如下:

publicfinalclassHttpServerCodec

????extendsCombinedChannelDuplexHandler<HttpRequestDecoder, HttpResponseEncoder>

????implementsSourceCodec {

????// ......

}

觀察會(huì)發(fā)現(xiàn)廊镜,該類繼承自CombinedChannelDuplexHandler這個(gè)組合類牙肝,它組合了編碼器、解碼器嗤朴。

這也就意味著HttpServerCodec即可以對(duì)客戶端的數(shù)據(jù)做解碼配椭,也可以對(duì)服務(wù)端響應(yīng)的數(shù)據(jù)做編碼。

同時(shí)除開添加了這個(gè)處理器外播赁,在第二個(gè)處理器中打印了一下客戶端的消息類型颂郎,最后一個(gè)處理器中,對(duì)客戶端的請(qǐng)求做出了響應(yīng)容为,其實(shí)也就是返回了一句話而已乓序。

此時(shí)在瀏覽器輸入http://127.0.0.1:8888/index.html,結(jié)果如下:

消息類型:classio.netty.handler.codec.http.DefaultHttpRequest

消息類型:classio.netty.handler.codec.http.LastHttpContent$1

客戶端的請(qǐng)求路徑:/index.html

此時(shí)來看結(jié)果坎背,客戶端的請(qǐng)求會(huì)被解析成兩個(gè)部分:

1)第一個(gè)是請(qǐng)求信息替劈;

2)第二個(gè)是主體信息。

但按理來說瀏覽器發(fā)出的請(qǐng)求得滤,屬于GET類型的請(qǐng)求陨献,GET請(qǐng)求是沒有請(qǐng)求體信息的,但Netty依舊會(huì)解析成兩部分~懂更,只不過GET請(qǐng)求的第二部分是空的眨业。

在第三個(gè)處理器中急膀,咱們直接向客戶端返回了一個(gè)h1標(biāo)簽,同時(shí)也要記得在響應(yīng)頭里面龄捡,加上響應(yīng)內(nèi)容的長(zhǎng)度信息卓嫂,否則瀏覽器的加載圈,會(huì)一直不同的轉(zhuǎn)動(dòng)聘殖,畢竟瀏覽器也不知道內(nèi)容有多長(zhǎng)晨雳,就會(huì)一直反復(fù)加載,嘗試等待更多的數(shù)據(jù)奸腺。

在第三個(gè)處理器中餐禁,咱們直接向客戶端返回了一個(gè)h1標(biāo)簽,同時(shí)也要記得在響應(yīng)頭里面突照,加上響應(yīng)內(nèi)容的長(zhǎng)度信息帮非,否則瀏覽器的加載圈,會(huì)一直不同的轉(zhuǎn)動(dòng)绷旗,畢竟瀏覽器也不知道內(nèi)容有多長(zhǎng)喜鼓,就會(huì)一直反復(fù)加載,嘗試等待更多的數(shù)據(jù)衔肢。

7庄岖、自定義消息傳輸協(xié)議

7.1概述

Netty除開提供了HTTP協(xié)議的處理器外,還提供了DNS角骤、HaProxy隅忿、MemCache、MQTT邦尊、Protobuf背桐、Redis、SCTP蝉揍、RTSP.....一系列協(xié)議的實(shí)現(xiàn)链峭,具體定義位于io.netty.handler.codec這個(gè)包下,當(dāng)然又沾,咱們也可以自己實(shí)現(xiàn)自定義協(xié)議弊仪,按照自己的邏輯對(duì)數(shù)據(jù)進(jìn)行編解碼處理。

很多基于Netty開發(fā)的中間件/組件杖刷,其內(nèi)部基本上都開發(fā)了專屬的通信協(xié)議励饵,以此來作為不同節(jié)點(diǎn)間通信的基礎(chǔ),所以解下來咱們基于Netty也來自己設(shè)計(jì)一款通信協(xié)議滑燃,這也會(huì)作為后續(xù)實(shí)現(xiàn)聊天程序時(shí)的基礎(chǔ)役听。

所謂的協(xié)議設(shè)計(jì),其實(shí)僅僅只需要按照一定約束,實(shí)現(xiàn)編碼器與解碼器即可典予,發(fā)送方在發(fā)出數(shù)據(jù)之前甜滨,會(huì)經(jīng)過編碼器對(duì)數(shù)據(jù)進(jìn)行處理,而接收方在收到數(shù)據(jù)之前熙参,則會(huì)由解碼器對(duì)數(shù)據(jù)進(jìn)行處理艳吠。

7.2自定義協(xié)議的要素

在自定義傳輸協(xié)議時(shí),咱們必然需要考慮幾個(gè)因素孽椰,如下:

1)魔數(shù):用來第一時(shí)間判斷是否為自己需要的數(shù)據(jù)包;

2)版本號(hào):提高協(xié)議的拓展性扛稽,方便后續(xù)對(duì)協(xié)議進(jìn)行升級(jí)藏鹊;

3)序列化算法:消息正文具體該使用哪種方式進(jìn)行序列化傳輸旭贬,例如Json、ProtoBuf锐涯、JDK...;

4)消息類型:第一時(shí)間判斷出當(dāng)前消息的類型填物;

5)消息序號(hào):為了實(shí)現(xiàn)雙工通信纹腌,客戶端和服務(wù)端之間收/發(fā)消息不會(huì)相互阻塞;

6)正文長(zhǎng)度:提供給LTC解碼器使用滞磺,防止解碼時(shí)出現(xiàn)粘包升薯、半包的現(xiàn)象;

7)消息正文:本次消息要傳輸?shù)木唧w數(shù)據(jù)击困。

在設(shè)計(jì)協(xié)議時(shí)涎劈,一個(gè)完整的協(xié)議應(yīng)該涵蓋上述所說的幾方面,這樣才能提供雙方通信時(shí)的基礎(chǔ)阅茶。

基于上述幾個(gè)字段蛛枚,能夠在第一時(shí)間內(nèi)判斷出:

1)消息是否可用;

2)當(dāng)前協(xié)議版本脸哀;

3)消息的具體類型蹦浦;

4)消息的長(zhǎng)度等各類信息。

從而給后續(xù)處理器使用(自定義的協(xié)議規(guī)則本身就是一個(gè)編解碼處理器而已)撞蜂。

7.3自定義協(xié)議實(shí)戰(zhàn)

前面簡(jiǎn)單聊到過盲镶,所謂的自定義協(xié)議就是自己規(guī)定消息格式,以及自己實(shí)現(xiàn)編/解碼器對(duì)消息實(shí)現(xiàn)封裝/拆解谅摄,所以這里想要自定義一個(gè)消息協(xié)議徒河,就只需要滿足前面兩個(gè)條件即可。

因此實(shí)現(xiàn)如下:

@ChannelHandler.Sharable

publicclassChatMessageCodec extendsMessageToMessageCodec<ByteBuf, Message> {

????// 消息出站時(shí)會(huì)經(jīng)過的編碼方法(將原生消息對(duì)象封裝成自定義協(xié)議的消息格式)

????@Override

????protectedvoidencode(ChannelHandlerContext ctx, Message msg,

??????????????????????????List<Object> list) throwsException {

????????ByteBuf outMsg = ctx.alloc().buffer();

????????// 前五個(gè)字節(jié)作為魔數(shù)

????????byte[] magicNumber = newbyte[]{'Z','h','u','Z','i'};

????????outMsg.writeBytes(magicNumber);

????????// 一個(gè)字節(jié)作為版本號(hào)

????????outMsg.writeByte(1);

????????// 一個(gè)字節(jié)表示序列化方式? 0:JDK送漠、1:Json顽照、2:ProtoBuf.....

????????outMsg.writeByte(0);

????????// 一個(gè)字節(jié)用于表示消息類型

????????outMsg.writeByte(msg.getMessageType());

????????// 四個(gè)字節(jié)表示消息序號(hào)

????????outMsg.writeInt(msg.getSequenceId());

????????// 使用Java-Serializable的方式對(duì)消息對(duì)象進(jìn)行序列化

????????ByteArrayOutputStream bos = newByteArrayOutputStream();

????????ObjectOutputStream oos = newObjectOutputStream(bos);

????????oos.writeObject(msg);

????????byte[] msgBytes = bos.toByteArray();

????????// 使用四個(gè)字節(jié)描述消息正文的長(zhǎng)度

????????outMsg.writeInt(msgBytes.length);

????????// 將序列化后的消息對(duì)象作為消息正文

????????outMsg.writeBytes(msgBytes);

????????// 將封裝好的數(shù)據(jù)傳遞給下一個(gè)處理器

????????list.add(outMsg);

????}

????// 消息入站時(shí)會(huì)經(jīng)過的解碼方法(將自定義格式的消息轉(zhuǎn)變?yōu)榫唧w的消息對(duì)象)

????@Override

????protectedvoiddecode(ChannelHandlerContext ctx,

??????????????????????????ByteBuf inMsg, List<Object> list) throwsException {

????????// 讀取前五個(gè)字節(jié)得到魔數(shù)

????????byte[] magicNumber = newbyte[5];

????????inMsg.readBytes(magicNumber,0,5);

????????// 再讀取一個(gè)字節(jié)得到版本號(hào)

????????byteversion = inMsg.readByte();

????????// 再讀取一個(gè)字節(jié)得到序列化方式

????????byteserializableType = inMsg.readByte();

????????// 再讀取一個(gè)字節(jié)得到消息類型

????????bytemessageType = inMsg.readByte();

????????// 再讀取四個(gè)字節(jié)得到消息序號(hào)

????????intsequenceId = inMsg.readInt();

????????// 再讀取四個(gè)字節(jié)得到消息正文長(zhǎng)度

????????intmessageLength = inMsg.readInt();

????????// 再根據(jù)正文長(zhǎng)度讀取序列化后的字節(jié)正文數(shù)據(jù)

????????byte[] msgBytes = newbyte[messageLength];

????????inMsg.readBytes(msgBytes,0,messageLength);

????????// 對(duì)于讀取到的消息正文進(jìn)行反序列化,最終得到具體的消息對(duì)象

????????ByteArrayInputStream bis = newByteArrayInputStream(msgBytes);

????????ObjectInputStream ois = newObjectInputStream(bis);

????????Message message = (Message) ois.readObject();

????????// 最終把反序列化得到的消息對(duì)象傳遞給后續(xù)的處理器

????????list.add(message);

????}

}

上面自定義的處理器中,繼承了MessageToMessageCodec類代兵,主要負(fù)責(zé)將數(shù)據(jù)在原生ByteBuf與Message之間進(jìn)行相互轉(zhuǎn)換尼酿,而Message對(duì)象是自定義的消息對(duì)象,這里暫且無需過多關(guān)心植影。

其中主要實(shí)現(xiàn)了兩個(gè)方法:

1)encode():出站時(shí)會(huì)經(jīng)過的編碼方法裳擎,會(huì)將原生消息對(duì)象按自定義的協(xié)議封裝成對(duì)應(yīng)的字節(jié)數(shù)據(jù);

2)decode():入站時(shí)會(huì)經(jīng)過的解碼方法思币,會(huì)將協(xié)議格式的字節(jié)數(shù)據(jù)鹿响,轉(zhuǎn)變?yōu)榫唧w的消息對(duì)象。

上述自定義的協(xié)議谷饿,也就是一定規(guī)則的字節(jié)數(shù)據(jù)惶我,每條消息數(shù)據(jù)的組成如下:

1)魔數(shù):使用第1~5個(gè)字節(jié)來描述,這個(gè)魔數(shù)值可以按自己的想法自定義博投;

2)版本號(hào):使用第6個(gè)字節(jié)來描述绸贡,不同數(shù)字表示不同版本;

3)序列化算法:使用第7個(gè)字節(jié)來描述毅哗,不同數(shù)字表示不同序列化方式听怕;

4)消息類型:使用第8個(gè)字節(jié)來描述,不同的消息類型使用不同數(shù)字表示虑绵;

5)消息序號(hào):使用第9~12個(gè)字節(jié)來描述尿瞭,其實(shí)就是一個(gè)四字節(jié)的整數(shù);

6)正文長(zhǎng)度:使用第13~16個(gè)字節(jié)來描述蒸殿,也是一個(gè)四字節(jié)的整數(shù)筷厘;

7)消息正文:長(zhǎng)度不固定,根據(jù)每次具體發(fā)送的數(shù)據(jù)來決定宏所。

在其中酥艳,為了實(shí)現(xiàn)簡(jiǎn)單,這里的序列化方式爬骤,則采用的是JDK默認(rèn)的Serializable接口方式充石,但這種方式生成的對(duì)象字節(jié)較大,實(shí)際情況中最好還是選擇谷歌的ProtoBuf方式霞玄,這種算法屬于序列化算法中骤铃,性能最佳的一種落地實(shí)現(xiàn)。

當(dāng)然坷剧,這個(gè)自定義的協(xié)議是提供給后續(xù)的聊天業(yè)務(wù)使用的惰爬,但這種實(shí)戰(zhàn)型的內(nèi)容分享,基本上代碼量較高惫企,所以大家看起來會(huì)有些枯燥撕瞧,而本文所使用的聊天室案例陵叽,是基于《B站-黑馬Netty視頻教程》二次改良的,因此如若感覺文字描述較為枯燥丛版,可直接點(diǎn)擊前面給出的鏈接巩掺,觀看P101~P121視頻進(jìn)行學(xué)習(xí)。

最后來觀察一下页畦,大家會(huì)發(fā)現(xiàn)胖替,在咱們定義的這個(gè)協(xié)議編解碼處理器上,存在著一個(gè)@ChannelHandler.Sharable注解豫缨,這個(gè)注解的作用是干嗎的呢独令?其實(shí)很簡(jiǎn)單,用來標(biāo)識(shí)當(dāng)前處理器是否可在多線程環(huán)境下使用好芭,如果帶有該注解的處理器记焊,則表示可以在多個(gè)通道間共用,因此只需要?jiǎng)?chuàng)建一個(gè)即可栓撞,反之同理,如果不帶有該注解的處理器碗硬,則每個(gè)通道需要單獨(dú)創(chuàng)建使用瓤湘。

PS:如果你想系統(tǒng)學(xué)習(xí)Protobuf,可以從以下文章入手:

如何選擇即時(shí)通訊應(yīng)用的數(shù)據(jù)傳輸格式

強(qiáng)列建議將Protobuf作為你的即時(shí)通訊應(yīng)用數(shù)據(jù)傳輸格式

IM通訊協(xié)議專題學(xué)習(xí)(一):Protobuf從入門到精通恩尾,一篇就夠弛说!

IM通訊協(xié)議專題學(xué)習(xí)(二):快速理解Protobuf的背景、原理翰意、使用木人、優(yōu)缺點(diǎn)

IM通訊協(xié)議專題學(xué)習(xí)(三):由淺入深,從根上理解Protobuf的編解碼原理

IM通訊協(xié)議專題學(xué)習(xí)(四):從Base64到Protobuf冀偶,詳解Protobuf的數(shù)據(jù)編碼原理

IM通訊協(xié)議專題學(xué)習(xí)(八):金蝶隨手記團(tuán)隊(duì)的Protobuf應(yīng)用實(shí)踐(原理篇)

最后來觀察一下醒第,大家會(huì)發(fā)現(xiàn),在咱們定義的這個(gè)協(xié)議編解碼處理器上进鸠,存在著一個(gè)@ChannelHandler.Sharable注解稠曼,這個(gè)注解的作用是干嗎的呢?其實(shí)很簡(jiǎn)單客年,用來標(biāo)識(shí)當(dāng)前處理器是否可在多線程環(huán)境下使用霞幅,如果帶有該注解的處理器,則表示可以在多個(gè)通道間共用量瓜,因此只需要?jiǎng)?chuàng)建一個(gè)即可司恳,反之同理,如果不帶有該注解的處理器绍傲,則每個(gè)通道需要單獨(dú)創(chuàng)建使用扔傅。

PS:如果你想系統(tǒng)學(xué)習(xí)Protobuf,可以從以下文章入手:

如何選擇即時(shí)通訊應(yīng)用的數(shù)據(jù)傳輸格式

強(qiáng)列建議將Protobuf作為你的即時(shí)通訊應(yīng)用數(shù)據(jù)傳輸格式

IM通訊協(xié)議專題學(xué)習(xí)(一):Protobuf從入門到精通,一篇就夠铅鲤!

IM通訊協(xié)議專題學(xué)習(xí)(二):快速理解Protobuf的背景划提、原理、使用邢享、優(yōu)缺點(diǎn)

IM通訊協(xié)議專題學(xué)習(xí)(三):由淺入深鹏往,從根上理解Protobuf的編解碼原理

IM通訊協(xié)議專題學(xué)習(xí)(四):從Base64到Protobuf,詳解Protobuf的數(shù)據(jù)編碼原理

IM通訊協(xié)議專題學(xué)習(xí)(八):金蝶隨手記團(tuán)隊(duì)的Protobuf應(yīng)用實(shí)踐(原理篇)

12骇塘、系列文章

跟著源碼學(xué)IM(一):手把手教你用Netty實(shí)現(xiàn)心跳機(jī)制伊履、斷線重連機(jī)制

跟著源碼學(xué)IM(二):自已開發(fā)IM很難?手把手教你擼一個(gè)Andriod版IM

跟著源碼學(xué)IM(三):基于Netty款违,從零開發(fā)一個(gè)IM服務(wù)端

跟著源碼學(xué)IM(四):拿起鍵盤就是干唐瀑,教你徒手開發(fā)一套分布式IM系統(tǒng)

跟著源碼學(xué)IM(五):正確理解IM長(zhǎng)連接、心跳及重連機(jī)制插爹,并動(dòng)手實(shí)現(xiàn)

跟著源碼學(xué)IM(六):手把手教你用Go快速搭建高性能哄辣、可擴(kuò)展的IM系統(tǒng)

跟著源碼學(xué)IM(七):手把手教你用WebSocket打造Web端IM聊天

跟著源碼學(xué)IM(八):萬字長(zhǎng)文,手把手教你用Netty打造IM聊天

跟著源碼學(xué)IM(九):基于Netty實(shí)現(xiàn)一套分布式IM系統(tǒng)

跟著源碼學(xué)IM(十):基于Netty赠尾,搭建高性能IM集群(含技術(shù)思路+源碼)

跟著源碼學(xué)IM(十一):一套基于Netty的分布式高可用IM詳細(xì)設(shè)計(jì)與實(shí)現(xiàn)(有源碼)

跟著源碼學(xué)IM(十二):基于Netty打造一款高性能的IM即時(shí)通訊程序》(* 本文

SpringBoot集成開源IM框架MobileIMSDK力穗,實(shí)現(xiàn)即時(shí)通訊IM聊天功能

13、參考資料

[1]淺談IM系統(tǒng)的架構(gòu)設(shè)計(jì)

[2]簡(jiǎn)述移動(dòng)端IM開發(fā)的那些坑:架構(gòu)設(shè)計(jì)气嫁、通信協(xié)議和客戶端

[3]一套海量在線用戶的移動(dòng)端IM架構(gòu)設(shè)計(jì)實(shí)踐分享(含詳細(xì)圖文)

[4]一套原創(chuàng)分布式即時(shí)通訊(IM)系統(tǒng)理論架構(gòu)方案

[5]一套億級(jí)用戶的IM架構(gòu)技術(shù)干貨(上篇):整體架構(gòu)当窗、服務(wù)拆分等

[6]一套億級(jí)用戶的IM架構(gòu)技術(shù)干貨(下篇):可靠性、有序性寸宵、弱網(wǎng)優(yōu)化等

[7]史上最通俗Netty框架入門長(zhǎng)文:基本介紹崖面、環(huán)境搭建、動(dòng)手實(shí)戰(zhàn)

[8]強(qiáng)列建議將Protobuf作為你的即時(shí)通訊應(yīng)用數(shù)據(jù)傳輸格式

[9]IM通訊協(xié)議專題學(xué)習(xí)(一):Protobuf從入門到精通梯影,一篇就夠巫员!

[10]融云技術(shù)分享:全面揭秘億級(jí)IM消息的可靠投遞機(jī)制

[11]IM群聊消息如此復(fù)雜,如何保證不丟不重光酣?

[12]零基礎(chǔ)IM開發(fā)入門(四):什么是IM系統(tǒng)的消息時(shí)序一致性疏遏?

[13]如何保證IM實(shí)時(shí)消息的“時(shí)序性”與“一致性”?

[14]微信的海量IM聊天消息序列號(hào)生成實(shí)踐(算法原理篇)

[15]網(wǎng)易云信技術(shù)分享:IM中的萬人群聊技術(shù)方案實(shí)踐總結(jié)

[16]融云IM技術(shù)分享:萬人群聊消息投遞方案的思考和實(shí)踐

[17]為何基于TCP協(xié)議的移動(dòng)端IM仍然需要心跳本染活機(jī)制财异?

[18]一文讀懂即時(shí)通訊應(yīng)用中的網(wǎng)絡(luò)心跳包機(jī)制:作用、原理唱遭、實(shí)現(xiàn)思路等

[19]微信團(tuán)隊(duì)原創(chuàng)分享:Android版微信后臺(tái)贝链纾活實(shí)戰(zhàn)分享(網(wǎng)絡(luò)保活篇)

[20]融云技術(shù)分享:融云安卓端IM產(chǎn)品的網(wǎng)絡(luò)鏈路笨皆螅活技術(shù)實(shí)踐

[21]徹底搞懂TCP協(xié)議層的KeepAlive币呷担活機(jī)制

[22]深度解密釘釘即時(shí)消息服務(wù)DTIM的技術(shù)設(shè)計(jì)

(本文已同步發(fā)布于:http://www.52im.net/thread-4530-1-1.html

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末袖瞻,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子拆吆,更是在濱河造成了極大的恐慌聋迎,老刑警劉巖,帶你破解...
    沈念sama閱讀 206,311評(píng)論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件枣耀,死亡現(xiàn)場(chǎng)離奇詭異霉晕,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)捞奕,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,339評(píng)論 2 382
  • 文/潘曉璐 我一進(jìn)店門牺堰,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人颅围,你說我怎么就攤上這事伟葫。” “怎么了院促?”我有些...
    開封第一講書人閱讀 152,671評(píng)論 0 342
  • 文/不壞的土叔 我叫張陵筏养,是天一觀的道長(zhǎng)。 經(jīng)常有香客問我常拓,道長(zhǎng)撼玄,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 55,252評(píng)論 1 279
  • 正文 為了忘掉前任墩邀,我火速辦了婚禮,結(jié)果婚禮上盏浙,老公的妹妹穿的比我還像新娘眉睹。我一直安慰自己,他們只是感情好废膘,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,253評(píng)論 5 371
  • 文/花漫 我一把揭開白布竹海。 她就那樣靜靜地躺著,像睡著了一般丐黄。 火紅的嫁衣襯著肌膚如雪斋配。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,031評(píng)論 1 285
  • 那天灌闺,我揣著相機(jī)與錄音艰争,去河邊找鬼。 笑死桂对,一個(gè)胖子當(dāng)著我的面吹牛甩卓,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播蕉斜,決...
    沈念sama閱讀 38,340評(píng)論 3 399
  • 文/蒼蘭香墨 我猛地睜開眼逾柿,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼缀棍!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起机错,我...
    開封第一講書人閱讀 36,973評(píng)論 0 259
  • 序言:老撾萬榮一對(duì)情侶失蹤爬范,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后弱匪,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體青瀑,經(jīng)...
    沈念sama閱讀 43,466評(píng)論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 35,937評(píng)論 2 323
  • 正文 我和宋清朗相戀三年痢法,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了狱窘。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 38,039評(píng)論 1 333
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡财搁,死狀恐怖蘸炸,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情尖奔,我是刑警寧澤搭儒,帶...
    沈念sama閱讀 33,701評(píng)論 4 323
  • 正文 年R本政府宣布,位于F島的核電站提茁,受9級(jí)特大地震影響淹禾,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜茴扁,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,254評(píng)論 3 307
  • 文/蒙蒙 一铃岔、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧峭火,春花似錦毁习、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,259評(píng)論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至稍浆,卻和暖如春载碌,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背衅枫。 一陣腳步聲響...
    開封第一講書人閱讀 31,485評(píng)論 1 262
  • 我被黑心中介騙來泰國(guó)打工嫁艇, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人弦撩。 一個(gè)月前我還...
    沈念sama閱讀 45,497評(píng)論 2 354
  • 正文 我出身青樓裳仆,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親孤钦。 傳聞我的和親對(duì)象是個(gè)殘疾皇子歧斟,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,786評(píng)論 2 345

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