一. 服务器关闭与TCP异常


   1.1 服务器正常关闭

       “正常”关闭:调用close()关闭socket、没close但进程正常结束、进程core掉(进程由于内存越界等等而终止)、在shell命令行中kill掉进程,都可抽象成“正常”关闭。因为即使core掉,内核也会马上帮应用程序回收(close)socket文件描述符。
“正常”关闭,默认情况下(非默认即设置Linger,后续会介绍),关闭端即服务器端的TCP层会发FIN包,而对端即客户端的TCP层收到后,回ACK,服务器端进入FIN_WAIT2状态。此时,只有客户端socket“正常”关闭,才会发出这个FIN包。至此,服务器端进入TIME_WAIT状态。“正常”关闭:调用close()关闭socket、没close但进程正常结束、进程core掉(进程由于内存越界等等而终止)、在shell命令行中kill掉进程,都可抽象成“正常”关闭。因为即使core掉,内核也会马上帮应用程序回收(close)socket文件描述符。
   “正常”关闭,默认情况下(非默认即设置Linger,后续会介绍),关闭端即服务器端的TCP层会发FIN包,而对端即客户端的TCP层收到后,回ACK,服务器端进入FIN_WAIT2状态。此时,只有客户端socket“正常”关闭,才会发出这个FIN包。至此,服务器端进入TIME_WAIT状态。

     1.1.1 对FIN包处理

   如果服务器在“正常”关闭的情况下,客户端收到FIN包后,不做常规的处理(所谓常规处理,即关闭自己的套接字,向服务器发送FIN包),而是继续发送数据给服务器,会如何呢?比如客户端的程序如下(摘录自 《unix网络编程》 5.12节):

void str_cli(int sockfd)
    {
        char sendline[MAXLINE],recvline[MAXLINE];
        
        while(fgets(sendline, MAXLINE, stdin)!=NULL)  //从标准输入获得字符串
        {
            writen(sockfd, sendline, strlen(sendline)); //将字符串写入socket
            
            if(readline(sockfd, recvline, MAXLINE) == 0)    //从socket读取数据
            {
                perror("server terminated permaturely");
                eixt(0);
             }
            fputs(reavline, stdout);                     //将数据显示在标准输出
        }
    }<span style="font-family:Simsun;"><span style="background-color: rgb(255, 255, 255);"></span></span>


 a) 当服务器终止后,我们键入其他字符串,客户TCP将数据发送给服务器。让服务器TCP收到客户端的数据时,因为自己套接字所在进程已经终止,于是回应一个RST。


   b) 然而客户端进程看不到RST,由于writen之后立即调用readline,而之前已经收到服务器的FIN, 所以调用readline会立即返回0,从而显示出错信息(服务器过早终止)而退出,


   这个writen到 客户端退出的时间极短,所以基本看不到RST回应。


    

关闭服务器的容器中的防火墙_客户端


    1.1.2 不对FIN包处理 



    如果客户端不理会readline函数的返回错误,反而写入更多的数据到服务器上,那会发生什么呢?


    第一次writen时,服务器将返回RST,当客户端再次writen时,内核将向该进程发送一个SIGPIPE信号。该信号的默认行为是终止进程,因此该进程必须捕获它以免不情愿地被终止。 不论该进程是捕获了该进程并从其inhao处理函数返回,还是简单的忽略该信号,写操作都会返回EPIPE错误。
    客户端的程序修改如下(摘录自 《unix网络编程》 5.13节):

void str_cli(int sockfd)
    {
        char sendline[MAXLINE],recvline[MAXLINE];
        
        while(fgets(sendline, MAXLINE, stdin)!=NULL)  //从标准输入获得字符串
        {
            writen(sockfd, sendline, 1));             //写入一个字符,引发一个RST
            sleep(1);
            writen(sockfd, sendline, strlen(sendline)); //剩余字符写入socket,产生SIGPIPE信号
            
            if(readline(sockfd, recvline, MAXLINE) == 0)  //从socket读取数据
            {
                perror("server terminated permaturely");
                eixt(0);
             }
            fputs(reavline, stdout);                     //将数据显示在标准输出
        }
    }


    上述代码片,调用了两次writen:一次把数据的第一个字节写入套接字,目的是引发一个RST;第二次把剩余字节写入套接字,此时操作系统将产生SIGPIPE。

   

关闭服务器的容器中的防火墙_客户端_02





 1.2 服务器崩溃(非正常关闭)


