Implementación de comunicación servidor-cliente TCP en Linux con procesos e hilos múltiples

En un servidor TCP con un solo proceso, la función accept() bloquea la ejecución hasta que un cliente se conecta, y durante el manejo de esa conexión, no se pueden aceptar nuevas solicitudes. Esto limita la escalabilidad del servidor.

Para permitir el manejo concurrente de múltiples clientes, se pueden utilizar procesos o hilos. A continuación, se describen enfoques basados en múltiples procesos (incluyendo procesos nieto para evitar bloqueos) e hilos con pthreads.

Enfoque con múltiples procesos

Al aceptar una conexión, se crea un proceso hijo para atender al cliente. Sin embargo, el uso de waitpid() en el proceso principal puede causar bloqueos si los hijos no terminan rápidamente. Una solución es emplear procesos nieto: el hijo crea un nieto para gestioanr la comunicación y luego finaliza inmediatamente, permitiendo que el principle continúe aceptando conexiones sin esperas.

Ejemplo de código para el servidor con procesos nieto:

#include <iostream>
#include <string>
#include <cstring>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <sys/wait.h>
#include "log.hpp"
extern Log lg;
using namespace std;

const int MAX_CONEXIONES = 10;
const string IP_BASE = "0.0.0.0";
const uint16_t PUERTO_DEFECTO = 8080;

enum Errores {
    ERROR_USO = 1,
    ERROR_SOCKET,
    ERROR_ENLACE,
    ERROR_ESCUCHA
};

class ServidorTCP {
public:
    ServidorTCP(uint16_t puerto = PUERTO_DEFECTO)
        : puerto_(puerto), ip_(IP_BASE) {}
    
    void inicializar() {
        socketEscucha_ = socket(AF_INET, SOCK_STREAM, 0);
        if (socketEscucha_ < 0) {
            lg(Fatal, "Fallo al crear socket, errno: %d, error: %s", errno, strerror(errno));
            exit(ERROR_SOCKET);
        }
        lg(Info, "Socket de escucha creado, descriptor: %d", socketEscucha_);
        
        int opcionReutilizacion = 1;
        setsockopt(socketEscucha_, SOL_SOCKET, SO_REUSEADDR | SO_REUSEPORT, &opcionReutilizacion, sizeof(opcionReutilizacion));
        
        struct sockaddr_in direccionServidor;
        memset(&direccionServidor, 0, sizeof(direccionServidor));
        direccionServidor.sin_family = AF_INET;
        direccionServidor.sin_port = htons(puerto_);
        direccionServidor.sin_addr.s_addr = inet_addr(ip_.c_str());
        
        socklen_t longitudDireccion = sizeof(direccionServidor);
        if (bind(socketEscucha_, (struct sockaddr*)&direccionServidor, longitudDireccion) < 0) {
            lg(Fatal, "Error en bind, errno: %d, error: %s", errno, strerror(errno));
            exit(ERROR_ENLACE);
        }
        lg(Info, "Socket enlazado correctamente");
        
        if (listen(socketEscucha_, MAX_CONEXIONES) < 0) {
            lg(Fatal, "Fallo en listen, errno: %d, error: %s", errno, strerror(errno));
            exit(ERROR_ESCUCHA);
        }
        lg(Info, "Servidor en modo escucha");
    }
    
    void atenderCliente(int descriptorCliente, string ipCliente, uint16_t puertoCliente) {
        while (true) {
            char buffer[4096];
            ssize_t leidos = read(descriptorCliente, buffer, sizeof(buffer) - 1);
            if (leidos > 0) {
                buffer[leidos] = '\0';
                cout << "Cliente dice: " << buffer << endl;
                string respuesta = "Servidor responde: ";
                respuesta += buffer;
                write(descriptorCliente, respuesta.c_str(), respuesta.size());
            } else if (leidos == 0) {
                lg(Info, "Cliente %s:%d desconectado, cerrando descriptor %d", ipCliente.c_str(), puertoCliente, descriptorCliente);
                break;
            } else {
                lg(Info, "Error de lectura en cliente %s:%d, cerrando descriptor %d", ipCliente.c_str(), puertoCliente, descriptorCliente);
                break;
            }
        }
    }
    
