• 网络编程:TCP故障模式


    故障模式总结

    异常情况可归结为两大类:

    第一类,是对端无FIN包发送出来的情况;第二类是对端有FIN包发出来

    对端无FIN包发送出

    • 网络终端造成对端无FIN包

    很多原因都会造成网络中断,这种情况,TCP程序并不能及时感知异常信息。除非网络中的其他设备,如路由器发送出一条ICMP报文,说明目的网络或主机不可达,此时,通过read或write调用就会返回unreachable的错误

    在没有ICMP报文的情况下,TCP程序并不能感应到连接异常。如果程序是阻塞在read调用上,程序无法从异常中恢复,可以通过read操作设置超时来解决

    如果程序先调用了write操作发送了一段数据流,接下来阻塞在read调用上,结果又是另一种情况。Linux系统的TCP协议栈会不断尝试将发送缓冲区的数据发送出去,大概在重传12次,合计时间约9分钟之后,协议栈会标识连接异常,此时,阻塞的read调用会返回一个TIMEOUT的错误信息,如果程序还继续往这条连接上写数据,写操作会立即失败,返回一个SIGPIPE信号给应用程序

    • 系统崩溃造成的对端无FIN包

    当系统突然奔溃,如断电,网络连接来不及发出任何东西,和通过应用调用杀死应用程序非常不同的是,没有任何FIN包被发送出来。

    这种情况和网络中端造成的结果非常类似,在没有ICMP报文的情况下,TCP程序只能通过read和write调用的到网络连接异常的消息,超时错误是一种常见的结果。

    不过还有一种情况需要考虑,那就是系统在崩溃之后又重启,当重传的 TCP 分组到达重启后的系统,由于系统中没有该 TCP 分组对应的连接数据,系统会返回一个 RST 重置分节,TCP 程序通过 read 或 write 调用可以分别对 RST 进行错误处理。如果是阻塞的 read 调用,会立即返回一个错误,错误信息为连接重置(Connection Reset)。如果是一次 write 操作,也会立即失败,应用程序会被返回一个 SIGPIPE 信号。

    对端有FIN包发出

    对端如果有 FIN 包发出,可能的场景是对端调用了** close 或 shutdown** 显式地关闭了连接,也可能是对端应用程序崩溃,操作系统内核代为清理所发出的。从应用程序角度上看,无法区分是哪种情形。

    阻塞的 read 操作在完成正常接收的数据读取之后,FIN 包会通过返回一个 EOF 来完成通知,此时,read 调用返回值为 0。这里强调一点,收到 FIN 包之后 read 操作不会立即返回。你可以这样理解,收到 FIN 包相当于往接收缓冲区里放置了一个 EOF 符号,之前已经在接收缓冲区的有效数据不会受到影响。

    服务端程序:

    
    //服务端程序
    int main(int argc, char **argv) {
        int connfd;
        char buf[1024];
    
        connfd = tcp_server(SERV_PORT);
    
        for (;;) {
            int n = read(connfd, buf, 1024);
            if (n < 0) {
                error(1, errno, "error read");
            } else if (n == 0) {
                error(1, 0, "client closed \n");
            }
    
            sleep(5);
    
            int write_nc = send(connfd, buf, n, 0);
            printf("send bytes: %zu \n", write_nc);
            if (write_nc < 0) {
                error(1, errno, "error write");
            }
        }
    
        exit(0);
    }
    

    服务端程序是一个简单的应答程序,在收到数据流之后回显给客户端,在此之前,休眠 5 秒,以便完成后面的实验验证。

    客户端程序从标准输入读入,将读入的字符串传输给服务器端:
    客户端程序:

    
    //客户端程序
    int main(int argc, char **argv) {
        if (argc != 2) {
            error(1, 0, "usage: reliable_client01 <IPaddress>");
        }
    
        int socket_fd = tcp_client(argv[1], SERV_PORT);
        char buf[128];
        int len;
        int rc;
    
        while (fgets(buf, sizeof(buf), stdin) != NULL) {
            len = strlen(buf);
            rc = send(socket_fd, buf, len, 0);
            if (rc < 0)
                error(1, errno, "write failed");
            rc = read(socket_fd, buf, sizeof(buf));
            if (rc < 0)
                error(1, errno, "read failed");
            else if (rc == 0)
                error(1, 0, "peer connection closed\n");
            else
                fputs(buf, stdout);
        }
        exit(0);
    }
    
    • read直接感知FIN包
      依次启动服务器端和客户端程序,在客户端输入 good 字符之后,迅速结束掉服务器端程序,这里需要赶在服务器端从睡眠中苏醒之前杀死服务器程序。

    屏幕上打印出:peer connection closed。客户端程序正常退出

    $./reliable_client01 127.0.0.1
    $ good
    $ peer connection closed
    

    这说明客户端程序通过 read 调用,感知到了服务端发送的 FIN 包,于是正常退出了客户端程序。

    • 通过 write 产生 RST,read 调用感知 RST
      们仍然依次启动服务器端和客户端程序,在客户端输入 bad 字符之后,等待一段时间,直到客户端正确显示了服务端的回应“bad”字符之后,再杀死服务器程序。客户端再次输入 bad2,这时屏幕上打印出”peer connection closed“。
    $./reliable_client01 127.0.0.1
    $bad
    $bad
    $bad2
    $peer connection closed
    


    在很多书籍和文章中,对这个程序的解读是,收到 FIN 包的客户端继续合法地向服务器端发送数据,服务器端在无法定位该 TCP 连接信息的情况下,发送了 RST 信息,当程序调用 read 操作时,内核会将 RST 错误信息通知给应用程序。这是一个典型的 write 操作造成异常,再通过 read 操作来感知异常的样例。

    *向一个已关闭连接连续写,最终导致 SIGPIPE
    为模拟该过程,对服务端程序和客户端程序进行了修改:
    服务端:

    
    nt main(int argc, char **argv) {
        int connfd;
        char buf[1024];
        int time = 0;
    
        connfd = tcp_server(SERV_PORT);
    
        while (1) {
            int n = read(connfd, buf, 1024);
            if (n < 0) {
                error(1, errno, "error read");
            } else if (n == 0) {
                error(1, 0, "client closed \n");
            }
    
            time++;
            fprintf(stdout, "1K read for %d \n", time);
            usleep(1000);
        }
    
        exit(0);
    }
    

    服务器端每次读取 1K 数据后休眠 1 秒,以模拟处理数据的过程。

    客户端:

    
    int main(int argc, char **argv) {
        if (argc != 2) {
            error(1, 0, "usage: reliable_client02 <IPaddress>");
        }
    
        int socket_fd = tcp_client(argv[1], SERV_PORT);
    
        signal(SIGPIPE, SIG_IGN);
    
        char *msg = "network programming";
        ssize_t n_written;
    
        int count = 10000000;
        while (count > 0) {
            n_written = send(socket_fd, msg, strlen(msg), 0);
            fprintf(stdout, "send into buffer %ld \n", n_written);
            if (n_written <= 0) {
                error(1, errno, "send error");
                return -1;
            }
            count--;
        }
        return 0;
    }
    

    果在服务端读取数据并处理过程中,突然杀死服务器进程,我们会看到客户端很快也会退出,并在屏幕上打印出“Connection reset by peer”的提示。

    $./reliable_client02 127.0.0.1
    $send into buffer 5917291
    $send into buffer -1
    $send: Connection reset by peer
    

    这是因为服务端程序被杀死之后,操作系统内核会做一些清理的事情,为这个套接字发送一个 FIN 包,但是,客户端在收到 FIN 包之后,没有 read 操作,还是会继续往这个套接字写入数据。这是因为根据 TCP 协议,连接是双向的,收到对方的 FIN 包只意味着对方不会再发送任何消息。 在一个双方正常关闭的流程中,收到 FIN 包的一端将剩余数据发送给对面(通过一次或多次 write),然后关闭套接字。

    当数据到达服务器端时,操作系统内核发现这是一个指向关闭的套接字,会再次向客户端发送一个 RST 包,对于发送端而言如果此时再执行 write 操作,立即会返回一个 RST 错误信息。
    全过程的描述图:

    小结

    因为可能故障的存在,TCP并不是那么的“可靠”。故障分为两大类,一类是对端无 FIN 包,需要通过巡检或超时来发现;另一类是对端有 FIN 包发出,需要通过增强 read 或 write 操作的异常处理,帮助我们发现此类异常。

  • 相关阅读:
    Html 播放 mp4格式视频提示 没有发现支持的视频格式和mime类型
    如何防止网站短信验证码被攻击
    JS和C#.NET获取客户端IP
    H5案例分享:移动端touch事件判断滑屏手势的方向
    防止asp.net连续点击按钮重复提交
    JS正则表达式验证手机号和邮箱
    sql server查询数据库的大小和各数据表的大小
    大型分布式网站架构技术总结
    一个高逼格开发者必须理解的大型分布式网站的几点概念
    C# 在程序中控制IIS服务或应用程序池关闭重启
  • 原文地址:https://www.cnblogs.com/whiteBear/p/16028303.html
Copyright © 2020-2023  润新知