IT数码 购物 网址 头条 软件 日历 阅读 图书馆
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
图片批量下载器
↓批量下载图片,美女图库↓
图片自动播放器
↓图片自动播放器↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁
 
   -> 网络协议 -> Socket通信之TCP拆包和封包 -> 正文阅读

[网络协议]Socket通信之TCP拆包和封包

前言

我们在使用Socket传递信息时候,经常会遇到这样的问题,输入流数据读取不完整,或者输入流读取数据错乱的问题。下面就针对这俩个问题谈谈我的想法,以及解决思路。

举例说明

最近我在做一个工业物联网的项目,其中就用到了TCP,只要是通过TCP远程操控和实时获取工业设备信息(比如控制电灯,空调,智慧屏…),由于设备众多,为了保证服务端实时数据的完整性,我在数据链路层采用了异步传输,半双工传输。服务端读取数据后,通过CRC校验的方式来确定数据的完整性。但是后发现还是不能百分百保证数据的完整性。

public class SocketOkioManager {

	
	private List<Socket> mList = new ArrayList<Socket>();
	private ServerSocket server = null;
	private ExecutorService mExecutorService = null;
	private final String TAG = SocketOkioManager.class.getName();


	public SocketOkioManager(int PORT) {
		try {
			server = new ServerSocket(PORT);
			mExecutorService = Executors.newCachedThreadPool();
			Log.i(TAG,"服务器已启动...");
			Socket client = null;
			while (true) {
				client = server.accept();
				mList.add(client);
				mExecutorService.execute(new Service(client));
			}
		} catch (Exception e) {
			e.printStackTrace();
		}
	}

	class Service implements Runnable {
		private Socket socket;
		private BufferedSink mSink;
		private BufferedSource mSource;

		public Service(Socket socket) {
			this.socket = socket;
			try {
				mSink = Okio.buffer(Okio.sink(socket));
				mSource = Okio.buffer(Okio.source(socket));
				mSink.flush();
				Log.i(TAG,"成功连接服务器");
			} catch (IOException e) {
				e.printStackTrace();
			}
		}

		@Override
		public void run() {
			try {
				byte[] bytes = new byte[1024];
				while (true) {
					if (mSource.read(bytes)!=-1){
						Log.i(TAG,"服务器收到数据is:"+HexUtils.encode(bytes));
						//CRC-16/XMODEM x16+x12+x5+1
						char content_char = HexUtils.getCRC1021(bytes, bytes.length);
						//生成校验码
						String content_code = Integer.toHexString(content_char).toLowerCase();
						String cecekCode=HexUtils.encode(bytes);
						cecekCode=cecekCode.substring(cecekCode.length()-4,cecekCode.length()).toLowerCase();
						if (content_code.equals(cecekCode)){
							Log.i(TAG,"数据检验完整:");
						}
					}
				}

			} catch (Exception e) {
				Log.i(TAG,"Exception:"+e.toString());
				e.printStackTrace();
			}
		}
	}
}

