通过套接字传输时二进制文件会损坏

Binary files gets corrupted when transferred over a socket

您好,在使用套接字编程创建一个简单的 ftp 程序时,我遇到了以下问题。

我的申请简介

server端:读取client请求的文件,然后写入client socket。

客户端:读取客户端发送的数据,并保存到磁盘。

当我从我的服务器传输普通文本文件时,我在客户端得到了正确的文件。但是,当我传输其他一些文件(如 pdf 或可执行文件)时,当我比较两个文件时,它们具有相同的大小,但我的客户端保存到磁盘的文件已损坏。

例如,如果我的服务器将 4000 字节的二进制文件写入客户端套接字。然后当我的客户将它保存到磁盘时,大小是相同的 4000 字节。但是当我使用 chmod 赋予它可执行权限并尝试执行它时,我收到如下错误:无法执行二进制文件。

同样,当我传输pdf文件时,当我双击打开时,什么也没有显示。

在客户端,我也检查了读取调用是否正在读取整个数据,并且它正在从套接字读取整个数据。

这和连载有关吗。我的客户端和服务器都 运行 在同一系统上,仅使用相同的编译器编译。

我的程序很大,有很多错误检查,所以我在这里粘贴了一些修改过的代码来解释这个问题为了简单起见,我也使用了很多静态的东西:

server.c

int main(int argc, char* argv[])
{
// validate proper usage
if (argc != 4)
{
    fprintf(stderr, "Usage %s <serverBindIP> <serverBindPort> <CredentialsFilePath>\n", argv[0]);
    exit(-1);
}

// create signal hanlder's
// TODO

// store the command line arguments supplied
char* ip = argv[1];
int port = htons(atoi(argv[2]));
char* passwd_file = argv[3];
struct sockaddr_in server_addr, client_addr;

int server_fd, client_fd, result;
socklen_t length;

// Create an internet domain TCP socket
server_fd = socket(AF_INET, SOCK_STREAM, 0);
if (server_fd == -1)
{
    fprintf(stderr, "Unable to create socket\n");
    exit(-1);
}

server_addr.sin_family = AF_INET;
server_addr.sin_port = port;
server_addr.sin_addr.s_addr = inet_addr(ip);

// bind socket to an network interface
result = bind(server_fd, (struct sockaddr*) &server_addr, sizeof(server_addr));
if (result == -1)
{
    fprintf(stderr, "Unable to bind socket\n");
    exit(-1);
}

// mark the socket used for incoming requests
listen(server_fd, 5);

// accept an incoming connection
printf("Waiting for incoming connection\n");
length = sizeof(client_addr);
client_fd = accept(server_fd, (struct sockaddr*) &client_addr, &length);
if (client_fd == -1)
{
    fprintf(stderr, "Unable to accept peer connection\n");
    exit(-1);
}

// read and send one full file
struct stat stats;
stat("/home/xpansat/book.pdf", &stats);
int size = stats.st_size;

// send size of file to the client
write(client_fd, &size, sizeof(int));

FILE* in = fopen("/home/xpansat/book.pdf", "rb");
char *buffer = malloc(size);
fread(buffer, 1, size, in);

write(client_fd, buffer, size);

fclose(in);

return 0; }

client.c

