用C++实现一个多进程回显服务器

用C++实现一个多进程回显服务器

      本案例将用多进程实现一个基于Linux使用C++实现的C/S网络程序:客户端从终端输入,然后借助服务端回显。进而观察TCP的状态转换图,思考多进程网络编程存在的问题。

1. 服务端程序(Linux)

         服务进程:通过监听所有网卡的9877接口,当有客户端来连接时,使用fork创建一个子进程对客户端连接进行服务,然后父进程继续监听连接的到来。需要注意的是当父进程未退出时,子进程在结束后将进入僵尸状态。父进程未使用信号对这些僵尸进程进行处理,随着连接的增多,服务端将出现很多僵尸进程。当然,如果父进程退出,则其僵尸子进程将被过继给init进程(进程号为1),而init进程干的事情就是不断回收这些僵尸进程,系统将很快恢复正常。

        本代码还存在一个问题,就是当大量并发连接来临时,将创建一个子进程对客户端进行一一回复,这样创建的进程数将很快到达系统的极限,同时创建一个进程将是很耗资源的,服务端很快就会奔溃。。

tcpserv01.c : 

[cpp] view plain copy
  1. #include <netinet/in.h> // for htonl htons  
  2. #include <sys/socket.h> // for socket bind listen accept  
  3. #include <strings.h> // for bzero  
  4. #include <unistd.h> // for close fork and so on  
  5. #include <stdlib.h> // for exit  
  6. #include <errno.h> // for errno  
  7. #include <stdio.h>  
  8. #include <string.h>  
  9.   
  10. #define SERV_PORT 9877  
  11. #define LISTENQ 1024  
  12. #define MAXLINE 4096   
  13. // 定义通用的socket address  
  14. typedef struct sockaddr SA;  
  15. void str_echo(int sockfd);  
  16. ssize_t writen(int fd, const void *vptr, size_t n);  
  17. void print_error(const char* err);  
  18.   
  19. int main(int argc, char** argv){  
  20.     int listenfd, connfd;  
  21.     pid_t childpid;  
  22.     socklen_t clilen;  
  23.     // IPv4地址结构  
  24.     struct sockaddr_in cliaddr, servaddr;  
  25.   
  26.     // 使用IPv4和流协议     
  27.     listenfd = socket(AF_INET, SOCK_STREAM, 0);  
  28.     if(listenfd < 0){  
  29.         print_error("socket fail");  
  30.     }  
  31.     // 在初始化socket address数据结构之前,将其清零  
  32.     bzero(&servaddr, sizeof(servaddr));  
  33.     // IPv4 : 指定使用IPv4地址家族  
  34.     servaddr.sin_family = AF_INET;  
  35.     // 设置任何接口的IPv4地址,这里将32bit的主机整数转换为网络字节序  
  36.     servaddr.sin_addr.s_addr = htonl(INADDR_ANY);  
  37.     // 设置监听的端口地址  
  38.     servaddr.sin_port = htons(SERV_PORT);  
  39.     // 绑定监听的地址  
  40.     int ret = bind(listenfd, (SA*) &servaddr, sizeof(servaddr));  
  41.     if(ret < 0){  
  42.         print_error("bind fail");  
  43.     }  
  44.   
  45.     // 进入监听状态,服务进程  
  46.     listen(listenfd, LISTENQ);    
  47.         while(1){  
  48.         clilen = sizeof(cliaddr);  
  49.         // 阻塞,直到有连接到达为止,且可以获取客户端的连接地址,value-result  
  50.         connfd = accept(listenfd, (SA*) &cliaddr, &clilen);  
  51.         if((childpid = fork()) == 0){  
  52.             // 子进程:关闭共享的监听句柄  
  53.             close(listenfd);  
  54.             // 进行具体的操作  
  55.             str_echo(connfd);  
  56.             // 结束子进程,同时将会自动关闭所有打开的文件句柄  
  57.             exit(0);  
  58.         }         
  59.         // 父进程:关闭打开的连接句柄,然后继续接受连接  
  60.         close(connfd);  
  61.     }     
  62.         exit(0);      
  63. }  
  64.   
  65. // 保证一次能写n个字节,同时处理中断重入的情况  
  66. ssize_t writen(int fd, const void* vptr, size_t n){  
  67.     size_t nleft;  
  68.     ssize_t nwritten;  
  69.     const char* ptr;  
  70.     ptr = vptr;  
  71.     nleft = n;  
  72.     while(nleft > 0){  
  73.         if((nwritten = write(fd, ptr, nleft)) <= 0){  
  74.             if(nwritten < 0 && errno == EINTR){  
  75.                 nwritten = 0;  
  76.             }  
  77.             else{  
  78.                 return -1;  
  79.             }  
  80.         }  
  81.         nleft -= nwritten;  
  82.         ptr += nwritten;  
  83.     }  
  84.     return n;  
  85. }  
  86.   
  87. // 子进程处理的主函数,不断地把读到的字节写回去,直到读到的字节数为0或者出错  
  88. void str_echo(int sockfd){  
  89.     ssize_t n;  
  90.     char buf[MAXLINE];  
  91. again:  
  92.     while((n = read(sockfd, buf, MAXLINE)) > 0){  
  93.         writen(sockfd, buf, n);           
  94.     }  
  95.     if(n < 0 && errno == EINTR){  
  96.         // 忽视中断重入  
  97.         goto again;  
  98.     }  
  99.     if(n < 0){  
  100.         print_error("str_echo");          
  101.     }  
  102. }  
  103.   
  104. // 获取错误号对应的内容,输出错误信息,并退出  
  105. void print_error(const char* err){  
  106.     int errno_save = errno;  
  107.     printf("%s : %s\n", err, strerror(errno_save));  
  108.     exit(1);  
  109. }  

