赞
踩
IO多路转接也称为IO多路复用,它是一种网络通信的手段(机制),通过这种方式可以同时监测多个文件描述符并且这个过程是阻塞的,一旦检测到有文件描述符就绪( 可以读数据或者可以写数据)程序的阻塞就会被解除,之后就可以基于这些(一个或多个)就绪的文件描述符进行通信了。通过这种方式在单线程/进程的场景下也可以在服务器端实现并发。常见的IO多路转接方式有:select、poll、epoll。
下面先对多线程/多进程并发和IO多路转接的并发处理流程进行对比(服务器端):
主线程/父进程:
accept()
阻塞等待客户端连接请求。子线程/子进程:
read()
/ recv()
来接收数据,如果没有数据则阻塞,直到数据到达。write()
/ send()
来发送数据,如果写缓冲区满了则阻塞,直到可以写入数据。优点:
缺点:
IO多路复用:
select
、poll
、epoll
等函数委托操作系统监视多个文件描述符(包括监听和通信的文件描述符)。监听文件描述符:
select
等函数已经通知监听文件描述符就绪。accept()
接受连接,而不用等待。通信文件描述符:
优点:
缺点:
epoll
等更高级的IO复用机制。在IO多路复用中,select
是最基础和最古老的一种实现方式之一。select
函数允许程序同时监视多个文件描述符(sockets、stdin、stdout等),并在其中任何一个文件描述符准备好进行IO操作(如读或写)时通知程序。
程序猿通过调用这个函数可以委托内核帮助我们检测若干个文件描述符的状态,其实就是检测这些文件描述符对应的读写缓冲区的状态:
委托检测的文件描述符被遍历检测完毕之后,已就绪的这些满足条件的文件描述符会通过select()的参数分3个集合传出,程序猿得到这几个集合之后就可以分情况依次处理了。
select
函数是一个用于IO多路复用的系统调用,允许程序监视多个文件描述符(sockets、stdin、stdout等),并在其中任何一个文件描述符准备好进行IO操作(如读或写)时通知程序。
- #include <sys/select.h>
- struct timeval {
- time_t tv_sec; /* seconds */
- suseconds_t tv_usec; /* microseconds */
- };
- int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);
- 参数说明
- nfds:监视的所有文件描述符中最大的文件描述符加1。通常可以通过计算出最大的文件描述符值加1来设置这个参数,例如 (fd + 1),其中 fd 是最大的文件描述符。内核需要线性遍历这些集合中的文件描述
- 符,这个值是循环结束的条件
- readfds:要监视读操作的文件描述符集合。传入传出参数,读集合一般情况下都是需要检测的,这样
- 才知道通过哪个文件描述符接收数据
- writefds:要监视写操作的文件描述符集合。传入传出参数,如果不需要使用这个参数可以指定为NULL
- exceptfds:要监视异常情况的文件描述符集合(一般设置为 NULL,表示不关心异常情况)。传入传出
- 参数,如果不需要使用这个参数可以指定为NULL
- timeout:超时时间,用于设置select函数的阻塞时间。它可以指定程序阻塞的最长时间。如果设置为 NULL,select将一直阻塞,直到有事件发生;如果设置为指向具有零时间的 timeval 结构的指针,则
- select将立即返回;如果 timeout 不为 NULL,并且 select 在指定时间内没有检测到事件,则返回超时。
- 返回值说明
- -1:如果 select 函数调用失败,通常是由于参数错误或者系统调用中断(如信号中断)。
- 0:如果 select 超时,即在指定的超时时间内没有文件描述符准备好进行IO操作。
- 大于 0:返回已经准备好进行IO操作的文件描述符的数量。在返回时,fd_set 集合中会被修改,标识
- 出哪些文件描述符已经准备好。
-

