微言netty:不在浮沙筑高台

1. 写做缘起html

几年前,我在一家农业物联网公司,负责解决其物联网产品线。咱们当时基于.net平台打造了一套实时数据采集系统,能够把数以百万级的传感器传送回来的数据采集入库并根据这些数据进行建模。在搭建这套实时数据采集系统的时候,高并发高可用被首次提出,同时要求系统不会有太大的时延。一旦有时延,也就意味着损失。好比一个有3000头猪的猪舍,假设空气温度达到了比较高的水平,可是采集探头采集的数据上传到服务器管道中,因为被积压了5分钟后才被处理,那么主动预警系统打开风机的时候,也许已经晚了,这五分钟的时间里,上百头小猪仔由于温度太高的缘故死于非命。固然,鱼塘,蔬菜大棚等也有相似的场景。java

当时在打造此系统的时候,咱们用的仍是.net,翻阅了不少源码,查阅了不少资料,最后咱们基于SocketAsyncEventArgs来打造一个本身的物联网服务端。当时在.net里面,尚未一款可以匹敌netty的开源组件出来,这就致使咱们不只要处理心跳,并且还要处理粘包,甚至缓冲区都须要本身来处理,一旦消息没被及时拿出来,那么后到的数据会将以前的数据一古脑儿的覆盖。从底层来实现这些功能的好处是让咱们对服务端的编写有了很是清楚的认知,可是也因为思虑不全带来很是多的坑。能够说那几年是踩着TCP的坑走过来的。最后咱们基于SocketAsyncEventArgs封装了咱们本身的物联网通信框架:TinySocket。在那个时候,彼时的联想佳沃蓝莓基地依旧用数据库轮询的方式来支持物联网设备,和他们对接的时候,发现常常会由于遇到网络层面的问题而愁云满面,而彼时的咱们却由于咱们能够在任何设备上自动/手动控制咱们的设备而高兴不已。由于她的可靠度极高。数据库

后来,离开了那里,可是怀着要打造一个能支撑巨流量的物联网高并发和高可用架构的梦想,而选择了互联网公司来进行深造。也是在这个时候,我从.net平台转到了java平台,也正是在这个时候,我有缘认识了netty,一个仿佛是为了解决我当年的各类问题而生的框架,虽和她只有一面之缘,可是那一刻,我决定将她归入麾下,情定终生也许用在此刻再合适不过了。由于她有成熟的架构,普适的解决方式,优雅的接入方式,良好的社区支持,成熟的商业产品。这些特性,让咱们没法拒绝使用。设计模式

因为对netty的执迷,致使我提及了过往,止不住的文字流淌,接下来咱们就转入正题吧。安全

在数据传输过程当中,因为网络的不肯定性,每一个数据包都有可能遭遇形式各样的问题,诸如掉线,网络变差等,因此到达的时候,这些数据包有可能乱序,也有可能丢失。因此为了应对这些异常情况,TCP协议在其内部经过序列号来保证数据包乱序的问题,同时经过确认号来保证数据包丢失的问题。因此基于TCP协议实现的上层应用,都认为TCP传输是可靠的。可是经过一些网络抓包工具,能够窥见其具体实现数据包有序和防丢失的过程,感兴趣的能够本身去试试。服务器

那么上面提到序列号和确认号,到底是什么呢?咱们来看一下:网络

  •   Sequence Number: 顺序号,意即数据包的序号,主要用来解决数据包乱序问题。数据结构

  •   Acknowledgement Number:确认号,意即数据包用来进行双端消息确认的号码,主要用来解决网络传输过程当中,数据丢包的问题。架构

在TCP进行数据传输的过程当中,主机A传输数据给主机B,假设第一次A传输512字节的数据给B,那么seq=1;当B收到这512字节的时候,会将seq进行累加来避免乱序,在这里,B会将seq从新设置为512+1,而后回传给A,A收到B传回来的seq=513的时候,就知道第一个数据包已经传给了B。若是A收到B的回复,发现B没有收到数据包的话,那么将会进行重发操做,这样来防止丢包。并发

下面来讲下TCP的标志位,一共有6种:

  •      SYN(synchronous创建联机)

  •       ACK(acknowledgement 确认)

  •       PSH(push传送)

  •       FIN(finish结束)

  •       RST(reset重置)

  •       URG(urgent紧急)

