[ 监测系统 ] [3] TCP 自定义协议

回顾

前面两节介绍了 RS-485 和 Modbus 协议,两者关系如下:

RS-485 是工作在 OSI 七层网络模型,物理层的一种串口传输规范,主要用来负责信号的一些电气特性。通过差分信号来决定传输信号0或1。

Modbus 协议是专门针对 RS-485 设计的一种运行在网络层 TCP/IP上的报文传输协议。

TCP协议

  1. TCP 是一个面向连接的传输层协议
  2. 数据确认:数据发送后,请求接收方回传确认信息(ACK)。发送方如果在规定的时间内没有收到数据确认,就重传该数据。
  3. 数据编号:使接收方可以处理数据的失序和重复问题。
  4. 校验和:解决数据误码问题。
  5. 流量控制:保证可靠性的一个重要措施,若无流控,可能会因就收缓冲区溢出而丢失大量数据,导致许多重传,造成网络拥塞恶性循环。TCP采用可变窗口进行流量控制,由接收方控制发送方发送的数据。
  6. 可靠性提高的同时,会降低传输效率。

粘包问题

TCP粘包是指发送方发送的若干数据到接收方接收时粘成一包,从接收缓冲区看,后一包数据的头紧接着前一包数据的尾。

出现粘包现象的原因是多方面的,它即可由发送方造成,也可能由接收方造成。

发送方引起的粘包原因?

就收方引起的粘包原因?

粘包有两种情况:

1. 粘在一起的包都是完整的数据包

image.png

image.png

2. 粘在一起的包有不完整的包

image.png

不是所有的粘包现象都需要处理,若传输的数据为不带结构的连续流数据(如文件传输),则不必把粘连的包分开(简称分包)。

对于带结构的数据,分为定长结构数据和不定长结构数据,前者处理起来较为简单,后者就比较复杂。

自定义协议

为什么要自定义网络协议?

解决 TCP 网络传输,粘包现象,即发送方发送的若干数据到接收方接收时粘成一包。

什么是封包与解包?

TCP/IP 网络数据以流的方式传输,数据流是由包组成,如何判定接收方收到的包是一个完整的包就要在发送时对包进行处理,这就是封包技术,将包处理成包头和包体。

包头是包的开始标记,整个包的大小就是包的结束标志。

如何自定义协议?

发送时数据包由包头+数据组成,其中包头内容分为包类型 + 包长度。

接收时,只需要先保证将数据包的包头读完整,通过收到的数据包包头里的数据长度和数据包类型,判断出我们将要收到一个带有什么样类型的多少长度的数据。然后循环接收直到接收的数据大小等于数据长度停止,此时,完成接受一个完整数据包。

代码

参考 [2] 中的代码,运行时,总会出现 server 从客户端接收数据失败。对应代码如下:

bool TCPServer::server_recv(int conn_fd)  //接收数据函数
{
    int nrecvsize=0; //一次接收到的数据大小
    int sum_recvsize=0; //总共收到的数据大小
    int packersize;   //数据包总大小
    int datasize;     //数据总大小
    unsigned char recv_buffer[10000];  //接收数据的buffer


    memset(recv_buffer,0,sizeof(recv_buffer));  //初始化接收buffer


    while(sum_recvsize!=sizeof(NetPacketHeader))
    {
        nrecvsize=recv(conn_fd,recv_buffer+sum_recvsize,sizeof(NetPacketHeader)-sum_recvsize,0);
        cout << "nrecvsize = " << nrecvsize << endl;
        if(nrecvsize == 0)
        {
            // 报错 *1
            cout<< __LINE__ << ": 从客户端接收数据失败"<<endl;
            return false;
        }
        cout << "recv_buffer = " << recv_buffer << endl;
        sum_recvsize += nrecvsize;

    }

    NetPacketHeader *phead=(NetPacketHeader*)recv_buffer;
    packersize=phead->wDataSize;  //数据包大小
    datasize=packersize-sizeof(NetPacketHeader);     //数据总大小

    while(sum_recvsize!=packersize)
    {
        nrecvsize=recv(conn_fd,recv_buffer+sum_recvsize,packersize-sum_recvsize,0);
        if(nrecvsize==0)
        {
            cout << __LINE__ << "从客户端接收数据失败" << endl;
            return false;
        }
        sum_recvsize+=nrecvsize;
    }

    dealwithpacket(conn_fd,(unsigned char*)(phead+1),phead->wOpcode,datasize);  //处理接收到的数据

}

当数据接收完成后,会执行到这里(*1),需要做特殊判断,并不是真正意义上的错误。

参考

[1]杨小平,王胜开.解决TCP网络传输“粘包”问题[J].软件世界,1999,(11):54-55.

[2]CSDN一个简单的TCP自定义通信协议