C++后端开发入门-服务器模型编程

分时循环服务器

UDP版

服务端:

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
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <string.h>
#include <unistd.h>
#include <errno.h>
#include <stdio.h>
char rbuf[50], sbuf[100];
int main() {
int sockfd, size, ret;
char on = 1;
struct sockaddr_in saddr;
struct sockaddr_in raddr;
size = sizeof(struct sockaddr_in); //设置地址信息,ip信息
memset(&saddr, 0, size);
saddr.sin_family = AF_INET;
saddr.sin_port = htons(8888);
saddr.sin_addr.s_addr = htonl(INADDR_ANY);
sockfd = socket(AF_INET, SOCK_DGRAM, 0); //创建udp 的套接字
if (sockfd < 0) {
puts("socket failed");
return -1;
};
setsockopt(sockfd, SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on)); //设置端口复用
ret = bind(sockfd, (struct sockaddr*)&saddr, sizeof(struct sockaddr)); //绑定地址信息,ip信息
if (ret < 0) {
puts("sbind failed");
return -1;
};
int val = sizeof(struct sockaddr);
while (1) { //循环接收客户端发来的消息
puts("waiting data");
ret = recvfrom(sockfd, rbuf, 50, 0, (struct sockaddr*)&raddr, (socklen_t*)&val);
if (ret < 0)
perror("recvfrom failed");
printf("recv data :%s\n", rbuf);
sprintf(sbuf, "server has received your data(%s)\n", rbuf);
ret = sendto(sockfd, sbuf, strlen(sbuf), 0, (struct sockaddr*)&raddr, sizeof(struct sockaddr));
memset(rbuf, 0, 50);
};
close(sockfd); //关闭udp套接字
getchar();
return 0;
};

客户端:

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
#include <stdio.h>
#include <winsock.h>
#pragma comment(lib,"wsock32")
#define BUF_SIZE 200
#define PORT 8888
char wbuf[50], rbuf[100];
int main() {
SOCKET s;
int len;
WSADATA wsadata;
struct hostent* phe; /*host information */
struct servent* pse; /* server information */
struct protoent* ppe; /*protocol information */
struct sockaddr_in saddr, raddr; /*endpoint IP address */
int fromlen, ret, type;
if (WSAStartup(MAKEWORD(2, 0), &wsadata) != 0) {
printf("WSAStartup failed\n");
WSACleanup();
return -1;
};
memset(&saddr, 0, sizeof(saddr));
saddr.sin_family = AF_INET;
saddr.sin_port = htons(PORT);
saddr.sin_addr.s_addr = inet_addr("192.168.0.153");
/**** get protocol number from protocol name ****/
if ((ppe = getprotobyname("UDP")) == 0) {
printf("get protocol information error \n");
WSACleanup();
return -1;
};
s = socket(PF_INET, SOCK_DGRAM, ppe->p_proto);
if (s == INVALID_SOCKET) {
printf(" creat socket error \n");
WSACleanup();
return -1;
};
fromlen = sizeof(struct sockaddr);
printf("please enter data:");
scanf_s("%s", wbuf, sizeof(wbuf));
ret = sendto(s, wbuf, sizeof(wbuf), 0, (struct sockaddr*)&saddr, sizeof(struct sockaddr));
if (ret < 0)
perror("sendto failed");
len = recvfrom(s, rbuf, sizeof(rbuf), 0, (struct sockaddr*)&raddr, &fromlen);
if (len < 0)
perror("recvfrom failed");
printf("server reply:%s\n", rbuf);
closesocket(s);
WSACleanup();
return 0;
};

TCP版