    void iniciar() {
        lg(Info, "Servidor TCP ejecutándose");
        while (true) {
            struct sockaddr_in clienteDireccion;
            socklen_t longitudCliente = sizeof(clienteDireccion);
            
            int descriptorCliente = accept(socketEscucha_, (struct sockaddr*)&clienteDireccion, &longitudCliente);
            if (descriptorCliente < 0) {
                lg(Warning, "Fallo en accept, errno: %d, error: %s", errno, strerror(errno));
                continue;
            }
            
            uint16_t puertoCliente = ntohs(clienteDireccion.sin_port);
            string ipCliente = inet_ntoa(clienteDireccion.sin_addr);
            lg(Info, "Nueva conexión desde %s:%d, descriptor: %d", ipCliente.c_str(), puertoCliente, descriptorCliente);
            
            pid_t idProceso = fork();
            if (idProceso == 0) {
                close(socketEscucha_);
                if (fork() > 0) exit(0);
                atenderCliente(descriptorCliente, ipCliente, puertoCliente);
                close(descriptorCliente);
                exit(0);
            }
            close(descriptorCliente);
            waitpid(idProceso, nullptr, 0);
        }
    }
    
private:
    int socketEscucha_;
    string ip_;
    uint16_t puerto_;
};

Enfoque con hilos múltiples

Los hilos ofrecen una alternativa más eficiente, evittando los costos de creación de procesos. Se utiliza una estructura para pasar datos al hilo, y una función estática como punto de entrada, ya que pthread_create requiere una función con firma compatible.

Ejemplo de código con hilos:

#include <iostream>
#include <string>
#include <cstring>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <pthread.h>
#include "log.hpp"
extern Log lg;
using namespace std;

const int COLA_CONEXIONES = 10;
const string DIRECCION_DEFECTO = "0.0.0.0";
const uint16_t PUERTO_BASE = 8080;

enum CodigosError {
    ERR_USO = 1,
    ERR_CREACION_SOCKET,
    ERR_ENLACE,
    ERR_ESCUCHA
};

class ServidorRed;

struct DatosHilo {
    DatosHilo(int desc, const string& ip, uint16_t puerto, ServidorRed* servidor)
        : descriptorCliente(desc), ipCliente(ip), puertoCliente(puerto), punteroServidor(servidor) {}
    
    int descriptorCliente;
    string ipCliente;
    uint16_t puertoCliente;
    ServidorRed* punteroServidor;
};

class ServidorRed {
public:
    ServidorRed(uint16_t puerto = PUERTO_BASE)
        : puertoServicio_(puerto), direccionIP_(DIRECCION_DEFECTO) {}
    
    void configurar() {
        socketServidor_ = socket(AF_INET, SOCK_STREAM, 0);
        if (socketServidor_ < 0) {
            lg(Fatal, "No se pudo crear socket, errno: %d, mensaje: %s", errno, strerror(errno));
            exit(ERR_CREACION_SOCKET);
        }
        lg(Info, "Socket creado exitosamente, descriptor: %d", socketServidor_);
        
        int habilitarReutilizacion = 1;
        setsockopt(socketServidor_, SOL_SOCKET, SO_REUSEADDR | SO_REUSEPORT, &habilitarReutilizacion, sizeof(habilitarReutilizacion));
        
        struct sockaddr_in servidorAddr;
        memset(&servidorAddr, 0, sizeof(servidorAddr));
        servidorAddr.sin_family = AF_INET;
        servidorAddr.sin_port = htons(puertoServicio_);
        servidorAddr.sin_addr.s_addr = inet_addr(direccionIP_.c_str());
        
        socklen_t tamanoDireccion = sizeof(servidorAddr);
        if (bind(socketServidor_, (struct sockaddr*)&servidorAddr, tamanoDireccion) < 0) {
            lg(Fatal, "Error al enlazar, errno: %d, mensaje: %s", errno, strerror(errno));
            exit(ERR_ENLACE);
        }
        lg(Info, "Enlace completado");
        
        if (listen(socketServidor_, COLA_CONEXIONES) < 0) {
            lg(Fatal, "Fallo al escuchar, errno: %d, mensaje: %s", errno, strerror(errno));
            exit(ERR_ESCUCHA);
        }
        lg(Info, "Escuchando conexiones");
    }
    
    void procesarConexion(int descriptor, string ip, uint16_t puerto) {
        while (true) {
            char mensaje[4096];
            ssize_t cantidad = read(descriptor, mensaje, sizeof(mensaje) - 1);
            if (cantidad > 0) {
                mensaje[cantidad] = '\0';
                cout << "Recibido: " << mensaje << endl;
                string eco = "Eco del servidor: ";
                eco += mensaje;
                write(descriptor, eco.c_str(), eco.size());
            } else if (cantidad == 0) {
                lg(Info, "Conexión con %s:%d terminada, descriptor %d cerrado", ip.c_str(), puerto, descriptor);
                break;
            } else {
                lg(Info, "Lectura fallida para %s:%d, descriptor %d cerrado", ip.c_str(), puerto, descriptor);
                break;
            }
        }
    }
    
