C++网络编程之IO多路复用(二)

概述

在上一篇文章中,我们介绍了如何使用select进行IO多路复用。虽然select在很多场景下非常有用,但它存在线性扫描、复制文件描述符集合、不支持边缘触发模式、信号干扰等众多问题。因此,在更高效的IO多路复用方案中,往往会选择poll和epoll。在本篇中,我们将重点介绍poll,下一篇将介绍epoll。

poll

poll是对select的一个改进版本,它不再受固定数量限制的影响,而是采用动态数组来存储待监测的文件描述符列表。此外,poll还能更好地处理不同类型的数据流,比如:普通文件、管道等。

poll函数的接口原型如下。

C++ 复制代码
int poll(struct pollfd *fds, nfds_t nfds, int timeout);

各个参数和返回值的含义如下。

fds: 指向struct pollfd结构体数组的指针,每个元素代表一个要监视的文件描述符及其感兴趣的事件。pollfd的定义如下,其events和revents字段可以包含以下常量的组合:POLLIN(有数据可读)、POLLOUT(可写入数据)、POLLERR(发生错误)、POLLHUP(挂起事件)、POLLNVAL(无效请求)。

C++ 复制代码
struct pollfd
{
    int   fd;         // 文件描述符
    short events;     // 请求的事件
    short revents;    // 发生的事件
};

nfds:fds数组中的元素数量。

timeout:等待的时间上限(毫秒)。如果设置为-1,则无限期等待;如果为0,则立即返回不等待。

返回值:大于0表示准备好执行IO操作的文件描述符的数量;0表示超时;-1表示出错,可以通过errno查看具体的错误码。

注意:poll仍然需要遍历整个文件描述符列表来找出已经就绪的项,性能问题依旧存在。另外,还需要在用户空间与内核空间之间复制pollfd结构体,增加了一定的开销。

实战代码

虽然poll是Linux系统才有的API,但在Windows系统中,我们可以使用WSAPoll函数来实现类似的功能。在下面的示例代码中,我们使用WSAPoll函数实现了TCP服务器的IO多路复用。

首先,我们使用WSAStartup初始化Winsock库,并检查是否成功。接着,创建一个监听套接字,将其设置为非阻塞模式。同时,将其绑定到指定的端口8888,并调用listen开始监听连接请求。

然后,我们使用变量vctFds来存储所有需要监控的套接字,包括:监听套接字、所有已连接的客户端套接字。

紧接着,在无限循环中,我们使用WSAPoll函数来等待IO事件的发生。如果WSAPoll返回新的连接请求,接受新连接,并将新连接的套接字添加到vctFds向量中。对于每个已连接的客户端套接字,检查是否有数据可读。如果有,则读取数据并回显给客户端。如果客户端断开连接或发生错误,则关闭相应的套接字,并从vctFds中移除该套接字。

最后,当程序退出时,关闭所有打开的套接字,并清理Winsock库。

C++ 复制代码
#include <winsock2.h>
#include <iostream>
#include <vector>

using namespace std;

#pragma comment(lib, "ws2_32.lib")

