En los títulos y los textos vais a encontrar unas cuantas citaciones cinematográficas (y si, soy un cinéfilo). Si no os interesan podéis fingir no verlas, ya que no son fundamentales para la comprensión de los post...

Este blog es la versión en Español de mi blog en Italiano L'arte della programmazione in C. Espero que mis traducciones sean comprensibles...

domingo, 6 de septiembre de 2015

El Server oscuro: la leyenda renace
cómo escribir un Socket Server en C

Un poco como Batman, estoy de vuelta! Por supuesto, no es mi intención ponerme en el mismo nivel del legendario caballero oscuro, pero este es mi blog y me puedo permitir alguna licencia, ¿no?.
No es un blog de cine, ¿eh?
Como se intuye por el título reanudamos nuestra C-adventure hablando de Socket Server (o TCP Server, da igual). Espero que todos sepan lo que es, de lo contrario os recomiendo una util lectura con ejemplo incluido (¡pero mi ejemplo es mejor!), así que no pierdo tiempo y puedo ir directamente al código. ¡Ahí está!
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <arpa/inet.h>
#include <errno.h>

#define BACKLOG   10      // para listen()
#define MYBUFSIZE 1024

int main(int argc, char *argv[])
{
    // test argumentos
    if (argc != 2) {
        // error args
        printf("%s: numero argumentos equivocado\n", argv[0]);
        printf("uso: %s port [i.e.: %s 9999]\n", argv[0], argv[0]);
        return EXIT_FAILURE;
    }

    // crea un socket
    int my_socket;
    if ((my_socket = socket(AF_INET, SOCK_STREAM, 0)) < 0) {
        // error socket()
        printf("%s: could not create socket (%s)\n", argv[0], strerror(errno));
        return EXIT_FAILURE;
    }

    // prepara la struct sockaddr_in para este server
    struct sockaddr_in server;          // (local) server socket info
    server.sin_family = AF_INET;
    server.sin_addr.s_addr = INADDR_ANY;
    server.sin_port = htons(atoi(argv[1]));

    // bind informaciones del server al socket
    if (bind(my_socket, (struct sockaddr *)&server, sizeof(server)) < 0) {
        // error bind()
        printf("%s: bind failed (%s)", argv[0], strerror(errno));
        return EXIT_FAILURE;
    }

    // start escucha con una cola de max BACKLOG conexiones
    if (listen(my_socket, BACKLOG) < 0) {
        // error listen()
        printf("%s: listen failed (%s)\n", argv[0], strerror(errno));
        return EXIT_FAILURE;
    }

    // acepta conexiones de un client entrante
    printf("%s: espera connexiones entrantes...\n", argv[0]);
    socklen_t socksize = sizeof(struct sockaddr_in);
    struct sockaddr_in client;          // (remote) client socket info
    int client_sock;
    if ((client_sock = accept(my_socket, (struct sockaddr *)&client, &socksize)) < 0) {
        // error accept()
        printf("%s: accept failed (%s)\n", argv[0], strerror(errno));
        return EXIT_FAILURE;
    }

    // loop di ricezione messaggi dal client
    int read_size;
    char client_msg[MYBUFSIZE];
    while ((read_size = recv(client_sock, client_msg, MYBUFSIZE, 0)) > 0 ) {
        // send messaggio di ritorno al client
        printf("%s: recibido mensaje del sock %d: %s\n", argv[0], client_sock, client_msg);
        char server_msg[MYBUFSIZE];
        sprintf(server_msg, "me has escrito: %s", client_msg);
        write(client_sock, server_msg, strlen(server_msg));

        // clear buffer
        memset(client_msg, 0, MYBUFSIZE);
    }

    // loop terminado: test motivo
    if (read_size < 0) {
        // error recv()
        printf("%s: recv failed\n", argv[0]);
        return EXIT_FAILURE;
    }
    else if (read_size == 0) {
        // Ok: el client se ah desconectado
        printf("%s: client disconnected\n", argv[0]);
    }

    // salgo con Ok
    return EXIT_SUCCESS;
}

Ok, como se nota es ampliamente comentado y así se auto-explica, por lo cual no voy a detenerme sobre las instrucciones y/o grupos de instrucciones (¡leer los comentarios! ¡Están ahí para eso!), pero voy a añadir, solamente, algunos detalles estructurales.

la estructura es la clásica y básica de un Socket Server:
  1. socket() - crea un socket
  2. prepara la estructura sockaddr_in para este servidor
  3. bind() - bind informaciones del servidor al socket
  4. listen() - empieza a escuchar con una cola de max BACKLOG conexiones
  5. accept() - acepta conexiones entrantes de un cliente
  6. recv() - bucle de recepción mensajes del cliente
por supuesto, hay variaciones de esta estructura, pero este es la clásica. En este ejemplo, escrito y probado específicamente para el blog (bueno, en realidad he adaptado/modificado un poco de código que escribí para trabajo: ¡no es el primer Socket Server que escribo!), en el bucle de lectura se re-envía al Client el mensaje recibido: ¡sorpresa! En el próximo post veremos el Client, por lo que cerraremos el círculo y podremos probar en serio una conversación Client/Server.

Notar que, además, el main() comienza con una prueba de los argumentos con anexo ejemplo de uso en caso de error: esto le da al código un aire profesional que no puede faltar en una aplicación de este tipo.

Desde el punto de vista del estilo, he escrito este código sin respetar mi estilo favorito (hay múltiples puntos de salida) pero para un programa casi-secuencial como esto no es un estilo despreciable, y además... ¡me ha venido así! Por otra parte en su momento dije que se necesita un poco de elasticidad, que no hay que ser demasiado rígidos.

Un último detalle sobre la #define BACKLOG: encontrar el valor apropiado es un tema casi filosófico, así que os remito a una buena descripción que he encontrado en la red. Nos vemos para el cliente y, como siempre, no contengáis la respiración esperando...

¡Hasta el próximo post!