先描述一下整体的流程及思路:
客户端从标准输入读取一行文本,发送给服务器,服务器收到文本后,将文本直接返回给客户端,即回显。整体采用TCP协议完成。
客户端大致代码:
socket,connect函数略去
char sendline[1024],recvline[1024];
while( fgets(sendline, 1024, stdin) != NULL){ //从标准输入读取
writen(sockfd,sendline,strlen(sendline)); //发送给服务器,Sockfd就是与服务器联通的Socket
if(readline(sockfd, recvline, 1024) == 0) //从服务器接收
err_quit("Server terminated!");
fputs(recvline, stdout); //显示在屏幕上
}
服务器端大致代码:
socket,bind,listen函数略去
while(1){
socklen_t clilen = sizeof(cliaddr);
connfd = Accept(listenfd, (const SocketAddr_in *)&cliaddr, &clilen);
if((childpid = fork()) == 0){ //服务器端采用多进程来处理每一个客户连接
Close(listenfd); //对于子进程来说,Listenfd无用,可以直接关闭
str_server(connfd); //处理客户请求,详见下面
exit(0);
}
close(connfd);
}
void str_server(int sockfd){
ssize_t n;
char buf[1024];
again:
while( (n = read(sockfd, buf, 1024)) > 0)
writen(sockfd, buf, n);
if( n < 0 && errno == EINTR) //之后会说明为什么在这里要处理EINTR
goto again;
else if( n < 0)
err_sys("read error");
}
一、 服务器端用于与客户端通信的子进程终止
可以启动客户/服务器,然后使用Kill命令杀死服务器子进程。这时根据TCP连接中止的四步(不明白的先百度一下,稍候的博客中我会对TCP四步断开连接进行详细讲述),子进程所有打开的描述符都被关闭,这就导致向客户发送一个FIN,同时客户TCP则响应一个ACK。对于服务器端,SIGCHLD信号会发送到服务器父进程中,由于代码中并未写处理SIGCHLD的功能,所以默认被忽略掉。
这里要注意,客户端并未发生任何特殊之事,它并没有立刻得到服务器子进程崩溃的消息,而是阻塞在fgets(sendline, 1024, stdin)系统调用上,这时我们在客户端继续键入文本并发送给服务器,由于处理与客户通信的子进程已经关闭,所以会响应一个RST。但是客户却看不到这个RST,因为它在调用writen后立即调用readline,由于之前已经收到FIN,所以readline立即返回0,显示server terminate.也就是说,客户端并未获得RST所告知的错误信息。另外,大家注意到,在服务器子进程终止时,客户端并未立刻得到通知,因为它阻塞在Fgets调用上,虽然客户端已经收到FIN。
如果服务器子进程终止,客户端不通过发送数据也能知道的话,就必须不能让自己阻塞在Fgets上,这里需要用到I/O多路复用的技术,即select和poll函数。
二、通信过程中网络断开,或者说服务器主机崩溃
可以在通信过程中拔下网线。。。。和(一)中所讲述的一样,当客户端发送给服务器数据时,由于网络已经断开,因此writen(其实与send相似)发给服务器数据后,客户TCP会持续重传数据分节,以试图从服务器获得一个ACK,但是,源自Berkeley的实现重传大约等待9分钟才放弃重传,9分钟后才给客户进程返回一个错误,多数是ETIMEDOUT,即超时错误。而在实际应用中,9分钟显然太长了,因此需要对readline调用设置一个超时。
但问题仍然存在,因为我们仍然向服务器发送了数据后才检测出服务器主机崩溃或者网络断开了,如果在网络断开时客户就想知道,需要使用SO_KEEPALIVE套接字选项,即一直保持联通,当断开网络时,他会导致客户端直接返回错误信号,从而捕获它并进行处理。
三、通信过程中服务器主机崩溃,但之后重启
这与(二)所不同的地方是:当服务器重启后,网络仍然是联通的,只是与客户通信的进程消失了。这样以来,当重启后的服务器收到客户消息后,由于之前的通信进程已经终止,所以会响应一个RST给客户端,并导致客户端readline返回ECONNREST错误。假如一个应用对于检测服务器是否崩溃很重要,即和(二)所说一样,也就需要SO_KEEPALIVE套接字选项,另外一种技术是采用客户/服务器心搏函数,这个我不太了解。
四、通信过程中服务器关机
系统关机时,init进程通常先给所有进程发送SIGTERM信号,各进程需要捕获该信号并做好善后工作。等待一段时间后(5~20秒,这也就是为什么当我们关机时显示正在关机中的原因),再发送SIGKILL信号给所有进程。这时服务器进程终止。同(一)中所述一样,客户端也需要再发数据才能得到该信息。同样,客户端需要IO多路复用才能获得服务器的这种状态。
五、 阻塞在慢系统调用上时捕获信号,也就是处理被中断的系统调用
所谓慢系统调用,我们可以用Accept函数来描述它,当服务器端Listen后,就会阻塞在Accept函数上等待到来的客户,当三次握手后,Accept返回。此处需要注意的时,有可能一直没有客户连接,Accept有可能永远阻塞下去,永远无法返回,这类系统调用就可以认为是慢系统调用。包括fgets, readline等。
现有如此场景:有5个客户端与服务器保持连接,也就是服务器端有父进程+5个子进程,现在5个子进程同时终止,显然服务器端的5个子进程会终止返回,发送SIGCHLD信号给父进程,此时父进程阻塞在Accept慢系统调用上,这时内核会导致Accept返回一个EINTR错误(被中断的系统能够调用),如果没有if( n < 0 && errno == EINTR),则服务器会直接终止,当然,此处是将EINTR写在了服务器子进程中,更常用的方法是将if( errno == EINTR)的处理写在Accept函数之后。也就是说,不能讲EINTR视为一个硬错误,他只是阻塞在慢系统调用上的一个中断,我们需要捕获它并Continue我们的程序。
综述:
在本篇文章中,我没有写具体的代码,只是讲述了大概的工作及通信思路,只有这些思路原理都明白了,看他人的代码或者自己写才能有方案。
对于文章中所讲述的IO多路复用、TCP断开连接的详细过程,有时间的话在之后的博客中会进行介绍。。。 |