当在循环中突然退出 C 程序时,为什么会发生额外的循环迭代?

When abruptly exiting a C program mid-loop, why do additional loop iterations occur?

考虑下面的基本客户端和服务器程序(只是骨架/说明我的问题)。客户端发起与服务器的连接,提示用户输入消息,然后发送到服务器并打印到屏幕。

如果我在循环中间突然退出客户端程序(例如通过关闭终端window),有时客户端会继续迭代循环一段时间(即最后一次发送到服务器的消息/客户端关闭时当前驻留在写缓冲区中的消息被重复发送到服务器,通常直到循环耗尽)。然而,其他时候,服务器上的 read() 调用正确 returns 0,并且连接关闭没有问题(行为似乎相当随机)。

我不太明白这是怎么回事。首先,为什么程序关闭后会出现额外的循环迭代?终端 window 关闭与实际进程本身结束之间是否存在滞后时间?即使确实发生了额外的循环迭代,对 fgets() 的调用是否应该在用户输入消息之前阻塞?

我在 XFCE 桌面上使用 Fedora 25 工作站。

我尝试搜索这方面的信息,但运气不佳(我不确定如何以简洁的方式搜索它)。非常感谢任何帮助。

谢谢

客户:

#include <arpa/inet.h>
#include <netinet/in.h>
#include <stdio.h>
#include <sys/socket.h>
#include <unistd.h>

int main(void) {
    int sockfd = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);

    struct sockaddr_in server;
    server.sin_family = AF_INET;
    server.sin_port = htons(3000);
    inet_pton(AF_INET, "127.0.0.1", &server.sin_addr);
    connect(sockfd, (struct sockaddr *)&server, sizeof(server));

    int i;
    for (i = 0; i < 20; i++) {
        char buf[512];
        printf("Send a message: ");
        fgets(buf, 512, stdin);
        write(sockfd, buf, sizeof(buf));
    }

    close(sockfd);
}

服务器:

#include <arpa/inet.h>
#include <netinet/in.h>
#include <stdio.h>
#include <sys/socket.h>
#include <unistd.h>

int main(void) {
    int listenfd = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);

    struct sockaddr_in server;
    server.sin_family = AF_INET;
    server.sin_port = htons(3000);
    inet_pton(AF_INET, "127.0.0.1", &server.sin_addr);
    bind(listenfd, (struct sockaddr *)&server, sizeof(server));

    listen(listenfd, 10);
    printf("Listening...\n");

    struct sockaddr_in client;
    socklen_t client_size = sizeof(client);
    int clientfd = accept(listenfd, (struct sockaddr *)&client, &client_size);

    for (;;) {
        char buf[512];
        int i = read(clientfd, buf, sizeof(buf));

        if (i == 0) {
            close(clientfd);
            printf("Connection Closed\n");
            break;
        } else {
            printf("%s", buf);
        }
    }

    close(listenfd);
}

当您的终端(以及连接到您的进程 stdin/out/err 的 pty 设备的 remote/master 端)关闭时,fgets 将在 stdin,并且将 return 立即与不完整的行(不以 \n 结尾)或根本没有输入(空 return 值);实际上,它将是后者。如果您检查了结果,您会看到这一点并能够采取相应的行动。

在服务器中,这一行:

printf("%s", buf); 

应替换为:

*buf[i] = '\n'; 
printf( "%s", buf );

所以有一个有效的字符串要打印(read()不会终止字符串)

注意:如果发生 I/O 错误或出现信号等,则 read() 将 return 一个小于 0 的值,并应导致退出 for(;;; ) 循环,不继续循环,打印 buf[]

的 'old' 内容

在客户端的这一行:

write(sockfd, buf, sizeof(buf));

如果写入失败,会return一个小于0的值if/when这样的事件发生,应该退出循环,而不是继续循环,

检查所有错误情况非常重要。这种错误检查(以及由此产生的错误处理)很容易使代码的大小加倍,但必须这样做;否则,您所看到的 'odd' 事件将会发生,并且不会对发生的事情进行简单的解释。

当系统函数 return 出现错误指示时,使用函数 perror() 将您提供的一些文本显示在 stderr 上,以及来自系统的消息为什么它认为发生了错误。

If I abruptly quit the client program in the middle of the loop (e.g. by closing the terminal window),

关闭终端 window 不会退出客户端程序——它继续 运行,只是没有输入(因此从现在关闭的终端读取的任何内容都会 return EOF ).但是,您永远不会在客户端检查 fgets 的 return 值,因此您永远不会注意到,您只是不断循环,发送相同的缓冲区。

另外,代码:

fgets(buf, 512, stdin);
write(sockfd, buf, sizeof(buf));

从输入中读取一行最多 511 个字符,然后发送整个 512 字节缓冲区,无论实际消息有多长。你想要的更像是:

if (fgets(buf, 512, stdin))
    write(sockfd, buf, strlen(buf));
else
    break;  // there was an error or EOF on reading from stdin

当然,对于超过 511 字节的行,这仍然存在问题,然后是 TCP 不保留消息边界的问题,因此在服务器上,您可能会在单个消息中收到多个消息或仅消息的一部分读取调用。