UNP读书笔记 第五章 回射程序的各种改进

本章首先编写了1个服务器回射程序,1个客户发送程序。随后根据各类问题,进行相应的改进。这里记录一下 改进的过程。

服务器程序很简单, 大概是以下几步:

 创建套接字->绑定地址结构->监听

 进入while循环, 做accpet等待连接建立->一旦连接建立成功, 返回一个connecfd套接字,  并fork一个子进程 

 父进程关闭connecfd套接字,继续进入accpet等待连接

 子进程执行回射函数-> 回射函数是一个while循环,不断从客户套接字中read对应的数据,并发送回去。 当read返回<=0即输入了EOF或read出错时, 退出循环,输出报错信息

#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");
	}
}

int main(int argc, char **argv){
	int listenfd, connfd;
	pid_t childpid;
	socklen_t clilen;
	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);
	
	for( ; ; ){
		clilen = sizeof(cliaddr);   // 监听
		connfd = Accept(listenfd, (SA *) &cliaddr, &clilen); //如果返回,说明建立了一个连接
		 //fork,父进程关闭该连接描述符,并继续监听和接受新连接
		 //子进程关闭监听描述符,并进行连接的处理。
		if( (childpid =Fork()) ==0){  
			Close(listenfd);
			str_echo(connfd);
			exit(0);
		}
		Close(connfd);
	}
}

客户程序:

 创建套接字->与要发往的服务器地址进行connect

当connect返回时,说明连接建立成功,执行发送函数

发送函数是1个while循环。每次在标准输入上fgets读取一行,然后发送给服务器,

发送之后readline服务器上发回的数据,并输出。

如果readline返回错误,则报错。

#include "unp.h"
#include <time.h>


void str_cli(FILE *fp, int sockfd){
	char sendline[MAXLINE], recvline[MAXLINE];
	while(Fgets(sendline, MAXLINE, fp) != NULL){  //读取终端上输入的数据
		Writen(sockfd, sendline,strlen(sendline));
		if(Readline(sockfd, recvline, MAXLINE) == 0) //读取服务器发回的数据	
			err_quit("str_cli:server terminated prematurely");
		Fputs(recvline, stdout); //把服务器发回的数据发送到终端上
	}
}

int main(int argc, char **argv){
	int sockfd,i;
	struct sockaddr_in servaddr;
	if (argc != 2){
		err_quit("usage: tcpli<IPaddress>");
	}
	sockfd = Socket(AF_INET, SOCK_STREAM, 0);
	bzero(&servaddr, sizeof(servaddr));
	servaddr.sin_family = AF_INET;
	servaddr.sin_port = htons(SERV_PORT);
	Inet_pton(AF_INET, argv[1],  &servaddr.sin_addr);
	Connect(sockfd, (SA *) &servaddr, sizeof(servaddr));
	str_cli(stdin, sockfd);
	exit(0);
}


正常终止:

 当我们在客户进程上输入EOF关闭(ctrl +d)时,Fgets读入空指针->退出while循环->退出函数->程序执行完毕,进程关闭

->进程关闭时,自动关闭打开的所有套接字,于是客户TCP发送FIN给服务器,进行四次挥手。

注意:连接中止后,通过netstat可以查看到客户套接字此时正处于TIMEWAIT状态,大概好几秒之后才真正关闭。

存在的问题:服务器子进程因为readline收到FIN而退出,结束,但是父进程并没有处理子进程结束的操作

所以,子进程变成了僵死进程,并没有真正的被清除,其空间和副本被保留,如果忽略该处理,内存会被耗尽。


改进A——处理僵死进程:

在服务器进程开启监听之后, 加入Signal(SIGCHLD,sig_chld),即进程开启对“子进程终止”信号的捕捉

一旦捕捉到,则执行sig_chld函数

sig_chld函数: 该函数中执行了wait函数,等待1个子进程返回,并回收对应状态信息。

注意1:这个信号的捕捉一定是父进程才能捕捉得到。

 注意2:当该信号捕捉到时,父进程正阻塞于accept上,此时执行了信号中断,于是accpet出错

返回的错误是“中断错误”

 所以服务器代码中,在accpet返回的是错误且错误是中断错误时, 要继续执行while循环做accpet

而不是直接结束整个进程

 存在的问题:当有多个子进程同时终止时,会在同一时间内产生多个SIGCHLD信号, 导致wait的接受存在不确定性


 改进B——处理多个子进程终止的情况

 在sig_chld信号处理函数中, 加入while循环,并执行不阻塞的waitpid,用循环依次将死掉的子进程一个个取出。

#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");
	}
	
}

void sig_chld(int signo){
	pid_t pid;
	int stat;
	while( (pid = waitpid(-1,&stat,WNOHANG)) > 0)   //非阻塞的waitpid
		printf("child %d terminated\n", pid);
	return ;
}


int main(int argc, char **argv){
	int listenfd, connfd;
	pid_t childpid;
	socklen_t clilen;
	struct sockaddr_in cliaddr, servaddr;
	listenfd = Socket(AF_INET, SOCK_STREAM, 0);
	Signal(SIGCHLD, sig_chld); //设置“子进程退出信号”处理函数
	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);
	
	for( ; ; ){
		clilen = sizeof(cliaddr);
		if( (connfd = accept(listenfd, (SA *) &cliaddr, &clilen)) < 0){
			if(errno == EINTR)
				continue;
			else err_sys("accept error");
		}
		
		if( (childpid = Fork()) ==0){
			Close(listenfd);
			str_echo(connfd);
			exit(0);
		}
		Close(connfd);
	}
}


存在的问题: 如果突然kill掉服务器子进程, 虽然服务器作为结束发起方,发送了FIN给客户,客户也回应了ACK

但根据四次挥手的协议,客户要发完要发的数据,才会发送FIN

这时候客户阻塞在 读取终端输入上。只有当我们输入字符串,让客户端端发送时,客户才会收到服务器发回的RST

然后才执行FIN发送。

这个问题应该由select和poll解决


服务器的主机崩溃(类似于网络断开,不是进程关闭)

此时客户没有收到FIN等信息,于是会不断进行重传,直到超时,相应目的地不可达。


服务器主机崩溃后又重启

此时进程已经在崩溃后不存在了,但是并没有发送FIN等操作, 但主机上的TCP仍会收到客户发来的TCP

此时会自动相应一个RST


服务器关机

关机与崩溃不同,关机时,会用init进程来关闭所有进程,并关闭所有描述符,使得客户能够检测到。


发送二进制数据

可能会因为大小端不同,出现问题。


  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值