首页 > 代码库 > UNIX网络编程卷1 回射服务器程序 TCP服务器程序设计范式 四个版本
UNIX网络编程卷1 回射服务器程序 TCP服务器程序设计范式 四个版本
本文为senlie原创,转载请保留此地址:http://blog.csdn.net/zhengsenlie
这是一个简单的回射服务器程序。它将客户发送的数据读入缓冲区并回射其中内容
下面我会介绍同一个使用 TCP 协议的回射服务器程序的几个不同版本,分别是 fork 版本、select 版本、poll 版本、多线程版本
fork 版本:为每一个客户连接派生(fork) 一个子进程用来处理客户请求
/** * TCP/IPv4 协议相关 * **/ #include "unp.h" int main(int argc, char **argv) { int listenfd, connfd; pid_t childpid; socklen_t clilen; struct sockaddr_in cliaddr, 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" void str_echo(int sockfd) { ssize_t n; char buf[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" int main(int argc, char **argv) { int listenfd, connfd; pid_t childpid; socklen_t clilen; struct sockaddr_in cliaddr, servaddr; void sig_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() */ else err_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" void sig_chld(int signo) { pid_t pid; int stat; while ( (pid = waitpid(-1, &stat, WNOHANG)) > 0) printf("child %d terminated\n", pid); return; }
select 版本:使用 select 来处理任意个客户的单进程程序,而不是为每个客户派生一个子进程
/** * TCP 使用 select **/ #include "unp.h" int main(int argc, char **argv) { int i, maxi, maxfd, listenfd, connfd, sockfd; int nready, client[FD_SETSIZE]; ssize_t n; fd_set rset, allset; char buf[MAXLINE]; socklen_t clilen; struct sockaddr_in cliaddr, 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); #ifdef NOTDEF printf("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; } else Writen(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 */ int main(int argc, char **argv) { int i, maxi, listenfd, connfd, sockfd; int nready; ssize_t n; char buf[MAXLINE]; socklen_t clilen; //1.分配 pollfd 结构数组 struct pollfd client[OPEN_MAX]; struct sockaddr_in cliaddr, 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 //把其余各项的成员转为 -1 client[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); #ifdef NOTDEF printf("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 */ #ifdef NOTDEF printf("client[%d] aborted connection\n", i); #endif Close(sockfd); client[i].fd = -1; } else err_sys("read error"); //终止连接 } else if (n == 0) { /*4connection closed by client */ #ifdef NOTDEF printf("client[%d] closed connection\n", i); #endif Close(sockfd); client[i].fd = -1; } else Writen(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 */ int main(int argc, char **argv) { int listenfd, connfd; pthread_t tid; socklen_t addrlen, 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); else err_quit("usage: tcpserv01 [ <host> ] <service or port>"); cliaddr = Malloc(addrlen); //2.创建线程 for ( ; ; ) { len = addrlen; connfd = Accept(listenfd, cliaddr, &len); //accept 返回之后,改为调用 pthread_create 取代调用 fork Pthread_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 */ int main(int argc, char **argv) { int listenfd, *iptr; thread_t tid; socklen_t addrlen, 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); else err_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) { int connfd; connfd = *((int *) arg); free(arg); Pthread_detach(pthread_self()); str_echo(connfd); /* same function as before */ Close(connfd); /* done with connected socket */ return(NULL); }
UNIX网络编程卷1 回射服务器程序 TCP服务器程序设计范式 四个版本
声明:以上内容来自用户投稿及互联网公开渠道收集整理发布,本网站不拥有所有权,未作人工编辑处理,也不承担相关法律责任,若内容有误或涉及侵权可进行投诉: 投诉/举报 工作人员会在5个工作日内联系你,一经查实,本站将立刻删除涉嫌侵权内容。