本章首先编写了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进程来关闭所有进程,并关闭所有描述符,使得客户能够检测到。
发送二进制数据
可能会因为大小端不同,出现问题。