上篇文章:C/C++ Linux网络编程5 - 网络IO模型与select解决客户端并发连接问题

代码仓库:橘子真甜 (yzc-YZC) - Gitee.com

        poll是对select的改进。select的缺点如下:

1 打开的文件描述符fd有限制:为1024

2 有读事件集合,写事件集合和异常事件集合。

3 每一次都要重新设置好需要关心的事件

目录

一. poll系统调用

1.1 poll函数

1.2 struct pollfd结构体

二. poll TCP服务器实现

2.1 run函数

2.2 pollTcpSercer.hpp

2.3 pollTcpserver.cc

三. 总结和测试

3.1 总结

3.2 测试

3.3 性能测试对比表


一. poll系统调用

1.1 poll函数

#include 
//poll:关心事件就绪后就会返回
int poll(struct pollfd *fds, nfds_t nfds, int timeout);
参数说明:
//fds:表示需要关心的事件的集合
//nfds:表示需要监控事件的数量
//timeout:超时时间
返回值:返回事件大于0,说明有返回事件,=0说明超时了,小于0说明发送错误

1.2 struct pollfd结构体

struct pollfd
{
    int   fd;         /* file descriptor */
    short events;     /* requested events */
    short revents;    /* returned events */
};

        包含了就绪事件的fd和该事件的关心方式events,以及返回结果revents

二. poll TCP服务器实现

        首先拿出我们上次的select服务器代码,然后删除select的逻辑。其他部分都是可用的

