C++网络编程之异步IO

概述

        在网络编程中,IO操作是主要的性能瓶颈之一。传统的阻塞IO和非阻塞IO虽然各有优势,但在高并发和高性能要求的场景下,它们都有各自的局限性。异步IO(即AIO,Asynchronous IO)提供了一种更高效的方式来处理IO操作,特别是在需要同时处理大量连接的情况下。

工作原理

        异步IO允许应用程序发起一个IO请求后,立即返回控制权给调用者,而不需要等待IO操作完成。操作系统会在后台处理IO操作,并在完成后通过某种方式通知应用程序。异步IO的工作流程主要有以下四个步骤。

        1、发起请求。应用程序向操作系统发起一个IO请求,比如:读取或写入数据。

        2、立即返回。操作系统接收请求后,立即返回控制权给应用程序,应用程序可以继续执行其他任务。

        3、后台处理。操作系统在后台处理IO请求,与应用程序互不影响。

        4、完成通知。IO操作完成后,操作系统通过回调函数或事件通知机制告知应用程序。

        那么,如何实现异步IO呢?主要有以下三种方式,下面分别进行介绍。

        1、Windows AIO。基于Windows API的异步IO接口(重叠IO和完成端口),适用于Windows系统。

        2、POSIX AIO。基于POSIX标准的异步IO接口,适用于Linux系统(包括Unix系统)。

        3、第三方库。比如:Boost.Asio、libuv等,提供了跨平台的异步IO支持和统一的接口调用,使用者不用关心底层是如何实现的。

Windows AIO

        在Windows系统中,异步IO通常通过完成端口来实现。IOCP(IO Completion Ports)是Windows提供的一种高级异步IO机制,它允许应用程序有效地管理大量并发IO请求。IOCP的主要优点是:可以处理大量的客户端连接,并且能够与线程池很好地集成。

        IOCP的主要接口有两个,分别为:CreateIoCompletionPort、GetQueuedCompletionStatus。

        1、CreateIoCompletionPort函数:是Windows系统中用于创建或访问IO完成端口的函数。它允许我们将文件句柄、套接字等与一个IO完成端口关联起来,并在IO操作完成时接收通知。其函数原型如下。

HANDLE CreateIoCompletionPort(HANDLE FileHandle, HANDLE ExistingCompletionPort,
    ULONG_PTR CompletionKey, DWORD NumberOfConcurrentThreads);

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

        FileHandle:关联到IO完成端口的文件句柄(比如:套接字句柄)。如果为INVALID_HANDLE_VALUE,则创建一个新的IO完成端口。如果ExistingCompletionPort为NULL,那么FileHandle必须为INVALID_HANDLE_VALUE。如果ExistingCompletionPort不为 NULL,那么FileHandle可以是任何有效的文件句柄。

        ExistingCompletionPort:现有的IO完成端口句柄。如果为NULL,则创建一个新的IO定成端口。如果FileHandle为INVALID_HANDLE_VALUE,那么ExistingCompletionPort必须为NULL。如果FileHandle有效,那么ExistingCompletionPort可以是现有的IO完成端口句柄。

        CompletionKey:与文件句柄关联的应用程序定义的值。这个值会在GetQueuedCompletionStatus调用时返回,可用来区分不同的文件句柄。如果FileHandle为INVALID_HANDLE_VALUE,那么CompletionKey将被忽略。如果FileHandle有效,那么CompletionKey将与该文件句柄关联。

        NumberOfConcurrentThreads:建议的最大并发线程数,只在创建新的IO完成端口时有效。这个值告诉系统应该有多少个线程同时处理IO完成通知,如果为0,则系统会根据需要自动调整线程数。

        返回值:成功时,返回新创建的IO完成端口的句柄,或者现有的IO完成端口的句柄。失败时,返回NULL。

        2、GetQueuedCompletionStatus函数:用于从IO完成端口获取完成包。这个函数通常在工作线程中调用,以便处理异步IO操作的结果或手动发布的完成包。其函数原型如下。