文件描述符集合的操作:
select
之前,需要使用 FD_ZERO
、FD_SET
等函数来准备好 readfds
、writefds
和 exceptfds
这些文件描述符集合。文件描述符数量限制:
getrlimit()
函数查询当前进程的文件描述符数量限制。超时设置:
timeout
参数设置为 NULL
,此时 select
函数将一直阻塞,直到有文件描述符准备好。并发性和效率:
select
函数的效率可能会随着文件描述符数量的增加而下降,因为它在内核中需要线性扫描整个文件描述符集合。可移植性:
select
函数几乎在所有主流操作系统上都有实现,因此具有较好的跨平台性。另外初始化fd_set类型的参数还需要使用相关的一些列操作函数,具体如下:
- 1. void FD_CLR(int fd, fd_set *set);
- 功能:从文件描述符集合中删除指定的文件描述符。
- 参数:
- fd:要从集合中删除的文件描述符。
- set:要操作的文件描述符集合指针。
- 作用:将文件描述符集合 set 中的 fd 对应的标志位清零,即将其从集合中移除。
- 2. int FD_ISSET(int fd, fd_set *set);
- 功能:检查文件描述符集合中特定文件描述符的状态。
- 参数:
- fd:要检查的文件描述符。
- set:要操作的文件描述符集合指针。
- 返回值:
- 如果文件描述符 fd 在 set 集合中被设置(即处于就绪状态),返回非零值;
- 否则返回0。
- 作用:通常用于在调用 select 函数后,检查哪些文件描述符已经准备好进行IO操作。
- 3. void FD_SET(int fd, fd_set *set);
- 功能:向文件描述符集合中添加指定的文件描述符。
- 参数:
- fd:要添加的文件描述符。
- set:要操作的文件描述符集合指针。
- 作用:将文件描述符集合 set 中的 fd 对应的标志位设置为1,表示将该文件描述符加入到监视列表中。
- 4. void FD_ZERO(fd_set *set);
- 功能:清空文件描述符集合。
- 参数:
- set:要操作的文件描述符集合指针。
- 作用:将文件描述符集合 set 中的所有标志位清零,即清除所有文件描述符的状态,使集合为空集。

在select()函数中第2、3、4个参数都是fd_set类型,它表示一个文件描述符的集合,类似于信号集 sigset_t,这个类型的数据有128个字节,也就是1024个标志位,和内核中文件描述符表中的文件描述符个数是一样的。
sizeof(fd_set) = 128 字节 * 8 = 1024 bit // int [32]
这并不是巧合,而是故意为之。这块内存中的每一个bit 和 文件描述符表中的每一个文件描述符是一一对应的关系,这样就可以使用最小的存储空间将要表达的意思描述出来了。
下图中的fd_set中存储了要委托内核检测读缓冲区的文件描述符集合。
内核在遍历这个读集合的过程中,如果被检测的文件描述符对应的读缓冲区中没有数据,内核将修改这个文件描述符在读集合fd_set中对应的标志位,改为0,如果有数据那么这个标志位的值不变,还是1。
当select()函数解除阻塞之后,被内核修改过的读集合通过参数传出,此时集合中只要标志位的值为1,那么它对应的文件描述符肯定是就绪的,我们就可以基于这个文件描述符和客户端建立新连接或者通信了。
初始化准备
lfd
,并设置为非阻塞模式(可选但推荐,可以通过 fcntl
函数实现)。bind()
函数。listen()
函数。文件描述符集合准备
fd_set
结构体对象,用于存储所有需要监视的文件描述符。FD_ZERO()
初始化该结构体,清空所有文件描述符的状态。FD_SET()
将监听套接字 lfd
添加到监视集合中。循环处理事件
select()
函数来等待文件描述符的就绪事件。调用 select()
函数
select()
函数阻塞等待事件发生。一旦有文件描述符就绪(可以读或写),select()
函数将返回。处理就绪事件
FD_ISSET()
函数遍历文件描述符集合,检查哪些文件描述符已经就绪。lfd
就绪,表示有新的客户端连接请求到来:
accept()
函数接受新连接,并创建新的通信套接字 cfd
。cfd
添加到文件描述符集合中,使用 FD_SET()
。cfd
大于当前的 max_fd
,更新 max_fd
。cfd
就绪,表示有数据到达或可以发送:
close()
关闭套接字,并使用 FD_CLR()
将其从文件描述符集合中移除。错误处理和异常情况
select()
、accept()
、read()
、write()
等函数时,需要适时处理可能的错误情况和异常。accept()
返回 EAGAIN
或 EWOULDBLOCK
错误时,应适当延迟重试或放入队列等待处理。循环迭代
- #include <stdio.h>
- #include <stdlib.h>
- #include <unistd.h>
- #include <string.h>
- #include <arpa/inet.h>
- #include <errno.h>
-
- #define MAX_CLIENTS 10
- #define PORT 9999
-
- int main() {
- int lfd, cfd, maxfd, activity, i, valread;
- int client_socket[MAX_CLIENTS] = {0};
- fd_set readfds, rdtemp;
- struct sockaddr_in addr, cliaddr;
- socklen_t cliLen = sizeof(cliaddr);
- char buffer[1024];
-
- // 创建监听的套接字
- if ((lfd = socket(AF_INET, SOCK_STREAM, 0)) == 0) {
- perror("socket failed");
- exit(EXIT_FAILURE);
- }
-
- // 设置服务器地址结构
- addr.sin_family = AF_INET;
- addr.sin_port = htons(PORT);
- addr.sin_addr.s_addr = INADDR_ANY;
-
- // 绑定套接字到本地IP和端口
- if (bind(lfd, (struct sockaddr *)&addr, sizeof(addr)) < 0) {
- perror("bind failed");
- exit(EXIT_FAILURE);
- }
-
- // 开始监听连接
- if (listen(lfd, 128) < 0) {
- perror("listen");
- exit(EXIT_FAILURE);
- }
-
- printf("Waiting for connections on port %d...\n", PORT);
-
- // 初始化文件描述符集合
- FD_ZERO(&readfds);
- FD_SET(lfd, &readfds); // 将监听套接字加入集合
- maxfd = lfd; // 初始时最大的文件描述符是监听套接字
-
- while (1) {
- // 阻塞等待文件描述符就绪
- rdtemp = readfds;
- activity = select(maxfd + 1, &rdtemp, NULL, NULL, NULL);
-
- if (activity < 0 && errno != EINTR) {
- perror("select error");
- }
-
- // 处理新连接
- if (FD_ISSET(lfd, &rdtemp)) {
- if ((cfd = accept(lfd, (struct sockaddr *)&cliaddr, &cliLen)) < 0) {
- perror("accept failed");
- exit(EXIT_FAILURE);
- }
-
- printf("New connection: socket fd is %d, IP is : %s, port : %d\n",
- cfd, inet_ntoa(cliaddr.sin_addr), ntohs(cliaddr.sin_port));
-
- // 将新的通信套接字加入集合
- for (i = 0; i < MAX_CLIENTS; i++) {
- if (client_socket[i] == 0) {
- client_socket[i] = cfd;
- break;
- }
- }
-
- // 更新最大文件描述符
- if (cfd > maxfd) {
- maxfd = cfd;
- }
-
- FD_SET(cfd, &readfds); // 将新连接的套接字加入集合
- }
-
- // 处理客户端消息
- for (i = 0; i < MAX_CLIENTS; i++) {
- cfd = client_socket[i];
-
- if (FD_ISSET(cfd, &rdtemp)) {
- valread = read(cfd, buffer, sizeof(buffer));
-
- if (valread == 0) {
- // 客户端关闭连接
- getpeername(cfd, (struct sockaddr *)&cliaddr, &cliLen);
- printf("Host disconnected, IP %s, port %d, fd %d\n",
- inet_ntoa(cliaddr.sin_addr), ntohs(cliaddr.sin_port), cfd);
-
- close(cfd);
- FD_CLR(cfd, &readfds); // 从集合中移除套接字
- client_socket[i] = 0;
- } else {
- // 回显客户端消息
- buffer[valread] = '\0';
- printf("Client %s, fd %d: %s\n", inet_ntoa(cliaddr.sin_addr), cfd, buffer);
-
- // 发送回显数据给客户端
- send(cfd, buffer, strlen(buffer), 0);
- }
- }
- }
- }
-
- return 0;
- }

