网络IO与IO多路复用

诗林  金牌会员 | 2025-1-21 19:09:14 | 显示全部楼层 | 阅读模式
打印 上一主题 下一主题

主题 724|帖子 724|积分 2172

马上注册,结交更多好友,享用更多功能,让你轻松玩转社区。

您需要 登录 才可以下载或查看,没有账号?立即注册

x
一、网络IO基础


  • 系统对象

    • 网络IO涉及用户空间调用IO的历程或线程以及内核空间的内核系统。
    • 例如,当进行read操作时,会经历两个阶段:

      • 期待数据准备就绪。
      • 将数据从内核拷贝到历程或线程中。


  • 多种网络IO模子的出现缘故起因:由于上述两个阶段的不同环境,出现了多种网络IO模子。
二、阻塞IO(blocking IO)


  • 特点

    • 在Linux中,默认所有socket都是阻塞的。
    • 以读操作为例,当用户历程调用read系统调用,kernel开始IO的第一阶段(准备数据),对于网络IO,数据大概未到,kernel要期待,用户历程会被阻塞。
    • 直到kernel比及数据准备好,将数据从kernel拷贝到用户内存并返回结果,用户历程才解除阻塞状态。
    • 即阻塞IO在期待数据和拷贝数据两个阶段都阻塞历程。
    • 例如,使用listen()、send()、recv()等接口构建服务器/客户机模子,这些接口大多是阻塞型的。
    • 代码示例:

  1. // 创建服务器端的socket
  2. int serverSocket = socket(AF_INET, SOCK_STREAM, 0);
  3. // 绑定地址
  4. struct sockaddr_in serverAddr;
  5. serverAddr.sin_family = AF_INET;
  6. serverAddr.sin_port = htons(8080);
  7. serverAddr.sin_addr.s_addr = INADDR_ANY;
  8. bind(serverSocket, (struct sockaddr*)&serverAddr, sizeof(serverAddr));
  9. // 监听连接
  10. listen(serverSocket, 5);
  11. // 接受连接
  12. int clientSocket = accept(serverSocket, NULL, NULL);
  13. // 接收数据,在此处进程会阻塞,直到接收到数据或出错
  14. char buffer[1024];
  15. recv(clientSocket, buffer, sizeof(buffer), 0);
  16. // 发送数据,在此处进程也可能阻塞
  17. send(clientSocket, buffer, strlen(buffer), 0);
复制代码
  1. - 解释:上述代码首先创建了一个服务器端的socket,然后绑定地址和端口,接着监听连接。当调用`accept`时,如果没有连接请求,会阻塞等待。一旦有连接,调用`recv`接收数据时,会阻塞直到有数据到达。发送数据时也可能阻塞,例如网络拥堵或接收方接收缓冲区满。
复制代码


  • 多线程/多历程改进方案

    • 为解决单个毗连阻塞影响其他毗连的问题,可在服务器端使用多线程或多历程。
    • 多线程可使用pthread_create()创建,多历程使用fork()创建。
    • 多线程开销相对小,适合为较多客户机服务;历程更安全,适合单个服务执行体必要大量CPU资源的环境。
    • 例如,服务器端为多个客户机提供服务时,可在主线程期待毗连请求,有毗连时创建新线程或新历程提供服务。
    • 但当必要同时响应大量(成百上千)毗连请求时,多线程或多历程会严重占用系统资源,导致系统服从降落和假死。

三、非阻塞IO(non-blocking IO)


  • 特点

    • 通过设置socket为非阻塞,如使用fcntl( fd, F_SETFL, O_NONBLOCK );。
    • 当用户历程发出read操作,若kernel数据未准备好,不会阻塞用户历程,而是立刻返回error。
    • 用户历程可根据返回结果判断数据是否准备好,未准备好可再次发送read操作。
    • 当数据准备好,kernel会将数据拷贝到用户内存并返回。
    • 例如,recv()接口在非阻塞状态下调用后立刻返回,返回值有不同含义:

      • recv()返回值大于 0,表示担当数据完毕,返回值是接收到的字节数。
      • recv()返回 0,表示毗连正常断开。
      • recv()返回 -1,且errno即是EAGAIN,表示recv操作未完成。
      • recv()返回 - 1,且errno不即是EAGAIN,表示recv操作碰到系统错误errno。

    • 代码示例:

  1. // 创建服务器端的socket
  2. int serverSocket = socket(AF_INET, SOCK_STREAM, 0);
  3. // 设置为非阻塞
  4. fcntl(serverSocket, F_SETFL, O_NONBLOCK);
  5. // 绑定地址
  6. struct sockaddr_in serverAddr;
  7. serverAddr.sin_family = AF_INET;
  8. serverAddr.sin_port = htons(8080);
  9. serverAddr.sin_addr.s_addr = INADDR_ANY;
  10. bind(serverSocket, (struct sockaddr*)&serverAddr, sizeof(serverAddr));
  11. // 监听连接
  12. listen(serverSocket, 5);
  13. // 接受连接
  14. int clientSocket = accept(serverSocket, NULL, NULL);
  15. // 接收数据,此处不会阻塞,会根据返回值判断状态
  16. char buffer[1024];
  17. int recvResult;
  18. while ((recvResult = recv(clientSocket, buffer, sizeof(buffer), 0)) == -1 && errno == EAGAIN) {
  19.     // 数据未准备好,可进行其他操作或再次尝试接收
  20. }
  21. if (recvResult > 0) {
  22.     // 处理接收到的数据
  23. } else if (recvResult == 0) {
  24.     // 连接断开
  25. } else {
  26.     // 其他错误
  27. }