2. 客户端程序(Linux)

         客户端:从终端不断读取输入,然后发给服务端,最后客户端再从服务端读取回来,并在终端展示。

tcpcli01.c :

[cpp] view plain copy
  1. #include <netinet/in.h>  
  2. #include <strings.h>  
  3. #include <string.h>  
  4. #include <sys/socket.h>  
  5. #include <arpa/inet.h>  
  6. #include <unistd.h>  
  7. #include <stdlib.h>  
  8. #include <errno.h>  
  9. #include <stdio.h>  
  10. #define SERV_PORT 9877  
  11. #define MAXLINE 1024  
  12. typedef struct sockaddr SA;  
  13.   
  14. void print_error(const char* err);  
  15. void str_cli(FILE* fp, int sockfd);  
  16. ssize_t writen(int fd, const void* vptr, size_t n);  
  17. ssize_t readline(int fd, void *vptr, size_t maxlen);  
  18. ssize_t my_read(int fd, char* ptr);  
  19.   
  20. int main(int argc, char** argv){  
  21.     int sockfd;  
  22.     struct sockaddr_in servaddr;  
  23.     // 带一个参数作为服务端的IPv4地址  
  24.     if(argc != 2){  
  25.         printf("format : %s IPv4\n", argv[0]);  
  26.         exit(1);  
  27.     }  
  28.   
  29.     sockfd = socket(AF_INET, SOCK_STREAM, 0);  
  30.     if(sockfd < 0){  
  31.         print_error("socket error");      
  32.     }  
  33.     bzero(&servaddr, sizeof(servaddr));  
  34.     servaddr.sin_family = AF_INET;  
  35.     servaddr.sin_port = htons(SERV_PORT);  
  36.     // 将输入的点分十进制IPv4地址转换为网络字节地址  
  37.     inet_pton(AF_INET, argv[1], &servaddr.sin_addr);  
  38.     int ret = connect(sockfd, (SA*)&servaddr, sizeof(servaddr));  
  39.     if(ret < 0){  
  40.         print_error("connect fail");  
  41.     }  
  42.     // 客户端进程的主要方法  
  43.     str_cli(stdin, sockfd);  
  44.     exit(0);  
  45. }  
  46.   
  47. void print_error(const char* err){  
  48.     int errno_save = errno;  
  49.     printf("%s : %s\n", err, strerror(errno_save));  
  50.     exit(1);  
  51. }  
  52.   
  53. // 保证一次能写入n个字节,同时处理中断重入的情况  
  54. ssize_t writen(int fd, const void* vptr, size_t n){  
  55.     size_t nleft;  
  56.     ssize_t nwritten;  
  57.     const char* ptr;  
  58.     ptr = vptr;  
  59.     nleft = n;  
  60.     while(nleft > 0){  
  61.         if((nwritten = write(fd, ptr, nleft)) <= 0){  
  62.             if(nwritten < 0 && errno == EINTR){  
  63.                 nwritten = 0;  
  64.             }  
  65.             else{  
  66.                 return -1;  
  67.             }  
  68.         }  
  69.         nleft -= nwritten;  
  70.         ptr += nwritten;  
  71.     }  
  72.     return n;  
  73. }  
  74.   
  75. // 客户端逻辑的主要函数:从终端不断读取输入,然后发给服务端,最后客户端再从服务端读取回来,并在终端展示  
  76. void str_cli(FILE* fp, int sockfd){  
  77.     char sendline[MAXLINE], recvline[MAXLINE];  
  78.     while(fgets(sendline, MAXLINE, fp) != NULL && sendline[0]){  
  79.         writen(sockfd, sendline, strlen(sendline));  
  80.         if(readline(sockfd, recvline, sizeof(recvline)) == 0){  
  81.             print_error("server call close first");  
  82.         }  
  83.         // 已经添加了0  
  84.         fputs(recvline, stdout);  
  85.     }     
  86. }  
  87.   
  88. // 借助缓冲区减少使用read的次数,每次读取一个字符  
  89. ssize_t my_read(int fd, char* ptr){  
  90.     // 静态变量,保证一直都存在  
  91.     static int read_cnt;  
  92.     static char* read_ptr;  
  93.     static char read_buf[MAXLINE];  
  94.     // 如果能读的字符已经读完,则重新读取  
  95.     if(read_cnt <= 0){  
  96.         again:  
  97.             if((read_cnt = read(fd, read_buf, sizeof(read_buf))) < 0){  
  98.                 if(errno == EINTR){  
  99.                     goto again;  
  100.                 }  
  101.                 return -1;  
  102.             }  
  103.             else if(read_cnt == 0){  
  104.                 return 0;  
  105.             }  
  106.             read_ptr = read_buf;  
  107.     }  
  108.     // 读取一个字符,同时移动缓冲区的指针read_ptr  
  109.     read_cnt--;  
  110.     *ptr = *read_ptr++;  
  111.     return 1;  
  112. }  
  113. // 通过检查换行符读取一行,同时会添加0,这是C风格的字符串。如果没找到换行符,则以maxlen - 1个字符返回,同时包含一个0  
  114. ssize_t readline(int fd, void *vptr, size_t maxlen){  
  115.     ssize_t n, rc;  
  116.     char c, *ptr;  
  117.     ptr = vptr;  
  118.     // 读取maxlen个字符,判断是否出现换行符,留出一个空间填零  
  119.     for(n = 1; n < maxlen; n++){  
  120.         if((rc = my_read(fd, &c)) == 1){  
  121.             *ptr++ = c;  
  122.             if(c == '\n'){  
  123.                 n++;  
  124.                 break;  
  125.             }  
  126.         }  
  127.         else if(rc == 0){  
  128.             *ptr = 0;  
  129.             // 没有读取到字符,服务端已经结束, EOF   
  130.             return n - 1;  
  131.         }  
  132.         else{  
  133.             return -1;  
  134.         }  
  135.     }  
  136.     // 读取到换行,或者读满maxlen - 1个字符,最后一个字符自己添加,且是0  
  137.     *ptr = 0;     
  138.     return n - 1;  
  139. }     


