【问题标题】:Binary files gets corrupted when transferred over a socket通过套接字传输二进制文件时损坏
【发布时间】:2015-01-03 17:53:44
【问题描述】:

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

简单介绍一下我的应用

服务器端:读取客户端请求的文件,然后将其写入客户端套接字。

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

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

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

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

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

这与序列化有关吗?我的客户端和服务器都运行在同一个系统上,只用同一个编译器编译。

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

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

返回 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 函数,现在我也能够正确传输二进制文件了。所以,这解决了我的问题。

【问题讨论】:

  • 好吧,当传输文件时,服务器指示整个文件何时发送是很正常的(例如,通过关闭套接字以便客户端读取以 0 重新运行,或者使用一些更高的级协议)。至少可以说,在客户端获取一些文件大小是很糟糕的。
  • 当您像这样编码时,更明确地说明数据的字节序通常是明智的。如果客户端和服务器不同意,你会得到一些令人讨厌的结果。
  • 完成后客户端和服务器都应该关闭()套接字
  • 我强烈建议使用 'send()' 将消息/文件发送到客户端,而不是使用 'write()'
  • 我强烈建议使用'recv()' 从服务器获取消息/文件,而不是使用'read()'

标签: c sockets


【解决方案1】:

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

// 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> 

【讨论】:

  • 我实际上做了这个更改,但忘记在这里更新。总之非常感谢。为简单起见,我没有包含#includes
【解决方案2】:

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

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

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

请记住在每次调用 select() 之前始终重新设置超时变量。

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

在设置选择/读取循环时,读取套接字应设置为非阻塞,(尤其是)因为最终块(可能)不会是完整的读取块长度。 客户端应该在每次 read() 之后检查读取的字节数,以确保收到完整的块。

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

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

(对于包含文件名和总文件大小以及“this”数据包大小的初始块为 -1)

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

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

然后,在服务器收到最后一个 ack 数据包后,服务器就可以关闭套接字了。如果服务器收到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() 循环设置为一次只读取一个字节,这将意味着循环的更多次迭代,但会使读取套接字设置为非阻塞更安全的方法。

上述内容可能看起来比单次写入和单次读取要复杂得多,但它会消除未被注意到的通信错误和未被注意到的损坏数据

【讨论】:

  • 对于 fread 而不是这样做: fread(buffer, 1, size, in);我应该这样做:fread(buffer, size, 1, in);
  • 不需要使用非阻塞读取,只要有可用数据,socket 上的读取就会返回(因此未填充的读取缓冲区不会导致延迟或挂起)
  • @mSatyam:不,不要那样做;你所拥有的更好。性能将是相同的(因为任何理智的实现 - 例如 glibc - 都将在 size*nmemb 项目上转换为 read() ),但使用 1, size fread 将返回读取的字节数,而使用 @987654324 @ 它将返回 0,除非一次性读取全部数据。
  • @user3629249:“(通常)tcp/ip 不会传送大于 ~1600 字节的数据包。因此,通常情况下,数据应该发送得更快。”不,那是错误的。仅仅因为协议可能会将事物分成数据包并不意味着您也应该尝试这样做。性能将由很多因素决定,因此衡量很重要,但作为一般经验法则,最好尽量减少系统调用次数(例如read()/write());如果您拥有所有可用的数据,通常最好一次性发送所有数据:更少的系统调用和内核可以对数据包做出更明智的选择。
  • @user3629249:另请注意,TCP 没有“块”的概念:不能保证一端单独的 write() 调用将作为另一端单独的 read() 调用接收:可以将单独的写入合并为一个,和/或可以将单独的写入拆分为单独的读取。因此,如果您想在协议中使用块,您必须(例如)在每个块的开头添加一个标头及其长度,以便接收者可以知道每个块的开始和结束位置。如果您总是发送整个文件,那么在开始时发送文件长度可能同样简单,并且根本不使用块。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2011-08-19
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多