Netty 对通讯协议结构设计的启发和总结

Netty 通讯协议结构设计的总结

key words:通信,协议,结构设计,netty,解码器,LengthFieldBasedFrameDecoder

原创

包含与机器/设备的通讯协议结构的设计,安全性,数据有效性的设计思路记录

通讯协议结构选择

按照解决TCP粘包的解决方案的协议设计思路,大部分情况也就是:

  1. 定长消息,每个报文固定长度,不够补0或其他
  2. 用特殊字符/字节做分割符,遇到分隔符拆包
  3. 不定长报文,包头带长度,以长度字节为准进行消息分割

每种处理方式都有不同的适用场景(例如 方法2适合文本传输过程中的拆包,却不适合byte[]数据的拆包),方法1,2,3在netty里面得到了很好的支持,具体可以见详见netty 在TCP粘包问题处理 这篇文章

对于物联通讯来说,传输是最佳数据类型,所以方法3是比较合适的,这就要求通讯协议在设计时,需要把报文长度放在最前面,下面看看netty自带的基于包头不定长的解码器,能省去自己解决粘包的时间,把关注点放到业务数据的处理上

基于包头不固定长度的解码器:LengthFieldBasedFrameDecoder

LengthFieldBasedFrameDecoder参数说明

  • maxFrameLength:解码的帧的最大长度
  • lengthFieldOffset:长度属性的起始位(偏移位),包中存放有整个大数据包长度的字节,这段字节的起始位置
  • lengthFieldLength:长度属性的长度,即存放整个大数据包长度的字节所占的长度
  • lengthAdjustmen:长度调节值,在总长被定义为包含包头长度时,修正信息长度。
  • initialBytesToStrip:跳过的字节数,根据需要我们跳过lengthFieldLength个字节,以便接收端直接接受到不含“长度属性”的内容
  • failFast :为true,当frame长度超过maxFrameLength时立即报TooLongFrameException异常,为false,读取完整个帧再报异常

有了这个解码器,就很轻松完成拆包工作,拆出来的业务数据,再交由下一个decoder handler处理

那么封包呢?封包也不用自己加长度,直接在ChannelPipeline的最后加上LengthFieldPrepender 编码器

LengthFieldPrepender 编码器

参数说明:

  • lengthFieldLength:长度属性的字节长度
  • lengthIncludesLengthFieldLength:false,长度字节不算在总长度中,true,算到总长度中

配合使用LengthFieldPrepender,很容易就完成了,这样在flush前,netty自动会为报文加上一个length。

需要注意的是,在业务处理器里面要响应write时,请用pipeline.write,如果直接用ctx.write,最后报文就不会加长度,因为不会进入到LengthFieldPrepender编码器中去

示例代码:
@Component("MyChannelInit")
public class MyChannelInitializer extends ChannelInitializer<SocketChannel> {

    @Override
    protected void initChannel(SocketChannel channel) throws Exception {
        ChannelPipeline pipeline = channel.pipeline();
        //pipeline.addLast(new LoggingHandler(LogLevel.INFO));
        pipeline.addLast("frameDecode",new LengthFieldBasedFrameDecoder(1024,0,2,-2,2));
        pipeline.addLast("decoder", new MyDataDecoder());//in 1
        pipeline.addLast("handler", new MyInboundHandler());//in 2

        pipeline.addLast(new IdleStateHandler(40, 0, 0));//out3
        //pipeline.addLast("encoder", new DataEncoder());//out2
        pipeline.addLast("frameEncode",new LengthFieldPrepender(2,true));//out1
    }
}

收到设备发过来的数据,new LengthFieldBasedFrameDecoder(1024,0,2,-2,2):解码最大长度1024,起始偏移0,长度参数占字节数2,总长包含长度字节数,修正长度-2,传输到下一个Decoder时数据,跳过字节数2(也就是不带长度)

发送到设备的数据,new LengthFieldPrepender(2,true),自动加上两个字节的长度


 LengthFieldBasedFrameDecoder

常用的处理大数据分包传输问题的解决类,先对构造方法LengthFieldBasedFrameDecoder中的参数做以下解释说明“

maxFrameLength:解码的帧的最大长度

lengthFieldOffset :长度属性的起始位(偏移位),包中存放有整个大数据包长度的字节,这段字节的其实位置

lengthFieldLength:长度属性的长度,即存放整个大数据包长度的字节所占的长度

lengthAdjustmen:长度调节值,在总长被定义为包含包头长度时,修正信息长度。initialBytesToStrip:跳过的字节数,根据需要我们跳过lengthFieldLength个字节,以便接收端直接接受到不含长度属性的内容

failFast :为true,当frame长度超过maxFrameLength时立即报TooLongFrameException异常,为false,读取完整个帧再报异常

 

下面对各种情况分别描述:

 

1. 2 bytes length field at offset 0, do not strip header

 

