Este artículo explica la creación de un servidor TCP básico que maneja múltiples conexiones de clientes utilizando hilos en el lenguaje de programación C.
Configuración inicial del socket
El primer paso consiste en crear un descriptor de archivo para el socket, vincularlo a una dirección IP y puerto, y ponerlo en modo de escucha. Las funciones esenciales son socket(), bind() y listen().
#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 MAX_CONEXIONES 5
#define TAM_BUFFER 1024
int main() {
int descriptor_escucha = socket(AF_INET, SOCK_STREAM, 0);
if (descriptor_escucha == -1) {
perror("Error al crear socket");
exit(EXIT_FAILURE);
}
struct sockaddr_in dir_servidor = {
.sin_family = AF_INET,
.sin_addr.s_addr = htonl(INADDR_ANY),
.sin_port = htons(PUERTO)
};
if (bind(descriptor_escucha, (struct sockaddr*)&dir_servidor, sizeof(dir_servidor)) == -1) {
perror("Error en bind");
close(descriptor_escucha);
exit(EXIT_FAILURE);
}
if (listen(descriptor_escucha, MAX_CONEXIONES) == -1) {
perror("Error en listen");
close(descriptor_escucha);
exit(EXIT_FAILURE);
}
}
Aceptación de una conexión única
Para establecer una conexión, se utiliza la función accept() que bloquea la ejecución hasta que un cliente se conecta. Posteriormente, se pueden recibir y enviar datos mediante recv() y send().
struct sockaddr_in dir_cliente;
socklen_t tam_dir = sizeof(dir_cliente);
int descriptor_conexion = accept(descriptor_escucha, (struct sockaddr*)&dir_cliente, &tam_dir);
if (descriptor_conexion == -1) {
perror("Error en accept");
close(descriptor_escucha);
exit(EXIT_FAILURE);
}
printf("Cliente conectado desde: %s:%d\n",
inet_ntoa(dir_cliente.sin_addr),
ntohs(dir_cliente.sin_port));
char mensaje[TAM_BUFFER];
ssize_t bytes_recibidos = recv(descriptor_conexion, mensaje, TAM_BUFFER - 1, 0);
if (bytes_recibidos > 0) {
mensaje[bytes_recibidos] = '\0';
printf("Mensaje recibido: %s\n", mensaje);
send(descriptor_conexion, mensaje, bytes_recibidos, 0);
}
close(descriptor_conexion);
Implementación de bucle de comunicación
Para permitir un intercambio continuo de mensajes con un cliente, se envuelve la operación de recepción y envío en un bucle.
char buffer[TAM_BUFFER];
while (1) {
memset(buffer, 0, TAM_BUFFER);
ssize_t recibido = recv(descriptor_conexion, buffer, TAM_BUFFER - 1, 0);
if (recibido <= 0) {
if (recibido == 0) printf("Cliente desconectado\n");
else perror("Error en recv");
break;
}
printf("Datos recibidos: %s\n", buffer);
send(descriptor_conexion, buffer, recibido, 0);
}
Gestión de múltiples clientes mediante hilos
Para manejar simultáneamente varios clientes, se puede crear un hilo dedicado para cada conexión entrante. Esto requiere incluir la biblioteca de hilos POSIX.
#include <pthread.h>
void* manejar_cliente(void* arg) {
int sock_cliente = *((int*)arg);
free(arg);
char buf[TAM_BUFFER];
while (1) {
memset(buf, 0, TAM_BUFFER);
ssize_t n = recv(sock_cliente, buf, TAM_BUFFER - 1, 0);
if (n <= 0) {
if (n == 0) printf("Cliente se desconectó (fd %d)\n", sock_cliente);
break;
}
printf("Cliente %d: %s\n", sock_cliente, buf);
send(sock_cliente, buf, n, 0);
}
close(sock_cliente);
pthread_exit(NULL);
}
Servidor principal con soporte multi-hilo
El bucle principle del servidor acepta conexiones continuamente y asigna un hilo nuevo para cada cliente. Se debe compilar con la opción -lpthread.
int main() {
int srv_fd = socket(AF_INET, SOCK_STREAM, 0);
if (srv_fd == -1) { perror("socket"); exit(1); }
int opt = 1;
setsockopt(srv_fd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
struct sockaddr_in srv_addr = {
.sin_family = AF_INET,
.sin_port = htons(PUERTO),
.sin_addr.s_addr = INADDR_ANY
};
if (bind(srv_fd, (struct sockaddr*)&srv_addr, sizeof(srv_addr)) == -1) {
perror("bind"); close(srv_fd); exit(1);
}
if (listen(srv_fd, 10) == -1) {
perror("listen"); close(srv_fd); exit(1);
}
printf("Servidor escuchando en puerto %d...\n", PUERTO);
while (1) {
struct sockaddr_in cli_addr;
socklen_t cli_len = sizeof(cli_addr);
int* cli_fd = malloc(sizeof(int));
*cli_fd = accept(srv_fd, (struct sockaddr*)&cli_addr, &cli_len);
if (*cli_fd == -1) {
perror("accept"); free(cli_fd); continue;
}
printf("Nueva conexión: %s:%d\n",
inet_ntoa(cli_addr.sin_addr),
ntohs(cli_addr.sin_port));
pthread_t hilo;
if (pthread_create(&hilo, NULL, manejar_cliente, cli_fd) != 0) {
perror("pthread_create");
close(*cli_fd); free(cli_fd);
}
pthread_detach(hilo);
}
close(srv_fd);
return 0;
}
Consideraciones de diseño
Este modelo de un hilo por conexión funciona para un número moderado de clientes, pero escala mal con miles de conexiones simultáneas (problema C10K). Alternativas más eficientes incluyen el uso de multiplexación con select(), poll() o epoll() en Linux.