[英]Send and Receive a file in socket programming in Linux with C/C++ (GCC/G++)
我想使用能够发送和接收文件的套接字和 C/C++ 语言来实现在 Linux 上运行的客户端-服务器架构。 是否有任何库可以简化此任务? 任何人都可以提供一个例子吗?
最便携的解决方案是分块读取文件,然后在循环中将数据写入套接字(同样,在接收文件时也是如此)。 您分配一个缓冲区, read
入该缓冲区,并write
从缓冲区插入插座(你也可以使用send
和recv
,这是写入和读取数据的插座,具体方式)。 大纲看起来像这样:
while (1) {
// Read data into buffer. We may not have enough to fill up buffer, so we
// store how many bytes were actually read in bytes_read.
int bytes_read = read(input_file, buffer, sizeof(buffer));
if (bytes_read == 0) // We're done reading from the file
break;
if (bytes_read < 0) {
// handle errors
}
// You need a loop for the write, because not all of the data may be written
// in one call; write will return how many bytes were written. p keeps
// track of where in the buffer we are, while we decrement bytes_read
// to keep track of how many bytes are left to write.
void *p = buffer;
while (bytes_read > 0) {
int bytes_written = write(output_socket, p, bytes_read);
if (bytes_written <= 0) {
// handle errors
}
bytes_read -= bytes_written;
p += bytes_written;
}
}
请务必阅读文档read
和write
处理错误时小心翼翼,尤其是。 一些错误代码意味着您应该再试一次,例如只需使用continue
语句再次循环,而其他错误代码意味着某些东西已损坏,您需要停止。
要将文件发送到套接字,有一个系统调用sendfile
可以执行您想要的操作。 它告诉内核将文件从一个文件描述符发送到另一个文件描述符,然后内核可以处理其余的事情。 有一个警告,源文件描述符必须支持mmap
(例如,是一个实际的文件,而不是一个套接字),并且目标必须是一个套接字(所以你不能用它来复制文件,或直接从一个插座到另一个); 它旨在支持您描述的将文件发送到套接字的用法。 但是,它对接收文件没有帮助; 你需要自己做这个循环。 我不能告诉你为什么有一个sendfile
调用但没有类似的recvfile
。
请注意sendfile
是 Linux 特定的; 它不可移植到其他系统。 其他系统通常有自己的sendfile
版本,但确切的界面可能会有所不同(FreeBSD 、 Mac OS X 、 Solaris )。
在 Linux 2.6.17 中,引入了splice
系统调用,从 2.6.23 开始, 内部使用它来实现sendfile
。 splice
是比sendfile
更通用的 API。 有关splice
和tee
详细描述,请参阅Linus 本人的相当不错的解释。 他指出使用splice
基本上就像上面的循环一样,使用read
和write
,只是缓冲区在内核中,因此数据不必在内核和用户空间之间传输,甚至可能永远不会通过通过 CPU(称为“零拷贝 I/O”)。
做一个man 2 sendfile
。 你只需要打开客户端上的源文件和服务器上的目标文件,然后调用sendfile,内核就会切割和移动数据。
最小可运行 POSIX read
+ write
示例
用法:
在局域网上弄两台电脑。
例如,如果在大多数情况下两台计算机都连接到您的家用路由器,这将起作用,我就是这样测试的。
在服务器计算机上:
使用ifconfig
查找服务器本地 IP,例如192.168.0.10
运行:
./server output.tmp 12345
在客户端计算机上:
printf 'ab\\ncd\\n' > input.tmp ./client input.tmp 192.168.0.10 12345
结果:在包含'ab\\ncd\\n'
的服务器计算机上创建了一个文件output.tmp
!
服务器.c
/*
Receive a file over a socket.
Saves it to output.tmp by default.
Interface:
./executable [<output_file> [<port>]]
Defaults:
- output_file: output.tmp
- port: 12345
*/
#define _XOPEN_SOURCE 700
#include <stdio.h>
#include <stdlib.h>
#include <arpa/inet.h>
#include <fcntl.h>
#include <netdb.h> /* getprotobyname */
#include <netinet/in.h>
#include <sys/stat.h>
#include <sys/socket.h>
#include <unistd.h>
int main(int argc, char **argv) {
char *file_path = "output.tmp";
char buffer[BUFSIZ];
char protoname[] = "tcp";
int client_sockfd;
int enable = 1;
int filefd;
int i;
int server_sockfd;
socklen_t client_len;
ssize_t read_return;
struct protoent *protoent;
struct sockaddr_in client_address, server_address;
unsigned short server_port = 12345u;
if (argc > 1) {
file_path = argv[1];
if (argc > 2) {
server_port = strtol(argv[2], NULL, 10);
}
}
/* Create a socket and listen to it.. */
protoent = getprotobyname(protoname);
if (protoent == NULL) {
perror("getprotobyname");
exit(EXIT_FAILURE);
}
server_sockfd = socket(
AF_INET,
SOCK_STREAM,
protoent->p_proto
);
if (server_sockfd == -1) {
perror("socket");
exit(EXIT_FAILURE);
}
if (setsockopt(server_sockfd, SOL_SOCKET, SO_REUSEADDR, &enable, sizeof(enable)) < 0) {
perror("setsockopt(SO_REUSEADDR) failed");
exit(EXIT_FAILURE);
}
server_address.sin_family = AF_INET;
server_address.sin_addr.s_addr = htonl(INADDR_ANY);
server_address.sin_port = htons(server_port);
if (bind(
server_sockfd,
(struct sockaddr*)&server_address,
sizeof(server_address)
) == -1
) {
perror("bind");
exit(EXIT_FAILURE);
}
if (listen(server_sockfd, 5) == -1) {
perror("listen");
exit(EXIT_FAILURE);
}
fprintf(stderr, "listening on port %d\n", server_port);
while (1) {
client_len = sizeof(client_address);
puts("waiting for client");
client_sockfd = accept(
server_sockfd,
(struct sockaddr*)&client_address,
&client_len
);
filefd = open(file_path,
O_WRONLY | O_CREAT | O_TRUNC,
S_IRUSR | S_IWUSR);
if (filefd == -1) {
perror("open");
exit(EXIT_FAILURE);
}
do {
read_return = read(client_sockfd, buffer, BUFSIZ);
if (read_return == -1) {
perror("read");
exit(EXIT_FAILURE);
}
if (write(filefd, buffer, read_return) == -1) {
perror("write");
exit(EXIT_FAILURE);
}
} while (read_return > 0);
close(filefd);
close(client_sockfd);
}
return EXIT_SUCCESS;
}
客户端
/*
Send a file over a socket.
Interface:
./executable [<input_path> [<sever_hostname> [<port>]]]
Defaults:
- input_path: input.tmp
- server_hostname: 127.0.0.1
- port: 12345
*/
#define _XOPEN_SOURCE 700
#include <stdio.h>
#include <stdlib.h>
#include <arpa/inet.h>
#include <fcntl.h>
#include <netdb.h> /* getprotobyname */
#include <netinet/in.h>
#include <sys/stat.h>
#include <sys/socket.h>
#include <unistd.h>
int main(int argc, char **argv) {
char protoname[] = "tcp";
struct protoent *protoent;
char *file_path = "input.tmp";
char *server_hostname = "127.0.0.1";
char *server_reply = NULL;
char *user_input = NULL;
char buffer[BUFSIZ];
in_addr_t in_addr;
in_addr_t server_addr;
int filefd;
int sockfd;
ssize_t i;
ssize_t read_return;
struct hostent *hostent;
struct sockaddr_in sockaddr_in;
unsigned short server_port = 12345;
if (argc > 1) {
file_path = argv[1];
if (argc > 2) {
server_hostname = argv[2];
if (argc > 3) {
server_port = strtol(argv[3], NULL, 10);
}
}
}
filefd = open(file_path, O_RDONLY);
if (filefd == -1) {
perror("open");
exit(EXIT_FAILURE);
}
/* Get socket. */
protoent = getprotobyname(protoname);
if (protoent == NULL) {
perror("getprotobyname");
exit(EXIT_FAILURE);
}
sockfd = socket(AF_INET, SOCK_STREAM, protoent->p_proto);
if (sockfd == -1) {
perror("socket");
exit(EXIT_FAILURE);
}
/* Prepare sockaddr_in. */
hostent = gethostbyname(server_hostname);
if (hostent == NULL) {
fprintf(stderr, "error: gethostbyname(\"%s\")\n", server_hostname);
exit(EXIT_FAILURE);
}
in_addr = inet_addr(inet_ntoa(*(struct in_addr*)*(hostent->h_addr_list)));
if (in_addr == (in_addr_t)-1) {
fprintf(stderr, "error: inet_addr(\"%s\")\n", *(hostent->h_addr_list));
exit(EXIT_FAILURE);
}
sockaddr_in.sin_addr.s_addr = in_addr;
sockaddr_in.sin_family = AF_INET;
sockaddr_in.sin_port = htons(server_port);
/* Do the actual connection. */
if (connect(sockfd, (struct sockaddr*)&sockaddr_in, sizeof(sockaddr_in)) == -1) {
perror("connect");
return EXIT_FAILURE;
}
while (1) {
read_return = read(filefd, buffer, BUFSIZ);
if (read_return == 0)
break;
if (read_return == -1) {
perror("read");
exit(EXIT_FAILURE);
}
/* TODO use write loop: https://stackoverflow.com/questions/24259640/writing-a-full-buffer-using-write-system-call */
if (write(sockfd, buffer, read_return) == -1) {
perror("write");
exit(EXIT_FAILURE);
}
}
free(user_input);
free(server_reply);
close(filefd);
exit(EXIT_SUCCESS);
}
进一步评论
可能的改进:
当前每次发送完成时output.tmp
都会被覆盖。
这要求创建一个允许传递文件名的简单协议,以便可以上传多个文件,例如:文件名最多为第一个换行符,文件名最大为 256 个字符,其余内容直到套接字关闭为止。 当然,这需要卫生设施以避免路径横向脆弱性。
或者,我们可以制作一个服务器来对文件进行散列以查找文件名,并将原始路径映射到磁盘上(在数据库上)的散列。
一次只能连接一个客户端。
如果有连接持续很长时间的慢速客户端,这是特别有害的:慢速连接使每个人都停下来。
解决此问题的一种方法是为每个accept
分叉一个进程/线程,立即再次开始侦听,并对文件使用文件锁同步。
添加超时,如果时间过长则关闭客户端。 否则很容易进行 DoS。
poll
或select
是一些选项: 如何在读取函数调用中实现超时?
一个简单的 HTTP wget
实现如下所示: How to make an HTTP get request in C without libcurl?
在 Ubuntu 15.10 上测试。
该文件将成为你的好sendfile
例如: http://tldp.org/LDP/LGNET/91/misc/tranter/server.c.txt
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.