本文介绍了Linux TCP 套接字:客户端已发送数据但服务器仍阻塞 read()的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

我有一个在 Linux 上使用 TCP 套接字的简单客户端-服务器示例.服务器侦听环回地址.客户端连接到服务器并发送一些整数加上END"用于标记数据结束的字符串.服务器读取数字,将它们全部相加并返回总和.但是,即使客户端已成功发送所有数据,我的服务器有时也会阻塞 read().

代码如下:

server.c:

#include #include #include #include #include #include #include #include #include #define 积压 5int main(int argc, char *argv[]) {结构 sockaddr_in 地址;int down_flag = 0;整数结果 = 0;int ret = 0;int sfd = socket(AF_INET, SOCK_STREAM, 0);如果 (sfd  0) {/* 确保缓冲区以 0 结尾 */buf[sizeof(buf) - 1] = 0;printf("读取数据:%s\n", buf);/* 处理命令 */如果(!strncmp(buf,向下",sizeof(buf))){down_flag = 1;休息;}如果 (!strncmp(buf, END", sizeof(buf))) {休息;}/* 添加收到的被加数 */结果 += atoi(buf);}如果(-1 == num_rd){perror(读取错误");}/* 发送结果 */sprintf(buf, %d", 结果);ret = write(cfd, buf, sizeof(buf));如果(-1 == ret){perror("写错误\n");转到_退出;}关闭(差价合约);/* 在 DOWN 命令上退出 */如果(向下标志){休息;}}_出口:关闭(sfd);返回0;}

client.c:

#include #include #include #include #include #include #include #include int main(int argc, char *argv[]) {结构 sockaddr_in 地址;int ret;int data_socket;字符缓冲区[100] = {0};int i = 0;data_socket = socket(AF_INET, SOCK_STREAM, 0);如果(-1 == data_socket){perror("创建客户端套接字错误");退出(EXIT_FAILURE);}/* 连接到服务器套接字 */memset(&addr, 0, sizeof(addr));addr.sin_family = AF_INET;addr.sin_port = htons(8888);addr.sin_addr.s_addr = htonl(INADDR_LOOPBACK);ret = connect(data_socket, (const struct sockaddr *) &addr, sizeof(addr));如果(-1 == ret){perror("连接错误");退出(EXIT_FAILURE);}/* 发送参数 */for (i = 1; i < argc; i++) {ret = write(data_socket, argv[i], strlen(argv[i]) + 1);如果(-1 == ret){perror(写错误");休息;}}strcpy(buf,结束");ret = write(data_socket, buf, strlen(buf) + 1);printf("将 %s 写入套接字,ret = %d\n", buf, ret);如果(-1 == ret){perror("写入套接字错误");退出(EXIT_FAILURE);}/* 读取结果 */memset(buf, 0, sizeof(buf));ret = read(data_socket, buf, sizeof(buf));如果(-1 == ret){perror(从客户端套接字读取错误");退出(EXIT_FAILURE);}buf[sizeof(buf) - 1] = 0;printf("结果 = %s\n", buf);关闭(数据套接字);退出(退出成功);}

多次运行客户端,服务器将在某些时候阻塞 read() 调用:

$ for i in {1..100};做 ./client 3 4 5 6;完毕将 END 写入套接字,ret = 4结果 = 18将 END 写入套接字,ret = 4

服务器输出:

$ ./server正在等待接受连接...接受的套接字 fd = 4读取数据:3读取数据:4读取数据:5读取数据:6读取数据:结束正在等待接受连接...接受的套接字 fd = 4读取数据:3

服务器在 while ((num_rd = read(cfd, buf, sizeof(buf))) > 0) 行阻塞.

我的问题是为什么 read() 块.AFAIK, read() 将阻塞,直到从套接字读取至少 1 个字节的数据.在这种情况下,客户端发送的数据多于服务器读取的数据,因此我认为可以从套接字读取可用数据.那么为什么 read() 仍然阻塞?

解决方案

问题的核心是代码测试缓冲区中的第一条消息,而忽略了同一缓冲区可能包含多条消息、部分消息或任何其他组合(请参阅编辑).因此,消息 END 有时会被忽略,read 循环从未终止.

代码假设single read 将完全接收single write 调用发送的内容.

这是非常不准确的,很少是真的,并且可能只有在客户端和服务器在同一台机器上时才有时有效.

单个 read 可能同时读取 2 个 write 调用,或者它可能读取半个 write 调用,然后再读取 1.5 个 write 稍后调用...

TCP/IP(不同于 UDP)是一个 流协议 并且不知道消息边界.


编辑:

为了澄清(按照评论中的要求),假设调用 read 收集以下数据 "1234\0EN"(下一个 read 将收集 "D\0")... 程序是做什么的?

另一种可能的情况是所有writes 都被一次性读取.即,buf 包含字符串 "3\04\05\06\0END\0".

此时循环内会发生什么?

