用C / C ++(GCC / G ++)在Linux下的套接字编程中发送和接收文件

我想实现在Linux上运行的客户端 – 服务器体系结构,使用能够发送和接收文件的套接字和C / C ++语言。 有没有任何图书馆使这个任务容易? 任何人都可以提供一个例子吗?

最便携的解决scheme就是以块读取文件,然后将数据写入到套接字中(在接收文件时也是如此)。 你分配一个缓冲区, read入该缓冲区,并从该缓冲区写入你的套接字(你也可以使用sendrecv ,这是写入和读取数据的特定于套接字的方式)。 大纲看起来像这样:

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

请务必仔细阅读文档进行write ,特别是在处理错误时。 一些错误代码意味着你应该再试一次,比如只用continue语句循环,而其他的则意味着某些事情被破坏了,你需要停下来。

为了将文件发送到套接字,有一个系统调用,发送文件,只是你想要的。 它告诉内核将文件从一个文件描述符发送到另一个文件描述符,然后内核可以处理其余的内容。 有一个告诫,源文件描述符必须支持mmap (如,是一个实际的文件,而不是一个套接字),目的地必须是一个套接字(所以你不能用它来复制文件,或直接发送数据一个sockets到另一个); 它被devise来支持你描述的用法,发送文件到套接字。 但是,这并没有帮助接收文件, 你需要自己做这个循环。 我不能告诉你为什么有一个sendfile调用,但没有类似recvfile

注意sendfile是Linux特定的; 它不能移植到其他系统。 其他系统经常有自己的sendfile版本,但确切的接口可能会有所不同( FreeBSD , Mac OS X , Solaris )。

在Linux 2.6.17中, 引入了splice系统调用,并且从2.6.23开始在内部使用sendfilesplice是比sendfile更通用的API。 对于splicetee一个很好的描述,请看Linus本人的相当好的解释 。 他指出如何使用splice基本上就像上面的循环一样,使用write ,除了缓冲区在内核中,所以数据不必在内核和用户空间之间传输,或者甚至可能不经过通过CPU(称为“零拷贝I / O”)。

做一个man 2 sendfile 。 您只需打开服务器上的客户端和目标文件上的源文件,然后调用sendfile,内核就会切断并移动数据。

这个文件将作为一个很好的发送文件的例子: http : //tldp.org/LDP/LGNET/91/misc/tranter/server.c.txt

最小的可运行POSIX示例

用法:

  1. 在局域网上获得两台电脑

  2. 在服务器计算机上:

    1. 使用ifconfig查找服务器本地IP,例如192.168.0.10

    2. 跑:

       ./server output.tmp 12345 
  3. 在客户端计算机上:

     printf 'ab\ncd\n' > input.tmp ./client input.tmp 192.168.0.10 12345 
  4. 结果:在包含'ab\ncd\n'的服务器上创build一个文件output.tmp

server.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; } 

client.c

 /* 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); } 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每次发送完成后都会被覆盖。

    这要求创build一个简单的协议,允许传递一个文件名,以便可以上传多个文件,例如:文件名到第一个换行符,最大文件名为256个字符,其余的直到套接字closures是内容。 当然,这需要卫生来避免path横穿的脆弱性 。

    或者,我们可以创build一个散列文件的服务器来查找文件名,并将原始path映射到磁盘上(数据库上)的散列。

  • 一次只能有一个客户端连接。

    如果连接速度很慢的客户端连接时间很长,这是特别有害的:连接速度慢会阻止所有人连接。

    解决这个问题的一种方法是为每个accept分派一个进程/线程,立即再次开始监听,并在文件上使用文件locking同步。

  • 添加超时,并closures客户端,如果他们需要太长时间。 否则,做一个DoS会很容易。

    pollselect是一些select: 如何实现阅读函数调用超时?

这个例子在GitHub上 。

在Ubuntu 15.10上testing