第一次握手:创建链接时,客户端发送syn包(syn=j)到服务器,并进入SYN_SEND状态,等待服务器确认;     

第二次握手:服务器收到syn包,必须确认客户的SYN(ack=j+1),同时本身也发送一个SYN包(syn=k),即SYN+ACK包,此时服务器进入SYN_RECV状态;   

第三次握手:客户端收到服务器的SYN+ACK包,向服务器发送确包ACK(ack=k+1),此包发送完毕,客户端和服务器进入ESTABLISHED状态,完成三次握手。

完成三次握手,客户端与服务器开始传送数据.

更多详细的信息,推荐阅读斯坦福大学的Transmission Control Protocol (TCP)的这篇短小精悍的文章。

大略讲解了下TCP的基础,咱们接下来开始咱们的netty之旅吧。因为JDK内置的NIO操做类库并不是咱们的讲解要点,因此这里我不会过多的进行讲解,直接从netty讲起吧。

2. 网络通信基础,包含(粘包拆包,编解码,鉴权认证,心跳检测,断线重连)

在设计网络通信框架的时候,有些设计点是必须被考虑进去的,这些设计点能够说是不可或缺的。接下来咱们就一一梳理并进行讲解。

 

>>粘包拆包

粘包拆包,顾名思义,粘包,就是指数据包黏在一块了;拆包,则是指完整的数据包被拆开了。因为TCP通信过程当中,会将数据包进行合并后再发出去,因此会有这两种状况发生,可是UDP通信则不会。下面咱们以两个数据包A,B来说解具体的粘包拆包过程:

bb0a0099-2e12-4191-be8a-1d4c031552be

第一种状况,A数据包和B数据包被分别接收且都是整包状态,无粘包拆包状况发生,此种状况最佳。

f9b580f9-bd49-4dd0-b9e7-742e63f6276f

第二种状况,A数据包和B数据包在一起且一块儿被接收,此种状况,即发生了粘包现象,须要进行数据包拆分处理。

a1f0cd86-ac4d-45a7-b79f-20c8dba93fed

第三种状况,A数据包和B数据包的一部分先被接收,而后收到B数据包的剩余部分,此种状况,即发生了拆包现象,即B数据包被拆分。

a1cfac00-7f70-4183-a57e-becbb95ac9e1

第四种状况,A数据包的一部分先被接收,而后收到A数据包的剩余部分和B数据包的完整部分,此种状况,即发生了拆包现象,即A数据包被拆分。

fd5757c1-63dd-4c8b-a28d-24c9c4b88fe9

第五种状况,也是最复杂的一种,先收到A数据包的部分,而后收到A数据包剩余部分和B数据包的一部分,最后收到B数据包的剩余部分,此种状况也发生了拆包现象。

上面五种粘包拆包现象的发生,其实归根到底,缘由有三:

  (1) 应用程序write写入的字节大小大于套接口发送缓冲区大小。

  (2) 进行MSS大小的TCP分段。

  (3) 以太网帧的payload大于MTU进行IP分片。

咱们来详细讲解一下。

对于(1)中的内容,咱们能够认定为应用程序内部自身的缓冲区,此缓冲区由于大小不一样会致使连续写入的数据太长被截断,从而致使一个完整的业务消息体被分为两段发送出去。

对于(2)中的内容,实际上是TCP协议里面的MSS大小,此大小会决定发送的数据包的长度。属于协议层面的缓冲区。

对于(3)中的内容,则属于网卡自身的缓冲区大小,属于硬件层面。

既然了解了粘包拆包发生的缘由了,那么有什么办法来应对呢?因为不一样业务有不一样的实现方式,因此通常状况下都会采用以下的解决方式来进行处理:

(1)  数据消息固定长度,好比说1024字节,接收方接收到数据,以1024字节为单位进行截取便可。如若当前接收到的数据不够1024字节,能够等后续的数据到达后,以1024为单位进行截取。适用于数据结构固定长度的场合。

(2)  数据消息采用分隔符,好比用换行符或者使用竖线分隔等,依据具体的业务来进行。在进行数据处理的时候,能够根据这些分隔符来截取数据。适用于数据结构长度不固定的场合。前面提到的物联网采集端通信协议就是采用的此种作法。