#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
namespace YZC
{
    // 设置默认端口和最大backlog
    const int defaultPort = 8080;
    const int maxBacklog = 128;
    // 设置回调函数
    using func_t = std::function;
    class tcpServer
    {
    public:
        tcpServer(func_t func, int port = defaultPort)
            : _port(port), _callback(func) {}
        void init()
        {
            // 1.创建socket
            _listensock = socket(AF_INET, SOCK_STREAM, 0);
            if (_listensock < 0)
            {
                std::cerr << "sockte err" << std::endl;
                exit(-1);
            }
            std::cout << "socket success" << std::endl;
            // 2 bind绑定fd和端口
            struct sockaddr_in serveraddr;
            memset(&serveraddr, 0, sizeof(serveraddr));
            // 设置地址的信息(协议,ip,端口)
            serveraddr.sin_family = AF_INET;
            serveraddr.sin_addr.s_addr = htonl(INADDR_ANY); // 绑定任意网卡ip,通常我们访问某一个IP地址是这个服务器的公网网卡IP地址
            serveraddr.sin_port = htons(_port);             // 注意端口16位,2字节需要使用htons。不可
            socklen_t len = sizeof(serveraddr);
            if (bind(_listensock, (struct sockaddr *)&serveraddr, len) < 0)
            {
                std::cerr << "bind err" << std::endl;
                exit(-1);
            }
            std::cout << "bind success" << std::endl;
            // 3 设置sockfd为监听fd
            if (listen(_listensock, maxBacklog) < 0)
            {
                std::cerr << "listen err" << std::endl;
                exit(-1);
            }
            std::cout << "listen success" << std::endl;
        }
        void run()
        {
            //poll逻辑
        }
    private:
        int _listensock;
        int _port;
        func_t _callback;
    };
    int serviceIO(int sockfd)
    {
        // 这里仅做简单的数据收发
        char buffer[128] = {0};
        int count = recv(sockfd, buffer, sizeof(buffer) - 1, 0);
        if (count < 0)
        {
            std::cerr << "recv err" << std::endl;
            exit(-1);
        }
        if (count == 0)
        {
            // 对方关闭
            return 0;
            close(sockfd);
        }
        printf("client --> server:%s\n", buffer);
        send(sockfd, buffer, strlen(buffer), 0);
        return count;
    }
    int serviceHTTP(int sockfd)
    {
        // 这里仅做简单的数据收发
        char buffer[128] = {0};
        int count = recv(sockfd, buffer, sizeof(buffer) - 1, 0);
        if (count < 0)
        {
            std::cerr << "recv err" << std::endl;
            exit(-1);
        }
        if (count == 0)
        {
            // 对方关闭
            return 0;
            close(sockfd);
        }
        printf("client --> server:%s\n", buffer);
        std::string outbuffer;
        std::string body = "

hello world

"; outbuffer = "HTTP/1.1 200 OK\r\n" "Content-Type: text/html; charset=utf-8\r\n" "Content-Length: " + std::to_string(body.size()) + "\r\n" "Server: Apache/2.4.41\r\n" "Date: Mon, 18 Dec 2023 08:32:10 GMT\r\n" "X-Frame-Options: DENY\r\n" "X-Content-Type-Options: nosniff\r\n" "Referrer-Policy: strict-origin-when-cross-origin\r\n" "\r\n" // 空行分隔头部和正文 + body; // 无脑向客户端发送一个简单http响应 send(sockfd, outbuffer.c_str(), outbuffer.size(), 0); return count; } }

主要逻辑就是位于run函数中

2.1 run函数⭐

        poll服务器中run函数的逻辑和select中的逻辑非常类似。

1 创建关心事件集合,并且关心监视事件,创建最大文件描述符maxfd

2 循环等待poll返回就绪事件集合

3 首先处理监听事件,然后线性扫描其他fd并判断该事件是否就绪

3 监听事件新增连接需要关心新事件,其他fd执行相对应的读写方法即可

4 注意如果客户端关闭,需要close fd 然后在事件集合中清空这个fd

 void run()
        {
            // 首先创建pollfd数组
            struct pollfd fds[fdnums] = {0};
            // 注册监视fd到poll关心事件中
            fds[_listensock].fd = _listensock;
            fds[_listensock].events = POLLIN;
            int maxfd = _listensock;
            //
            while (true)
            {
                int n = poll(fds, maxfd + 1, 0);
                if (fds[_listensock].revents & POLLIN)
                {
                    struct sockaddr_in clientaddr;
                    memset(&clientaddr, 0, sizeof(clientaddr));
                    socklen_t len = sizeof(clientaddr);
                    int sockfd = accept(_listensock, (struct sockaddr *)&clientaddr, &len);
                    // 将新的fd增加到rfd中,更新最大fd
                    fds[sockfd].fd = sockfd;
                    fds[sockfd].events = POLLIN;
                    maxfd = sockfd;
                }
                // 注意poll和select一样仍需要遍历所有关心的fd
                for (int i = _listensock + 1; i < maxfd + 1; i++)
                {
                    // 普通读写事件就绪
                    // 处理读事件
                    if (fds[i].revents & POLLIN)
                    {
                        int n = _callback(i);
                        if (n == 0)
                        {
                            // 说明对方关闭,重新处理关心的事件
                            fds[i].fd = -1;
                            fds[i].events = 0;
                            fds[i].revents = 0;
                        }
                    }
                }
            }
        }

2.2 pollTcpSercer.hpp

#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
const int fdnums = 100000;
namespace YZC
{
    // 设置默认端口和最大backlog
    const int defaultPort = 8080;
    const int maxBacklog = 128;
    // 设置回调函数
    using func_t = std::function;
    class tcpServer
    {
    public:
        tcpServer(func_t func, int port = defaultPort)
            : _port(port), _callback(func) {}
        void init()
        {
            // 1.创建socket
            _listensock = socket(AF_INET, SOCK_STREAM, 0);
            if (_listensock < 0)
            {
                std::cerr << "sockte err" << std::endl;
                exit(-1);
            }
            std::cout << "socket success" << std::endl;
            // 2 bind绑定fd和端口
            struct sockaddr_in serveraddr;
            memset(&serveraddr, 0, sizeof(serveraddr));
            // 设置地址的信息(协议,ip,端口)
            serveraddr.sin_family = AF_INET;
            serveraddr.sin_addr.s_addr = htonl(INADDR_ANY); // 绑定任意网卡ip,通常我们访问某一个IP地址是这个服务器的公网网卡IP地址
            serveraddr.sin_port = htons(_port);             // 注意端口16位,2字节需要使用htons。不可
            socklen_t len = sizeof(serveraddr);
            if (bind(_listensock, (struct sockaddr *)&serveraddr, len) < 0)
            {
                std::cerr << "bind err" << std::endl;
                exit(-1);
            }
            std::cout << "bind success" << std::endl;
            // 3 设置sockfd为监听fd
            if (listen(_listensock, maxBacklog) < 0)
            {
                std::cerr << "listen err" << std::endl;
                exit(-1);
            }
            std::cout << "listen success" << std::endl;
        }
        void run()
        {
            // 首先创建pollfd数组
            struct pollfd fds[fdnums] = {0};
            // 注册监视fd到poll关心事件中
            fds[_listensock].fd = _listensock;
            fds[_listensock].events = POLLIN;
            int maxfd = _listensock;
            //
            while (true)
            {
                int n = poll(fds, maxfd + 1, 0);
                if (fds[_listensock].revents & POLLIN)
                {
                    struct sockaddr_in clientaddr;
                    memset(&clientaddr, 0, sizeof(clientaddr));
                    socklen_t len = sizeof(clientaddr);
                    int sockfd = accept(_listensock, (struct sockaddr *)&clientaddr, &len);
                    // 将新的fd增加到rfd中,更新最大fd
                    fds[sockfd].fd = sockfd;
                    fds[sockfd].events = POLLIN;
                    maxfd = sockfd;
                }
                // 注意poll和select一样仍需要遍历所有关心的fd
                for (int i = _listensock + 1; i < maxfd + 1; i++)
                {
                    // 普通读写事件就绪
                    // 处理读事件
                    if (fds[i].revents & POLLIN)
                    {
                        int n = _callback(i);
                        if (n == 0)
                        {
                            // 说明对方关闭,重新处理关心的事件
                            fds[i].fd = -1;
                            fds[i].events = 0;
                            fds[i].revents = 0;
                        }
                    }
                }
            }
        }
    private:
        int _listensock;
        int _port;
        func_t _callback;
    };
    int serviceIO(int sockfd)
    {
        // 这里仅做简单的数据收发
        char buffer[128] = {0};
        int count = recv(sockfd, buffer, sizeof(buffer) - 1, 0);
        if (count < 0)
        {
            std::cerr << "recv err" << std::endl;
            exit(-1);
        }
        if (count == 0)
        {
            // 对方关闭
            return 0;
            close(sockfd);
        }
        printf("client --> server:%s\n", buffer);
        send(sockfd, buffer, strlen(buffer), 0);
        return count;
    }
    int serviceHTTP(int sockfd)
    {
        // 这里仅做简单的数据收发
        char buffer[128] = {0};
        int count = recv(sockfd, buffer, sizeof(buffer) - 1, 0);
        if (count < 0)
        {
            std::cerr << "recv err" << std::endl;
            exit(-1);
        }
        if (count == 0)
        {
            // 对方关闭
            return 0;
            close(sockfd);
        }
        printf("client --> server:%s\n", buffer);
        std::string outbuffer;
        std::string body = "

hello world

"; outbuffer = "HTTP/1.1 200 OK\r\n" "Content-Type: text/html; charset=utf-8\r\n" "Content-Length: " + std::to_string(body.size()) + "\r\n" "Server: Apache/2.4.41\r\n" "Date: Mon, 18 Dec 2023 08:32:10 GMT\r\n" "X-Frame-Options: DENY\r\n" "X-Content-Type-Options: nosniff\r\n" "Referrer-Policy: strict-origin-when-cross-origin\r\n" "\r\n" // 空行分隔头部和正文 + body; // 无脑向客户端发送一个简单http响应 send(sockfd, outbuffer.c_str(), outbuffer.size(), 0); return count; } }

2.3 pollTcpserver.cc

#include "tcpServer.hpp"
#include 
#include 
using namespace std;
// tcp 服务器,启动方式与udp server一样
//./tcpServer + local_port    //我们将本主机的所有ip与端口绑定
static void Usage(string proc)
{
    cout << "\nUsage:\n\t" << proc << " lock_port\n\n";
}
int main(int argc, char *argv[])
{
    if (argc != 2)
    {
        Usage(argv[0]);
    }
    uint16_t serverport = atoi(argv[1]);
    unique_ptr tsvr(new YZC::tcpServer(YZC::serviceIO, serverport));
    tsvr->init();
    tsvr->run();
    return 0;
}

运行结果如下:

三. 总结和测试⭐

3.1 总结

poll是对select的修补和改进,主要点如下:

1 取消了最大fd的限制,有用户自行决定

2 将多种事件集合集中为一个事件集合,编码方便

3 通过结构体控制每一个fd的fd,event,revent。编码清晰

4 无需每一次调用后重新关心所有事件,只需要重新设置关闭的事件

不过poll并没有改变select的其他致命缺点:

每一次都需要将关心的事件拷贝进出内核,频繁的拷贝和系统调用降低性能。

每一次都要线性扫描所有关心的事件,加入有 100w连接,消耗的时间巨大。

3.2 测试

        同理我们使用wrk测试一下相同条件下,poll服务器的QPS如何。首先拿出我们上一篇文章的测试结果用于对比。(云服务器的配置是 2核2G

上篇文章的测试结果如下:

使用wrk分别测试 1000/10000/25000/55555并发连接的QPS。结果如下

1000

[yzc@study wrk]$ ./wrk -c 1000 -d 10s -t 10 http://47.105.37.157:8080
Running 10s test @ http://47.105.37.157:8080
  10 threads and 1000 connections
  Thread Stats   Avg      Stdev     Max   +/- Stdev
    Latency   133.86ms  261.53ms   2.00s    90.31%
    Req/Sec     1.76k     1.49k   14.08k    90.23%
  170114 requests in 10.10s, 42.99MB read
  Socket errors: connect 0, read 0, write 0, timeout 391
Requests/sec:  16843.90
Transfer/sec:      4.26MB

10000

[yzc@study wrk]$ ./wrk -c 10000 -d 10s -t 10 http://47.105.37.157:8081
Running 10s test @ http://47.105.37.157:8081
  10 threads and 10000 connections
  Thread Stats   Avg      Stdev     Max   +/- Stdev
    Latency    79.63ms  204.09ms   2.00s    93.12%
    Req/Sec     1.74k     3.72k   30.87k    88.68%
  151096 requests in 10.11s, 38.19MB read
  Socket errors: connect 0, read 0, write 0, timeout 902
Requests/sec:  14939.97
Transfer/sec:      3.78MB

25000

[yzc@study wrk]$ ./wrk -c 25000 -d 10s -t 50 http://47.105.37.157:8080
Running 10s test @ http://47.105.37.157:8080
  50 threads and 25000 connections
  Thread Stats   Avg      Stdev     Max   +/- Stdev
    Latency   172.55ms  168.40ms   1.99s    92.70%
    Req/Sec   421.93    792.25    13.60k    91.79%
  80420 requests in 32.49s, 20.32MB read
  Socket errors: connect 0, read 442, write 0, timeout 694
Requests/sec:   2475.52
Transfer/sec:    640.64KB

55555

[root@study wrk]# ./wrk -c 55555 -d 10s -t 100 http://47.105.37.157:8080
Running 10s test @ http://47.105.37.157:8080
  100 threads and 55555 connections
  Thread Stats   Avg      Stdev     Max   +/- Stdev
    Latency   402.39ms  404.39ms   2.00s    87.15%
    Req/Sec   204.56    467.00    13.82k    92.72%
  152163 requests in 1.67m, 38.46MB read
  Socket errors: connect 15515, read 2144, write 394, timeout 9859
Requests/sec:   1514.17
Transfer/sec:    391.85KB

制作成表格如下:

3.3 性能测试对比表

并发数架构线程数QPS总请求数平均延迟吞吐量错误数错误类型测试状态数据来源
1,000多进程107,28173,625100ms1.84MB/s482482超时✅ 正常原表
1,000多线程108,65087,421126ms2.19MB/s7373超时✅ 最佳原表
1,000select1015,965160,34648ms4.03MB/s286286超时 优异原表
1,000poll1016,844170,114134ms4.26MB/s391391超时 优异本次测试
10,000多进程105,52255,745102ms1.40MB/s433123读+310超时✅ 正常原表
10,000多线程107,37574,453194ms1.86MB/s353107读+246超时✅ 最佳原表
10,000poll1014,940151,09680ms3.78MB/s902902超时 优异本次测试
25,000多进程501,04235,604420ms270KB/s10,97277读+8932写+1963超时▲ 高压稳定原表
25,000多线程5031324,298205ms81KB/s953691读+262超时▲ 性能衰减原表
25,000poll502,47680,420173ms640KB/s1,136442读+694超时▲ 性能衰减本次测试
55,555多进程100000us0B/s37,17037170写错误❌ 崩溃原表
55,555多线程100N/AN/AN/AN/AN/A测试被终止❌ 崩溃原表
55,555poll1001,514152,163402ms392KB/s29,91215515连接+2144读+394写+9859超时❌ 严重过载本次测试

        可以看到,对比于select。poll可以直接处理更多的fd,而select不修改内核情况下只能处理1024个fd。如果修改内核其实select和poll的效率是差不多的

        对比于多线程/多进程,使用IO多路复用明显无论是效率还是稳定都更有效。

当然还有更高效的epoll,epoll改善了select/poll的两个致命缺陷。epoll是当代Linux服务器的最多选择