一、主要丢包原因
1、接收端处理时间过长导致丢包:调用recv方法接收端收到数据后,处理数据花了一些时间,处理完后再次调用recv方法,在这二次调用间隔里,发过来的包可能丢失。对于这种情况可以修改接收端,将包接收后存入一个缓冲区,然后迅速返回继续recv。
2、发送的包巨大丢包:虽然send方法会帮你做大包切割成小包发送的事情,但包太大也不行。例如超过50K的一个udp包,不切割直接通过send方法发送也会导致这个包丢失。这种情况需要切割成小包再逐个send。
3、发送的包较大,超过接受者缓存导致丢包:包超过mtu size数倍,几个大的udp包可能会超过接收者的缓冲,导致丢包。这种情况可以设置socket接收缓冲。以前遇到过这种问题,我把接收缓冲设置成64K就解决了。
int nRecvBuf=32*1024;//设置为32K
setsockopt(s,SOL_SOCKET,SO_RCVBUF,(const char*)&nRecvBuf,sizeof(int));
巨型帧可以设置到9K缓存
4、发送的包频率太快:虽然每个包的大小都小于mtu size 但是频率太快,例如40多个mut size的包连续发送中间不sleep,也有可能导致丢包。这种情况也有时可以通过设置socket接收缓冲解决,但有时解决不了。所以在发送频率过快的时候还是考虑sleep一下吧。
5、局域网内不丢包,公网上丢包。这个问题我也是通过切割小包并sleep发送解决的。如果流量太大,这个办法也不灵了。总之udp丢包总是会有的,如果出现了用我的方法解决不了,还有这个几个方法: 要么减小流量,要么换tcp协议传输,要么做丢包重传的工作。
二、具体问题分析
1.发送频率过高导致丢包
很多人会不理解发送速度过快为什么会产生丢包,原因就是UDP的SendTo不会造成线程阻塞,也就是说,UDP的SentTo不会像TCP中的SendTo那样,直到数据完全发送才会return回调用函数,它不保证当执行下一条语句时数据是否被发送。(SendTo方法是异步的)这样,如果要发送的数据过多或者过大,那么在缓冲区满的那个瞬间要发送的报文就很有可能被丢失。至于对“过快”的解释,作者这样说:“A few packets a second are not an issue; hundreds or thousands may be an issue.”(一秒钟几个数据包不算什么,但是一秒钟成百上千的数据包就不好办了)。 要解决接收方丢包的问题很简单,首先要保证程序执行后马上开始监听(如果数据包不确定什么时候发过来的话),其次,要在收到一个数据包后最短的时间内重新回到监听状态,其间要尽量避免复杂的操作(比较好的解决办法是使用多线程回调机制)。
2.报文过大丢包
至于报文过大的问题,可以通过控制报文大小来解决,使得每个报文的长度小于MTU。以太网的MTU通常是1500 bytes,其他一些诸如拨号连接的网络MTU值为1280 bytes,如果使用speaking这样很难得到MTU的网络,那么最好将报文长度控制在1280 bytes以下。
3.发送方丢包
发送方丢包:内部缓冲区(internal buffers)已满,并且发送速度过快(即发送两个报文之间的间隔过短); 接收方丢包:Socket未开始监听; 虽然UDP的报文长度最大可以达到64 kb,但是当报文过大时,稳定性会大大减弱。这是因为当报文过大时会被分割,使得每个分割块(翻译可能有误差,原文是fragmentation)的长度小于MTU,然后分别发送,并在接收方重新组合(reassemble),但是如果其中一个报文丢失,那么其他已收到的报文都无法返回给程序,也就无法得到完整的数据了。
引用:http://www.voidcn.com/article/p-agyzitmn-ot.html
开辟了一个新的线程负责将数据写入到文件,原来的线程只负责接收数据。当接收数据的线程收到一副图像的最后一个包时,马上置位相应变量,另一个线程不断检查该变量,条件满足时将数据写入到文件。其实这种轮询的方式也很耗费CPU资源,最好的办法是用事件(Event)来同步,不过这个版本的结果就很完美了,基本上没有丢失包,我就没有继续改进了。
#include // ......省略头文件
//......此处省略大量变量的声明和定义
bool IsNewImageReceived = false;
uint32_t GetImageNo(char data[PACKSIZ]);
uint16_t GetBlockNo(char data[PACKSIZ]);
// 和多线程相关的变量
static HANDLE g_hMutex = INVALID_HANDLE_VALUE;
HANDLE hThread = INVALID_HANDLE_VALUE;
DWORD WINAPI Thread_fcn(LPVOID lpParameter); // 线程处理函数
int main()
{
// ......此处省略socket、bind的过程
// 设置接受缓冲区的大小
int nRecvBuf = 38400 * 1024;
if (setsockopt(s, SOL_SOCKET, SO_RCVBUF, (const char *)&nRecvBuf, sizeof(int)) == -1) {
printf("Set receive buffer size failed\n");
exit(EXIT_FAILURE);
}
// 建立新进程负责写入数据到文件
hThread = CreateThread(NULL, 0, Thread_fcn, NULL, 0, NULL);
// 互斥量保护数据
g_hMutex = CreateMutex(NULL, FALSE, L"Mutex");
// 主线程只负责接受数据
for (;;) {
if ((recvfrom(s, packetData, PACKSIZ, 0, (struct sockaddr *)&si_other, &slen)) == SOCKET_ERROR) {
printf("recvfrom() failed with error code : %d", WSAGetLastError());
exit(EXIT_FAILURE);
}
uint32_t imageno = GetImageNo(packetData); // 获取图像编号
WaitForSingleObject(g_hMutex, INFINITE); // LOCK
// 如果新的一副图像数据的一个包来了,立刻通知另外的线程保存数据
if (imageno != curr_imageno) {
prev_imageno = curr_imageno;
curr_imageno = imageno;
// 如果只使用一个数组,写入文件时新的数据可能会覆盖数组内容,因此使用两个数组,两个指针分别指向它们
// 当写入条件满足时,用另外一个数组用于存放新的数据,此时交换指针即可
swap(ptr_write, ptr_read);
IsNewImageReceived = true; // 置位变量
}
pack_cnt++;
ReleaseMutex(g_hMutex); // UNLOCK
// 把包拼凑起来
uint16_t blockno = GetBlockNo(packetData);
uint32_t offset = (blockno - 1) * BLKSIZ;
memcpy(ptr_read+offset, packetData + 12, BLKSIZ);
}
closesocket(s);
WSACleanup();
return 0;
}
// 线程函数,负责写入数据到文件
DWORD WINAPI Thread_fcn(LPVOID lpParameter)
{
for (;;) {
WaitForSingleObject(g_hMutex, INFINITE); // LOCK
// 不断检查条件是否满足
if (IsNewImageReceived) {
IsNewImageReceived = false; // 清变量
pack_cnt = 0;
// 文件名
stringstream ss;
ss << prev_imageno;
string filename = path + string("image") + ss.str() + string(".raw");
ReleaseMutex(g_hMutex); // UNLOCK
// 写入文件
ofstream fout(filename.c_str(), ofstream::binary);
if (!fout) {
cerr << "Failed to open file!" << endl;
}
fout.write(ptr_write, FILESIZ);
fout.close();
// 将接受区清零
memset(ptr_write, 0, FILESIZ);
} else {
ReleaseMutex(g_hMutex);
}
}
}