前言:TCP与UDP是大家耳熟能详的两种传输层通信协议,本质区别在于传输控制策略不相同:使用TCP协议,可以保证传输层数据包能够有序地被接受方接收到,依赖其内部一系列复杂的机制,比如握手协商,ACK确认,超时重传,拥塞控制等; 而UDP基本上没有额外的控制策略,所以接收方能不能接收到传输层数据包是无法保证的。正是因为不能保证每一个数据包有序到达,UDP数据包与包之间,必须是相互独立的,每一个都应该是有意义的可以被解析出完整应用层报文的数据块,因此UDP又被称为面向(单个)报文的协议;而每一个TCP数据包则可以是应用层报文的某一部分,多个有序的数据包就可以拼接出完整的应用层报文,因此TCP被称作面向流的协议。
我们知道,网络层(即IP层)数据包是有最大长度MTU限制的(因为物理层大包丢包概率很高),所以不论是发送UDP包还是TCP包,如果突破了该限制,数据包将会被IP层切片,接收方的IP层会根据分片id对传输层数据片段的进行重组,分片和重组都会占用cpu和内存资源,严重降低通讯效率。如果通信双方采用TCP通信,在握手连接阶段会协商MSS,即一个TCP最大包含的数据量,有了MSS约定,TCP层交付给IP层的数据包就不会超过IP层的MTU限制,也就是说分片工作在TCP传输层完成。而使用UDP时,一旦UDP数据包被IP层分片,接收方大概率是无法组成完整的UDP数据包的,因为就算某些片段丢失了,发送方也不会对整个UDP包进行重发,因此UDP通讯是禁止IP层分片的(一旦超过MTU,会直接丢弃)。
所以,如果我们的应用需要传输大的数据包,就没办法使用单纯的UDP协议传输了,除非基于UDP在应用层自行实现一种类似于TCP内部的分片控制机制,完成数据的可靠传输。
一. 自定义网络协议
假设我们的应用层报文一般比较小,不超过底层的MTU限制,这样一来,我们既可以使用TCP,也可以使用UDP来进行传输。下面是一个最简单的协议定义示例,包含消息头定义和消息体定义:
struct SmHeader
{
int m_length;// 消息头长度+消息体长度
int m_request_type;//请求类型
int m_reply_type;//响应类型
int m_body_type;//消息体类型
};
struct Body1
{
char m_char_b1;
int m_int_b1;
float m_float_b1;
};
struct Body2
{
int m_int_array_b2[12];
float m_float_array_b2[15];
};
struct Body3
{
char m_char_array_b3[512];
};
struct Body1Assemble
{
int m_count;
Body1* m_b1;
};
union SmBody
{
Body1 b1;
Body2 b2;
Body3 b3;
Body1Assemble b4;
};
struct SmMessage
{
SmHeader head;
SmBody body;
};
由上述协议的定义可知,这样一个完整的消息最少有sizeof(SmHeader) = 16字节(一个消息可以没有消息体,比如PING/PONG心跳包,只有消息头即可),由于Body1Assemble类型的数据体长度不确定,因此用TCP的话,可以传递很长的消息。
二. 消息的接收
由于发送方的发送速率与接收方的接收速率很难匹配,在接收方的接收缓冲区内会形成数据包累积,所以我们需要上述定义的消息头协助完成数据包的提取,有效处理接收端粘包问题。
char buffer[1024];//在应用层定义一个数据缓冲区,至少能够放得下最大的数据包
int bfsize = 1024;//缓冲区长度
int legacy_bytes = 0;//上一次解析处理剩余的字节数
bool skip_recv = false;//是否可以直接使用上一次剩余数据解析出完整数据
while(1)
{
int current_size = 0;
if(!skip_recv)
{
current_size = recv(fd,buffer+legacy_bytes,bfsize-legacy_bytes);
if(current_size<=0)
break;
current_size += legacy_bytes;
legacy_bytes = 0;
}
else
{
current_size = legacy_bytes;
legacy_bytes = 0;
skip_recv = false;
}
int expected_size = -1;
if(current_size>=sizeof(SmHeader))
{
const SmHeader* head = (SmHeader*)buffer;
expected_size = head->m_length;
if(expected_size<=0 || expected_size>bfsize)
{
printf("Invalid message header or buffer insufficient.\r\n");
}
else if(current_size>=expected_size)
{
/***********process a complete message******/
// 解析buffer中的消息;
SmMessage* msg = (SmMessage*)buffer;
if(head->m_body_type == BODY1ASSEMBLE)
{
msg->body.b4.m_b1 = (Body1*)(buffer + sizeof(SmHeader) + sizeof(Body1Assemble));
}
// 处理消息
// balabala...
/********************end********************/
if(current_size>expected_size)
{
legacy_bytes = current_size-expected_size;
memmove(buffer,buffer+expected_size,legacy_bytes);
if(legacy_bytes>=sizeof(SmHeader))
{
const SmHeader* next_head = (SmHeader*)buffer;
int next_expected_size = next_head->m_length;
if(next_expected_size>0)
skip_recv = legacy_bytes>=next_expected_size;
}
}
else
{
legacy_bytes = 0;
}
}
else
{
legacy_bytes = current_size;
printf("Incompleted message.\r\n");
}
}
else
{
legacy_bytes = current_size;
}
}