在上面的代码中,创建了两个fd_set变量,用于保存要检测的读集合:
- // 初始化检测的读集合
- fd_set rdset; //要检测的位置集合
- fd_set rdtemp; //这些检测位置集合中有变化的的集合,就是有就绪状态的文件描述符
rdset用于保存要检测的原始数据,这个变量不能作为参数传递给select函数,因为在函数内部这个变量中的值会被内核修改,函数调用完毕返回之后,里边就不是原始数据了,大部分情况下是值为1的标志位变少了,不可能每一轮检测,所有的文件描述符都是就行的状态。因此需要通过rdtemp变量将原始数据传递给内核,select() 调用完毕之后再将内核数据传出,这两个变量的功能是不一样的。
客户端代码:
- #include <stdio.h>
- #include <stdlib.h>
- #include <unistd.h>
- #include <string.h>
- #include <arpa/inet.h>
-
- int main()
- {
- // 1. 创建用于通信的套接字
- int fd = socket(AF_INET, SOCK_STREAM, 0);
- if(fd == -1)
- {
- perror("socket");
- exit(0);
- }
-
- // 2. 连接服务器
- struct sockaddr_in addr;
- addr.sin_family = AF_INET; // ipv4
- addr.sin_port = htons(9999); // 服务器监听的端口, 字节序应该是网络字节序
- inet_pton(AF_INET, "127.0.0.1", &addr.sin_addr.s_addr);
- int ret = connect(fd, (struct sockaddr*)&addr, sizeof(addr));
- if(ret == -1)
- {
- perror("connect");
- exit(0);
- }
-
- // 通信
- while(1)
- {
- // 读数据
- char recvBuf[1024];
- // 写数据
- // sprintf(recvBuf, "data: %d\n", i++);
- fgets(recvBuf, sizeof(recvBuf), stdin);
- write(fd, recvBuf, strlen(recvBuf)+1);
- // 如果客户端没有发送数据, 默认阻塞
- read(fd, recvBuf, sizeof(recvBuf));
- printf("recv buf: %s\n", recvBuf);
- sleep(1);
- }
-
- // 释放资源
- close(fd);
-
- return 0;
- }

虽然使用select这种IO多路转接技术可以降低系统开销,提高程序效率,但是它也有局限性:
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。