Saltar al contenido

Enviar y recibir un archivo en la programación de socket en Linux con C / C ++ (GCC / G ++)

Posterior a de esta larga selección de datos dimos con la solución esta preocupación que pueden tener muchos usuarios. Te brindamos la solución y nuestro objetivo es serte de mucha ayuda.

Solución:

La solución más portátil es simplemente leer el archivo en trozos y luego escribir los datos en el socket, en un bucle (y de la misma manera, al revés cuando se recibe el archivo). Usted asigna un búfer, read en ese búfer, y write desde ese búfer en su socket (también puede usar send y recv, que son formas específicas de socket de escribir y leer datos). El esquema se vería así:

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;
    

Asegúrese de leer la documentación para read y write con cuidado, especialmente al manejar errores. Algunos de los códigos de error significan que debería intentarlo de nuevo, por ejemplo, simplemente haciendo un bucle de nuevo con un continue declaración, mientras que otros significan que algo está roto y debe detenerse.

Para enviar el archivo a un socket, hay una llamada al sistema, sendfile que hace exactamente lo que quieres. Le dice al kernel que envíe un archivo de un descriptor de archivo a otro, y luego el kernel puede encargarse del resto. Hay una advertencia de que el descriptor del archivo de origen debe admitir mmap (por ejemplo, ser un archivo real, no un socket), y el destino debe ser un socket (por lo que no puede usarlo para copiar archivos o enviar datos directamente de un socket a otro); está diseñado para admitir el uso que usted describe de enviar un archivo a un socket. Sin embargo, no ayuda a recibir el archivo; tendrías que hacer el bucle tú mismo para eso. No puedo decirte por qué hay un sendfile llamar pero no análogo recvfile.

Cuidado con eso sendfile es específico de Linux; no es portátil a otros sistemas. Otros sistemas suelen tener su propia versión de sendfile, pero la interfaz exacta puede variar (FreeBSD, Mac OS X, Solaris).

En Linux 2.6.17, el splice se introdujo la llamada al sistema y, a partir de 2.6.23, se utiliza internamente para implementar sendfile. splice es una API de propósito más general que sendfile. Para una buena descripción de splice y tee, vea la explicación bastante buena del propio Linus. Señala cómo usar splice es básicamente como el ciclo anterior, usando read y write, excepto que el búfer está en el kernel, por lo que los datos no tienen que transferirse entre el kernel y el espacio del usuario, o es posible que ni siquiera pasen por la CPU (conocido como "E / S de copia cero").

Hacer unman 2 sendfile. Solo necesita abrir el archivo de origen en el cliente y el archivo de destino en el servidor, luego llamar a sendfile y el kernel cortará y moverá los datos.

POSIX ejecutable mínimo read + write ejemplo

Uso:

  1. Consiga dos computadoras en una LAN.

    Por ejemplo, esto funcionará si ambas computadoras están conectadas al enrutador de su hogar en la mayoría de los casos, que es como lo probé.

  2. En la computadora del servidor:

    1. Encuentre la IP local del servidor con ifconfig, p.ej 192.168.0.10

    2. Correr:

      ./server output.tmp 12345
      
  3. En la computadora cliente:

    printf 'abncdn' > input.tmp
    ./client input.tmp 192.168.0.10 12345
    
  4. Resultado: un archivo output.tmp se crea en el servidor que contiene 'abncdn'!

servidor.c

/*
Receive a file over a socket.

Saves it to output.tmp by default.

Interface:

    ./executable [ []]

Defaults:

- output_file: output.tmp
- port: 12345
*/

#define _XOPEN_SOURCE 700

#include 
#include 

#include 
#include 
#include  /* getprotobyname */
#include 
#include 
#include 
#include 

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 %dn", server_port);

    while (1)  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;

cliente.c

/*
Send a file over a socket.

Interface:

    ./executable [ [ []]]

Defaults:

- input_path: input.tmp
- server_hostname: 127.0.0.1
- port: 12345
*/

#define _XOPEN_SOURCE 700

#include 
#include 

#include 
#include 
#include  /* getprotobyname */
#include 
#include 
#include 
#include 

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 sentido ascendente.

Más comentarios

Posibles mejoras:

  • En la actualidad output.tmp se sobrescribe cada vez que se realiza un envío.

    Esto pide la creación de un protocolo simple que permita pasar un nombre de archivo para que se puedan cargar varios archivos, por ejemplo: nombre de archivo hasta el primer carácter de nueva línea, nombre de archivo máximo de 256 caracteres y el resto hasta el cierre del socket son el contenido. Eso sí, eso requeriría saneamiento para evitar un camino de vulnerabilidad transversal.

    Alternativamente, podríamos crear un servidor que aplica un hash a los archivos para encontrar nombres de archivo y mantiene un mapa desde las rutas originales hasta los hash en el disco (en una base de datos).

  • Solo un cliente puede conectarse a la vez.

    Esto es especialmente dañino si hay clientes lentos cuyas conexiones duran mucho tiempo: la conexión lenta detiene a todos.

    Una forma de evitarlo es bifurcar un proceso / subproceso para cada accept, comience a escuchar de nuevo inmediatamente y utilice la sincronización de bloqueo de archivos en los archivos.

  • Agregue tiempos de espera y cierre clientes si demoran demasiado. De lo contrario, sería fácil hacer un DoS.

    poll o select Hay algunas opciones: ¿Cómo implementar un tiempo de espera en la llamada a la función de lectura?

Un HTTP simple wget la implementación se muestra en: ¿Cómo hacer una solicitud HTTP get en C sin libcurl?

Probado en Ubuntu 15.10.

Valoraciones y comentarios

Recuerda difundir este artículo si te ayudó.

¡Haz clic para puntuar esta entrada!
(Votos: 0 Promedio: 0)



Utiliza Nuestro Buscador

Deja una respuesta

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *