【网络编程下】五种网络IO模型

目录

前言

一.I/O基本概念

1.同步和异步

2.阻塞和非阻塞

二.五种网络I/O模型

1.阻塞I/O模型

2.非阻塞式I/O模型

​编辑

3.多路复用

4.信号驱动式I/O模型

5. 异步I/O模型

三.五种I/O模型比较​编辑

六.I/O代码示例

1. 阻塞IO

2.非阻塞I/O

 3.多路复用

(1)select

(2)poll

(3)epoll

(4)select,poll和epoll各自优缺点

select

poll

epoll

4.信号驱动式I/O

5. 异步I/O



前言

本篇文章主要从网络IO角度讲解IO模型,着重讲解多路复用在网络编程上的的应用

一.I/O基本概念

IO 是 Input/Output 的缩写,指的是输入和输出。在计算机当中,IO 操作通常指将数据从一个设备或文件中读取到计算机内存中,或将内存中的数据写入设备或文件中。这些设备可以包括硬盘驱动器、网卡、键盘、屏幕等。

通常用户进程中的一个完整I/O分为两个阶段: 用户进程空间→内核空间 内核空间→设备空间 I/O分为内存I/O、网络I/O和磁盘I/O三种

Linux中进程无法直接操作I/O设备,其必须通过系统调用请求内核来协助完成I/O操作。 内核会为每个I/O设备维护一个缓冲区。 对于一个输入操作来说,进程I/O系统调用后,内核会先看缓冲区中有没有相应的缓存数据,没有的话再到设备(比如网卡设备)中读取(因为设备I/O一般速度较慢,需要等待); 内核缓冲区有数据则直接复制到用户进程空间。 所以,对于一个网络输入操作通常包括两个不同阶段:

        (1)等待网络数据到达网卡,把数据从网卡读取到内核缓冲区,准备好数据。

        (2)从内核缓冲区复制数据到用户进程空间。

网络I/O的本质是对socket的读取,socket在Linux系统中被抽象为流,I/O可以理解为对流的操作。 网络I/O的模型可分为两种:

  • 异步I/O(asynchronous I/O)

  • 同步I/O(synchronous I/O)

同步I/O又包括

  • 阻塞I/O(blocking I/O)

  • 非阻塞I/O(non-blocking I/O)

  • 多路复用I/O(multiplexing I/O)

  • 信号驱动I/O(signal-driven I/O)

强调一下:信号驱动I/O属于同步I/O。 信号驱动I/O和异步I/O只作概念性的讲解,不作为学习重点。

1.同步和异步

(1)对于一个线程的请求调用来讲,同步和异步的区别在于是否要等这个请求出最终结果

(2)对于多个线程而言,同步或异步就是线程间的步调是否要一致、是否要协调

(3)同步也经常用在一个线程内先后两个函数的调用上

(4)异步就是一个请求返回时一定不知道结果,还得通过其他机制来获知结果,如:主动轮询或被动通知

2.阻塞和非阻塞

阻塞与非阻塞与等待消息通知时的状态(调用线程)有关

阻塞和同步是完全不同的概念。同步是对于消息的通知机制而言,阻塞是针对等待消息通知时的状态来说的

进程从创建、运行到结束总是处于下面五个状态之一:新建状态、就绪状态、运行状态、阻塞状态及死亡状态

二.五种网络I/O模型

1.阻塞I/O模型

        对于一个套接字上的输入操作,第一步通常涉及等待数据从网络中到达,当所有等待分组到达时,它被复制到内核中的某个缓冲区。第二步是把数据从内核缓冲区复制到应用程序缓冲区。 同步阻塞I/O模型是最常用、最简单的模型。在Linux中,默认情况下,所有套接字都是阻塞的。下面我们以阻塞套接字的recvfrom的调用图来说明阻塞,如图所示  

2.非阻塞式I/O模型

        非阻塞的recvform系统调用之后,进程并没有被阻塞,内核马上返回给进程,如果数据还没准备好,此时会返回一个errorEAGAINEWOULDBLOCK)。 进程在返回之后,可以先处理其他的业务逻辑,稍后再发起recvform系统调用。 采用轮询的方式检查内核数据,直到数据准备好。再拷贝数据到进程,进行数据处理。 在Linux下,可以通过设置套接字选项使其变为非阻塞。非阻塞的套接字的recvfrom操作如图所示  

可以看到前三次调用recvfrom请求时,并没有数据返回,内核返回errno(EWOULDBLOCK),并不会阻塞进程。 当第四次调用recvfrom时,数据已经准备好了,于是将它从内核空间拷贝到程序空间,处理数据。但是将数据从内核拷贝到用户空间,这个阶段阻塞。  

3.多路复用

        I/O多路复用的好处在于单个进程就可以同时处理多个网络连接的I/O。它的基本原理是不再由应用程序自己监视连接,而由内核替应用程序监视文件描述符。通过 select、poll、epoll 等机制,允许一个进程同时监视多个文件描述符,当某个文件描述符就绪时再进行 IO 操作。这种模型下,程序可以同时处理多个连接,提高了并发处理能力。 以select函数为例,当用户进程调用了select,那么整个进程会被阻塞,而同时,kernel会“监视”所有select负责的socket,当任何一个socket中的数据准备好,select就会返回。 这个时候用户进程再调用read操作,将数据从内核拷贝到用户进程,如下图所示。  

4.信号驱动式I/O模型

        该模型允许socket进行信号驱动I/O,并注册一个信号处理函数,进程继续运行并不阻塞。当数据准备好时,进程会收到一个SIGIO信号,可以在信号处理函数中调用I/O操作函数处理数据,如图所示  

        注意:虽然信号驱动IO在注册完信号处理函数以后,就可以做其他事情了。但是第二阶段拷贝数据的过程当中进程依然是被阻塞的,而后要介绍的异步IO是完全不会阻塞进程的,所以信号驱动虽然具有异步的特点,但依然属于同步IO 。

5. 异步I/O模型

        相对于同步I/O,异步I/O不是按顺序执行。用户进程进行aio_read系统调用之后,就可以去处理其他逻辑了,无论内核数据是否准备好,都会直接返回给用户进程,不会对进程造成阻塞。这是因为aio_read只向内核递交申请,并不关心有没有数据。 等到数据准备好了,内核直接复制数据到进程空间,然后内核向进程发送通知,此时数据已经在用户空间了,可以对数据进行处理。  

 

三.五种I/O模型比较

前四种I/O模型都是同步I/O操作,它们的区别在于第一阶段,而第二阶段是一样的:在数据从内核复制到应用缓冲区期间(用户空间),进程阻塞于recvfrom调用。 相反,异步I/O模型在等待数据和接收数据的这两个阶段都是非阻塞的,可以处理其他的逻辑,用户进程将整个I/O操作交由内核完成,内核完成后会发送通知。在此期间,用户进程不需要检查I/O操作的状态,也不需要主动拷贝数据。

六.I/O代码示例

1. 阻塞IO

        通常情况下在 linux 中,Socket 在创建时会默认采用阻塞式 IO。这意味着当调用 Socket 的接收数据或发送数据函数时,如果没有数据可用或者无法立即发送数据,程序会被阻塞,直到数据准备好或者可以发送数据为止。阻塞IO一个简单的改进方案是在服务器端使用多线程(或多进程)。多线程(或多进程)的目的是让每个连接都拥有独立的线程(或进程),这样任何一个连接的阻塞都不会影响其他的连接。具体使用多进程还是多线程,并没有一个特定的模式。传统意义上,进程的开销要远远大于线程,所以如果需要同时为较多的客户机提供服务,则不推荐使用多进程;如果单个服务执行体需要消耗较多的 CPU 资源,譬如需要进行大规模或长时间的数据运算或文件访问,则进程较为安全。

server.c  服务器端代码

#include <stdio.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <stdlib.h>
#include <arpa/inet.h>
#include <unistd.h>#define PORT 5001
#define BACKLOG 5int main(int argc, char *argv[])
{int fd, newfd;char buf[BUFSIZ] = {}; //BUFSIZ 8142struct sockaddr_in addr;//配置ipv4的地址族结构体/*创建套接字*/fd = socket(AF_INET, SOCK_STREAM, 0);if(fd < 0){perror("socket");exit(0);}addr.sin_family = AF_INET;addr.sin_port = htons(PORT);//主机转网络addr.sin_addr.s_addr = 0;//自己的ip地址/*绑定通信结构体*/if(bind(fd, (struct sockaddr *)&addr, sizeof(addr) ) == -1){perror("bind");exit(0);}/*设置套接字为监听模式*/if(listen(fd, BACKLOG) == -1){perror("listen");exit(0);}/*接受客户端的连接请求,生成新的用于和客户端通信的套接字*/newfd = accept(fd, NULL, NULL);if(newfd < 0){perror("accept");exit(0);}printf("BUFSIZ = %d\n", BUFSIZ);read(newfd, buf, BUFSIZ);printf("buf = %s\n", buf);close(fd);return 0;
}

client.c 客户端代码

#include <stdio.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <stdlib.h>
#include <arpa/inet.h>
#include <unistd.h>#define PORT 5001
#define BACKLOG 5
#define STR "Hello World!"int main(int argc, char *argv[])
{int fd;struct sockaddr_in addr;/*创建套接字*/fd = socket(AF_INET, SOCK_STREAM, 0);if(fd < 0){perror("socket");exit(0);}addr.sin_family = AF_INET;addr.sin_port = htons(PORT);addr.sin_addr.s_addr = inet_addr("127.0.0.1");//要访问的ip,这个ip是本机/*向服务端发起连接请求*/if(connect(fd, (struct sockaddr *)&addr, sizeof(addr) ) == -1){perror("connect");exit(0);}write(fd, STR, sizeof(STR) );printf("STR = %s\n", STR);close(fd);return 0;
}

2.非阻塞I/O

