解决UDP丢包问题的经验
2016-01-21 11:31
721 查看
月初参与了一个关于图像网络传输的小项目,图像采集端用的FPGA,每秒采集接近20MB数据,将大块的数据分割成一个个包,每个包加上图像编号和包的序号等信息,使用UDP协议发送出去。我的任务是写一个接收端(Windows下),将一个个包拼凑起来,并将二进制的图像数据转换成可视的RGB图像。
这是我第一次用UDP协议做有用的事,书本上阐述的UDP的丢包特性终于成了实实在在的问题摆在我面前。我写的第一个版本的程序,20M的数据丢失了近一半。在解决问题的路上,这篇文章UDP丢包原因给了我很大帮助。
我首先尝试增大接受缓冲区的大小,马上就有效果了,丢包率降低到10%以下。第二个改进是使用多线程。在我的单线程的版本中,当一整幅图像接收完毕后便将数据写入到文件中,这个操作很耗费时间,当有新的数据过来时,程序依然在执行写入操作,来不及接受新过来的数据,因此而造成丢包。我开辟了一个新的线程负责将数据写入到文件,原来的线程只负责接收数据。当接收数据的线程收到一副图像的最后一个包时,马上置位相应变量,另一个线程不断检查该变量,条件满足时将数据写入到文件。其实这种轮询的方式也很耗费CPU资源,最好的办法是用事件(Event)来同步,不过这个版本的结果就很完美了,基本上没有丢失包,我就没有继续改进了。
下面是我的源代码:
这是我第一次用UDP协议做有用的事,书本上阐述的UDP的丢包特性终于成了实实在在的问题摆在我面前。我写的第一个版本的程序,20M的数据丢失了近一半。在解决问题的路上,这篇文章UDP丢包原因给了我很大帮助。
我首先尝试增大接受缓冲区的大小,马上就有效果了,丢包率降低到10%以下。第二个改进是使用多线程。在我的单线程的版本中,当一整幅图像接收完毕后便将数据写入到文件中,这个操作很耗费时间,当有新的数据过来时,程序依然在执行写入操作,来不及接受新过来的数据,因此而造成丢包。我开辟了一个新的线程负责将数据写入到文件,原来的线程只负责接收数据。当接收数据的线程收到一副图像的最后一个包时,马上置位相应变量,另一个线程不断检查该变量,条件满足时将数据写入到文件。其实这种轮询的方式也很耗费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); } } }