代码开源(6)——UNIX并发编程

来源:互联网 发布:翻墙加速软件 编辑:程序博客网 时间:2024/05/17 13:45

  之前在代码开源(3)——UNIX中CS简单实现 给出的代码,存在一个问题,那就是只支持单个连接。本文整理给出了三种方法:多进程、IO多路复用、多线程,主要参考《深入理解计算机系统》一书。对源码做了修改整理,加了些批注。下面一一给出,只修改服务器端的主程序,客户端代码不变。其中用到的rio在代码开源(2)——UNIX 健壮I/O函数 已给出。

       首先给出原先的版本,即不支持多个连接。为了突出重点,做了些简化,比如去掉了一些异常的判断(accept调用失败,select调用失败等),实际编写中应该加上这些判断。

view plainprint?
  1. #include "server.h"  
  2.   
  3. int main(int argc, char **argv)  
  4. {  
  5.     int listenfd, connfd;  
  6.     unsigned int clientlen;         //地址长度  
  7.     struct sockaddr_in clientaddr;  //客户端地址  
  8.   
  9.     if(argc != 2)                   //参数必须是2个  
  10.     {  
  11.         fprintf(stderr, "usage: %s <port>\n",argv[0]);  
  12.         return 0;  
  13.     }  
  14.   
  15.     listenfd = open_listenfd(atoi(argv[1])); //进入监听状态  
  16.     clientlen = sizeof(clientaddr);  
  17.     while(1) //只支持单个连接  
  18.     {  
  19.         connfd = accept(listenfd, (struct sockaddr *)&clientaddr, &clientlen); //建立连接  
  20.         exchange_data(connfd);   //与客户端交换数据  
  21.         close(connfd);           //客户端断开连接  
  22.     }  
  23.     return 0;  
  24. }  

       下面给出多进程版本,主要利用了fork调用。注意一点:fork调用后,父进程需关闭连接描述符。这是因为fork调用后,父进程和子进程的连接描述符指向同一个文件表表项,当子进程终止时,由于父进程仍拥有该连接描述符,所以不会被释放,从而导致内存泄露,所以父进程应关闭连接描述符。另外,子进程最好关闭监听描述符。

       多进程版本的主要缺点就是进程控制和IPC(进程间通信)开销很高,速度比较慢。

view plainprint?
  1. //支持多个连接,多进程版  
  2. #include "server.h"      
  3. #include <signal.h>    //signal函数需包含头文件  
  4. #include <sys/wait.h>  //waitpid函数需要包含头文件  
  5.   
  6. //子进程暂停或终止时,会产生信号SIGCHLD  
  7. //从而调用下面这个函数,waitpid(-1, 0, WNOHANG)返回终止的子进程号  
  8. void sigchld_handler(int sig)  
  9. {  
  10.     while(waitpid(-1, 0, WNOHANG) > 0) //-1表示等待集合为父进程所有子进程  
  11.         ;  
  12.     return ;  
  13. }  
  14.   
  15. int main(int argc, char **argv)  
  16. {  
  17.     int listenfd, connfd;  
  18.     unsigned int clientlen;         //地址长度  
  19.     struct sockaddr_in clientaddr;  //客户端地址  
  20.   
  21.     if(argc != 2)                   //参数必须是2个  
  22.     {  
  23.         fprintf(stderr, "usage: %s <port>\n",argv[0]);  
  24.         return 0;  
  25.     }  
  26.   
  27.     signal(SIGCHLD, sigchld_handler);  //注册信号处理程序  
  28.   
  29.     listenfd = open_listenfd(atoi(argv[1])); //进入监听状态  
  30.     clientlen = sizeof(clientaddr);  
  31.     while(1) //支持多个连接  
  32.     {  
  33.         connfd = accept(listenfd, (struct sockaddr *)&clientaddr, &clientlen); //建立连接  
  34.         if(fork() == 0)              //建立子进程  
  35.         {  
  36.             close(listenfd);         //关闭监听描述符  
  37.             exchange_data(connfd);   //与客户端交换数据  
  38.             close(connfd);           //客户端断开连接  
  39.             exit(0);                 //子进程终止  
  40.         }  
  41.         close(connfd); //关闭连接描述符,子进程中有  
  42.     }  
  43.     return 0;  
  44. }  
        下面给出多路IO复用版本。其中主要用到了select调用,另外定义了一个称为pool的数据结构,用来保存监听描述符和连接描述符,具体实现后面给出,完整摘自《深入理解计算机系统》一书,只修改了pool结构中数组的容量。

       相比多进程版本,IO复用的版本比较复杂,实现起来不易。

