【网络编程下】五种网络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.rhkb.cn/news/319379.html

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

相关文章

Llama 3 安装使用方法

Llama3简介&#xff1a; llama3是一种自回归语言模型&#xff0c;采用了transformer架构&#xff0c;目前开源了8b和70b参数的预训练和指令微调模型&#xff0c;400b正在训练中&#xff0c;性能非常强悍&#xff0c;并且在15万亿个标记的公开数据进行了预训练&#xff0c;比ll…

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(…

Postman的一些使用技巧

Postman 是一个流行的 API 开发工具&#xff0c;用于设计、开发、测试、发布和监控 API。在现代web开发中使用非常广泛。后端开发必备而且必会的工具。 目录 1.配置环境变量 2.动态变量 3.脚本 4.测试 5.模拟 6.监控 7.集合运行器 8.响应保存 9.请求历史 10.同步请求…

如何在iPhone上恢复出厂设置后恢复数据

你不想让这种情况发生&#xff0c;但它确实发生了。您必须将iPhone恢复出厂设置。当您的 iPhone 上出现软件问题且无法修复时&#xff0c;可能会发生这种情况。相反&#xff0c;在更新期间&#xff0c;或者您的iPhone遇到问题时&#xff0c;iPhone上的数据不再存在。 不过不用…

leetcode_47.全排列 II

47. 全排列 II 题目描述&#xff1a;给定一个可包含重复数字的序列 nums &#xff0c;按任意顺序 返回所有不重复的全排列。 示例 1&#xff1a; 输入&#xff1a;nums [1,1,2] 输出&#xff1a; [[1,1,2],[1,2,1],[2,1,1]]示例 2&#xff1a; 输入&#xff1a;nums [1,2,3] …

性能优化(一):ArrayList还是LinkedList?

引言 集合作为一种存储数据的容器&#xff0c;是我们日常开发中使用最频繁的对象类型之一。JDK为开发者提供了一系列的集合类型&#xff0c;这些集合类型使用不同的数据结构来实现。因此&#xff0c;不同的集合类型&#xff0c;使用场景也不同。 很多同学在面试的时候&#x…

Flutter笔记:使用Flutter私有类涉及的授权协议问题

Flutter笔记 使用Flutter私有类涉及的授权协议问题 - 文章信息 - Author: 李俊才 (jcLee95) Visit me at CSDN: https://jclee95.blog.csdn.netMy WebSite&#xff1a;http://thispage.tech/Email: 291148484163.com. Shenzhen ChinaAddress of this article:https://blog.cs…

使用docker-compose编排lnmp(dockerfile)完成wordpress

文章目录 使用docker-compose编排lnmp&#xff08;dockerfile&#xff09;完成wordpress1、服务器环境2、Docker、Docker-Compose环境安装2.1 安装Docker环境2.2 安装Docker-Compose 3、nginx3.1 新建目录&#xff0c;上传安装包3.2 编辑Dockerfile脚本3.3 准备nginx.conf配置文…

【哈希表】Leetcode 14. 最长公共前缀

题目讲解 14. 最长公共前缀 算法讲解 我们使用当前第一个字符串中的与后面的字符串作比较&#xff0c;如果第一个字符串中的字符没有出现在后面的字符串中&#xff0c;我们就直接返回&#xff1b;反之当容器中的所有字符串都遍历完成&#xff0c;说明所有的字符串都在该位置…

Java枚举(enum)

“今天我们来学习枚举吧&#xff0c;三妹&#xff01;”我说&#xff0c;“同学让你去她家玩了两天&#xff0c;感觉怎么样呀&#xff1f;” “心情放松了不少。”三妹说&#xff0c;“可以开始学 Java 了&#xff0c;二哥。” “OK。” “枚举&#xff08;enum&#xff09;…

Linux服务器安全基础 - 查看入侵痕迹

1. 常见系统日志 /var/log/cron 记录了系统定时任务相关的日志 /var/log/dmesg 记录了系统在开机时内核自检的信息&#xff0c;也可以使用dmesg命令直接查看内核自检信息 /var/log/secure:记录登录系统存取数据的文件;例如:pop3,ssh,telnet,ftp等都会记录在此. /var/log/btmp:记…

【数据结构】链表专题2

前言 本篇博客继续探讨有关链表的专题&#xff0c;这片博客的题&#xff0c;提前打个预防针&#xff0c;有点意思哦&#xff0c;哈哈哈&#xff0c;话不多说&#xff0c;进入正文 &#x1f493; 个人主页&#xff1a;小张同学zkf ⏩ 文章专栏&#xff1a;数据结构 若有问题 评论…

ubuntu下安装配置python3.11

方案1 添加仓库&#xff1a; $ sudo add-apt-repository ppa:deadsnakes/ppa $ sudo apt update $ sudo apt install python3.11然后查看有多少个python版本已经安装了&#xff1a; ls -l /usr/bin/python*python2.7,python 3.8 ,python 3.11. 然后&#xff0c;设置系统默认…

【IC设计】CRC(循环冗余校验)

目录 理论解读CRC应用CRC算法参数解读常见CRC参数模型 设计实战校招编程题分类串行输入、并行计算、串行输出**串行计算、串行输出&#xff08;线性移位寄存器&#xff09;LSFR线性移位寄存器&#xff08;并转串&#xff09;(并行计算)模二除 总结——串行、并行计算的本质参考…

【数据结构】--- 深入剖析二叉树(上篇)--- 初识树和二叉树

Welcome to 9ilks Code World (๑•́ ₃ •̀๑) 个人主页: 9ilk (๑•́ ₃ •̀๑) 文章专栏&#xff1a; 数据结构之旅 &#x1f3e0; 初识树 &#x1f4d2; 树的概念 树是一种非线性的数据结构&#xff0c;它是由n&#xff08;n>0&#xff09;个有限结点…

旅游系列之:庐山美景

旅游系列之&#xff1a;庐山美景 一、路线二、住宿二、庐山美景 一、路线 庐山北门乘坐大巴上山&#xff0c;住在上山的酒店东线大巴游览三叠泉&#xff0c;不需要乘坐缆车&#xff0c;步行上下三叠泉即可&#xff0c;线路很短 二、住宿 长江宾馆庐山分部 二、庐山美景

SpringBoot 快速开始 Dubbo RPC

文章目录 SpringBoot 快速开始 Dubbo RPC下载 Nacos项目启动项目的创建创建主项目接口定义服务的创建Dubbo 服务提供者的创建服务的消费者创建 添加依赖给 Provider、Consumer 添加依赖 开始写代码定义接口在 Provider 中实现在 Consumer 里面使用创建启动类 注册中心配置启动 …

基于Spring Boot的校园博客系统设计与实现

基于Spring Boot的校园博客系统设计与实现 开发语言&#xff1a;Java框架&#xff1a;springbootJDK版本&#xff1a;JDK1.8数据库工具&#xff1a;Navicat11开发软件&#xff1a;eclipse/myeclipse/idea 系统部分展示 系统功能界面图&#xff0c;在系统首页可以查看首页、文…

状态模式

文章目录 1.UML类图2.状态基类3.状态实现类3.状态机管理类使用示例 1.UML类图 2.状态基类 public abstract class State {public string? Name { get; set; }public StateMachine? StateMachine {get; set;}public abstract void Exit();public abstract void Enter(); }3.…

(三)Appdesigner-界面转换及数据导入和保存

提示&#xff1a;文章为系列文章&#xff0c;可以在对应学习专栏里面进行学习。对应资源已上传 目录 前言 一、Appdesigner是什么&#xff1f; 二、界面切换 三、数据导入及保存 &#xff08;一&#xff09;数据导入 &#xff08;二&#xff09;数据保存 总结 前言 Appd…