Netty自定义编-解码器解决TCP通讯粘包拆包的问题
1. TCP 粘包和拆包基本介绍
TCP
是面向连接的,面向流的,提供高可靠性服务。收发两端(客户端和服务器端)都要有一一成对的socket
,因此,发送端为了将多个发给接收端的包,更有效的发给对方,使用了优化方法(Nagle
算法),将多次间隔较小且数据量小的数据,合并成一个大的数据块,然后进行封包。这样做虽然提高了效率,但是接收端就难于分辨出完整的数据包了,因为面向流的通信是无消息保护边界的- 由于
TCP
无消息保护边界,需要在接收端处理消息边界问题,也就是我们所说的粘包、拆包问题,看一张图 - 示意图
TCP
粘包、拆包图解
对图的说明: 假设客户端分别发送了两个数据包 D1
和 D2
给服务端,由于服务端一次读取到字节数是不确定的,故可能存在以下四种情况:
- 服务端分两次读取到了两个独立的数据包,分别是
D1
和D2
,没有粘包和拆包 - 服务端一次接受到了两个数据包,
D1
和D2
粘合在一起,称之为TCP
粘包 - 服务端分两次读取到了数据包,第一次读取到了完整的
D1
包和D2
包的部分内容,第二次读取到了D2
包的剩余内容,这称之为TCP
拆包 - 服务端分两次读取到了数据包,第一次读取到了
D1
包的部分内容D1_1
,第二次读取到了D1
包的剩余部分内容D1_2
和完整的D2
包。
2. TCP 粘包和拆包解决方案
- 使用自定义协议+编解码器来解决
- 关键就是要解决服务器端每次读取数据长度的问题,这个问题解决,就不会出现服务器多读或少读数据的问题,从而避免的
TCP
粘包、拆包。
3. 看一个具体的实例
这是一个真实的案例,使我们公司开发的协议,我们在和充电桩进行通讯的时候,协议报文格式长这样:
报文说明:
报文里面有起始域域和长度域,我们可以先判断前两个字节是不是AAF5,再取3.4字节获取包长度,最后按照包长度取定长的数据
解决问题之前首先说明一个问题:
首先,我自定义了一个非常简单的解码器,其实并不具备解码功能,目的就是为了证实我的一个猜测:
public class DecodeHandler extends ByteToMessageDecoder { @Override protected void decode(ChannelHandlerContext channelHandlerContext, ByteBuf byteBuf, List<Object> list) throws Exception { System.out.println(byteBuf.writerIndex()); } }
在解码器中,输出的是写索引的位置。
然后,我开始尝试触发解码器,发现当我不断向byteBuf中写入内容后,写索引也不断增长,我写入两字节,写索引就增大2,写入三字节,写索引就增加3。由此,我猜测每次接收数据准备进行解码的bytebuf都是同一个!而不是新建的bytebuf,所以我们就可以使用这个bytebuf来实现粘包分包问题!
确定了读索引的位置就比较好办了,接下来的解析方式就看数据的具体格式了,在解析之前有必要检测一下数据长度是否完整,如果不完整,可以选择跳过这一波解析,等待数据接收完整再解析(记得要将读索引恢复到正确的位置)
大部分协议中,数据是有开头标识和长度域的,比如此协议。那么可以先找到数据的起始值和长度域在哪里。
具体我们看代码,本部分代码在解码器里实现:
/** * 解码器 */ public class DecodeUtil extends ByteToMessageDecoder { @Override protected void decode(ChannelHandlerContext channelHandlerContext, ByteBuf byteBuf, List<Object> list) { try { //byteBuf的长度 int bufNum = byteBuf.readableBytes(); //byteBuf当前的读索引 int readerIndex = byteBuf.readerIndex(); byte[] bytes = new byte[2]; if (bufNum >= 4) { //byteBuf的长度大于4, //查看前两个字节判断消息头 for (int index = 0; index < 2; index++) { bytes[index] = byteBuf.getByte(readerIndex); readerIndex++; } //将前2个字节转换为16进制 String header = ConvertCode.receiveHexToString(bytes); int length = 0; if (header.toUpperCase().equals("AAF5")) { //获取包长度 bytes = new byte[2]; bytes[0] = byteBuf.getByte(2); bytes[1] = byteBuf.getByte(3); length = ConvertCode.getShort(bytes, 0); } else { return; } if (bufNum >= length) { bytes = new byte[length]; byteBuf.readBytes(bytes); list.add(bytes); } } } catch (Exception e) { e.printStackTrace(); } } }
工具类
public class ConvertCode { /** * @Title:bytes2HexString * @Description:字节数组转16进制字符串 * @param b * 字节数组 * @return 16进制字符串 * @throws */ public static String bytes2HexString(byte[] b) { StringBuffer result = new StringBuffer(); String hex; for (int i = 0; i < b.length; i++) { hex = Integer.toHexString(b[i] & 0xFF); if (hex.length() == 1) { hex = '0' + hex; } result.append(hex.toUpperCase()); } return result.toString(); } /** * @Title:hexString2Bytes * @Description:16进制字符串转字节数组 * @param src 16进制字符串 * @return 字节数组 */ public static byte[] hexString2Bytes(String src) { int l = src.length() / 2; byte[] ret = new byte[l]; for (int i = 0; i < l; i++) { ret[i] = (byte) Integer.valueOf(src.substring(i * 2, i * 2 + 2), 16).byteValue(); } return ret; } /** * @Title:string2HexString * @Description:字符串转16进制字符串 * @param strPart 字符串 * @return 16进制字符串 */ public static String string2HexString(String strPart) { StringBuffer hexString = new StringBuffer(); for (int i = 0; i < strPart.length(); i++) { int ch = (int) strPart.charAt(i); String strHex = Integer.toHexString(ch); hexString.append(strHex); } return hexString.toString(); } /** * @Title:hexString2String * @Description:16进制字符串转字符串 * @param src * 16进制字符串 * @return 字节数组 * @throws */ public static String hexString2String(String src) { String temp = ""; for (int i = 0; i < src.length() / 2; i++) { //System.out.println(Integer.valueOf(src.substring(i * 2, i * 2 + 2),16).byteValue()); temp = temp+ (char)Integer.valueOf(src.substring(i * 2, i * 2 + 2),16).byteValue(); } return temp; } /** * @Title:char2Byte * @Description:字符转成字节数据char-->integer-->byte * @param src * @return * @throws */ public static Byte char2Byte(Character src) { return Integer.valueOf((int)src).byteValue(); } /** * @Title:intToHexString * @Description:10进制数字转成16进制 * @param a 转化数据 * @param len 占用字节数 * @return * @throws */ public static String intToHexString(int a,int len){ len<<=1; String hexString = Integer.toHexString(a); int b = len -hexString.length(); if(b>0){ for(int i=0;i<b;i++) { hexString = "0" + hexString; } } return hexString; } /** * 将16进制的2个字符串进行异或运算 * http://blog.csdn.net/acrambler/article/details/45743157 * @param strHex_X * @param strHex_Y * 注意:此方法是针对一个十六进制字符串一字节之间的异或运算,如对十五字节的十六进制字符串异或运算:1312f70f900168d900007df57b4884 先进行拆分:13 12 f7 0f 90 01 68 d9 00 00 7d f5 7b 48 84 13 xor 12-->1 1 xor f7-->f6 f6 xor 0f-->f9 .... 62 xor 84-->e6 即,得到的一字节校验码为:e6 * @return */ public static String xor(String strHex_X,String strHex_Y){ //将x、y转成二进制形式 String anotherBinary=Integer.toBinaryString(Integer.valueOf(strHex_X,16)); String thisBinary=Integer.toBinaryString(Integer.valueOf(strHex_Y,16)); String result = ""; //判断是否为8位二进制,否则左补零 if(anotherBinary.length() != 8){ for (int i = anotherBinary.length(); i <8; i++) { anotherBinary = "0"+anotherBinary; } } if(thisBinary.length() != 8){ for (int i = thisBinary.length(); i <8; i++) { thisBinary = "0"+thisBinary; } } //异或运算 for(int i=0;i<anotherBinary.length();i++){ //如果相同位置数相同,则补0,否则补1 if(thisBinary.charAt(i)==anotherBinary.charAt(i)) result+="0"; else{ result+="1"; } } return Integer.toHexString(Integer.parseInt(result, 2)); } /** * Convert byte[] to hex string.这里我们可以将byte转换成int * @param src byte[] data * @return hex string */ public static String bytes2Str(byte[] src){ StringBuilder stringBuilder = new StringBuilder(""); if (src == null || src.length <= 0) { return null; } for (int i = 0; i < src.length; i++) { int v = src[i] & 0xFF; String hv = Integer.toHexString(v); if (hv.length() < 2) { stringBuilder.append(0); } stringBuilder.append(hv); } return stringBuilder.toString(); } /** * @return 接收字节数据并转为16进制字符串 */ public static String receiveHexToString(byte[] by) { try { /*io.netty.buffer.WrappedByteBuf buf = (WrappedByteBuf)msg; ByteBufInputStream is = new ByteBufInputStream(buf); byte[] by = input2byte(is);*/ String str = bytes2Str(by); str = str.toUpperCase(); return str; } catch (Exception ex) { ex.printStackTrace(); System.out.println("接收字节数据并转为16进制字符串异常"); } return null; } /** * "7dd",4,'0'==>"07dd" * @param input 需要补位的字符串 * @param size 补位后的最终长度 * @param symbol 按symol补充 如'0' * @return * N_TimeCheck中用到了 */ public static String fill(String input, int size, char symbol) { while (input.length() < size) { input = symbol + input; } return input; } // public static void main(String args[]) { // String productNo = "3030303032383838"; // System.out.println(hexString2String(productNo)); // productNo = "04050103000001070302050304"; // System.out.println(hexString2String(productNo)); // } /** * 获取short,小端 * * @param src * @param index * @return */ public static short getShort(byte[] src, int index) { return (short) (((src[index + 1] << 8) | src[index] & 0xff)); } }
我们用真实的报文来验证处理的结果:
完整的报文:AAF56E0010026A0000000000363130313133303032373030303031000000000000000000000000000000000000A851000001006400000001010000010A02ED0B000020210414180000FF00000000000000000000000000000000000000000000000000000035C901000000000024
1.首先模拟完整报文发送100次
服务端接收解码后的结果:没有粘包
2.模拟1包完整报文+半包报文,再发后半包报文
至此,完美解决粘包问题,其他情况各位可以自己模拟!