说明
本文记录了在使用netty作为TCP服务端的情况下,如何解决TCP粘包、半包问题,以及如何使用16进制与客户端进行通信。
依赖
首先添加maven依赖:
1 | <dependency> |
代码
创建字节转换类
首先创建一个将字节转换为16进制的处理类,并判断包头包尾是否正确。
1 | public class CustomDecode extends ByteToMessageDecoder { |
以上为数据处理类,首先判断包头是否为约定的16进制数据,其次判断包尾是否为约定的字节,如不是则return继续接收数据直到接收到了包尾。
工具类 StringUtil.byteToHexString
的作用是将字节转换为16进制字符串,此工具类为netty自带,**(src[0] & 0x000000ff)
表示将下标为0的字节转换为16进制**
创建字节处理初始化类
1 | public class ServerInitializer extends ChannelInitializer<SocketChannel> { |
初始化字节处理类,并使用netty自带的粘包处理器解决粘包问题,addLast(new DelimiterBasedFrameDecoder(1024,false,Unpooled.copiedBuffer(TCPServerUtils.hexStr2bytes("EFGG"))))
表示以EFGG结尾的数据分割包
简单的说明一下半包和粘包,半包: 假设数据包以16进制 AABB开头,以EFGG结尾,中间放具体数据,但是TCP传输可能只传了AABB,EFGG可能是在第二次传输过来,因此要等一个完整的数据包传输过来后再做处理。
粘包: 假设数据包内容为”AABBHHEFGG“是一个完整的包,但是TCP在传输过程中可能是这样传的:AABBHHEFGGAABBH
,本来应该是两个包的内容给放在了一起,而且还不完整,这就是粘包。
创建业务处理类
有了上面的字节处理类,在这一层拿到的就是分割好的完整数据包了,此时可以对字节做截取,去掉包头包尾读取中间内容,代码如下:
1 | public class TCPServerHandler extends ChannelInboundHandlerAdapter{ |
注意点:一定要新建一个Map保存认证设备的唯一识别码,netty中的channel即表示一个会话,因此在请求认证时就要将识别码与channel进行绑定,这样才可以随时向客户端发送消息。
checksum工具类代码如下:
1 | public static String makeChecksum(String data) { |
创建netty TCP服务启动类
由于我是使用的spring boot框架因此直接在初始化事件中创建netty服务,也可以直接在main方法中启动,代码如下 :
1 | public class ServerStarter implements ApplicationListener<ContextRefreshedEvent>{ |
OK,现在TCP服务的架子已经搭好,只需往里面填充业务代码即可。
常用的工具类
附上可能会用到的工具类,比如16进制转10进制,16进制转2进制等
十六进制转byte[]数组
1 | public static byte[] hexStr2bytes(String hexStr) { |
大小端处理 ,低位字节排放在内存的低地址端,高位字节排放在内存的高地址端。
1 | public static int toLittleEndian(int a) { |
10进制数字转16进制数字
1 | public static String numberToHexString(Integer number) { |
checksum,上面已经贴过再贴一次
1 | public static String makeChecksum(String data) { |
截取字节 (这个可能是最常用的了,后来我才发现java自带的bytebuffer已经自带大小端转换功能)
1 | public static String catoutByte(byte[] sourceBytes,int startPos,int len) { |
使用Redis作为设备指令发送队列
更新日期:2021-7-7 ,新增使用redis作简单队列,发送指令到设备。
此方式需要可容忍指令丢失,因为有可以在取出指令后服务器挂掉或一直未获取到锁导致指令超时,可靠消息列队应当使用队列中间件
首先在你的发送指令类的构造函数中创建一个线程池,在指令并发较大时可以有效节省资源
1 | ExecutorService pool; |
1 |
|
可调用public boolean pushCommand(String deviceId,String responseData)
方法将指令放入队列,等待获取指令列队的线程执行。
指令实体类:
1 | public class Command { |
结尾
最后一个注意点,一定要做重试机制,TCP传输过程中很可能会丢包,我目前的做法是在发送消息成功后保存一个临时的缓存,当接收到客户端回复后将缓存移除,如若一直未接收到回复(比如1秒后)则再次重新发送。
- 本文作者: reiner
- 本文链接: https://reiner.host/posts/94ca3821.html
- 版权声明: 转载请注明出处,并附上原文链接