操作系统课程的实验,要求使用多进程、多线程、多路复用实现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; 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); 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; }
|
运行结果:
多线程实现服务器并发
定义一个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; if (argc != 2) { fprintf(stderr, "usage: %s <port>\n", argv[0]); exit(1); }
listenfd = Open_listenfd(argv[1]);
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; }
|
运行结果:
多路复用实现并发
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; if (argc != 2) { fprintf(stderr, "usage: %s <port>\n", argv[0]); exit(1); }
listenfd = Open_listenfd(argv[1]);
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; }
|
运行结果:
实验结果分析
使用进程最简单,因为每个进程互相独立,不影响主程序的稳定性,子进程崩溃没关系,每个子进程都有自己的地址空间和相关资源,总体能够达到的性能上限非常大,但是调度开销比较大。
多路复用编程复杂度高,但是由于多路复用是在单一进程的上下文中的,因此每个逻辑流程都能访问该进程的全部地址空间,所以开销比多进程低得多。
多线程每个线程都有自己的线程上下文,所有的运行在一个进程里的线程共享该进程的整个虚拟地址空间。由于线程运行在单一进程中,因此共享这个进程虚拟地址空间的整个内容,包括它的代码、数据、堆、共享库和打开的文件。优点是程序逻辑和控制方式简单,所有线程可以直接共享内存和变量等,耗的总资源比进程方式好。但是由于每个线程与主程序共用地址空间,地址空间受限,一个线程的崩溃也可能影响到整个程序的稳定性,性能提高也有限制。
源代码
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; 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); Getnameinfo((SA *) &clientaddr, clientlen, hostname, MAXLINE, port, MAXLINE, 0); printf("Accepted connection from (%s, %s)\n", hostname, port); doit(connfd); Close(connfd); }
return 0; }
|