lengthFieldOffset   = 0

 lengthFieldLength   = 2

 lengthAdjustment    = 0

 initialBytesToStrip = 0 (= do not strip header)

 

 

 

 BEFORE DECODE (14 bytes)         AFTER DECODE (14 bytes)

+--------+----------------+       +--------+----------------+

 | Length | Actual Content |---->| Length | Actual Content |

 | 0x000C | "HELLO, WORLD" |      | 0x000C | "HELLO, WORLD" |

 +--------+----------------+      +--------+----------------+

 

此时数据格式不做任何改变(没有跳过任何字节)

 

2. 2 bytes length field at offset 0, strip header

 

lengthFieldOffset   = 0

 lengthFieldLength   = 2

 lengthAdjustment    = 0

 initialBytesToStrip = 2 (= the length of the Length field)

 

 BEFORE DECODE (14 bytes)         AFTER DECODE (12 bytes)
 +--------+----------------+      +----------------+
 | Length | Actual Content |---->| Actual Content |
 | 0x000C | "HELLO, WORLD" |      | "HELLO, WORLD" |
 +--------+----------------+      +----------------+

 

此时帧长度为14个字节,但由于lengthFieldOffset = 0两个lengthFieldLength = 2)字节是表示帧长度的字节,不计入数据,故真实的数据长度为12个字节。

 

3. 2 bytes length field at offset 0, do not strip header, the length field represents the length of the whole message

 

lengthFieldOffset   =  0

 lengthFieldLength   =  2

 lengthAdjustment    = -2 (= the length of the Length field)

 initialBytesToStrip =  0

 

 BEFORE DECODE (14 bytes)         AFTER DECODE (14 bytes)
 +--------+----------------+     +--------+----------------+
 | Length | Actual Content |---->| Length | Actual Content |
 | 0x000E | "HELLO, WORLD" |      | 0x000E | "HELLO, WORLD" |
 +--------+----------------+     +--------+----------------+
 

此处定义的Length0x000E共占了两个字节,表示的帧长度为14个字节,lengthFieldOffset = 0两个lengthFieldLength = 2)字节为Length,由于设置的lengthAdjustment    = -2 (= the length of the Length field),故修正的信息实际长度补2,即解码时往前推2个字节,解码后还是14个字节长度(此种情况是把整个长度封装,一般来讲,我们只封装数据长度)

 

4. 3 bytes length field at the end of 5 bytes header, do not strip header

lengthFieldOffset   = 2 (= the length of Header 1)

 lengthFieldLength   = 3

 lengthAdjustment    = 0

 initialBytesToStrip = 0

 

BEFORE DECODE (17 bytes)                 AFTER DECODE (17 bytes)
 +---------+---------+--------------+    +---------+---------+------------+
 | Header 1| Length  |Actual Content|--->| Header 1| Length | Actual Content|
 |  0xCAFE | 0x00000C|"HELLO, WORLD"|    |  0xCAFE   |0x00000C| "HELLO, WORLD"|
 +---------+---------+--------------+    +----------+--------+-----------+

 

此处lengthFieldOffset   = 2,从第3个字节开始表示数据长度,长度占3个字节,真实数据长度为0x00000C 12个字节,而lengthAdjustment=0initialBytesToStrip = 0,故解码后的数据与解码前的数据相同。

4. 3 bytes length field at the beginning of 5 bytes header, do not strip header

lengthFieldOffset   = 0

 lengthFieldLength   = 3

 lengthAdjustment    = 2 (= the length of Header 1)

 initialBytesToStrip = 0

 

BEFORE DECODE (17 bytes)                          AFTER DECODE (17 bytes)
 +----------+----------+----------------+      +----------+----------+----------------+
 |  Length  | Header 1 | Actual Content |----->|  Length  | Header 1 | Actual Content |
 | 0x00000C |  0xCAFE  | "HELLO, WORLD" |      | 0x00000C |  0xCAFE  | "HELLO, WORLD" |
 +----------+----------+----------------+      +----------+----------+----------------+

此处由于修正的字节数是2,且initialBytesToStrip = 0,故整个数据的解码数据保持不变

