通过 TCP 套接字进行 i/o 调用的问题

Posted

技术标签:

【中文标题】通过 TCP 套接字进行 i/o 调用的问题【英文标题】:Issues with i/o calls over TCP socket 【发布时间】:2022-01-05 16:45:18 【问题描述】:

对于某些背景,我正在编写一个 TCP 套接字服务器/客户端通信程序来将一个文本文件复制到另一个。我正在努力实现通过套接字读取。我正在linux上开发。

这是客户端读取函数通过套接字发送读取操作码时调用的服务器端读取函数。程序使用正确的 fd 和计数正确地到达此代码。

/* s_read
 * server side read
 * Send in socket: 4 bytes return value, 4 bytes errno, n bytes data
 */
int s_read(int conn)
    
    // Get fd
    unsigned char fdstr[4];
    int L     = sizeof(char) * 4;
    int bytes = 0;
    int total = 0;
    while ( total < L ) 
        if ( (bytes = read(conn, fdstr + total, L - total)) < 0) 
            printf("r_server.c::s_read: Couldn't receive fd\n");
            return -1;
        
        total += bytes;
    
    int fd = 0;
    fd = (fdstr[0] << 24) | (fdstr[1] << 16) | (fdstr[2] <<  8) | fdstr[3];
    
    // Get count
    unsigned char countstr[4];
    L     = sizeof(char) * 4;
    bytes = 0;
    total = 0;
    while ( total < L ) 
        if ( (bytes = read(conn, countstr + total, L - total)) < 0) 
            printf("r_server.c::s_read: Couldn't receive count\n");
            return -1;
        
        total += bytes;
    
    int count = 0;
    count = (countstr[0] << 24) | (countstr[1] << 16) | (countstr[2] <<  8) | countstr[3];
    
    // Prepare return message
    int    return_value;
    L = 8 +           // 4 bytes for return value, 4 bytes for errno
        count;        // count bytes for read in data
    int    error = 0;
    char * msg;
    char * read_value = malloc(count);
    
    // Execute open call
    bytes = 0;
    total = 0;
    while ( total < count ) 
        if ( (return_value = read(fd, read_value + total, count - total)) < 0 ) 
            error = errno;
            break;
        
        total += return_value;
    
    
    // Build return message
    msg = malloc(L);
    L=0;
    msg[L++] = (return_value >> 24) & 0xff; // put the kernel return value
    msg[L++] = (return_value >> 16) & 0xff;
    msg[L++] = (return_value >>  8) & 0xff;
    msg[L++] = (return_value      ) & 0xff;
    msg[L++] = (error >> 24) & 0xff;        // put the errno
    msg[L++] = (error >> 16) & 0xff;
    msg[L++] = (error >>  8) & 0xff;
    msg[L++] = (error      ) & 0xff;
    for (int i=0; i < count; i++)
        msg[L++] = read_value[i];           // put the read in data.
    
    // Send return message
    bytes = 0;
    total = 0;
    while ( total < L ) 
        if ( (bytes = write(conn, msg + total, L - total)) < 0) 
            printf("r_server.c::s_read: Error sending r_read return value to client\n");
            return -1;
        
        total += bytes;
    
    
    free(read_value);
    free(msg);
    
    return 0;

这是客户端读取函数,它将有效负载放在一起并通过套接字发送以请求服务器对其进行读取。

/* r_read
 * remote read
 */
int r_read(int fd, void *buf, int count) 
    
    int    L;
    char * msg;
    int    in_msg;
    int    in_err;
    
    L = 1           +   // byte for opcode
        sizeof(fd)  +   // int bytes for fd.
        sizeof(count);  // int bytes for count.
    
    msg = malloc(L);
    L=0;
    msg[L++] = 3;                    // this is the code for read.
    
    msg[L++] = (fd >> 24) & 0xff;    // put the fd.
    msg[L++] = (fd >> 16) & 0xff;
    msg[L++] = (fd >>  8) & 0xff;
    msg[L++] = (fd      ) & 0xff;
    
    msg[L++] = (count >> 24) & 0xff; // put the count.
    msg[L++] = (count >> 16) & 0xff;
    msg[L++] = (count >>  8) & 0xff;
    msg[L++] = (count      ) & 0xff;
    
    int bytes = 0;
    int total = 0;
    while ( total < L ) 
        if ( (bytes = write(sock, msg + total, L - total)) < 0) 
            printf("Failed to send r_read to server\n");
            return -1;
        
        total += bytes;
    
    
    bytes = 0;
    total = 0;
    while ( total < 8 ) 
        if ( (bytes = read(sock, msg + total, 8 - total)) < 0) 
            printf("Failed to receive r_read from server\n");
            return -1;
        
        total += bytes;
    
    
    in_msg = (msg[0] << 24) | (msg[1] << 16) | (msg[2] << 8) | msg[3];
    in_err = (msg[4] << 24) | (msg[5] << 16) | (msg[6] << 8) | msg[7];
    for (int i = 0; i < count; i++) 
        *(char *)(buf + i) = msg[i + 8];
    
    
    errno = in_err;
    
    free(msg);
    
    return in_msg;

