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

概述

在C++网络编程中,处理并发连接是一个非常关键的核心问题。为了有效管理来自多个客户端的请求,服务器需要能够同时监听多个套接字上的事件,这通常通过IO多路复用来实现。

IO多路复用是一种工作机制,它可以让程序监视多个文件描述符(通常是套接字),等待其中一个或多个文件描述符变为就绪状态。一旦某个文件描述符就绪,即该文件描述符上可以进行无阻塞读写操作,操作系统就会通知应用程序。然后,应用程序就可以对该文件描述符进行相应的读写操作。

使用IO多路复用后,不需要为每个连接创建一个独立的线程,节省了资源。同时,也避免了频繁的上下文切换开销,提高了效率。常见的IO多路复用技术主要有三种,分别为:select、poll、epoll。在本篇中,我们将重点介绍select,后续文章将介绍poll和epoll。

select

select是最古老的IO多路复用API,几乎支持所有类型的Unix系统(包括:Windows、Linux、Mac等)。它的基本工作原理是:用户态进程将一组文件描述符传递给内核,由内核来检查这些文件描述符的状态变化;当调用返回时,会告诉用户哪些文件描述符已经准备好了读写操作。

select函数的接口原型如下。

cpp 复制代码
int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, 
    struct timeval *timeout);

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

nfds:指定要监视的最大文件描述符值加1。

readfds:指向一个集合,该集合包含了想要监视读就绪状态的文件描述符。

writefds:指向一个集合,该集合包含了想要监视写就绪状态的文件描述符。

exceptfds:指向一个集合,该集合包含了想要监视异常条件的文件描述符。

timeout:一个时间结构体,指定了等待超时的时间。如果设置为NULL,则会一直阻塞,直到至少有一个文件描述符准备好。如果设置了具体时间,则在指定时间内没有事件发生将返回0。

返回值:如果返回正数,这意味着至少有一个文件描述符已经准备好执行请求的操作(读、写或异常条件)。返回的具体数值,表示有多少个文件描述符已就绪。如果返回 0,这意味着在指定的超时时间内没有任何文件描述符准备好,并且超时时间已过。如果返回负数,这意味着调用过程中发生了错误。常见的原因包括传入了无效的参数或者系统本身遇到了问题。

在Windows系统和Linux系统下使用select函数时,它们之间存在一些细微的差异,具体如下。

1、头文件和库不同。

Windows:需要包含winsock2.h头文件,且链接到Winsock库ws2_32.lib。

Linux:需要包含sys/select.h头文件,不需要链接额外的库,因为select是标准POSIX库的一部分。

2、类型不同。

Windows:Socket类型为套接字句柄。

Linux:Socket类型为文件描述符,这是所有IO操作的基础。

3、FD_SETSIZE的定义不同。

Windows:默认的FD_SETSIZE是64。

Linux:默认的FD_SETSIZE是1024。

4、错误码不同。

Windows:返回SOCKET_ERROR,表示发生了错误。可通过WSAGetLastError函数获取更具体的错误值,比如:WSAEINTR(被信号中断)、WSAEINVAL(无效参数)等。

Linux:返回-1,表示发生了错误。可通过errno获取更具体的错误值,比如:EINTR(被信号中断)、EINVAL(无效参数)等。

注意:select只能处理有限数量的文件描述符,通常这个数量由上面介绍的FD_SETSIZE定义。当监视大量的文件描述符时,select的性能会显著下降。每次调用select都需要复制所有文件描述符集合到内核空间,然后在内核中进行线性扫描。这种机制对于少量的文件描述符是可行的,但对于大规模并发应用则效率低下。

实战代码

在Windows系统下如何使用select进行IO多路复用,可参考下面的TCP服务器的示例代码。

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

然后,我们使用fd_set类型的变量masterSet来存储所有需要监控的套接字,包括:监听套接字、所有已连接的客户端套接字;workingSet则用于临时存储select检查的结果。

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

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

cpp 复制代码
#include <iostream>
#include <winsock2.h>
#include <vector>
using namespace std;

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

#define TCP_LISTEN_PORT                   8888
#define BUFFER_SIZE                       1024