view plainprint?
  1. #include "server.h"  
  2. #include "pool.h"  
  3.   
  4. int main(int argc, char **argv)  
  5. {  
  6.     int listenfd, connfd;  
  7.     unsigned int clientlen;         //地址长度  
  8.     struct sockaddr_in clientaddr;  //客户端地址  
  9.     pool client_pool;  
  10.   
  11.     if(argc != 2)                   //参数必须是2个  
  12.     {  
  13.         fprintf(stderr, "usage: %s <port>\n",argv[0]);  
  14.         return 0;  
  15.     }  
  16.   
  17.     listenfd = open_listenfd(atoi(argv[1])); //进入监听状态  
  18.     clientlen = sizeof(clientaddr);  
  19.   
  20.     init_pool(listenfd, &client_pool);  //初始化描述符池  
  21.     while(1) //支持多个连接  
  22.     {  
  23.         client_pool.ready_set = client_pool.read_set;  
  24.         client_pool.nready = select(client_pool.maxfd + 1, &client_pool.ready_set, NULL, NULL, NULL); //等待描述符就绪  
  25.   
  26.         if(FD_ISSET(listenfd, &client_pool.ready_set)) //监听描述符就绪,增加一个连接描述符  
  27.         {  
  28.             connfd = accept(listenfd, (struct sockaddr *)&clientaddr, &clientlen); //建立连接  
  29.             add_client(connfd, &client_pool);                                      //增加一个连接  
  30.         }  
  31.         check_clients(&client_pool); //检查所有的连接描述符,判断是否有数据从客户端发来  
  32.     }  
  33.     return 0;  
  34. }  
