前面两节介绍了 RS-485 和 Modbus 协议,两者关系如下:
RS-485 是工作在 OSI 七层网络模型,物理层的一种串口传输规范,主要用来负责信号的一些电气特性。通过差分信号来决定传输信号0或1。
Modbus 协议是专门针对 RS-485 设计的一种运行在网络层 TCP/IP上的报文传输协议。
TCP粘包是指发送方发送的若干数据到接收方接收时粘成一包,从接收缓冲区看,后一包数据的头紧接着前一包数据的尾。
出现粘包现象的原因是多方面的,它即可由发送方造成,也可能由接收方造成。
发送方引起的粘包原因?
就收方引起的粘包原因?
粘包有两种情况:
1. 粘在一起的包都是完整的数据包
2. 粘在一起的包有不完整的包
不是所有的粘包现象都需要处理,若传输的数据为不带结构的连续流数据(如文件传输),则不必把粘连的包分开(简称分包)。
对于带结构的数据,分为定长结构数据和不定长结构数据,前者处理起来较为简单,后者就比较复杂。
为什么要自定义网络协议?
解决 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.