网络编程中四种高性能的IO模型
1.阻塞IO
read()``recv()``recvfrom()
这些函数本身不具有阻塞属性,而是文件描述符本身的阻塞属性导致这个这些函数执行表现出来的形式是阻塞.- 在默认情况向下,Linux下建立的socket套接字都是阻塞的.
提问:read()
这个函数是否是阻塞函数,如果不是,请举例说明?为什么?
read()函数可以是阻塞或非阻塞的,具体取决于所使用的编程语言、操作系统以及读取的数据源(如文件、套接字等)的特性。
示例和说明:
阻塞式读取:
在某些情况下,read()函数是阻塞的,这意味着当调用该函数时,程序会一直等待,直到有可读取的数据才会继续执行下面的代码。这种情况通常发生在以下情况下:
-
从键盘或终端读取输入:当用户在终端输入数据时,read()函数会阻塞直到用户按下回车键或输入特定的结束符号才会返回并提供输入内容。
-
从套接字(Socket)读取数据:当调用套接字的read()函数时,如果没有数据可用,套接字可能会进入阻塞状态,等待数据到达。
非阻塞式读取:
在其他情况下,read()函数可以是非阻塞的。这意味着当调用该函数时,如果当前没有可读取的数据,函数会立即返回,并且不会等待数据的到达。这种情况通常发生在以下情况下:
-
设置套接字为非阻塞模式:通过将套接字设置为非阻塞模式,可以确保read()函数在没有可读取数据时立即返回。在非阻塞模式下,您可以使用其他的机制来检查数据是否可用,例如轮询(polling)或使用回调函数。
-
从缓冲区读取数据:如果数据已经被读取到一个缓冲区中,read()函数通常可以立即将缓冲区中的数据返回,而无需等待额外的I/O操作。
需要注意的是,这些行为取决于所使用的编程语言和底层库的实现。因此,在使用read()函数时,得区分情况.
2.非阻塞IO
- 给文件描述符添加非阻塞属性
- 由于非阻塞属性,所以不断询问套接字中是否有数据到达
3.多路复用
- 同时对多个IO口进行操作(也就是同时监听几个套接字)
- 可以在规定的时间内检测数据是否到达
4.信号驱动
- 属于异步通信方式
- 当socket中有数据到达时,通过发送信号告知用户
二.阻塞IO
1.概念
阻塞IO(Blocking IO)是一种IO操作的方式,指的是在进行IO操作时,程序会一直等待数据传输完成后再继续执行后续的代码。在阻塞IO中,当程序发起一个IO请求后,它会一直等待,直到数据从输入/输出设备传输完成并返回结果给程序。
在阻塞IO模型中,当一个IO操作发生时,程序将进入阻塞状态,无法继续执行其他任务,直到IO操作完成。这意味着当某个IO操作阻塞时,整个程序的执行流程也会被阻塞住。因此,在处理多个IO任务时,阻塞IO可能会导致系统效率低下。
阻塞IO的一个常见示例是文件读写操作。当程序需要从磁盘中读取文件内容时,如果采用阻塞IO方式,程序会一直等待文件的读取操作完成后才能继续执行后续代码。同样地,当程序将数据写入磁盘时,如果使用阻塞IO方式,程序会等待写入操作完成后才能继续执行。
为了提高系统的并发性和效率,有一种替代方案是非阻塞IO(Non-blocking IO)。
读阻塞:当数据缓冲区中没有数据可以读取时,调用read
/recv
/recvfrom
就会无限阻塞.
写阻塞:当缓冲区剩余的大小 小于 写入的数据量,就会发生阻塞,直到缓冲区中的数据被读取了.
三.非阻塞IO
非阻塞IO允许程序在进行IO操作时继续执行其他任务,而不必等待IO操作完成。非阻塞IO通常与事件驱动(Event-driven)或回调(Callback)机制结合使用,以便在IO操作完成时通知程序进行相应的处理。非阻塞IO适用于需要同时处理多个IO任务的情况,可以提高系统的吞吐量和响应性能。
非阻塞IO与阻塞IO流程比较
阻塞IO流程:
建立套接字(阻塞)------>读取数据------>判断缓冲区中有没有数据
如果没有数据----->进入一个无限等待的状态----->直到缓冲区中有数据为止。
如果有数据 ----->读取数据----->进入一个无限等待的状态----->直到缓冲区中有数据为止。
非阻塞IO流程:
建立套接字(阻塞)----->将套接字文件描述符设置成非阻塞属性----->读取数据
如果没有数据----->没有----->读取失败----->接口会马上返回,不会一直阻塞
如果有数据 ----->有----->读取成功----->接口也会返回
那么该怎么给文件描述符设置非阻塞属性?
fcntl函数是一个系统调用,用于对文件描述符(file descriptor)进行各种操作。函数原型如下:
#include <unistd.h> #include <fcntl.h> int fcntl(int fd, int cmd, ...);
参数说明:
-
fd
:文件描述符。表示需要进行操作的文件描述符。 -
cmd
:操作命令。表示要执行的具体操作。 -
...
:可选的第三个参数,根据不同的操作命令,可能需要传入额外的参数。
返回值:
fcntl函数的返回值根据不同的操作命令而有所不同:
-
对于获取或设置文件状态标志、文件记录锁等操作,返回值通常是与操作有关的信息,具体取决于操作命令。
-
对于复制文件描述符、设置文件描述符标志等操作,返回值是新的文件描述符或执行操作的结果,或者-1表示操作失败。
参数值的选项:
fcntl函数的cmd
参数可以取以下常用操作命令之一:
-
复制文件描述符:
-
F_DUPFD
:复制文件描述符,生成一个大于或等于第三个参数的新文件描述符。 -
F_DUPFD_CLOEXEC
:与F_DUPFD
相同,但同时将新文件描述符的FD_CLOEXEC
标志设置为关闭时执行exec函数。
-
-
获取/设置文件状态标志:
-
F_GETFL
:获取文件的状态标志。 -
F_SETFL
:设置文件的状态标志。
-
-
获取/设置文件记录锁:
-
F_GETLK
:获取文件记录锁的信息。 -
F_SETLK
:设置文件记录锁,如果锁冲突则阻塞。 -
F_SETLKW
:设置文件记录锁,如果锁冲突则等待。
-
-
获取/设置文件描述符标志:
-
F_GETFD
:获取文件描述符的标志。 -
F_SETFD
:设置文件描述符的标志。
-
-
设置/获取异步IO所有权:
-
F_GETOWN
:获取异步IO的所有权(进程ID或进程组ID)。 -
F_SETOWN
:设置异步IO的所有权。
-
注意:
File access mode (O_RDONLY, O_WRONLY, O_RDWR) and file creation flags (i.e., O_CREAT, O_EXCL, O_NOCTTY, O_TRUNC) in arg are ignored. ---这些状态 不能通过fcntl函数进行设置
On Linux this command can change only the O_APPEND, O_ASYNC, O_DIRECT, O_NOATIME, and O_NONBLOCK flags.
- fcntl函数可以设置这些状态
- O_APPEND 文本追加
- O_ASYNC 信号触发模式
- O_DIRECT 不使用缓冲区写入
- O_NOATIME 不更新文件的修改时间
- O_NONBLOCK(常用) 非阻塞属性
下面时 fcntl()
函数的例子
int status = fcntl(fd, F_GETFL ); //得到文件描述符的状态 status |= O_NONBLOCK ;//在原来的基础上新增非阻塞属性 fcntl(fd, F_SETFL,status); //把变量status的状态设置到文件描述符中
返回值
F_GETFL Value of file status flags.
F_SETFL
- 成功返回0
- 失败返回 -1
例子:TCP非阻塞IO轮循服务器
#include <stdio.h> #include <string.h> //strlen #include <unistd.h> //close #include <pthread.h> //pthread_create #include <sys/types.h> /* See NOTES */ //soket #include <sys/socket.h> #include <netinet/in.h> //inet_addr #include <arpa/inet.h> #include <fcntl.h> #define SERVER_IP "192.168.1.148" // ubuntu的服务器的ip #define SERVER_PORT 8888 /* 非阻塞IO轮循并发服务器 */ typedef struct client { int socket_fd; struct sockaddr_in client_addr; } ClientInfo; // 一个客户端的套接字对应一个客户端的地址 int main() { int ret; // 建立套接字 // 建立套接字---socket int socket_fd; // SOCK_STREAM--TCP的协议 socket_fd = socket(AF_INET, SOCK_STREAM, 0); if (socket_fd < 0) { perror("socket fail"); return -1; } // 设置端口重复复用 int optval = 1; setsockopt(socket_fd, SOL_SOCKET, SO_REUSEADDR, &optval, sizeof(optval)); // 绑定本机ip和端口 // 填充服务器的ip和端口号----新结构体 struct sockaddr_in server_addr; server_addr.sin_family = AF_INET; // Ipv4 网络协议 server_addr.sin_port = htons(SERVER_PORT); // 本机端口转换为网络端口host to net short server_addr.sin_addr.s_addr = inet_addr(SERVER_IP); // 本机ip转换为网络ip socklen_t addrlen = sizeof(struct sockaddr_in); // 绑定本机ip和端口号(必须要有)---bind ret = bind(socket_fd, (struct sockaddr *)&server_addr, addrlen); if (ret < 0) { perror("bind fail"); return -1; } printf("绑定本机成功[%s][%d]\n", SERVER_IP, SERVER_PORT); // 监听 ret = listen(socket_fd, 20); if (ret < 0) { perror("listen fail"); return -1; } int status = fcntl(socket_fd, F_GETFL); // 得到文件描述符的状态 status |= O_NONBLOCK; // 在原来的基础上新增非阻塞属性 fcntl(socket_fd, F_SETFL, status); // 把变量status的状态设置到文件描述符中 int socket_client; struct sockaddr_in client_addr; // 用来接收客户端的ip和端口 socklen_t len = sizeof(struct sockaddr_in); char buf[1024] = {0}; // 用数组存储新的客户端套接字最大存10个 ClientInfo client_list[10]; // 最多保存10个客户端信息 memset(client_list, 0, sizeof(client_list)); int num_clients = 0; // 当前客户端数量 while (1) { // 接收客户端的链接---accept(阻塞) socket_client = accept(socket_fd, (struct sockaddr *)&client_addr, &len); if (socket_client != -1) // 有新的客户端上线就存起来 { ClientInfo new_client; new_client.socket_fd = socket_client; new_client.client_addr = client_addr; client_list[num_clients++] = new_client; int status = fcntl(new_client.socket_fd, F_GETFL); // 得到文件描述符的状态 status |= O_NONBLOCK; // 在原来的基础上新增非阻塞属性 fcntl(new_client.socket_fd, F_SETFL, status); // 把变量status的状态设置到文件描述符中 printf("新的客户端上线[%d]:[%s][%d]\n", socket_client, inet_ntoa(new_client.client_addr.sin_addr), ntohs(new_client.client_addr.sin_port)); } // 轮循遍历所有的套接字 for (int i = 0; i < num_clients; i++) { // 与客户端之间交互数据---send/recv memset(buf, 0, sizeof(buf)); ret = recv(client_list[i].socket_fd, buf, sizeof(buf), 0); if (ret > 0) // 客户端发来有效数据 { printf("[IP:%s][PORT:%d]接收到来自客户端的消息:%s\n", inet_ntoa(client_list[i].client_addr.sin_addr), ntohs(client_list[i].client_addr.sin_port), buf); } if (ret == 0) // 客户端掉线,重置套接字 { // 客户端掉线说明 printf("[IP:%s][PORT:%d] 客户端已下线.\n", inet_ntoa(client_list[i].client_addr.sin_addr), ntohs(client_list[i].client_addr.sin_port)); close(client_list[i].socket_fd); client_list[i].socket_fd = -1; } } } // 关闭套接字 close(socket_fd); return 0; }
因为内容比较多,后面还会继续更新。