用C++实现一个多进程回显服务器
用C++实现一个多进程回显服务器
本案例将用多进程实现一个基于Linux使用C++实现的C/S网络程序:客户端从终端输入,然后借助服务端回显。进而观察TCP的状态转换图,思考多进程网络编程存在的问题。
1. 服务端程序(Linux)
服务进程:通过监听所有网卡的9877接口,当有客户端来连接时,使用fork创建一个子进程对客户端连接进行服务,然后父进程继续监听连接的到来。需要注意的是当父进程未退出时,子进程在结束后将进入僵尸状态。父进程未使用信号对这些僵尸进程进行处理,随着连接的增多,服务端将出现很多僵尸进程。当然,如果父进程退出,则其僵尸子进程将被过继给init进程(进程号为1),而init进程干的事情就是不断回收这些僵尸进程,系统将很快恢复正常。
本代码还存在一个问题,就是当大量并发连接来临时,将创建一个子进程对客户端进行一一回复,这样创建的进程数将很快到达系统的极限,同时创建一个进程将是很耗资源的,服务端很快就会奔溃。。
tcpserv01.c :
- #include <netinet/in.h> // for htonl htons
- #include <sys/socket.h> // for socket bind listen accept
- #include <strings.h> // for bzero
- #include <unistd.h> // for close fork and so on
- #include <stdlib.h> // for exit
- #include <errno.h> // for errno
- #include <stdio.h>
- #include <string.h>
- #define SERV_PORT 9877
- #define LISTENQ 1024
- #define MAXLINE 4096
- // 定义通用的socket address
- typedef struct sockaddr SA;
- void str_echo(int sockfd);
- ssize_t writen(int fd, const void *vptr, size_t n);
- void print_error(const char* err);
- int main(int argc, char** argv){
- int listenfd, connfd;
- pid_t childpid;
- socklen_t clilen;
- // IPv4地址结构
- struct sockaddr_in cliaddr, servaddr;
- // 使用IPv4和流协议
- listenfd = socket(AF_INET, SOCK_STREAM, 0);
- if(listenfd < 0){
- print_error("socket fail");
- }
- // 在初始化socket address数据结构之前,将其清零
- bzero(&servaddr, sizeof(servaddr));
- // IPv4 : 指定使用IPv4地址家族
- servaddr.sin_family = AF_INET;
- // 设置任何接口的IPv4地址,这里将32bit的主机整数转换为网络字节序
- servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
- // 设置监听的端口地址
- servaddr.sin_port = htons(SERV_PORT);
- // 绑定监听的地址
- int ret = bind(listenfd, (SA*) &servaddr, sizeof(servaddr));
- if(ret < 0){
- print_error("bind fail");
- }
- // 进入监听状态,服务进程
- listen(listenfd, LISTENQ);
- while(1){
- clilen = sizeof(cliaddr);
- // 阻塞,直到有连接到达为止,且可以获取客户端的连接地址,value-result
- connfd = accept(listenfd, (SA*) &cliaddr, &clilen);
- if((childpid = fork()) == 0){
- // 子进程:关闭共享的监听句柄
- close(listenfd);
- // 进行具体的操作
- str_echo(connfd);
- // 结束子进程,同时将会自动关闭所有打开的文件句柄
- exit(0);
- }
- // 父进程:关闭打开的连接句柄,然后继续接受连接
- close(connfd);
- }
- exit(0);
- }
- // 保证一次能写n个字节,同时处理中断重入的情况
- ssize_t writen(int fd, const void* vptr, size_t n){
- size_t nleft;
- ssize_t nwritten;
- const char* ptr;
- ptr = vptr;
- nleft = n;
- while(nleft > 0){
- if((nwritten = write(fd, ptr, nleft)) <= 0){
- if(nwritten < 0 && errno == EINTR){
- nwritten = 0;
- }
- else{
- return -1;
- }
- }
- nleft -= nwritten;
- ptr += nwritten;
- }
- return n;
- }
- // 子进程处理的主函数,不断地把读到的字节写回去,直到读到的字节数为0或者出错
- 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;
- }
- if(n < 0){
- print_error("str_echo");
- }
- }
- // 获取错误号对应的内容,输出错误信息,并退出
- void print_error(const char* err){
- int errno_save = errno;
- printf("%s : %s\n", err, strerror(errno_save));
- exit(1);
- }
2. 客户端程序(Linux)
客户端:从终端不断读取输入,然后发给服务端,最后客户端再从服务端读取回来,并在终端展示。
tcpcli01.c :
- #include <netinet/in.h>
- #include <strings.h>
- #include <string.h>
- #include <sys/socket.h>
- #include <arpa/inet.h>
- #include <unistd.h>
- #include <stdlib.h>
- #include <errno.h>
- #include <stdio.h>
- #define SERV_PORT 9877
- #define MAXLINE 1024
- typedef struct sockaddr SA;
- void print_error(const char* err);
- void str_cli(FILE* fp, int sockfd);
- ssize_t writen(int fd, const void* vptr, size_t n);
- ssize_t readline(int fd, void *vptr, size_t maxlen);
- ssize_t my_read(int fd, char* ptr);
- int main(int argc, char** argv){
- int sockfd;
- struct sockaddr_in servaddr;
- // 带一个参数作为服务端的IPv4地址
- if(argc != 2){
- printf("format : %s IPv4\n", argv[0]);
- exit(1);
- }
- sockfd = socket(AF_INET, SOCK_STREAM, 0);
- if(sockfd < 0){
- print_error("socket error");
- }
- bzero(&servaddr, sizeof(servaddr));
- servaddr.sin_family = AF_INET;
- servaddr.sin_port = htons(SERV_PORT);
- // 将输入的点分十进制IPv4地址转换为网络字节地址
- inet_pton(AF_INET, argv[1], &servaddr.sin_addr);
- int ret = connect(sockfd, (SA*)&servaddr, sizeof(servaddr));
- if(ret < 0){
- print_error("connect fail");
- }
- // 客户端进程的主要方法
- str_cli(stdin, sockfd);
- exit(0);
- }
- void print_error(const char* err){
- int errno_save = errno;
- printf("%s : %s\n", err, strerror(errno_save));
- exit(1);
- }
- // 保证一次能写入n个字节,同时处理中断重入的情况
- ssize_t writen(int fd, const void* vptr, size_t n){
- size_t nleft;
- ssize_t nwritten;
- const char* ptr;
- ptr = vptr;
- nleft = n;
- while(nleft > 0){
- if((nwritten = write(fd, ptr, nleft)) <= 0){
- if(nwritten < 0 && errno == EINTR){
- nwritten = 0;
- }
- else{
- return -1;
- }
- }
- nleft -= nwritten;
- ptr += nwritten;
- }
- return n;
- }
- // 客户端逻辑的主要函数:从终端不断读取输入,然后发给服务端,最后客户端再从服务端读取回来,并在终端展示
- void str_cli(FILE* fp, int sockfd){
- char sendline[MAXLINE], recvline[MAXLINE];
- while(fgets(sendline, MAXLINE, fp) != NULL && sendline[0]){
- writen(sockfd, sendline, strlen(sendline));
- if(readline(sockfd, recvline, sizeof(recvline)) == 0){
- print_error("server call close first");
- }
- // 已经添加了0
- fputs(recvline, stdout);
- }
- }
- // 借助缓冲区减少使用read的次数,每次读取一个字符
- ssize_t my_read(int fd, char* ptr){
- // 静态变量,保证一直都存在
- static int read_cnt;
- static char* read_ptr;
- static char read_buf[MAXLINE];
- // 如果能读的字符已经读完,则重新读取
- if(read_cnt <= 0){
- again:
- if((read_cnt = read(fd, read_buf, sizeof(read_buf))) < 0){
- if(errno == EINTR){
- goto again;
- }
- return -1;
- }
- else if(read_cnt == 0){
- return 0;
- }
- read_ptr = read_buf;
- }
- // 读取一个字符,同时移动缓冲区的指针read_ptr
- read_cnt--;
- *ptr = *read_ptr++;
- return 1;
- }
- // 通过检查换行符读取一行,同时会添加0,这是C风格的字符串。如果没找到换行符,则以maxlen - 1个字符返回,同时包含一个0
- ssize_t readline(int fd, void *vptr, size_t maxlen){
- ssize_t n, rc;
- char c, *ptr;
- ptr = vptr;
- // 读取maxlen个字符,判断是否出现换行符,留出一个空间填零
- for(n = 1; n < maxlen; n++){
- if((rc = my_read(fd, &c)) == 1){
- *ptr++ = c;
- if(c == '\n'){
- n++;
- break;
- }
- }
- else if(rc == 0){
- *ptr = 0;
- // 没有读取到字符,服务端已经结束, EOF
- return n - 1;
- }
- else{
- return -1;
- }
- }
- // 读取到换行,或者读满maxlen - 1个字符,最后一个字符自己添加,且是0
- *ptr = 0;
- return n - 1;
- }
3. 测试
3.1 获取IP地址
运行环境是Ubuntu15.04。首先,通过运行:ifconfig,获得服务端的IP地址
显然轮回网卡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
从中可以看到服务进程的进程号为27209,已经在9877处于监听状态。*:9877中*表示任意接口地址,是上面设置了INADDR_ANY的结果。
同时,可以在运行服务端的终端下运行 : tty
从而获取到服务端运行的伪终端:
3.3 运行客户端程序
这里在跟服务端同一台机器(Ubuntu)上运行两个客户端程序:
./tcpcli01 192.168.1.100
./tcpcli01 127.0.0.1
3.4 获取进程启动时的相关信息和网络状态
netstat可以查看socket的网络连接状态。在另一个终端运行 :
netstat -a | grep 9877
ps -t /dev/pts/1 -o pid,ppid,tty,stat,args,wchan
注意:使用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:
这时输入结束,客户端进程将退出,从而发起四次挥手,结束数据交换和连接。最后客户进程exit终止,但主动结束连接者将进入TIME_WAIT状态,并且会停留2MSL的时间,这是客户进程无法看到的:
3.6 子服务进程进入僵尸状态
同时对应的子服务进程是被动关闭的一方,在收到最后一个ACK后连接就终止了。但是子服务进程却进入僵尸状态:
上述PID = 31715的子服务进程的状态是Z,即zombie,僵尸状态,进程表项一直没被回收。
3.7 终止父服务进程
终止了父服务进程,所有僵尸进程将被过继给init进程,被回收。
用C++实现一个多进程回显服务器
本案例将用多进程实现一个基于Linux使用C++实现的C/S网络程序:客户端从终端输入,然后借助服务端回显。进而观察TCP的状态转换图,思考多进程网络编程存在的问题。
1. 服务端程序(Linux)
服务进程:通过监听所有网卡的9877接口,当有客户端来连接时,使用fork创建一个子进程对客户端连接进行服务,然后父进程继续监听连接的到来。需要注意的是当父进程未退出时,子进程在结束后将进入僵尸状态。父进程未使用信号对这些僵尸进程进行处理,随着连接的增多,服务端将出现很多僵尸进程。当然,如果父进程退出,则其僵尸子进程将被过继给init进程(进程号为1),而init进程干的事情就是不断回收这些僵尸进程,系统将很快恢复正常。
本代码还存在一个问题,就是当大量并发连接来临时,将创建一个子进程对客户端进行一一回复,这样创建的进程数将很快到达系统的极限,同时创建一个进程将是很耗资源的,服务端很快就会奔溃。。
tcpserv01.c :
- #include <netinet/in.h> // for htonl htons
- #include <sys/socket.h> // for socket bind listen accept
- #include <strings.h> // for bzero
- #include <unistd.h> // for close fork and so on
- #include <stdlib.h> // for exit
- #include <errno.h> // for errno
- #include <stdio.h>
- #include <string.h>
- #define SERV_PORT 9877
- #define LISTENQ 1024
- #define MAXLINE 4096
- // 定义通用的socket address
- typedef struct sockaddr SA;
- void str_echo(int sockfd);
- ssize_t writen(int fd, const void *vptr, size_t n);
- void print_error(const char* err);
- int main(int argc, char** argv){
- int listenfd, connfd;
- pid_t childpid;
- socklen_t clilen;
- // IPv4地址结构
- struct sockaddr_in cliaddr, servaddr;
- // 使用IPv4和流协议
- listenfd = socket(AF_INET, SOCK_STREAM, 0);
- if(listenfd < 0){
- print_error("socket fail");
- }
- // 在初始化socket address数据结构之前,将其清零
- bzero(&servaddr, sizeof(servaddr));
- // IPv4 : 指定使用IPv4地址家族
- servaddr.sin_family = AF_INET;
- // 设置任何接口的IPv4地址,这里将32bit的主机整数转换为网络字节序
- servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
- // 设置监听的端口地址
- servaddr.sin_port = htons(SERV_PORT);
- // 绑定监听的地址
- int ret = bind(listenfd, (SA*) &servaddr, sizeof(servaddr));
- if(ret < 0){
- print_error("bind fail");
- }
- // 进入监听状态,服务进程
- listen(listenfd, LISTENQ);
- while(1){
- clilen = sizeof(cliaddr);
- // 阻塞,直到有连接到达为止,且可以获取客户端的连接地址,value-result
- connfd = accept(listenfd, (SA*) &cliaddr, &clilen);
- if((childpid = fork()) == 0){
- // 子进程:关闭共享的监听句柄
- close(listenfd);
- // 进行具体的操作
- str_echo(connfd);
- // 结束子进程,同时将会自动关闭所有打开的文件句柄
- exit(0);
- }
- // 父进程:关闭打开的连接句柄,然后继续接受连接
- close(connfd);
- }
- exit(0);
- }
- // 保证一次能写n个字节,同时处理中断重入的情况
- ssize_t writen(int fd, const void* vptr, size_t n){
- size_t nleft;
- ssize_t nwritten;
- const char* ptr;
- ptr = vptr;
- nleft = n;
- while(nleft > 0){
- if((nwritten = write(fd, ptr, nleft)) <= 0){
- if(nwritten < 0 && errno == EINTR){
- nwritten = 0;
- }
- else{
- return -1;
- }
- }
- nleft -= nwritten;
- ptr += nwritten;
- }
- return n;
- }
- // 子进程处理的主函数,不断地把读到的字节写回去,直到读到的字节数为0或者出错
- 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;
- }
- if(n < 0){
- print_error("str_echo");
- }
- }
- // 获取错误号对应的内容,输出错误信息,并退出
- void print_error(const char* err){
- int errno_save = errno;
- printf("%s : %s\n", err, strerror(errno_save));
- exit(1);
- }
2. 客户端程序(Linux)
客户端:从终端不断读取输入,然后发给服务端,最后客户端再从服务端读取回来,并在终端展示。
tcpcli01.c :
- #include <netinet/in.h>
- #include <strings.h>
- #include <string.h>
- #include <sys/socket.h>
- #include <arpa/inet.h>
- #include <unistd.h>
- #include <stdlib.h>
- #include <errno.h>
- #include <stdio.h>
- #define SERV_PORT 9877
- #define MAXLINE 1024
- typedef struct sockaddr SA;
- void print_error(const char* err);
- void str_cli(FILE* fp, int sockfd);
- ssize_t writen(int fd, const void* vptr, size_t n);
- ssize_t readline(int fd, void *vptr, size_t maxlen);
- ssize_t my_read(int fd, char* ptr);
- int main(int argc, char** argv){
- int sockfd;
- struct sockaddr_in servaddr;
- // 带一个参数作为服务端的IPv4地址
- if(argc != 2){
- printf("format : %s IPv4\n", argv[0]);
- exit(1);
- }
- sockfd = socket(AF_INET, SOCK_STREAM, 0);
- if(sockfd < 0){
- print_error("socket error");
- }
- bzero(&servaddr, sizeof(servaddr));
- servaddr.sin_family = AF_INET;
- servaddr.sin_port = htons(SERV_PORT);
- // 将输入的点分十进制IPv4地址转换为网络字节地址
- inet_pton(AF_INET, argv[1], &servaddr.sin_addr);
- int ret = connect(sockfd, (SA*)&servaddr, sizeof(servaddr));
- if(ret < 0){
- print_error("connect fail");
- }
- // 客户端进程的主要方法
- str_cli(stdin, sockfd);
- exit(0);
- }
- void print_error(const char* err){
- int errno_save = errno;
- printf("%s : %s\n", err, strerror(errno_save));
- exit(1);
- }
- // 保证一次能写入n个字节,同时处理中断重入的情况
- ssize_t writen(int fd, const void* vptr, size_t n){
- size_t nleft;
- ssize_t nwritten;
- const char* ptr;
- ptr = vptr;
- nleft = n;
- while(nleft > 0){
- if((nwritten = write(fd, ptr, nleft)) <= 0){
- if(nwritten < 0 && errno == EINTR){
- nwritten = 0;
- }
- else{
- return -1;
- }
- }
- nleft -= nwritten;
- ptr += nwritten;
- }
- return n;
- }
- // 客户端逻辑的主要函数:从终端不断读取输入,然后发给服务端,最后客户端再从服务端读取回来,并在终端展示
- void str_cli(FILE* fp, int sockfd){
- char sendline[MAXLINE], recvline[MAXLINE];
- while(fgets(sendline, MAXLINE, fp) != NULL && sendline[0]){
- writen(sockfd, sendline, strlen(sendline));
- if(readline(sockfd, recvline, sizeof(recvline)) == 0){
- print_error("server call close first");
- }
- // 已经添加了0
- fputs(recvline, stdout);
- }
- }
- // 借助缓冲区减少使用read的次数,每次读取一个字符
- ssize_t my_read(int fd, char* ptr){
- // 静态变量,保证一直都存在
- static int read_cnt;
- static char* read_ptr;
- static char read_buf[MAXLINE];
- // 如果能读的字符已经读完,则重新读取
- if(read_cnt <= 0){
- again:
- if((read_cnt = read(fd, read_buf, sizeof(read_buf))) < 0){
- if(errno == EINTR){
- goto again;
- }
- return -1;
- }
- else if(read_cnt == 0){
- return 0;
- }
- read_ptr = read_buf;
- }
- // 读取一个字符,同时移动缓冲区的指针read_ptr
- read_cnt--;
- *ptr = *read_ptr++;
- return 1;
- }
- // 通过检查换行符读取一行,同时会添加0,这是C风格的字符串。如果没找到换行符,则以maxlen - 1个字符返回,同时包含一个0
- ssize_t readline(int fd, void *vptr, size_t maxlen){
- ssize_t n, rc;
- char c, *ptr;
- ptr = vptr;
- // 读取maxlen个字符,判断是否出现换行符,留出一个空间填零
- for(n = 1; n < maxlen; n++){
- if((rc = my_read(fd, &c)) == 1){
- *ptr++ = c;
- if(c == '\n'){
- n++;
- break;
- }
- }
- else if(rc == 0){
- *ptr = 0;
- // 没有读取到字符,服务端已经结束, EOF
- return n - 1;
- }
- else{
- return -1;
- }
- }
- // 读取到换行,或者读满maxlen - 1个字符,最后一个字符自己添加,且是0
- *ptr = 0;
- return n - 1;
- }
3. 测试
3.1 获取IP地址
运行环境是Ubuntu15.04。首先,通过运行:ifconfig,获得服务端的IP地址
显然轮回网卡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
从中可以看到服务进程的进程号为27209,已经在9877处于监听状态。*:9877中*表示任意接口地址,是上面设置了INADDR_ANY的结果。
同时,可以在运行服务端的终端下运行 : tty
从而获取到服务端运行的伪终端:
3.3 运行客户端程序
这里在跟服务端同一台机器(Ubuntu)上运行两个客户端程序:
./tcpcli01 192.168.1.100
./tcpcli01 127.0.0.1
3.4 获取进程启动时的相关信息和网络状态
netstat可以查看socket的网络连接状态。在另一个终端运行 :
netstat -a | grep 9877
ps -t /dev/pts/1 -o pid,ppid,tty,stat,args,wchan
注意:使用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:
这时输入结束,客户端进程将退出,从而发起四次挥手,结束数据交换和连接。最后客户进程exit终止,但主动结束连接者将进入TIME_WAIT状态,并且会停留2MSL的时间,这是客户进程无法看到的:
3.6 子服务进程进入僵尸状态
同时对应的子服务进程是被动关闭的一方,在收到最后一个ACK后连接就终止了。但是子服务进程却进入僵尸状态:
上述PID = 31715的子服务进程的状态是Z,即zombie,僵尸状态,进程表项一直没被回收。
3.7 终止父服务进程
终止了父服务进程,所有僵尸进程将被过继给init进程,被回收。