红联Linux门户
Linux帮助

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

发布时间:2017-02-20 10:13:10来源:linux网站作者:ac_dao_di
本案例将用多进程实现一个基于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地址
用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++实现一个多进程回显服务器
注意:使用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终止父服务进程,之后原来的僵尸进程都消失了。
 
本文永久更新地址:http://www.linuxdiyf.com/linux/28550.html