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 unshortde host a red;htonl()unlong. Las funciones inversas sonntohs()yntohl().
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
