UNIX网络编程卷1 回射服务器程序 TCP服务器程序设计范式 四个版本

来源:互联网 发布:linux下源码安装mysql 编辑:程序博客网 时间:2024/05/17 02:37

本文为senlie原创,转载请保留此地址:http://blog.csdn.net/zhengsenlie


这是一个简单的回射服务器程序。它将客户发送的数据读入缓冲区并回射其中内容



下面我会介绍同一个使用 TCP 协议的回射服务器程序的几个不同版本,分别是 fork 版本、select 版本、poll 版本、多线程版本


fork 版本:为每一个客户连接派生(fork) 一个子进程用来处理客户请求
/** * TCP/IPv4 协议相关 * **/#include"unp.h"intmain(int argc, char **argv){intlistenfd, connfd;pid_tchildpid;socklen_tclilen;struct sockaddr_incliaddr, servaddr;    //1.创建套接字,捆绑服务器的众所周知端口listenfd = Socket(AF_INET, SOCK_STREAM, 0);bzero(&servaddr, sizeof(servaddr));servaddr.sin_family      = AF_INET;servaddr.sin_addr.s_addr = htonl(INADDR_ANY);servaddr.sin_port        = htons(SERV_PORT);Bind(listenfd, (SA *) &servaddr, sizeof(servaddr));Listen(listenfd, LISTENQ);    //2.等待完成客户连接for ( ; ; ) {clilen = sizeof(cliaddr);connfd = Accept(listenfd, (SA *) &cliaddr, &clilen);        //3.并发服务器if ( (childpid = Fork()) == 0) {/* 子进程 */Close(listenfd);/* 关闭监听套接字*/str_echo(connfd);/* 处理客户请求*/exit(0);}Close(connfd);/* 父进程关闭连接套接字 */}}#include"unp.h"voidstr_echo(int sockfd){ssize_tn;charbuf[MAXLINE];again:// 读入缓冲区并回射其中内容while ( (n = read(sockfd, buf, MAXLINE)) > 0)Writen(sockfd, buf, n);if (n < 0 && errno == EINTR)goto again;else if (n < 0)err_sys("str_echo: read error");} 


问题:僵死子进程
改善:通过捕获 SIGCHLD 信号并在信号的调用  waitpid 函数来回收子进程的资源。
之所以用 waitpid 而不用 wait 是因为 Unix 信号是不排队的,而 waitpid 因为可以设置不阻塞,所以可以在循环中
被调用,这样可以处理同时到达的多个 SIGCHLD 信号。


/*** TCP/IPv4 协议相关 收拾终止了的子进程**/#include"unp.h"intmain(int argc, char **argv){intlistenfd, connfd;pid_tchildpid;socklen_tclilen;struct sockaddr_incliaddr, servaddr;voidsig_chld(int);//1.创建套接字,绑定众所周知的端口listenfd = Socket(AF_INET, SOCK_STREAM, 0);bzero(&servaddr, sizeof(servaddr));servaddr.sin_family      = AF_INET;servaddr.sin_addr.s_addr = htonl(INADDR_ANY);servaddr.sin_port        = htons(SERV_PORT);Bind(listenfd, (SA *) &servaddr, sizeof(servaddr));Listen(listenfd, LISTENQ);//2.当fork 子进程时,必须捕获 SIGCHLD 信号, sig_chld 为信号处理函数Signal(SIGCHLD, sig_chld);/* must call waitpid() *///3.等待客户连接for ( ; ; ) {clilen = sizeof(cliaddr);//4.当捕获信号时,必须处理被中断的系统调用if ( (connfd = accept(listenfd, (SA *) &cliaddr, &clilen)) < 0) {if (errno == EINTR)continue;/* back to for() */elseerr_sys("accept error");}//5.并发服务器if ( (childpid = Fork()) == 0) {/* child process */Close(listenfd);/* close listening socket */str_echo(connfd);/* process the request */exit(0);}Close(connfd);/* parent closes connected socket */}}#include"unp.h"voidsig_chld(int signo){pid_tpid;intstat;while ( (pid = waitpid(-1, &stat, WNOHANG)) > 0)printf("child %d terminated\n", pid);return;}



select 版本:使用 select 来处理任意个客户的单进程程序,而不是为每个客户派生一个子进程