服务端:

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
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <string.h>
#include <unistd.h>
#include <errno.h>
#include <stdio.h>
#define BUF_SIZE 200
#define PORT 8888
int main() {
struct sockaddr_in fsin;
int clisock, alen, connum = 0, len, s;
char buf[BUF_SIZE] = "hi,client", rbuf[BUF_SIZE];
struct servent* pse; /* server information */
struct protoent* ppe; /* proto information */
struct sockaddr_in sin; /* endpoint IP address */
memset(&sin, 0, sizeof(sin));
sin.sin_family = AF_INET;
sin.sin_addr.s_addr = INADDR_ANY;
sin.sin_port = htons(PORT);
s = socket(PF_INET, SOCK_STREAM, 0);
if (s == -1) {
printf("creat socket error \n");
getchar();
return -1;
};
if (bind(s, (struct sockaddr*)&sin, sizeof(sin)) == -1) {
printf("socket bind error \n");
getchar();
return -1;
};
if (listen(s, 10) == -1) {
printf(" socket listen error \n");
getchar();
return -1;
};
while (1) {
alen = sizeof(struct sockaddr);
puts("waiting client...");
clisock = accept(s, (struct sockaddr*)&fsin, (socklen_t*)&alen);
if (clisock == -1) {
printf("accept failed\n");
getchar();
return -1;
};
connum++;
printf("%d client comes\n", connum);
len = recv(clisock, rbuf, sizeof(rbuf), 0);
if (len < 0)
perror("recv failed");
sprintf(buf, "Server has received your data(%s).", rbuf);
send(clisock, buf, strlen(buf), 0);
close(clisock);
};
return 0;
};

客户端:

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
#include <stdio.h>
#include <winsock.h>
#pragma comment(lib,"wsock32")
#define BUF_SIZE 200
#define PORT 8888
char wbuf[50], rbuf[100];
int main() {
char buff[BUF_SIZE];
SOCKET s;
int len;
WSADATA wsadata;
struct hostent* phe; /*host information */
struct servent* pse; /* server information */
struct protoent* ppe; /*protocol information */
struct sockaddr_in saddr; /*endpoint IP address */
int type;
if (WSAStartup(MAKEWORD(2, 0), &wsadata) != 0) {
printf("WSAStartup failed\n");
WSACleanup();
return -1;
};
memset(&saddr, 0, sizeof(saddr));
saddr.sin_family = AF_INET;
saddr.sin_port = htons(PORT);
saddr.sin_addr.s_addr = inet_addr("192.168.0.153");
s = socket(PF_INET, SOCK_STREAM, 0);
if (s == INVALID_SOCKET) {
printf(" creat socket error \n");
WSACleanup();
return -1;
};
if (connect(s, (struct sockaddr*)&saddr, sizeof(saddr)) == SOCKET_ERROR) {
printf("connect socket error \n");
WSACleanup();
return -1;
};
printf("please enter data:");
scanf_s("%s", wbuf, sizeof(wbuf));
len = send(s, wbuf, sizeof(wbuf), 0);
if (len < 0)
perror("send failed");
len = recv(s, rbuf, sizeof(rbuf), 0);
if (len < 0)
perror("recv failed");
printf("server reply:%s\n", rbuf);
closesocket(s);
WSACleanup();
return 0;
};

多进程TCP服务器

服务端:

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
#include <cstdio>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
int main(int argc, char* argv[]) {
unsigned short port = 8888; // 本地端口
char on = 1;
int sockfd = socket(AF_INET, SOCK_STREAM, 0);//1.创建tcp套接字
if (sockfd < 0) {
perror("socket");
exit(-1);
};
struct sockaddr_in my_addr; //配置本地网络信息
bzero(&my_addr, sizeof(my_addr)); // 清空
my_addr.sin_family = AF_INET; // IPv4
my_addr.sin_port = htons(port); // 端口
my_addr.sin_addr.s_addr = htonl(INADDR_ANY); // ip
setsockopt(sockfd, SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on));//设置端口复用
int err_log = bind(sockfd, (struct sockaddr*)&my_addr, sizeof(my_addr));//2.绑定
if (err_log != 0) {
perror("binding");
close(sockfd);
getchar();
exit(-1);
};
err_log = listen(sockfd, 10);//3.监听,套接字变被动
if (err_log != 0) {
perror("listen");
close(sockfd);
exit(-1);
};
while (1) {//主进程 循环等待客户端的连接
char cli_ip[INET_ADDRSTRLEN] = { 0 };
struct sockaddr_in client_addr;
socklen_t cliaddr_len = sizeof(client_addr);
puts("Father process is waitting client...");
int connfd = accept(sockfd, (struct sockaddr*)&client_addr, &cliaddr_len); // 取出客户端已完成的连接
if (connfd < 0) {
perror("accept");
close(sockfd);
exit(-1);
};
pid_t pid = fork();
if (pid < 0) {
perror("fork");
_exit(-1);
}
else
if (0 == pid) { //子进程 接收客户端的信息,并返回给客户端
close(sockfd); // 关闭监听套接字,这个套接字是从父进程继承过来
char recv_buf[1024] = { 0 };
int recv_len = 0;
memset(cli_ip, 0, sizeof(cli_ip)); // 清空
inet_ntop(AF_INET, &client_addr.sin_addr, cli_ip, INET_ADDRSTRLEN);
printf("----------------------------------------------\n");
printf("client ip=%s,port=%d\n", cli_ip, ntohs(client_addr.sin_port)); // 打印客户端的 ip 和端口
while ((recv_len = recv(connfd, recv_buf, sizeof(recv_buf), 0)) > 0) { // 接收数据
printf("recv_buf: %s\n", recv_buf); // 打印数据
send(connfd, recv_buf, recv_len, 0); // 给客户端回数据
};
printf("client_port %d closed!\n", ntohs(client_addr.sin_port));
close(connfd); //关闭已连接套接字
exit(0); //子进程结束
}
else
if (pid > 0) // 父进程
close(connfd); //关闭已连接套接字
};
close(sockfd);
return 0;
};