3. 测试

3.1 获取IP地址

运行环境是Ubuntu15.04。首先,通过运行:ifconfig,获得服务端的IP地址

用C++实现一个多进程回显服务器

显然轮回网卡lo的IP地址是127.0.0.1,wlan0的IP地址是192.168.1.100。


3.2 运行服务端程序

编译程序:

gcc -o tcpserv01 tcpserv01.c

gcc -o tcpcli01 tcpcli01.c

以后台运行的形式启动服务端:

./tcpserv01 &

然后查看进程的状态 :

netstat -a

用C++实现一个多进程回显服务器

从中可以看到服务进程的进程号为27209,已经在9877处于监听状态。*:9877中*表示任意接口地址,是上面设置了INADDR_ANY的结果。


同时,可以在运行服务端的终端下运行 : tty

从而获取到服务端运行的伪终端:

用C++实现一个多进程回显服务器


3.3 运行客户端程序

        这里在跟服务端同一台机器(Ubuntu)上运行两个客户端程序:

./tcpcli01 192.168.1.100

./tcpcli01 127.0.0.1

用C++实现一个多进程回显服务器


3.4 获取进程启动时的相关信息和网络状态

        netstat可以查看socket的网络连接状态。在另一个终端运行 :

netstat -a | grep 9877

ps -t /dev/pts/1 -o pid,ppid,tty,stat,args,wchan