/*** TCP 使用 select **/#include"unp.h"intmain(int argc, char **argv){inti, maxi, maxfd, listenfd, connfd, sockfd;intnready, client[FD_SETSIZE];ssize_tn;fd_setrset, allset;charbuf[MAXLINE];socklen_tclilen;struct sockaddr_incliaddr, servaddr;//1.创建套接字,绑定众所周知的端口listenfd = Socket(AF_INET, SOCK_STREAM, 0);bzero(&servaddr, sizeof(servaddr));servaddr.sin_family      = AF_INET;servaddr.sin_addr.s_addr = htonl(INADDR_ANY);servaddr.sin_port        = htons(SERV_PORT);Bind(listenfd, (SA *) &servaddr, sizeof(servaddr));Listen(listenfd, LISTENQ);//2.初始化maxfd = listenfd;/* initialize */maxi = -1;/* index into client[] array */for (i = 0; i < FD_SETSIZE; i++)client[i] = -1;/* -1 indicates available entry */FD_ZERO(&allset);FD_SET(listenfd, &allset);/* end fig01 *//* include fig02 */for ( ; ; ) {//3.阻塞于 select//select 等待某个事件发生:或是新客户连接的建立,或是数据、FIN或 RST的到达rset = allset;/* structure assignment */nready = Select(maxfd+1, &rset, NULL, NULL, NULL);//4.accept 新的连接if (FD_ISSET(listenfd, &rset)) {/* new client connection */clilen = sizeof(cliaddr);connfd = Accept(listenfd, (SA *) &cliaddr, &clilen);#ifdefNOTDEFprintf("new client: %s, port %d\n",Inet_ntop(AF_INET, &cliaddr.sin_addr, 4, NULL),ntohs(cliaddr.sin_port));#endif//用 client 数组中第一个未用项记录这个已连接描述符for (i = 0; i < FD_SETSIZE; i++)if (client[i] < 0) {client[i] = connfd;/* save descriptor */break;}if (i == FD_SETSIZE)err_quit("too many clients");//在 allset 描述符中打开已连接套接字 connfd 的对应位FD_SET(connfd, &allset);/* add new descriptor to set */if (connfd > maxfd)maxfd = connfd;/* for select */if (i > maxi)maxi = i;/* max index in client[] array *///nready 是 select 函数的返回值,表示跨所有描述符集的已就绪的总位数//就绪描述符数目减 1,若其值变为 0,就可以避免进入下一个 for 循环if (--nready <= 0)continue;/* no more readable descriptors */}//5.检查现有连接//对于每个现在的客户连接,测试其描述符是否在 select 返回的描述符集中 --> ? select 有返回描述符集吗 ?//如果是,就从客户读入一行文本并回射给它。//如果关闭了连接,那么 read 将返回 0,要相应地更新数据结构for (i = 0; i <= maxi; i++) {/* check all clients for data */if ( (sockfd = client[i]) < 0)continue;if (FD_ISSET(sockfd, &rset)) {if ( (n = Read(sockfd, buf, MAXLINE)) == 0) {/*4connection closed by client */Close(sockfd);FD_CLR(sockfd, &allset);client[i] = -1;} elseWriten(sockfd, buf, n);if (--nready <= 0)break;/* no more readable descriptors */}}}}/* end fig02 */


poll 版本:在 select 版本中,必须分配一个 client 数组以及一个名为 rset 的描述符集。改用 poll 后,我们只需分配 一个 pollfd 结构的数组来
维护客户信息,而不必分配另一个数组。


