FireFour's Studio.

Linux非阻塞socket编程之epoll

2019/11/18 Share

Linux下非阻塞socket编程常用的有两种实现:select和epoll。但是select受限于描述符数目限制,实际使用的时候人们大多数都会采用epoll形式来实现(如果在windows下没有epoll原生支持,可以使用select方式)。

Epoll主要有以下三个接口:

1
2
3
4
#include <sys/epoll.h>
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_create主要用于创建一个epoll的句柄,参数是要监听的文件描述符的最大个数。epoll_ctl主要用于添加,删除和修改监听事件。epoll_wait类似select查看是否有文件描述符对应事件已经产生。三个函数接口的详细说明可以参考Linux manual page,这里要说一下,epoll_wait的最后一个参数timeout用来设置等待的时间,若设置为1,则epoll阻塞等待的时间不确定(官方文档描述);若设置为0则是完全非阻塞状态,会立即返回;若设置为其他时间则是规定时间内没有事件触发则超时后返回。

一般情况下,我们在网络编程的服务端常用到epoll来非阻塞地监听客户端发来的连接,下面是我测试通过的epoll服务端的简单示例代码,仅供参考。

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
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <ctype.h>
#include <unistd.h>
#include <netinet/in.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <sys/epoll.h>
#include <sys/types.h>
#include <errno.h>

#define INVALID_SOCKET -1
#define SOCKET_ERROR -1

#define SERVER_IP "127.0.0.1"
#define SERVER_PORT 23333
#define EPOLL_EVENTS 100
#define FDSIZE 1000
#define BUFFER_SZ 4096

static void add_event(int epollfd, int fd, int state)
{
struct epoll_event ev;
ev.events = state;
ev.data.fd = fd;
epoll_ctl(epollfd, EPOLL_CTL_ADD, fd, &ev);
}

static void delete_event(int epollfd,int fd,int state)
{
struct epoll_event ev;
ev.events = state;
ev.data.fd = fd;
epoll_ctl(epollfd, EPOLL_CTL_DEL, fd, &ev);
}

static void modify_event(int epollfd,int fd,int state)
{
struct epoll_event ev;
ev.events = state;
ev.data.fd = fd;
epoll_ctl(epollfd, EPOLL_CTL_MOD, fd, &ev);
}

int main()
{
int serverfd;
if ((serverfd = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP)) == INVALID_SOCKET)
{
printf("create socket error!\n");
return -1;
}

sockaddr_in server_addr;
memset(&server_addr, 0, sizeof(sockaddr_in));
server_addr.sin_family = AF_INET;
server_addr.sin_addr.s_addr = inet_addr(SERVER_IP);
server_addr.sin_port = htons(SERVER_PORT);

//套接字地址绑定
if (bind(serverfd, (sockaddr *)&server_addr, sizeof(sockaddr)) == SOCKET_ERROR)
{
printf("Socket bind failed!\n");
close(serverfd);
return -1;
}

/*监听连接请求--监听队列长度为5*/
if (listen(serverfd, 5)<0)
{
printf("listen error\n");
return -1;
};

printf("start listen...\n");

////////////////////////////////////////////////
int epollfd;

struct epoll_event events[EPOLL_EVENTS];
int fd_num;
//创建epoll描述符
epollfd = epoll_create(FDSIZE);

//添加监听描述符事件
add_event(epollfd, serverfd, EPOLLIN);