客户端同上。

多线程并发TCP服务器

服务端:

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
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <pthread.h>
void* client_process(void* arg) {
int recv_len = 0;
char recv_buf[1024] = ""; // 接收缓冲区
long tmp = (long)arg;
int connfd = (int)tmp; // 传过来的已连接套接字
while ((recv_len = recv(connfd, recv_buf, sizeof(recv_buf), 0)) > 0) { // 接收数据
printf("recv_buf: %s\n", recv_buf); // 打印数据
send(connfd, recv_buf, recv_len, 0); // 给客户端回数据
};
printf("client closed!\n");
close(connfd); //关闭已连接套接字
return NULL;
};
int main() {
int sockfd = 0, connfd = 0, err_log = 0;
char on = 1;
struct sockaddr_in my_addr; // 服务器地址结构体
unsigned short port = 8888; // 监听端口
pthread_t thread_id;
sockfd = socket(AF_INET, SOCK_STREAM, 0); // 创建TCP套接字
if (sockfd < 0) {
perror("socket error");
exit(-1);
};
bzero(&my_addr, sizeof(my_addr)); // 初始化服务器地址
my_addr.sin_family = AF_INET;
my_addr.sin_port = htons(port);
my_addr.sin_addr.s_addr = htonl(INADDR_ANY);
printf("Binding server to port %d\n", port);
setsockopt(sockfd, SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on)); //端口复用
err_log = bind(sockfd, (struct sockaddr*)&my_addr, sizeof(my_addr));// 绑定
if (err_log != 0) {
perror("bind");
close(sockfd);
getchar();
exit(-1);
};
err_log = listen(sockfd, 10); // 监听,套接字变被动
if (err_log != 0) {
perror("listen");
close(sockfd);
exit(-1);
};
while (1) {
char cli_ip[INET_ADDRSTRLEN] = ""; // 用于保存客户端IP地址
struct sockaddr_in client_addr; // 用于保存客户端地址
socklen_t cliaddr_len = sizeof(client_addr); // 必须初始化!!!
printf("Waiting client...\n");
connfd = accept(sockfd, (struct sockaddr*)&client_addr, &cliaddr_len); //获得一个已经建立的连接
if (connfd < 0) {
perror("accept this time");
continue;
};
inet_ntop(AF_INET, &client_addr.sin_addr, cli_ip, INET_ADDRSTRLEN); // 打印客户端的 ip 和端口
printf("----------------------------------------------\n");
printf("client ip=%s,port=%d\n", cli_ip, ntohs(client_addr.sin_port));
if (connfd > 0) { //由于同一个进程内的所有线程共享内存和变量,因此在传递参数时需作特殊处理,值传递。
pthread_create(&thread_id, NULL, client_process, (void*)connfd); //创建线程
pthread_detach(thread_id); // 线程分离,让子线程结束时自动回收资源
};
};
close(sockfd);
return 0;
};

