web-dev-qa-db-fra.com

Envoyer et recevoir un fichier en programmation socket sous Linux avec C / C ++ (GCC / G ++)

Je voudrais implémenter une architecture client-serveur fonctionnant sous Linux en utilisant des sockets et un langage C/C++ capable d'envoyer et de recevoir des fichiers. Existe-t-il une bibliothèque qui facilite cette tâche? Quelqu'un pourrait-il donner un exemple?

37
Sajad Bahmani

La solution la plus portable consiste simplement à lire le fichier par morceaux, puis à écrire les données dans le socket, en boucle (et également dans l'autre sens lors de la réception du fichier). Vous allouez un tampon, read dans ce tampon, et write de ce tampon dans votre socket (vous pouvez également utiliser send et recv, qui sont des méthodes spécifiques à la socket pour écrire et lire des données). Le contour ressemblerait à ceci:

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

Assurez-vous de lire attentivement la documentation de read et write, en particulier lors de la gestion des erreurs. Certains des codes d'erreur signifient que vous devez simplement réessayer, par exemple simplement boucler à nouveau avec une instruction continue, tandis que d'autres signifient que quelque chose est cassé et que vous devez arrêter.

Pour envoyer le fichier à un socket, il existe un appel système, sendfile qui fait exactement ce que vous voulez. Il indique au noyau d'envoyer un fichier d'un descripteur de fichier à un autre, puis le noyau peut s'occuper du reste. Il y a une mise en garde que le descripteur du fichier source doit prendre en charge mmap (comme dans, être un fichier réel, pas un socket), et la destination doit être un socket (vous ne pouvez donc pas l'utiliser pour copier des fichiers, ou envoyer des données directement d'une prise à une autre); il est conçu pour prendre en charge l'utilisation que vous décrivez, d'envoyer un fichier vers un socket. Cependant, cela n'aide pas à recevoir le fichier; vous auriez besoin de faire la boucle vous-même pour cela. Je ne peux pas vous dire pourquoi il y a un appel sendfile mais pas d'analogue recvfile.

Attention, sendfile est spécifique à Linux; il n'est pas portable sur d'autres systèmes. D'autres systèmes ont souvent leur propre version de sendfile, mais l'interface exacte peut varier ( FreeBSD , Mac OS X , Solaris ).

Dans Linux 2.6.17, l'appel système splice était introduit , et depuis 2.6.23 est tilisé en interne pour implémenter sendfile . splice est une API plus générale que sendfile. Pour une bonne description de splice et tee, voir le plutôt bon explication de Linus lui-même . Il souligne que l'utilisation de splice est fondamentalement similaire à la boucle ci-dessus, en utilisant read et write, sauf que le tampon est dans le noyau, donc les données n'ont pas à transférer entre le noyau et l'espace utilisateur, ou peut même ne jamais passer par le CPU (connu sous le nom "E/S sans copie").

66
Brian Campbell

Fait uneman 2 sendfile. Il vous suffit d'ouvrir le fichier source sur le client et le fichier de destination sur le serveur, puis d'appeler sendfile et le noyau coupera et déplacera les données.

17
florin

Exemple minimal POSIX exécutable read + write

Usage:

  1. obtenir deux ordinateurs sur un LAN .

    Par exemple, cela fonctionnera si les deux ordinateurs sont connectés à votre routeur domestique dans la plupart des cas, c'est ainsi que je l'ai testé.

  2. Sur l'ordinateur serveur:

    1. Recherchez l'IP locale du serveur avec ifconfig, par exemple 192.168.0.10

    2. Courir:

      ./server output.tmp 12345
      
  3. Sur l'ordinateur client:

    printf 'ab\ncd\n' > input.tmp
    ./client input.tmp 192.168.0.10 12345
    
  4. Résultat: un fichier output.tmp est créé sur l'ordinateur serveur contenant '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 en amont .

Autres commentaires

Améliorations possibles:

  • Actuellement output.tmp est écrasé à chaque envoi.

    Cela demande la création d'un protocole simple qui permet de passer un nom de fichier afin que plusieurs fichiers puissent être téléchargés, par exemple: le nom de fichier jusqu'au premier caractère de nouvelle ligne, le nom de fichier max 256 caractères et le reste jusqu'à la fermeture du socket est le contenu. Bien sûr, cela nécessiterait un assainissement pour éviter une vulnérabilité transversale du chemin .

    Alternativement, nous pourrions créer un serveur qui hache les fichiers pour trouver les noms de fichiers, et conserve une carte des chemins d'origine vers les hachages sur le disque (sur une base de données).

  • Un seul client peut se connecter à la fois.

    Cela est particulièrement dangereux s'il existe des clients lents dont les connexions durent longtemps: la connexion lente stoppe tout le monde.

    Une façon de contourner ce problème est de bifurquer un processus/thread pour chaque accept, de recommencer immédiatement l'écoute et d'utiliser la synchronisation du verrouillage de fichier sur les fichiers.

  • Ajoutez des délais d'attente et fermez les clients s'ils prennent trop de temps. Sinon, il serait facile de faire un DoS.

    poll ou select sont quelques options: Comment implémenter un timeout dans l'appel de fonction de lecture?

Une simple implémentation HTTP wget est montrée à: Comment faire une requête get HTTP en C sans libcurl?

Testé sur Ubuntu 15.10.

Ce fichier vous servira d'exemple sendfile: http://tldp.org/LDP/LGNET/91/misc/tranter/server.c.txt

8
Kornel Kisielewicz