I/O多路复用
通过这种方式可以同时监测多个文件描述符并且这个过程是阻塞的,一旦检测到有文件描述符就绪,程序的阻塞就会被解除,之后就可以基于这些就绪的文件描述符进行通信。通过这种方式在单线程 / 进程的场景下也可以在服务器端实现并发。
- 多线程 / 多进程并发
- 主线程 / 父进程:调用 accept() 函数接受客户端连接请求。如果没有新的客户端的连接请求,当前线程 / 进程会阻塞。如果有新的客户端连接请求则解除阻塞,建立连接。
- 子线程 / 子进程:与建立连接的客户端通信。调用 read() / recv() 接收客户端发送的通信数据,如果没有通信数据,当前线程 / 进程会阻塞,数据到达之后阻塞自动解除。调用 write() / send() 给客户端发送数据,如果写缓冲区已满,当前线程 / 进程会阻塞,否则将待发送数据写入写缓冲区中。
- IO 多路复用并发
使用 IO 多路转接函数委托内核检测服务器端所有的文件描述符,这个检测过程会导致进程 / 线程的阻塞,如果检测到已就绪的文件描述符阻塞解除,并将这些已就绪的文件描述符传出。根据类型对传出的所有已就绪文件描述符进行判断,并做出不同的处理
- 监听的文件描述符:与客户端建立连接。此时调用 accept() 不会导致程序阻塞的,因为监听的文件描述符是已就绪的。
- 通信的文件描述符:调用通信函数和已建立连接的客户端通信。调用 read() / recv() 不会阻塞程序,因为通信的文件描述符是就绪的,读缓冲区内已有数据。调用 write() / send() 不会阻塞程序,因为通信的文件描述符是就绪的,写缓冲区不满,可以往里面写数据。
select
#include <sys/select.h>
struct timeval
{
time_t tv_sec; // 秒
suseconds_t tv_usec; // 微秒
};
int select(
int nfds, // 委托内核检测的下列三个集合中最大的文件描述符+1
// 内核需要线性遍历这些集合中的文件描述符,这个值是循环结束的条件
fd_set *readfds, // 传入传出参数,内核只检测该集合中文件描述符对应的读缓冲区
fd_set *writefds, // 传入传出参数,内核只检测该集合中文件描述符对应的写缓冲区
fd_set *exceptfds, // 传入传出参数,内核只检测该集合中文件描述符是否异常
struct timeval *timeout // 超时时长,用来强制解除函数阻塞
);
// 返回值:发生错误返回值为-1,超时返回值为0,成功返回就绪的文件描述符的个数。
// 将集合中所有的文件描述符对应的标志位设置为0
void FD_ZERO(fd_set *set);
// 将fd对应的标志位设置为0
void FD_CLR(int fd, fd_set *set);
// 将fd对应的标志位设置为1
void FD_SET(int fd, fd_set *set);
// fd对应的标志位是0还是1
int FD_ISSET(int fd, fd_set *set);
局限性:
- 待检测的文件描述符集合需要频繁的在用户区和内核区之间进行数据的拷贝,效率低。
- 文件描述符集合通过线性表描述,内核线性地遍历文件描述符集合。
- 在32位机器上,能够检测的最大文件描述符个数上限默认是1024,但可以修改源码重新编译内核。
- 函数只返回发生了事件的文件描述符的个数,想知道是谁发生了事件,同样需要遍历。
优势:
- 可以跨平台使用。
示例
// server.c
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <arpa/inet.h>
int main()
{
// 1. 创建监听的文件描述符
int lfd = socket(AF_INET, SOCK_STREAM, 0);
// 2. 绑定
struct sockaddr_in addr;
addr.sin_family = AF_INET;
addr.sin_port = htons(9999);
addr.sin_addr.s_addr = INADDR_ANY;
bind(lfd, (struct sockaddr *)&addr, sizeof(addr));
// 3. 设置监听
listen(lfd, 128);
// 最大的文件描述符
int maxfd = lfd;
// 委托内核检测的读集合
fd_set rdset;
// 读事件就绪的文件描述符集合
fd_set rdtemp;
FD_ZERO(&rdset);
// 将用于监听的文件描述符设置到集合中
FD_SET(lfd, &rdset);
// 应该让内核持续检测
while (1)
{
rdtemp = rdset;
int num = select(maxfd + 1, &rdtemp, NULL, NULL, NULL);
// 检测用于监听的文件描述符
if (FD_ISSET(lfd, &rdtemp))
{
struct sockaddr_in cliaddr;
int cliLen = sizeof(cliaddr);
// 接受连接请求(不会阻塞)
int cfd = accept(lfd, (struct sockaddr *)&cliaddr, &cliLen);
// 将用于通信的文件描述符设置到集合中
FD_SET(cfd, &rdset);
// 重置最大的文件描述符
maxfd = cfd > maxfd ? cfd : maxfd;
}
// 检测用于通信的文件描述符
for (int i = 0; i < maxfd + 1; ++i)
{
if (i != lfd && FD_ISSET(i, &rdtemp))
{
// 接收数据
char buf[10] = {
0};
int len = read(i, buf, sizeof(buf));
if (len == 0)
{
printf("客户端关闭了连接...\n");
// 将检测的文件描述符从读集合中删除
FD_CLR(i, &rdset);
close(i);
}
else if (len > 0)
{
// 发送接收到的数据
printf("客户端:%s\n", buf);
write(i, buf, strlen(buf) + 1);
}
else
{
// 异常
perror("read");
}
}
}
}
return