用C++实现一个多进程回显服务器

用C++实现一个多进程回显服务器

注意:使用ps aux | grep tcp可以获得进程对应的伪终端。因为每个进程都有一个执行命令,而我们的执行命令中含有tcp这个子串。


可以看到出现五个进程:

服务端进程:

父服务进程处于LISTEN状态 ,pid = 27209

子服务进程192.168.1.100,pid = 31715, ppid = 27209

子服务进程127.0.0.1,pid = 32133, ppid = 27209,子进程同时拥有与父进程相同的伪终端pts/7

客户端进程:

192.168.1.100, pid = 31714, port number = 51177

127.0.0.1, pid = 32132, port number = 33189

刚好可以从第一幅图看到5条网络连接状态。此时其他两条网络连接已经创建(established),两个子服务进程处于休眠状态(Sleep),阻塞。而两个客户端进程在等待用户输入,也处于休眠状态。


3.5 主动结束客户端

        当我在客户终端192.168.1.100输入数据时,将出现回显,最后按下Ctrl + D:

用C++实现一个多进程回显服务器

        这时输入结束,客户端进程将退出,从而发起四次挥手,结束数据交换和连接。最后客户进程exit终止,但主动结束连接者将进入TIME_WAIT状态,并且会停留2MSL的时间,这是客户进程无法看到的:

用C++实现一个多进程回显服务器


3.6 子服务进程进入僵尸状态

        同时对应的子服务进程是被动关闭的一方,在收到最后一个ACK后连接就终止了。但是子服务进程却进入僵尸状态:

用C++实现一个多进程回显服务器

上述PID = 31715的子服务进程的状态是Z,即zombie,僵尸状态,进程表项一直没被回收。


3.7 终止父服务进程

        终止了父服务进程,所有僵尸进程将被过继给init进程,被回收。

用C++实现一个多进程回显服务器

上图显示两个子服务进程都变成僵尸进程,然后使用fg使得后台进程变成前台进程,同时按下Ctrl + C终止父服务进程,之后原来的僵尸进程都消失了。


参考文献:UNIX网络编程 卷1 套接字联网API
版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/ac_dao_di/article/details/53386156

用C++实现一个多进程回显服务器

      本案例将用多进程实现一个基于Linux使用C++实现的C/S网络程序:客户端从终端输入,然后借助服务端回显。进而观察TCP的状态转换图,思考多进程网络编程存在的问题。

1. 服务端程序(Linux)

         服务进程:通过监听所有网卡的9877接口,当有客户端来连接时,使用fork创建一个子进程对客户端连接进行服务,然后父进程继续监听连接的到来。需要注意的是当父进程未退出时,子进程在结束后将进入僵尸状态。父进程未使用信号对这些僵尸进程进行处理,随着连接的增多,服务端将出现很多僵尸进程。当然,如果父进程退出,则其僵尸子进程将被过继给init进程(进程号为1),而init进程干的事情就是不断回收这些僵尸进程,系统将很快恢复正常。

        本代码还存在一个问题,就是当大量并发连接来临时,将创建一个子进程对客户端进行一一回复,这样创建的进程数将很快到达系统的极限,同时创建一个进程将是很耗资源的,服务端很快就会奔溃。。

tcpserv01.c : 