服务器端代码

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/socket.h>
#include <netinet/in.h>#define PORT 8080
#define BUFFER_SIZE 1024int main() 
{int server_fd, new_socket;struct sockaddr_in address;int addrlen = sizeof(address);char buffer[BUFFER_SIZE] = {0};const char *response = "Hello from server";// 创建 TCP 套接字if ((server_fd = socket(AF_INET, SOCK_STREAM, 0)) == 0) {perror("socket failed");exit(EXIT_FAILURE);}// 设置服务器地址和端口address.sin_family = AF_INET;address.sin_addr.s_addr = INADDR_ANY;address.sin_port = htons(PORT);// 将套接字设置为非阻塞模式if (fcntl(server_fd, F_SETFL, O_NONBLOCK) < 0) {perror("fcntl failed");exit(EXIT_FAILURE);}// 将套接字绑定到服务器地址和端口if (bind(server_fd, (struct sockaddr *)&address, sizeof(address)) < 0) {perror("bind failed");exit(EXIT_FAILURE);}// 监听连接if (listen(server_fd, 3) < 0) {perror("listen failed");exit(EXIT_FAILURE);}printf("Server started. Waiting for connections...\n");while (1) {// 非阻塞地等待并接受客户端连接if ((new_socket = accept(server_fd, (struct sockaddr *)&address, (socklen_t*)&addrlen)) >= 0) {// 设置新连接的套接字为非阻塞模式if (fcntl(new_socket, F_SETFL, O_NONBLOCK) < 0) {perror("fcntl failed");exit(EXIT_FAILURE);}printf("New client connected\n");}// 从客户端接收数据int bytes_received;while ((bytes_received = recv(new_socket, buffer, BUFFER_SIZE, 0)) > 0) {printf("Received: %s\n", buffer);// 发送响应给客户端send(new_socket, response, strlen(response), 0);// 清空缓冲区memset(buffer, 0, BUFFER_SIZE);}if (bytes_received == 0) {printf("Client disconnected\n");close(new_socket);}}close(server_fd);return 0;
}

客户端代码

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>#define SERVER_IP "127.0.0.1"
#define PORT 8080
#define BUFFER_SIZE 1024int main() 
{int sock = 0;struct sockaddr_in serv_addr;char buffer[BUFFER_SIZE] = {0};const char *message = "Hello from client";// 创建 TCP 套接字if ((sock = socket(AF_INET, SOCK_STREAM, 0)) < 0) {perror("socket creation failed");exit(EXIT_FAILURE);}// 设置服务器地址和端口serv_addr.sin_family = AF_INET;serv_addr.sin_port = htons(PORT);// 将服务器 IP 地址转换为网络字节序并设置到 sockaddr_in 结构体中if(inet_pton(AF_INET, SERVER_IP, &serv_addr.sin_addr) <= 0) {perror("invalid address");exit(EXIT_FAILURE);}// 连接到服务器if (connect(sock, (struct sockaddr *)&serv_addr, sizeof(serv_addr)) < 0) {perror("connection failed");exit(EXIT_FAILURE);}printf("Connected to server\n");// 发送数据给服务器send(sock, message, strlen(message), 0);printf("Message sent to server: %s\n", message);// 接收服务器的响应int bytes_received = recv(sock, buffer, BUFFER_SIZE, 0);if (bytes_received > 0) {printf("Response from server: %s\n", buffer);} else if (bytes_received == 0) {printf("Server disconnected\n");} else {perror("recv failed");}close(sock);return 0;
}

 3.多路复用

(1)select

select:select 使用了一个 fd_set 集合来保存需要监控的文件描述符,并提供了 select() 函数来检查这些文件描述符的状态。当调用 select() 函数时,内核会遍历这个 fd_set 集合,检查每个文件描述符的状态是否就绪。如果某个文件描述符就绪,select() 函数就会返回,否则会阻塞程序直到有文件描述符就绪或超时。

客户端操作服务器时就会产生这三种文件描述符(简称fd):writefds(写)、readfds(读)、和 exceptfds(异常)。select 会阻塞住监视 3 类文件描述符,等有数据、可读、可写、出异常或超时就会返回;返回后通过遍历 fdset 整个数组来找到就绪的描述符 fd,然后进行对应的 IO 操作。

优点:能够在多个平台上使用,是标准的 POSIX 调用。
适用于小规模连接,文件描述符数量不大的情况。
缺点:监视的文件描述符数量有最大限制,通常为1024,增加数量会降低性能。
需要复制大量的句柄数据结构,可能产生大量开销。
返回的数组中包含所有就绪的句柄,需要遍历整个数组才能找到发生事件的句柄。
触发方式是水平触发,如果未完成I/O操作,每次调用都会通知文件描述符就绪。
内核实现使用轮询方法,性能有限。

select函数

int select(int nfds,  fd_set * readfds,  fd_set * writefds,     fd_set *exceptfds, struct timeval *timeout);

nfds: 是三个集合中编号最高的文件描述符,加上 1

readfds/writefds/exceptfds: 可读集合/可写集合/异常集合

timeout:

         NULL:永久阻塞

         0:非阻塞模式

fd_set结构体

编程流程

Ⅰ.准备文件描述符集合

在使用 select() 函数之前,需要准备三个文件描述符集合,分别是读文件描述符集合、写文件描述符集合和异常文件描述符集合。可以使用 fd_set 类型的变量来表示这些集合,并使用 FD_ZERO() 宏将其初始化为空集。


Ⅱ.设置需要监视的文件描述符

将需要监视的文件描述符添加到相应的文件描述符集合中,可以使用 FD_SET() 宏将文件描述符添加到集合中。

Ⅲ.调用 select() 函数调用 select() 函数来监视文件描述符的状态变化,函数返回时,返回值表示就绪文件描述符的数量,如果出现错误则返回 -1。

