阻塞&非阻塞IO
基本概念:
- 阻塞:在进/线程中,发起一个调用时,在调用返回前,进/线程会被阻塞等待,等待中进/线程让出cpu的使用权。
- 非阻塞:在进/线程中,发起一个调用时,会立刻返回。
- 会阻塞的四个函数:
connect()、accept()、send()、recv()。
应用场景:
- 传统的网络服务端中(每连接每线程/进程),采用阻塞IO。
- 在IO复用模型中,事件循环不能被阻塞在任何环节,所以应该采用非阻塞IO。
非阻塞IO-connect()
- 对非阻塞的IO调用connect()函数会返回失败,errno ==EINPROGRESS。
- 对非阻塞的IO调用connect()函数后,如果socket的状态是可写的,证明连接是成功的,否则是失败的。
设置socket为非阻塞:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| #include <fcntl.h>
int setnonblocking(int fd){ int flags; if((flags = fcntl(fd,F_GETFL,0))==-1) flags = 0; return fcntl(fd,F_SETFL,flags|O_NONBLOCK); }
int sockfd if((sockfd =socket(AF_INET,SOCK_STREAM,0))<0){printf("socket()failed\n");return -1;}
setnonblocking(sockfd);
|
非阻塞IO-accept()
- 对非阻塞的IO调用accept(),如果已连接队列中没有socket,函数立即返回失败,errno == EAGAIN
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
| #include <fcntl.h>
int setnonblocking(int fd){ int flags; if((flags = fcntl(fd,F_GETFL,0))==-1) flags = 0; return fcntl(fd,F_SETFL,flags|O_NONBLOCK); }
int listenfd = initserver(IP)
setnonblocking(listenfd);
if(accept(listensock,0,0)==-1){ if(errno != EAGAIN){ perror("accept()"); return -1; } }
|
非阻塞IO-recv和send
水平触发&边缘触发
水平触发
- 读事件:如果epoll_wait触发了读事件,表示数据可读,如果程序没有把数据读完,再次调用了epoll_wait,将立即再次触发读事件
- 写事件:如果发送的缓冲区没有满,表示可以写入数据,只要缓冲区没有被写满,再次调用epoll_wait的时候将立即再次触发写事件。
边缘触发
- 读事件:epoll_wait触发读事件后,不管程序有没有处理读事件,epoll_wait都不会再触发读时间,只有当新数据到达时,才再次触发读事件。
- 写事件:epoll_wait触发写事件之后,如果缓冲区仍可以写(发送缓冲区没有满),epoll_wait不会再次触发写事件,只有当发送缓存区由满变成不满时,才再次触发写事件。
1 2 3 4 5 6 7 8
| epollevent ev; ev.data.fd = listensock; ev.events = EPOLLIN;
ev.events = EPOLLIN|EPOLLET;
|
示例代码:
在网络编程(四)中我们这里的使用如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| if(evs[ii].data.fd == listensock){ struct sockaddr_in client; socklen_t len = sizeof(client); int clientsock = accept(listensock,(struct sockaddr*)&client,&len); printf("accept client(socket = %d)ok.\n",clientsock);
ev.data.fd =clientsock; ev.events = EPOLLIN; epoll_ctl(epollfd,EPOLL_CTL_ADD,clientsock,&ev); }else{
char buffer[1024]; memset(buffer,0,sizeof(buffer)); if(recv(evs[ii].data.fd,buffer,sizeof(buffer),0)<=0) { printf("client(eventfd = %d) disconnected.\n",evs[ii].data.fd); close(evs[ii].data.fd); }else{ printf("recv(eventfd = %d):%S\n",evs[ii].data.fd,buffer); send(evs[i].data.fd,buffer,strlen(buffer),0); } }
|
以上代码是我们在前置条件为水平触发时对连接处理的 代码,那么切换至边缘触发后以上的多个客户端的连接处理过程中会造成连接丢失的情况所以需要使用循环,那么如果要使用循环就需要将accept使用非阻塞的模式,以防止阻塞循环的进行,同理recv
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 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55
| int setnonblocking(int fd){ int flags; if((flags = fcntl(fd,F_GETFL,0))==-1) flags = 0; return fcntl(fd,F_SETFL,flags|O_NONBLOCK); }
int main(){ ... setnonblocking(listensock); ... epoll_event ev; ev.data.fd = listensock; ev.events = EPOLLIN|EPOLLET
if(evs[ii].data.fd == listensock){ while(true){ struct sockaddr_in client; socklen_t len = sizeof(client); int clientsock = accept(listensock,(struct sockaddr*)&client,&len); if((clientsock<0)&&(errno == EAGAIN)) break; printf("accept client(socket = %d)ok.\n",clientsock);
ev.data.fd =clientsock; ev.events = EPOLLIN; epoll_ctl(epollfd,EPOLL_CTL_ADD,clientsock,&ev); } }else{ char buffer[1024]; memset(buffer,0,sizeof(buffer)); int readn; char* ptr = buffer; while(true){ if(recv(evs[ii].data.fd,buffer,sizeof(buffer),0)<=0) { if((readn<=0)&& (errno== EAGAIN)) printf("recv(eventfd = %d):%S\n",evs[ii].data.fd,buffer); send(evs[i].data.fd,buffer,strlen(buffer),0); }else{ printf("client(eventfd = %d) disconnected.\n",evs[ii].data.fd); close(evs[ii].data.fd); break; }else{ ptr = ptr+readn; } } } }
|
注意:以上代码中,边缘触发的代码再水平触发模式可以使用,但是水平出发的代码场景无法给边缘触发使用。