在这个示例场景中,if 语句 (strncmp(buf, "END", sizeof(buf)) 总是 false(并且不安全),导致服务器永远不会从 while(read) 循环中断的情况.

由于 while 循环继续,当没有可用数据时,服务器将尝试另一个 read,导致服务器阻塞,直到客户端发送额外数据.>

I have a simple client-server example using TCP socket on Linux. The server listens on the loopback address. The client connects to server and sends some integer plus an "END" string to mark the end of data. Server reads the numbers, add them all and returns the total sum. However, my server sometime blocks on read() even though the client has sent all the data successfully.

Here's the code:

server.c:

#include <stdio.h>
#include <errno.h>
#include <string.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#define BACKLOG 5

int main(int argc, char *argv[]) {
    struct sockaddr_in addr;
    int down_flag = 0;
    int result = 0;
    int ret = 0;

    int sfd = socket(AF_INET, SOCK_STREAM, 0);
    if (sfd < 0) {
        perror("Create server socket error: %s\n");
        return 0;
    }

    /* Bind socket to loopback address */
    memset((void *) &addr, 0, sizeof(struct sockaddr_in));
    addr.sin_family = AF_INET;
    addr.sin_port = htons(8888);
    addr.sin_addr.s_addr = htonl(INADDR_LOOPBACK);
    if (bind(sfd, (struct sockaddr *) &addr, sizeof(struct sockaddr_in)) == -1) {
        perror("Bind server socket failed");
        goto _exit;
    }

    if (listen(sfd, BACKLOG) == -1) {
        perror("Listen failed");
        goto _exit;
    }

    ssize_t num_rd = 0;
    char buf[100] = {0};
    for (;;)
    {
        printf("Waiting to accept a connection...\n");
        int cfd = accept(sfd, NULL, NULL);
        printf("Accepted socket fd = %d\n", cfd);
        result = 0;
        while ((num_rd = read(cfd, buf, sizeof(buf))) > 0) {
            /* Ensure the buffer is 0-terminated */
            buf[sizeof(buf) - 1] = 0;
            printf("Read data: %s\n", buf);

            /* Handle commands */
            if (!strncmp(buf, "DOWN", sizeof(buf))) {
                down_flag = 1;
                break;
            }
            if (!strncmp(buf, "END", sizeof(buf))) {
                break;
            }
            /* Add received summand */
            result += atoi(buf);
        }
        if (-1 == num_rd) {
            perror("Read error");
        }

        /* Send result */
        sprintf(buf, "%d", result);
        ret = write(cfd, buf, sizeof(buf));
        if (-1 == ret) {
            perror("Write error\n");
            goto _exit;
        }
        close(cfd);
        /* Quit on DOWN command */
        if (down_flag) {
            break;
        }
    }
_exit:
    close(sfd);
    return 0;
}

client.c:

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

int main(int argc, char *argv[]) {
    struct sockaddr_in addr;
    int ret;
    int data_socket;
    char buf[100] = {0};
    int i = 0;

    data_socket = socket(AF_INET, SOCK_STREAM, 0);
    if (-1 == data_socket) {
        perror("Create client socket error");
        exit(EXIT_FAILURE);
    }

    /* Connect to server socket */
    memset(&addr, 0, sizeof(addr));
    addr.sin_family = AF_INET;
    addr.sin_port = htons(8888);
    addr.sin_addr.s_addr = htonl(INADDR_LOOPBACK);
    ret = connect(data_socket, (const struct sockaddr *) &addr, sizeof(addr));
    if (-1 == ret) {
        perror("Connect error");
        exit(EXIT_FAILURE);
    }

    /* Send arguments */
    for (i = 1; i < argc; i++) {
        ret = write(data_socket, argv[i], strlen(argv[i]) + 1);
        if (-1 == ret) {
            perror("Write error");
            break;
        }
    }
    strcpy(buf, "END");
    ret = write(data_socket, buf, strlen(buf) + 1);
    printf("write %s to socket, ret = %d\n", buf, ret);
    if (-1 == ret) {
        perror("Write to socket error");
        exit(EXIT_FAILURE);
    }
    /* Read the result */
    memset(buf, 0, sizeof(buf));
    ret = read(data_socket, buf, sizeof(buf));
    if (-1 == ret) {
        perror("Read from client socket error");
        exit(EXIT_FAILURE);
    }
    buf[sizeof(buf) - 1] = 0;
    printf("Result = %s\n", buf);
    close(data_socket);
    exit(EXIT_SUCCESS);
}

Run the client a few times and the server will be blocking on the read() call at some points:

$ for i in {1..100}; do ./client 3 4 5 6; done
write END to socket, ret = 4
Result = 18
write END to socket, ret = 4

Server output:

$ ./server
Waiting to accept a connection...
Accepted socket fd = 4
Read data: 3
Read data: 4
Read data: 5
Read data: 6
Read data: END
Waiting to accept a connection...
Accepted socket fd = 4
Read data: 3

The server is blocking on while ((num_rd = read(cfd, buf, sizeof(buf))) > 0) line.

Edit: My question is why the read() blocks. AFAIK, read() will block until there's at least 1 byte of data to be read from the socket. In this case, the client has sent more data than the server has read, so I think there's available data to be read from the socket. So why does read() still block?

解决方案

The core of the issue is that the code tests for the first message in a buffer and ignores the possibility that the same buffer might contain multiple messages, partial messages or any other combination (see edit). For this reason, the message END is sometimes overlooked and the read loop never terminated.

The code assumes that a single read will receive exactly what a single write call had sent.

This is highly inaccurate, rarely true and might work only sometimes, when both client and server are on the same machine.

A single read might read 2 write calls simultaneously, or it might read half a write call and then another 1.5 write calls later on...

TCP/IP (unlike UDP) is a streaming protocol and has no knowledge of message boundaries.


EDIT:

To clarify (as requested in the comment), imagine a call to read collects the following data "1234\0EN" (the next read will collect "D\0")... what does the program do?

Another possible situation is that all writes are read in one go. i.e, buf contains the string "3\04\05\06\0END\0".

What happens within the loop at this point?

In this example scenario, the if statement (strncmp(buf, "END", sizeof(buf)) is always false (and unsafe), resulting in a situation where the server never breaks from the while(read) loop.

Since the while loop continues, the server will attempt another read when there's no data available, resulting in the server blocking until the client will send additional data.

这篇关于Linux TCP 套接字:客户端已发送数据但服务器仍阻塞 read()的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持!

10-19 07:44