先看一段代码,实现的是echo回射功能,我们从这个程序中,来模拟服务器进程先于客户进程终止后,它会发生哪些问题。。
#include "unp.h"
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_port=htosn(SERV_PORT);
servaddr.sin_addr.s_addr=htonl(INADDR_ANY);
Bind(listenfd,(SA*)&servaddr,sizeof(servaddr));
Listen(listenfd,LISTENQ);
for(;;)
{
clilen=sizeof(cliaddr);
connfd=Accept(listenfd,(SA*)&cliaddr,&clilen);
if((childpi=fork())==0)
{
Close(listenfd);
str_echo(connfd);
exit(0);
}
Close(connfd);
}
}
void str_echo(int connfd)
{
ssize_t n;
char buf[MAXLINE];
again:
while((n=read(connfd,buf,MAXLINE))>0)
Write(connfd,buf,n);
if(n<0&&errno==EINTR)
goto again; // 这里EINTR表示是系统调用中断,若是这种情况
// 则重新调用系统调用。
//
else if(n<0)
err_sys("str_echo:read error");
}
//
//client.c
int main(int argc,char**argv)
{
int sockfd;
struct sockaddr_in servaddr;
if(argc!=2)
err_quit("usage:tcpcli<IPaddress>");
sockfd=Socket(AF_IENT,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);
}
void str_cli(FILE*fp,int sockfd)
{
char sendline[MAXLINE],recvline[MAXLINE];
while(Fgets(sendline,MAXLIEN,fp)!=NULL)
{
Write(sockfd,sendline,strlen(sendline));
if(Read(sockfd,recvline,MAXLINE)==0)
err_quit("str_cli:server terimnate prematily");
Fputs(recvline,stdout);
}
}
现在启动我们的客户/服务器对,一切正常,回射功能实现完好。
接下来,我们先杀死服务器子进程。这是在模拟服务器进程崩溃的情形,我们可查看客户将发生什么,(要区分服务器崩溃和服务器进程崩溃的情形)
1,执行kill命令,作为进程终止处理的部分工作,子进程中的所有打开的进程描述符都被关闭,这就导致向客户发送一个FIN,而客户TCP则响应一个ACK(这是TCP的自动实现机制,并不需要用户做什么)
(这里要注意用词, 区别客户进程,客户TCP ,服务器进程,服务器TCP),这就是TCP连接终止的前半部分。
2,SIGCHILD信号被发送个服务器父进程,并得到正确处理。
3,这时候的状态是,FIN已经到了客户的TCP,即到了接受缓冲区了,但是还没有到用户缓冲区,客户程序没有读取,而服务器端TCP已经接受了ACK,进入了FIN_WAIT2
状态了。 且,因为客户TCP接受到了FIN分节,故它进入了CLOSE_WAIT状态。 可以用netstat查看。
4, 然而,问题在于,这个FIN分节并没有被客户进程读取,它还存在与接受缓冲区,这时客户进程阻塞在fgets上面,等待用户从终端输入。
当我们键入 “另外一行” ,这时候客户进程调用write,紧接着,客户TCP把数据发送出去。(TCP允许这么做,因为客户TCP受到FIN,只是表示服务器进程已经关闭了连接的
服务器端,从而不再向其中发送数据而已,但还可有接受数据,FIN的接受并没有告知客户TCP,服务器进程,(注意用词,TCP和进程)已经终止了,不过它在本例中确实终止了,因为我们已经kill了。)
5,当服务器TCP接受到客户的数据时候,既然先前打开的那个套接字的进程已经终止了,于是,服务器TCP会响应一个RST。告知对端,端口已经关闭。(这时候服务器子进程已经终止,是内核中的TCP协议来做的这件事)。
5,然而,客户进程确看不见这个RST,原因在于:因为它调用write后,立即会调用read从接受缓冲区中接受数据,我们还记得,在接受缓冲区中存在一个分节,即
还没有别客户进程读取的服务器TCP发送的FIN分节,那么这时候调用read,它就会读到这个FIN,然后,read理解返回0,于是打印“server terminated prematruely”,
6,自此,客户终止进程了(调用err_quit),所有打开的描述符都关闭了 。这个 客户进程永远也读不到这个RST分节。
本例子的问题在于:当FIN达到套接字缓冲区时候,客户正阻塞在fgets调用。客户实际上在应对两个描述符----套接字和用户输入。事实上,这正视select和poll两个函数的目的之一。不然进程阻塞在某个特定的输入上。 一旦服务器杀死子进程,客户就会立刻从缓冲区读出FIN。
从这个例子也很好地反映了 TCP进程和TCP 的一些关联与区别。 以及 描述符与流的关系 缓冲区 的关系。