...exactamente la imagen que te esperas en un blog de programación... |
Y ahora vamos al grano, ¡vamos con el código!
#include <stdio.h> #include <stdlib.h> #include <string.h> #include <arpa/inet.h> #include <errno.h> #include <pthread.h> #define BACKLOG 10 // para listen() #define MYBUFSIZE 1024 // prototipos locales void *connHandler(void *conn_sock); int main(int argc, char *argv[]) { // test argumentos if (argc != 2) { // error args printf("%s: numero argumentos equivocado\n", argv[0]); printf("uso: %s port [i.e.: %s 9999]\n", argv[0], argv[0]); return EXIT_FAILURE; } // crea un socket int my_socket; if ((my_socket = socket(AF_INET, SOCK_STREAM, 0)) == -1) { // errore socket() printf("%s: could not create socket (%s)\n", argv[0], strerror(errno)); return EXIT_FAILURE; } // prepara la struct sockaddr_in para este server struct sockaddr_in server; // (local) server socket info server.sin_family = AF_INET; server.sin_addr.s_addr = INADDR_ANY; server.sin_port = htons(atoi(argv[1])); // bind informaciones del server al socket if (bind(my_socket, (struct sockaddr *)&server, sizeof(server)) == -1) { // error bind() printf("%s: bind failed (%s)", argv[0], strerror(errno)); return EXIT_FAILURE; } // start escucha con una cola de max BACKLOG conexiones if (listen(my_socket, BACKLOG) == -1) { // error listen() printf("%s: listen failed (%s)\n", argv[0], strerror(errno)); return EXIT_FAILURE; } // acepta conexiones de un client entrante printf("%s: espera conexiones entrantes...\n", argv[0]); pthread_t thread_id; socklen_t socksize = sizeof(struct sockaddr_in); struct sockaddr_in client; // (remote) client socket info int client_sock; while ((client_sock = accept(my_socket, (struct sockaddr *)&client, &socksize)) != -1) { printf("%s: conexion aceptada\n", argv[0]); if (pthread_create(&thread_id, NULL, &connHandler, (void*)&client_sock) == -1) { // error pthread_create() printf("%s: pthread_create failed (%s)\n", argv[0], strerror(errno)); return EXIT_FAILURE; } } // error accept() printf("%s: accept failed (%s)\n", argv[0], strerror(errno)); return EXIT_FAILURE; } // thread function para gestión conexiones void *connHandler(void *conn_sock) { // extrae el client socket del argumento int client_sock = *(int*)conn_sock; // loop di recepción mensajes del client int read_size; char client_msg[MYBUFSIZE]; while ((read_size = recv(client_sock, client_msg, MYBUFSIZE, 0)) > 0 ) { // send mensaje de retorno al client printf("%s: recibido mensaje del sock %d: %s\n", __func__, client_sock, client_msg); char server_msg[MYBUFSIZE]; sprintf(server_msg, "me has escrito: %s", client_msg); send(client_sock, server_msg, strlen(server_msg), 0); // clear buffer memset(client_msg, 0, MYBUFSIZE); } // loop terminado: test motivo if (read_size == -1) { // error recv() printf("%s: recv failed\n", __func__); } else { // read_size == 0: el client se ha desconectado printf("%s: client disconnected\n", __func__); } return NULL; }
Bueno, no vamos a contar otra vez cómo funciona un Socket Server (ya hecho en el antiguo post, releerlo, please), pero vamos a centrarnos en las diferencias entre el codigo single-thread y el multithread: seguramente habeis notado que son prácticamente idénticos hasta la fase de listen(), e, incluso después, las diferencias son mínimas: la fase de accept() está ahora en un loop y se crea un nuevo thread para cada conexión aceptada (de un Client remoto). ¿Y qué hace el thread? Ejecuta la función connHandler() que contiene, por cierto, el loop de recv() que en el código antiguo se ejecutava inmediatamente después de la fase de accept(). tambien el siguiente test del motivo de salida (antes de tiempo) del loop está contenido en connHandler(), y enseña la señal de error correcta (recv() error o client disconnected, dependiendo del código devuelto por la recv()).
¿Qué añadir? Simple y super-funcional: ¡un Socket Server multithread con cuatro líneas de código! Por supuesto, la sintaxis de creación de los thread y la ejecución de la start_routine del mismo son idénticas a las descritas aquí. Para probar el nuestro Socket Server, también es necesario compilar un Socket Client (por supuesto el que se describe en mi antiguo post El Client oscuro: la leyenda renace) y ejecutar, por ejemplo, una instancia del Socket Server y dos instancias del Socket Client (en tres terminales diferentes de la misma máquina, o en tres máquinas diferentes). Ejecutando en mi máquina (Linux, por supuesto) en tres terminales, el resultado es el siguiente:
En la terminal 1:
aldo@ao-linux-nb:~/blogtest$ ./sockserver-mt 9999 ./sockserver-mt: espera conexiones entrantes... ./sockserver-mt: conexión aceptada ./sockserver-mt: conexión aceptada connHandler: recibido mensaje del sock 4: pippo connHandler: recibido mensaje del sock 5: pluto connHandler: client disconnected connHandler: client disconnected
En la terminal 2:
aldo@ao-linux-nb:~/blogtest$ ./sockclient 127.0.0.1 9999 Escribe un mensaje para el Server remoto: pippo ./sockclient: Server reply: me has escrito: pippo Escribe un mensaje para el Server remoto: ^C aldo@ao-linux-nb:~/blogtest$
En la terminal 3:
aldo@ao-linux-nb:~/blogtest$ ./sockclient 127.0.0.1 9999 Escribe un mensaje para el Server remoto: pluto ./sockclient: Server reply: me has escrito: pluto Escribe un mensaje para el Server remoto: ^C aldo@ao-linux-nb:~/blogtest$
notar que cuando uno de los Client sale (con un CTRL-C, por ejemplo) el Server se da cuenta y enseña, como era de esperar, client disconnected... perfecto.
Ok, hemos acabado con los thread. Ahora intentaré pensar en algún nuevo tema interesante para el próximo post. Como siempre os recomiendo de no aguantar la respiración en la espera...
¡Hasta el próximo post!