C/C++ Linux网络编程6 - poll解决客户端并发连接问题

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

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

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

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

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

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

目录

[一. poll系统调用](#一. poll系统调用)

[1.1 poll函数](#1.1 poll函数)

[1.2 struct pollfd结构体](#1.2 struct pollfd结构体)

[二. poll TCP服务器实现](#二. poll TCP服务器实现)

[2.1 run函数](#2.1 run函数)

[2.2 pollTcpSercer.hpp](#2.2 pollTcpSercer.hpp)

[2.3 pollTcpserver.cc](#2.3 pollTcpserver.cc)

[三. 总结和测试](#三. 总结和测试)

[3.1 总结](#3.1 总结)

[3.2 测试](#3.2 测试)

[3.3 性能测试对比表](#3.3 性能测试对比表)


一. poll系统调用

1.1 poll函数

复制代码
#include <poll.h>

//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 <iostream>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <sys/select.h>
#include <cstring>

#include <functional>
#include <thread>

namespace YZC
{
    // 设置默认端口和最大backlog
    const int defaultPort = 8080;
    const int maxBacklog = 128;

    // 设置回调函数
    using func_t = std::function<int(int)>;

    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 = "<h1>hello world</h1>";
        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

cpp 复制代码
 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

cpp 复制代码
#include <iostream>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <poll.h>
#include <cstring>

#include <functional>
#include <thread>

const int fdnums = 100000;

namespace YZC
{
    // 设置默认端口和最大backlog
    const int defaultPort = 8080;
    const int maxBacklog = 128;

    // 设置回调函数
    using func_t = std::function<int(int)>;

    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 = "<h1>hello world</h1>";
        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

cpp 复制代码
#include "tcpServer.hpp"
#include <iostream>
#include <memory>
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<YZC::tcpServer> 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

cpp 复制代码
[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

cpp 复制代码
[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

cpp 复制代码
[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

cpp 复制代码
[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 多进程 10 7,281 73,625 100ms 1.84MB/s 482 482超时 ✅ 正常 原表
1,000 多线程 10 8,650 87,421 126ms 2.19MB/s 73 73超时 ✅ 最佳 原表
1,000 select 10 15,965 160,346 48ms 4.03MB/s 286 286超时 🎯 优异 原表
1,000 poll 10 16,844 170,114 134ms 4.26MB/s 391 391超时 🎯 优异 本次测试
10,000 多进程 10 5,522 55,745 102ms 1.40MB/s 433 123读+310超时 ✅ 正常 原表
10,000 多线程 10 7,375 74,453 194ms 1.86MB/s 353 107读+246超时 ✅ 最佳 原表
10,000 poll 10 14,940 151,096 80ms 3.78MB/s 902 902超时 🎯 优异 本次测试
25,000 多进程 50 1,042 35,604 420ms 270KB/s 10,972 77读+8932写+1963超时 ▲ 高压稳定 原表
25,000 多线程 50 313 24,298 205ms 81KB/s 953 691读+262超时 ▲ 性能衰减 原表
25,000 poll 50 2,476 80,420 173ms 640KB/s 1,136 442读+694超时 ▲ 性能衰减 本次测试
55,555 多进程 100 0 0 0us 0B/s 37,170 37170写错误 ❌ 崩溃 原表
55,555 多线程 100 N/A N/A N/A N/A N/A 测试被终止 ❌ 崩溃 原表
55,555 poll 100 1,514 152,163 402ms 392KB/s 29,912 15515连接+2144读+394写+9859超时 ❌ 严重过载 本次测试

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

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

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

相关推荐
铭哥的编程日记1 小时前
【标准项目】C++基于正倒排索引的Boost搜索引擎
c++·搜索引擎
9***Y481 小时前
Java开发工具IntelliJ IDEA技巧
java·开发语言·intellij-idea
码力码力我爱你1 小时前
C++性能基准测试
开发语言·c++
张人玉1 小时前
C#WPF——MVVM框架编写管理系统所遇到的问题
开发语言·c#·wpf·mvvm框架
zwm_yy2 小时前
服务器检查内存爆满
运维·服务器
摩尔元数2 小时前
2025,服务器通信MES厂商谁主沉浮?
运维·服务器
last demo2 小时前
nfs服务器
linux·运维·服务器·php
java1234_小锋2 小时前
讲讲Mybatis的一级、二级缓存?
java·开发语言·mybatis
翼龙云_cloud2 小时前
阿里云渠道商:自建或RDS怎么迁移到阿里云PolarDB?
运维·服务器·阿里云·云计算