view plainprint?
  1. #ifndef POOL_H_  
  2. #define POOL_H_  
  3.   
  4. #include "rio.h"  
  5.   
  6. #define MAX_LINE 1024  
  7. #define SET_SIZE 8  
  8. typedef struct  
  9. {  
  10.     int maxfd;         //读集合的最大描述符  
  11.     fd_set read_set;   //读集合  
  12.     fd_set ready_set;  //读就绪集合  
  13.     int nready;        //准备好的集合数  
  14.     int maxi;          //当前使用的最高位置,即最大下标  
  15.     int clientfd[SET_SIZE];  
  16.     rio_t clientrio[SET_SIZE];  
  17. }pool;  
  18.   
  19. void init_pool(int listenfd, pool *p);  //初始化活动客户池  
  20. void add_client(int connfd, pool *p);   //增加一个客户连接  
  21. void check_clients(pool *p);            //为准备好的客户端连接服务  
  22.   
  23. //初始化活动客户池  
  24. void init_pool(int listenfd, pool *p)  
  25. {  
  26.     int i;  
  27.     for(i = 0; i < SET_SIZE; i++) //初始化为-1表示未用  
  28.         p->clientfd[i] = -1;  
  29.   
  30.     FD_ZERO(&p->read_set);  
  31.     FD_SET(listenfd, &p->read_set);  
  32.     p->maxfd = listenfd;            //只有监听描述符  
  33.     p->maxi = -1;                   //客户端连接数组未用  
  34. }  
  35. //增加一个客户连接  
  36. void add_client(int connfd, pool *p)  
  37. {  
  38.     int i;  
  39.     p->nready--;                  //准备就绪的连接符数减1,因为已处理完客户端的连接  
  40.     for(i = 0; i < SET_SIZE; i++) //在客户端连接数组中,寻找可用的空位  
  41.     {  
  42.         if(p->clientfd[i] < 0)  //找到一个空位  
  43.         {  
  44.             //设置相关信息  
  45.             p->clientfd[i] = connfd;  
  46.             rio_readinitb(&p->clientrio[i], connfd);  
  47.   
  48.             FD_SET(connfd, &p->read_set);  
  49.             if(connfd > p->maxfd) //更新读集合的最大描述符  
  50.                 p->maxfd = connfd;  
  51.             if(i > p->maxi)       //更新当前使用的最高位置  
  52.                 p->maxi = i;  
  53.             break;  
  54.         }  
  55.     }  
  56. }  
  57. //为准备好的客户端连接服务  
  58. void check_clients(pool *p)  
  59. {  
  60.     int i, connfd, n;  
  61.     char buf[MAX_LINE];  
  62.     rio_t rio;  
  63.     for(i = 0; (i <= p->maxi) && (p->nready > 0); i++) //遍历,寻找准备好的客户端连接  
  64.     {  
  65.         connfd = p->clientfd[i];  
  66.         rio = p->clientrio[i];  
  67.         if((connfd > 0) && (FD_ISSET(connfd, &p->ready_set))) //已建立连接,并且读就绪  
  68.         {  
  69.             p->nready--;  
  70.             if((n = rio_readlineb(&rio, buf, MAX_LINE)) != 0)  
  71.             {  
  72.                 printf("server received %d bytes\n", n); //收到的字节数  
  73.                 rio_writen(connfd, buf, n);  
  74.             }  
  75.             else  
  76.             {  
  77.                 close(connfd);  
  78.                 FD_CLR(connfd, &p->read_set);  
  79.                 p->clientfd[i] = -1;  
  80.             }  
  81.         }  
  82.     }  
  83. }  
  84. #endif /* POOL_H_ */  
      最后给出多线程版本。用到了几个线程函数,在gcc中编译需加-lpthread。本人实现时用的是Eclipse C++,需在Project->Properties->C/C++ Build->Setting->GCC Compile->Command中加 -lpthread,在Project->Properties->C/C++ Build->Setting->GCC linker->Command中加 -lpthread。

      这里需要注意的一点,每次连接,都需要动态申请空间,用来放连接描述符。这是因为线程例程的参数只能是一个指针,如果连接描述符的空间不是动态分配的,那么一种可能的情况是对等线程执行int connfd = *((int *)argv)前,主线程又收到了连接请求,这时对等线程中的连接描述符就被设置成新连接描述符。这显然是不对的。

view plainprint?
  1. #include "server.h"  
  2. #include <pthread.h>  
  3.   
  4. voidthread(void *argv)  
  5. {  
  6.     int connfd = *((int *)argv);    //获得已连接描述符  
  7.     pthread_detach(pthread_self()); //线程分离,它的资源在终止时由系统自动释放  
  8.     free(argv);  
  9.     exchange_data(connfd);  
  10.     close(connfd);  
  11.     return NULL;  
  12. }  
  13. int main(int argc, char **argv)  
  14. {  
  15.     int listenfd, *connfdp;  
  16.     unsigned int clientlen;         //地址长度  
  17.     struct sockaddr_in clientaddr;  //客户端地址  
  18.     pthread_t tid;  
  19.   
  20.     if(argc != 2)                   //参数必须是2个  
  21.     {  
  22.         fprintf(stderr, "usage: %s <port>\n",argv[0]);  
  23.         return 0;  
  24.     }  
  25.   
  26.     listenfd = open_listenfd(atoi(argv[1])); //进入监听状态  
  27.     clientlen = sizeof(clientaddr);  
  28.   
  29.     while(1) //支持多个连接  
  30.     {  
  31.         connfdp = malloc(sizeof(int)); //必须是动态生成,否则会有问题  
  32.         *connfdp = accept(listenfd, (struct sockaddr *)&clientaddr, &clientlen); //建立连接  
  33.         pthread_create(&tid, NULL, thread, connfdp); //创建线程  
  34.     }  
  35.     return 0;  
  36. }