下面是CRC校验的代码(注意:我这里校验采用的是CRC-16/XMODEM x16+x12+x5+1,不同的CRC校验方式结果是不同的

/**
 * CRC1021余式表
 */
static final char TABLE1021[] = {
                0x0000, 0x1021, 0x2042, 0x3063, 0x4084, 0x50a5, 0x60c6, 0x70e7,
                0x8108, 0x9129, 0xa14a, 0xb16b, 0xc18c, 0xd1ad, 0xe1ce, 0xf1ef,
                0x1231, 0x0210, 0x3273, 0x2252, 0x52b5, 0x4294, 0x72f7, 0x62d6,
                0x9339, 0x8318, 0xb37b, 0xa35a, 0xd3bd, 0xc39c, 0xf3ff, 0xe3de,
                0x2462, 0x3443, 0x0420, 0x1401, 0x64e6, 0x74c7, 0x44a4, 0x5485,
                0xa56a, 0xb54b, 0x8528, 0x9509, 0xe5ee, 0xf5cf, 0xc5ac, 0xd58d,
                0x3653, 0x2672, 0x1611, 0x0630, 0x76d7, 0x66f6, 0x5695, 0x46b4,
                0xb75b, 0xa77a, 0x9719, 0x8738, 0xf7df, 0xe7fe, 0xd79d, 0xc7bc,
                0x48c4, 0x58e5, 0x6886, 0x78a7, 0x0840, 0x1861, 0x2802, 0x3823,
                0xc9cc, 0xd9ed, 0xe98e, 0xf9af, 0x8948, 0x9969, 0xa90a, 0xb92b,
                0x5af5, 0x4ad4, 0x7ab7, 0x6a96, 0x1a71, 0x0a50, 0x3a33, 0x2a12,
                0xdbfd, 0xcbdc, 0xfbbf, 0xeb9e, 0x9b79, 0x8b58, 0xbb3b, 0xab1a,
                0x6ca6, 0x7c87, 0x4ce4, 0x5cc5, 0x2c22, 0x3c03, 0x0c60, 0x1c41,
                0xedae, 0xfd8f, 0xcdec, 0xddcd, 0xad2a, 0xbd0b, 0x8d68, 0x9d49,
                0x7e97, 0x6eb6, 0x5ed5, 0x4ef4, 0x3e13, 0x2e32, 0x1e51, 0x0e70,
                0xff9f, 0xefbe, 0xdfdd, 0xcffc, 0xbf1b, 0xaf3a, 0x9f59, 0x8f78,
                0x9188, 0x81a9, 0xb1ca, 0xa1eb, 0xd10c, 0xc12d, 0xf14e, 0xe16f,
                0x1080, 0x00a1, 0x30c2, 0x20e3, 0x5004, 0x4025, 0x7046, 0x6067,
                0x83b9, 0x9398, 0xa3fb, 0xb3da, 0xc33d, 0xd31c, 0xe37f, 0xf35e,
                0x02b1, 0x1290, 0x22f3, 0x32d2, 0x4235, 0x5214, 0x6277, 0x7256,
                0xb5ea, 0xa5cb, 0x95a8, 0x8589, 0xf56e, 0xe54f, 0xd52c, 0xc50d,
                0x34e2, 0x24c3, 0x14a0, 0x0481, 0x7466, 0x6447, 0x5424, 0x4405,
                0xa7db, 0xb7fa, 0x8799, 0x97b8, 0xe75f, 0xf77e, 0xc71d, 0xd73c,
                0x26d3, 0x36f2, 0x0691, 0x16b0, 0x6657, 0x7676, 0x4615, 0x5634,
                0xd94c, 0xc96d, 0xf90e, 0xe92f, 0x99c8, 0x89e9, 0xb98a, 0xa9ab,
                0x5844, 0x4865, 0x7806, 0x6827, 0x18c0, 0x08e1, 0x3882, 0x28a3,
                0xcb7d, 0xdb5c, 0xeb3f, 0xfb1e, 0x8bf9, 0x9bd8, 0xabbb, 0xbb9a,
                0x4a75, 0x5a54, 0x6a37, 0x7a16, 0x0af1, 0x1ad0, 0x2ab3, 0x3a92,
                0xfd2e, 0xed0f, 0xdd6c, 0xcd4d, 0xbdaa, 0xad8b, 0x9de8, 0x8dc9,
                0x7c26, 0x6c07, 0x5c64, 0x4c45, 0x3ca2, 0x2c83, 0x1ce0, 0x0cc1,
                0xef1f, 0xff3e, 0xcf5d, 0xdf7c, 0xaf9b, 0xbfba, 0x8fd9, 0x9ff8,
                0x6e17, 0x7e36, 0x4e55, 0x5e74, 0x2e93, 0x3eb2, 0x0ed1, 0x1ef0
        };
    //CRC-16/XMODEM x16+x12+x5+1
    public static char getCRC1021(byte b[], int len) {
        char crc = 0;
        byte hb = 0;
        int j = 0;
        int index;
        while (len-- != 0) {
            hb = (byte) (crc / 256); //以8位二进制数的形式暂存CRC的高8位
            index = ((hb ^ b[j]) & 0xff); //求得所查索引下标
            crc <<= 8; // 左移8位,相当于CRC的低8位乘以
            crc ^= (TABLE1021[index]); // 高8位和当前字节相加后再查表求CRC ,再加上以前的CRC
            j++;
        }
        return (crc);
    }

然后,我们运行看一下结果:

2021-08-16 15:34:00.699 2540-2683/com.test.andserver.sample I/com.test.andserver.sample.util.SocketOkioManager : 服务器收到数据is:5aa50000ff3201e10201ff00016400000000000094b42a3c0000000000001f6a6425b30509ffff0295ff2a430006000001000000001fba55a20fff00ffff0496862a3c0002000000000000001fe20714f21717ffff0494222a3c00020065e3001ffecf67c40b24ffff0d948b2a3c0004003be5001feed8eb601827ffff1191c12a41000100000002830112000000001f6d4694e5121effff1491162a410000000000027a010f000000001f8cc5265e0209ffff1597c106060606b23fd0000000c65275cb0103ffff1794b52a3c0000000000001f052144c50509ffff2295022a430006000001000000001fc4b88623ff00ffff2396872a3c0002000000000000001f374ff1551713ffff2594232a3c00020065e3001f918a06b20c23ffff2895032a430006000001000000001fb982c9b5ff00ffff28948c2a3c0004003be5001ff22297861827ffff2c91172a410000000000027a010f000000001f9c27e29c0209ffff2e91c22a41000100000002840113000000001fded8e570121effff3195052a430006000001000000001f72bec9d8ff00ffff3c96882a3c0002000000000000001f4454ba801715ffff3d94b62a3c0000000000001fb4eee75f0609ffff4297c206060606b23fd0000000bea853a30003ffff4394242a3c00020065e3001f8d707a540c21ffff47ff8e5aa50000ff3200
I/com.test.andserver.sample.util.SocketOkioManager:检测不通过

I/com.test.andserver.sample.util.SocketOkioManager : 服务器收到数据is:5aa50000ff3201e10201ff00016400000000000094b42a3c0000000000001f6a6425b30509ffff0295ff2a430006000001000000001fba55a20fff00ffff0496862a3c0002000000000000001fe20714f21717ffff0494222a3c00020065e3001ffecf67c40b24ffff0d948b2a3c0004003be5001feed8eb601827ffff1191c12a41000100000002830112000000001f6d4694e5121effff1491162a410000000000027a010f000000001f8cc5265e0209ffff1597c106060606b23fd0000000c65275cb0103ffff1794b52a3c0000000000001f052144c50509ffff2295022a430006000001000000001fc4b88623ff00ffff2396872a3c0002000000000000001f374ff1551713ffff2594232a3c00020065e3001f918a06b20c23ffff2895032a430006000001000000001fb982c9b5ff00ffff28948c2a3c0004003be5001ff22297861827ffff2c91172a410000000000027a010f000000001f9c27e29c0209ffff2e91c22a41000100000002840113000000001fded8e570121effff3195052a430006000001000000001f72bec9d8ff00ffff3c96882a3c0002000000000000001f4454ba801715ffff3d94b62a3c0000000000001fb4eee75f0609ffff4297c206060606b23fd0000000bea853a30003ffff4394242a3c00020065e3001f8d707a540c21ffff47ff8e5aa50000ff3200a7020291182a410000000000027a010e000000001f2c390c840209ffff47948d2a3c0004003be5001f9d67f6f01827ffff4991c32a41000100000002830112000000001f4c82d471121effff5195062a430006000001000000001f778d177dff00ffff5a96892a3c0002000000000000001fb2a16a401713ffff5a94b72a3c0000000000001fdbab8629070effff5e91192a410000000000027a010f000000001f797ba35a030dffff635324
I/com.test.andserver.sample.util.SocketOkioManager:检测不通过

我们认证分析16进制字符串数据,你会发现俩次检测都不通过,第一次crc校验出的结果是:1B5F,如下所示,但是实际设备的校验码是:3200(读取到的数据的最后4个字符)
在这里插入图片描述
第二次crc校验出的结果是:CD07,如下所示,但是实际设备的校验码是:5324(读取到的数据的最后4个字符)
在这里插入图片描述

那么为什么会出现数据不完整,校验不通过的问题,其实主要还是因为TCP协议本身导致的,所以,在使用TCP时,如果要保证数据的完整性,我们必须的对数据进行封包和拆包。

为什么基于TCP的通讯程序需要进行封包和拆包?

TCP是个"流"协议,所谓流,就是没有界限的一串数据.大家可以想想河里的流水,是连成一片的,其间是没有分界线的.但一般通讯程序开发是需要定义一个个相互独立的数据包的,比如用于登陆的数据包,用于注销的数据包.由于TCP"流"的特性以及网络状况,在进行数据传输时会出现以下几种情况:

假设我们连续调用两次send方法,分别发送两段数据A和B,在接收端有可能存在有以下几种接收情况(这里只列出了有代表性的情况):

  1. 先接收到A,然后接收到B
  2. .先接收到A的部分数据,然后接收到A余下的部分以及B的全部.
  3. 先接收到了A的全部数据和B的部分数据,然后接收到了B的余下的数据.
  4. 一次性接收到了A和B的全部数据.

对于1这种情况正是我们需要的,不再做讨论.对于2,3,4的情况就是大家经常说的"粘包",就需要我们把接收到的数据进行“拆包”,拆成一个个独立的数据包.为了拆包就必须在发送端进行封包.另外补充一点,对于UDP来说,就不存在拆包的问题,因为UDP是个"数据包"协议,也就是两段数据间是有界限的,在接收端要么接收不到数据要么就是接收一个完整的一段数据,不会少接收也不会多接收.

下面我们就分析一下如何会出现2,3,4这三种情况。

为什么会发生 “粘包”

以上后三者就是我们俗称的"粘包"。"粘包"可发生在发送端也可发生在接收端。所以,我们要分端来分析:

  1. 粘包发生在发送端
    Nagle算法造成的发送端的粘包:Nagle算法是一种改善网络传输效率的算法.简单的说,当我们提交一段数据给TCP发送时,TCP并不立刻发送此段数据,而是等待一小段时间,看看在等待期间是否还有要发送的数据,若有则会一次把这两段数据拼接在一起发送出去.如果等待期间没有,那么收到什么就发送什么。

  2. 粘包发生在接受端
    接收端接收不及时造成的接收端粘包:TCP会把接收到的数据存在自己的缓冲区中,然后通知应用层取数据.当应用层由于某些原因不能及时的把TCP的数据取出来,就会造成TCP缓冲区中存放了几段数据,那么,最终可能导致应用层取到的数据有可能是好多次何在一起的结果,但是,对于应用层此时所获取的数据的完整性,就需要更复杂的通信或者数据数据链路层的规范来确定数据的合法性和完整性。

好了,既然知道了原因,我们回过头来,看看上面例子的的数据,我们来分析一下,看看数据是不是被分割发送了,也就是发送端出现了"粘包"。我们把俩次接受到的数据合并到一起看看:

I/com.test.andserver.sample.util.SocketOkioManager : 服务器收到数据is:5aa50000ff3201e10201ff00016400000000000094b42a3c0000000000001f6a6425b30509ffff0295ff2a430006000001000000001fba55a20fff00ffff0496862a3c0002000000000000001fe20714f21717ffff0494222a3c00020065e3001ffecf67c40b24ffff0d948b2a3c0004003be5001feed8eb601827ffff1191c12a41000100000002830112000000001f6d4694e5121effff1491162a410000000000027a010f000000001f8cc5265e0209ffff1597c106060606b23fd0000000c65275cb0103ffff1794b52a3c0000000000001f052144c50509ffff2295022a430006000001000000001fc4b88623ff00ffff2396872a3c0002000000000000001f374ff1551713ffff2594232a3c00020065e3001f918a06b20c23ffff2895032a430006000001000000001fb982c9b5ff00ffff28948c2a3c0004003be5001ff22297861827ffff2c91172a410000000000027a010f000000001f9c27e29c0209ffff2e91c22a41000100000002840113000000001fded8e570121effff3195052a430006000001000000001f72bec9d8ff00ffff3c96882a3c0002000000000000001f4454ba801715ffff3d94b62a3c0000000000001fb4eee75f0609ffff4297c206060606b23fd0000000bea853a30003ffff4394242a3c00020065e3001f8d707a540c21ffff47ff8e5aa50000ff32005aa50000ff3201e10201ff00016400000000000094b42a3c0000000000001f6a6425b30509ffff0295ff2a430006000001000000001fba55a20fff00ffff0496862a3c0002000000000000001fe20714f21717ffff0494222a3c00020065e3001ffecf67c40b24ffff0d948b2a3c0004003be5001feed8eb601827ffff1191c12a41000100000002830112000000001f6d4694e5121effff1491162a410000000000027a010f000000001f8cc5265e0209ffff1597c106060606b23fd0000000c65275cb0103ffff1794b52a3c0000000000001f052144c50509ffff2295022a430006000001000000001fc4b88623ff00ffff2396872a3c0002000000000000001f374ff1551713ffff2594232a3c00020065e3001f918a06b20c23ffff2895032a430006000001000000001fb982c9b5ff00ffff28948c2a3c0004003be5001ff22297861827ffff2c91172a410000000000027a010f000000001f9c27e29c0209ffff2e91c22a41000100000002840113000000001fded8e570121effff3195052a430006000001000000001f72bec9d8ff00ffff3c96882a3c0002000000000000001f4454ba801715ffff3d94b62a3c0000000000001fb4eee75f0609ffff4297c206060606b23fd0000000bea853a30003ffff4394242a3c00020065e3001f8d707a540c21ffff47ff8e5aa50000ff3200a7020291182a410000000000027a010e000000001f2c390c840209ffff47948d2a3c0004003be5001f9d67f6f01827ffff4991c32a41000100000002830112000000001f4c82d471121effff5195062a430006000001000000001f778d177dff00ffff5a96892a3c0002000000000000001fb2a16a401713ffff5a94b72a3c0000000000001fdbab8629070effff5e91192a410000000000027a010f000000001f797ba35a030dffff635324

通过CRC校验,你会发现,果然实在发送端发生了"粘包",我们把俩次读取的数据合拼拼接在一起,CRC校验后果然是5324,说明CRC校验通过,接受到的数据是一帧完整数据。

怎样封包和拆包

最初遇到"粘包"的问题时,我是通过在两次send之间调用sleep来休眠一小段时间来解决。这个解决方法的缺点是显而易见的,使传输效率大大降低,而且也并不可靠。后来就是通过应答的方式来解决,尽管在大多数时候是可行的,但是不能解决像2的那种情况,而且采用应答方式增加了通讯量,加重了网络负荷(但是像FTP等协议采用的就是应答方式)。再后来就是对数据包进行封包和拆包的操作

  1. 封包
    封包就是给一段数据加上包头,这样一来数据包就分为包头和包体两部分内容了。包头其实上是个大小固定的结构体,其中有个结构体成员变量表示包体的长度,这是个很重要的变量,其他的结构体成员可根据需要自己定义。根据包头长度固定以及包头中含有包体长度的变量就能正确的拆分出一个完整的数据包.

  2. 拆包
    对于拆包目前我最常用的是以下两种方式.

    1.动态缓冲区暂存方式:之所以说缓冲区是动态的,是因为当需要缓冲的数据长度超出缓冲区的长度时会增大缓冲区长度.大概过程描述如下:

    • 为每一个连接动态分配一个缓冲区,同时把此缓冲区和SOCKET关联,常用的是通过结构体关联.

    • 当接收到数据时首先把此段数据存放在缓冲区中.

    • 判断缓存区中的数据长度是否够一个包头的长度,如不够,则不进行拆包操作.

    • 根据包头数据解析出里面代表包体长度的变量.

    • 判断缓存区中除包头外的数据长度是否够一个包体的长度,如不够,则不进行拆包操作.

    • 取出整个数据包.这里的"取"的意思是不光从缓冲区中拷贝出数据包,而且要把此数据包从缓存区中删除掉.删除的办法就是把此包后面的数据移动到缓冲区的起始地址.

      这种方法有两个缺点:

         1. 为每个连接动态分配一个缓冲区增大了内存的使用
       
         2. 有三个地方需要拷贝数据,一个地方是把数据存放在缓冲区,一个地方是把完整的数据包从缓冲区取出来,一个地方是把数据包从缓冲区中删除,这种拆包的改进方法会解决和完善部分缺点
      

    先看包头结构定义

    #pragma pack(push,1) //开始定义数据包, 采用字节对齐方式
    /*----------------------包头---------------------*/
    typedef struct tagPACKAGEHEAD
    {
    BYTE Version;
    WORD Command;
    WORD nDataLen;//包体的长度
    }PACKAGE_HEAD;
    #pragma pack(pop) //结束定义数据包, 恢复原来对齐方式
    

    然后看存放数据和"取"数据函数:

    // Description:添加数据到缓存
    // Input:pBuff[in]-待添加的数据;nLen[in]-待添加数据长度
    // Return: 如果当前缓冲区没有足够的空间存放pBuff则返回FALSE;否则返回TRUE。
    BOOL CDataBufferPool::AddBuff( char *pBuff, int nLen )
    {
        m_cs.Lock();///临界区锁
    	if ( nLen < 0 )
    	{
    	    m_cs.Unlock();
    	    return FALSE;
    	}
    
    	if ( nLen <= GetFreeSize()) //判断剩余空间是否足够存放nLen长的数据
    	{
    	memcpy(m_pBuff + m_nOffset, pBuff, nLen);
    	   m_nOffset += nLen;
    	}
    	else{
    	  //若不够则扩充原有的空间
    	{ 
    	char *p = m_pBuff;
    	m_nSize += nLen*2;//每次增长2*nLen
    	m_pBuff = new char[m_nSize];
    	memcpy(m_pBuff,p,m_nOffset);
    	delete []p;
    	memcpy(m_pBuff + m_nOffset, pBuff, nLen);
    	m_nOffset += nLen;
    	m_cs.Unlock();
    	return FALSE;
    	}
        m_cs.Unlock();
    return TRUE;
    }
    

    校验包的合法性:

    // Description:获取一个完整的包
    // Input:Buf[out]-获取到的数据;nLen[out]-获取到的数据长度
    // Return: 1、当前缓冲区不够一个包头的数据 2、当前缓冲区不够一个包体的数据
    
    int CDataBufferPool::GetFullPacket( char *Buf, int& nLen )
    {
       	m_cs.Lock();
    	if ( m_nOffset < m_PacketHeadLen )//当前缓冲区不够一个包头的数据
    	{
    	     m_cs.Unlock();
         	return 1;
    	}
    	PACKAGE_HEAD *p = (PACKAGE_HEAD *)m_pBuff;
    	if( (m_nOffset-m_PacketHeadLen) < (int)p->nDataLen )//当前缓冲区不够一个包体的数据
    	{
    	   m_cs.Unlock();
    	   return 2;
    	}
    	//判断包的合法性
    	/* int IsIntegrallity = ValidatePackIntegrality(p);
    	if( IsIntegrallity != 0 )
    	{
    	m_cs.Unlock();
    	return IsIntegrallity;
    	}
    	*/
    	nLen = m_PacketHeadLen+p->nDataLen;
    	memcpy( Buf, m_pBuff, nLen );
    	m_nOffset -= nLen;
    	memcpy( m_pBuff, m_pBuff+nLen, m_nOffset );
    	
    	m_cs.Unlock();
    	return 0;
    }
    
    

    前面提到过这种方法的缺点。下面给出一个改进办法, 即采用环形缓冲。但是这种改进方法还是不能解决第一个缺点以及第一个数据拷贝,只能解决第三个地方的数据拷贝(这个地方是拷贝数据最多的地方)。

    环形缓冲实现方案是定义两个指针,分别指向有效数据的头和尾。在存放数据和删除数据时只是进行头尾指针的移动。下面的代码是采用一个开源的游戏服务器的代码,我对此代码有所修改。

    int CCircularBufferPool::PutData(TCHAR *pData, int len)
    {
    	if( len <= 0 ) 
    	return 1;
    
    	EnterCriticalSection(&m_cs);
    	while (IsOverFlowCondition(len))///判断缓冲区剩余空间是否够存放len长的数据
    	{
    	BufferResize(len);///若不够,则扩充缓冲区.
    	}
    
    	if (IsIndexOverFlow(len))///判断"尾"指针的位置.
    	{
    		int FirstCopyLen = m_iBufSize-m_iTailPos;
    		int SecondCopyLen = len - FirstCopyLen;
    		CopyMemory(m_pBuffer+m_iTailPos, pData, FirstCopyLen);
    		if (SecondCopyLen)
    		{
    		   CopyMemory(m_pBuffer, pData+FirstCopyLen, SecondCopyLen);
    		   m_iTailPos = SecondCopyLen;
    		}else{
    		   m_iTailPos = 0;
    		}
    	}else{
    	    CopyMemory(m_pBuffer+m_iTailPos, pData, len);
    	    m_iTailPos += len;
    	}
    	LeaveCriticalSection(&m_cs);
    	return 0;	
     }
    
    
    void CCircularBufferPool::GetData(TCHAR *pData, int len, bool Delete)
    {
    	if (len < m_iBufSize-m_iHeadPos)
    	{
    	  CopyMemory(pData, m_pBuffer+m_iHeadPos, len);
    		if(Delete==true)
    		   m_iHeadPos += len;
    		}else{
    		int fc, sc;
    		fc = m_iBufSize-m_iHeadPos;
    		sc = len - fc;
    		 CopyMemory(pData, m_pBuffer+m_iHeadPos, fc);
    		 if (sc) CopyMemory(pData+fc, m_pBuffer, sc);
    		 
    		 if(Delete==true)
    		    m_iHeadPos = sc;
    		    
    		 if(m_iHeadPos >= m_iBufSize)
    		   m_iHeadPos = 0;
    		}
    	}
    

    进行自定义包的解析

    int CCircularBufferPool::GetFullPacket( TCHAR *Buf, int &nLen )
    {
        EnterCriticalSection(&m_cs);
    	if( GetValidCount() < m_PacketHeadLen )//当前缓冲区不够一个包头的数据
    	{
    	LeaveCriticalSection(&m_cs);
    	return 1;
    	}
        GetData(Buf,m_PacketHeadLen,false);
        PACKAGE_HEAD *p = (PACKAGE_HEAD *)Buf;
    	if( (GetValidCount()-m_PacketHeadLen) < (int)p->nDataLen )//当前缓冲区不够一个包体的数据
    	{
    	LeaveCriticalSection(&m_cs);
    	 return 2;
    	}
    

    判断包的合法性:

    int IsIntegrallity = ValidatePackIntegrality(p);
    	if( IsIntegrallity != 0 )
    	{
    	LeaveCriticalSection(&m_cs);
    	return IsIntegrallity;
    	}
    
       GetData(Buf,m_PacketHeadLen+p->nDataLen,true);
       nLen = m_PacketHeadLen+p->nDataLen;
       LeaveCriticalSection(&m_cs);
        return 0;
    }
    

2.利用底层的缓冲区来进行拆包:
由于TCP也维护了一个缓冲区,所以我们完全可以利用TCP的缓冲区来缓存我们的数据,这样一来就不需要为每一个连接分配一个缓冲区了。另一方面我们知道recv或者wsarecv都有一个参数,用来表示我们要接收多长长度的数据。利用这两个条件我们就可以对第一种方法进行优化了。

对于阻塞SOCKET来说,我们可以利用一个循环来接收包头长度的数据,然后解析出代表包体长度的那个变量,再用一个循环来接收包体长度的数据。相关代码如下:

char PackageHead[1024];
char PackageContext[1024*20];

int len;
PACKAGE_HEAD *pPackageHead;
while( m_bClose == false )
{
    memset(PackageHead,0,sizeof(PACKAGE_HEAD));
    len = m_TcpSock.ReceiveSize((char*)PackageHead,sizeof(PACKAGE_HEAD));
	if( len == SOCKET_ERROR )
	{
	      break;
	}
	if(len == 0)
	{
	      break;
	}
	
    pPackageHead = (PACKAGE_HEAD *)PackageHead;
    memset(PackageContext,0,sizeof(PackageContext));
	if(pPackageHead->nDataLen>0)
	{
	   len = m_TcpSock.ReceiveSize((char*)PackageContext,pPackageHead->nDataLen);
	}       
}

m_TcpSock是一个封装了SOCKET的类的变量,其中的ReceiveSize用于接收一定长度的数据,直到接收了一定长度的数据或者网络出错才返回

int winSocket::ReceiveSize( char* strData, int iLen )
{
	if( strData == NULL )
	return ERR_BADPARAM;
	char *p = strData;
	int len = iLen;
	int ret = 0;
	int returnlen = 0;
	while( len > 0)
	{
	   ret = recv( m_hSocket, p+(iLen-len), iLen-returnlen, 0 );
	   if ( ret == SOCKET_ERROR || ret == 0 ){
	   return ret;
	  }
       len -= ret;
	   returnlen += ret;
	}
   return returnlen;
}

对于非阻塞的SOCKET。比如完成端口,我们可以提交接收包头长度的数据的请求,当GetQueuedCompletionStatus返回时,我们判断接收的数据长度是否等于包头长度,若等于,则提交接收包体长度的数据的请求,若不等于则提交接收剩余数据的请求。当接收包体时,采用类似的方法。

enum IOType 
{
	IOInitialize,
	IORead,
	IOWrite,
	IOIdle
};

class OVERLAPPEDPLUS 
{
public:
	OVERLAPPED   m_ol;
	IOType    m_ioType;
	bool         m_bIsPackageHead;//当前接收的数据是否是包头数据。
	
	int          m_count;
	WSABUF       m_wsaBuffer;
	int          m_RecvPos;
	char         m_Buffer[1024*8];//此缓冲要尽可能大
	
	OVERLAPPEDPLUS(IOType ioType) {
	ZeroMemory(this, sizeof(OVERLAPPEDPLUS));
	m_ioType = ioType;
 }
};

	//接收连接后发出的第一个请求,请求接收包头大小的数据.
	OVERLAPPEDPLUS *pOverlappedPlus = new OVERLAPPEDPLUS;
	pOverlappedPlus->m_wsaBuffer.buf = pOverlappedPlus->m_Buffer;
	pOverlappedPlus->m_wsaBuffer.len = PACKAGE_HEAD_LEN;///包头的长度
	pOverlappedPlus->m_bIsPackageHead = true;
	pOverlappedPlus->m_RecvPos = 0;
	pOverlappedPlus->m_ioType = IORead;

	DWORD RecvBytes;
	     DWORD Flags;
	Flags = 0;
	if (WSARecv(clientSocket, &(pOverlappedPlus->m_wsaBuffer), 1, &RecvBytes, &Flags,
	&pOverlappedPlus->m_ol, NULL) == SOCKET_ERROR)
	{
		if (WSAGetLastError() != ERROR_IO_PENDING)
		{
		   delete pOverlappedPlus;
		}
		else
		{
		   //相关的错误处理	
		}
	}
	else
	{
	//相关的错误处理
	}


//在GetQueuedCompletionStatus所在的函数中.
	if( pOverlapPlus->m_ioType== IORead)
	{ 
	if( pOverlapPlus->m_wsaBuffer.len == dwIoSize )
	{
	   if( pOverlapPlus->m_bIsPackageHead == true )//接收到的是包头。
	   {
	    PACKAGE_HEAD *pPackageHead = (PACKAGE_HEAD *)(pOverlapPlus->m_Buffer);
	
	    if(pThis->IsLegalityPackageHead(pPackageHead)==false)//判断是否是合法的包
	    {
	     closesocket(lpClientContext->m_Socket);
	     continue;
	    }	
	    pOverlapPlus->m_bIsPackageHead = false;
	    pOverlapPlus->m_wsaBuffer.len = pPackageHead->nDataLen;
	    pOverlapPlus->m_RecvPos += dwIoSize;
	    pOverlapPlus->m_wsaBuffer.buf = pOverlapPlus->m_Buffer+pOverlapPlus->m_RecvPos;	
	   }
	   else///接收到的是包体
	   {
	   pOverlapPlus->m_RecvPos += dwIoSize;
	   //这时pOverlapPlus->m_Buffer里就存放了一个完整的数据包,长度为pOverlapPlus->m_RecvPos	
	   //继续请求 请求下一个数据包的包头
	   pOverlapPlus->m_wsaBuffer.buf = pOverlapPlus->m_Buffer;
	   memset(pOverlapPlus->m_Buffer,0,sizeof(pOverlapPlus->m_Buffer));
	   pOverlapPlus->m_wsaBuffer.len = PACKAGE_HEAD_LEN;
	   pOverlapPlus->m_bIsPackageHead = true;
	   pOverlapPlus->m_RecvPos = 0;
	     
	   }
	}
	else///接收的数据还不完整
	{
	   pOverlapPlus->m_wsaBuffer.len -= dwIoSize;
	   pOverlapPlus->m_RecvPos += dwIoSize;
	   pOverlapPlus->m_wsaBuffer.buf = pOverlapPlus->m_Buffer+pOverlapPlus->m_RecvPos;
	}
	pOverlapPlus->m_ioType = IORead;
	state = WSARecv(lpClientContext->m_Socket, &(pOverlapPlus->m_wsaBuffer), 1, &RecvBytes, &Flags,
	    &pOverlapPlus->m_ol, NULL);
	if ( state == SOCKET_ERROR)
	{
	   if(WSAGetLastError() != ERROR_IO_PENDING)
	   {
	       
	       //关闭套接字 释放相应资源
	     continue;
	   }
	}
	   
	}

解决办法

通过上面的分析,我们可以进一步把代码完善如下,从而确保数据完整接受,解决方式如下:

  1. 更具包头长度读取
    在传递需要的数据之前,先传递数据的长度,然后在输入流中的读取数据长度做比较,如果读取的长度小于完整数据的长度就一直读取:
int count = 0;
byte[] data = new byte[dataLen];
//封装输入流(接收客户端的流)
BufferedInputStream bis = new BufferedInputStream(socket.getInputStream());
DataInputStream dis = new DataInputStream(bis);
while(count<dataLen){
    int available = dis.available();
    if(available>data.length-count){
        available=data.length-count
    }else{
       //CRC-16/XMODEM x16+x12+x5+1
	   char content_char = HexUtils.getCRC1021(content_byte, content_byte.length);
	   //生成校验码
	   String content_code = Integer.toHexString(content_char).toLowerCase();
    }
    count += dis.read(data,count,available)
}
  1. 一字节一字节读取,循环校验

                //封装输入流(接收客户端的流)
                BufferedInputStream bis = new BufferedInputStream(
                        socket.getInputStream());
                DataInputStream dis = new DataInputStream(bis);
                List<String> newData = new ArrayList<>();
                byte[] bytes = new byte[1]; // 一次读取一个byte
                StringBuilder msg = new StringBuilder();
                long time=System.currentTimeMillis();
                while (dis.read(bytes) != -1) {
                    msg.append(HexUtils.encode(bytes));
                    if (dis.available() == 0) {
                        Log.i(TAG, "服务器收到数据is:" + msg);
                        String datas=msg.toString();
                        //取出字符串后4位作为校验码,用来校验数据是否合格
                        String checkCode=datas.substring(datas.length()-4,datas.length());
                        Log.i(TAG, "本轮 checkCode is:" +checkCode);
                        //先crc检验是否合格
                        byte[] content_byte = HexUtils.decode(datas.substring(0,datas.length()-4));
                        //CRC-16/XMODEM x16+x12+x5+1
                        char content_char = HexUtils.getCRC1021(content_byte, content_byte.length);
                        //生成校验码
                        String content_code = Integer.toHexString(content_char).toLowerCase();
                        Log.i(TAG, "this content_code start is:" +content_code);
                        //自动拼接让content_code为2字节
                        if (content_code.length() == 1) {
                            content_code = "000" + content_code;
                        }
                        if (content_code.length() == 2) {
                            content_code = "00" + content_code;
                        }
                        if (content_code.length() == 3) {
                            content_code = "0" + content_code;
                        }
                        Log.i(TAG, "this  content_code end is:" +content_code);
                        //在判断是否是完整的针数据
                        if (content_code.equals(checkCode)){
                            //处理完整数据,并清空接受对象
                            if (datas.contains(target)){
                                String[] split = datas.split(target);
                                if (split.length >= 2) {
                                    for (int i = 0; i < split.length; i++) {
                                        String data = split[i];
                                        if (!data.equals("") && data.length() > 0) {
                                            newData.add(target + data);
                                        }
                                    }
                                    //回调
                                    //todo ....
                                }
                            }
                            Log.i(TAG, "this is complete data!");
                            //置空
                            msg.delete(0,msg.length());
                            newData.clear();
                        }else {
                            //不完整跳过不处理
                            Log.i(TAG, "this is not complete data!");
                        }
                    }
                }

总结

我们已经知道TCP是个"流"协议,所谓流,就是没有界限的一串数据.真是因为如此,会导致数据的不完整性,为了解决这种问题,在使用TCP时,一定要先拆包和粘包,拆包粘包之后再判断包的合法性,通过校验(比如CRC校验)从而确保包数据的完整性。

判断包的合法性可以结合下面两种方式来判断

  1. 通过包头的结构来判断包的合法性.

    最初的时候我是根据包头来判断包的合法性,比如判断Command是否超出命令范围,nDataLen是否大于最大包的长度。但是这种方法无法过滤掉非法包,当出现非法包时我们唯一能做的就是断开连接,或许这也是最好的处理办法。

    我们可以给一个完整的包加上开始和结束标志,标志可以是个整数,也可以是一串字符串。以第一种拆包方式为例来说明。当要拆一个完整包时我们先从缓冲区有效数据头指针地址搜索包的开始标志,搜索到后并且当前数据够一个包头数据,则判断开始标志和包头是否合法,若合法则根据代表数据长度的变量的值定位到包尾,判断包尾标志是否与我们定义的一致,若一致则这个包是合法的包。若有一项不一致则继续寻找下个包的开始标志,并把下个合法包的前面的数据全部舍弃。

  2. 通过逻辑层来判断包的合法性
    当取出一个合法的包时,我们还要根据当前数据处理的逻辑来判断包的合法性.比如说在登陆成功后的某段时间服务器又收到了同一个客户端的登陆包,那我们就可以判断这个包是非法的,简单处理就是断开连接。

  网络协议 最新文章
使用Easyswoole 搭建简单的Websoket服务
常见的数据通信方式有哪些?
Openssl 1024bit RSA算法---公私钥获取和处
HTTPS协议的密钥交换流程
《小白WEB安全入门》03. 漏洞篇
HttpRunner4.x 安装与使用
2021-07-04
手写RPC学习笔记
K8S高可用版本部署
mySQL计算IP地址范围
上一篇文章      下一篇文章      查看所有文章
加:2021-08-17 15:45:03  更:2021-08-17 15:45:23 
 
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁

360图书馆 购物 三丰科技 阅读网 日历 万年历 2024年5日历 -2024/5/17 18:09:06-

图片自动播放器
↓图片自动播放器↓
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
图片批量下载器
↓批量下载图片,美女图库↓
  网站联系: qq:121756557 email:121756557@qq.com  IT数码