Отправка и получение файла при программировании сокетов в Linux с использованием C / C ++ (GCC / G ++) - PullRequest
37 голосов
/ 06 января 2010

Я хотел бы реализовать архитектуру клиент-сервер, работающую в Linux, используя сокеты и язык C / C ++, который способен отправлять и получать файлы. Есть ли библиотека, которая облегчает эту задачу? Может ли кто-нибудь привести пример?

Ответы [ 4 ]

65 голосов
/ 06 января 2010

Самое переносимое решение - просто прочитать файл порциями, а затем записать данные в сокет в цикле (и аналогично, наоборот при получении файла). Вы выделяете буфер, 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 является более универсальным API, чем sendfile. Хорошее описание splice и tee см. В довольно хорошем объяснении самого Линуса . Он указывает, что использование splice в основном похоже на цикл выше, используя read и write, за исключением того, что буфер находится в ядре, поэтому данные не должны передаваться между ядром и пользовательским пространством, или может даже не проходить через ЦП (известный как «ввод-вывод нулевой копии»).

17 голосов
/ 06 января 2010

Do man 2 sendfile. Вам нужно всего лишь открыть исходный файл на клиенте и файл назначения на сервере, затем вызвать sendfile, и ядро ​​преобразует и перемещает данные.

10 голосов

Минимальная работоспособность POSIX read + write пример

Использование:

  1. получить два компьютера в LAN .

    Например, это будет работать, если оба компьютера подключены к вашему домашнему маршрутизатору в большинстве случаев, как я и проверял.

  2. На сервере:

    1. Найдите локальный IP-адрес сервера с помощью ifconfig, например, 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. Результат: на компьютере сервера создан файл output.tmp, содержащий 'ab\ncd\n'!

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

Дополнительные комментарии

Возможные улучшения:

  • В настоящее время output.tmp перезаписывается при каждой отправке.

    Это требует создания простого протокола, который позволяет передавать имя файла, чтобы можно было загрузить несколько файлов, например: имя файла до первого символа новой строки, максимальное имя файла 256 символов и остальное до закрытия сокета. , Конечно, для этого потребуется санитария, чтобы избежать сквозной уязвимости .

    В качестве альтернативы, мы могли бы создать сервер, который хэширует файлы для поиска имен файлов и сохраняет карту от исходных путей до хэшей на диске (в базе данных).

  • Одновременно может подключаться только один клиент.

    Это особенно вредно, если есть медленные клиенты, чьи соединения длятся долго: медленное соединение останавливает всех.

    Один из способов обойти это - обработать процесс / поток для каждого accept, немедленно начать прослушивание и использовать синхронизацию блокировки файлов для файлов.

  • Добавьте таймауты и закройте клиентов, если они занимают слишком много времени. В противном случае было бы легко сделать DoS.

    poll или select: некоторые опции: Как реализовать тайм-аут при вызове функции чтения?

Простая реализация HTTP wget показана по адресу: Как сделать HTTP-запрос на получение в C без libcurl?

Протестировано на Ubuntu 15.10.

8 голосов
/ 06 января 2010

Этот файл послужит вам хорошим sendfile примером: http://tldp.org/LDP/LGNET/91/misc/tranter/server.c.txt

...