本文由竹子愛熊貓分享论寨,原題“(十一)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 在線查閱地址:
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)