[cpp] view plain copy
  1. #include <netinet/in.h> // for htonl htons  
  2. #include <sys/socket.h> // for socket bind listen accept  
  3. #include <strings.h> // for bzero  
  4. #include <unistd.h> // for close fork and so on  
  5. #include <stdlib.h> // for exit  
  6. #include <errno.h> // for errno  
  7. #include <stdio.h>  
  8. #include <string.h>  
  9.   
  10. #define SERV_PORT 9877  
  11. #define LISTENQ 1024  
  12. #define MAXLINE 4096   
  13. // 定义通用的socket address  
  14. typedef struct sockaddr SA;  
  15. void str_echo(int sockfd);  
  16. ssize_t writen(int fd, const void *vptr, size_t n);  
  17. void print_error(const char* err);  
  18.   
  19. int main(int argc, char** argv){  
  20.     int listenfd, connfd;  
  21.     pid_t childpid;  
  22.     socklen_t clilen;  
  23.     // IPv4地址结构  
  24.     struct sockaddr_in cliaddr, servaddr;  
  25.   
  26.     // 使用IPv4和流协议     
  27.     listenfd = socket(AF_INET, SOCK_STREAM, 0);  
  28.     if(listenfd < 0){  
  29.         print_error("socket fail");  
  30.     }  
  31.     // 在初始化socket address数据结构之前,将其清零  
  32.     bzero(&servaddr, sizeof(servaddr));  
  33.     // IPv4 : 指定使用IPv4地址家族  
  34.     servaddr.sin_family = AF_INET;  
  35.     // 设置任何接口的IPv4地址,这里将32bit的主机整数转换为网络字节序  
  36.     servaddr.sin_addr.s_addr = htonl(INADDR_ANY);  
  37.     // 设置监听的端口地址  
  38.     servaddr.sin_port = htons(SERV_PORT);  
  39.     // 绑定监听的地址  
  40.     int ret = bind(listenfd, (SA*) &servaddr, sizeof(servaddr));  
  41.     if(ret < 0){  
  42.         print_error("bind fail");  
  43.     }  
  44.   
  45.     // 进入监听状态,服务进程  
  46.     listen(listenfd, LISTENQ);    
  47.         while(1){  
  48.         clilen = sizeof(cliaddr);  
  49.         // 阻塞,直到有连接到达为止,且可以获取客户端的连接地址,value-result  
  50.         connfd = accept(listenfd, (SA*) &cliaddr, &clilen);  
  51.         if((childpid = fork()) == 0){  
  52.             // 子进程:关闭共享的监听句柄  
  53.             close(listenfd);  
  54.             // 进行具体的操作  
  55.             str_echo(connfd);  
  56.             // 结束子进程,同时将会自动关闭所有打开的文件句柄  
  57.             exit(0);  
  58.         }         
  59.         // 父进程:关闭打开的连接句柄,然后继续接受连接  
  60.         close(connfd);  
  61.     }     
  62.         exit(0);      
  63. }  
  64.   
  65. // 保证一次能写n个字节,同时处理中断重入的情况  
  66. ssize_t writen(int fd, const void* vptr, size_t n){  
  67.     size_t nleft;  
  68.     ssize_t nwritten;  
  69.     const char* ptr;  
  70.     ptr = vptr;  
  71.     nleft = n;  
  72.     while(nleft > 0){  
  73.         if((nwritten = write(fd, ptr, nleft)) <= 0){  
  74.             if(nwritten < 0 && errno == EINTR){  
  75.                 nwritten = 0;  
  76.             }  
  77.             else{  
  78.                 return -1;  
  79.             }  
  80.         }  
  81.         nleft -= nwritten;  
  82.         ptr += nwritten;  
  83.     }  
  84.     return n;  
  85. }  
  86.   
  87. // 子进程处理的主函数,不断地把读到的字节写回去,直到读到的字节数为0或者出错  
  88. void str_echo(int sockfd){  
  89.     ssize_t n;  
  90.     char buf[MAXLINE];  
  91. again:  
  92.     while((n = read(sockfd, buf, MAXLINE)) > 0){  
  93.         writen(sockfd, buf, n);           
  94.     }  
  95.     if(n < 0 && errno == EINTR){  
  96.         // 忽视中断重入  
  97.         goto again;  
  98.     }  
  99.     if(n < 0){  
  100.         print_error("str_echo");          
  101.     }  
  102. }  
  103.   
  104. // 获取错误号对应的内容,输出错误信息,并退出  
  105. void print_error(const char* err){  
  106.     int errno_save = errno;  
  107.     printf("%s : %s\n", err, strerror(errno_save));  
  108.     exit(1);  
  109. }  

2. 客户端程序(Linux)

         客户端:从终端不断读取输入,然后发给服务端,最后客户端再从服务端读取回来,并在终端展示。

tcpcli01.c :

