▌写在前面:
大家好,我是大吴号,在前面的文章中,尼恩已经再一次的进行了通讯协议的重新选择
这就是:放弃了大家非常熟悉的json 格式,选择了性能更佳的 protobuf协议
在上一篇文章中,并且完成了netty 和 protobuf协议整合实战
具体的文章为: netty+protobuf 整合一:实战案例,带源码
另外,专门开出一篇文章,介绍了通讯消息数据包的几条设计准则
具体的文章为: netty +protobuf 整合二:protobuf 消息通讯协议设计的几个准则
在开始聊天器实战开发之前,还有一个非常基础的问题,需要解决:
这就是通讯的粘包和半包问题
注:本文以 pdf 持续更新,最新尼恩 架构笔记、面试题 的pdf文件,请到《技术自由圈》公众号获取
▌什么是粘包和半包?
先从数据包的发送和接收开始讲起
发送一次数据,举例如下:
channel.writeandflush(buffer);读取一次数据,举例如下:
public void channelread(channelhandlercontext ctx, object msg){ bytebuf bytebuf = (bytebuf) msg; //....}我们的理想是:发送端每发送一个buffer,接收端就能接收到一个一模一样的buffer
然而,理想很丰满,现实很骨感
在实际的通讯过程中,并没有大家预料的那么完美
一种意料之外的情况,如期而至这就是粘包和半包
那么,什么是粘包和半包?
粘包和半包定义如下:
▌粘包和半包 图解:
上面的理论比较抽象,下面用一幅图来形象说明
下图中,发送端发出4个数据包,接受端也接受到了4个数据包但是,通讯过程中,接收端出现了 粘包和半包

接收端收到的第一个包,正常
接收端收到的第二个包,就是一个粘包 将发送端的第二个包、第三个包,粘在一起了
接收端收到的第三个包,第四个包,就是半包将发送端的的第四个包,分开成了两个了
▌半包的实验:
由于在前文 netty+protobuf 整合一:实战案例,带源码 的源码中,没有看到异常的现象是因为代码屏蔽了半包的输出,所以看到的都是正常的数据包
稍微调整一下,在前文解码器的代码,加上半包的提示信息输出,就可以看到半包的提示
示意图如下:

调整过的半包警告的代码,如下:
/** * 解码器 * */public class protobufdecoder extends bytetomessagedecoder { //.... protected void decode(channelhandlercontext ctx, bytebuf in, listobject out) throws exception { //... // 读取传送过来的消息的长度 int length = in.readunsignedshort(); //... if (length in.readablebytes()) { // 读到的半包 // ... log.error("告警:读到的消息体长度小于传送过来的消息长度"); return; } //... 省略了正常包的处理 }}具体的源码,请参见本文末的源码工程:netty 粘包/半包原理与拆包实战 源码
可以根据文末源码,进行实验
▌粘包和半包更全实验:
上面的实例,只能看到半包的结果,看不到粘包的结果
为了看到粘包的场景,这里,不使用protobuf 协议,直接使用缓冲区进行读写通讯,设计了一个的简单的演示实验案例
案例已经设计好,可以根据文末源码,进行实验
运行实例,不仅可以看到半包的提示信息输出,而且可以看到粘包的提示信息输出,示意图如下:

我们可以看到,服务器收到的数据包,有包含多个发送端数据包的,这就是粘包了
另外,接收端还有出现乱码的数据包,就是只包含部分发送端数据,这就是半包了
这个实例的源码,直接简化了前面的基于protobuf协议通讯的实例源码代码的逻辑结构,是一样的
本实验的具体的源码,还是请参见本文末的源码工程:netty 粘包/半包原理与拆包实战 源码
▌粘包和半包原理:
这得从底层说起
在操作系统层面来说,我们使用了 tcp 协议
这就是粘包和半包的根源
首先,上层应用层每次读取底层缓冲的数据容量是有限制的,当tcp底层缓冲数据包比较大时,将被分成多次读取,造成断包,在应用层来说,就是半包
其次,如果上层应用层一次读到多个底层缓冲数据包,就是粘包
如何解决呢?
基本思路是,在接收端,需要根据自定义协议来,来读取底层的数据包,重新组装我们应用层的数据包,这个过程通常在接收端称为拆包
▌拆包的原理:
拆包基本原理,简单来说:
▌netty 中的拆包器:
拆包这个工作,netty 已经为大家备好了很多不同的拆包器本着不重复发明轮子的原则,我们直接使用netty现成的拆包器
netty 中的拆包器大致如下:
每个应用层数据包的都拆分成都是固定长度的大小,比如 1024字节
这个显然不大适应在 java 聊天程序 进行实际应用
每个应用层数据包,都以换行符作为分隔符,进行分割拆分
这个显然不大适应在 java 聊天程序 进行实际应用
每个应用层数据包,都通过自定义的分隔符,进行分割拆分
这个版本,是linebasedframedecoder 的通用版本,本质上是一样的
这个显然不大适应在 java 聊天程序 进行实际应用
将应用层数据包的长度,作为接收端应用层数据包的拆分依据按照应用层数据包的大小,拆包这个拆包器,有一个要求,就是应用层协议中包含数据包的长度
这个显然比较适和在 java 聊天程序 进行实际应用下面我们来应用这个拆分器
▌拆包之前的消息包装:
在使用
lengthfieldbasedframedecoder 拆包器之前 ,在发送端需要对protobuf 的消息包进行一轮包装
发送端包装的方法是:
在实际的protobuf 二进制消息包的前面,加上四个字节
前两个字节为版本号,后两个字节为实际发送的 protobuf 的消息长度

强调一下,二进制消息包装,在发送端进行
修改发送端的编码器 protobufencoder ,代码如下:
/** * 编码器 */ public class protobufencoder extends messagetobyteencoderprotomsg.message {@overrideprotected void encode(channelhandlercontext ctx, protomsg.message msg, bytebuf out) throws exception{ byte bytes = msg.tobytearray();// 将对象转换为byte int length = bytes.length;// 读取 protomsg 消息的长度 bytebuf buf = unpooled.buffer(2 + length); // 先将消息协议的版本写入,也就是消息头 buf.writeshort(constants.protocol_version); // 再将 protomsg 消息的长度写入 buf.writeshort(length); // 写入 protomsg 消息的消息体 buf.writebytes(bytes); //发送 out.writebytes(buf); }}发送端的步骤是:
buf.writeshort(constants.protocol_version);
▌开发一个接收端的自定义拆包器:
使用netty中,基于长度域拆包器
lengthfieldbasedframedecoder,按照实际的应用层数据包长度来拆分
需要做两个工作:
在前面的小节中,我们的长度信息(长度域)的占用字节数为 2个字节; 在报文中的所处的位置,长度信息(长度域)处于版本号之后
版本号是2个字节,从0开始数,长度信息(长度域)的在数据包中的位置为2
这些数据定义在constansts常量类中
public class constants{//协议版本号public static final short protocol_version = 1;//头部的长度: 版本号 + 报文长度public static final short protocol_headlength = 4;//长度的偏移public static final short length_offset = 2;//长度的字节数public static final short length_bytes_count = 2;}有了这些数据之后,可以基于netty 的长度拆包器
lengthfieldbasedframedecoder, 开发自己的长度分割器
新开发的分割器为packagespliter,代码如下:
package com.crazymakercircle.chat.common.codec;public class packagespliter extends lengthfieldbasedframedecoder{ public packagespliter() { super(integer.max_value, constants.length_offset,constants.length_bytes_count); } @override protected object decode(channelhandlercontext ctx, bytebuf in) throws exception { return super.decode(ctx, in); }}分割器 packagespliter 继承了
lengthfieldbasedframedecoder,传入了三个参数
分割器 写好之后,只需要在 pipeline 的最前面加上这个分割器,就可以使用这个分割器(自定义的拆包器)
▌自定义拆包器的实际应用:
在服务器端的 pipeline 的最前面加上这个分割器,代码如下:
package com.crazymakercircle.chat.server;//...@service("chatserver")public class chatserver{ static final logger logger = loggerfactory.getlogger(chatserver.class);//...//有连接到达时会创建一个channelprotected void initchannel(socketchannel ch) throws exception{ //应用自定义拆包器 ch.pipeline().addlast(new packagespliter()); ch.pipeline().addlast(new protobufdecoder()); ch.pipeline().addlast(new protobufencoder()); // pipeline管理channel中的handler // 在channel队列中添加一个handler来处理业务 ch.pipeline().addlast("serverhandler", serverhandler);}});//....}在发送端的 pipeline 的最前面加上这个分割器,代码也是类似的, 这里不再赘述大家可以在文末源码查看
▌为什么拆包器要加在pipeline 的最前面?
这一点,需要从packagespliter 的根源讲起
下面是自定义分割器 packagespliter 的继承关系图

由此可见,分割器 packagespliter 继承了
channelinboundhandleradapter
本质上,它是一个入站处理器
在 关于netty的入站处理流程一文 pipeline inbound 中, 我们已经知道,netty的入站处理的顺序,是从pipelin 流水线的前面到后面
由于在入站过程中,解码器 protobufdecoder 进行应用层 protobuf 的数据包的解码,而在此之前,必须完成应用包的正确分割
所以, 分割器 packagespliter 必须处于入站流水线处理的第一站,放在最前面
题外话, packagespliter 分割器 和 protobufencoder 编码器 是否有关系呢?
从流水线处理的角度来说,是没有次序关系的
packagespliter 是入站处理器 在入站流程中用到
protobufencoder 是出站处理器,在出站流程中用到
特别提示一下: 发送端不存在粘包和半包问题这是接收端的事情
总之,在出站和入站处理流程上,分割器 packagespliter 和 编码器protobufencoder , 没有半毛钱关系的
▌写在最后:
至此为止,终于完成了 java 聊天程序实战的一些基础开发工作
包括了协议的编码解码包括了粘包和半包的拆包处理
大家好,我是大吴号,基本上可以开始 聊天器的正式设计和开发的详细讲解了
本文的源码工程:netty 粘包/半包原理与拆包实战 源码(
▌技术自由的实现路径 pdf获取:
▌实现你的架构自由:
… 更多架构文章,正在添加中
▌实现你的 响应式 自由:
▌实现你的 spring cloud 自由:
▌实现你的 linux 自由:
▌实现你的 网络 自由:
▌实现你的 分布式锁 自由:
▌实现你的 王者组件 自由:
▌实现你的 面试题 自由:
4000页《尼恩java面试宝典》pdf 40个专题
....
注:以上尼恩 架构笔记、面试题 的pdf文件,请到《技术自由圈》公众号获取
还需要啥自由,可以告诉尼恩 尼恩帮你实现.......
【gucci包包的红黑榜】不知道你们有没有踩雷继续往下看↓↓↓希望你们喜欢哦第一组:古驰gucci红黑榜参考价:2100米色/乌木色gg supreme帆布,饰棕色皮革滚边金色调配件麂皮质感超细纤维衬里正面马衔扣细节顶部提手可拆卸皮革背带长20厘米;饰互扣式双g可拆卸链式肩带长52厘米重量:约0.7千克
#头条文章养成计划# 我总觉得“包治百病”这话容易误导女人,因为似乎在引导着我们都要拥有名牌的、流行的或是所谓代表赚钱能力的包,才算是踏入成功人士行列,或至少,有个愿意为你买包的好老公。可如果抛开所谓外界的标准,身为芸芸众生的我们,所赚的薪水也有限,真的需要去追求名牌包吗?谁说生活幸福或好品味的标准都要用高昂的奢侈品来衡量,更何况,不追求名牌,我们一样能找到适合自己的时尚单品,而且反而更独特不会撞衫,是更靠谱的时尚思路。
夏天什么颜色的包包最百搭?答案必然是浅色系的,黑色、棕色的百搭色系,一碰到夏天的穿搭就马上变得厚重,而桃红、苍蓝等色彩艳丽的,却不容易相处融洽,白色不容易清洁,相比之下,清爽的奶茶色包包就成了夏日百搭色首选。不会过于突出、米色带点复古、含蓄中带点温柔中性感觉的奶茶色甘心做配角,却又能给人温暖的调性,此外它的百搭能让无论是什么肤色的人,都能与之良好地融合搭配。
秋天的第一杯奶茶你喝了,那秋天的第一包你买了吗?夏季鲜艳亮眼的包包是时候该暂时下岗了,焦糖色、奶茶色、秋叶色的包包到了展现的时候。趁着刚刚入秋,出一期秋冬包包推荐合集,推荐几款包包给在这个阶段想要剁手的姐妹。还贴心给大家找了官网价格和二手行情价,想要入手二手奢侈品的姐妹也可以参考。1、lv pochette metis邮差包和dauphine达芙妮
爱马仕出了款新香twilly d’hermès,瓶颈用实打实的丝巾材质打了个好看的蝴蝶结,这不相当于,买瓶30ml香水白送丝巾?于是,一上市就火到断货的节奏,因为:“即使是“细面”版,也算拥有了人生第一件爱马仕哦~”tattoo on my facenlsn - tattoo on my face这瓶“丝巾香水”,目前还没闻过,
前段时间是法棍包诞生25周年,fendi可以说为它风光大办,一口气出了四个联名。虽然这个经典腋下包比我的粉丝们年纪都大,但是在包袋界,属实算是小小辈了。像爱马仕最老的成员hac,出生于1892年,如今还在专柜屹立不倒呢。然后是爱马仕1923年推出的bolide,品牌为了提升自家产品的价值,设计出了史上第一款拉链包。
hi大家好呀,月月说时尚又上线啦!包包和衣柜里的衣服同样重要,除了能出行装一些必带的物品,它更大的作用是起到配饰协调的作用。光秃秃的一身衣装打扮,没有包包的融入就会单调乏味很多。要是想花钱给自己找点快乐的话,假期去种草一个包包是个不错的主意。看到一个喜欢的包包,然后把它带回家,那种快乐是和穿新衣服不一样的。
天冷了,感觉胃口大开,吃嘛嘛香,饭量不仅大增,而且是刚吃饱没多久,又感觉想吃东西了,这样下去,是不是一个月要肥上10斤呀?肥就肥吧,美食还是不要错过的,不仅吃美食,做美食的过程也是一种幸福的体验哦,虽然天气冷,但进入厨房后,全身都会暖暖的,所以呀,别以冷为借口而不在家开伙哦。今天爱上我的生活要给大家分享的这一道美食,早上做来当早餐十分营养又美味,不过我们以前一直是做来当菜的,因为太好吃了,儿子直接是拿来当饭吃,后来我们早餐不知道吃什么的时候,就做来当早餐吃了,现在儿子隔三差五就点名要吃这一道早餐,他说比肠
女包作为时尚界的重要元素,是每个女性必备的时尚单品之一。在如今多元化的市场中,有许多备受追捧的女包品牌。本文将为您介绍女包品牌排行榜前十名,帮助您了解时下最受欢迎的女包品牌。561. hermès(爱马仕)hermès作为奢侈品行业的代表品牌,以其精湛的工艺和高品质的皮革制品而闻名。每一款hermès的女包都体现了奢华与雅致的风格,成为了成功人士追逐的梦想。
扒美包我们是一个“扒遍全世界”的邪恶且时髦的组织...每年一到春夏,小仙女那颗想换包包的心就开始蠢蠢欲动!要时髦、要百搭、要容量正合适,最好还要颜色选择多!挑来选去,貌似只有韩国的小众包包find kapoor符合要求!怪不得辣么多明星、时尚博主都爱它!find kapoor是一个主打水桶包的韩国小众品牌,在设计上选取了时下最流行的宽肩带+撞色搭配+铆钉手带,每款包包都像行走的“马卡龙”一样可爱!