《博学谷C++》四.2-3Tcp状态转移和IO多路复用

服务器多种模式:

  • 阻塞循环(多进程、多线程)

  • 循环轮询

  • 多路I/O复用(转接)技术

    多路I/O复用(转接)主要有select和epoll。其中select是跨平台API,windows就是用这种;epoll高效但仅支持在linux下使用。

释义和不同技术的区别:

https://www.zhihu.com/question/32163005

IO 多路复用是什么意思?

1.select

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/* According to POSIX.1-2001, POSIX.1-2008 */
#include <sys/select.h>

/* According to earlier standards */
#include <sys/time.h>
#include <sys/types.h>
#include <unistd.h>

int select(int nfds, fd_set *readfds, fd_set *writefds,
fd_set *exceptfds, struct timeval *timeout);

void FD_CLR(int fd, fd_set *set);
int FD_ISSET(int fd, fd_set *set);
void FD_SET(int fd, fd_set *set);
void FD_ZERO(fd_set *set);

select原理:

每次把需要监控的fd添加到readfds集合(这时需要备份),之后select复制数组到内核,检测到fd的w/r缓冲区更改后仅保留变更的fd,然后复制到应用层数组。同时返回变更的fd的个数,遍历数组即可知道哪些fd发生了更改。

缺点:最多仅支持1024个

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
#include <stdio.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <sys/select.h>
#include <unistd.h>
#include <string.h>
#include <stdlib.h>
#include <arpa/inet.h>


int main()
{
int ret;

//lfd socket
int lfd = socket(AF_INET, SOCK_STREAM, 0);
if (lfd == -1) {
printf("socket error.");
return 1;
}

//serAddr
struct sockaddr_in serAddr;
serAddr.sin_family = AF_INET;
serAddr.sin_port = htons(8888);
inet_pton(AF_INET, "192.168.138.129", (void*)&serAddr.sin_addr.s_addr);

//端口复用
int opt = 1;
setsockopt(lfd, SOL_SOCKET, SO_REUSEPORT, &opt, sizeof(opt));

//bind
ret = bind(lfd, (struct sockaddr*)&serAddr, sizeof(serAddr));
if (ret == -1) {
close(lfd);
printf("bind error.");
return 1;
}

//listen
ret = listen(lfd, 128);
if (ret == -1) {
close(lfd);
printf("listen error.");
return 1;
}

//select param
fd_set originSet, rSet;
FD_ZERO(&originSet);
FD_ZERO(&rSet);
FD_SET(lfd, &originSet);
int maxfd = lfd;

//select
while (1) {
rSet = originSet;
int n = select(maxfd +1, &rSet, NULL, NULL, NULL);
if (n == -1) {
close(lfd);
printf("select error.");
return 1;
}
else if (n == 0)
{
continue;
}
else//更新了fd
{
//如果lfd更新了
if (FD_ISSET(lfd, &rSet)) {
struct sockaddr_in cliAddr;
socklen_t len = sizeof(cliAddr);
int cfd = accept(lfd, (struct sockaddr*)&cliAddr, &len);
if (cfd==-1)
{
close(lfd);
printf("accept:");
return 1;
}
char ip[16];
printf("new client:ip%s,port %d. \n",inet_ntop(AF_INET,(void*)&cliAddr.sin_addr.s_addr, ip,16),ntohs(cliAddr.sin_port));
FD_SET(cfd, &originSet);
if (cfd > maxfd)
{
maxfd = cfd;
}

if (n==1)
{
continue;
}
}
else { //如果cfd更新了——客户端需要响应
for (int i = lfd+1; i <= maxfd; i++)
{
if (!FD_ISSET(i,&rSet))
{
continue;
}
int cfd = i;

//通信
const int size = 1500;
char buf[size] = "";
int num = read(cfd, buf, size);
if (num < 0)
{
printf("read error.\n");//遇到\n才会刷新缓冲区,如果不加,可能会没有显示
close(cfd);
FD_CLR(cfd, &originSet);
continue;
}
else if (num == 0) { //客户端已关闭
printf("client closed.\n");
close(cfd);
FD_CLR(cfd, &originSet);
}
else //成功收到
{
write(1, buf, num);
write(cfd, buf, num);
}
}
}
}

}

//缺点:退出前没有关闭所有cfd

cleanUp: close(lfd);

return 0;
}

优点:跨平台

缺点:

  • 文件描述符1024限制(最多只能监听1024个fd,和系统打开fd数量无关)

  • 只是返回变化的文件描述符的个数,具体哪个需要遍历

  • 每次需要将监听的文件描述符拷贝到内核,消耗较大

  • 假设4-1023需要监听,而5-1000关闭了/只有5、1002发来消息,这样遍历效率很低(大量并发,少量活跃时select效率很低)(部分关闭时可以自己进行优化,但只有部分发来消息时无解)

2.poll

优点:

  • 没有最大监听fd的限制
  • 请求和返回值分离(不是一个参数,不会覆盖,因此参数也只需设置一次)

缺点和select一样:

  • 仍需遍历才知道哪个变化了
  • 每次需要将需要监听的fd从应用层拷贝到内核
  • 大量并发,少量活跃效率低

3.epoll

