TCP 是可靠的?

发送端并无法获取对应数据流的 ACK 情况,也就是说,发送端没有办法判断对端的接收方是否已经接收发送的数据流,如果需要知道这部分信息,就必须在应用层自己添加处理逻辑,例如显式的报文确认机制。

tcp 没有告诉应用层对方是否收到.

从接收端来说,也没有办法保证 ACK 过的数据部分可以被应用程序处理,因为数据需要接收端程序从接收缓冲区中拷贝,可能出现的状况是,已经 ACK 的数据保存在接收端缓冲区中,接收端处理程序突然崩溃了,这部分数据就没有办法被应用程序继续处理。

故障模式总结

image.png

网络中断造成的对端无 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 符号,之前已经在接收缓冲区的有效数据不会受到影响。

演示程序

服务端程序:

  1. //服务端程序
  2. int main(int argc, char **argv) {
  3. int connfd;
  4. char buf[1024];
  5. connfd = tcp_server(SERV_PORT);
  6. for (;;) {
  7. int n = read(connfd, buf, 1024);
  8. if (n < 0) {
  9. error(1, errno, "error read");
  10. } else if (n == 0) {
  11. error(1, 0, "client closed \n");
  12. }
  13. sleep(5);
  14. int write_nc = send(connfd, buf, n, 0);
  15. printf("send bytes: %zu \n", write_nc);
  16. if (write_nc < 0) {
  17. error(1, errno, "error write");
  18. }
  19. }
  20. exit(0);
  21. }

客户端程序:

//客户端程序
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

image.png

通过 write 产生 RST,read 调用感知 RST

依次启动服务器端和客户端程序,在客户端输入 bad 字符之后,等待一段时间,直到客户端正确显示了服务端的回应“bad”字符之后,再杀死服务器程序。客户端再次输入 bad2,这时屏幕上打印出”peer connection closed“。

linux:

$./reliable_client01 127.0.0.1
bad
bad
bad2
peer connection closed
  • write 没感知到, 是 read 感知 rst 的

image.png

Mac:

$./reliable_client01 127.0.0.1
bad
bad
bad2
read failed: Connection reset by peer (54)

向一个已关闭连接连续写,最终导致 SIGPIPE

服务端:

int 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);
}

客户端:

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”的提示。

linux:

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

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

image.png

mac:

send into buffer 19 
send into buffer -1 
send error: Broken pipe (32)