客户端在 in_msg 中接收到一个虚假的 return_value,但服务器在发送之前看到了正确的 return_value。实际读取的数据在两端似乎也是虚假的。

这是实际调用 i/o 函数以将一个文件复制到另一个文件的代码。我有一个类似的从 local 文件复制到远程文件的文件,并且该文件可以正常工作。在 main 接收到套接字信息并从 argv[] 中剥离这些 args 后,它会从 main 调用。

int entry(int argc, char* argv[])
    
    // Input guards
    if (argc != 3) 
        printf("Invalid arguments\n");
        return -1;
    
    
    // Get file names
    char *filename = argv[1];
    char *copyfilename = argv[2];
    printf("rclient2.c::entry: Copying remote file %s to local file %s\n", filename, copyfilename);
    
    // Open files
    int fd = r_open((const char*) filename, O_RDWR, (mode_t) 0600);
    if (fd < 0) 
        printf("rclient2.c::entry: r_open failed.\n");
        return -1;
    
    int copyfd = open((const char*) copyfilename, O_RDWR | O_CREAT, (mode_t) 0600);
    if (copyfd < 0) 
        printf("rclient2.c::entry: open failed.\n");
        return -1;
    
    
    // Seek to position 10
    //r_lseek(fd, 10, SEEK_SET); // Later requirement once read is working

    // Copy file
    char buf;
    while ( r_read(fd, &buf, 1) > 0 ) 
        if (write(copyfd, &buf, 1) < 0) 
            printf("rclient2::entry: write failed.\n");
            return -1;
        
    
    
    // Close files
    r_close(fd);
    close(copyfd);
    
    return 0;

有关错误行为的更多详细信息: 服务器在关闭之前只获得了 6 次 s_read() 调用,而它应该获得大约 41 次。在读取“复制”文件时,它充满了人类无法读取的虚假值。除了 close() 调用之后的一个错误之外,我的任何错误都没有被触发。特别是“r_server.c::main: 无法接收操作码”。错误。因此,我在下面包含了服务器的 main 函数(它读取客户端函数提供的操作码)。

/* main - server implementation
 */
int main(int argc, char *argv[])
    
    // Setup socket data
    int listener, conn;
    unsigned int length;
    struct sockaddr_in s1, s2;
    
    // Create server socket
    listener = socket(AF_INET, SOCK_STREAM, 0);
    if (listener < 0) 
        printf("r_server.c::main: Error creating server socket\n");
        return -1;
    
    
    // Establish server socket
    bzero((char *) &s1, sizeof(s1));
    s1.sin_family = AF_INET;
    s1.sin_port = htons(0);
    s1.sin_addr.s_addr = inet_addr("127.0.0.1");
    if( bind( listener, (struct sockaddr *) &s1, sizeof(s1)) < 0) 
        printf("r_server.c::main: Server couldn't bind to the port\n");
        return -1;
    
    
    // Print port and start server
    length = sizeof(s1);
    getsockname(listener, (struct sockaddr *) &s1, &length);
    printf("%d\n", s1.sin_port);
    if ( listen(listener, 1) < 0) 
        printf("r_server.c::main: Server error while listening\n");
        return -1;
    

    // While server running
    while(1) 
        
        // Connect to new client
        length = sizeof(s2);
        conn = accept(listener, (struct sockaddr *) &s2, &length);
        if( conn < 0 ) 
            printf("r_server.c::main: Server failed to accept incoming stuff\n");
            return -1;
        
        
        // Fork to manage client and listen for new clients
        if (fork() == 0) 
            
            // Until client disconnects
            while (1) 
                
                // Get opcode
                unsigned char opcode;
                int success;
                if ((success = read(conn, &opcode, sizeof(opcode))) < 0) 
                    printf("r_server.c::main: Couldn't receive opcode. opcode = %d\n", opcode);
                    return -1;
                
                
                // Client disconnected
                if (success == 0) 
                    return 0;
                
                
                // Call related server side function
                switch (opcode) 
                    case (1):
                        printf("Opening...\n");
                        if (s_open(conn) < 0)
                            return -1;
                        break;
                    case (2):
                        printf("Closing...\n");
                        if (s_close(conn) < 0)
                            return -1;
                        break;
                    case (3):
                        printf("Reading...\n");
                        if (s_read(conn) < 0)
                            return -1;
                        break;
                    case (4):
                        printf("Writing...\n");
                        if (s_write(conn) < 0)
                            return -1;
                        break;
                    case (5):
                        printf("Seeking...\n");
                        if (s_lseek(conn) < 0)
                            return -1;
                        break;
                    case (6):
                        printf("Piping...\n");
                        if (s_pipe(conn) < 0)
                            return -1;
                        break;
                    case (7):
                        printf("Duping...\n");
                        if (s_dup2(conn) < 0)
                            return -1;
                        break;
                    default:
                        return -1;              
                
            
            return 0;
        
    
    return 0;

