《深入理解计算机系统》Tiny服务器3——poll类型IO复用版和多线程版Tiny

  上次的博客,记录到了根据CSAPP里客户端池的IO多路复用版Tiny的实现。今天学习了《UNIX网络编程》的前几章。在第6章里讲述了基于select()和poll()函数的IO复用,并且给出了和CSAPP中相似的代码实现。这不过,Stevens大神没有将主要元素设为一个结构体。顺便一提,上次的基于select()函数的服务器实现过程中我遇到了很多问题,主要有以下几个方面:
  1) 在上一个多路复用服务器实现中,每次进行静态页面的请求时,在服务器端都会输出一条错误信息:

  *** stack smashing detected ***: ./csapp_select_tiny terminated
  Aborted (core dumped)
这是由于程序中分配的缓冲区太小了。我们把缓冲区大小BUFSIZE设置为4000,即可解决问题。
  2) 有时候在浏览器中获取了静态页面或动态数据之后,服务器端会不断有乱码刷出来。经过GDB跟踪调试,发现是在页面获取成功之后,相应的文件描述符没有取消掉。增加一条FD_CLR宏即可。
  3) 另外认识到浏览器与web服务器之间的链接一般是短链接。以浏览器请求动态CGI程序为例。在浏览器地址栏中输入"localhost:8888/",浏览器首先向服务器发出connect()请求,获取主页面。当我们输入要相加的数据之后,此时浏览器与服务器已经断开了连接。还需要浏览器向服务器发送connect()连接请求,重新建立连接,并进行动态内容的访问。在服务器中输出的信息如下:

Request headers:
GET / HTTP/1.1             
User-Agent: Mozilla/5.0 (X11; Ubuntu; Linux i686; rv:54.0) Gecko/20100101 Firefox/54.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate
Connection: keep-alive
Upgrade-Insecure-Requests: 1

Response headers:
HTTP/1.0 200 OK
Server: Tiny Web Server
Connection: close
Content-length: 290
Content-type: text/html

Request headers:
GET /cgi-bin/adder?num1=12&num2=34 HTTP/1.1
User-Agent: Mozilla/5.0 (X11; Ubuntu; Linux i686; rv:54.0) Gecko/20100101 Firefox/54.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate
Referer: http://localhost:8888/
Connection: keep-alive
Upgrade-Insecure-Requests: 1

^C

看似很简单的思想,帮助我解决了很多调试中不容易发现的问题。

  今天我还参照CSAPP的结构体和函数设计,针对poll()方法设计了相应的结构体和函数,总体比较来看变化不大。现将代码贴在下边。

 1 typedef struct {
 2     int        maxi;            //client数组当前最大下标值
 3     int        nready;            //准备的描述符
 4         struct pollfd    client[OPEN_MAX];    //pollfd结构数组
 5     rio_t        clientrio[FD_SETSIZE];  //与描述符对应的缓冲区
 6 } pool;
 7 
 8 
 9 void init_pool(int listenfd, pool *p)