复制代码
  1. - 解释:在这个示例中,将服务器端socket设置为非阻塞后,调用`recv`时不会阻塞进程。如果`recv`返回-1且`errno`为`EAGAIN`,说明数据还未准备好,程序可继续执行其他操作或过段时间再尝试接收,而不是像阻塞IO那样一直等待。
复制代码
四、多路复用IO(IO multiplexing)


  • 特点

    • 又称变乱驱动IO(event driven IO),如select/epoll。
    • 单个历程可同时处置惩罚多个网络毗连的IO,基本原理是select/epoll函数不断轮询所负责的所有socket,当某个socket有数据到达,关照用户历程。
    • 流程:用户历程调用select会被阻塞,kernel监视select负责的socket,当有socket数据准备好,select返回,用户历程再调用read操作将数据从kernel拷贝到用户历程。
    • 固然使用select必要两个系统调用(select和read),但上风是可在一个线程内处置惩罚多个socket的IO请求。
    • 代码示例:

  1. #include <stdio.h>
  2. #include <sys/select.h>
  3. #include <sys/socket.h>
  4. #include <netinet/in.h>
  5. #include <unistd.h>
  6. int main() {
  7.     int serverSocket = socket(AF_INET, SOCK_STREAM, 0);
  8.     struct sockaddr_in serverAddr;
  9.     serverAddr.sin_family = AF_INET;
  10.     serverAddr.sin_port = htons(8080);
  11.     serverAddr.sin_addr.s_addr = INADDR_ANY;
  12.     bind(serverSocket, (struct sockaddr*)&serverAddr, sizeof(serverAddr));
  13.     listen(serverSocket, 5);
  14.     fd_set readfds;
  15.     FD_ZERO(&readfds);
  16.     FD_SET(serverSocket, &readfds);
  17.     int maxFd = serverSocket;
  18.     while (1) {
  19.         fd_set tempFds = readfds;
  20.         int activity = select(maxFd + 1, &tempFds, NULL, NULL, NULL);
  21.         if (FD_ISSET(serverSocket, &tempFds)) {
  22.             int clientSocket = accept(serverSocket, NULL, NULL);
  23.             FD_SET(clientSocket, &readfds);
  24.             if (clientSocket > maxFd) {
  25.                 maxFd = clientSocket;
  26.             }
  27.         }
  28.         for (int i = 0; i <= maxFd; i++) {
  29.             if (FD_ISSET(i, &tempFds) && i!= serverSocket) {
  30.                 char buffer[1024];
  31.                 int valread = recv(i, buffer, 1024, 0);
  32.                 if (valread == 0) {
  33.                     close(i);
  34.                     FD_CLR(i, &readfds);
  35.                 } else {
  36.                     // 处理接收到的数据
  37.                 }
  38.             }
  39.         }
  40.     }
  41.     return 0;
  42. }
复制代码
  1. - 解释:首先创建服务器socket,绑定并监听。`FD_ZERO`和`FD_SET`用于初始化和设置文件描述符集合。`select`函数会阻塞,等待集合中文件描述符的可读事件。当`serverSocket`有新连接时,添加新连接的socket到集合,并更新最大文件描述符。当其他socket可读时,接收数据并处理。