优点:

  • 没有数量限制
  • 不需要拷贝fd到内核(红黑树在内核,每次拷贝发生变化的结构体数组到应用层)
  • 返回变化的fd,不需要遍历
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
#include <sys/epoll.h>
#define MAX_EVENTS 10
struct epoll_event ev, events[MAX_EVENTS];

/*
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 需要监听的fd等
};
*/

//epoll_create
int epoll_create(int size);//创建并返回epoll文件描述符epfd,出错返回-1
int epoll_create1(int flags);

//epoll_ctl
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);//根据epfd,对fd进行op操作,event描述fd的信息。成功返回0,失败返回-1

//epoll_wait
int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);//返回等待响应的fd数量,出错返回-1
int epoll_pwait(int epfd, struct epoll_event *events, int maxevents, int timeout, const sigset_t *sigmask);

底层原理是创建一颗红黑树,树的节点是epoll_event(包含fd、被监听的事件等),用epfd表示。

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
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <fcntl.h>
#include <poll.h>
#include <iostream>
#include <string.h>
#include <vector>
#include <errno.h>
#include <sys/epoll.h>


int main(int argc, char *argv[])
{
//1.创建socket
int lfd = socket(AF_INET, SOCK_STREAM, 0);
if (lfd == -1)
{
perror("socket\n");
return 1;
}

//2.bind,端口复用
int on = 1;
int ret = setsockopt(lfd, SOL_SOCKET, SO_REUSEPORT,(void*)&on,sizeof(on));
if (ret == -1)
{
perror("setsockopt\n");
return 1;
}

struct sockaddr_in seraddr;
seraddr.sin_family = AF_INET;
//inet_pton(AF_INET,"192.168.138.129",(void*)&seraddr.sin_addr.s_addr);
seraddr.sin_addr.s_addr = htonl(INADDR_ANY); //
seraddr.sin_port = htons(8888);
ret = bind(lfd, (struct sockaddr *)&seraddr, sizeof(seraddr));
if (ret == -1)
{
perror("bind\n");
return 1;
}

//3.listen
ret = listen(lfd, 128);
if (ret == -1)
{
perror("listen\n");
return 1;
}

//4.epoll init
int epfd = epoll_create(1);
if (epfd == -1) {
perror("epoll_create\n");
return 1;
}
struct epoll_event ev;
ev.events = EPOLLIN;
ev.data.fd = lfd;
ret = epoll_ctl(epfd, EPOLL_CTL_ADD,lfd,&ev);//加入初始fd:lfd
if (ret == -1)
{
perror("epoll_ctl(add lfd)\n");
return 1;
}

//5.epoll_wait,accept
const int MAXSIZE = 100;
while (true)
{
struct epoll_event evs[MAXSIZE];
int n = epoll_wait(epfd, evs, MAXSIZE, -1);
if (n == -1)
{
perror("epoll_ctl(add cfd) \n");
return 1;
}

for (int i = 0; i < n; i++)
{
int fd = evs[i].data.fd;
//如果lfd改变,有新的客户端连接
if (fd== lfd)
{
struct sockaddr_in cliaddr;
int len = sizeof(cliaddr);
int cfd = accept(lfd,(struct sockaddr*)&cliaddr,(socklen_t *)&len);//可以用accept4同时设置非阻塞
if (cfd == -1)
{
continue;
}
struct epoll_event ev;
ev.events = EPOLLIN;
ev.data.fd = cfd;
ret = epoll_ctl(epfd, EPOLL_CTL_ADD, cfd, &ev);//加入初始fd:lfd
if (ret == -1)
{
perror("epoll_ctl(add cfd) \n");
continue;
}
}
//如果lfd改变,可读
else
{
const int MAXBUFSIZE = 1500;
char buf[MAXBUFSIZE];
ret = read(fd, buf, MAXBUFSIZE);
if (ret == -1) {
printf("Error occured.\n");
epoll_ctl(epfd, EPOLL_CTL_DEL, fd, NULL);//关闭无效fd
close(fd);
continue;
}
else if (ret == 0)
{
printf("Client closed.\n");
epoll_ctl(epfd, EPOLL_CTL_DEL, fd,NULL);//关闭无效fd
close(fd);
continue;
}
write(1, buf, ret);
ret = write(fd, buf, ret);
if (ret == -1) {
printf("Error occured.\n");
epoll_ctl(epfd, EPOLL_CTL_DEL, fd, NULL);//关闭无效fd
close(fd);
continue;
}
else if (ret == 0)
{
printf("Client closed.\n");
epoll_ctl(epfd, EPOLL_CTL_DEL, fd, NULL);//关闭无效fd
close(fd);
continue;
}
}
}

}

close(lfd);

return 0;
}

为什么要有ET模式:

  • 因为设置为水平触发,只要缓存区有数据epoll_wait就会被触发,epoll_wait是一个系统调用,尽量少调用,所以尽量使用边沿触发
  • write时边缘触发更有用,否则读缓冲区空闲时一直触发没有必要。

做法:

边沿触发,数据来一次只触发一次,这个时候要求一次性将数据读完,所以while循环读,读到最后read默认带阻塞,不能让read阻塞,因为没有机会再去监听,设置cfd为非阻塞,read读到最后一次返回值为-1.判断errno的值为EAGAIN,代表数据读干净

工作中 边沿触发 + 非阻塞 = 高速模式