IO多路复用
IO多路复用
引言
在上篇[“理解网络IO”]中实现的一请求一线程的方式优点是其代码逻辑简单,但缺点也是很明显的,不太利于并发,若每个线程8M,16G内存大概能做到1k的并发,无法做到更多。 故而需要用多路复用技术如select、poll、epoll。
select
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
fd_set rfds, rset;
//将rfds集合清空
FD_ZERO(&rfds);
//将ID为sockfd的IO置1
FD_SET(sockfd,&rfds);
int maxfd = sockfd;
while(1){
rset = rfds;
//看有几个fd就绪
int nready = select(maxfd+1, &rset, NULL, NULL, NULL);
//首先看监听的fd是否就绪
if(FD_ISSET(sockfd,&rset)){ // accept-->listenfd
//创建客户端fd
int clientfd = accept(sockfd,(struct sockaddr*)&clientaddr, &len);
printf("accept finished %d\n",clientfd);
//将当前fd加入集合
FD_SET(clientfd,&rfds);
if(clientfd > maxfd) maxfd = clientfd;
}
// recv
//监听到有fd后遍历所有非监听fd来进行数据的接收
int i = 0;
for(i = sockfd+1; i <= maxfd; i++){
if(FD_ISSET(i,&rset)){
char buffer[1024] = {0};
int count = recv(i,buffer,1024,0);
if(count == 0){ //disconnect
printf("client disconnect %d\n",i);
close(i);
FD_CLR(i, &rfds);
continue;
}
printf("RECV: %s\n",buffer);
count = send(i,buffer,count,0);
}
}
}
select核心是通过集合set来对IO进行管理,理论上每个IO只占用3个bit来表示所有信息,这极大地减少了内存的占用
poll
poll的多路复用是通过定义下面的pollfd结构体来代替set集合实现的一个pollfd代表一个IO。
1
2
3
4
5
struct pollfd {
int fd; /* 要监视的文件描述符 */
short events; /* 要监视的事件(输入) */
short revents; /* 实际发生的事件(输出) */
};
完整代码如下,逻辑与select很像:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
//pollfd这里fds数组结构体跟select中的set集合是一个意思
struct pollfd fds[1024] = {0};
fds[sockfd].fd = sockfd;
fds[sockfd].events = POLLIN; //关注读事件
int maxfd = sockfd;
while(1){
//参数较少
int nready = poll(fds, maxfd+1, -1);
if(fds[sockfd].revents & POLLIN){ //accept
int clientfd = accept(sockfd,(struct sockaddr*)&clientaddr, &len);
printf("accept finished %d\n", clientfd);
fds[clientfd].fd = clientfd;
fds[clientfd].events = POLLIN;
if(clientfd > maxfd) maxfd = clientfd;
}
int i = 0;
for(i = sockfd+1; i <= maxfd; i++){
if(fds[i].revents & POLLIN){
char buffer[1024] = {0};
int count = recv(i,buffer,1024,0);
if(count == 0){ //disconnect
printf("client disconnect %d\n",i);
close(i);
fds[i].fd = -1;
fds[i].events = POLLIN;
continue;
}
printf("RECV: %s\n",buffer);
count = send(i,buffer,count,0);
}
}
}
epoll(重点)
背景:
在服务器端用的最多的系统是Linux,其最核心的原因就是有epoll支持。在Linux 2.4版本还没有epoll,server端没有人使用都是用的Unix或Windows,但在Linux 2.6之后引入了epoll,这使得server端能够对IO做到更多操作。
epoll的理解如上图所示,epoll的工作即为在一段固定时间去检查就绪fd里拿取fd来进行操作。
与select对比优势:
- 能做到100wIO。
- 100wIO是慢慢积累起来,来一个添加一个,且处理时只处理就绪IO就行了,而select需要集合一起放进函数。
- epoll创立了就绪集与整个集合,这两个分开能使我们更好地只关注需要处理的事件。
epoll需要掌握的主要有三个接口函数:
1
2
3
int epoll_create(int size);
int epoll_ctl(int epfd, int op, int fd, struct epoll_event* event);
int epoll_wait(int epfd,struct epoll_event *events,int maxevents,int timeout);
epoll中也有一个结构体叫epoll_event
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
int epfd = epoll_create(1024);
struct epoll_event ev;
ev.events = EPOLLIN;
ev.data.fd = sockfd;
epoll_ctl(epfd, EPOLL_CTL_ADD, sockfd, &ev);
while(1){
struct epoll_event events[1024] = {0};
int nready = epoll_wait(epfd, events, 1024, -1);
int i = 0;
for(i = 0; i < nready; i++){
int connfd = events[i].data.fd;
if(connfd == sockfd){
int clientfd = accept(sockfd,(struct sockaddr*)&clientaddr, &len);
printf("accept finished %d\n", clientfd);
ev.events = EPOLLIN;
ev.data.fd = clientfd;
epoll_ctl(epfd, EPOLL_CTL_ADD, clientfd, &ev);
} else {
char buffer[1024] = {0};
int count = recv(connfd,buffer,1024,0);
if(count == 0){ //disconnect
printf("client disconnect %d\n",connfd);
close(connfd);
epoll_ctl(epfd, EPOLL_CTL_DEL, connfd, NULL);
continue;
}
printf("RECV: %s\n",buffer);
count = send(connfd,buffer,count,0);
}
}
}
本文由作者按照 CC BY 4.0 进行授权
