wake-up-neo.com

Senden und Empfangen einer Datei in der Socket-Programmierung unter Linux mit C / C ++ (GCC / G ++)

Ich möchte eine Client-Server-Architektur implementieren, die unter Linux mit Sockets und C/C++ ausgeführt wird und Dateien senden und empfangen kann. Gibt es eine Bibliothek, die diese Aufgabe erleichtert? Könnte jemand bitte ein Beispiel geben?

37
Sajad Bahmani

Die tragbarste Lösung besteht darin, die Datei nur in Blöcken zu lesen und die Daten dann in einer Schleife in den Socket zu schreiben (und umgekehrt, wenn die Datei empfangen wird). Sie ordnen einen Puffer zu, read in diesem Puffer, und write aus diesem Puffer in Ihrem Socket (Sie können auch send und recv, die sockelspezifische Methoden zum Schreiben und Lesen von Daten darstellen). Die Gliederung würde ungefähr so ​​aussehen:

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

Lesen Sie die Dokumentation zu read und write sorgfältig durch, insbesondere beim Umgang mit Fehlern. Einige der Fehlercodes bedeuten, dass Sie es einfach noch einmal versuchen sollten, z. B. eine Schleife mit einer continue -Anweisung, während andere bedeuten, dass etwas kaputt ist und Sie stoppen müssen.

Zum Senden der Datei an einen Socket gibt es einen Systemaufruf sendfile , der genau das tut, was Sie wollen. Der Kernel wird angewiesen, eine Datei von einem Dateideskriptor an einen anderen zu senden, und der Kernel kann sich dann um den Rest kümmern. Es gibt eine Einschränkung, dass der Quellendateideskriptor mmap unterstützen muss (wie in, eine tatsächliche Datei, kein Socket), und das Ziel muss ein Socket sein (so dass Sie es nicht zum Kopieren von Dateien verwenden können, oder Daten direkt von einem Socket zu einem anderen senden); Es wurde entwickelt, um die von Ihnen beschriebene Verwendung des Sendens einer Datei an einen Socket zu unterstützen. Es hilft jedoch nicht beim Empfangen der Datei. Dafür müssten Sie die Schleife selbst durchführen. Ich kann Ihnen nicht sagen, warum es einen sendfile -Aufruf gibt, aber keinen analogen recvfile.

Beachten Sie, dass sendfile Linux-spezifisch ist. Es ist nicht auf andere Systeme übertragbar. Andere Systeme haben häufig ihre eigene Version von sendfile, aber die genaue Schnittstelle kann variieren ( FreeBSD , Mac OS X , Solaris ).

In Linux 2.6.17 wurde der Systemaufruf spliceeingeführt und ab 2.6.23 intern zur Implementierung von sendfile . splice ist eine allgemeinere API als sendfile. Für eine gute Beschreibung von splice und tee siehe das ziemlich gute Erklärung von Linus selbst . Er weist darauf hin, dass die Verwendung von splice im Grunde genommen genau wie die obige Schleife ist, wobei read und write verwendet werden, mit der Ausnahme, dass sich der Puffer im Kernel befindet, sodass die Daten nicht vorhanden sind zwischen dem Kernel und dem Benutzerbereich übertragen werden oder möglicherweise nicht einmal die CPU passieren (bekannt als "Zero-Copy I/O").

66
Brian Campbell

Mach aman 2 sendfile. Sie müssen nur die Quelldatei auf dem Client und die Zieldatei auf dem Server öffnen, dann sendfile aufrufen und der Kernel zerhackt und verschiebt die Daten.

17
florin

Minimal ausführbares POSIX read + write Beispiel

Verwendung:

  1. holen Sie sich zwei Computer auf einem LAN .

    Dies funktioniert beispielsweise, wenn in den meisten Fällen beide Computer an Ihren Heimrouter angeschlossen sind, wie ich es getestet habe.

  2. Auf dem Servercomputer:

    1. Suchen Sie die lokale IP des Servers mit ifconfig, z. 192.168.0.10

    2. Lauf:

      ./server output.tmp 12345
      
  3. Auf dem Clientcomputer:

    printf 'ab\ncd\n' > input.tmp
    ./client input.tmp 192.168.0.10 12345
    
  4. Ergebnis: eine Datei output.tmp wird auf dem Server erstellt, auf dem '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 .

Weitere Kommentare

Mögliche Verbesserungen:

  • Zur Zeit output.tmp wird bei jedem Sendevorgang überschrieben.

    Dies erfordert die Erstellung eines einfachen Protokolls, das die Übergabe eines Dateinamens ermöglicht, so dass mehrere Dateien hochgeladen werden können, z. Dies würde natürlich sanitäre Einrichtungen erfordern, um eine pfadübergreifende Sicherheitsanfälligkeit zu vermeiden.

    Alternativ könnten wir einen Server einrichten, der die Dateien hascht, um Dateinamen zu finden, und eine Zuordnung von den ursprünglichen Pfaden zu den Hashes auf der Festplatte (in einer Datenbank) beibehält.

  • Es kann immer nur ein Client eine Verbindung herstellen.

    Dies ist besonders schädlich, wenn es langsame Clients gibt, deren Verbindungen lange dauern: Die langsame Verbindung hält alle an.

    Eine Möglichkeit, dies zu umgehen, besteht darin, für jedes accept einen Prozess/Thread zu erstellen, die Wiedergabe sofort erneut zu starten und die Dateisperrensynchronisierung für die Dateien zu verwenden.

  • Fügen Sie Zeitüberschreitungen hinzu und schließen Sie Clients, wenn diese zu lange dauern. Andernfalls wäre es einfach, einen DoS durchzuführen.

    poll oder select sind einige Optionen: Wie wird eine Zeitüberschreitung beim Aufruf der Lesefunktion implementiert?

Eine einfache HTTP wget -Implementierung finden Sie unter: Wie wird eine HTTP-Abrufanforderung in C ohne libcurl erstellt?

Getestet unter Ubuntu 15.10.

Diese Datei ist ein gutes Beispiel für sendfile: http://tldp.org/LDP/LGNET/91/misc/tranter/server.c.txt

8