复制代码


  • 接口原型

    • FD_ZERO(int fd, fd_set* fds):初始化fd_set。
    • FD_SET(int fd, fd_set* fds):将句柄添加到fd_set。
    • FD_ISSET(int fd, fd_set* fds):查抄句柄是否在fd_set中。
    • FD_CLR(int fd, fd_set* fds):从fd_set中移除句柄。
    • int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout):用于探测多个文件句柄的状态变化,readfds、writefds和exceptfds作为输入和输出参数,可设置超时时间。

  • 问题

    • 当必要探测的句柄值较大时,select()接口本身必要大量时间轮询,很多操作系统提供更高效接口如epoll(Linux)、kqueue(BSD)、/dev/poll(Solaris)等。
    • 该模子将变乱探测和变乱响应混合,若变乱响应执行体庞大,会降低变乱探测的实时性。
    • 可使用变乱驱动库如libevent、libev库解决上述问题。

五、异步IO(Asynchronous I/O)


  • 特点

    • Linux下的异步IO主要用于磁盘IO读写操作,从内核2.6版本开始引入。
    • 用户历程发起read操作后可做其他事,kernel收到asynchronous read后立刻返回,不会阻塞用户历程。
    • kernel期待数据准备完成,将数据拷贝到用户内存,完成后给用户历程发送信号关照。
    • 代码示例(使用aio_read):

  1. #include <aio.h>
  2. #include <stdio.h>
  3. #include <stdlib.h>
  4. #include <unistd.h>
  5. #include <fcntl.h>
  6. void aio_completion_handler(union sigval sigval) {
  7.     struct aiocb *req = (struct aiocb *)sigval.sival_ptr;
  8.     if (aio_error(req) == 0) {
  9.         char *buffer = (char *)malloc(aio_return(req));
  10.         // 处理读取到的数据
  11.         free(buffer);
  12.     }
  13.     aio_destroy(req);
  14. }
  15. int main() {
  16.     int fd = open("test.txt", O_RDONLY);
  17.     if (fd < 0) {
  18.         perror("open");
  19.         return 1;
  20.     }
  21.     struct aiocb my_aiocb;
  22.     memset(&my_aiocb, 0, sizeof(struct aiocb));
  23.     my_aiocb.aio_fildes = fd;
  24.     my_aiocb.aio_buf = malloc(1024);
  25.     my_aiocb.aio_nbytes = 1024;
  26.     my_aiocb.aio_offset = 0;
  27.     my_aiocb.aio_sigevent.sigev_notify = SIGEV_THREAD;
  28.     my_aiocb.aaiocb.sigev_notify_function = aio_completion_handler;
  29.     my_aiocb.aio_sigevent.sigev_notify_attributes = NULL;
  30.     my_aiocb.aio_sigevent.sigev_value.sival_ptr = &my_aiocb;
  31.     if (aio_read(&my_aiocb) < 0) {
  32.         perror("aio_read");
  33.         close(fd);
  34.         return 1;
  35.     }
  36.     // 进程可做其他事情
  37.     sleep(1);
  38.     return 0;
  39. }
复制代码
  1. - 解释:上述代码使用`aio_read`进行异步读操作。首先打开文件,设置`aiocb`结构(包含文件描述符、缓冲区、字节数等),并设置信号通知方式和处理函数。调用`aio_read`后,进程可以继续做其他事情,当读操作完成,会调用`aio_completion_handler`处理结果。
复制代码


  • 重要性:异步IO是真正非阻塞的,对高并发网络服务器实现至关重要。
六、信号驱动IO(signal driven I/O, SIGIO)


  • 特点

    • 允许套接口进行信号驱动I/O并安装信号处置惩罚函数,历程继续运行不阻塞。
    • 当数据准备好,历程收到SIGIO信号,可在信号处置惩罚函数中调用I/O操作函数处置惩罚数据。
    • 上风在于期待数据报到达期间,历程可继续执行,制止了select的阻塞与轮询。