【问题讨论】:

【参考方案1】:

代码盲目地假设read 将始终准确读取请求的字节数,write 将始终写入给定的完整缓冲区。这两个假设都是错误的。

read 只会读取给定大小,但实际上可能会读取更少。类似的write 只会最多写入整个缓冲区,但可能会写入更少。因此,必须检查返回值以确定实际读取或写入了多少数据。如果不是所有内容都被读取或写入,则必须对剩余数据重复操作,甚至可能多次。

【讨论】:

啊,我的错。在调试时,我将读写调用更改为 i/o 大小为 41 的字符串,而不是逐个字符地进行。当我提出这个问题时,我忘了改变它,所以它处于中间状态。现在它期望一次只读/写一个字节,就像我最初的打算一样。这能解决这个问题吗? @zq_Aux:代码中的实际读写调用仍然是多个字节,仍然不检查实际读取或写入了多少。除此之外,一次只读/写几个字节是非常低效的。 我清理了所有的读写调用,但仍然出现未定义的行为。 “我清理了所有的读写调用” - 我在您发布的代码中没有看到任何此类更新,因此无法知道是什么你确实做到了。 "getting undefined behavior" 也不是有用的错误描述。请准确描述您期望得到什么以及您得到什么,包括任何类型的错误消息。 很抱歉。我已经更新了我的问题。我希望将其文件名通过命令行提供的一个现有远程文件中的文本复制到其文件名也通过命令行提供的本地文件。相反,被复制的数据是非人类可读的垃圾。【参考方案2】:

在 r_read() 函数中,我们从未读入服务器放入响应中的值。我们只读取了与return_value和errno对应的8个字节。

代替

int bytes = 0;
int total = 0;
while ( total < L ) 
    if ( (bytes = write(sock, msg + total, L - total)) < 0) 
        printf("Failed to send r_read to server\n");
        return -1;
    
    total += bytes;

    
bytes = 0;
total = 0;
while ( total < 8 ) 
    if ( (bytes = read(sock, msg + total, 8 - total)) < 0) 
        printf("Failed to receive r_read from server\n");
        return -1;
    
    total += bytes;


in_msg = (msg[0] << 24) | (msg[1] << 16) | (msg[2] << 8) | msg[3];
in_err = (msg[4] << 24) | (msg[5] << 16) | (msg[6] << 8) | msg[7];
for (int i = 0; i < count; i++) 
    *(char *)(buf + i) = msg[i + 8];

我们需要做的

int bytes = 0;
int total = 0;
while ( total < L ) 
    if ( (bytes = write(sock, msg + total, L - total)) < 0) 
        printf("Failed to send r_read to server\n");
        return -1;
    
    total += bytes;


bytes = 0;
total = 0;
while ( total < 8 ) 
    if ( (bytes = read(sock, msg + total, 8 - total)) < 0) 
        printf("Failed to receive r_read from server\n");
        return -1;
    
    total += bytes;


in_msg = (msg[0] << 24) | (msg[1] << 16) | (msg[2] << 8) | msg[3]; // # of bytes remaining in the socket buffer (the data we asked for)
in_err = (msg[4] << 24) | (msg[5] << 16) | (msg[6] << 8) | msg[7];


bytes = 0;
total = 0;
while ( total < in_msg ) 
    if ( (bytes = read(sock, buf + total, in_msg - total)) < 0) 
        printf("Failed to read the r_read data from server\n");
        return -1;
    

请注意,只有在您需要处理读取或写入调用未完全成功的情况时,才需要while ( total ...) 逻辑。就我而言,如果发生这种情况,我们可以简单地退出并出现错误。

【讨论】:

以上是关于通过 TCP 套接字进行 i/o 调用的问题的主要内容,如果未能解决你的问题,请参考以下文章

五十linux 编程——TCP 编程模型

TCP客户端服务器编程模型

Linux网络学习_TCP客户端服务器编程

I/O复用 - 各种不同的IO模型

为啥不尝试 I/O 就不可能检测到 TCP 套接字已被对等方优雅地关闭?

非阻塞式I/O