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

问题描述 投票:37回答:4

我想使用能够发送和接收文件的套接字和C / C ++语言实现在Linux上运行的客户端 - 服务器体系结构。是否有任何库使这项任务变得简单?有人可以提供一个例子吗?

c++ c linux sockets network-programming
4个回答
66
投票

最便携的解决方案是以块的形式读取文件,然后在循环中将数据写入套接字(同样,在接收文件时反过来)。你将缓冲区read分配到缓冲区,并将write从缓冲区分配到你的套接字中(你也可以使用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;
    }
}

请务必仔细阅读readwrite的文档,尤其是在处理错误时。一些错误代码意味着您应该再次尝试,例如只使用continue语句再次循环,而其他错误代码意味着某些东西被破坏而您需要停止。

为了将文件发送到套接字,有一个系统调用,sendfile可以满足您的需求。它告诉内核将文件从一个文件描述符发送到另一个文件描述符,然后内核可以处理其余的文件描述符。有一点需要注意,源文件描述符必须支持mmap(如实际文件,而不是套接字),并且目标必须是套接字(因此您不能使用它来复制文件,或直接从一个插座到另一个插座);它旨在支持您描述的将文件发送到套接字的用法。但是,它对接收文件没有帮助;你需要自己做循环。我不能告诉你为什么有一个sendfile电话但没有类似的recvfile

请注意sendfile是特定于Linux的;它不能移植到其他系统。其他系统经常有自己的sendfile版本,但确切的界面可能会有所不同(FreeBSDMac OS XSolaris)。

在Linux 2.6.17中,splice系统调用是introduced,从2.6.23开始是used internally to implement sendfilesplice是比sendfile更通用的API。有关splicetee的详细描述,请参阅相当不错的explanation from Linus himself。他指出如何使用splice基本上就像上面的循环,使用readwrite,除了缓冲区在内核中,因此数据不必在内核和用户空间之间传输,或者甚至可能都没有通过通过CPU(称为“零拷贝I / O”)。


17
投票

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


10
投票

最小的可运行POSIX read + write示例

用法:

  1. LAN上获得两台计算机。 例如,如果两台计算机在大多数情况下连接到您的家用路由器,这将是有效的,这就是我测试它的方式。
  2. 在服务器计算机上: 使用ifconfig查找服务器本地IP,例如192.168.0.10 跑: ./server output.tmp 12345
  3. 在客户端计算机上: printf 'ab\ncd\n' > input.tmp ./client input.tmp 192.168.0.10 12345
  4. 结果:在包含output.tmp的服务器计算机上创建了一个文件qazxsw po!

server.c

'ab\ncd\n'

client.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); }

进一步评论

可能的改进:

  • 目前GitHub upstream每次发送都会被覆盖。 这需要创建一个简单的协议,允许传递文件名,以便可以上传多个文件,例如:文件名到第一个换行符,最大文件名为256个字符,其余的直到套接字关闭为内容。当然,这需要卫生设施以避免output.tmp。 或者,我们可以创建一个哈希文件的服务器来查找文件名,并保留从原始路径到磁盘上的哈希(在数据库上)的映射。
  • 一次只能连接一个客户端。 如果存在连接持续很长时间的慢速客户端,则这是特别有害的:慢速连接会使所有人停止运行。 解决这个问题的一种方法是为每个path transversal vulnerability分叉进程/线程,立即再次开始侦听,并对文件使用文件锁同步。
  • 添加超时,如果时间过长则关闭客户端。否则,做一个DoS会很容易。 acceptpoll是一些选择:select

一个简单的HTTP How to implement a timeout in read function call?实现如下所示:wget

在Ubuntu 15.10上测试过。


8
投票

这个文件将作为一个很好的How to make an HTTP get request in C without libcurl?示例:sendfile

© www.soinside.com 2019 - 2024. All rights reserved.