⟅UNIX网络编程⟆⦔TCP客户/服务器程序实例(三)

说在前面

服务器进程终止

  • 模拟方式
    正常启动服务端、客户端,验证双方功能正常后杀死服务器子进程。
  • 详细过程
  1. 在同一个主机上同时启动服务端以及客户端,并在客户端键入一行文本,验证双方一切正常。(该服务器与客户端功能见⟅UNIX网络编程⟆⦔TCP客户/服务器程序实例(一)

  2. 找到服务器子进程的进程ID,并使用kill命令杀死该进程。作为进程终止处理的部分工作,子进程所有打开的描述符都被关闭。这就导致服务端TCP向客户发送一个FIN,客户TCP则响应一个ACK。
    在这里插入图片描述

    (第一行为主进程,第二行为子进程)

  3. 子进程终止,SIGCHLD信号被发送给服务器主(父)进程,正常回收。
    在这里插入图片描述

  4. 客户端上没有任何特殊事件发生。客户TCP接收来自服务器TCP的FIN并响应一个ACK;但是此时客户进程阻塞在fgets调用上,等待标准输入。

  5. 此时,在另一个窗口上运行netstat命令,观察套接字的状态。
    在这里插入图片描述
    第二行为服务端 (主动关闭,进入FIN_WAIT状态) ,第三行为客户端 (被动关闭,进入CLOSE_WAIT状态) 。

  6. 在客户端上键入一行文本,结果如下
    在这里插入图片描述

    (输入bye前将服务器进程终止)
    客户端再次键入数据后,str_cli调用writen,客户TCP接着将数据发送给服务器(客户TCP接收到FIN只是表示服务器进程已关闭了连接的服务器端,服务器进程不再往其中发送数据。FIN的接收并没有告知客户TCP服务器进程已经终止,虽然本例中服务器子进程已经终止)。 当服务器TCP接收到来自客户的数据时,由于先前打开对应套接字的进程已经终止,于是服务器TCP响应一个RST。 可以使用tcpdump命令抓取该RST包。
  7. 但是这个RST客户端进程看不到。
    见如下客户端代码,客户进程在调用writen后立即调用readline函数,但是由于客户端在第2步收到的FIN,客户端已经了解了服务端连接已经关闭,所以readline立即返回0(表示EOF)。这个EOF导致执行err_quit函数,输出"server terminated prematurely"(服务器过早终止)并退出。

    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);
        }
    }
    
  8. 当客户端进程终止后,进程所有打开着的描述符被关闭。

  • 代码

  • SIGPIPE信号
    若客户端不理会readline函数返回的错误,反而向服务器写入更多的数据,多次写操作中的第一次写操作引发RST,接着内核会向进程发送一个SIGPIPE信号。

    当一个进程向某个已经收到RST的套接字执行写操作时,内核向进程发送一个SIGPIPE信号。该信号的默认行为是终止进程,因此进程必须捕获它以免意外终止。

服务器主机崩溃

  • 模拟方式
    分别在两个主机上运行客户端、服务端,验证双方功能正常后断开服务端网络。
    (这里实验的设备为一台pc_客户端以及一台树莓派_服务端)
  • 详细过程
  1. 当服务器主机崩溃时,已有的网络连接上发不出任何数据。(服务器崩溃与服务器关机有一定区别)

  2. 在两个主机上运行客户端、服务端。在客户端键入一行文本,验证双方一切正常。(该服务器与客户端功能见⟅UNIX网络编程⟆⦔TCP客户/服务器程序实例(一)
    在这里插入图片描述

  3. 断开服务器的网络连接,直接拔网线或者关wifi

  4. 在客户端上键入一行文本,由writen写入内核,再由客户TCP作为数据分节发送出去。此时客户进程阻塞于readline调用,等待服务器响应。

  5. 此时使用tcpdump抓包,可以看到客户TCP持续重发数据分节,试图从TCP服务器接收一个ACK (源自Berkeley的TCP实现会重传数据分节12次,共等待约9分钟后终止,下图显示的结果与上述说明不同,可能实现不同)。

    tcpdump -nn -i wlp2s0 -w data.txt
    # nn 表示显示的数据格式
    # -i 网卡设备名 这里是wlp2s0 可以使用ifconfig命令查看
    # -w data.txt 表示写入到文件data.txt
    
    tcpdump -r data.txt
    # 解析并显示data.txt文件中的数据包
    

    在这里插入图片描述
    (可以看到在断开服务器wifi后pc主机不断重发,最后是ARP信息)
    当客户TCP最终放弃 (在这段时间内服务器并未重启或者服务器依旧不可达),给客户进程返回一个错误。由于此时客户进程阻塞在readline调用,该函数也返回一个错误。
    若服务器崩溃,对客户的数据分节没有响应,那么该错误为ETIMEOUT若某个中间路由器判定服务器不可达,继而响应一个"desitination unreachable"的ICMP消息,那么该错误为EHOSTUNREACHENETUNREACH
    尽管客户端最终还是会发现服务器崩溃或者不可达,但有的时候需要在更快的时间内确定,这时我们可以对readline设置一个超时。

服务器主机崩溃后重启

  • 模拟方式
    分别在两个主机上运行客户端、服务端,验证双方功能正常后断开服务端网络,服务端主机关机;之后重启服务端主机并连接网络。
  • 详细过程
  1. 在不同主机分别运行客户端以及服务端,在客户端键入一行文本验证双方功能完整。
  2. 服务端主机断开网络、重启、连接网络(模拟崩溃重启)。
  3. 在客户端再次键入一行文本,它将作为一个数据分节被发送到服务器。
  4. 服务器主机崩溃重启后,其TCP丢失了崩溃前的所有连接信息,故而TCP对所有到达的数据分节响应一个RST。
  5. 当客户TCP收到RST时,客户正阻塞于readline调用,导致该调用返回ECONNRESET错误。
    若对于客户端来说检测服务器是否崩溃十分重要,那么要求客户端不主动发送数据也能检测出来,因此需要一些其他手段(例如SO_KEEPALIVE选项或心跳函数)

服务器主机关机

  • 模拟方式
    关机即可。
  • 详细过程
    UNIX系统关机时,init进程通常会给所有进程先发送SIGTERM信号 (该信号可以被捕获) ,等待一段固定的时间 (通常为5s-20s) ,然后给所有仍在运行的进程发送SIGKILL信号 (该信号不可被捕获) 。
    若不捕获SIGTERM信号,服务器进程将被SIGKILL终止,该过程与 服务器进程终止 相同。

发布了106 篇原创文章 · 获赞 41 · 访问量 4万+

猜你喜欢

转载自blog.csdn.net/qq_33446100/article/details/104056020