首页 > 代码库 > UNIX网络编程卷1 回射客户程序 TCP客户程序设计范式
UNIX网络编程卷1 回射客户程序 TCP客户程序设计范式
本文为senlie原创,转载请保留此地址:http://blog.csdn.net/zhengsenlie
下面我会介绍同一个使用 TCP 协议的客户端程序的几个不同版本,分别是停等版本、select 加阻塞式 I/O 版本、
非阻塞式 I/O 版本、fork 版本、线程化版本。它们都由同一个 main 函数调用来实现同一个功能,即回射程序客户端。它从标准输入读入一行文本,写到服务器上,读取服务器对该行的回射,并把回射行写到标准输出上。
其中,非阻塞式 I/O 版本是所有版本中执行速度最快的,但它的代码比较复杂。
创建一个新线程通常比使用 fork 派生一个新进程快得多,
下面代码中的线程化版本执行速度略快于 fork版本,稍慢于非阻塞式 I/O。一般编程推荐使用线程版本。
下面是《Unix 网络编程:卷1》中给出的各个版本的性能测试结果:
354.0秒,停等版本
12.3秒, select加阻塞式I/O版本
6.9秒, 非阻塞式I/O版本
8.7秒, fork 版本
8.5秒, 线程化版本
下面是 main 函数的代码
#include "unp.h" int main(int argc, char **argv) { int sockfd; struct sockaddr_in servaddr; if (argc != 2) err_quit("usage: tcpcli <IPaddress>"); //1.创建 TCP 套接字 sockfd = Socket(AF_INET, SOCK_STREAM, 0); //2.指定服务器的 IP 地址和端口 bzero(&servaddr, sizeof(servaddr)); servaddr.sin_family = AF_INET; servaddr.sin_port = htons(SERV_PORT); Inet_pton(AF_INET, argv[1], &servaddr.sin_addr); //3.建立与服务器的连接 Connect(sockfd, (SA *) &servaddr, sizeof(servaddr)); //4.str_cli 函数完成剩余部分的客户处理工作 str_cli(stdin, sockfd); exit(0); }
最初代码(停等版本):
#include "unp.h" void str_cli(FILE *fp, int sockfd) { char sendline[MAXLINE], recvline[MAXLINE]; //1.从 fp 读入一行,存放到 sendline while (Fgets(sendline, MAXLINE, fp) != NULL) { //2.将 sendline 的内容写到 sockfd 连接的服务器 Writen(sockfd, sendline, strlen(sendline)); //3.从服务器读入回射行,存放到 recvline if (Readline(sockfd, recvline, MAXLINE) == 0) err_quit("str_cli: server terminated prematurely"); //4.将 recvline 的内容写到标准输出 Fputs(recvline, stdout); } }
问题1:当套接字上发生某些事件时,客户可能阻塞于 fgets 调用
改善1:使用 select 重写 str_cli 函数,阻塞于 select 调用,或是等待标准输入可读,
或是等待套接字可读。这样服务器一终止,客户就能马上得到通知。
/** * TCP 使用 select **/ #include "unp.h" void str_cli(FILE *fp, int sockfd) { int maxfdp1; // maxfdpl 参数指定待测定的描述符个数 fd_set rset; //可读描述符集合 char sendline[MAXLINE], recvline[MAXLINE]; //1.调用 select FD_ZERO(&rset); //初始化 rset for ( ; ; ) { FD_SET(fileno(fp), &rset); // 打开标准I/O文件指针 fp 对应的位 FD_SET(sockfd, &rset); // 打开套接字 sockfd 对应的位 maxfdp1 = max(fileno(fp), sockfd) + 1; //调用 select 阻塞到某个描述符就绪为止 Select(maxfdp1, &rset, NULL, NULL, NULL); //2.处理可读套接字 if (FD_ISSET(sockfd, &rset)) { //使用 readline 读入回射文本,再用 fputs 输出到 stdout if (Readline(sockfd, recvline, MAXLINE) == 0) err_quit("str_cli: server terminated prematurely"); Fputs(recvline, stdout); } //3.处理可读输入 if (FD_ISSET(fileno(fp), &rset)) { //使用 fgets 读入一行文本,再用 writen 把它写到套接字中 if (Fgets(sendline, MAXLINE, fp) == NULL) return; /* all done */ Writen(sockfd, sendline, strlen(sendline)); } } }
改善1的问题:批量方式运行用 select 编写的回射客户程序,发现即使已经遇到了用户输入结尾,
仍可能有数据处于运往或来自服务器的管道中。
改善2(select加阻塞式I/O版本):使用 shutdown 函数利用上 TCP 的半关闭特性
下面代码使用了 select 和 shutdown,前者只要服务器关闭它那一端的连接就会通知客户,
后者允许客户正确地处理批量输入。这个版本还废弃了以文本行为为中心的代码,发布针对缓冲区操作。
/** * TCP 使用 select 并操纵缓冲区 **/ #include "unp.h" void str_cli(FILE *fp, int sockfd) { //stdineof 表示标准输入是否结束 int maxfdp1, stdineof; fd_set rset; char buf[MAXLINE]; int n; //1.调用 select stdineof = 0; FD_ZERO(&rset); for ( ; ; ) { if (stdineof == 0) FD_SET(fileno(fp), &rset); FD_SET(sockfd, &rset); maxfdp1 = max(fileno(fp), sockfd) + 1; Select(maxfdp1, &rset, NULL, NULL, NULL); //2.处理可读套接字 if (FD_ISSET(sockfd, &rset)) { /* socket is readable */ if ( (n = Read(sockfd, buf, MAXLINE)) == 0) { if (stdineof == 1) //如果在套接字上读到 EOF,并且已在标准输入上遇到 EOF,那就是正常的终止 return; else //否则,服务器进程过早终止了 err_quit("str_cli: server terminated prematurely"); } Write(fileno(stdout), buf, n); } //3.处理可读输入 if (FD_ISSET(fileno(fp), &rset)) { //如果在标准输入上碰到 EOF 时,把 stdineof 置为 1 //并调用 shutdown 函数向服务器发送 FIN,关闭客户端向服务器的写操作 if ( (n = Read(fileno(fp), buf, MAXLINE)) == 0) { stdineof = 1; Shutdown(sockfd, SHUT_WR); FD_CLR(fileno(fp), &rset); continue; } Writen(sockfd, buf, n); } } }
改善2问题:阻塞式I/O
例如,如果在标准输入有一行文本可读,我们就调用 read 读入它,再调用 writen 把它发送给服务器。
然而如果套接字发送缓冲区已满, writen 调用将会阻塞。在进程阻塞于 writen 调用期间,可能有来自套接字接收缓冲区的数据可供读取。
类似的,如果从套接字有一行输入文本可读,那么一旦标准输出比网络还要慢,进程照样可能阻塞于后续的 write 调用。
改善3(非阻塞式I/O版本):使用非阻塞式 I/O
/* include nonb1 */ #include "unp.h" void str_cli(FILE *fp, int sockfd) { int maxfdp1, val, stdineof; ssize_t n, nwritten; fd_set rset, wset; char to[MAXLINE], fr[MAXLINE]; //to 容纳从标准输入到服务器去的数据 //from 容纳自服务器到标准输出来的数据 char *toiptr, *tooptr, *friptr, *froptr; //1.把描述符设置为非阻塞 val = Fcntl(sockfd, F_GETFL, 0); Fcntl(sockfd, F_SETFL, val | O_NONBLOCK); //套接字描述符 val = Fcntl(STDIN_FILENO, F_GETFL, 0); Fcntl(STDIN_FILENO, F_SETFL, val | O_NONBLOCK); //标准输入 val = Fcntl(STDOUT_FILENO, F_GETFL, 0); Fcntl(STDOUT_FILENO, F_SETFL, val | O_NONBLOCK); //标准输出 //2.初始化缓冲区指针 toiptr = tooptr = to; friptr = froptr = fr; stdineof = 0; maxfdp1 = max(max(STDIN_FILENO, STDOUT_FILENO), sockfd) + 1; //3.主循环:调用 select //一个 select 调用后对所关注各个条件进行单独测试 for ( ; ; ) { //指定所关注的描述符 FD_ZERO(&rset); FD_ZERO(&wset); if (stdineof == 0 && toiptr < &to[MAXLINE]) FD_SET(STDIN_FILENO, &rset); /* 打开读描述符集中对应标准输入的位 */ if (friptr < &fr[MAXLINE]) FD_SET(sockfd, &rset); /* 打开读描述符集中对应套接字的位 */ if (tooptr != toiptr) FD_SET(sockfd, &wset); /* 打开写描述符集中对应套接字的位 */ if (froptr != friptr) FD_SET(STDOUT_FILENO, &wset); /* 打开写描述符集中对应标准输出的位 */ //调用 select Select(maxfdp1, &rset, &wset, NULL, NULL); /* end nonb1 */ /* include nonb2 */ //4.对所关注各个条件进行单独测试 //从标准输入 read if (FD_ISSET(STDIN_FILENO, &rset)) { //read 返回错误。 EWOULDBLOCK 是正常的错误,表明这个描述符还处在阻塞中,应该忽略该错误 if ( (n = read(STDIN_FILENO, toiptr, &to[MAXLINE] - toiptr)) < 0) { if (errno != EWOULDBLOCK) err_sys("read error on stdin"); } //read 返回 EOF,标准输入处理结束 else if (n == 0) { #ifdef VOL2 fprintf(stderr, "%s: EOF on stdin\n", gf_time()); #endif stdineof = 1; /* 标志标准输入结束 */ if (tooptr == toiptr) // to 缓冲区中已经没有数据要发送 Shutdown(sockfd, SHUT_WR);/* 关闭客户端向服务器的写操作 */ } //read 返回数据 else { #ifdef VOL2 fprintf(stderr, "%s: read %d bytes from stdin\n", gf_time(), n); #endif toiptr += n; //增加 toiptr FD_SET(sockfd, &wset); //打开写描述符集中对应套接字的位 } } //从套接字 read if (FD_ISSET(sockfd, &rset)) { //read 返回错误。 EWOULDBLOCK 是正常的错误,表明这个描述符还处在阻塞中,应该忽略该错误 if ( (n = read(sockfd, friptr, &fr[MAXLINE] - friptr)) < 0) { if (errno != EWOULDBLOCK) err_sys("read error on socket"); } //read 返回 EOF,服务器返回数据结束 else if (n == 0) { #ifdef VOL2 fprintf(stderr, "%s: EOF on socket\n", gf_time()); #endif if (stdineof) //如果遇到来自服务器的 EOF ,并且已经在标准输入上遇到 EOF ,则正常结束;否则就是服务器过早终止了 return; /* normal termination */ else err_quit("str_cli: server terminated prematurely"); } //read 返回数据 else { #ifdef VOL2 fprintf(stderr, "%s: read %d bytes from socket\n", gf_time(), n); #endif friptr += n; //增加 friptr FD_SET(STDOUT_FILENO, &wset); // 打开写描述符集中对应标准输出的位 } } /* end nonb2 */ /* include nonb3 */ //write 到标准输出 if (FD_ISSET(STDOUT_FILENO, &wset) && ( (n = friptr - froptr) > 0)) { //write 返回错误。 if ( (nwritten = write(STDOUT_FILENO, froptr, n)) < 0) { if (errno != EWOULDBLOCK) err_sys("write error to stdout"); } //write 成功 else { #ifdef VOL2 fprintf(stderr, "%s: wrote %d bytes to stdout\n", gf_time(), nwritten); #endif froptr += nwritten; //增加写出字节数 if (froptr == friptr) froptr = friptr = fr; //如果输出指针追上输入指针, 这两个指针不同时恢复为指向缓冲区开始处 } } //write 到套接字 if (FD_ISSET(sockfd, &wset) && ( (n = toiptr - tooptr) > 0)) { //write 失败 if ( (nwritten = write(sockfd, tooptr, n)) < 0) { if (errno != EWOULDBLOCK) err_sys("write error to socket"); } //write 成功 else { #ifdef VOL2 fprintf(stderr, "%s: wrote %d bytes to socket\n", gf_time(), nwritten); #endif tooptr += nwritten; if (tooptr == toiptr) { // toiptr = tooptr = to; if (stdineof) //如果标准输入已经读完,并且已经缓冲区的数据都已经发送给服务器,就关闭写套接字 Shutdown(sockfd, SHUT_WR); /* send FIN */ } } } } } /* end nonb3 */
改善3问题:非阻塞式版本比较复杂
改善4:需要使用非阻塞I/O时,更简单的方法是把应用程序任务划分到多个进程(使用 fork) 或多个线程
fork 版本:
/** * TCP 使用两个进程 (fork) **/ #include "unp.h" void str_cli(FILE *fp, int sockfd) { pid_t pid; char sendline[MAXLINE], recvline[MAXLINE]; //子进程:从服务器读取数据并输出到 stdout if ( (pid = Fork()) == 0) { while (Readline(sockfd, recvline, MAXLINE) > 0) Fputs(recvline, stdout); kill(getppid(), SIGTERM); /* 子进程向父进程发送一个 SIGTERM 信号,终止父进程 */ exit(0); } //父进程:从 stdin 读取数据并发送到服务器 while (Fgets(sendline, MAXLINE, fp) != NULL) Writen(sockfd, sendline, strlen(sendline)); //在 stdin 读取 EOF,输入结束,关闭写套接字 Shutdown(sockfd, SHUT_WR); /* EOF on stdin, send FIN */ //父进程完成数据凰调用 pause 让自己进入睡眠状态 pause(); return; }
线程化版本:
/** * TCP 使用两个线程 **/ #include "unpthread.h" void *copyto(void *); static int sockfd; /* global for both threads to access */ static FILE *fp; void str_cli(FILE *fp_arg, int sockfd_arg) { char recvline[MAXLINE]; pthread_t tid; //1.把参数保存在外部变量中 sockfd = sockfd_arg; /* copy arguments to externals */ fp = fp_arg; //2.创建新线程 Pthread_create(&tid, NULL, copyto, NULL); //3.主线程循环:从套接字到标准输出复制 while (Readline(sockfd, recvline, MAXLINE) > 0) Fputs(recvline, stdout); } //4.copyto 线程:从标准输入到套接字复制 void * copyto(void *arg) { char sendline[MAXLINE]; while (Fgets(sendline, MAXLINE, fp) != NULL) Writen(sockfd, sendline, strlen(sendline)); Shutdown(sockfd, SHUT_WR); /* EOF on stdin, send FIN */ return(NULL); /* 4return (i.e., thread terminates) when EOF on stdin */ }
UNIX网络编程卷1 回射客户程序 TCP客户程序设计范式
声明:以上内容来自用户投稿及互联网公开渠道收集整理发布,本网站不拥有所有权,未作人工编辑处理,也不承担相关法律责任,若内容有误或涉及侵权可进行投诉: 投诉/举报 工作人员会在5个工作日内联系你,一经查实,本站将立刻删除涉嫌侵权内容。