使用多进程、多线程、多路复用实现Web服务器

操作系统课程的实验,要求使用多进程、多线程、多路复用实现Web服务器并发,使用的是tiny的一个简单的WEB服务器源码进行修改,使其支持多进程、多线程、多路复用。

相关原理

TCP套接字通信

Echo通信例子:

select:

文件描述符集合操作:

一般来说,在每次使用select()函数之前,首先使用FD_ZERO()和FD_SET()来初始化文件描述符集(在需要重复调用select()函数的时候,先把一次初始化好的文件描述符集备份下来,每次读取它即可)。在select()函数返回之后,可循环使用FD_ISSET()来测试描述符集,在执行完对相关文件描述符的操作之后,使用FD_CLR()来清除描述符集。
另外,select()函数中的timeout是一个struct timeval类型的指针,该结构体如下所示:

1
2
3
4
5
struct timeval 
{
long tv_sec; /* 秒 */
long tv_unsec; /* 微秒 */
}

WEB server

模型:

实现过程

多进程实现服务器并发

调用Fork创建子进程,父进程继续监听。在子进程中对客户端进行处理,因为fork完全复制父进程,所以要在子进程中关掉监听套接字listenfd。子进程处理完之后,父子进程都要关掉连接套接字connfd。

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
int main(int argc, char **argv) 
{
int listenfd, connfd;
char hostname[MAXLINE], port[MAXLINE];
socklen_t clientlen;
struct sockaddr_storage clientaddr;


/* Check command line args */
if (argc != 2) {
fprintf(stderr, "usage: %s <port>\n", argv[0]);
exit(1);
}

listenfd = Open_listenfd(argv[1]);

//原始模式
/*while (1) {
clientlen = sizeof(clientaddr);
connfd = Accept(listenfd, (SA *)&clientaddr, &clientlen); //line:netp:tiny:accept
Getnameinfo((SA *) &clientaddr, clientlen, hostname, MAXLINE,
port, MAXLINE, 0);
printf("Accepted connection from (%s, %s)\n", hostname, port);
doit(connfd); //line:netp:tiny:doit
Close(connfd); //line:netp:tiny:close
}*/
// 多进程
while (1) {
clientlen = sizeof(clientaddr);
connfd = Accept(listenfd, (SA *)&clientaddr, &clientlen);
if (Fork() == 0) {
Close(listenfd);
Getnameinfo((SA *) &clientaddr, clientlen, hostname, MAXLINE,
port, MAXLINE, 0);
printf("Accepted connection from (%s, %s)\n", hostname, port);
doit(connfd);
Close(connfd);
exit(0);
}
Close(connfd);
}
return 0;

}

运行结果:

1
2
make
sudo ./tiny 6666

多线程实现服务器并发

定义一个tid用于记录创建线程的id,主线程用于监听套接字,有新的连接建立之后创建一个副线程,主线程继续监听。副线程调用执行线程函数,在该函数中完成对新建立连接的处理,处理完之后关闭套接字,副线程结束。

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
void *thread(void *argv)
{
int connfd = *((int *)argv);
Pthread_detach(pthread_self());
doit(connfd);
Close(connfd);

return NULL;
}

int main(int argc, char **argv)
{
int listenfd, connfd;
char hostname[MAXLINE], port[MAXLINE];
socklen_t clientlen;
struct sockaddr_storage clientaddr;


/* Check command line args */
if (argc != 2) {
fprintf(stderr, "usage: %s <port>\n", argv[0]);
exit(1);
}

listenfd = Open_listenfd(argv[1]);

//原始模式
/*while (1) {
clientlen = sizeof(clientaddr);
connfd = Accept(listenfd, (SA *)&clientaddr, &clientlen); //line:netp:tiny:accept
Getnameinfo((SA *) &clientaddr, clientlen, hostname, MAXLINE,
port, MAXLINE, 0);
printf("Accepted connection from (%s, %s)\n", hostname, port);
doit(connfd); //line:netp:tiny:doit
Close(connfd); //line:netp:tiny:close
}*/

//多线程
pthread_t tid;
while (1) {
clientlen = sizeof(clientaddr);
connfd = Accept(listenfd, (SA *)&clientaddr, &clientlen);
Getnameinfo((SA *) &clientaddr, clientlen, hostname, MAXLINE,
port, MAXLINE, 0);
printf("Accepted connection from (%s, %s)\n", hostname, port);
Pthread_create(&tid, NULL, thread, &connfd);
}

return 0;

}

运行结果:

1
2
make
sudo ./tiny 6666

多路复用实现并发