10 {
11     int i;
12 
13     p->client[0].fd = listenfd;
14     p->client[0].events = POLLRDNORM;
15     for (i = 1; i < OPEN_MAX; i++)
16         p->client[i].fd = -1;
17     p->maxi = 0;
18     p->nready = 0;
19 }
20 
21 void add_client(int connfd, pool *p)
22 {
23     int i;
24     p->nready--;
25     for (i = 0; i < OPEN_MAX; i++)
26 
27         if (p->client[i].fd < 0) {
28             p->client[i].fd = connfd;
29             rio_readinitb(&p->clientrio[i], connfd);
30 
31             p->client[i].events = POLLRDNORM;
32 
33             if (i > p->maxi)
34                 p->maxi = i;
35             break;
36         }
37     if (i == OPEN_MAX)
38     {
39         fprintf(stderr, "too many clients\n");
40         exit(1);
41     }
42 }
43 
44 void check_clients(pool *p)
45 {
46     int i, connfd, n;
47     char buf[MAXLINE];
48     rio_t rio;
49 
50     for (i = 1; (i <= p->maxi) && (p->nready > 0); i++) {
51         connfd = p->client[i].fd;
52         rio = p->clientrio[i];
53 
54         if ((connfd > 0) && (p->client[i].revents & (POLLRDNORM | POLLERR)))
55         {
56             doit(&connfd, p);
57         }
58 
59         close(connfd);
60         p->client[i].fd = -1;
61         p->nready--;
62     }
63 }

  在调试这个基于poll()函数的Tiny时,遇到了一个很诡异的情况。运行服务器之后,直接返回了段错误:
  Segmentation fault (core dumped)
  在用GDB调试时发现,程序刚刚进入main()函数的入口就出错了。经过上网搜索,得知是由于我们这个程序的结构体太大了,超过了栈的大小。所以我们在main函数里的pool结构体实例前加上static关键字,把它放到全局区。这样再运行就没事了。新程序整体上和select方式的程序一样,只不过是一些子函数和结构体的替换。由此也体会到了模块化设计的重要性。

  (3) 多线程版Tiny

  在经过了IO多路复用版本Tiny的摧残之后,多线程版本就显得小菜一碟了。当监听套接字接受到来自浏览器的请求之后,创建一个线程对其进行业务处理,提供静态页面服务或动态CGI服务。为了便于线程结束之后自动清理,我们利用pthread_detach(pthread_self())将子线程和主线程分离开。多线程版Tiny服务器程序的主要框架如下,为了便于观察,这里省略掉了很多和源代码一样的部分。

 1 static void *doit(void *arg);
 2 
 3 int main(int argc, char *argv[])
 4 {
 5     int listenfd, *connfdp;      //声明一个指针,存放connfd
 6     pthread_t thread;
 7     //...其余变量声明
 8 
 9     listenfd = open_listenfd(argv[1]);
10     while (1) {
11         clientlen = sizeof(clientaddr);
12         connfdp = malloc(sizeof(int));          //动态分配一块内存给connfd,使得每个线程都有各自的已连接描述符副本
13         *connfdp = accept(listenfd, (SA*)&clientaddr, &clientlen);
14         if (pthread_create(&thread, NULL, &doit, connfdp) != 0)   //创建线程
15         {
16             fprintf(stderr, "pthread_create error\n");
17             exit(1);
18         }
19     }
20 }
21 
22 static void *doit(void *arg)
23 {
24     int fd = *((int *)arg);         //获取参数
25     free(arg);                        //释放内存空间
26         pthread_detach(pthread_self());        //线程分离,线程执行结束后自动回收资源
27 
28     //...函数主体,参考CSAPP源代码
29     pthread_exit(0);         //线程退出
30 }
31     

  注意,第5行我们不是声明了一个int型变量用来存放已连接描述符,而是声明了一个指向int型变量的指针。在12-13行,对其动态分配内存空间,这样,对于每个创建的线程,都有各自的已连接描述符的副本。这里如果不对其进行动态分配内存,由于其指向不明,冒然写入会引起"Segmentation fault"错误。详情请参考《UNIX网络编程》第3版540页。

  至此,并发服务器的三种模型我们都简单的实现了一遍。这里再总结一下:

  (1) 多进程和多线程版本的Tiny服务器实现起来都比较简单,只需要将主体业务处理程序放到子进程或线程里,其他地方稍加修改就行;

  (2) IO多路复用模型与其他两种模型比较起来比较复杂,但通过我们构造一个基于连接池的框架,也可以像多进程和多线程一样比较容易的实现。IO复用要注意的是,在处理完一个文件描述符之后,及时将其清理掉。IO多路复用还可以使用epoll()函数,整体上和select()以及poll()函数没有什么区别,这里就不给出实现了。感兴趣的朋友可以自己实现一下基于epoll()函数的连接池模型。

 

posted @ 2017-07-12 10:45  liude2134  阅读(536)  评论(0编辑  收藏  举报