Prerrequisito: Programación de sockets en C/C++ , fork() Llamada al sistema
Declaración del problema: en este artículo, vamos a escribir un programa que ilustra el modelo cliente-servidor usando una llamada al sistema fork() que puede manejar varios clientes al mismo tiempo.
La llamada Fork() crea múltiples procesos secundarios para clientes concurrentes y ejecuta cada bloque de llamada en su propio bloque de control de procesos (PCB).
Necesidad de diseñar un servidor concurrente para manejar clientes usando la llamada fork():
A través del modelo TCP básico servidor-cliente, un servidor atiende a un solo cliente en un momento determinado .
Pero ahora estamos tratando de hacer que nuestro servidor TCP maneje más de un cliente. Aunque podemos lograr esto usando la llamada al sistema select() pero podemos facilitar todo el proceso.
¿Cómo va a ayudar la llamada al sistema fork() en esto?
Fork() crea un nuevo proceso secundario que se ejecuta en sincronización con su proceso principal y devuelve 0 si el proceso secundario se crea correctamente.
- Siempre que un nuevo cliente intente conectarse al servidor TCP, crearemos un nuevo proceso secundario que se ejecutará en paralelo con la ejecución de otros clientes. De esta forma, vamos a diseñar un servidor concurrente sin utilizar la llamada al sistema Select().
- Se utilizará un tipo de datos pid_t (Id. de proceso) para contener la Id. de proceso del niño. Ejemplo: pid_t = tenedor( ) .
Diferencia de los otros enfoques:
Esta es la técnica más simple para crear un servidor concurrente. Cada vez que un nuevo cliente se conecta al servidor, se ejecuta una llamada fork() creando un nuevo proceso secundario para cada nuevo cliente.
- Multi-Threading logra un servidor concurrente usando un solo programa procesado. Compartir datos/archivos con conexiones suele ser más lento con una bifurcación() que con subprocesos.
- La llamada al sistema Select() no crea múltiples procesos. En cambio, ayuda a multiplexar todos los clientes en un solo programa y no necesita E/S sin bloqueo.
Programa para diseñar un servidor concurrente para manejar múltiples clientes usando fork()
- Aceptar un cliente crea un nuevo proceso secundario que se ejecuta simultáneamente con otros clientes y el proceso principal
C
// Accept connection request from client in cliAddr // socket structure clientSocket = accept( sockfd, (struct sockaddr*)&cliAddr, &addr_size); // Make a child process by fork() and check if child // process is created successfully if ((childpid = fork()) == 0) { // Send a confirmation message to the client for // successful connection send(clientSocket, "hi client", strlen("hi client"), 0); }
- Implementación del servidor:
C
// Server side program that sends // a 'hi client' message // to every client concurrently #include <arpa/inet.h> #include <netinet/in.h> #include <stdio.h> #include <stdlib.h> #include <string.h> #include <sys/socket.h> #include <sys/types.h> #include <unistd.h> // PORT number #define PORT 4444 int main() { // Server socket id int sockfd, ret; // Server socket address structures struct sockaddr_in serverAddr; // Client socket id int clientSocket; // Client socket address structures struct sockaddr_in cliAddr; // Stores byte size of server socket address socklen_t addr_size; // Child process id pid_t childpid; // Creates a TCP socket id from IPV4 family sockfd = socket(AF_INET, SOCK_STREAM, 0); // Error handling if socket id is not valid if (sockfd < 0) { printf("Error in connection.\n"); exit(1); } printf("Server Socket is created.\n"); // Initializing address structure with NULL memset(&serverAddr, '\0', sizeof(serverAddr)); // Assign port number and IP address // to the socket created serverAddr.sin_family = AF_INET; serverAddr.sin_port = htons(PORT); // 127.0.0.1 is a loopback address serverAddr.sin_addr.s_addr = inet_addr("127.0.0.1"); // Binding the socket id with // the socket structure ret = bind(sockfd, (struct sockaddr*)&serverAddr, sizeof(serverAddr)); // Error handling if (ret < 0) { printf("Error in binding.\n"); exit(1); } // Listening for connections (upto 10) if (listen(sockfd, 10) == 0) { printf("Listening...\n\n"); } int cnt = 0; while (1) { // Accept clients and // store their information in cliAddr clientSocket = accept( sockfd, (struct sockaddr*)&cliAddr, &addr_size); // Error handling if (clientSocket < 0) { exit(1); } // Displaying information of // connected client printf("Connection accepted from %s:%d\n", inet_ntoa(cliAddr.sin_addr), ntohs(cliAddr.sin_port)); // Print number of clients // connected till now printf("Clients connected: %d\n\n", ++cnt); // Creates a child process if ((childpid = fork()) == 0) { // Closing the server socket id close(sockfd); // Send a confirmation message // to the client send(clientSocket, "hi client", strlen("hi client"), 0); } } // Close the client socket id close(clientSocket); return 0; }
- Implementación del cliente:
C
// Client Side program to test // the TCP server that returns // a 'hi client' message #include <arpa/inet.h> #include <netinet/in.h> #include <stdio.h> #include <stdlib.h> #include <string.h> #include <sys/socket.h> #include <sys/types.h> #include <unistd.h> // PORT number #define PORT 4444 int main() { // Socket id int clientSocket, ret; // Client socket structure struct sockaddr_in cliAddr; // char array to store incoming message char buffer[1024]; // Creating socket id clientSocket = socket(AF_INET, SOCK_STREAM, 0); if (clientSocket < 0) { printf("Error in connection.\n"); exit(1); } printf("Client Socket is created.\n"); // Initializing socket structure with NULL memset(&cliAddr, '\0', sizeof(cliAddr)); // Initializing buffer array with NULL memset(buffer, '\0', sizeof(buffer)); // Assigning port number and IP address serverAddr.sin_family = AF_INET; serverAddr.sin_port = htons(PORT); // 127.0.0.1 is Loopback IP serverAddr.sin_addr.s_addr = inet_addr("127.0.0.1"); // connect() to connect to the server ret = connect(clientSocket, (struct sockaddr*)&serverAddr, sizeof(serverAddr)); if (ret < 0) { printf("Error in connection.\n"); exit(1); } printf("Connected to Server.\n"); while (1) { // recv() receives the message // from server and stores in buffer if (recv(clientSocket, buffer, 1024, 0) < 0) { printf("Error in receiving data.\n"); } // Printing the message on screen else { printf("Server: %s\n", buffer); bzero(buffer, sizeof(buffer)); } } return 0; }
Compilar secuencia de comandos:
- Ejecutando código del lado del servidor
⇒ gcc servidor.c -o ser
./ser
- Ejecutando código del lado del cliente
⇒ gcc cliente.c -o cli
./cli
Producción:
Ventajas: Las ventajas de utilizar este proceso son:
- Fácil de implementar en un programa que realiza una tarea mucho más compleja.
- Cada proceso secundario (cliente) se ejecuta de forma independiente y no puede leer/escribir datos de otros clientes.
- El servidor se comporta como si solo tuviera un cliente conectado. Los procesos secundarios no necesitan preocuparse por otras conexiones entrantes o la ejecución de procesos secundarios paralelos. Por lo tanto, la programación con la llamada al sistema fork() es transparente y requiere menos esfuerzo.
Desventajas: Las desventajas son las mencionadas aquí .
- Fork es menos eficiente que los subprocesos múltiples porque crea una gran sobrecarga al crear un nuevo proceso, pero un subproceso es un proceso liviano que comparte los recursos del proceso principal.
- El sistema operativo necesitará un costo de sincronización o uso compartido de memoria para lograr la concurrencia.