UDP数据包接收逻辑的优化修改以及对性能的影响
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <errno.h>
#include <string.h>
#include <netdb.h>
#include <signal.h>
#include <arpa/inet.h>
#include <sys/types.h>
#include <sys/socket.h>
static int totalMsg = 0;
void sigINT(int dwsigno)
{
printf("totalMsg: %d\n", totalMsg);
exit(0);
}
int openServer()
{
struct addrinfo hints;
memset(&hints, 0, sizeof(hints));
hints.ai_flags = AI_PASSIVE;
hints.ai_socktype = SOCK_DGRAM;
struct addrinfo *res;
static char *port = "4020";
int e = getaddrinfo(NULL, port, &hints, &res);
if (e == EAI_SYSTEM)
{
printf("openServer: getaddrinfo error=%d(%s)!!!\n", errno, strerror(errno));
return -1;
}
else if (e != 0)
{
printf("openServer: getaddrinfo error=%s!!!\n", gai_strerror(e));
return -1;
}
int fd = socket(res->ai_family, res->ai_socktype, res->ai_protocol);
if (fd < 0)
{
printf("openServer: create socket error=%d(%s)!!!\n", errno, strerror(errno));
freeaddrinfo(res);
return -1;
}
int rcvBufSize = 131071;
socklen_t optlen = sizeof(rcvBufSize);
if (setsockopt(fd, SOL_SOCKET, SO_RCVBUF, &rcvBufSize, optlen) < 0)
{
printf("openServer: setsockopt error=%d(%s)!!!\n", errno, strerror(errno));
freeaddrinfo(res);
return -1;
}
int rcvRealSize = -1;
if (getsockopt(fd, SOL_SOCKET, SO_RCVBUF, &rcvRealSize, &optlen) < 0)
{
printf("openServer: getsockopt error=%d(%s)!!!\n", errno, strerror(errno));
freeaddrinfo(res);
return -1;
}
printf("recrive-buff-size: %d\n", rcvRealSize);
if (bind(fd, (struct sockaddr *)res->ai_addr, res->ai_addrlen) < 0)
{
printf("openServer: bind error=%d(%s)!!!\n", errno, strerror(errno));
freeaddrinfo(res);
return -1;
}
printf("create udp socket(%d) ok!\n", fd);
return fd;
}
void monUdpSock(int udpSock)
{
static fd_set fds;
FD_ZERO(&fds);
FD_SET(udpSock, &fds);
static struct timeval tv = {0, 20000};
int readyNum = select(udpSock+1, &fds, NULL, NULL, &tv);
if (readyNum < 0)
{
printf("monUdpSock: select error=%d(%s)!!!\n", errno, strerror(errno));
// 异常处理
return;
}
else if (readyNum == 0)
return; // select超时,do nothing
else
; // 存在可读写fd
if (!FD_ISSET(udpSock, &fds))
return;
static char udpMsg[1024*64]; // 64KB
int rbytes = read(udpSock, udpMsg, sizeof(udpMsg));
if (rbytes <= 0)
return;
// 处理收到的Udp消息
totalMsg++;
}
int main()
{
if (signal(SIGINT,sigINT) == SIG_ERR)
{
printf("set single handler error!\n");
exit(1);
}
int udpSock = openServer();
while (1)
{
monUdpSock(udpSock);
usleep(3); // 做其他工作,占用了3ms
}
}
如上所示是一个UDP服务端程序,用以接收UDP数据包并统计接收总量。
这个UDP Server设置自己的UDP服务Socket接收缓冲区为128K,实际在内核占用的缓冲区为256K。
在主循环中的睡眠,是为了模拟程序做其他的工作,每次耗时3ms。
性能统计小脚本:
#!/bin/bash
while [ true ]; do
sleep 1
netstat -an | grep $1
done
脚本功能是每秒中去netstat查看/列出指定UDP Socket的接收缓冲区队列中滞留的,尚未被read到用户态的UDP数据量。
模拟压力测试,每个UDP数据包为1800字节左右:
1)1000caps,总量100000个UDP数据包:
脚本输出部分如下:
udp 0 0 0.0.0.0:4020 0.0.0.0:*
udp 0 0 0.0.0.0:4020 0.0.0.0:*
udp 0 0 0.0.0.0:4020 0.0.0.0:*
udp 0 0 0.0.0.0:4020 0.0.0.0:*
udp 0 0 0.0.0.0:4020 0.0.0.0:*
udp 0 0 0.0.0.0:4020 0.0.0.0:*
udp 0 0 0.0.0.0:4020 0.0.0.0:*
udp 0 0 0.0.0.0:4020 0.0.0.0:*
udp 4304 0 0.0.0.0:4020 0.0.0.0:*
udp 4304 0 0.0.0.0:4020 0.0.0.0:*
udp 0 0 0.0.0.0:4020 0.0.0.0:*
udp 2152 0 0.0.0.0:4020 0.0.0.0:*
udp 2152 0 0.0.0.0:4020 0.0.0.0:*
udp 0 0 0.0.0.0:4020 0.0.0.0:*
udp 2152 0 0.0.0.0:4020 0.0.0.0:*
udp 0 0 0.0.0.0:4020 0.0.0.0:*
udp 2152 0 0.0.0.0:4020 0.0.0.0:*
udp 4304 0 0.0.0.0:4020 0.0.0.0:*
udp 0 0 0.0.0.0:4020 0.0.0.0:*
udp 0 0 0.0.0.0:4020 0.0.0.0:*
udp 0 0 0.0.0.0:4020 0.0.0.0:*
udp 4304 0 0.0.0.0:4020 0.0.0.0:*
udp 4304 0 0.0.0.0:4020 0.0.0.0:*
udp 2152 0 0.0.0.0:4020 0.0.0.0:*
输出表明:
程序执行时,接收缓冲区存在很多数据来不及收,虽然没有太大的堆积量,也没有到接收缓冲区上限256K,但是UDP服务器并不能及时的将数据取出来(即:UDP服务器还是有问题的!!!)。
[jiang@localhost svr]$ ./main
recrive-buff-size: 262142
create udp socket(3) ok!
^CtotalMsg: 100000
实际UDP服务端收到了100000个UDP数据包,并未发生丢包。
但未丢包不代表万事大吉,只能说明,UDP服务器的处理速度(从内核读缓冲区中取数据的速度)和模拟端的写速度达到了一个相对平衡的状态。
此时如果增加一倍的caps呢?
2)2000caps,总量200000个UDP数据包:
脚本输出部分如下:
[jiang@localhost pm]$ ./netstat.sh 4020
udp 0 0 0.0.0.0:4020 0.0.0.0:*
udp 0 0 0.0.0.0:4020 0.0.0.0:*
udp 0 0 0.0.0.0:4020 0.0.0.0:*
udp 0 0 0.0.0.0:4020 0.0.0.0:*
udp 0 0 0.0.0.0:4020 0.0.0.0:*
udp 0 0 0.0.0.0:4020 0.0.0.0:*
udp 17216 0 0.0.0.0:4020 0.0.0.0:*
udp 4304 0 0.0.0.0:4020 0.0.0.0:*
udp 8608 0 0.0.0.0:4020 0.0.0.0:*
udp 15064 0 0.0.0.0:4020 0.0.0.0:*
udp 4304 0 0.0.0.0:4020 0.0.0.0:*
udp 15064 0 0.0.0.0:4020 0.0.0.0:*
udp 8608 0 0.0.0.0:4020 0.0.0.0:*
udp 15064 0 0.0.0.0:4020 0.0.0.0:*
udp 8608 0 0.0.0.0:4020 0.0.0.0:*
udp 4304 0 0.0.0.0:4020 0.0.0.0:*
udp 6456 0 0.0.0.0:4020 0.0.0.0:*
udp 8608 0 0.0.0.0:4020 0.0.0.0:*
udp 262544 0 0.0.0.0:4020 0.0.0.0:*
udp 258240 0 0.0.0.0:4020 0.0.0.0:*
udp 262544 0 0.0.0.0:4020 0.0.0.0:*
udp 260392 0 0.0.0.0:4020 0.0.0.0:*
udp 260392 0 0.0.0.0:4020 0.0.0.0:*
udp 262544 0 0.0.0.0:4020 0.0.0.0:*
输出表明:
程序运行时,由于服务端程序从内核读缓冲区中read慢了(读出速度小于写入速度),导致读缓冲区中数据堆积,UDP接收缓冲区溢出,出现丢包的情况。
[jiang@localhost svr]$ ./main
recrive-buff-size: 262142
create udp socket(3) ok!
^CtotalMsg: 170327
我模拟发送20万个UDP数据包,实际只收到170327个,丢了近3w个数据包。
我们的主循环,除了接收UDP数据包,还会干别的事情(3ms)。
最理想状态下,假如CPU时间片切换、信号唤醒等立刻完成(不耗用时间),睡眠3ms,在2000caps的情况下,意味着这3ms中将会有6个UDP数据包一定堆积。
假设一个UDP数据包100K,也就是说,从3ms开始的时刻计算,到3ms结束的时刻,共计有600K的数据堆积在缓冲区中,得不到处理。
实际情况更为复杂,堆积的数据只会比理想状态下多,绝不会比其少。
仔细观察UDP数据包接收逻辑:
static struct timeval tv = {0, 20000};
int readyNum = select(udpSock+1, &fds, NULL, NULL, &tv);
if (readyNum < 0)
{
printf("monUdpSock: select error=%d(%s)!!!\n", errno, strerror(errno));
// 异常处理
return;
}
else if (readyNum == 0)
return; // select超时,do nothing
else
; // 存在可读写fd
if (!FD_ISSET(udpSock, &fds))
return;
static char udpMsg[1024*64]; // 64KB
int rbytes = read(udpSock, udpMsg, sizeof(udpMsg));
if (rbytes <= 0)
return;
// 处理收到的Udp消息
totalMsg++;
由于每时每刻都有数据到来,所以select并不会真的等待20ms,而是立刻返回。
即,主循环在不停歇地干两件事情:
1)从udp server sock fd中读取【一个】UDP数据包;
2)干其他事情,花费3ms;
换言之,我们可以认为至少每3ms这个周期中,才会去接收**一个**UDP数据包。
实际上,在select指明有数据到来时,我们可以不断地读取接收缓冲区中的数据包,直到缓冲区再无可读取的数据包。
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <errno.h>
#include <string.h>
#include <fcntl.h>
#include <netdb.h>
#include <signal.h>
#include <arpa/inet.h>
#include <sys/types.h>
#include <sys/socket.h>
static int totalMsg = 0;
void sigINT(int dwsigno)
{
printf("totalMsg: %d\n", totalMsg);
exit(0);
}
int openServer()
{
struct addrinfo hints;
memset(&hints, 0, sizeof(hints));
hints.ai_flags = AI_PASSIVE;
hints.ai_socktype = SOCK_DGRAM;
struct addrinfo *res;
static char *port = "4020";
int e = getaddrinfo(NULL, port, &hints, &res);
if (e == EAI_SYSTEM)
{
printf("openServer: getaddrinfo error=%d(%s)!!!\n", errno, strerror(errno));
return -1;
}
else if (e != 0)
{
printf("openServer: getaddrinfo error=%s!!!\n", gai_strerror(e));
return -1;
}
int fd = socket(res->ai_family, res->ai_socktype, res->ai_protocol);
if (fd < 0)
{
printf("openServer: create socket error=%d(%s)!!!\n", errno, strerror(errno));
freeaddrinfo(res);
return -1;
}
int rcvBufSize = 131071;
socklen_t optlen = sizeof(rcvBufSize);
if (setsockopt(fd, SOL_SOCKET, SO_RCVBUF, &rcvBufSize, optlen) < 0)
{
printf("openServer: setsockopt error=%d(%s)!!!\n", errno, strerror(errno));
freeaddrinfo(res);
return -1;
}
int rcvRealSize = -1;
if (getsockopt(fd, SOL_SOCKET, SO_RCVBUF, &rcvRealSize, &optlen) < 0)
{
printf("openServer: getsockopt error=%d(%s)!!!\n", errno, strerror(errno));
freeaddrinfo(res);
return -1;
}
printf("recrive-buff-size: %d\n", rcvRealSize);
if (bind(fd, (struct sockaddr *)res->ai_addr, res->ai_addrlen) < 0)
{
printf("openServer: bind error=%d(%s)!!!\n", errno, strerror(errno));
freeaddrinfo(res);
return -1;
}
freeaddrinfo(res);
// 设置UDP服务Socket为非阻塞模式
int sockflag;
if ((sockflag = fcntl(fd, F_GETFL, 0)) < 0)
{
printf("openServer: get socket(%d) flag error=%d(%s)", fd, errno, strerror(errno));
return -1;
}
sockflag = sockflag | O_NONBLOCK;
if (fcntl(fd, F_SETFL, sockflag) < 0)
{
printf("openServer: set socket(%d) flag error=%d(%s)", fd, errno, strerror(errno));
return -1;
}
printf("create udp socket(%d) ok!\n", fd);
return fd;
}
void monUdpSock(int udpSock)
{
static fd_set fds;
FD_ZERO(&fds);
FD_SET(udpSock, &fds);
static struct timeval tv = {0, 20000};
int readyNum = select(udpSock+1, &fds, NULL, NULL, &tv);
if (readyNum < 0)
{
printf("monUdpSock: select error=%d(%s)!!!\n", errno, strerror(errno));
// 异常处理
return;
}
else if (readyNum == 0)
return; // select超时,do nothing
else
; // 存在可读写fd
if (!FD_ISSET(udpSock, &fds))
return;
static char udpMsg[1024*64]; // 64KB
int rbytes = read(udpSock, udpMsg, sizeof(udpMsg));
while (rbytes > 0)
{
// 处理收到的Udp消息
totalMsg++;
rbytes = read(udpSock, udpMsg, sizeof(udpMsg));
}
if (rbytes < 0 &&
errno != EAGAIN &&
errno != EWOULDBLOCK &&
errno != EINTR &&
errno != EINVAL)
{
printf("monUdpSock: read error=%d(%s)!!!\n", errno, strerror(errno));
return;
}
}
int main()
{
if (signal(SIGINT,sigINT) == SIG_ERR)
{
printf("set single handler error!\n");
exit(1);
}
int udpSock = openServer();
while (1)
{
monUdpSock(udpSock);
usleep(3); // 做其他工作,占用了3ms
}
}
注意:
除了在monUdpSock中不断read直到read返回非正值退出的逻辑改动之外,在openServer时,还将服务端的UDP Socket设置为非阻塞模式。
如果不是非阻塞模式会有什么后果呢?
如果read不发生错误,monUdpSock永远不会结束,即使没有UDP数据到来。
由于sock是阻塞型的,读不到就一直挂着自己(read挂起),这个线程再也没办法做别的事情(例如usleep(3))。
如果设置为非阻塞模式,read不到数据并不会真的把自己(执行线程)挂起来,而是立刻返回-1,设置errno为EAGAIN。
我们的核心思想就是:
用read从内核接收缓冲区中尝试收一个UDP数据包,收的到,就继续尝试再收下一个……直到收不到返回。
新版的UDP服务端程序压测如下:
UDP服务端Socket接收缓冲区256KB(同上),每个消息1800B,3000caps:
udp 0 0 0.0.0.0:4020 0.0.0.0:*
udp 0 0 0.0.0.0:4020 0.0.0.0:*
udp 0 0 0.0.0.0:4020 0.0.0.0:*
udp 0 0 0.0.0.0:4020 0.0.0.0:*
udp 0 0 0.0.0.0:4020 0.0.0.0:*
udp 0 0 0.0.0.0:4020 0.0.0.0:*
udp 0 0 0.0.0.0:4020 0.0.0.0:*
udp 0 0 0.0.0.0:4020 0.0.0.0:*
udp 0 0 0.0.0.0:4020 0.0.0.0:*
udp 0 0 0.0.0.0:4020 0.0.0.0:*
udp 10760 0 0.0.0.0:4020 0.0.0.0:*
udp 0 0 0.0.0.0:4020 0.0.0.0:*
udp 12912 0 0.0.0.0:4020 0.0.0.0:*
udp 0 0 0.0.0.0:4020 0.0.0.0:*
udp 0 0 0.0.0.0:4020 0.0.0.0:*
略微有点堆积,但是也是正常的,那个时刻可能由于CPU调度线程没得到时间片,没来得及收。
[jiang@localhost svr]$ ./main
recrive-buff-size: 262142
create udp socket(3) ok!
^CtotalMsg: 30000
3000cap毫无压力!
最高压测:
UDP服务端Socket接收缓冲区256KB(同上),每个消息1800B,30000caps(不是3000,是3wcaps):
[jiang@localhost pm]$ ./netstat.sh 4020
udp 0 0 0.0.0.0:4020 0.0.0.0:*
udp 0 0 0.0.0.0:4020 0.0.0.0:*
udp 0 0 0.0.0.0:4020 0.0.0.0:*
udp 0 0 0.0.0.0:4020 0.0.0.0:*
udp 0 0 0.0.0.0:4020 0.0.0.0:*
udp 0 0 0.0.0.0:4020 0.0.0.0:*
udp 122664 0 0.0.0.0:4020 0.0.0.0:*
udp 126968 0 0.0.0.0:4020 0.0.0.0:*
udp 0 0 0.0.0.0:4020 0.0.0.0:*
udp 0 0 0.0.0.0:4020 0.0.0.0:*
此次测试是30000个包/秒,数据量也就是:30000*1800B=51MB/秒。
接收缓冲区略微有点堆积也应该是系统调度的原因。
测试共计5次,30000caps无丢包现象。
UDP Socket接收缓冲区在256KB,30000caps,1800B/消息,不会出接收缓冲区满发生丢包。
但是此时再增加量到40000caps,则总出现少许的丢包现象。
其实可以通过增大内核读缓冲区,支持更大的数据量。
通过setsockopt修改读缓冲区大小为1MB,测试100000caps(10wcaps),1800B/消息:
udp 294824 0 0.0.0.0:4020 0.0.0.0:*
udp 0 0 0.0.0.0:4020 0.0.0.0:*
udp 294824 0 0.0.0.0:4020 0.0.0.0:*
udp 299128 0 0.0.0.0:4020 0.0.0.0:*
udp 294824 0 0.0.0.0:4020 0.0.0.0:*
udp 0 0 0.0.0.0:4020 0.0.0.0:*
udp 0 0 0.0.0.0:4020 0.0.0.0:*
udp 290520 0 0.0.0.0:4020 0.0.0.0:*
udp 355080 0 0.0.0.0:4020 0.0.0.0:*
udp 0 0 0.0.0.0:4020 0.0.0.0:*
udp 288368 0 0.0.0.0:4020 0.0.0.0:*
udp 0 0 0.0.0.0:4020 0.0.0.0:*
udp 0 0 0.0.0.0:4020 0.0.0.0:*
udp 0 0 0.0.0.0:4020 0.0.0.0:*
udp 0 0 0.0.0.0:4020 0.0.0.0:*
udp 0 0 0.0.0.0:4020 0.0.0.0:*
udp 458376 0 0.0.0.0:4020 0.0.0.0:*
udp 393816 0 0.0.0.0:4020 0.0.0.0:*
udp 0 0 0.0.0.0:4020 0.0.0.0:*
udp 0 0 0.0.0.0:4020 0.0.0.0:*
udp 0 0 0.0.0.0:4020 0.0.0.0:*
udp 0 0 0.0.0.0:4020 0.0.0.0:*
udp 292672 0 0.0.0.0:4020 0.0.0.0:*
udp 294824 0 0.0.0.0:4020 0.0.0.0:*
udp 314192 0 0.0.0.0:4020 0.0.0.0:*
udp 0 0 0.0.0.0:4020 0.0.0.0:*
udp 0 0 0.0.0.0:4020 0.0.0.0:*
udp 294824 0 0.0.0.0:4020 0.0.0.0:*
udp 0 0 0.0.0.0:4020 0.0.0.0:*
堆积量挺多,不过还远未接近1MB上限,并不会发生丢包。
[jiang@localhost pm]$ ./main 10000000 100000
totalUdp: 10000000
maxRate: 100000
udp data len: 1806
loaded 1806 Bytes Data
finish 10000000 in 100.001, rate: 99999
[jiang@localhost svr]$ ./main
recrive-buff-size: 2097152
create udp socket(3) ok!
^CtotalMsg: 10000000
1000万个UDP包,一个也没有丢失,全部收到。
所谓的缓冲区,除了提供UDP数据包的数据之外,还有一个功能就是容错,容时差。
UDP数据包可以在接收缓冲区中堆积,但一定是由于某种原因暂时的堆积,例如时间片切换,可以堆积部分包,到下次线程轮到时间片时全部处理。
如果服务端程序从接收缓冲区读出的速度小于写入速度,那即使缓冲区设置再大也没有用,终将会堆满接收缓冲区并溢出丢包。
增大接收缓冲区,可以提高由于某些原因导致服务端“抖动”(间隔不规律的读出)而导致接收缓冲区满(UDP溢出)的抗性;
增强服务器消耗(从缓冲区中读出数据)的性能,可以从根本上减少读缓冲区溢出的问题。