I/O多路复用服务器

select

select声明:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
typedef long int __fd_mask;
typedef struct {
/* XPG4.2 requires this member name. Otherwise avoid the namefrom the global namespace. */
__fd_mask fds_bits[__FD_SETSIZE / __NFDBITS];
#define __FDS_BITS(set) ((set)->fds_bits)
} fd_set;
struct timeval{
__time_t tv_sec; /* Seconds. */
__suseconds_t tv_usec; /* Microseconds. */
};

#include <sys/select.h>
int select(
int nfds, //集合中所有文件描述符范围 值为文件描述符最大值+1
fd_set* _Nullable restrict readfds, //要监视读变化的文件描述符
fd_set* _Nullable restrict writefds, //要监视写变化的文件描述符
fd_set* _Nullable restrict exceptfds, //监视文件错误异常文件
struct timeval* _Nullable restrict timeout //等待时间 为NULL则阻塞 为0则非阻塞 非0则有超时时间
); //有文件可读/写则返回大于0 超时0 错误负值

select返回后,readfds、writefds和exceptfds会返回相应套接字。readfds包括有数据可读的(用recv可立即收到数据的)、连接已关闭/重设/终止的、正请求建立连接的(用accept会成功)套接字。writefds包括有数据可发出的(用send可发送数据的)、用connect连接成功的套接字。exceptfds包括用connect连接失败的、有带外数据可读的套接字。

fd_set默认最大值为1024个,要改还得重新编译内核,改用下面的poll模型可规避该问题。下面宏可对fd_set进行操作:

1
2
3
4
5
//前3个返回值void 最后一个int 参数fd为int 参数fdsetp为fd_set*
#define FD_SET(fd, fdsetp) __FD_SET (fd, fdsetp) //将fd套接字加入到fdsetp集合中
#define FD_CLR(fd, fdsetp) __FD_CLR (fd, fdsetp) //从fdsetp集合中删除fd套接字
#define FD_ISSET(fd, fdsetp) __FD_ISSET (fd, fdsetp) //检查套接字fd是否为fdsetp集合成员
#define FD_ZERO(fdsetp) __FD_ZERO (fdsetp) //将fdsetp集合初始化为空集合