(3)  数据消息包含数据头和数据体,数据头中包含数据长度,此种作法可让数据定义更为灵活多变,可是会让数据结构变得臃肿,很是适合于自定义通信协议的场合中。

(4)  其余根据具体业务而衍生出来的处理方式。好比Dubbo通信协议等。

 

>>编解码

当咱们将数据从本机发到远端的时候,咱们须要将数据转换为二进制放到缓冲区,而后发送出去,这叫作编码。当咱们接收远端数据到本机的时候,咱们须要将缓冲区的二进制数据还原为对象,这叫作解码。

因为目前可以进行这种编解码的组件很是的多,好比ProtoBuffer,ProtoStuff,Marshalling,MessagePack等,因为这些组件有性能上的差异和使用简便性方面的差异,因此须要本身经过Benchmark来选择最适合本身业务的。因为ProtoStuff是对ProtoBuffer的封装,省去了咱们手写协议文件的烦恼,且性能上的损耗在能够接收范围内,因此咱们接下来的讲解均以此组件来进行。

 

>>鉴权认证

双端的机器在进行通信的时候,必需要进行身份认证后才能进行链接,此举能够防止非法用户经过构造数据包来非法访问服务数据的做用。此鉴权认证发生在双方机器第一次进行链接通信的时候,客户端必须先发送鉴权认证的数据包给服务端,服务端对此客户端进行鉴权认证,若是鉴权认证不经过(好比客户端ip在黑名单中或者客户端的请求token无效等),则拒绝链接。

其实这种鉴权认证就相似我们访问网页时候,须要先进行用户登陆的状况同样。虽然此种作法没法百分之百的保证非法用户的访问,可是能够在极大程度上提高服务端的安全性能。

 

>>心跳检测

双端的机器在进行通信的时候,因为链路保持在活跃状态,因此不会致使链路中断。可是一旦当一方机器(好比说客户端)因为网络变差,网络闪断,机器挂掉等缘由致使掉线,那么此种状况下,服务端是感知不到客户端掉线的。因此这里须要利用心跳包来检测客户端的这种行为。心跳包的实现方式有多种,可是无外乎以下几种状况:

(1)  服务端发送心跳包给客户端,客户端接收到后计数清零,当客户端在规定的时间间隔内(好比1分钟)没有接收到服务端发送的心跳包,则计数器递增一次,累积递增三次,则视为服务端掉线。此种方式主要检测服务端存活。好比物联网采集模块中,就须要客户端实时检测服务端的存活。

(2)  客户端发送心跳给服务端,服务端接收到后计数清零,当服务端在规定的时间间隔内(好比1分钟)没有接收到客户端发送的心跳包,则计数器递增一次,累积递增三次,则视为客户端掉线。此种方式主要检测客户端存活。好比IM通信软件中,经过此方法能够检测哪一个用户掉线,而后将此掉线用户广播给其余用户告知掉线信息。

(3)  客户端发送心跳给服务端,服务端接收后计数清零,同时服务端给客户端发送一个心跳包,客户端接收后计数清零。当双端任何一方未能及时收到心跳包,则计数器进行递增,累积递增三次,则视为对方掉线。此种方式能够同时检测服务端和客户端的存活。

固然,上面是我常常用到的三种心跳包设计模式,若是有更好的设计方式,还请指教。

 

>>断线重连

客户端因为种种缘由,致使和服务端的链接中断,此种状况下,须要考虑到重连。此种机制可最大程度的保证总体服务的稳定性和可用性。因此其重要性毋庸置疑。

上面就是在设计通信组件的时候,必需要考虑的诸多细节,因为不一样的业务对这些细节的依赖度有高有低,因此在实际设计的时候,能够依据业务来进行详细定制或者粗粒度实现,由此出发,打造一套本身的通信组件,不是什么难事儿了。

上面都是一些理论点,如何将这些理论点变成实践,则是接下来要讲的内容了。Netty,终于要出场了。

 

3. 自定义协议栈。

封装一个通用的通信组件所具有的一些要点,已经讲解的比较全面和清楚了,可是只是理论知识,本着实践出真知的态度,咱们决定利用上面的知识点来打造一款本身的通信协议,这个通信协议会在基于CS模型(Client-Server)的通信组件上进行信息传输。本次咱们将采用Netty做为通信组件的底层,ProtoStuff做为编解码的工具。接下来就开始吧。

 