[cpp] view plain copy
  1. #include <netinet/in.h>  
  2. #include <strings.h>  
  3. #include <string.h>  
  4. #include <sys/socket.h>  
  5. #include <arpa/inet.h>  
  6. #include <unistd.h>  
  7. #include <stdlib.h>  
  8. #include <errno.h>  
  9. #include <stdio.h>  
  10. #define SERV_PORT 9877  
  11. #define MAXLINE 1024  
  12. typedef struct sockaddr SA;  
  13.   
  14. void print_error(const char* err);  
  15. void str_cli(FILE* fp, int sockfd);  
  16. ssize_t writen(int fd, const void* vptr, size_t n);  
  17. ssize_t readline(int fd, void *vptr, size_t maxlen);  
  18. ssize_t my_read(int fd, char* ptr);  
  19.   
  20. int main(int argc, char** argv){  
  21.     int sockfd;  
  22.     struct sockaddr_in servaddr;  
  23.     // 带一个参数作为服务端的IPv4地址  
  24.     if(argc != 2){  
  25.         printf("format : %s IPv4\n", argv[0]);  
  26.         exit(1);  
  27.     }  
  28.   
  29.     sockfd = socket(AF_INET, SOCK_STREAM, 0);  
  30.     if(sockfd < 0){  
  31.         print_error("socket error");      
  32.     }  
  33.     bzero(&servaddr, sizeof(servaddr));  
  34.     servaddr.sin_family = AF_INET;  
  35.     servaddr.sin_port = htons(SERV_PORT);  
  36.     // 将输入的点分十进制IPv4地址转换为网络字节地址  
  37.     inet_pton(AF_INET, argv[1], &servaddr.sin_addr);  
  38.     int ret = connect(sockfd, (SA*)&servaddr, sizeof(servaddr));  
  39.     if(ret < 0){  
  40.         print_error("connect fail");  
  41.     }  
  42.     // 客户端进程的主要方法  
  43.     str_cli(stdin, sockfd);  
  44.     exit(0);  
  45. }  
  46.   
  47. void print_error(const char* err){  
  48.     int errno_save = errno;  
  49.     printf("%s : %s\n", err, strerror(errno_save));  
  50.     exit(1);  
  51. }  
  52.   
  53. // 保证一次能写入n个字节,同时处理中断重入的情况  
  54. ssize_t writen(int fd, const void* vptr, size_t n){  
  55.     size_t nleft;  
  56.     ssize_t nwritten;  
  57.     const char* ptr;  
  58.     ptr = vptr;  
  59.     nleft = n;  
  60.     while(nleft > 0){  
  61.         if((nwritten = write(fd, ptr, nleft)) <= 0){  
  62.             if(nwritten < 0 && errno == EINTR){  
  63.                 nwritten = 0;  
  64.             }  
  65.             else{  
  66.                 return -1;  
  67.             }  
  68.         }  
  69.         nleft -= nwritten;  
  70.         ptr += nwritten;  
  71.     }  
  72.     return n;  
  73. }  
  74.   
  75. // 客户端逻辑的主要函数:从终端不断读取输入,然后发给服务端,最后客户端再从服务端读取回来,并在终端展示  
  76. void str_cli(FILE* fp, int sockfd){  
  77.     char sendline[MAXLINE], recvline[MAXLINE];  
  78.     while(fgets(sendline, MAXLINE, fp) != NULL && sendline[0]){  
  79.         writen(sockfd, sendline, strlen(sendline));  
  80.         if(readline(sockfd, recvline, sizeof(recvline)) == 0){  
  81.             print_error("server call close first");  
  82.         }  
  83.         // 已经添加了0  
  84.         fputs(recvline, stdout);  
  85.     }     
  86. }  
  87.   
  88. // 借助缓冲区减少使用read的次数,每次读取一个字符  
  89. ssize_t my_read(int fd, char* ptr){  
  90.     // 静态变量,保证一直都存在  
  91.     static int read_cnt;  
  92.     static char* read_ptr;  
  93.     static char read_buf[MAXLINE];  
  94.     // 如果能读的字符已经读完,则重新读取  
  95.     if(read_cnt <= 0){  
  96.         again:  
  97.             if((read_cnt = read(fd, read_buf, sizeof(read_buf))) < 0){  
  98.                 if(errno == EINTR){  
  99.                     goto again;  
  100.                 }  
  101.                 return -1;  
  102.             }  
  103.             else if(read_cnt == 0){  
  104.                 return 0;  
  105.             }  
  106.             read_ptr = read_buf;  
  107.     }  
  108.     // 读取一个字符,同时移动缓冲区的指针read_ptr  
  109.     read_cnt--;  
  110.     *ptr = *read_ptr++;  
  111.     return 1;  
  112. }  
  113. // 通过检查换行符读取一行,同时会添加0,这是C风格的字符串。如果没找到换行符,则以maxlen - 1个字符返回,同时包含一个0  
  114. ssize_t readline(int fd, void *vptr, size_t maxlen){  
  115.     ssize_t n, rc;  
  116.     char c, *ptr;  
  117.     ptr = vptr;  
  118.     // 读取maxlen个字符,判断是否出现换行符,留出一个空间填零  
  119.     for(n = 1; n < maxlen; n++){  
  120.         if((rc = my_read(fd, &c)) == 1){  
  121.             *ptr++ = c;  
  122.             if(c == '\n'){  
  123.                 n++;  
  124.                 break;  
  125.             }  
  126.         }  
  127.         else if(rc == 0){  
  128.             *ptr = 0;  
  129.             // 没有读取到字符,服务端已经结束, EOF   
  130.             return n - 1;  
  131.         }  
  132.         else{  
  133.             return -1;  
  134.         }  
  135.     }  
  136.     // 读取到换行,或者读满maxlen - 1个字符,最后一个字符自己添加,且是0  
  137.     *ptr = 0;     
  138.     return n - 1;  
  139. }     


