一、select
select函数原型
|
1
2
3
4
|
int select (int __nfds, fd_set *__restrict __readfds,
fd_set *__restrict __writefds,
fd_set *__restrict __exceptfds,
struct timeval *__restrict __timeout);
|
1、函数参数
数值最大的文件描述符(maxfd) + 1。之所以加1是因为select内部实现的for循环的判断范围是for(i = 0; i <maxfd; i++),传入maxfd+1就可以循环到最大的文件描述符。
可读集合。按bit可表示1024个的socket。select完成后,内核会将这1024个socket的可读状态存储到__readfds返回给应用程序。
可写集合。按bit可表示1024个的socket。select完成后,内核会将这1024个socket的可写状态存储到__writefds返回给应用程序。
错误集合。按bit可表示1024个的socket。select完成后,内核会将这1024个socket的错误状态存储到__exceptfds返回给应用程序。
等待的超时时间。如果传NULL,则表示一直等待,直到有可读、可写或者错误发生。
返回的是就绪的文件描述符(socket)个数。
2、select缺点
- 每次把待检测io集合,copy进入内核,耗CPU,并且用户也需要遍历所有设定的文件描述符,来判断哪个socket就绪了。
- 对IO的数量是有限制的,最大为1024。
- 由于监听集合和满足条件的集合是同一个集合,因此每次应用进程调用一次select之前,都需要重新设定writefds和readfds,这会影响cpu效率。
- 内核每一次等待文件描述符 都会重新扫描所有readfds或者writefds中的所有文件描述符,如果有较多的文件描述符,则会影响效率。
3、内核提供的宏
1. void FD_SET(int fd, fd_set *set); //将fd设置到set中去,位图置为1
2. void FD_ZERO(fd_set *set); //将set集合清空为0值
3. int FD_ISSET(int fd, fd_set *set); //判断fd是否在集合中,返回值为1,说明满足了条件
4. void FD_CLR(int fd, fd_set *set); //将fd从set中清除出去,位图置为0
4、select处理的流程
1. 创建fd_set 位图集合(3个集合,一个readfds,一个writefds,一个exceptfds)
2. FD_ZERO将set清空
3. 使用FD_SET将需要监听的fd设置对应的事件
4. select函数监听事件,只要select函数返回了大于1的值,说明有事件触发,这时候把set拿出来做判断
5. FD_ISSET判断fd触发了什么事件
5、服务端代码示例
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
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
|
#include <iostream>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <string.h>
#include <unistd.h>
using namespace std;
#define SERVER_SOCKET_PORT 2048 /*服务端端口号*/
#define SERVER_SOCKET_LISTEN_NUM 10 /*服务端监听数量*/
int main(int argc, char *argv[])
{
//创建socket
int listenfd = socket(AF_INET, /*地址族-TCP*/
SOCK_STREAM, /*套接字类型/数据传输方式-面向连接套接字/流式套接字*/
0/*协议类型*/);
if (listenfd == -1)
{
perror("creat socket");
return -1;
}
//绑定socket
sockaddr_in serverAddr;
memset(&serverAddr, 0, sizeof(serverAddr));
serverAddr.sin_family = AF_INET; //地址族-TCP
serverAddr.sin_addr.s_addr = htonl(INADDR_ANY); //服务端的地址INADDR_ANY:0代表任何网卡的任何IP
serverAddr.sin_port = htons(SERVER_SOCKET_PORT); //服务端的端口
if (-1 == bind(listenfd, (sockaddr*)&serverAddr, sizeof(serverAddr)))
{
perror("bind");
return -1;
}
//监听客户端的连接
if (-1 == listen(listenfd, SERVER_SOCKET_LISTEN_NUM))
{
perror("listen");
return -1;
}
fd_set rfds, rset; //fd_set结构体里是一个1024bit的数组,最多可支持1024个客户端连接
FD_ZERO(&rfds); //将文件描述符的集合置空
FD_SET(listenfd, &rfds); //将listen socket设置到文件描述符的集合里
int maxfd = listenfd;
while (1)
{
rset = rfds; //重新将可读集合赋值为文件描述符集合,因为下一次循环时的rset为select()函数返回的就绪集合,需要重复赋值为fd集合
//select轮询就绪的socket(timeout 为null,一直等,直到有就绪的socket)
printf("select begin\n");
//select的第一个参数为maxfd + 1是因为select内部实现的for循环的判断条件是小于maxfd,传入的值+1就可以循环到maxfd
int nready = select(maxfd + 1, &rset, NULL, NULL, NULL);
printf("select end\n");
//通过FD_ISSET()判断listen socket是否可读
if (FD_ISSET(listenfd, &rset))
{
sockaddr_in clientAddr;
socklen_t len = sizeof(clientAddr);
//与客户端建立连接
int clientfd = accept(listenfd, (sockaddr*)&clientAddr, &len);
if (clientfd == -1) {
perror("accept");
continue;
}
printf("accept %s:%d\n", inet_ntoa(clientAddr.sin_addr), ntohs(clientAddr.sin_port));
FD_SET(clientfd, &rfds); //将新连接的客户端描述符设置到描述符集合里
maxfd = clientfd; //将新连接的clientfd作为最大的fd
}
for (int i = listenfd + 1; i <= maxfd; i++)
{
if (FD_ISSET(i, &rset))
{
char buf[128] = {0};
int rSize = recv(i, buf, sizeof(buf), 0);
if (rSize == 0) {
cout << "客户端下线" << endl;
FD_CLR(i, &rfds); //将该socket在集合里置空
close(i); //关闭该socket
continue;
}
printf("recv==>> clientfd: %d, count: %d, buffer: %s\n", i, rSize, buf);
send(i+1, buf, 2, 0);
}
}
}
close(listenfd);
return 0;
}
|
二、poll
poll函数原型
|
1
|
int poll (struct pollfd *__fds, nfds_t __nfds, int __timeout);
|
1、函数参数
2、poll优点
- poll函数相比于select函数来说,最大的优点就是突破了1024个文件描述符的限制,这使得百万并发变得可能。
- 不同于select,poll函数的监听和返回是分开的,因此不用在每次操作之前都单独备份一份了,简化了代码实现。因此,可以理解为select的升级增强版。
3、poll缺点
- 虽然poll不需要遍历所有的文件描述符了,只需要遍历加入数组中的描述符,范围缩小了很多,但缺点仍然是需要遍历。假设真有百万并发的场景,当仅有两三个事件触发的时候,仍然要遍历上百万个文件描述符,只为了找到触发事件的那两三个fd,这样看来 ,就有些得不偿失了。而这个缺点,将在epoll中得以彻底解决。
4、服务端代码示例
poll服务端代码示例
|
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
|
#include <iostream>
#include <sys/socket.h>
#include <netinet/in.h>
#include <string.h>
#include <sys/poll.h>
#include <unistd.h>
#include <arpa/inet.h>
using namespace std;
#define SERVER_SOCKET_PORT 2048 /*服务端端口号*/
#define SERVER_SOCKET_POLL_FD_NUM 2048 /*poll fd数组大小*/
#define SERVER_SOCKET_LISTEN_NUM 10 /*服务端监听数量*/
int main(int argc, const char *argv[])
{
int listenfd = socket(AF_INET, SOCK_STREAM, 0);
sockaddr_in serverAddr;
memset(&serverAddr, 0, sizeof(serverAddr));
serverAddr.sin_family = AF_INET;
serverAddr.sin_addr.s_addr = htonl(INADDR_ANY);
serverAddr.sin_port = htons(SERVER_SOCKET_PORT);
bind(listenfd, (sockaddr*)&serverAddr, sizeof(serverAddr));
listen(listenfd, SERVER_SOCKET_LISTEN_NUM);
/*
poll只是对select函数传入的参数的一个封装,底层实现还是select,好处就是需要传入的参数少了
*/
//pollfd数组, 一个元素为一个 描述符以及相对应的事件。元素的个数为可处理socket的最大个数,突破了select的1024的限制
pollfd pfds[SERVER_SOCKET_POLL_FD_NUM] = {0};
pfds[listenfd].fd = listenfd;
pfds[listenfd].events = POLLIN; //可读
//pfds.revents //revents为poll返回的事件
int maxfd = listenfd;
while (1)
{
int nready = poll(pfds, maxfd + 1, -1);
//监听socket有可读事件
if (pfds[listenfd].revents & POLLIN)
{
sockaddr_in clientAddr;
socklen_t len = sizeof(clientAddr);
int clientfd = accept(pfds[listenfd].fd, (sockaddr*)&clientAddr, &len);
printf("accept==>> clientfd:%d %s:%d\n", clientfd, inet_ntoa(clientAddr.sin_addr), ntohs(clientAddr.sin_port));
if (-1 == clientfd)
{
continue;
}
//请注意这里的clientfd越界问题,这里就不做处理了
//需要注意的是,如果有客户端关闭连接了,再次有新的客户端连接进来时,accept返回的是没有使用的最小的描述符
//比如现在有4,5,6客户端建立了连接,4和5客户端断开了,再次有新的客户端建立连接后,accept会给分配4描述符,这点内核还是很人性化的。
pfds[clientfd].fd = clientfd;
pfds[clientfd].events = POLLIN;
maxfd = clientfd;
}
for (int i = listenfd + 1; i <= maxfd; i++)
{
if (pfds[i].revents & POLLIN)
{
char buf[128] = {0};
int count = recv(pfds[i].fd, buf, sizeof(buf), 0);
if (count == 0)
{
printf("close\n");
close(pfds[i].fd);
pfds[i].fd = -1;
pfds[i].events = 0;
continue;
}
printf("clientfd:%d, count:%d, buf:%s\n", pfds[i].fd, count, buf);
send(pfds[i].fd, buf, count, 0);
}
}
}
close(listenfd);
return 0;
}
|
三、epoll
epoll模型三个函数
|
1
2
3
|
1. int epoll_create (int __size);
2. int epoll_ctl (int __epfd, int __op, int __fd, struct epoll_event *__event);
3. int epoll_wait (int __epfd, struct epoll_event *__events, int __maxevents, int __timeout);
|
1、epoll_create函数原型及参数
epoll_create函数
|
1
|
int epoll_create(int __size);
|
size是树的大小,它代表你将监听多少个文件描述符。epoll_create将按照传入的大小,构造出一棵大小为size的红黑树。
注意: 这个size只是建议值,实际内核并不一定局限于size的大小,可以监听比size更多的文件描述符。现在这个值已经没有意义了,只要大于0就行,一般为1,后续有新的socket连入时,通过调用epoll_ctrl()函数可将其添加到内核维护的红黑树中。
函数返回一个文件描述符,这个文件描述符并不是 一个常规意义的文件描述符,而是一个平衡二叉树(准确来说是红黑树)的根节点。
2、epoll_ctl函数原型及参数
epoll_ctl函数
|
1
|
int epoll_ctl(int __epfd, int __op, int __fd, struct epoll_event *__event);
|
函数主要用来操作epoll句柄(即epoll_create()创建的红黑树的根节点),可以使用该函数往红黑树里增加文件描述符,修改文件描述符,和删除文件描述符。可以看到,select和poll使用的都是bitmap位图,而epoll使用的是红黑树。
EPOLL_CTL_ADD: 向树增加文件描述符
EPOLL_CTL_MOD: 修改树中的文件描述符
EPOLL_CTL_DEL: 删除树中的文件描述符
typedef union epoll_data结构体
|
1
2
3
4
5
6
7
8
9
10
11
12
13
|
typedef union epoll_data
{
void *ptr;
int fd;
uint32_t u32;
uint64_t u64;
}epoll_data_t;
struct epoll_event
{
uint32_t events; /* Epoll events */
epoll_data_t data; /* User data variable */
};
|
epoll_event结构体的第一个元素为uint32_t类型的events,这个和poll类似,是一个bit mask,主要使用到的标志位有:
EPOLLIN: 读事件
EPOLLOUT: 写事件
EPOLLERR: 异常事件
第二个元素,是一个epoll_data_t类型的联合体。里面的fd,它代表一个文件描述符,初始化的时候传入需要监听的文件描述符,当监听返回时,此处会传出一个有事件发生的文件描述符,因此,无需我们遍历所有的文件描述符,就可以得到结果了。
3、epoll_wait函数原型及参数
epoll_wait函数
|
1
|
int epoll_wait(int __epfd, struct epoll_event *__events, int __maxevents, int __timeout);
|
epoll_wait才是真正的监听函数。返回一个事件数组,该事件数组只包含发生了事件的文件描述符及其所对应的事件。
是epoll_create()的返回值。
待内核传出的发生事件的事件数组,由用户自己创建。
事件数组的大小。
等待的超时时间。0代表立即返回,-1代表永久阻塞,如果大于0,则代表毫秒数(注意select的timeout是微秒)。
有多少个事件触发,通过此返回值确定遍历的次数。
4、epoll监听流程
1. 首先,创建一个服务端的listenfd
2. 然后,使用epoll_create创建一个句柄
3. 使用epoll_ctl将listenfd加入到红黑树中,监听EPOLLIN事件
4. 使用epoll_wait监听
5. 如果EPOLLIN事件触发,说明有客户端连接上来,将新客户端加入到红黑树中,重新监听
6. 如果再有EPOLLIN事件触发: 遍历events,
7. 如果fd是listenfd,则说明又有新客户端连接上来,重复上面的步骤,将新客户端加入到红黑树中
8. 如果fd不为listenfd,这说明客户端有数据发过来,直接调用recv函数读取内容即可。
5、触发(水平和边沿)
epoll有两种触发方式,分别为水平触发(LT)和边沿触发(ET)。
水平触发
所谓的水平触发,就是只要仍有数据处于就绪状态,那么可读事件就会一直触发。举个例子,假设客户端一次性发来了4K数据 ,但是服务器recv函数定义的buffer大小仅为1024字节,那么一次肯定是不能将所有数据都读取完的,这时候就会继续触发可读事件,直到所有数据都处理完成。
epoll默认的触发方式就是水平触发。
边沿触发
边沿触发恰好相反,边沿触发是只有数据发送过来的时候会触发一次,即使数据没有读取完,也不会继续触发。必须client再次调用send函数触发了可读事件,才会继续读取。
假设客户端 一次性发来4K数据,服务器recv的buffer大小为1024字节,那么服务器在第一次收到1024字节之后就不会继续触发可读件。只有当客户端再次发送数据的时候,服务器可读事件触发 ,才会继续读取第二个1024字节数据。
注意: 第二次可读事件触发时,它读取的仍然是上次未读完的数据 ,而不是客户端第二次发过来的新数据。也就是说:数据没读完虽然不会继续触发EPOLLIN,但不会丢失数据。
触发方式的设置:
水平触发和边沿触发在内核里 使用两个bit mask区分,分别为:
EPOLLLT 水平触发
EPOLLET 边沿触发
我们只需要在注册事件的时候将其与需要注册的事件做一个位或运算即可:
ev.events = EPOLLIN; //LT
ev.events = EPOLLIN | EPOLLET; //ET
6、epoll优点
epoll的优点显而易见,它解决了poll需要遍历所有注册的fd的问题,只需要关心触发了事件的极少量fd即可,大大提升了效率。
而更有意思 的是epoll_data_t这个联合体,它里面有四个元素:
typedef union epoll_data联合体
|
1
2
3
4
5
6
|
typedef union epoll_data {
void *ptr;
int fd;
uint32_t u32;
uint64_t u64;
}epoll_data_t;
|
简单开发时,我们可以将fd记录在其中,但是我们注意到 这里面还有一个void *类型的元素,那就提供了无限可能。它可以是一个struct,也可以是一个callback,也可以是struct嵌套callback,从而有无限扩展的可能。大名鼎鼎的reactor反应堆模型就是通过这种方式完成的。
7、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
|
#include <iostream>
#include <sys/socket.h>
#include <netinet/in.h>
#include <string.h>
#include <sys/poll.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/epoll.h>
using namespace std;
#define SERVER_SOCKET_PORT 2048 /*服务端端口号*/
#define SERVER_SOCKET_EPOLL_NUM 1024 /*epoll event数组大小*/
#define SERVER_SOCKET_LISTEN_NUM 10 /*服务端监听数量*/
int main(int argc, const char *argv[])
{
int listenfd = socket(AF_INET, SOCK_STREAM, 0);
sockaddr_in serverAddr;
memset(&serverAddr, 0, sizeof(serverAddr));
serverAddr.sin_family = AF_INET;
serverAddr.sin_addr.s_addr = htonl(INADDR_ANY);
serverAddr.sin_port = htons(SERVER_SOCKET_PORT);
bind(listenfd, (sockaddr*)&serverAddr, sizeof(serverAddr));
listen(listenfd, SERVER_SOCKET_LISTEN_NUM);
int epfd = epoll_create(1); // int size
epoll_event ev;
ev.events = EPOLLIN;
ev.data.fd = listenfd;
epoll_ctl(epfd, EPOLL_CTL_ADD, listenfd, &ev);
struct epoll_event events[SERVER_SOCKET_EPOLL_NUM] = {0};
while (1)
{
int nready = epoll_wait(epfd, events, SERVER_SOCKET_EPOLL_NUM, -1);
int i = 0;
for (i = 0;i < nready;i ++)
{
int connfd = events[i].data.fd;
if (listenfd == connfd)
{
struct sockaddr_in clientaddr;
socklen_t len = sizeof(clientaddr);
int clientfd = accept(listenfd, (struct sockaddr*)&clientaddr, &len);
ev.events = EPOLLIN; //默认水平触发(LT),有(数据)事件就会一直触发,知道全部处理完
/*
EPOLLET为边沿触发(ET),当有事件发生时只触发一次,
比如来数据了,如果一次没有读完,不会再触发了,所以必须全部读完,在进行下一次epoll_wait
*/
//ev.events = EPOLLIN | EPOLLET;
ev.data.fd = clientfd;
epoll_ctl(epfd, EPOLL_CTL_ADD, clientfd, &ev);
printf("clientfd: %d\n", clientfd);
}
else if (events[i].events & EPOLLIN)
{
char buffer[128] = {0};
int count = recv(connfd, buffer, sizeof(buffer), 0);
if (count == 0)
{
printf("disconnect\n");
epoll_ctl(epfd, EPOLL_CTL_DEL, connfd, NULL);
close(i);
continue;
}
send(connfd, buffer, count, 0);
printf("clientfd: %d, count: %d, buffer: %s\n", connfd, count, buffer);
}
}
}
close(listenfd);
return 0;
}
|