>>编解码

在Netty中,编码是指将数据转换为缓冲区中的二进制数据,对应的编码类是MessageToByteEncoder,此类中的write方法能够将消息对象进行编码,而后写入到发送管道中。因为在此类中,encode编码方法是abstract的,因此须要用户来本身实现,咱们就以ProtoStuff来书写一下。而解码则是指将缓冲区中的二进制数据转换为数据对象,对应的解码类是ByteToMessageDecoder,相似的,咱们须要本身实现decode的编码方法,由于它也是abstract的。

首先咱们须要封装一个SerializeUtil通用类出来,此类只包含基于ProtoStuff实现的serialize(Object object)和deserialize(byte[] data, Class<T> clazz)出来,具体封装以下:

2a17475f-30b8-41a7-89ff-ffe34030ca11

因为Netty提供了MessageToByteEncoder和ByteToMessageDecoder这两个类供咱们进行编码解码,因此咱们须要分别继承这两个类来实现咱们的编码器,解码器。

首先来看看编码器,主要是将二进制数据放入管道中。

9af8e4c6-873b-4d53-b9b4-960bfd9a6768

而后来看看解码器,主要是将二进制数据提取出来并转换为消息对象。

86715cba-2ad7-4404-840b-68a62dbb161d

注意这里咱们并不是直接继承自ByteToMessageDecoder来实现,是由于单纯的继承自这个类,须要咱们本身手动处理粘包拆包的状况,比较麻烦。因此咱们继承自LengthFieldBasedFrameDecoder这个用来处理粘包拆包的类,此类正是继承自ByteToMessageDecoder,因此大大简化了咱们的工做。粘包拆包的具体实现,后面咱们会详细讲解。

从上面的代码中,咱们就能够看到在Netty中,实现本身的编码解码器是多么的简单和方便。须要注意的是,在解码的时候,因为ByteBuf自己的readerIndex和writeIndex机制,在读取的时候须要用readBytes来使得readerIndex索引后移,不能够用getBytes来操做,不然会致使readerIndex不能向后移动,从而致使netty did not read anything but decoded a message的错误,这个错误的意思就是你当前读取的数据是空的,没法转化为消息对象,缘由是由于咱们以前已经读过此数据了,因为readerIndex未更新,致使咱们读取的是空数据。关于readerIndex和writIndex更多详细内容,能够翻阅此文,我在这里作了更加详细的讲解。

 

>>粘包拆包

在Netty中,已经提供好了粘包拆包的公共类库,他们是:LineBasedFrameDecoder,StringDecoder,LengthFieldBasedFrameDecoder,DelimiterBasedFrameDecoder,FixedLengthFrameDecoder。其中StringDecoder扩展自MessageToMessageDecoder类,其余的几个均扩展自ByteToMessageDecoder类。为何扩展自ByteToMessageDecoder类呢?由于粘包拆包发生在从缓冲区中将二进制数据读取出来的过程当中,而ByteToMessageDecoder类,是将二进制数据转换为具体的消息对象的类,因此这些类库继承自这个类也是理所固然的事情了。接下来咱们对这些粘包拆包工具进行一一讲解和实践。

LineBasedFrameDecoder:遍历ByteBuf中的可读字节,而后看是否有\n或者\r\n,若是存在,就认为当前寻找的消息体已经找寻完毕。同时此类也支持最大长度的数据匹配,当读取的数据长度已达到最大长度可是仍旧没有找到\n或者\r\n换行结束符的时候,将会抛出异常,同时忽略掉以前读取的异常码流。

StringDecoder:将接收到的内容转换为String串。

将LineBasedFrameDecoder+StringDecoder组合起来,就能够造成按行进行切分的文本解码器,使用这种组合来进行粘包拆包处理,很是可靠易用。因为此组合只支持数据消息含有结束换行符的,因此只适合简单的纯文本场合。

LengthFieldBasedFrameDecoder:此解码器主要是经过消息头部附带的消息体的长度来进行粘包拆包操做的。因为其配置参数过多(maxFrameLength,lengthFieldOffset,lengthFieldLength,lengthAdjustment,initialBytesToStrip等),因此能够最大程度的保证能用消息体长度字段来进行消息的解码操做。这些不一样的配置参数能够组合出不一样的粘包拆包处理效果。