char buffer[BUFFER_SZ + 1];
while(1)
{
//获取已经准备好的描述符事件
fd_num = epoll_wait(epollfd, events, EPOLL_EVENTS, -1);

int i;
int fd;
//进行选好遍历
for (i = 0; i < fd_num; i++)
{
fd = events[i].data.fd;
//根据描述符的类型和事件类型进行处理
if ((fd == serverfd) && (events[i].events & EPOLLIN))
{
//调用accept与客户端建立连接
int clientfd;
struct sockaddr_in client_addr;
socklen_t client_addrlen;
/*等待客户端连接请求到达*/
if((clientfd = accept(serverfd, (struct sockaddr *)&client_addr, &client_addrlen)) < 0)
{
printf("accept error\n");
}
else
{
printf("accept a new client: [%s:%d]\n", inet_ntoa(client_addr.sin_addr), client_addr.sin_port);

//添加一个客户端描述符和事件用于收发数据
add_event(epollfd, clientfd, EPOLLIN);
}
}
else if (events[i].events & EPOLLIN)
{
int nRecEcho = recv(fd, (char*)buffer, BUFFER_SZ , 0);

if(nRecEcho < 0)
{
if(errno == EWOULDBLOCK)
{
printf("Send cache full!");
continue;
}

printf("receive error!\n");
close(fd);
delete_event(epollfd, fd, EPOLLIN);
}
else if(nRecEcho == 0)
{
printf("client close.\n");
close(fd);
delete_event(epollfd, fd, EPOLLIN);
}
else
{
printf("receive %d bytes: [%s]!\n", nRecEcho, buffer);
}
}
else if (events[i].events & EPOLLOUT)
{
//...省略发送部分
printf("Available to send!\n");
}
}
}

close(epollfd);
////////////////////////////////////////////////
return 0;
}

此外,这里还需要交待一个可能有些人并没有想清楚的问题:epoll == 非阻塞socket?

其实并不是,首先,socket只是众多描述符中的一种,还有很多非阻塞IO都可以通过epoll来实现。另外,非阻塞socket的实现方式也不单单是epoll和socket,其实socket本身的recv函数就自带一个参数来控制是阻塞还是非阻塞状态方式接收。

1
ssize_t recv(int sockfd, void *buf, size_t len, int flags);

一般我们自己写的recv通常会把第四个参数flags置为0,但是当把它设置为MSG_DONTWAIT时,此时,若接收缓冲区没有数据可读时,recv函数并不会阻塞等待,而是会返回一个EWOULDBLOCK的错误表示缓冲区当前实际已经阻塞,这一点类似于send函数的返回值,当发送缓冲区满后,send函数也会返回一个EWOULDBLOCK表示当前发送缓冲已经阻塞。

此外,如果不想在每次recv函数调用时都传参来控制阻塞还是非阻塞接收,可以调用以下函数达到类似的效果:

1
2
int on = 1;
ioctlsocket(socket, FIONBIO, (char*)&on);

这个函数设置以后,对应的socket就可以通过非阻塞方式工作了。

上述两种介绍的方式来实现非阻塞socket貌似更加直接方便,那是不是epoll就可以不用了呢?

显然不是的,epoll可以同时监听多个描述符是否可读写,而这对于socket网络编程中的服务端编写是必不可少的,实际的网络场景中,服务端需要通过非阻塞的方式来监听每一个客户端发来的连接请求同时还要以非阻塞的方式来与对端进行数据收发,因此epoll的使用也尤为重要。

除此之外,对于一个服务端socket程序,ioctlsocket和epoll同时使用会更加高效,可以参考以下代码:

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
int on = 1;
ioctlsocket(socketfd, FIONBIO, (char*)&on);

for (int i = 0; i < 8; ++i)
{
int nRecEcho = recv(socketfd, (char*)buffer, BUFFER_SZ , 0);
if (nRecEcho > 0)
{
//...读取数据
}
else if (read_len < 0)
{
if (errno == WSAEWOULDBLOCK)
{
break;
}
printf("缓冲阻塞无法读取!\n");
close(socketfd);
break;
}
else
{
printf("socket读取异常!\n");
close(socketfd);
break;
}
}

如果在程序一开始不使用ioctlsocket将socket设置为非阻塞状态,当epoll_wait检测到socket可读的时候,执行上述代码可能会造成阻塞!原因就在于最外层for循环,循环的目的在于socket缓冲区可能一次性接收到很多字节,而BUFFER_SZ不一定能够把所有的都读取出来,因此通过for循环几次可以比较方便高效地在检测到一次epoll事件可读时将更多的字节读取出来,但由于socket是非阻塞工作模式,因此如果缓冲区已经空了,再调用recv函数,就会阻塞等待。如果不想设置套接字为非阻塞,单纯用epoll来实现,就需要在每次调用recv之前都要先通过epoll_wait判断套接字是否可读,即两者必须成对调用,才能保证程序接收不会发生阻塞。

CATALOG