1. 什么是IO多路转接
IO操作方式有两种
阻塞等待
- 优点:不占用CPU时间片
- 缺点:同一时刻只能处理一个操作,效率低下
非阻塞(忙轮询)
- 优点是提高了程序的执行效率,缺点是需要占用更多的CPU和系统资源
- 只有一个任务时
- 多个任务
对于非阻塞方式多任务的场景,也就是上图中的情况,解决方法是使用IO多路转接技术,常用的IO多路转接技术包括select/poll/epoll。
select/poll —— 实现方式为线性表遍历
在通信的时候,委托内核去检测连接到server的client,有哪些client是在通信的,比如说有10个client连接,但是只有6个发送了数据,要把这6个client找出来,这个工作由内核去做。但是内核只能给出发送数据的client的个数6,至于是哪6个client,需要进程自己去遍历。
在这两种方式下,可以这么理解,select 代收员比较懒, 她只会告诉你有几个快递到了,但是具体是哪个快递,你需要挨个遍历一遍。
实际上,多路转接就是进程委托内核去做一些事情,在进程中只要调用select/poll/epoll就可以了,这样就实现了多任务的处理。
epoll —— 通过红黑树实现
epoll代收快递员很勤快,她不仅会告诉你有几个快递到了,还会告诉你是哪个快递公司的快递。
通过上面介绍已经大体了解了多路转接是什么,那么多路转接技术是怎么工作的呢?
先构造一张有关文件描述符的列表,将要监听的文件描述符添加到该表中。(类似于阻塞信号集)
然后调用一个函数,监听该表中的文件描述符,直到这些描述符表中的一个进行I/O操作时,该函数才返回。(select/poll/epoll).
该函数为阻塞函数
- 函数对文件描述符的检测操作是由内核完成的
- 在返回时,它告诉进程有多少(哪些)描述符要进行I/O操作。
- 文件描述符对应的是内核缓冲区,监听文件描述符,实际上就是监听内核缓冲区的read区,因为read区有数据就说明有进程给我发送数据。
- select/poll会返回发生IO操作的进程个数;
- epoll返回发生IO操作的进程个数,以及是哪些进程。
2. IO多路转接技术——select详解
(1)select()函数详解
- 函数原型
int select( int nfds,
fd_set *readfds, /*传入传出参数 | 传入传出参数:传入函数之前,指针指向的内存就已经有值了,函数执行完毕后,这个内存的值可能发生变化,并通过指针传递出来。*/
fd_set *writefds,
fd_set *exceptfds,
struct timeval *timeout );
- 函数参数
settitimer()
struct {
long tv_sec;
long tv_usec;
};
/*赋值的时候,秒和微秒都要赋值,因为最终结果是二者之和,否则得到的就是一个随机数。*/
- 函数返回值
(2)文件描述符操作函数
- 全部清空
- 从集合中删除某一项
- 将某个文件描述符添加到集合
(3)使用select函的优缺点
- 优点:跨平台
- 缺点:
(4)select工作过程分析
首先假设客户端A、B、C、D、E、 F连接到服务器,分别对应文件描述符 3、4、100、101、102、103(fd都是server端的,每有一个client连接到server,都会产生一个用于通信的fd)。
现在,server通过select函数来委托内核去检测客户端ABCDEF是否给server发数据了。
在上面的图中
(5)select多路转接代码实现
#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
#include<sys/types.h>
#include<string.h>
#include<sys/socket.h>
#include<arpa/inet.h>
#include<ctype.h>
intmain(int argc, constchar* argv[])
{
if(argc < 2)
{
printf("eg: ./a.out port\n");
exit(1);
}
structsockaddr_inserv_addr;
socklen_t serv_len = sizeof(serv_addr);
int port = atoi(argv[1]);
// 创建套接字
int lfd = socket(AF_INET, SOCK_STREAM, 0);
// 初始化服务器 sockaddr_in
memset(&serv_addr, 0, serv_len);
serv_addr.sin_family = AF_INET; // 地址族
serv_addr.sin_addr.s_addr = htonl(INADDR_ANY); // 监听本机所有的IP
serv_addr.sin_port = htons(port); // 设置端口
// 绑定IP和端口
bind(lfd, (struct sockaddr*)&serv_addr, serv_len);
// 设置同时监听的最大个数
listen(lfd, 36);
printf("Start accept ......\n");
structsockaddr_inclient_addr;
socklen_t cli_len = sizeof(client_addr);
// 最大的文件描述符
int maxfd = lfd;
// 文件描述符读集合
fd_set reads, temp;
// init 初始化
FD_ZERO(&reads);
FD_SET(lfd, &reads);
while(1)
{
// 委托内核做IO检测
temp = reads;
//在Linux下maxfd必须写正确,要及时更新;在Windows下可以随便写
int ret = select(maxfd+1, &temp, NULL, NULL, NULL);
if(ret == -1)
{
perror("select error");
exit(1);
}
// 客户端发起了新的连接
// 用于监听的文件描述符有且只有1个lfd,lfd对应位为1,说明有新的连接请求
if(FD_ISSET(lfd, &temp))
{
// 接受新连接,返回一个用于通信的cfd,并加入到原始的读集合reads(备份)
// 接受连接请求 - accept不阻塞 //因为只要进入if语句,就说明有新连接
int cfd = accept(lfd, (struct sockaddr*)&client_addr, &cli_len);
if(cfd == -1)
{
perror("accept error");
exit(1);
}
char ip[64];
printf("new client IP: %s, Port: %d\n",
inet_ntop(AF_INET, &client_addr.sin_addr.s_addr, ip, sizeof(ip)),
ntohs(client_addr.sin_port));
// 将cfd加入到待检测的读集合中 - 下一次就可以检测到了
// 下次循环的时候,如果cfd发生变化就可以检测到,当前循环是检测不到的,这也说明select是异步的。
FD_SET(cfd, &reads);
// 更新最大的文件描述符//maxfd决定了内核遍历检测的范围
maxfd = maxfd < cfd ? cfd : maxfd;
}
// 已经连接的客户端有数据到达
// 需要遍历去判断哪个client通信的cfd发生了变化(说明通信了),变化则read读取数据。
// i为啥是从lfd+1开始的?
// 因为lfd是第一个创建的文件描述符,而文件描述符创建的规则是当前最小空闲,所以lfd+1应该就是第一个用于通信的文件描述符cfd。
for(int i=lfd+1; i<=maxfd; ++i)
{
if(FD_ISSET(i, &temp))
{
char buf[1024] = {0};
int len = recv(i, buf, sizeof(buf), 0);
if(len == -1)
{
perror("recv error");
exit(1);
}
elseif(len == 0)
{
printf("客户端已经断开了连接\n");
close(i);
// 从读集合中删除
FD_CLR(i, &reads);
}
else
{
printf("recv buf: %s\n", buf);
send(i, buf, strlen(buf)+1, 0);
//strlen(buf)不包括'\0',所以需要+1,并且前提是buf已经被初始化为0
//必须把'\0'发出去来表示字符串结束,否则数据可能出错(比实际数据长),出现乱码
}
}
}
}
close(lfd);
return0;
}