DelimiterBasedFrameDecoder:此解码器主要经过设定分隔符来进行消息的粘包拆包处理。

FixedLengthFrameDecoder:此解码器主要是经过设置固定数据长度来进行消息的粘包拆包处理。

 

>>鉴权认证

此包为Client链接Server的时候,须要发送的第一个数据包,Server端接收到此包的内容后,经过业务解析,来对当前请求登陆的Client进行鉴权操做。若是操做成功,则容许登陆,不然拒绝登陆。因为业务解析这块不属于咱们重点讲解的内容,在示例代码中,咱们以简单的鉴权操做来进行延时讲解:

首先,Client端链接到Server端,当链路Active的时候,Client端开始发送鉴权申请。

787e3535-118a-4f2f-827c-d6599098defc

而后,Server端接收到Client的鉴权申请,进行鉴权操做:

0d0cba8e-0807-4692-bb8e-4a51ff8c8cae

当Server端鉴权成功以后,会将鉴权成功的信息发送给Client端,Client端接收到鉴权成功的信息后,打印出鉴权成功信息:

874de5e0-c908-4655-9bce-0e1ce1fc26ac

这样,一个鉴权认证的基本流程就出来了,从Client端到Server端,而后再到Client端。因为鉴权的具体方式和业务关联性比较高,因此能够利用具体鉴权业务进行替换便可。

 

>>心跳检测

当鉴权经过以后,Client端和Server端的正常通信创建。能够进行业务消息的交流。可是因为网络缘由等会形成Client和Server的交流中断,并且此种中断是没法被感知的,因此Client端的心跳检测设计以下:

c508e8a3-b1f4-4159-afc0-e4b2bb66db9f

从代码能够看出,咱们的HeartBeatTask会以固定5秒的频率向Server端发送一次心跳信息,若是收到Server端的心跳回复,则打印出来。

而后来看看Server端的心跳检测代码:

7b8677eb-1a45-41c2-8b03-e4876544782d

从代码能够看出,Server端收到Client端的心跳包后,会打印出来,而后构建另外一个心跳包回复给Client端,也就是向Client端报告我还活着。

这样,经过一来一去的心跳包检测机制,就能够对Server端和Client端进行探活操做,避免业务上的不可用问题。

 

>>断线重连

为了提升高可用性,能够对Client端加上此项特性保证服务的可用率。Client端示例代码以下:

bedd335b-4eda-424c-99aa-48e8b3533783

因为Client关闭后,会跑到finally代码块中,因此在这里能够进行重连操做。

 

>>服务端编写

首先来看看Netty建立服务端的时序图:

image

从图示能够看出,ServerBootstrap实例是出发点;而后绑定EventLoopGroup线程池;以后设置并绑定服务端Channel,绑定各类Handler;最后就绑定到本机进行监听。此时Selector会一直进行轮询操做,一旦发现注册的Channel处于Ready状态,则执行Handler链调用。

因为以上全部的组件都准备齐全,因此咱们这里能够很方便的进行服务端编码了:

d4f94d7c-aa4a-45c7-b539-715fde2007c7

从代码中咱们能够看到,以前讲过的鉴权认证,编码解码,粘包拆包等都体如今了服务端Handler中,因此很是的简介明了。

 

>>客户端编写

首先来看看Netty建立客户端的时序图:

image

从图示能够看出,BootStrap是出发点;而后设置EventLoopGroup线程池;以后设置并绑定客户端Channel和各类Handler;最后经过Connect方法进行服务端链接操做。其实和服务端差异不大。因为其设计也涉及到鉴权认证,编码解码,粘包拆包等,因此编码是有些相似的:

e6e4e4ff-1e86-41b2-bb32-6b1b073a333e

好了,到了这里,咱们就已经可以打造出来一个通用的通信框架了,此框架虽然简单,可是胜在囊括了各类必须的设计元素。能够做为指导框架进行业务逻辑的耦合设计,避免出现设计过程当中由于缺少指导思想致使设计出来的东西不符合业务需求,好比高可用需求。

 

上面就是Netty初级应用,咱们介绍了在设计一个简单通信框架过程当中所涉及到的比较重要的特性,接下来的篇章,咱们将会讲解如何设计分布式服务框架等一些中级内容,但愿您可以继续驻足品尝。

相关文章
相关标签/搜索