3. 测试

3.1 获取IP地址

运行环境是Ubuntu15.04。首先,通过运行:ifconfig,获得服务端的IP地址

用C++实现一个多进程回显服务器

显然轮回网卡lo的IP地址是127.0.0.1,wlan0的IP地址是192.168.1.100。


3.2 运行服务端程序

编译程序:

gcc -o tcpserv01 tcpserv01.c

gcc -o tcpcli01 tcpcli01.c

以后台运行的形式启动服务端:

./tcpserv01 &

然后查看进程的状态 :

netstat -a

用C++实现一个多进程回显服务器

从中可以看到服务进程的进程号为27209,已经在9877处于监听状态。*:9877中*表示任意接口地址,是上面设置了INADDR_ANY的结果。


同时,可以在运行服务端的终端下运行 : tty

从而获取到服务端运行的伪终端:

用C++实现一个多进程回显服务器


3.3 运行客户端程序

        这里在跟服务端同一台机器(Ubuntu)上运行两个客户端程序:

./tcpcli01 192.168.1.100

./tcpcli01 127.0.0.1

用C++实现一个多进程回显服务器


3.4 获取进程启动时的相关信息和网络状态

        netstat可以查看socket的网络连接状态。在另一个终端运行 :

netstat -a | grep 9877

ps -t /dev/pts/1 -o pid,ppid,tty,stat,args,wchan

用C++实现一个多进程回显服务器

用C++实现一个多进程回显服务器

注意:使用ps aux | grep tcp可以获得进程对应的伪终端。因为每个进程都有一个执行命令,而我们的执行命令中含有tcp这个子串。


可以看到出现五个进程:

服务端进程:

父服务进程处于LISTEN状态 ,pid = 27209

子服务进程192.168.1.100,pid = 31715, ppid = 27209

子服务进程127.0.0.1,pid = 32133, ppid = 27209,子进程同时拥有与父进程相同的伪终端pts/7

客户端进程:

192.168.1.100, pid = 31714, port number = 51177

127.0.0.1, pid = 32132, port number = 33189

刚好可以从第一幅图看到5条网络连接状态。此时其他两条网络连接已经创建(established),两个子服务进程处于休眠状态(Sleep),阻塞。而两个客户端进程在等待用户输入,也处于休眠状态。


3.5 主动结束客户端

        当我在客户终端192.168.1.100输入数据时,将出现回显,最后按下Ctrl + D:

用C++实现一个多进程回显服务器

        这时输入结束,客户端进程将退出,从而发起四次挥手,结束数据交换和连接。最后客户进程exit终止,但主动结束连接者将进入TIME_WAIT状态,并且会停留2MSL的时间,这是客户进程无法看到的:

用C++实现一个多进程回显服务器


3.6 子服务进程进入僵尸状态

        同时对应的子服务进程是被动关闭的一方,在收到最后一个ACK后连接就终止了。但是子服务进程却进入僵尸状态:

用C++实现一个多进程回显服务器

上述PID = 31715的子服务进程的状态是Z,即zombie,僵尸状态,进程表项一直没被回收。


3.7 终止父服务进程

        终止了父服务进程,所有僵尸进程将被过继给init进程,被回收。

用C++实现一个多进程回显服务器

上图显示两个子服务进程都变成僵尸进程,然后使用fg使得后台进程变成前台进程,同时按下Ctrl + C终止父服务进程,之后原来的僵尸进程都消失了。


参考文献:UNIX网络编程 卷1 套接字联网API