int main()
{
    WSADATA wsaData;
    if (WSAStartup(MAKEWORD(2, 2), &wsaData) != 0)
    {
        cout << "Failed to initialize Winsock" << endl;
        return -1;
    }

    SOCKET listenSock = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
    if (listenSock == INVALID_SOCKET)
    {
        cout << "Create socket failed: " << WSAGetLastError() << endl;
        WSACleanup();
        return -1;
    }

    u_long mode = 1;
    ioctlsocket(listenSock, FIONBIO, &mode);

    sockaddr_in serverAddr;
    memset(&serverAddr, 0, sizeof(serverAddr));
    serverAddr.sin_family = AF_INET;
    serverAddr.sin_port = htons(8888);
    serverAddr.sin_addr.s_addr = htonl(INADDR_ANY);
    if (bind(listenSock, (SOCKADDR *)&serverAddr, sizeof(serverAddr)) == SOCKET_ERROR)
    {
        cout << "Bind failed: " << WSAGetLastError() << endl;
        closesocket(listenSock);
        WSACleanup();
        return -1;
    }

    if (listen(listenSock, SOMAXCONN) == SOCKET_ERROR)
    {
        cout << "Listen failed: " << WSAGetLastError() << endl;
        closesocket(listenSock);
        WSACleanup();
        return -1;
    }

    vector<WSAPOLLFD> vctFds;
    vctFds.push_back({ listenSock, POLLIN, 0 });
    while (true)
    {
        int result = WSAPoll(vctFds.data(), static_cast<ULONG>(vctFds.size()), -1);
        if (result == SOCKET_ERROR)
        {
            cout << "WSAPoll failed: " << WSAGetLastError() << endl;
            break;
        }

        for (size_t i = 0; i < vctFds.size(); ++i)
        {
            if (vctFds[i].revents & POLLIN)
            {
                if (vctFds[i].fd == listenSock) 
                {
                    sockaddr_in clientAddr;
                    int addrLen = sizeof(clientAddr);
                    SOCKET connSock = accept(listenSock, (SOCKADDR *)&clientAddr, &addrLen);
                    if (connSock != INVALID_SOCKET) 
                    {
                        vctFds.push_back({ connSock, POLLIN, 0 });
                        cout << "New connection" << endl;
                    }
                }
                else
                {
                    char buffer[1024] = { 0 };
                    int bytesReceived = recv(vctFds[i].fd, buffer, sizeof(buffer) - 1, 0);
                    if (bytesReceived > 0)
                    {
                        cout << "Message from client: " << buffer << endl;
                        send(vctFds[i].fd, buffer, bytesReceived, 0);
                    }
                    else if (bytesReceived == 0 || (bytesReceived == SOCKET_ERROR && 
                            WSAGetLastError() != WSAEWOULDBLOCK))
                    {
                        cout << "Client disconnected" << endl;
                        closesocket(vctFds[i].fd);
                        vctFds.erase(vctFds.begin() + i);
                        --i;
                    }
                }
            }
            else if (vctFds[i].revents & (POLLERR | POLLHUP | POLLNVAL))
            {
                cout << "Error on fd " << vctFds[i].fd << ": ";
                if (vctFds[i].revents & POLLERR)
                {
                    cout << "POLLERR ";
                }
                if (vctFds[i].revents & POLLHUP)
                {
                    cout << "POLLHUP ";
                }
                if (vctFds[i].revents & POLLNVAL)
                {
                    cout << "POLLNVAL ";
                }
                cout << endl;

                closesocket(vctFds[i].fd);
                vctFds.erase(vctFds.begin() + i);
                --i;
            }
        }
    }

    for (auto &fd : vctFds)
    {
        if (fd.fd != INVALID_SOCKET)
        {
            closesocket(fd.fd);
        }
    }

    closesocket(listenSock);
    WSACleanup();
    return 0;
}
相关推荐
小学生的信奥之路1 分钟前
力扣509题:斐波那契数列的解法与代码注释
c++·算法·leetcode·动态规划·斐波那契数列
无影无踪的青蛙22 分钟前
[C++]洛谷B3626 跳跃机器人(题干 + 详细讲解, BFS练习题)
开发语言·c++·算法·bfs·广度优先
kyle~1 小时前
C/C++---隐式显式转换
c语言·开发语言·c++
四谷夕雨1 小时前
C++八股 —— 手撕定时器
开发语言·c++
supingemail1 小时前
深度剖析 MCP SDK 最新版:Streamable HTTP 模式
网络·网络协议·http
四谷夕雨2 小时前
C++八股 —— 手撕shared_ptr
开发语言·c++
十五年专注C++开发2 小时前
CMake指令:add_library()
开发语言·c++·cmake·自动化编译工具
九州ip动态3 小时前
自媒体运营新利器:账号矩阵+指纹浏览器,解锁流量密码
网络·网络协议·tcp/ip
无垠的广袤3 小时前
【萤火工场GD32VW553-IOT开发板】ADC电压的LabVIEW采集
c++·单片机·嵌入式硬件·物联网·labview
十五年专注C++开发4 小时前
CMake基础:CMakeLists.txt 文件结构和语法
开发语言·c++·cmake·跨平台编译