“非”正常关闭:服务器崩溃了(或者网络上断开服务器主机),此时肯定发不出FIN包了(当然啦,内核都没机会帮应用程序回收资源了)。这种情况,服务器端有如下两种情况:



  •  客户端发送的数据,应为服务器已经崩溃了,客户端会不断的重传数据包,试图从服务器上接收一个ACK。Berkeley的实现重传数据包为8次,相对第一次传的15分钟后仍然没有收到ACK,则返回ETIMEDOUT(超时)或EHOSTUNREAC(网络不可达)错误。
  •  上述方式需要客户端主动发数据来检测服务器主机的崩溃。如果不主动向它发数据,依靠应用层的心跳机制或者keepalive,也能发觉TCP断链。


 1.3 服务器崩溃后重启


       正如前一节所讲的,如果客户在服务器主机崩溃时不主动发数据给服务器,那么客户是不知道服务器已经崩溃的(假设这里不用套接字选项SO_KEEPALIVE)。该种情况下TCP的流程如下:

     a). 启动服务器和客户,保证两者间连接的确立;

     b). 服务器主机崩溃并重启;

     c). 客户端发送数据给服务器;

     d). 当服务器主机崩溃后重启时,它的TCP丢失了崩溃前的所有连接信息,所以服务器TCP对接收的客户数据包以RST响应。

     e). 当RST到达时,客户端阻塞于send调用,导致它返回ECONNRST (连接重启)错误。

  如果服务器不是关闭后重启,而是服务器端将网线拔除后,重新连接上,此时服务器上还存在连接信息,重新连接上后两端通信正常。


    


   二. RST出现情况

   2.1 FIN与RST区别

          FIN是正常关闭,它会根据缓冲区的顺序来发的,即缓冲区FIN之前的包都发出去后再发送FIN包,这与RST不同。


          RST表示复位,用来异常的关闭连接,就像上面所说,发送RST包关闭时不必等缓冲区的包都发出去,直接就求其缓冲过去而发送RST包,而接收端收到RST包后,也不必发送ACK包来确认。


   2.2 RST出现情况分析


        1. 端口未打开

        服务器程序端口未打开而客户端来连接。telnet连接一个未打开的TCP的端口可能会出现这种错误。这个和操作系统的实现有关。在某些情况下,操作系统也会完全不理会这些发到未打开端口请求。

       2. 请求超时

       在建立了socket之后,用setsockopt的SO_RCVTIMEO选项设置了recv的超时时间。如果接收数据超时了。会发送RST包表示拒绝进一步发送数据。

如果双方连接未完全建立,此时客户端发送一个RST,将会导致accept返回一个错误。该情况的模拟情形:启动服务器,调用sokcet,bind和listen,然后在调用accept之前睡眠一段时间;启动客户端,调用socket和connect,一旦connect返回,就设置SO_LINGER套接字选项以产生这个RST(下篇讲解),然后终止。


 

关闭服务器的容器中的防火墙_服务器_03


      这回导致accept返回一个非致命的错误。对于中止的连接处理,不同协议处理方式不同。源自Berkeley的实现完全在内核中处理,服务器进程根本看不到。SVR4实现返回一个错误给服务器进程,作为accept的返回结果,而POSIX指出返回的errno值必须使ECONNABORTED。服务器遇到该错误时,可以忽略它,然后再次调用accept就行。

    3. 在一个已经关闭的socket上收到数据

    以下通过实例代码分析该情况(代码来源于网络博文《几种TCP连接中出现RST的情况》)

    服务器端程序如下:



int main(int argc, char** argv)
 {
     int listen_fd, real_fd;
     struct sockaddr_in listen_addr, client_addr;
     socklen_t len = sizeof(struct sockaddr_in);
     listen_fd = socket(AF_INET, SOCK_STREAM, 0);
     if(listen_fd == -1)
     {
         perror("socket failed ");
         return -1;
     }
     bzero(&listen_addr,sizeof(listen_addr));
     listen_addr.sin_family = AF_INET;
     listen_addr.sin_addr.s_addr = htonl(INADDR_ANY);
     listen_addr.sin_port = htons(SERV_PORT);
     bind(listen_fd,(struct sockaddr *)&listen_addr, len);
     listen(listen_fd, WAIT_COUNT);
     while(1)
     {
         real_fd = accept(listen_fd, (struct sockaddr*)&client_addr, &len);
         if(real_fd == -1)
         {
            perror("accpet fail ");
             return -1;
         }
         if(fork() == 0)  //产生子进程处理连接
         {
             close(listen_fd);
             char pcContent[4096];    //客户端发送5000个字节,接收端接收4096个
             read(real_fd,pcContent,4096);
             close(real_fd);                //立即关闭连接套接字
             exit(0);
         }
         close(real_fd);
     }
     return 0;
 }


    客户端程序如下:



#include <sys/socket.h>
 #include <arpa/inet.h>
 #include <fcntl.h>
 #include <sys/syscall.h>
 #include <unistd.h>
 #include <strings.h>
 #include <stdlib.h>
 #include <stdio.h>
 int main(int argc, char** argv)
 {
     int send_sk;
     struct sockaddr_in s_addr;
     socklen_t len = sizeof(s_addr);
     send_sk = socket(AF_INET, SOCK_STREAM, 0);
     if(send_sk == -1)
     {
         printf("socket failed ");
         return -1;
     }
     bzero(&s_addr, sizeof(s_addr));
     s_addr.sin_family = AF_INET;

     inet_pton(AF_INET,"172.16.130.240",&s_addr.sin_addr);
     s_addr.sin_port = htons(8000);
     if(connect(send_sk,(struct sockaddr*)&s_addr,len) == -1)
     {
         printf("connect fail ");
         return -1;
     }
     char pcContent[5000]={0};
     write(send_sk,pcContent,5000); 
     sleep(1);
     close(send_sk);
 }




   客户端在服务端已经关闭掉socket之后,仍然在发送数据(数据是分组发送的)。这时服务端会回应RST(内核也会向客户进程发送SIGPIPE信号)。下图是笔者通过tcpdump抓到的数据包:

  


关闭服务器的容器中的防火墙_服务器_04


  其中,包所携带的标志如下:


   S=SYN 发起连接标志


   P=PUSH 传送数据标志


   F=FIN 关闭连接标志


   ack 表示确认包


   RST 异常关闭连接


   . 表示没有任何标志



  其中,前三个信令交互为TCP连接的三次握手, 后续为数据的交互以及FIN、RST的交互。具体交互流程可见下图所示:


 

关闭服务器的容器中的防火墙_服务器_05