服务端实现:

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
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <errno.h>
#include <string.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <sys/time.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#define MYPORT 8888 //连接时使用的端口
#define MAXCLINE 5 //连接队列中的个数
#define BUF_SIZE 200
int fd[MAXCLINE]; //连接的fd
int conn_amount; //当前的连接数
void showclient() {
int i;
printf("client amount:%d\n", conn_amount);
for (i = 0; i < MAXCLINE; i++)
printf("[%d]:%d ", i, fd[i]);
printf("\n\n");
return;
};
int main(void){
int sock_fd, new_fd; //监听套接字 连接套接字
struct sockaddr_in server_addr; // 服务器的地址信息
struct sockaddr_in client_addr; //客户端的地址信息
socklen_t sin_size;
int yes = 1;
char buf[BUF_SIZE];
int ret;
int i;
if ((sock_fd = socket(AF_INET, SOCK_STREAM, 0)) == -1) { //建立sock_fd套接字
perror("setsockopt");
exit(1);
};
if (setsockopt(sock_fd, SOL_SOCKET, SO_REUSEADDR, &yes, sizeof(int)) == -1) { //设置套接口的选项 SO_REUSEADDR 允许在同一个端口启动服务器的多个实例 setsockopt的第二个参数SOL SOCKET 指定系统中,解释选项的级别 普通套接字
perror("setsockopt error \n");
exit(1);
};
server_addr.sin_family = AF_INET; //主机字节序
server_addr.sin_port = htons(MYPORT);
server_addr.sin_addr.s_addr = INADDR_ANY;//通配IP
memset(server_addr.sin_zero, '\0', sizeof(server_addr.sin_zero));
if (bind(sock_fd, (struct sockaddr*)&server_addr, sizeof(server_addr)) == -1) {
perror("bind error!\n");
getchar();
exit(1);
};
if (listen(sock_fd, MAXCLINE) == -1) {
perror("listen error!\n");
exit(1);
};
printf("listen port %d\n", MYPORT);
fd_set fdsr; //文件描述符集的定义
int maxsock;
struct timeval tv;
conn_amount = 0;
sin_size = sizeof(client_addr);
maxsock = sock_fd;
while (1) {
FD_ZERO(&fdsr); //初始化文件描述符集合 清除描述符集
FD_SET(sock_fd, &fdsr); //把sock_fd加入描述符集
tv.tv_sec = 30; //超时的设定
tv.tv_usec = 0;
for (i = 0; i < MAXCLINE; i++) //添加活动的连接
if (fd[i] != 0)
FD_SET(fd[i], &fdsr);
ret = select(maxsock + 1, &fdsr, NULL, NULL, &tv); //如果文件描述符中有连接请求 会做相应的处理,实现I/O的复用 多用户的连接通讯
if (ret < 0) {//没有找到有效的连接 失败
perror("select error!\n");
break;
}
else
if (ret == 0) {// 指定的时间到,
printf("timeout \n");
continue;
};
for (i = 0; i < conn_amount; i++) //循环判断有效的连接是否有数据到达
if (FD_ISSET(fd[i], &fdsr)) {
ret = recv(fd[i], buf, sizeof(buf), 0);
if (ret <= 0) {//客户端连接关闭,清除文件描述符集中的相应的位
printf("client[%d] close\n", i);
close(fd[i]);
FD_CLR(fd[i], &fdsr);
fd[i] = 0;
conn_amount--;
}
else { //否则有相应的数据发送过来 ,进行相应的处理
if (ret < BUF_SIZE)
memset(&buf[ret], '\0', 1);
printf("client[%d] send:%s\n", i, buf);
send(fd[i], buf, sizeof(buf), 0);//反射回去
};
};
if (FD_ISSET(sock_fd, &fdsr)) {
new_fd = accept(sock_fd, (struct sockaddr*)&client_addr, &sin_size);
if (new_fd <= 0) {
perror("accept error\n");
continue;
};
if (conn_amount < MAXCLINE) { //添加新的fd 到数组中 判断有效的连接数是否小于最大的连接数,如果小于的话,就把新的连接套接字加入集合
for (i = 0; i < MAXCLINE; i++)
if (fd[i] == 0) {
fd[i] = new_fd;
break;
};
conn_amount++;
printf("new connection client[%d]%s:%d\n", conn_amount, inet_ntoa(client_addr.sin_addr), ntohs(client_addr.sin_port));
if (new_fd > maxsock)
maxsock = new_fd;
}
else {
printf("max connections arrive ,exit\n");
send(new_fd, "bye", 4, 0);
close(new_fd);
continue;
};
};
showclient();
};
for (i = 0; i < MAXCLINE; i++)
if (fd[i] != 0)
close(fd[i]);
return 0;
};

select和下面的poll模型中,包含大量文件描述符的数组被整体复制于用户态和内核地址空间之间,开销随数量增加而变大。select使用数组维护文件描述符,poll用链表维护,所以不需要担心数量。

poll

poll用途与select大致相同,其中fds参数在返回后被清零,需要自己重新设置:

1
2
3
4
5
6
7
8
9
10
11
12
struct pollfd {
int fd; /* File descriptor to poll. 文件描述符 */
short int events; /* Types of events poller cares about. 等待的事件 由用户设置 */
short int revents; /* Types of events that actually occurred. 实际发生的事件 返回时由内核设置 */
};

#include <poll.h>
int poll(
struct pollfd* __fds,
nfds_t __nfds, //fds参数元素个数
int __timeout //等待毫秒数 -1永远等待 0立即返回
);//成功返回revents域不为0的文件描述符个数 超时前无事件发生则0 失败-1

常见错误值如下:

错误值 含义
EBADF 结构体中有文件描述符无效
EFAULT fds指向地址超出进程地址空间
EINTR 请求的事件之前产生一个信号 调用可重新发起
EINVAL nfds超出PLIMIT_NOFILE
ENOMEM 内存不足

events与revents域常用取值如下:

事件 取值 含义
读事件 POLLIN 普通或优先带数据可读
POLLRDNORM 普通数据可读
POLLRDBAND 优先级带数据可读
POLLPRI 高优先级数据可读
写事件 POLLOUT 普通或优先带数据可写
POLLWRNORM 普通数据可写
POLLWRBAND 优先级带数据可写
错误事件,仅作为revents域取值 POLLERR 发生错误
POLLHUP 发生挂起
POLLVAL 描述不是打开的文件

该实例中将sendrecv改为使用writeread。当read读取常规文件时,若实际读取数据少于想读取的数据,则不阻塞并返回。当从终端读入输入的数据没有换行符时阻塞,否则情况不阻塞。当从网络设备读时若没接收到数据报则阻塞,读取数值少于想读取字节数也不阻塞。

1
2
3
4
5
6
7
8
9
10
ssize_t write(
int __fd, //句柄 要写数据的目标
const void* __buf, //数据缓冲区
size_t __n //要写数据个数
); //成功返回实际写入字节数 错误-1 错误码errno
ssize_t read(
int __fd, //句柄 要读数据的目标
void* __buf,
size_t __nbytes //想读的数据长度
); //成功返回实际读到的字节数

服务端实现:

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
#include <unistd.h>
#include <fcntl.h>
#include <poll.h>
#include <time.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <cstdio>
#include <cstdlib>
#include <errno.h>
#include <cstring>
#include <vector> // 每个stl都需要对应的头文件
#include <initializer_list>
using std::initializer_list;
using std::vector;
void errExit(){
getchar();
exit(-1);
return;
};
const char resp[] = "HTTP/1.1 200\r\n\
Content-Type: application/json\r\n\
Content-Length: 13\r\n\
Date: Thu, 2 Aug 2021 04:02:00 GMT\r\n\
Keep-Alive: timeout=60\r\n\
Connection: keep-alive\r\n\r\n\
[HELLO WORLD]\r\n\r\n";
int main() {
const int port = 8888; //创建套接字
int sd, ret;
sd = socket(AF_INET, SOCK_STREAM, 0);
fprintf(stderr, "created socket\n");
if (sd == -1)
errExit();
int opt = 1;
if (setsockopt(sd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(int)) == -1) // 重用地址
errExit();
fprintf(stderr, "socket opt set\n");
sockaddr_in addr;
addr.sin_family = AF_INET, addr.sin_port = htons(port);
addr.sin_addr.s_addr = INADDR_ANY;
socklen_t addrLen = sizeof(addr);
if (bind(sd, (sockaddr*)&addr, sizeof(addr)) == -1)
errExit();
fprintf(stderr, "socket binded\n");
if (listen(sd, 1024) == -1)
errExit();
fprintf(stderr, "socket listen start\n");
//套接字创建完毕 初始化监听列表
int currentFdNum = 1; //number of poll fds
pollfd* fds = static_cast<pollfd*>(calloc(100, sizeof(pollfd)));
fds[0].fd = sd, fds[0].events = POLLIN;
nfds_t nfds = 1;
int timeout = -1;
fprintf(stderr, "polling\n");
while (1) {
ret = poll(fds, nfds, timeout); //执行poll操作
fprintf(stderr, "poll returned with ret value: %d\n", ret);
if (ret == -1)
errExit();
else
if (ret == 0) {
fprintf(stderr, "return no data\n");
}
else { // ret > 0
fprintf(stderr, "checking fds\n"); // got accept
if (fds[0].revents & POLLIN) { //检查是否有新客户端建立连接
sockaddr_in childAddr;
socklen_t childAddrLen;
int childSd = accept(sd, (sockaddr*)&childAddr, &(childAddrLen));
if (childSd == -1)
errExit();
fprintf(stderr, "child got\n");
int flags = fcntl(childSd, F_GETFL); // set non_block
if (fcntl(childSd, F_SETFL, flags | O_NONBLOCK) == -1) // accept并设置为非阻塞
errExit();
fprintf(stderr, "child set nonblock\n");
fds[currentFdNum].fd = childSd, fds[currentFdNum].events = (POLLIN | POLLRDHUP); // add child to list 假如到poll的描述符集,关心POLLIN事件
nfds++, currentFdNum++;
fprintf(stderr, "child: %d pushed to poll list\n", currentFdNum - 1);
};
for (int i = 1; i < currentFdNum; i++) { // child read & write 检查其他描述符的事件
if (fds[i].revents & (POLLHUP | POLLRDHUP | POLLNVAL)) {
fprintf(stderr, "child: %d shutdown\n", i); //客户端描述符关闭 设置events=0, fd=-1,不再关心 set not interested
close(fds[i].fd);
fds[i].events = 0;
fds[i].fd = -1;
continue;
};

if (fds[i].revents & POLLIN) { // read
char buffer[1024] = {};
while (1) {
ret = read(fds[i].fd, buffer, 1024); //读取请求数据
fprintf(stderr, "read on: %d returned with value: %d\n", i, ret);
if (ret == 0) {
fprintf(stderr, "read returned 0(EOF) on: %d, breaking\n", i);
break;
};
if (ret == -1) {
const int tmpErrno = errno;
if (tmpErrno == EWOULDBLOCK || tmpErrno == EAGAIN) { //会阻塞,这里认为读取完毕 实际需要检查读取数据是否完毕
fprintf(stderr, "read would block, stop reading\n");
fds[i].events |= POLLOUT; // read is over http pipe line? need to put resp into a queue 可以监听写事件了 POLLOUT
break;
}
else
errExit();
};
};
};
if (fds[i].revents & POLLOUT) { // write
ret = write(fds[i].fd, resp, sizeof(resp)); //写事件,把请求返回
fprintf(stderr, "write on: %d returned with value: %d\n", i, ret);
if (ret == -1) //这里需要处理 EAGAIN EWOULDBLOCK
errExit();
fds[i].events &= !(POLLOUT);
};
};
};
};
return 0;
};