​​​​​​​Ⅳ.检查就绪文件描述符在 select() 函数返回后,可以使用 FD_ISSET() 宏来检查具体哪些文件描述符已经就绪。

Ⅴ.处理就绪文件描述符根据 FD_ISSET() 的返回值来判断哪些文件描述符已经就绪,然后进行相应的操作,比如读取数据、发送数据等。
Ⅵ.重复以上步骤可以循环调用 select() 函数来重复监视文件描述符的状态变化,实现长时间的事件驱动循环。

代码:

 net.h

#ifndef _NET_H_
#define _NET_H_#include <stdio.h>
#include <stdlib.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <netinet/tcp.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <strings.h>
#include <errno.h>typedef struct sockaddr Addr;
typedef struct sockaddr_in Addr_in;
#define BACKLOG 5
#define ErrExit(msg) do { perror(msg); exit(EXIT_FAILURE); } while(0)void Argment(int argc, char *argv[]);
int CreateSocket(char *argv[]);
int DataHandle(int fd);#endif

 socket.c

#include "net.h"void Argment(int argc, char *argv[]){if(argc < 3){fprintf(stderr, "%s<addr><port>\n", argv[0]);exit(0);}
}
int CreateSocket(char *argv[]){/*创建套接字*/int fd = socket(AF_INET, SOCK_STREAM, 0);if(fd < 0)ErrExit("socket");/*允许地址快速重用*/int flag = 1;if( setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, &flag, sizeof(flag) ) )perror("setsockopt");/*设置通信结构体*/Addr_in addr;bzero(&addr, sizeof(addr) );addr.sin_family = AF_INET;addr.sin_port = htons( atoi(argv[2]) );/*绑定通信结构体*/if( bind(fd, (Addr *)&addr, sizeof(Addr_in) ) )ErrExit("bind");/*设置套接字为监听模式*/if( listen(fd, BACKLOG) )ErrExit("listen");return fd;
}
int DataHandle(int fd){char buf[BUFSIZ] = {};Addr_in peeraddr;socklen_t peerlen = sizeof(Addr_in);if( getpeername(fd, (Addr *)&peeraddr, &peerlen) )perror("getpeername");int ret = recv(fd, buf, BUFSIZ, 0);if(ret < 0)perror("recv");if(ret > 0){printf("[%s:%d]data: %s\n", inet_ntoa(peeraddr.sin_addr), ntohs(peeraddr.sin_port), buf);}return ret;
}

 server.c

#include "net.h"
#include <sys/select.h>
#define MAX_SOCK_FD 1024int main(int argc, char *argv[])
{int i, ret, fd, newfd;fd_set set, tmpset;Addr_in clientaddr;socklen_t clientlen = sizeof(Addr_in);/*检查参数,小于3个 直接退出进程*/Argment(argc, argv);/*创建已设置监听模式的套接字*/fd = CreateSocket(argv);FD_ZERO(&set);FD_ZERO(&tmpset);FD_SET(fd, &set);while(1){tmpset = set;if( (ret = select(MAX_SOCK_FD, &tmpset, NULL, NULL, NULL)) < 0)ErrExit("select");if(FD_ISSET(fd, &tmpset) ){/*接收客户端连接,并生成新的文件描述符*/if( (newfd = accept(fd, (Addr *)&clientaddr, &clientlen) ) < 0)perror("accept");printf("[%s:%d]已建立连接\n", inet_ntoa(clientaddr.sin_addr), ntohs(clientaddr.sin_port));FD_SET(newfd, &set);}else{ //处理客户端数据for(i = fd + 1; i < MAX_SOCK_FD; i++){if(FD_ISSET(i, &tmpset)){if( DataHandle(i) <= 0){if( getpeername(i, (Addr *)&clientaddr, &clientlen) )perror("getpeername");printf("[%s:%d]断开连接\n", inet_ntoa(clientaddr.sin_addr), ntohs(clientaddr.sin_port));FD_CLR(i, &set);}}}}}return 0;
}


(2)poll

基本原理与 select 一致,也是轮询+遍历。唯一的区别就是 poll 没有最大文件描述符限制(使用链表的方式存储 fd)。


优点:相对于 select,没有监视文件数量限制,使用链表保存文件描述符。
缺点:仍然需要复制大量的数据结构。
需要遍历整个链表来找到就绪的文件描述符。

int poll(struct pollfd *fds, nfds_t nfds, int timeout);

struct pollfd {

        int   fd;             /* 文件描述符 */

        short events;    /* 请求的事件 */

        short revents;   /* 返回的事件 */

};

timeout:

        设置阻塞的时间(毫秒)

        0为非阻塞

        负数表示永久阻塞

使用流程:

Ⅰ准备 pollfd 数组在使用 poll() 函数之前,需要准备一个 struct pollfd 类型的数组,数组中的每个元素代表一个待监视的文件描述符。这个结构体的定义如下:

Ⅱ设置需要监视的文件描述符对于每个待监视的文件描述符,将其添加到 pollfd 数组中,并设置所关心的事件类型。


Ⅲ调用 poll() 函数调用 poll() 函数来监视文件描述符的状态变化,函数返回时,返回值表示就绪文件描述符的数量,如果出现错误则返回 -1。


Ⅳ.检查就绪文件描述符在 poll() 函数返回后,遍历 pollfd 数组,检查每个文件描述符的 revents 成员,以确定哪些文件描述符已经就绪。


Ⅴ.处理就绪文件描述符根据 revents 成员的值来判断哪些文件描述符已经就绪,然后进行相应的操作,比如读取数据、发送数据等。


Ⅵ.重复以上步骤可以循环调用 poll() 函数来重复监视文件描述符的状态变化,实现长时间的事件驱动循环。

代码: 

net.h

#ifndef _NET_H_
#define _NET_H_#include <stdio.h>
#include <stdlib.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <netinet/tcp.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <strings.h>
#include <errno.h>typedef struct sockaddr Addr;
typedef struct sockaddr_in Addr_in;
#define BACKLOG 5
#define ErrExit(msg) do { perror(msg); exit(EXIT_FAILURE); } while(0)void Argment(int argc, char *argv[]);
int CreateSocket(char *argv[]);
int DataHandle(int fd);#endif

socket.c

#include "net.h"void Argment(int argc, char *argv[]){if(argc < 3){fprintf(stderr, "%s<addr><port>\n", argv[0]);exit(0);}
}
int CreateSocket(char *argv[]){/*创建套接字*/int fd = socket(AF_INET, SOCK_STREAM, 0);if(fd < 0)ErrExit("socket");/*允许地址快速重用*/int flag = 1;if( setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, &flag, sizeof(flag) ) )perror("setsockopt");/*设置通信结构体*/Addr_in addr;bzero(&addr, sizeof(addr) );addr.sin_family = AF_INET;addr.sin_port = htons( atoi(argv[2]) );/*绑定通信结构体*/if( bind(fd, (Addr *)&addr, sizeof(Addr_in) ) )ErrExit("bind");/*设置套接字为监听模式*/if( listen(fd, BACKLOG) )ErrExit("listen");return fd;
}
int DataHandle(int fd){char buf[BUFSIZ] = {};Addr_in peeraddr;socklen_t peerlen = sizeof(Addr_in);if( getpeername(fd, (Addr *)&peeraddr, &peerlen) )perror("getpeername");int ret = recv(fd, buf, BUFSIZ, 0);if(ret < 0)perror("recv");if(ret > 0){printf("[%s:%d]data: %s\n", inet_ntoa(peeraddr.sin_addr), ntohs(peeraddr.sin_port), buf);}return ret;
}