BOOL GetQueuedCompletionStatus(HANDLE CompletionPort, 
    LPDWORD lpNumberOfBytesTransferred, PULONG_PTR lpCompletionKey,
    LPOVERLAPPED *lpOverlapped, DWORD dwMilliseconds);

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

        CompletionPort:指定要从中获取完成包的IO完成端口句柄。这个句柄必须是由CreateIoCompletionPort创建的。

        lpNumberOfBytesTransferred:指向一个变量的指针,该变量接收与完成包关联的字节数。如果不需要这个信息,可以传递NULL。

        lpCompletionKey:指向一个变量的指针,该变量接收与完成包关联的应用程序定义的值。如果不需要这个信息,可以传递NULL。

        lpOverlapped:指向OVERLAPPED结构的指针的指针。如果完成包是由于重叠IO操作完成而产生的,那么这个参数会接收到相应的OVERLAPPED结构的地址。如果完成包是通过PostQueuedCompletionStatus发布的,并且提供了OVERLAPPED结构,那么这个参数也会接收到该结构的地址。如果不需要这个信息,可以传递NULL。

        dwMilliseconds:指定等待完成包的时间,以毫秒为单位。如果为INFINITE,则表示无限期等待,直到有完成包可用。如果为0,则表示立即返回,不等待。如果大于0,则表示等待指定的毫秒数。

        返回值:成功时,返回TRUE,并且完成包的信息会被填充到提供的指针所指向的变量中。失败时,返回FALSE。可以通过调用GetLastError获取错误代码,如果为WAIT_TIMEOUT,表示在指定的时间内没有完成包可用。

POSIX AIO

        POSIX AIO是基于POSIX标准的异步IO接口,并不是所有系统都完全支持,某些Linux发行版可能默认不启用AIO支持。虽然POSIX AIO提供了异步IO的能力,但在某些特定情况下,其性能可能不如其他异步IO机制(比如:epoll、kqueue等)。

        POSIX AIO的主要接口包括以下几个函数和结构体。

        aio_read和aio_write函数:发起异步读取和写入请求。这些函数接受一个struct aiocb结构体作为参数,该结构体包含了IO操作的所有相关信息,其定义如下。

struct aiocb
{
    int aio_fildes;                   // 文件描述符
    off_t aio_offset;                 // 偏移量
    volatile void *aio_buf;           // 缓冲区指针
    size_t aio_nbytes;                // 传输的字节数
    int aio_reqprio;                  // 请求优先级
    struct sigevent aio_sigevent;     // 通知机制
    int aio_lio_opcode;               // 仅在lio_listio中使用
};

        aio_error函数:用于查询异步IO请求的状态。如果请求已完成,则返回0。如果仍在进行中,则返回EINPROGRESS。如果发生错误,则返回相应的错误码。

        aio_return函数:获取异步IO请求的结果。通常与aio_error函数一起使用,在确认请求完成后,获取实际传输的字节数。

        lio_listio函数:批量发起多个异步IO请求。可同时提交多个读写操作,并在所有操作完成后,一次性通知应用程序。

        sigevent:用于指定异步IO操作完成后的通知机制。可以选择信号、线程或自定义回调函数等方式来接收通知,具体如下。

        (1)SIGEV_NONE:不使用任何通知。

        (2)SIGEV_SIGNAL:发送一个信号给进程。

        (3)SIGEV_THREAD:创建一个新的线程来执行回调函数。

        (4)SIGEV_THREAD_ID:在指定的线程中执行回调函数。

        下面,我们给出使用POSIX AIO进行异步读取的一段示例代码,方便大家理解上面的函数。

#include <iostream>
#include <aio.h>
#include <fcntl.h>
#include <unistd.h>
#include <cstring>
#include <signal.h>

using namespace std;

// 回调函数
static void aio_completion_handler(sigval_t sigval)
{
    struct aiocb *cb = (struct aiocb *)sigval.sival_ptr;
    if (aio_error(cb) == 0)
    {
        ssize_t bytes_read = aio_return(cb);
        cout << "Read " << bytes_read << " bytes: " << 
            string((char *)cb->aio_buf, bytes_read) << endl;
    }
    else
    {
        perror("aio_error");
    }
}

int main()
{
    int fd = open("hope_wisdom.txt", O_RDONLY);
    if (fd < 0)
    {
        perror("open");
        return -1;
    }

    char buffer[1024];
    struct aiocb aio_cb;
    memset(&aio_cb, 0, sizeof(aio_cb));

    // 设置aiocb结构体
    aio_cb.aio_fildes = fd;
    aio_cb.aio_buf = (void *)buffer;
    aio_cb.aio_nbytes = sizeof(buffer);
    aio_cb.aio_offset = 0;
    aio_cb.aio_sigevent.sigev_notify = SIGEV_THREAD;
    aio_cb.aio_sigevent.sigev_notify_function = aio_completion_handler;
    aio_cb.aio_sigevent.sigev_value.sival_ptr = &aio_cb;

    // 发起异步读取请求
    if (aio_read(&aio_cb) < 0)
    {
        perror("aio_read");
        close(fd);
        return -1;
    }

    // 等待异步IO完成
    while (aio_error(&aio_cb) == EINPROGRESS)
    {
        // 这里只是为了演示
        sleep(1);
    }

    close(fd);
    return 0;
}

猜你喜欢

转载自blog.csdn.net/hope_wisdom/article/details/143449736