epoll

epoll可代替poll,在具有大量客户端请求时有更好的性能。select模型中,select对所有句柄进行轮循,可近似解释为时间复杂度为$O(n)$。poll在实现方式上与select没有较大差别。epoll与poll最大都支持65535个文件描述符。

epoll通过内核与用户空间mmap同一块内存实现,使得这块物理内存对内核和用户均可见,减少用户态和内核态之间的数据交换,内核可直接看到epoll监听的句柄。epoll采用红黑树存储所有套接字。把事件添加进来时,将该事件与相应网卡设备驱动建立回调关系,该回调函数在内核中称为ep_poll_callback,就是将该事件添加到rdllist双向链表中。用epoll_wait时检查rdllist中是否存在注册的事件,所以epoll模型可近似解释为时间复杂度为$O(1)$。

服务器模型分为两种工作方式。水平触发LT为默认工作方式,当一个描述符就绪,内核就会通知处理,若不进行处理,内核下次还会通知;边缘触发ET只支持非阻塞描述符,需程序保证缓冲区数据全被读/写出,描述符就绪后不会再次通知。select和poll模型只支持LT,epoll支持更高效的ET。

epoll_create创建一个epoll句柄,在早期epoll实现中,监控文件描述符的组织不是红黑树而是哈希表,所以这里size参数已无意义。在成功创建后可在/proc/PID/fd/下看到该fd,所以使用epoll后必须用close关闭,否则可能导致fd被耗尽。

1
2
3
4
#include <sys/epoll.h>
int epoll_create(
int __size //需要监听的文件描述符数目
); //返回epoll句柄

epoll_ctl控制epoll监控的文件描述符上的事件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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_PACKED;

int epoll_ctl(
int __epfd, //要操作的文件描述符
int __op, //操作
int __fd, //操作实施的对象
struct epoll_event* __event //要监听什么事件
);

op参数常用取值如下:

取值 含义
EPOLL_CTL_ADD 注册新的fd到epfd中
EPOLL_CTL_MOD 修改已注册的fd的监听事件
EPOLL_CTL_DEL 从epfd中删除一个fd

参数event取值可以是以下某些选项的集合:

选项 含义
EPOLLIN 文件描述符可读,以及套接字正常关闭
EPOLLOUT 文件描述符可写
EPOLLPRI 文件描述符有带外数据到来
EPOLLERR 文件描述符发生错误
EPOLLHUP 文件描述符被挂断
EPOLLET 将epoll设为ET模式
EPOLLONESHOT 只监听一次事件,监听后需再次将该套接字加入epoll队列