server.c

#include "net.h"
#include <poll.h>#define MAX_SOCK_FD 1024
int main(int argc, char *argv[])
{int i, j, fd, newfd;nfds_t nfds = 1;struct pollfd fds[MAX_SOCK_FD] = {};Addr_in addr;socklen_t addrlen = sizeof(Addr_in);/*检查参数,小于3个 直接退出进程*/Argment(argc, argv);/*创建已设置监听模式的套接字*/fd = CreateSocket(argv);fds[0].fd = fd;fds[0].events = POLLIN;while(1){if( poll(fds, nfds, -1) < 0)ErrExit("poll");for(i = 0; i < nfds; i++){/*接收客户端连接,并生成新的文件描述符*/if(fds[i].fd == fd && fds[i].revents & POLLIN){if( (newfd = accept(fd, (Addr *)&addr, &addrlen) ) < 0)perror("accept");fds[nfds].fd = newfd;fds[nfds++].events = POLLIN;printf("[%s:%d][nfds=%lu] connection successful.\n", inet_ntoa(addr.sin_addr), ntohs(addr.sin_port), nfds);}/*处理客户端数据*/if(i > 0 && fds[i].revents & POLLIN){if(DataHandle(fds[i].fd) <= 0){if( getpeername(fds[i].fd, (Addr *)&addr, &addrlen) < 0)perror("getpeername");printf("[%s:%d][fd=%d] exited.\n", inet_ntoa(addr.sin_addr), ntohs(addr.sin_port), fds[i].fd);close(fds[i].fd);for(j=i; j<nfds-1; j++)fds[j] = fds[j+1];nfds--;i--;}}}}close(fd);return 0;
}

(3)epoll

epoll可以理解为event poll,它是一种事件驱动的I/O模型,可以用来替代传统的select和poll模型。epoll的优势在于它可以同时处理大量的文件描述符,而且不会随着文件描述符数量的增加而降低效率。

epoll的实现机制是通过内核与用户空间共享一个事件表,这个事件表中存放着所有需要监控的文件描述符以及它们的状态,当文件描述符的状态发生变化时,内核会将这个事件通知给用户空间,用户空间再根据事件类型进行相应的处理。

epoll 使用了一个事件表(event table)来保存需要监控的文件描述符和相应的事件类型,并提供了 epoll_ctl() 函数来向事件表中添加、修改或删除文件描述符。与 select 和 poll 不同的是,epoll 的设计更加高效,它使用了内核中的事件通知机制,可以避免遍历文件描述符集合,当文件描述符的状态发生变化时,内核会立即通知应用程序。这样可以避免遍历文件描述符集合,减少了不必要的 CPU 消耗,从而提高了效率。当调用 epoll_wait() 函数时,内核会立即返回已就绪的文件描述符列表,无需遍历整个事件表。