/** * TCP/IPv4 协议相关,使用poll,单个进程处理所有客户 * **//* include fig01 */#include"unp.h"#include<limits.h>/* for OPEN_MAX */intmain(int argc, char **argv){inti, maxi, listenfd, connfd, sockfd;intnready;ssize_tn;charbuf[MAXLINE];socklen_tclilen;    //1.分配 pollfd 结构数组struct pollfdclient[OPEN_MAX];struct sockaddr_incliaddr, servaddr;listenfd = Socket(AF_INET, SOCK_STREAM, 0);bzero(&servaddr, sizeof(servaddr));servaddr.sin_family      = AF_INET;servaddr.sin_addr.s_addr = htonl(INADDR_ANY);servaddr.sin_port        = htons(SERV_PORT);Bind(listenfd, (SA *) &servaddr, sizeof(servaddr));Listen(listenfd, LISTENQ);    //2.初始化    //把 client 数组的第一项用于监听套接字,并设置 events为 POLLRDNORM    //把其余各项的成员转为 -1client[0].fd = listenfd;client[0].events = POLLRDNORM;for (i = 1; i < OPEN_MAX; i++)client[i].fd = -1;/* -1 indicates available entry */maxi = 0;/* max index into client[] array *//* end fig01 *//* include fig02 */for ( ; ; ) {        //3.调用 poll,检查新的连接nready = Poll(client, maxi+1, INFTIM);        //检查 client[0],即监听套接字上是否有新客户连接if (client[0].revents & POLLRDNORM) {/* new client connection */clilen = sizeof(cliaddr);connfd = Accept(listenfd, (SA *) &cliaddr, &clilen);#ifdefNOTDEFprintf("new client: %s\n", Sock_ntop((SA *) &cliaddr, clilen));#endif            //保存已连接套接字for (i = 1; i < OPEN_MAX; i++)if (client[i].fd < 0) {client[i].fd = connfd;/* save descriptor */break;}if (i == OPEN_MAX)err_quit("too many clients");client[i].events = POLLRDNORM;if (i > maxi)maxi = i;/* max index in client[] array */if (--nready <= 0)continue;/* no more readable descriptors */}        //4.检查某个现有连接上的数据        // i 从 1,开始,因为 client[0]是监听套接字for (i = 1; i <= maxi; i++) {/* check all clients for data */if ( (sockfd = client[i].fd) < 0)continue;if (client[i].revents & (POLLRDNORM | POLLERR)) {                //读取数据并回射if ( (n = read(sockfd, buf, MAXLINE)) < 0) {if (errno == ECONNRESET) {/*4connection reset by client */#ifdefNOTDEFprintf("client[%d] aborted connection\n", i);#endifClose(sockfd);client[i].fd = -1;} elseerr_sys("read error");                //终止连接} else if (n == 0) {/*4connection closed by client */#ifdefNOTDEFprintf("client[%d] closed connection\n", i);#endifClose(sockfd);client[i].fd = -1;} elseWriten(sockfd, buf, n);if (--nready <= 0)break;/* no more readable descriptors */}}}}/* end fig02 */


多线程版本:这个版本的代码为每个客户使用一个线程,而不是子进程,同时它使用了 tcp_listen 函数来使得该程序与协议无关。


/** * TCP 每个用户一个线程 * **/#include"unpthread.h"static void*doit(void *);/* each thread executes this function */intmain(int argc, char **argv){intlistenfd, connfd;pthread_ttid;socklen_taddrlen, len;struct sockaddr*cliaddr;    //1.创建监听套接字if (argc == 2)listenfd = Tcp_listen(NULL, argv[1], &addrlen);else if (argc == 3)listenfd = Tcp_listen(argv[1], argv[2], &addrlen);elseerr_quit("usage: tcpserv01 [ <host> ] <service or port>");cliaddr = Malloc(addrlen);    //2.创建线程for ( ; ; ) {len = addrlen;connfd = Accept(listenfd, cliaddr, &len);        //accept 返回之后,改为调用 pthread_create 取代调用 forkPthread_create(&tid, NULL, &doit, (void *) connfd);}}//3.线程函数static void *doit(void *arg){    //pthread_detach 使自身脱离主线程,这样主线程不用等待它Pthread_detach(pthread_self());str_echo((int) arg);/* same function as before */    //关闭已连接套接字Close((int) arg);/* done with connected socket */return(NULL);}


问题:上面的代码把整数变量 connfd 类型强制转换成 void 指针,这并不具备兼容性。
改善:可以把一个整数指针类型强制转换为 void *,然后把这个 void * 指针类型强制转换回原来的整数指针。
但如果主线程中只有一个整数变量 connfd 的话,每次调用 accept 该变量都会被覆写为另一个新值。
因此还应该专门为每一个线程 malloc 一个整数变量的空间,用来存放该线程的处理的已连接套接字描述符,
同时还要在线程中 free 掉该空间。


/** * TCP 每个客户一个线程,可移植的参数传递 * **/#include"unpthread.h"static void*doit(void *);/* each thread executes this function */intmain(int argc, char **argv){intlistenfd, *iptr;thread_ttid;socklen_taddrlen, len;struct sockaddr*cliaddr;    //1.创建监听套接字if (argc == 2)listenfd = Tcp_listen(NULL, argv[1], &addrlen);else if (argc == 3)listenfd = Tcp_listen(argv[1], argv[2], &addrlen);elseerr_quit("usage: tcpserv01 [ <host> ] <service or port>");cliaddr = Malloc(addrlen);    //2.创建线程for ( ; ; ) {len = addrlen;iptr = Malloc(sizeof(int));*iptr = Accept(listenfd, cliaddr, &len);        Pthread_create(&tid, NULL, &doit, iptr);}}//3.线程函数static void *doit(void *arg){intconnfd;connfd = *((int *) arg);free(arg);Pthread_detach(pthread_self());str_echo(connfd);/* same function as before */Close(connfd);/* done with connected socket */return(NULL);}


0 0