UDP数据包接收逻辑的优化修改以及对性能的影响

97 篇文章 7 订阅
30 篇文章 1 订阅

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溢出)的抗性;

增强服务器消耗(从缓冲区中读出数据)的性能,可以从根本上减少读缓冲区溢出的问题。

UDP数据包是否丢失,与一个UDP数据包的大小、服务端接收缓冲区的大小、读出(接收缓冲区数据)的性能密切相关。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值