int main(int argc, char* argv[])
{
// validate proper usage
if (argc != 3)
{
    fprintf(stderr, "Usage: %s <serverIP> <serverPort>\n", argv[0]);
    exit(-1);
}

// store the command line arguments 
char *server_ip = argv[1];
int server_port = htons(atoi(argv[2]));

// stores address of remote server to connect
struct sockaddr_in server_addr;
int fd, option;

fd = socket(AF_INET, SOCK_STREAM, 0);
if (fd == -1)
{
    fprintf(stderr, "Error creating socket\n");
    exit(-1);
}

memset(&server_addr, 0, sizeof(server_addr));

server_addr.sin_family = AF_INET;
server_addr.sin_addr.s_addr = inet_addr(server_ip);
server_addr.sin_port = server_port;

if (connect(fd, (struct sockaddr *)&server_addr, sizeof(server_addr)) == -1)
{
    fprintf(stderr, "Error connecting to server\n");
    exit(-1);
}
int size = 0;

// read file size first
read(fd, &size, sizeof(int));

int bytes_read = 0;
int to_read = size;
FILE* out = fopen("book2.pdf", "wb");

char *buffer = malloc(size);
do
{
    bytes_read = read(fd, buffer, to_read);
    printf("To read: %d\n", to_read);
    printf("Data read: %d\n", bytes_read);
    to_read = to_read - bytes_read;

    // save content to disk
    fwrite(buffer, 1, bytes_read, out);
} while (to_read != 0);


return 0;
}

虽然我对这段代码有很好的建议,但我所知道的我粘贴在这里的这段代码实际上并没有说明我的问题,因为我发现在填充缓冲区以发送给客户端时我是使用 strncpy 函数将数据复制到其中,这会使可执行文件在某种程度上损坏(可能是因为它把额外的 \0 放在最后,但我不确定为什么)。所以真正解决我问题的是:用 memcpy 函数替换所有 strncpy 函数,现在我也可以正确传输二进制文件了。所以,这解决了我的问题。

注意:对于fread和fwrite,参数的顺序是: &buffer, sizeOfElement, 元素数量, fileDescriptor
对于发布的代码,指示是每个元素都是 1 个字节长 并且有 'fileSize' 个元素

(通常)tcp/ip 不传递大于 ~1600 字节的数据包。
因此,通常情况下,发送数据的速度越快。

正常的方法是在循环中使用select()和read(),直到select()上的超时到期,其中传递的块号和块长度字段来自第一个数据包指示在输入缓冲区中放置下一个读取数据块的位置。

记住在每次调用 select() 之前总是 re-set 超时变量。

TCP/IP 中数据块大小的这种大小限制表明数据应该一次写入合理的块大小,比如每次调用 write()

1024 个数据字节

设置select/read循环时,读取套接字应设置为non-blocking,(特别是)因为最终块(可能)的长度不会是完整的读取块。 客户端应该在每次 read() 之后检查读取的字节数,以确保收到完整的块。

最好发送一个包含文件名、要传输的实际字节数、整个文件的 check-sum 和数据块大小的初始块。

每个数据包都应该有一个header,表示数据包中的块号和数据字节数。

(-1 表示包含文件名和总文件大小的初始块,以及 'this' 数据包大小)

在没有首先从客户端获得文件已正确传输的指示之前,不要关闭写入套接字。

确保所有数据都被客户端读取, 让客户端在收到每个数据块后发送一个 'ack' 数据包。 建议 ack 数据包包含接收数据包中的块号。

然后,当服务器收到最后一个ack包后,服务器就可以关闭socket了。如果服务器收到nak包,说明需要重新传输文件。

the client should be doing these things,
1) waiting for the select() to timeout,
2) assuring that all blocks were received
3) assuring the file checksum matches the passed checksum from data block 0
4) sending a ack for each packet received
5) sending a final ack if the checksum matches, else send a nak

注意: 虽然速度较慢,但​​我喜欢将 select()/read() 循环设置为一次只读取一个字节,这将意味着更多的循环迭代,但将读取套接字设置为non-blocking 一种更安全的方法。

以上内容可能看起来比单次写入和单次读取复杂得多,但它会消除 un-noticed 通信错误和 un-noticed 损坏的数据

我对服务器进行了此更改,它突然开始工作了。

// send size of file to the client
write(client_fd, &size, sizeof(int));

我还在顶部添加了一些#includes

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>          /* See NOTES */
#include <sys/socket.h>
#include <arpa/inet.h>
#include <sys/stat.h>
#include <string.h>