epoll_wait收集epoll监控中已发生的事件:

1
2
3
4
5
6
int epoll_wait(
int __epfd, //要操作的文件描述符
struct epoll_event* __events, //返回检测到的事件集合
int __maxevents, //最多监听多少个事件
int __timeout //超时时间 单位毫秒 -1永远阻塞 0立即返回
); //成功返回就绪的文件描述符个数 失败-1 错误码errno

服务端实现:

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
#include <ctype.h>
#include <cstdio>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <sys/epoll.h>
#include <errno.h>
#include <unistd.h> //for close
#define MAXLINE 80
#define SERV_PORT 8888
#define OPEN_MAX 1024
int main(int argc, char* argv[]) {
int i, j, maxi, listenfd, connfd, sockfd;
int nready, efd, res;
ssize_t n;
char buf[MAXLINE], str[INET_ADDRSTRLEN];
socklen_t clilen;
int client[OPEN_MAX];
struct sockaddr_in cliaddr, servaddr;
struct epoll_event tep, ep[OPEN_MAX];//接收 存放数据
listenfd = socket(AF_INET, SOCK_STREAM, 0); //网络socket初始化
bzero(&servaddr, sizeof(servaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
servaddr.sin_port = htons(SERV_PORT);
if (-1 == bind(listenfd, (struct sockaddr*)&servaddr, sizeof(servaddr)))
perror("bind");
if (-1 == listen(listenfd, 20))
perror("listen");
puts("listen ok");
for (i = 0; i < OPEN_MAX; i++)
client[i] = -1;
maxi = -1;//后面数据初始化赋值时 数据初始化为-1
efd = epoll_create(OPEN_MAX); //创建树
if (efd == -1)
perror("epoll_create");
tep.events = EPOLLIN; //添加监听套接字
tep.data.fd = listenfd;
res = epoll_ctl(efd, EPOLL_CTL_ADD, listenfd, &tep);
if (res == -1)
error("epoll_ctl");
for (; ; ) {
nready = epoll_wait(efd, ep, OPEN_MAX, -1);//阻塞监听
if (nready == -1)
perror("epoll_wait");
for (i = 0; i < nready; i++) { //如果有事件发生 开始数据处理
if (!(ep[i].events & EPOLLIN)) //是否是读事件
continue;
if (ep[i].data.fd == listenfd) { //若处理的事件和文件描述符相等 数据处理
clilen = sizeof(cliaddr); //接收客户端
connfd = accept(listenfd, (struct sockaddr*)&cliaddr, &clilen);
printf("received from %s at PORT %d\n", inet_ntop(AF_INET, &cliaddr.sin_addr, str, sizeof(str)), ntohs(cliaddr.sin_port));
for (j = 0; j < OPEN_MAX; j++)
if (client[j] < 0) {
client[j] = connfd; //将通信套接字存放到client
break;
};
if (j == OPEN_MAX) //是否到达最大值 保护判断
perror("too many clients");
if (j > maxi) //更新client下标
maxi = j;
tep.events = EPOLLIN; //添加通信套接字到树上
tep.data.fd = connfd;
res = epoll_ctl(efd, EPOLL_CTL_ADD, connfd, &tep);
if (res == -1)
perror("epoll_ctl");
}
else {
sockfd = ep[i].data.fd;//将connfd赋值给socket
n = read(sockfd, buf, MAXLINE);//读取数据
if (n == 0) { //无数据则删除该结点
for (j = 0; j <= maxi; j++) //将Client中对应fd数据值恢复为-1
if (client[j] == sockfd) {
client[j] = -1;
break;
};
res = epoll_ctl(efd, EPOLL_CTL_DEL, sockfd, NULL);//删除树结点
if (res == -1)
perror("epoll_ctl");
close(sockfd);
printf("client[%d] closed connection\n", j);
}
else { //有数据则写回数据
printf("recive client's data:%s\n", buf); //这里可以根据实际情况扩展,模拟对数据进行处理
for (j = 0; j < n; j++)
buf[j] = toupper(buf[j]); //现在简单的转为大写
write(sockfd, buf, n); //回送给客户端
};
};
};
};
close(listenfd);
close(efd);
return 0;
};