没有 fd 个数限制,用户态拷贝到内核态只需要一次,使用时间通知机制来触发。通过 epoll_ctl 注册 fd,一旦 fd 就绪就会通过 callback 回调机制来激活对应 fd,进行相关的 io 操作。epoll 之所以高性能是得益于它的三个函数:

epoll_create() 系统启动时,在 Linux 内核里面申请一个B+树结构文件系统,返回 epoll 对象,也是一个 fd。
epoll_ctl() 每新建一个连接,都通过该函数操作 epoll 对象,在这个对象里面修改添加删除对应的链接 fd,绑定一个 callback 函数
epoll_wait() 轮训所有的 callback 集合,并完成对应的 IO 操作

优点:适用于大规模连接,仅监听已准备好的文件描述符,效率较高。
使用边缘触发(只通知状态变化),提高了效率。
在 Linux 上有较好的性能,采用更先进的事件通知机制。
无文件描述符数量限制。
缺点:只能在Linux操作系统上可用。

使用流程:

Ⅰ.创建 epoll 实例首先,需要调用 epoll_create() 函数创建一个 epoll 实例,并获取一个文件描述符用于操作 epoll 实例。函数返回一个文件描述符,用于引用新创建的 epoll 实例。


Ⅱ.添加文件描述符到 epoll 实例使用 epoll_ctl() 函数向 epoll 实例中添加或删除文件描述符,或者修改文件描述符上关注的事件。在添加文件描述符时,需要先设置 event 结构体的 events 成员,指定文件描述符关注的事件类型,然后调用 epoll_ctl() 函数进行添加操作。


Ⅲ.等待就绪事件使用 epoll_wait() 函数等待就绪事件的发生,该函数会阻塞程序直到有事件发生或超时。函数返回就绪事件的数量,如果超时则返回 0,如果出现错误则返回 -1。

Ⅳ.处理就绪事件在 epoll_wait() 函数返回后,遍历 events 数组,处理每个就绪事件。可以通过 events[i].data.fd 获取就绪事件对应的文件描述符,并通过 events[i].events 获取该文件描述符上发生的事件类型。


Ⅴ.重复以上步骤可以循环调用 epoll_wait() 函数来重复等待就绪事件的发生,从而实现长时间的事件驱动循环。

代码: 

net.h

#ifndef _NET_H_
#define _NET_H_#include <stdio.h>
#include <stdlib.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <netinet/tcp.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <strings.h>
#include <errno.h>typedef struct sockaddr Addr;
typedef struct sockaddr_in Addr_in;
#define BACKLOG 5
#define ErrExit(msg) do { perror(msg); exit(EXIT_FAILURE); } while(0)void Argment(int argc, char *argv[]);
int CreateSocket(char *argv[]);
int DataHandle(int fd);#endif

socket.c

#include "net.h"void Argment(int argc, char *argv[]){if(argc < 3){fprintf(stderr, "%s<addr><port>\n", argv[0]);exit(0);}
}
int CreateSocket(char *argv[]){/*创建套接字*/int fd = socket(AF_INET, SOCK_STREAM, 0);if(fd < 0)ErrExit("socket");/*允许地址快速重用*/int flag = 1;if( setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, &flag, sizeof(flag) ) )perror("setsockopt");/*设置通信结构体*/Addr_in addr;bzero(&addr, sizeof(addr) );addr.sin_family = AF_INET;addr.sin_port = htons( atoi(argv[2]) );/*绑定通信结构体*/if( bind(fd, (Addr *)&addr, sizeof(Addr_in) ) )ErrExit("bind");/*设置套接字为监听模式*/if( listen(fd, BACKLOG) )ErrExit("listen");return fd;
}
int DataHandle(int fd){char buf[BUFSIZ] = {};Addr_in peeraddr;socklen_t peerlen = sizeof(Addr_in);if( getpeername(fd, (Addr *)&peeraddr, &peerlen) )perror("getpeername");int ret = recv(fd, buf, BUFSIZ, 0);if(ret < 0)perror("recv");if(ret > 0){printf("[%s:%d]data: %s\n", inet_ntoa(peeraddr.sin_addr), ntohs(peeraddr.sin_port), buf);}return ret;
}

server.c

#include "net.h"
#include <sys/epoll.h>#define MAX_SOCK_FD 1024int main(int argc, char *argv[])
{int i, nfds, fd, epfd, newfd;Addr_in addr;socklen_t addrlen = sizeof(Addr_in);struct epoll_event tmp, events[MAX_SOCK_FD] = {};/*检查参数,小于3个 直接退出进程*/Argment(argc, argv);/*创建已设置监听模式的套接字*/fd = CreateSocket(argv);if( (epfd = epoll_create(1)) < 0)ErrExit("epoll_create");tmp.events = EPOLLIN;tmp.data.fd = fd;if( epoll_ctl(epfd, EPOLL_CTL_ADD, fd, &tmp) )ErrExit("epoll_ctl");while(1) {if( (nfds = epoll_wait(epfd, events, MAX_SOCK_FD, -1) ) < 0)ErrExit("epoll_wait");printf("nfds = %d\n", nfds);for(i = 0; i < nfds; i++) {if(events[i].data.fd == fd){/*接收客户端连接,并生成新的文件描述符*/if( (newfd = accept(fd, (Addr *)&addr, &addrlen) ) < 0)perror("accept");printf("[%s:%d] connection.\n", inet_ntoa(addr.sin_addr), ntohs(addr.sin_port) );tmp.events = EPOLLIN;tmp.data.fd = newfd;if( epoll_ctl(epfd, EPOLL_CTL_ADD, newfd, &tmp) )ErrExit("epoll_ctl");}else{/*处理客户端数据*/if(DataHandle(events[i].data.fd) <= 0){if( epoll_ctl(epfd, EPOLL_CTL_DEL, events[i].data.fd, NULL) )ErrExit("epoll_ctl");if( getpeername(events[i].data.fd, (Addr *)&addr, &addrlen) )perror("getpeername");printf("[%s:%d] exited.\n", inet_ntoa(addr.sin_addr), ntohs(addr.sin_port) );close(events[i].data.fd);}}}}close(epfd);close(fd);return 0;
}