总字节数是17,开始的三个字节表示字节长度:12,修正的字节是2,(即从第三个字节开始,再加两个开始是真正的数据,其中跳过的字节数是0

 

5. 2 bytes length field at offset 1 in the middle of 4 bytes header, strip the first header field and the length field

 

 

lengthFieldOffset   = 1 (= the length of HDR1)

 lengthFieldLength   = 2

 lengthAdjustment    = 1 (= the length of HDR2)

 initialBytesToStrip = 3 (= the length of HDR1 + LEN)

 

BEFORE DECODE (16 bytes)                       AFTER DECODE (13 bytes)

 +------+--------+------+----------------+      +------+----------------+

 | HDR1 | Length | HDR2 | Actual Content |----->| HDR2 | Actual Content |

 | 0xCA | 0x000C | 0xFE | "HELLO, WORLD" |      | 0xFE | "HELLO, WORLD" |

 +------+--------+------+----------------+      +------+----------------+

 

从第2个字节开始解码,取两个字节作为帧长度,为12个字节,然后,修正一个字节,从第5个字节到最后表示帧数据,解码时,由于initialBytesToStrip=3,表示跳过前三个字节(去掉),故从第四个字节开始解析,解析出来后,如右图所示。

 

6. 2 bytes length field at offset 1 in the middle of 4 bytes header, strip the first header field and the length field, the length field represents the length of the whole message

 

lengthFieldOffset   =  1

 lengthFieldLength   =  2

 lengthAdjustment    = -3 (= the length of HDR1 + LEN, negative)

 initialBytesToStrip =  3

 

BEFORE DECODE (16 bytes)                       AFTER DECODE (13 bytes)

 +------+--------+------+----------------+      +------+----------------+

 | HDR1 | Length | HDR2 | Actual Content |----->| HDR2 | Actual Content |

 | 0xCA | 0x0010 | 0xFE | "HELLO, WORLD" |      | 0xFE | "HELLO, WORLD" |

 +------+--------+------+----------------+      +------+----------------+

 

从第二个字节开始,取两个字节作为帧长度,为16个字节,然后补3个字节,故往前找三个字节,从HDP1开始解码,而又因为initialBytesToStrip=3,解码时忽略掉前三个字节,故从第四个字节开始解析,解析结果如右图所示。

 

总结:一般来讲,当lengthAdjustment 为负数时,Length表示的是整个帧的长度,当lengthAdjustment为正数或0时,表示真实数据长度。

(6)  LengthFieldPrepender

编码类,自动将

+----------------+

| "HELLO, WORLD" |

+----------------+

格式的数据转换成

+--------+----------------+

 + 0x000C | "HELLO, WORLD" |

 +--------+----------------+

格式的数据,

如果lengthIncludesLengthFieldLength设置为true,则编码为

+--------+----------------+

+ 0x000E | "HELLO, WORLD" |

+--------+----------------+

格式的数据

 

应用场景:自定义pipelineFactoryMyPipelineFactory implements ChannelPipelineFactory

pipeline.addLast("frameEncode", new LengthFieldPrepender(4, false));

(7)  TooLongFrameException

定义的数据包超过预定义大小异常类

(8)  CorruptedFrameException

定义的数据包损坏异常类

3.            frame包应用demo

解决分包问题,通常配置MyPipelineFactory中设置,示例如下:

 

public class MyPipelineFactory implements ChannelPipelineFactory {

 

    @Override

    public ChannelPipeline getPipeline() throws Exception {

       ChannelPipeline pipeline = Channels.pipeline();

       pipeline.addLast("decoder", new LengthFieldBasedFrameDecoder(Integer.MAX_VALUE, 0, 4, 0, 4)); 

        pipeline.addLast("encoder", new LengthFieldPrepender(4, false));

       pipeline.addLast("handler", new MyHandler());

       return pipeline;

    }

 

}

 

 

在客户端设置pipeline.addLast("encoder", new LengthFieldPrepender(4, false));

       pipeline.addLast("handler", new MyHandler());

 

前四个字节表示真实的发送的数据长度Length,编码时会自动加上;

 

在服务器端设置pipeline.addLast("decoder", new LengthFieldBasedFrameDecoder(Integer.MAX_VALUE, 0, 4, 0, 4));

真实数据最大字节数为Integer.MAX_VALUE,解码时自动去掉前面四个字节






通讯数据的保密性

如果不想让人拦截有效数据和入侵破坏,通讯数据最好还是带上加密,最好还是动态的,不要说什么MD5 RSA DES之类的,终端硬件那边处理器性能没那么强悍,服务端这边也影响性能

所以,哪怕就是个简单的加解密,也足够让90%的人知难而退(大部分人没事突突你干嘛(-__-)b)

我们当时设计了token机制,token有时效性,每隔一段时间就需要从服务端获取新的token值,而数据解密的参数就跟token有关,这样就算拿着数据去分析规律,由于token值时不时变换,导致解密方法和解密参数都不一样,这样也许能起到部分作用

因为token,一段时间就会失效,所以我们就有一条专门获取token的指令,为了保证协议的一致性(凡是数据传递都需要token),所以订了一个特殊的token和特殊的加解密,这样可以保证获取token获取能够通过程序的辨识

所以数据结构的头 为: LENGTH + TOKEN

通讯数据的有效性验证

对于高要求的的数据传输,是否有必要进行校验,CRC16 CRC32校验应该就够了数据结构的头 :LENGTH + TOKEN + CRC + DATA

后面的就是具体传输的数据的处理


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值