int main()
{
    // 初始化Winsock库
    WSADATA wsaData;
    int result = WSAStartup(MAKEWORD(2, 2), &wsaData);
    if (result != 0)
    {
        cout << "WSAStartup failed: " << result << endl;
        return 1;
    }

    SOCKET listenSocket = INVALID_SOCKET;
    struct sockaddr_in serverAddr;

    // 创建监听 socket
    listenSocket = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
    if (listenSocket == INVALID_SOCKET)
    {
        cout << "Socket creation failed: " << WSAGetLastError() << endl;
        WSACleanup();
        return 1;
    }

    // 设置服务器地址
    serverAddr.sin_family = AF_INET;
    serverAddr.sin_addr.s_addr = INADDR_ANY;
    serverAddr.sin_port = htons(TCP_LISTEN_PORT);
    if (bind(listenSocket, (struct sockaddr *)&serverAddr, 
        sizeof(serverAddr)) == SOCKET_ERROR)
    {
        cout << "Bind failed: " << WSAGetLastError() << endl;
        closesocket(listenSocket);
        WSACleanup();
        return 1;
    }

    // 开始监听
    if (listen(listenSocket, SOMAXCONN) == SOCKET_ERROR)
    {
        cout << "Listen failed: " << WSAGetLastError() << endl;
        closesocket(listenSocket);
        WSACleanup();
        return 1;
    }

    cout << "Server listening on port " << TCP_LISTEN_PORT << endl;

    fd_set masterSet;
    FD_ZERO(&masterSet);
    FD_SET(listenSocket, &masterSet);

    fd_set workingSet;
    // 存储新连接的套接字句柄
    vector<SOCKET> activeSockets;
    activeSockets.push_back(listenSocket);

    while (true)
    {
        workingSet = masterSet;

        // 调用select
        int selectResult = select(0, &workingSet, NULL, NULL, NULL);
        if (selectResult == SOCKET_ERROR)
        {
            cout << "Select failed: " << WSAGetLastError() << endl;
            break;
        }

        // 检查是否有新的连接请求
        if (FD_ISSET(listenSocket, &workingSet))
        {
            SOCKET clientSocket = accept(listenSocket, NULL, NULL);
            if (clientSocket == INVALID_SOCKET)
            {
                cout << "Accept failed: " << WSAGetLastError() << endl;
                continue;
            }

            cout << "New connection: " << clientSocket << endl;
            FD_SET(clientSocket, &masterSet);
            activeSockets.push_back(clientSocket);
        }

        // 检查其他已连接的客户端是否有数据可读
        for (const auto &sock : activeSockets)
        {
            // 跳过监听Socket
            if (sock == listenSocket)
            {
                continue;
            }

            if (FD_ISSET(sock, &workingSet))
            {
                char buffer[BUFFER_SIZE] = { 0 };
                int bytesReceived = recv(sock, buffer, BUFFER_SIZE, 0);
                if (bytesReceived > 0)
                {
                    cout << "Received from client " << sock << ": " << buffer << endl;
                    // 回显消息给客户端
                    send(sock, buffer, bytesReceived, 0);
                }
                else if (bytesReceived == 0)
                {
                    cout << "Client disconnected: " << sock << endl;
                    closesocket(sock);
                    FD_CLR(sock, &masterSet);

                    // 移除断开的连接
                    auto it = find(activeSockets.begin(), activeSockets.end(), sock);
                    if (it != activeSockets.end())
                    {
                        activeSockets.erase(it);
                    }
                }
                else
                {
                    cout << "Recv failed: " << WSAGetLastError() << endl;
                    closesocket(sock);
                    FD_CLR(sock, &masterSet);

                    // 移除断开的连接
                    auto it = find(activeSockets.begin(), activeSockets.end(), sock);
                    if (it != activeSockets.end())
                    {
                        activeSockets.erase(it);
                    }
                }
            }
        }
    }

    // 释放资源
    for (const auto &sock : activeSockets)
    {
        closesocket(sock);
    }

    closesocket(listenSocket);
    WSACleanup();
    return 0;
}
相关推荐
就爱六点起23 分钟前
C/C++ 中的类型转换方式
c语言·开发语言·c++
宛唐羁客44 分钟前
ODBC连接PostgreSQL数据库后,网卡DOWN后,客户端进程阻塞问题解决方法
网络·数据库
Beekeeper&&P...1 小时前
web钩子什么意思
前端·网络
召木1 小时前
C++小白实习日记——Day 2 TSCNS怎么读取当前时间
c++·职场和发展
St_Ludwig1 小时前
C语言 蓝桥杯某例题解决方案(查找完数)
c语言·c++·后端·算法·游戏·蓝桥杯
Jack黄从零学c++1 小时前
opencv(c++)---自带的卷积运算filter2D以及应用
c++·人工智能·opencv
sweetheart7-72 小时前
LeetCode20. 有效的括号(2024冬季每日一题 11)
c++·算法·力扣··括号匹配
清风.春不晚2 小时前
shell脚本2---清风
网络·网络安全
枫叶丹42 小时前
【在Linux世界中追寻伟大的One Piece】手写序列化与反序列化
linux·运维·网络
gma9992 小时前
brpc 与 Etcd 二次封装
数据库·c++·rpc·etcd