Sockets en C: comunicación de red con BSD sockets en Linux paso a paso

La API de BSD sockets es la interfaz estándar para programación de red en sistemas POSIX. Nacida en BSD Unix en los años 80, hoy está disponible en Linux, macOS, FreeBSD y Windows (WinSock). En C, trabajar directamente con sockets da control total sobre el protocolo, los buffers y la gestión de conexiones, algo que las capas de abstracción de otros lenguajes ocultan.

Conceptos previos

Antes del código, tres conceptos clave:

  • AF_INET / AF_INET6: familia de direcciones IPv4 / IPv6.
  • SOCK_STREAM: socket TCP (orientado a conexión, fiable).
  • SOCK_DGRAM: socket UDP (sin conexión, no fiable, más rápido).
  • Byte order: la red usa big-endian. htons() convierte un short de host a red; htonl() un long. Las funciones inversas son ntohs() y ntohl().

Servidor TCP completo

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>

#define PUERTO 8080
#define BACKLOG 10
#define BUF_SIZE 1024

int main(void) {
    /* 1. Crear socket */
    int servidor = socket(AF_INET, SOCK_STREAM, 0);
    if (servidor < 0) { perror("socket"); exit(1); }

    /* Opción para reusar la dirección rápidamente tras reiniciar */
    int opt = 1;
    setsockopt(servidor, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));

    /* 2. Asociar a dirección y puerto */
    struct sockaddr_in addr = {
        .sin_family = AF_INET,
        .sin_addr.s_addr = INADDR_ANY,   /* todas las interfaces */
        .sin_port = htons(PUERTO)
    };
    if (bind(servidor, (struct sockaddr*)&addr, sizeof(addr)) < 0) {
        perror("bind"); exit(1);
    }

    /* 3. Escuchar conexiones entrantes */
    if (listen(servidor, BACKLOG) < 0) { perror("listen"); exit(1); }
    printf("Servidor escuchando en puerto %dn", PUERTO);

    /* 4. Bucle de aceptación */
    while (1) {
        struct sockaddr_in cliente_addr;
        socklen_t len = sizeof(cliente_addr);
        int cliente = accept(servidor, (struct sockaddr*)&cliente_addr, &len);
        if (cliente < 0) { perror("accept"); continue; }

        char ip_cliente[INET_ADDRSTRLEN];
        inet_ntop(AF_INET, &cliente_addr.sin_addr, ip_cliente, sizeof(ip_cliente));
        printf("Conexión de %s:%dn", ip_cliente, ntohs(cliente_addr.sin_port));

        /* Leer datos */
        char buf[BUF_SIZE];
        ssize_t n = recv(cliente, buf, sizeof(buf) - 1, 0);
        if (n > 0) {
            buf[n] = '';
            printf("Recibido: %sn", buf);
            /* Responder */
            const char* respuesta = "HTTP/1.0 200 OKrnContent-Length: 5rnrnHolan";
            send(cliente, respuesta, strlen(respuesta), 0);
        }
        close(cliente);
    }
    close(servidor);
    return 0;
}

Cliente TCP

int main(void) {
    int sock = socket(AF_INET, SOCK_STREAM, 0);
    if (sock < 0) { perror("socket"); exit(1); }

    struct sockaddr_in servidor = {
        .sin_family = AF_INET,
        .sin_port = htons(8080)
    };
    /* Convertir IP textual a binario */
    inet_pton(AF_INET, "127.0.0.1", &servidor.sin_addr);

    if (connect(sock, (struct sockaddr*)&servidor, sizeof(servidor)) < 0) {
        perror("connect"); exit(1);
    }

    const char* msg = "GET / HTTP/1.0rnrn";
    send(sock, msg, strlen(msg), 0);

    char buf[1024];
    ssize_t n = recv(sock, buf, sizeof(buf) - 1, 0);
    if (n > 0) { buf[n] = ''; printf("%sn", buf); }

    close(sock);
    return 0;
}

I/O no bloqueante y multiplexing

El servidor anterior maneja un cliente a la vez. Para manejar miles de conexiones concurrentes existen tres mecanismos principales:

select() — portabilidad máxima

fd_set readfds;
FD_ZERO(&readfds);
FD_SET(servidor, &readfds);
struct timeval timeout = {5, 0}; /* 5 segundos */
int ready = select(servidor + 1, &readfds, NULL, NULL, &timeout);
if (ready > 0 && FD_ISSET(servidor, &readfds)) {
    /* hay conexión pendiente */
}

Limitado a FD_SETSIZE (generalmente 1024) descriptores. Para muchas conexiones, usar poll() o epoll().

epoll — Linux, alto rendimiento

#include <sys/epoll.h>

int epfd = epoll_create1(0);
struct epoll_event ev = { .events = EPOLLIN, .data.fd = servidor };
epoll_ctl(epfd, EPOLL_CTL_ADD, servidor, &ev);

struct epoll_event eventos[64];
int n = epoll_wait(epfd, eventos, 64, -1); /* espera indefinida */
for (int i = 0; i < n; i++) {
    if (eventos[i].data.fd == servidor) {
        /* nueva conexión */
    } else {
        /* datos en conexión existente */
    }
}

epoll escala a decenas de miles de conexiones concurrentes con complejidad O(1) por evento, a diferencia de select() que es O(n).

Socket UDP

/* Servidor UDP */
int sock = socket(AF_INET, SOCK_DGRAM, 0);
struct sockaddr_in addr = { .sin_family = AF_INET, .sin_port = htons(9000),
                             .sin_addr.s_addr = INADDR_ANY };
bind(sock, (struct sockaddr*)&addr, sizeof(addr));

char buf[512];
struct sockaddr_in remoto;
socklen_t len = sizeof(remoto);
ssize_t n = recvfrom(sock, buf, sizeof(buf)-1, 0,
                     (struct sockaddr*)&remoto, &len);
buf[n] = '';
/* Responder al mismo cliente */
sendto(sock, "ACK", 3, 0, (struct sockaddr*)&remoto, len);
close(sock);

Para aprender a depurar el código de red con herramientas como gdb y detectar condiciones de carrera, el artículo sobre debugging en C con gdb y ASan es el complemento natural. Si buscas estructuras de datos para gestionar conexiones concurrentes eficientemente, el artículo sobre estructuras de datos en C cubre las tablas hash que se usan para mapear descriptores de fichero a estado de conexión.

Imagen: Pexels / Sergei Starostin

COMPARTE ESTE ARTÍCULO

COMPARTIR EN FACEBOOK
COMPARTIR EN TWITTER
COMPARTIR EN LINKEDIN
COMPARTIR EN WHATSAPP