(4)select,poll和epoll各自优缺点

select
  1. 单个进程能够监视的文件描述符的数量有最大限制,通常是1024,虽然可以更改数量,但由于select采用轮询的方式扫描文件描述符,文件描述符数量越多,性能越差

  2. 内核/用户空间内存拷贝问题,select需要复制大量的句柄数据结构,会产生巨大的开销

  3. select返回的是含有整个句柄的数组,应用程序需要遍历整个数组才能发现哪些句柄发生了事件

  4. select的触发方式是水平触发,应用程序如果没有完成对一个已经就绪的文件描述符进行I/O操作,那么之后每次select调用还是会将这些文件描述符通知进程

  5. 内核中实现select是用轮询方法,即每次检测都会遍历所有FD_SET中的句柄

假设服务器需要支持100万的并发连接,在__FD_SETSIZE1024的情况下,则我们至少需要开辟1000个进程才能实现100万的并发连接

poll

poll使用链表保存文件描述符,因此没有了监视文件数量的限制,但其他三个缺点依然存在 select与poll目前在小规模服务器上还是有用武之地,并且维护老系统代码的时候,经常会用到这两个函数;

epoll

epoll是Linux下多路复用I/O接口select/poll的增强版本 epoll只需要监听那些已经准备好的队列集合中的文件描述符,效率较高

4.信号驱动式I/O

5. 异步I/O

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/831871.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Electron 对 SQLite 进行加密

上一篇讲了如何在 Electron使用 SQLite&#xff0c;如果 SQLite 中存有敏感数据&#xff0c;客户端采用明文存储风险很高&#xff0c;为了保护客户数据&#xff0c;就需要对数据进行加密&#xff0c;由于 electron 对代码并不加密&#xff0c;所以这里排除通过逆向工程进行数据…

想要快速接收的看过来:Cell旗下毕业神刊,中科院二区、平均审稿周期1个月,冲!

我是娜姐 迪娜学姐 &#xff0c;一个SCI医学期刊编辑&#xff0c;探索用AI工具提效论文写作和发表。 就是它了&#xff0c;Cell旗下全OA期刊iScience。影响因子5.8分&#xff0c;中科院二区&#xff0c;年发文量逐年上涨&#xff0c;2023年发文近3000篇&#xff0c;2024年第一季…

一周学会Django5 Python Web开发 - Django5 ModelForm表单定义与使用

锋哥原创的Python Web开发 Django5视频教程&#xff1a; 2024版 Django5 Python web开发 视频教程(无废话版) 玩命更新中~_哔哩哔哩_bilibili2024版 Django5 Python web开发 视频教程(无废话版) 玩命更新中~共计51条视频&#xff0c;包括&#xff1a;2024版 Django5 Python we…

一种算法分类方式及其应用

在计算机科学领域&#xff0c;算法是解决问题的有效方法&#xff0c;而对算法进行分类有助于理解它们的特性、优劣以及在不同场景下的应用。常见的算法分类方法&#xff0c;包括按设计思想、问题类型、数据结构和应用领域等&#xff0c;每一类算法会对应有其典型和实际应用。 算…

spring boot3多模块项目工程搭建-上(团队开发模板)

⛰️个人主页: 蒾酒 &#x1f525;系列专栏&#xff1a;《spring boot实战》 目录 写在前面 多模块结构优缺点 模块介绍 Common 模块&#xff1a; API 模块&#xff1a; Web 模块&#xff1a; Service 模块&#xff1a; DAO 模块&#xff1a; 搭建步骤 1.创建 父…

深入理解分布式事务⑨ ---->MySQL 事务的实现原理 之 MySQL 中的XA 事务(基本原理、流程分析、事务语法、简单例子演示)详解

目录 MySQL 事务的实现原理 之 MySQL 中的XA 事务&#xff08;基本原理、流程分析、事务语法、简单例子演示&#xff09;详解MySQL 中的 XA 事务1、XA 事务的基本原理1-1&#xff1a;XA 事务模型图&#xff1a;1-2&#xff1a;XA 事务模型的两阶段提交操作&#xff1a;Prepare …

Stable Diffusion webUI 最全且简单配置指南

Stable Diffusion webUI 配置指南 本博客主要介绍部署Stable Diffusion到本地&#xff0c;生成想要的风格图片。 文章目录 Stable Diffusion webUI 配置指南1、配置环境&#xff08;1&#xff09;pip环境[可选]&#xff08;2&#xff09;conda环境[可选] 2、配置Stable Diffu…

