多路复用I/O
多路I/O复用表示支持多个任务同时对某一进程的I/O进程操作,普通的read/write只能实现同一时间操作一个,无法实现网络通信的并发操作。那么多路复用I/O分为三种机制:select/poll/epoll
多进程多线程的socket模型具有明显缺陷
1.占用内存多 2.进程(线程)切换时间多。3.进程(线程)之间同步麻烦
多路复用的解决理念:
在主控线程中将需要监控的文件描述符保存到文件描述符集中,该文件描述符集为一个位图,我们知道文件描述符正常情况下总是累加上去的,也是一个整数,因此这个整数巧好可以表示该文件描述符在位图的位置(例如位图上的3号位置为1,表示文件描述符等于3有事件发生,否则为空闲。),将服务器和客户端的文件描述符加入到该(文件描述符集)位图中进行监控,若有事件发生则才处理。
API分析:
-
int select(int nfds, fd_set *readfds, fd_set *writefds,fd_set *exceptfds, struct timeval timeout);
nfds: 监控的文件描述符集里最大文件描述符加1,因为此参数会告诉内核检测前多少个文件描述符的状态
readfds:监控有读数据到达文件描述符集合,传入传出参数
writefds:监控写数据到达文件描述符集合,传入传出参数
exceptfds:监控异常发生达文件描述符集合,如带外数据到达异常,传入传出参数
timeout:定时阻塞监控时间,3种情况
1.NULL,永远等下去
2.设置 timeval,等待固定时间
3.设置 timeval里时间均为0,检查描述字后立即返回,轮询
struct timeval {
long tv_sec; / 秒 /
long tv_usec; / 微妙 */
}; -
void FD_CLR(int fd, fd_set *set); 把文件描述符集合里fd清0
-
int FD_ISSET(int fd, fd_set *set); 测试文件描述符集合里fd是否置1
-
void FD_SET(int fd, fd_set *set); 把文件描述符集合里fd位置1
-
void FD_ZERO(fd_set *set); 把文件描述符集合里所有位清0
函数2-5均为宏定义函数
通过上述API可知道:
select监控的有三种事件的文件描述符集(读、写、异常)。以及可设置阻塞时间,大大地解决死等的弊端,select无请求后,主控函数将执行后续指令。
但是要注意的是:
- select监听的文件描述符集最大支持1024,大小可用宏定义表示FD_SETSIZE( = 1024),解决1024以下客户端时使用select是很合适的,后面会讲解poll,解决1024的限制。
- select采用的是轮询模型,数量级在千级还是很适合,但是如果更大会导致每次监控都要从文件描述符集从0到maxfd+1进行遍历,会大大降低服务器响应效率,不应在select上投入更多精力
扩展:
int pselect(int nfds, fd_set *readfds, fd_set *writefds,fd_set *exceptfds, const struct timespec *timeout,const sigset_t *sigmask);
参数timeout类型:
struct timespec {
long tv_sec; /* 秒 */
long tv_nsec; /* 纳秒 */
};
用sigmask替代当前进程的阻塞信号集,调用返回后还原原有阻塞信号集
我们知道调用pause()函数时,会因多任务竞态导致pause()无法被定时信号唤醒,而suspend则集成睡眠与信号屏蔽的功能,保证信号提前抵达,主程序从其他任务切换回来后能够正常执行。该pselect也是同样功能,不过该用法不多。
关于网络并发优化问题:
- 大量客户端连接到服务器后,会从0遍历到maxfd,返回就绪态任务数量,再根据数量在通过FD_ISSET重新遍历判断事件发生的文件描述符,如果某些客户端断开连接(文件描述符小于maxfd)造成浪费在遍历已断开连接的客户端上的时间,由于select函数固定,第一次遍历优化不方便(修改内核源码),我们可以对第二次遍历进行优化。
- 我们知道文件描述符从0-2是标准输入、输出、错误,这三个不需要遍历。
- 如果在处理客户端数据时,某一次read没有对数据读完,那么造成重新进行下一次时select,获取上一次未处理完的文件描述符,从0开始遍历到maxfd,对上一次的进行再一次操作,效率十分低。
对应的代码优化:
- 定义一个客户端数组,初始化为-1,表示空闲,等待加载客户端信息,按顺序记录下连接成功的客户端文件描述符到该数组,客户端遍历该数组注册到元素值为-1的位置,若某一客户端断开连接,首先会产生读事件(即客户端会向服务器发送FIN信号),在服务器端select监控到有读事件发生,遍历客户端数组,并用FD_ISSET(fd,&readfds)判断具体是哪一个客户端产生事件,获取到客户端文件描述符后,调用close关闭服务器端打开的客户端文件描述符,在用FD_CLR清除监控该文件描述符,并将该客户端对应的数组位置等于-1,表示空闲,这样在第二次遍历时,我们可以通过遍历客户端数组提高效率,也同时解决了问题2,客户端数组只存有客户端文件描述符。
- 对读缓存区循环读,直到返回EAGAIN在处理数据。
附上详细注释的代码(只包含服务器端):
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/socket.h>
#include "wrap.h"
#define SERV_PORT 8000
#define MAXLINE 128
int main(void)
{
int serv_fd,connfd,sockfd,maxfd,maxi,i;
int nready, client[FD_SETSIZE]; //FD_SETSIZE=1024,select模型最大监控数
ssize_t n;
char buf[MAXLINE];
char str[INET_ADDRSTRLEN];
struct sockaddr_in servaddr,cliaddr;
struct socklen_t cliaddr_len;
fd_set rset, allset;
//1.创建socket
serv_fd = Socket(AF_INET,SOCK_STREAM,0);
//2.绑定socket对应网络进程
bzero(&servaddr,sizeof(servaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
servaddr.sin_port = htons(SERV_PORT);
Bind(serv_fd, &servaddr, sizeof(servaddr));
//3.创建监听队列
listen(serv_fd,20);
//4.1.select监听的最大文件描述符,告诉内核检测前多少个文件描述符的状态
maxfd = serv_fd;
maxi = -1;
//4.2.初始化记录客户端socket号的缓冲区,空闲则为-1,非-1表示已有客户占有
for(i = 0; i< FD_SETSIZE; i++)
client[i] = -1;
//4.3.初始化select读/写/出错的文件描述符集合
FD_ZERO(&allset);
//4.4.将服务器的网络进程加入到该监听队列
FD_SET(serv_fd,&allset);
for(;;){
//rset作为操作对象,备份,每次循环都要将监控的文件描述符集的状态重新赋值
rset = allset;
//4.5.select开始阻塞监听读事件发生,rset的变化由内核实现
nready = select(maxfd + 1, &rset, NULL, NULL, NULL);
//5.1.循环select错误判断,成功返回描述符集内事件发生的个数,没有则为0
if(nready < 0)
perr_exit("select error");
//5.2.测试描述符集内服务器是否发生事件
if(FD_ISSET(serv_fd, &rset)){
cliaddr_len = sizeof(cliaddr);
//5.2.1.调用accept与客户端进行连接
connfd = Accept(serv_fd, (struct sockaddr *)&cliaddr,\
&cliaddr_len);
printf("received form %s at PORT %d\n",\
inet_ntop(AF_INET,&cliaddr.sin_addr.s_addr,str,sizeof(str)),\
ntohs(cliaddr.sin_port));
//5.2.2.1.将该客户端的信息保存在数组中
for(i = 0; i<FD_SETSIZE; i++)
if(client[i]<0){
client[i] = connfd;
break;
}
//5.2.2.2.若自定义保持客户端信息的数组已满,则报错退出
if(i == FD_SETSIZE){
fputs("too many clients\n", stderr);
exit(1);
}
//5.2.3.将客户端文件描述符加入监控的文件描述符集中
FD_SET(connfd, &allset);
//5.2.4.对要监控的最大文件描述符进行修改
if(connfd > maxfd)
maxfd = connfd;
//5.2.5.更改client数组下标
if(i > maxi)
maxi = i;
/*5.2.6.如果没有更多的就绪文件描述符继续回到上面select阻塞监听,负责处理未
处理完的就绪文件描述符*/
if(--nready == 0)
continue;
}
//循环判断客户端是否有事件发生
//6.获取已连接的客户端信息
for(i = 0; i<=maxi; i++){
if((sockfd = client[i])<0)
continue;
//6.1.判断当前客户端是否有事件发生
if(FD_ISSET(sockfd, &rset)){
//6.1.1.阻塞接受客户端数据
n = Read(sockfd, buf, MAXLINE);
//6.1.2.1.若为0,表示客户端异常断开连接
if(n == 0){
Close(sockfd);
FD_CLR(sockfd, &allset);
client[i] = -1;
//6.1.2.2.否则,处理数据并回传给客户端
}else{
int j;
for(j = 0; j < n; j++)
buf[j] = toupper(buf[j]);
Write(sockfd, buf, n);
}
//6.1.3. 跳出循环
if(--nready == 0)
break;
}
}
}
//7.关闭服务器
close(serv_fd);
return 0;
}