Fd_set rds,tmp定义两个文件描述符集,rds为用于监控的集合,tmp用于临时复制rds。FD_ZERO将rds清零,RD_SET将选定的位置1,fd_max是监听的套接字中最大的套接字描述符加一。
因为每次执行完select之后,未发生状态变化的套接字位会被置0,所以在循环开始时要对rds进行复制,对副本tmp进行select操作。select会把未就绪的描述符位置0,遍历文件描述符至fd_max,对每个文件描述符进行判断是否在文件描述符集tmp中,若在则进行处理。因为处理的都是就绪的套接字,所以不会发生阻塞。
如果为监听套接字就绪,新建立连接的描述符要加入rds中,如果为连接套接字,处理完之后要将其从文件描述符集rds中清除。

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
int main(int argc, char **argv) 
{
int listenfd, connfd;
char hostname[MAXLINE], port[MAXLINE];
socklen_t clientlen;
struct sockaddr_storage clientaddr;


/* Check command line args */
if (argc != 2) {
fprintf(stderr, "usage: %s <port>\n", argv[0]);
exit(1);
}

listenfd = Open_listenfd(argv[1]);

//原始模式
/*while (1) {
clientlen = sizeof(clientaddr);
connfd = Accept(listenfd, (SA *)&clientaddr, &clientlen); //line:netp:tiny:accept
Getnameinfo((SA *) &clientaddr, clientlen, hostname, MAXLINE,
port, MAXLINE, 0);
printf("Accepted connection from (%s, %s)\n", hostname, port);
doit(connfd); //line:netp:tiny:doit
Close(connfd); //line:netp:tiny:close
}*/
//多路复用
fd_set rds, tmp;
int fd_max,i;
FD_ZERO(&rds);
FD_SET(listenfd, &rds);
fd_max = listenfd + 1;
while (1) {
tmp = rds;
select(fd_max, &tmp, NULL, NULL, NULL);
for (i=0; i<fd_max; i++) {
if (FD_ISSET(i, &tmp)) {
if (listenfd == i) {
clientlen = sizeof(clientaddr);
connfd = Accept(listenfd, (SA *)&clientaddr, &clientlen);
FD_SET(connfd, &rds);
if (fd_max <= connfd) fd_max = connfd+1;

Getnameinfo((SA *) &clientaddr, clientlen, hostname, MAXLINE,
port, MAXLINE, 0);
printf("Accepted connection from (%s, %s)\n", hostname, port);
}
else {
doit(i);
FD_CLR(i, &rds);
Close(i);
}
}
}
}
return 0;

}

运行结果:

1
2
make
sudo ./tiny 6666

实验结果分析

使用进程最简单,因为每个进程互相独立,不影响主程序的稳定性,子进程崩溃没关系,每个子进程都有自己的地址空间和相关资源,总体能够达到的性能上限非常大,但是调度开销比较大。
多路复用编程复杂度高,但是由于多路复用是在单一进程的上下文中的,因此每个逻辑流程都能访问该进程的全部地址空间,所以开销比多进程低得多。
多线程每个线程都有自己的线程上下文,所有的运行在一个进程里的线程共享该进程的整个虚拟地址空间。由于线程运行在单一进程中,因此共享这个进程虚拟地址空间的整个内容,包括它的代码、数据、堆、共享库和打开的文件。优点是程序逻辑和控制方式简单,所有线程可以直接共享内存和变量等,耗的总资源比进程方式好。但是由于每个线程与主程序共用地址空间,地址空间受限,一个线程的崩溃也可能影响到整个程序的稳定性,性能提高也有限制。

源代码

tiny.c修改的main函数部分代码:

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
void *thread(void *argv)
{
int connfd = *((int *)argv);
Pthread_detach(pthread_self());
doit(connfd);
Close(connfd);

return NULL;
}

int main(int argc, char **argv)
{
int listenfd, connfd;
char hostname[MAXLINE], port[MAXLINE];
socklen_t clientlen;
struct sockaddr_storage clientaddr;


/* Check command line args */
if (argc != 2) {
fprintf(stderr, "usage: %s <port>\n", argv[0]);
exit(1);
}

listenfd = Open_listenfd(argv[1]);

//原始模式
while (1) {
clientlen = sizeof(clientaddr);
connfd = Accept(listenfd, (SA *)&clientaddr, &clientlen); //line:netp:tiny:accept
Getnameinfo((SA *) &clientaddr, clientlen, hostname, MAXLINE,
port, MAXLINE, 0);
printf("Accepted connection from (%s, %s)\n", hostname, port);
doit(connfd); //line:netp:tiny:doit
Close(connfd); //line:netp:tiny:close
}
// 多进程
/*while (1) {
clientlen = sizeof(clientaddr);
connfd = Accept(listenfd, (SA *)&clientaddr, &clientlen);
if (Fork() == 0) {
Close(listenfd);
Getnameinfo((SA *) &clientaddr, clientlen, hostname, MAXLINE,
port, MAXLINE, 0);
printf("Accepted connection from (%s, %s)\n", hostname, port);
doit(connfd);
Close(connfd);
exit(0);
}
Close(connfd);
}*/
//多路复用
/*
fd_set rds, tmp;
int fd_max,i;
FD_ZERO(&rds);
FD_SET(listenfd, &rds);
fd_max = listenfd + 1;
while (1) {
tmp = rds;
select(fd_max, &tmp, NULL, NULL, NULL);
for (i=0; i<fd_max; i++) {
if (FD_ISSET(i, &tmp)) {
if (listenfd == i) {
clientlen = sizeof(clientaddr);
connfd = Accept(listenfd, (SA *)&clientaddr, &clientlen);
FD_SET(connfd, &rds);
if (fd_max <= connfd) fd_max = connfd+1;

Getnameinfo((SA *) &clientaddr, clientlen, hostname, MAXLINE,
port, MAXLINE, 0);
printf("Accepted connection from (%s, %s)\n", hostname, port);
}
else {
doit(i);
FD_CLR(i, &rds);
Close(i);
}
}
}
}*/
//多线程

/*pthread_t tid;
while (1) {
clientlen = sizeof(clientaddr);
connfd = Accept(listenfd, (SA *)&clientaddr, &clientlen);
Getnameinfo((SA *) &clientaddr, clientlen, hostname, MAXLINE,
port, MAXLINE, 0);
printf("Accepted connection from (%s, %s)\n", hostname, port);
Pthread_create(&tid, NULL, thread, &connfd);
}*/

return 0;

}

使用多进程、多线程、多路复用实现Web服务器
https://chujian521.github.io/blog/2019/10/30/使用多进程、多线程、多路复用实现Web服务器/
作者
Encounter
发布于
2019年10月30日
许可协议