Jenkins docker部署springboot项目

1、创建jenkins容器 1&#xff0c;首先&#xff0c;我们需要创建一个 Jenkins 数据卷&#xff0c;用于存储 Jenkins 的配置信息。可以通过以下命令创建一个数据卷&#xff1a; docker volume create jenkins_data启动 Jenkins 容器并挂载数据卷&#xff1a; docker run -dit…

Leetcode—422. 有效的单词方块【简单】Plus

2024每日刷题&#xff08;126&#xff09; Leetcode—422. 有效的单词方块 实现代码 class Solution { public:bool validWordSquare(vector<string>& words) {int row words.size();for(int i 0; i < row; i) {// 当前这一行的列数int col words[i].length(…

Java高阶私房菜:JVM性能优化案例及讲解

目录 核心思想 优化思考方向 压测环境准备 堆大小配置调优 调优前 调优后 分析结论 垃圾收集器配置调优 调优前 调优后 分析结论 JVM性能优化是一项复杂且耗时的工作&#xff0c;该环节没办法一蹴而就&#xff0c;它需要耐心雕琢&#xff0c;逐步优化至理想状态。“…

驾校考试宝典vip一点通驾考精简500题科目一四速记口诀c1答题技巧

下载地址&#xff1a;驾校考试宝典vip一点通驾考精简500题科目一四速记口诀c1答题技巧.zip 这份速记口诀考点总结 评分标准 和答题技巧&#xff0c;很详细&#xff0c;全科目速记口诀秘笈.pdf

QT中的容器

Qt中的容器 关于Qt中的容器类&#xff0c;下面我们来进行一个总结&#xff1a; Qt的容器类比标准模板库&#xff08;STL&#xff09;中的容器类更轻巧、安全和易于使用。这些容器类是隐式共享和可重入的&#xff0c;而且他们进行了速度和存储的优化&#xff0c;因此可以减少可…

mysql 指定根目录 迁移根目录

mysql 指定根目录 迁移根目录 1、问题描述2、问题分析3、解决方法3.1、初始化mysql前就手动指定mysql根目录为一个大的分区(支持动态扩容)&#xff0c;事前就根本上解决mysql根目录空间不够问题3.1.0、方法思路3.1.1、卸载mariadb3.1.2、下载Mysql安装包3.1.3、安装Mysql 8.353…

TouchGFX 总结

文章目录 使用中文字体多屏幕间交换数据UI to MCUMCU to UI API文档参考横竖屏切换 使用中文字体 添加一个textArea&#xff0c;默认的英文文本可见&#xff0c;输入中文字体后就看不见了&#xff0c;是因为这个默认的字体不支持中文&#xff0c;改一下字体就可以了&#xff1…

全方位解析Node.js:从模块系统、文件操作、事件循环、异步编程、性能优化、网络编程等高级开发到后端服务架构最佳实践以及Serverless服务部署指南

Node.js是一种基于Chrome V8引擎的JavaScript运行环境&#xff0c;专为构建高性能、可扩展的网络应用而设计。其重要性在于革新了后端开发&#xff0c;通过非阻塞I/O和事件驱动模型&#xff0c;实现了轻量级、高并发处理能力。Node.js的模块化体系和活跃的npm生态极大加速了开发…

网络基础-网络设备介绍

本系列文章主要介绍思科、华为、华三三大厂商的网络设备 网络设备 网络设备是指用于构建和管理计算机网络的各种硬件设备和设备组件。以下是常见的网络设备类型&#xff1a; 路由器&#xff08;Router&#xff09;&#xff1a;用于连接不同网络并在它们之间转发数据包的设备…

深入理解网络原理2----UDP协议

文章目录 前言一、UDP协议协议段格式&#xff08;简图&#xff09;校验和 二、UDP与TCP 前言 随着时代的发展&#xff0c;越来越需要计算机之间互相通信&#xff0c;共享软件和数据&#xff0c;即以多个计算机协同⼯作来完成业务&#xff0c;就有了⽹络互连。 一、UDP协议 协…

java发送请求-http和https

http和https区别 1、http是网络传输超文本协议&#xff0c;client---- http------ server 2、httpshttpssl证书&#xff0c;让网络传输更安全 &#xff0c;client---- httpssl------ server 3、ssl证书是需要客户端认可的&#xff0c;注意官方证书和jdk生成的证书的用户来使…

【Github】将github仓库作为图床使用

创建github仓库 首先创建一个github仓库专门用于存储图片&#xff0c;具体步骤如下&#xff1a; 1.点击新的仓库按钮 2.初始配置&#xff1a;随便填写一个仓库名&#xff1b;这里的仓库状态一定要是public公开的&#xff0c;不然后面访问不了图片 下载PicGo PicGo官网 在A…

获取淘宝商品销量数据接口

淘宝爬虫商品销量数据采集通常涉及以下几个步骤&#xff1a; 1、确定采集目标&#xff1a;需要明确要采集的商品类别、筛选条件&#xff08;如天猫、价格区间&#xff09;、销量和金额等数据。例如&#xff0c;如果您想了解“小鱼零食”的销量和金额&#xff0c;您需要设定好价…