阻塞&非阻塞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;

//获取fd状态
if((flags = fcntl(fd,F_GETFL,0))==-1)
flags = 0;
return fcntl(fd,F_SETFL,flags|O_NONBLOCK);
}

//在客户端中创建了socket后设置为非阻塞

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;

//获取fd状态
if((flags = fcntl(fd,F_GETFL,0))==-1)
flags = 0;
return fcntl(fd,F_SETFL,flags|O_NONBLOCK);
}

//在客户端中创建了socket后设置为非阻塞

int listenfd = initserver(IP)

setnonblocking(listenfd);
//对于非阻塞的accept返回失败要判断错误代码
if(accept(listensock,0,0)==-1){
if(errno != EAGAIN){
perror("accept()");
return -1;
}

}

非阻塞IO-recv和send

  • 对于非阻塞的IO调用recv()如果没有数据可以读(接收缓存区为空),函数立即返回失败,errno==EAGAIN

  • 对于非阻塞IO调用send(),如果socket不可写(发送缓冲区已满),函数立即返回失败,errno==EAGAIN


水平触发&边缘触发

水平触发

  • 读事件:如果epoll_wait触发了读事件,表示数据可读,如果程序没有把数据读完,再次调用了epoll_wait,将立即再次触发读事件
  • 写事件:如果发送的缓冲区没有满,表示可以写入数据,只要缓冲区没有被写满,再次调用epoll_wait的时候将立即再次触发写事件。

边缘触发

  • 读事件:epoll_wait触发读事件后,不管程序有没有处理读事件,epoll_wait都不会再触发读时间,只有当新数据到达时,才再次触发读事件。
  • 写事件:epoll_wait触发写事件之后,如果缓冲区仍可以写(发送缓冲区没有满),epoll_wait不会再次触发写事件,只有当发送缓存区由满变成不满时,才再次触发写事件。
1
2
3
4
5
6
7
8
//默认情况下epoll是水平触即设置事件时只需要设置为EPOLLIN/EPOLLOUT即可进行水平触发
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;//每次调用recv的返回值
char* ptr = buffer;//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;//buffer的报文结尾位置后移
}
}


}
}

注意:以上代码中,边缘触发的代码再水平触发模式可以使用,但是水平出发的代码场景无法给边缘触发使用。