七、服务器模子Reactor与Proactor


  • Reactor模子

    • 是一种变乱驱动机制,用于同步I/O。
    • 应用步伐将处置惩罚I/O变乱的接口注册到Reactor上,若相应变乱发生,Reactor调用注册的接口(回调函数)。
    • 三个重要组件:

      • 多路复用器:如select、poll、epoll等系统调用。
      • 变乱分发器:将多路复用器返回的就绪变乱分到对应的处置惩罚函数。
      • 变乱处置惩罚器:负责处置惩罚特定变乱的处置惩罚函数。

    • 具体流程:

      • 注册读就绪变乱和相应变乱处置惩罚器。
      • 变乱分离器期待变乱。
      • 变乱到来,激活分离器,分离器调用变乱对应的处置惩罚器。
      • 变乱处置惩罚器完成实际的读操作,处置惩罚数据,注册新变乱,返还控制权。

    • 长处:响应快,编程相对简朴,可扩展性和可复用性好。
    • 缺点:当步伐必要使用多核资源时会有局限,因为通常是单线程的。
    • 代码示例:

  1. #include <iostream>
  2. #include <sys/epoll.h>
  3. #include <sys/socket.h>
  4. #include <netinet/in.h>
  5. #include <unistd.h>
  6. #include <fcntl.h>
  7. #include <string.h>
  8. class EventHandler {
  9. public:
  10.     virtual void handleEvent(int fd) = 0;
  11. };
  12. class Reactor {
  13. private:
  14.     int epollFd;
  15.     std::vector<EventHandler*> handlers;
  16. public:
  17.     Reactor() {
  18.         epollFd = epoll_create1(0);
  19.     }
  20.     ~Reactor() {
  21.         close(epollFd);
  22.     }
  23.     void registerHandler(int fd, EventHandler* handler) {
  24.         struct epoll_event event;
  25.         event.data.fd = fd;
  26.         event.events = EPOLLIN;
  27.         epoll_ctl(epollFd, EPOLL_CTL_ADD, fd, &event);
  28.         handlers.push_back(handler);
  29.     }
  30.     void handleEvents() {
  31.         struct epoll_event events[10];
  32.         int numEvents = epoll_wait(epollFd, events, 10, -1);
  33.         for (int i = 0; i < numEvents; i++) {
  34.             int fd = events[i].data.fd;
  35.             for (EventHandler* handler : handlers) {
  36.                 handler->handleEvent(fd);
  37.             }
  38.         }
  39.     }
  40. };
  41. class EchoHandler : public EventHandler {
  42. public:
  43.     void handleEvent(int fd) override {
  44.         char buffer[1024];
  45.         int bytesRead = recv(fd, buffer, sizeof(buffer), 0);
  46.         if (bytesRead > 0) {
  47.             send(fd, buffer, bytesRead, 0);
  48.         }
  49.     }
  50. };
  51. int main() {
  52.     Reactor reactor;
  53.     int serverSocket = socket(AF_INET, SOCK_STREAM, 0);
  54.     struct sockaddr_in serverAddr;
  55.     serverAddr.sin_family = AF_INET;
  56.     serverAddr.sin_port = htons(8080);
  57.     serverAddr.sin_addr.s_addr = INADDR_ANY;
  58.     bind(serverSocket, (struct sockaddr*)&serverAddr, sizeof(serverAddr));
  59.     listen(serverSocket, 5);
  60.     EchoHandler handler;
  61.     reactor.registerHandler(serverSocket, &handler);
  62.     while (1) {
  63.         reactor.handleEvents();
  64.     }
  65.     return 0;
  66. }
复制代码
  1. - 解释:上述C++代码中,`Reactor`类负责创建`epoll`,注册和处理事件。`EventHandler`是抽象基类,`EchoHandler`是具体处理接收和发送的派生类。`main`函数创建服务器socket,注册`EchoHandler`到`Reactor`,并不断调用`handleEvents`处理事件。
复制代码


  • Proactor模子

    • 最大特点是使用异步I/O,所有I/O操作都交由系统的异步I/O接口执行,工作线程只负责业务逻辑。
    • 具体流程:

      • 处置惩罚器发起异步操作并关注I/O完成变乱。
      • 变乱分离器期待操作完成变乱。
      • 分离器期待时,内核并行执行实际I/O操作并将结果存入用户缓冲区,关照分离器读操作完成。
      • I/O完成后,通过变乱分离器召唤处置惩罚器。
      • 变乱处置惩罚器处置惩罚用户缓冲区中的数据。

    • 增加了编程复杂度,但给工作线程带来更高服从,可使用系统态的读写优化。
    • 在Windows上常用IOCP支持高并发,Linux上因aio性能不佳,主要以Reactor模子为主。
    • 也可使用Reactor模拟Proactor,但在读写并行本领上会有区别。

八、同步I/O和异步I/O的区别总结


  • 阻塞与非阻塞IO的区别

    • 调用阻塞IO会阻塞历程直到操作完成,非阻塞IO在kernel准备数据时会立刻返回。

  • 同步与异步IO的区别

    • 同步IO在做“IO operation”(如read系统调用)时会阻塞历程。阻塞IO、非阻塞IO、多路复用IO都属于同步IO。
    • 非阻塞IO在数据准备好时的拷贝数据阶段会阻塞历程;而异步IO在整个过程中,历程不会被阻塞,历程发起IO操作后直接做其他事,直到kernel发送信号关照完成。


免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。
回复

使用道具 举报

0 个回复

倒序浏览

快速回复

您需要登录后才可以回帖 登录 or 立即注册

本版积分规则

诗林

金牌会员
这个人很懒什么都没写!
快速回复 返回顶部 返回列表