cpp网络编程基础(四)
IO多路复用模型
IO多路复用就是用一个线程/进程处理多个TCP以减少系统开销。
IO多路复用模型有三种:select(1024),poll(数千),epoll(百万)。
重点概念:
-
网络通讯的读事件:
- 已连接的队列中已经有准备好的socket(有新的客户端连接上来)
- 接收缓存中有数据可以读(对端发送的报文已到达)
- tcp连接已断开(对端调用close()函数关闭了连接)
-
网络通讯的写事件:
发送缓冲区没有满,可以写入数据(可以向对端发送报文)
1. select模型
1 | //select模型需要调用select函数 |
select():允许程序同时监视多个文件描述符,检测他们的变化(如数据可读可写),从而高效的管理多个I/O操作,而不需要单独为每个操作创建独立的线程或进程,同时也支持非阻塞IO。
-
fd_set:这个结构体是用来存储链接的集合,本质是一个32位的整形数组,所以共有$4832=1024$bitmap位其中提供了四个宏来操作位图:
void FD_CLR(int fd,fd_set *set):将对应socket从位图中删除int FD_ISSET(int fd,fd_set *set):判断socket是否在位图中,如果不在返回0,如果在返回大于0的值void FD_SET(int fd,fd_set *set):将socket加入到对应位图中FD_ZERO(fd_set* set):初始化位图,将其中1024个位置都置为0
1
2
3fd_set readfds; //需要监视的读事件的socket集合,大小为16字节(1024位)的bitmap
FD_ZERO(&readfds); //初始化readfds
FD_SET(listensock,&readfds);//把服务端监听的socket加入bitmap -
timeval:是超时时间的结构体:成员有:
tv_sec和tv_usec分别是超时的秒和微秒1
2
3struct timeval timeout;
timeout.tv_sec = 10;//秒
timeout.tv_usec = 0;//微秒 -
fds:实际操作的bitmap的大小+1,告诉要操作的bitmap -
readfds:需要监视的读的bitmap -
writefds:需要监视的写的bitmap -
exceptfds:需要监视的异常的bitmap,select也可以监视普通IO,在监视普通IO时多用网络编程中不常用。 -
timeout:超时时间阈值设置- 如果为NULL,那么select会无限等待直到至少有一个文件描述符就绪
- 如果两个参数都设置为0,select会立即返回用于轮询
- 设置具体时间后,在这段时间内select会等待文件描述符就绪,时间结束会返回超时报错。
使用:
1 | int maxfd = listensock;//获取readfds中实际的最大值 |
select返回值:
-
如果是小于零则调用失败
-
如果是等于0则是超时
-
如果成功调用则是返回成功发生事件的个数
具体使用:
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
36for (int eventfd=0;eventfd<=maxfd;eventfd++){//循环遍历bitmap
if(FD_ISSET(eventfd,&tmpfds)==0)continue;//如果当前位置为0则没任何事件
//如果是listen发生了事件则是说明有客户端连接上来了(已经有准备好的socket)
if(eventfd == listensock){
struct sockaddr_in client;
socklen_t len =sizof(client);
int clientsock = accept(listensock,(struct sockaddr*)&client,&len);
if(listensock < 0){
perror("accept() faild");
continue;
}
std::cout << "accept client(socket ="<< clientsock<<") ok"<<std::endl;
FD_SET(clientsock,&readfds); //把新连接上来的客户端标志位设置为1
if(maxfd < clientsock) maxfd = clientsock;//更新maxfd
}else{//如果是客户端连接上的socket有事件,表示接收缓存中有数据可以读,或者有客户端断开连接
char buffer[1024];
memset(buffer,0,sizeof(buffer));
if(recv(eventfd,buffer,sizeof(buffer),0)<=0){
std::cout << "client(eventfd = "<< eventfd<<")disconnected"<<std::endl;
close(eventfd);
FD_CLR(eventfd,&readfds);
if(eventfd == maxfd)
for(int ii =maxfd;ii>0;ii--)
if(FD_ISSET(ii,&readfds)){
maxfd = ii;
break;
}
}else{
//如果有报文发送过来
std::cout << "recv(eventfd="<< eventfd<<"):"<<buffer<<std::endl;
send(eventfd,buffer,strlen(buffer),0);//暂时把报文发送回去
}
}
} -
select-写事件:
- 如果tcp发送缓存区没有满,那么,socket连接是可写的。
- 一般来说,发送缓冲区不容易被填满。
- 如果发送数据量太大,或者网络带宽不够,发送缓冲区有填满的可能性。
- 综上所述,一般业务中不需要关心监听写事件
-
select-水平触发:
select()监视的socket如果发生了事件,select()会返回(通知应用程序处理事件)。- 如果事件没有被处理,再次调用
select()的时候会立即再通知。
-
select-压力测试:大概可以处理12w个事务
-
select-存在的问题:
- 采用轮询方式扫描bitmap,性能会随着socket数量增多而下降
- 每次调用
select(),需要拷贝bitmap - bitmap的大小(单个进程/线程打开的socket数量)由
FD_SETSIZE宏设置,默认是1024个,可以修改,但是效率将更低。
2. poll模型
poll模型大体与select有些相似,再poll模型中要使用pollfd来存放需要监视的socket
1 | pollfd fds[1024]; |
poll的具体使用
1 | int poll(struct pollfd* nfds,nfds_t,int timeout); |
nfds:结构体数组地址nfds_t:结构体最大数+1timeout:超时时间(ms)
具体代码:
1 | //初始化服务器监听端口获取listensock |
- poll模型缺点:
- 在程序中,poll的数据结构是数组,传入内核后转换成了链表
- 每调用一次
select()需要拷贝两次bitmap,poll拷贝一次结构体数组 - poll监视没有1024的显示,但是同样是遍历的方式,所以依旧是监视的socket越多,效率越低
3. epoll模型
epoll模型解决了上面两个模型的痛点,再效率方面有了极大提升,根据以下介绍顺序逐步解释并且使用epoll
epoll的使用:
1 | int epoll_create(); //创建epoll句柄 |
epoll_create()在linux2.6.8版本之前有一个int size的参数,在此之后就没有了被忽略掉了,填任意大于0的数即可
epoll的数据结构epoll_event:
1 | struct epoll_event{ |
epoll_event的使用:
1 | //为服务端的listensock准备可读事件 |
epoll_ctl的使用
1 | int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);//epoll_ctl定义 |
epfd:为创建的epoll句柄op:为操作类型:EPOLL_CTL_ADD:添加EPOLL_CTL_DEL:删除EPOLL_CTL_MOD:修改
fd:需要操作的目标文件符event:需要监控的事件类型
最后声明一组epoll_events来接收返回事件:
1 | epoll_event evs[10];//存放epoll返回的事件 |
后续使用如下:
1 | while(true){ |