    static void* rutinaHilo(void* argumento) {
        pthread_detach(pthread_self());
        DatosHilo* datos = static_cast<DatosHilo*>(argumento);
        datos->punteroServidor->procesarConexion(datos->descriptorCliente, datos->ipCliente, datos->puertoCliente);
        close(datos->descriptorCliente);
        delete datos;
        return nullptr;
    }
    
    void ejecutar() {
        lg(Info, "Servidor en operación");
        while (true) {
            struct sockaddr_in clienteAddr;
            socklen_t tamanoCliente = sizeof(clienteAddr);
            
            int descCliente = accept(socketServidor_, (struct sockaddr*)&clienteAddr, &tamanoCliente);
            if (descCliente < 0) {
                lg(Warning, "Error en accept, errno: %d, mensaje: %s", errno, strerror(errno));
                continue;
            }
            
            uint16_t puertoCliente = ntohs(clienteAddr.sin_port);
            string ipCliente = inet_ntoa(clienteAddr.sin_addr);
            lg(Info, "Cliente conectado desde %s:%d, descriptor: %d", ipCliente.c_str(), puertoCliente, descCliente);
            
            DatosHilo* datos = new DatosHilo(descCliente, ipCliente, puertoCliente, this);
            pthread_t hiloId;
            pthread_create(&hiloId, nullptr, rutinaHilo, datos);
        }
    }
    
private:
    int socketServidor_;
    string direccionIP_;
    uint16_t puertoServicio_;
};

Archivos adicionales del proyecto

Código del cliente:

#include <iostream>
#include <string>
#include <unistd.h>
#include <cstring>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
using namespace std;

void mostrarAyuda(const string& nombrePrograma) {
    cout << "\n\tUso: " << nombrePrograma << " ip_servidor puerto" << endl;
}

int main(int argc, char* argv[]) {
    if (argc != 3) {
        mostrarAyuda(argv[0]);
        return 0;
    }
    
    string servidorIp = argv[1];
    uint16_t servidorPuerto = stoi(argv[2]);
    
    int miSocket = socket(AF_INET, SOCK_STREAM, 0);
    if (miSocket < 0) {
        cerr << "Error al crear socket" << endl;
        return 1;
    }
    
    struct sockaddr_in servidorAddr;
    servidorAddr.sin_family = AF_INET;
    servidorAddr.sin_port = htons(servidorPuerto);
    servidorAddr.sin_addr.s_addr = inet_addr(servidorIp.c_str());
    socklen_t addrLen = sizeof(servidorAddr);
    
    if (connect(miSocket, (struct sockaddr*)&servidorAddr, addrLen) < 0) {
        perror("Fallo en connect");
        cerr << "No se pudo establecer conexión" << endl;
        return 2;
    }
    
    string lineaEntrada;
    char bufferRespuesta[4096];
    while (true) {
        cout << "Ingrese texto: ";
        getline(cin, lineaEntrada);
        
        ssize_t escritos = write(miSocket, lineaEntrada.c_str(), lineaEntrada.size());
        if (escritos < 0) {
            cerr << "Error al enviar datos" << endl;
            break;
        }
        
        ssize_t leidos = read(miSocket, bufferRespuesta, sizeof(bufferRespuesta) - 1);
        if (leidos > 0) {
            bufferRespuesta[leidos] = '\0';
            cout << "Respuesta: " << bufferRespuesta << endl;
        } else {
            break;
        }
    }
    
    close(miSocket);
    return 0;
}

Archivo principal del servidor:

#include <iostream>
#include <memory>
#include "server.hpp"

void instruccionesUso(const std::string& programa) {
    std::cout << "\n\tUso: " << programa << " puerto[1024+]\n" << std::endl;
}

int main(int argc, char* argv[]) {
    if (argc != 2) {
        instruccionesUso(argv[0]);
        exit(ERR_USO);
    }
    
    uint16_t puertoSeleccionado = std::stoi(argv[1]);
    std::unique_ptr<ServidorRed> servidor(new ServidorRed(puertoSeleccionado));
    servidor->configurar();
    servidor->ejecutar();
    
    return 0;
}

Makefile para compilación:

.PHONY: todos limpiar

todos: servidor cliente

servidor: main.cpp
	g++ -o servidor main.cpp -std=c++11 -lpthread

cliente: client.cpp
	g++ -o cliente client.cpp -std=c++11

limpiar:
	rm -f servidor cliente

Etiquetas: linux tcp procesos hilos sockets

Publicado el 5-29 11:10