Herramientas de usuario

Herramientas del sitio


sockets-jo

jsockets: clientes/servidores de red

Baje el archivo jsockets4.tgz que contiene los programas de la primera parte de este capítulo:

  • jsocket.h
  • libjsocket.c
  • server_echo.c
  • client_echo.c
  • client_echo2.c
  • server_echo2.c
  • server_echo2.5.c
  • server_echo3.c
  • server_chat.c
  • client_chat.c
  • Makefile

Servidor de Eco Mono-Cliente

En esta parte usaremos una API simplificada de sockets que llamamos “jsocket4”, que incluye soporte transparente para IPv4.

La idea es que necesitamos un nombre de computador (como “dcc.uchile.cl”) y un número de port (como 1818) para encontrar un servidor particular. A lo largo de los ejemplos usaremos “localhost” como nombre de computador y 1818 como port. Eso permite probar los programas en un computador que ni siquiera está conectado a Internet.

Este es el primer servidor que escribiremos, que simplemente responde todo lo que recibe: server_echo.c

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include "jsocket.h"
 
/*
 * server_echo: servidor de echo mono-cliente
 */
 
#define BUF_SIZE 200
 
int main() {
  int s, s2;
  int cnt, size = BUF_SIZE;
  char buf[BUF_SIZE];
 
  s = j_socket();
 
  if (j_bind(s, 1818) < 0) {
    fprintf(stderr, "bind failed\n");
    exit(1);
  }
 
  for (;;) {
    s2 = j_accept(s);
    fprintf(stderr, "cliente conectado: %d\n", s2);
    while ((cnt=read(s2, buf, size)) > 0)
      write(s2, buf, cnt);
    close(s2);
    fprintf(stderr, "cliente desconectado\n");
  }
 
  return 0;
}

Compile este programa con el Makefile provisto y ejecútelo:

  % cd .../jsockets
  % make server_echo
  % ./server_echo

Este programa no termina, si no que permanece a la espera de conectarse con los clientes. El servidor se puede probar directamente vía telnet en la misma máquina, pero en un shell distinto:

% telnet localhost 1818
hola
hola
...

La primera forma de terminar este cliente es invocando kill -TERM seguido del identificador de proceso que Ud. debe ubicar invocando ps -aux | grep telnet. La segunda forma de terminarlo matando el propio servidor con control-C en el respectivo shell.

Pero también podemos hacer un cliente que envía “hola” al servidor y luego lee todo lo que se envía de vuelta (es decir “hola”) y termina.

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include "jsocket.h"
 
/*
 * client_echo: envía “hola” al servidor y luego lee todo
 * lo que se envía de vuelta (es decir “hola”) y termina
 */
 
#define BUF_SIZE 10
 
int main() {
  int s;
  int cnt, n, size = BUF_SIZE;
  char buf[BUF_SIZE];
 
  s = j_socket();
 
  if (j_connect(s, "localhost", 1818) < 0) {
    fprintf(stderr, "connection refused\n");
    exit(1);
  }
 
  write(s, "hola", 5);
  n = 0;
  while ((cnt=read(s, buf+n, size-n)) > 0) {
    n += cnt;
    if(n >= 5) break;
  }
 
  if (n < 5)
    printf("fallo el read\n");
  else
    printf("%s\n", buf);
 
  close(s);
 
  return 0;
}

Compile este programa con make client_echo y ejecútelo con ./client_echo.

O uno que envía toda su entrada estándar al servidor y todo lo que recibe de vuelta lo envía a su salida estándar:

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <errno.h>
 
#include "jsocket.h"
 
#define BUF_SIZE 1024
 
int main() {
   int s;
   int rc;
   int cnt, cnt2;
   char buf[BUF_SIZE];
 
   s = j_socket();
 
   rc= j_connect(s, "localhost", 1818);
   if (rc<0) {
        perror("connect");
        exit(1);
   }
 
   while ((cnt=read(0, buf, BUF_SIZE)) > 0) {
        if(write(s, buf, cnt) != cnt) {
           perror("write");
           exit(1);
        }
        while(cnt > 0) {
           cnt2=read(s, buf, BUF_SIZE);
           if (cnt2<=0) {
             if (cnt2==0)
               fprintf(stderr, "El socket fue cerrado por el servidor\n");
             else
               perror("read");
             return 1;
           }
           write(1, "echo: ", 6);
           write(1, buf, cnt2);
           cnt -= cnt2;
        }
   }
 
   return 0;
}

Este programa se comunica con el servidor de echo hasta que el usuario ingrese control-D (fin de la entrada estándar). Compílelo con make client_echo2 y ejecútelo con ./client_echo2.

Servidor Multi-Clientes con Fork

El problema de esa solución de servidor es que no puede atender otro cliente mientras está con uno conectado. Para soportar múltiples clientes simultáneos, podemos crear un hijo por cada cliente que se encargue de atenderlo:

#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>
#include <errno.h>
 
#include "jsocket.h"
 
/*
 * server_echo2: servidor de echo multi-cliente usando procesos pesados
 * - si un hijo muere, no importa
 * - si mato con ctrl-C se mueren todos: para independizarse del terminal
 *   debieramos usar setpgrp()
 */
 
#define BUF_SIZE 200
 
/*
 * Esta es la forma mas simple de enterrar a los hijos sin complicarse la vida
 */
void child() {
  int status;
  while (waitpid(-1, &status, WNOHANG)>0)
    ;
  signal(SIGCHLD, child);
}
 
 
/*
 * Este es el servidor y el codigo para un socket cliente ya conectado: s
 */
void serv(int s) {
  int cnt, size = BUF_SIZE;
  char buf[BUF_SIZE];
 
  fprintf(stderr, "cliente conectado\n");
  while ((cnt=read(s, buf, size)) > 0)
    write(s, buf, cnt);
  fprintf(stderr, "cliente desconectado\n");
}
 
/*
 * Este es el principal: solo acepta conexiones y crea a los hijos servidores
 */
int main() {
  int s, s2;
 
  signal(SIGCHLD, child);
 
  s = j_socket();
 
  if (j_bind(s, 1818) < 0) {
    fprintf(stderr, "bind failed\n");
    exit(1);
  }
 
  for (;;) {
    s2 = j_accept(s);
    if (s2>0) {
      if (fork() == 0) { /* Este es el hijo */
        close(s); /* cerrar el socket que no voy a usar */
        serv(s2);
        exit(0);
      }
      else /* es el padre */
        close(s2); /* cerrar el socket que no voy a usar */
    }
    else if (errno!=EINTR) {
      perror("accept");
      break;
    }
  }
 
  return 1;
}

Si quiséramos limitar el máximo de procesos hijos vivos que mantenemos:

#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>
#include <errno.h>
 
#include "jsocket.h"
 
/*
 * server_echo2.5: servidor de echo multi-cliente usando procesos pesados
 * - si un hijo muere, no importa
 * - si mato con ctrl-C se mueren todos: para independizarse del terminal
 *   debieramos usar setpgrp()
 * - Este tiene un control de hijos: acepta máximo MAX_PROCS
 *   clientes simultáneos
 */
 
#define BUF_SIZE 200
#define MAX_PROCS 2
 
/*
 * Esta es la forma mas simple de enterrar a los hijos sin complicarse la vida
 */
 
int chld_cnt = 0;
 
void child() {
  int status;
 
  while (waitpid(-1, &status, WNOHANG)>0)
    chld_cnt--; /* Aca puede ocurrir un datarace */
  signal(SIGCHLD, child);
}
 
/*
 * Este es el servidor y el codigo para un socket cliente ya conectado: s
 */
void serv(int s) {
  int cnt, size = BUF_SIZE;
  char buf[BUF_SIZE];
 
  fprintf(stderr, "cliente conectado\n");
  while ((cnt=read(s, buf, size)) > 0)
    write(s, buf, cnt);
  fprintf(stderr, "cliente desconectado\n");
}
 
/*
 * Este es el servidor y el codigo para un socket cliente ya conectado: s
 */
void serv(int s) {
  int cnt, size = BUF_SIZE;
  char buf[BUF_SIZE];
 
  fprintf(stderr, "cliente conectado\n");
  while ((cnt=read(s, buf, size)) > 0)
    write(s, buf, cnt);
  fprintf(stderr, "cliente desconectado\n");
}
 
/*
 * Este es el principal: solo acepta conexiones y crea a los hijos servidores
 */
int main() {
  int s, s2;
 
  signal(SIGCHLD, child);
 
  s = j_socket();
 
  if (j_bind(s, 1818) < 0) {
    fprintf(stderr, "bind failed\n");
    exit(1);
  }
 
  for (;;) {
    s2 = j_accept(s);
    if (s2>0) {
      if (chld_cnt >= MAX_PROCS) {
        fprintf(stderr, "client rechazado\n");
        close(s2);
        continue;
      }
      chld_cnt++; /* Si justo llega SIGCHLD puede haber un datarace */
      if (fork() == 0) { /* Este es el hijo */
        close(s); /* cerrar el socket que no voy a usar */
        serv(s2);
        exit(0);
      }
      else
        close(s2); /* cerrar el socket que no voy a usar */
    }
    else if (errno!=EINTR) {
      perror("accept");
      break;
    }
  }
 
  return 1;
}

Ejercicio resuelto: Servidor para cat remoto

Estudie los programas servidor-cat.c y cat-remoto.c en este link. Esta es la solución de la tarea 4 del semestre Primavera de 2012.

Servidor Multi-Clientes con Threads

Implementaremos un servidor de chat con su respectivo cliente. Pero previamente necesitamos resolver el problema para threads.

Difusión de mensajes entre threads

Se desea implementar la siguiente API:

  • void broadcast(char *buf, int n): difunde los n caracteres contenidos en buf a todos los threads que estén esperándolo con una invocación de get_message. La primera difusión emite un mensaje identificado como 0, la segunda el mensaje 1, etc. Los mensajes son de hasta BUF_SIZE bytes.
  • int get_message(char *buf, int id): se bloquea a la espera del mensaje número id. Si el mensaje ya fue emitido, retorna de inmediato. El mensaje se deposita a partir de buf y se retorna su tamaño en bytes.

He aquí la solución usando monitores:

  #define BUF_SIZE 200
  #define N 10

  typedef struct {
    char buf[BUF_SIZE];
    int size;
  } Message;

  pthread_mutex_t mutex= PTHREAD_MUTEX_INITIALIZER;
  pthread_cond_t cond= PTHREAD_COND_INITIALIZER;

  Message messages[N];
  int next_message= 0;

  void broadcast(char *buf, int size) {
    Message *msg;
    pthread_mutex_lock(&mutex);
    msg= &messages[next_message%N];
    memcpy(msg->buf, buf, size);
    msg->size= size;
    next_message++;
    pthread_cond_broadcast(&cond);
    pthread_mutex_unlock(&mutex);
  }

  int get_message(char *buf, int msg_id) {
    int size;
    Message *msg;
    pthread_mutex_lock(&mutex);
    while (msg_id>=next_message) {
      printf("waiting\n");
      pthread_cond_wait(&cond, &mutex);
    }
    printf("got message\n");
    msg= &messages[msg_id%N];
    size= next_message-msg_id>N ? 0 : msg->size;
    memcpy(buf, msg->buf, size);
    pthread_mutex_unlock(&mutex);
    return size;
  }

Difusión entre procesos pesados: chat

Ahora usaremos la API anterior para implementar el sistema de chat. Por cada cliente de chat se crean 2 threads: uno ejecuta writer_fun y el otro serv. El primero se encarga de enviar los mensajes recibidos hacia el cliente y el segundo de recibir los mensajes de ese cliente.

  void *writer_fun(long s) {
    char buf[BUF_SIZE];
    int curr= next_message;
    for (;;) {
      int cnt= get_message(buf, curr);
      if (cnt>0) {
        printf("writing message %d (%d bytes)\n", curr, cnt);
        if (write(s, buf, cnt)<=0) {
          printf("broken pipe\n");
          return NULL; /* Broken pipe */
        }
      }
      curr++;
    }
  }
  typedef void *(*Thread_fun)(void *);
 
  void *serv(long s) {
    int cnt, size = BUF_SIZE;
    char buf[BUF_SIZE];
    pthread_t writer;
 
    if (pthread_create(&writer, NULL, (Thread_fun)writer_fun, (void*)s) != 0) {
      fprintf(stderr, "No pude crear un writer\n");
      return NULL;
    }
 
    fprintf(stderr, "cliente conectado\n");
    while ((cnt= read(s, buf, size)) > 0) {
      fwrite(buf, cnt, 1, stdout);
      broadcast(buf, cnt);
    }
 
    close(s);
    pthread_join(writer, NULL);
    fprintf(stderr, "cliente desconectado\n");
    return NULL;
  }

Este es el servidor que se encarga esperar la conexión de los clientes. Para no tener que enterrar los threads creados, se usa el atributo PTHREAD_CREATE_DETACHED al lanzarlo.

  int main(int argc, char **argv) {
    long s, s2;
    pthread_t pid;
    int port= argc>=2 ? atoi(argv[1]) : 1818;
 
    signal(SIGPIPE, SIG_IGN);
 
    s = j_socket();
 
    if(j_bind(s, port) < 0) {
      fprintf(stderr, "bind failed\n");
      exit(1);
    }
 
    /* Cada vez que se conecta un cliente le creo un thread */
    for(;;) {
      pthread_attr_t attr;
      s2= j_accept(s);
      pthread_attr_init(&attr);
      if (pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED) != 0) {
        fprintf(stderr, "No se puede establecer el atributo\n");
      }
      if ( pthread_create(&pid, &attr, (Thread_fun)serv, (void *)s2) != 0) {
        fprintf(stderr, "No pude crear thread para nuevo cliente %ld!!!\n", s2);
        close(s2);
      }
      pthread_attr_destroy(&attr);
    }
 
    return 0;
  }

Cliente de chat multi-thread

El cliente de chat usa un thread para mandar al servidor todo lo que lee de la consola y otro thread para mandar a la consola todo lo que lee del servidor.

  typedef void *(*Thread_fun)(void *);
 
  #define BUF_SIZE 200
 
  void *reader_fun(long s) {
    int cnt;
    char buf[BUF_SIZE];
    printf("reader started for %ld\n", s);
    while ((cnt= read(s, buf, BUF_SIZE))>0)
      write(1, buf, cnt);
 
    printf("reader finished\n");
    return NULL;
  }
 
  int main(int argc, char **argv) {
    long s;
    int cnt;
    char buf[BUF_SIZE];
    pthread_t reader;
    char *host= argc>=2 ? argv[1] : "localhost";
    int port= argc>=3 ? atoi(argv[2]) : 1818;
 
    s = j_socket();
 
    if(j_connect(s, host, port) < 0) {
      fprintf(stderr, "connection refused\n");
      exit(1);
    }
 
    if (pthread_create(&reader, NULL, (Thread_fun)reader_fun, (void*)s) != 0) {
      fprintf(stderr, "No puede crear thread lector\n");
      exit(1);
    }
 
    while ((cnt= read(0, buf, BUF_SIZE)) > 0) {
      if (write(s, buf, cnt) != cnt) {
        fprintf(stderr, "Fallo el write al servidor\n");
        break;
      }
    }
 
    close(s);
 
    pthread_join(reader, NULL);
 
    printf("Client finished\n");
 
    return 0;
  }

Ejercicios propuestos

  • Modifique el servidor de chat para que no reenvíe el mensaje al cliente que lo envía.
  • Modifique el cliente de chat para que agregue el nombre del usuario a todos los mensajes.

Ejercicios resueltos:

  • Servidor para un diccionario inter procesos. Estudie los programas serv-dict.c, serv-dict2.c y dict.c en este link en formato tar comprimido con gzip. El programa serv-dict.c es una solución incompleta de la tarea 4 del semestre Otoño de 2013. Es incompleta porque si un cliente consulta una llave y la llave no está definida, el cliente no espera. La solución completa es serv-dict2.c. Además los comandos usados son d para definir (no def), q para consultar (no query) y e para eliminar.
  • Determinar si un número es primo buscando algún factor. Estudie la solución del control 3 del semestre Primavera 2013. Pruebe las distintas implementaciones con números primos sacados de la wikipedia.

Servidor Multi-cliente con select

Atendemos todos los clientes en el mismo ciclo, usando select. Por simplicidad volveremos a la funcionalidad del cliente de echo, es decir mandamos de vuelta el mensaje solo al cliente que lo mandó.

#include <stdio.h>
#include <stdlib.h>
#include "jsocket.h"
#include <sys/time.h>
#include <sys/types.h>
#include <unistd.h>
#include <signal.h>
 
/*
 * server_echo3: servidor multi-clientes usando select
 */
 
/*
       int select(int numfds, fd_set *readfds, fd_set *writefds ,
       fd_set *exceptfds, struct timeval * timeout);
 
       FD_CLR(int fd, fd_set *set);
       FD_ISSET(int fd, fd_set *set);
       FD_SET(int fd, fd_set *set);
       FD_ZERO(fd_set * set);
*/
 
#define BUF_SIZE 200
#define MAX_CLIENTS 2
 
main() {
   int s, s2;
   int cnt, size = BUF_SIZE;
   char buf[BUF_SIZE];
   int ret;
   fd_set mask;
   int client[MAX_CLIENTS];
   int i;
 
/* para no morirme al morir un cliente */
   signal(SIGPIPE, SIG_IGN);
 
/* Arreglo de sockets de los clientes */
   for( i = 0; i < MAX_CLIENTS; i++ )
	client[i] = -1;
 
   s = j_socket();
 
   if(j_bind(s, 1818) < 0) {
	fprintf(stderr, "bind failed\n");
	exit(1);
   }
 
   for(;;) {
	FD_ZERO(&mask);
	FD_SET(s, &mask);	/* el socket de accept */
	for(i = 0; i < MAX_CLIENTS; i++) /* cada socket cliente */
		if( client[i] != -1 )
		    FD_SET(client[i], &mask);
 
/* espero que haya actividad en algun socket */
	ret = select( getdtablesize(), &mask, NULL, NULL, NULL);
 
	if( ret <= 0 ) { perror("select"); exit(-1);}
 
/* Atendemos los clientes con datos */
	for(i = 0; i < MAX_CLIENTS; i++) {
	    if(client[i] != -1 && FD_ISSET(client[i], &mask)) {
		if( (cnt=read(client[i], buf, size)) > 0) && 
	    	    write(client[i], buf, cnt) > 0)
			;
		else { /* murio un cliente */
		    fprintf(stderr, "cliente desconectado\n");
		    close(client[i]);
		    client[i] = -1;
		}
	    }
	}
 
/* aceptamos conexion pendiente si hay una */
	if(FD_ISSET(s, &mask)) {
	    for( i = 0; i < MAX_CLIENTS; i++ )
		if(client[i] == -1) break;
	    if(i == MAX_CLIENTS) {
		fprintf(stderr, "No mas clientes!\n");
		close(j_accept(s)); /* lo rechazo */
		continue;
	    }
	    client[i] = j_accept(s);
	    fprintf(stderr, "cliente conectado\n");
	}
   }
}

Ejercicios

  1. Convierta el servidor anterior en un servidor de chat, sin hacer uso de threads adicionales o fork.
  2. Reprograme el cliente de chat de modo que use select en vez de lanzar threads.

Un problema que se puede dar en cuanto a desempeño de su solución es que si un cliente se demora en leer, por problemas de red por ejemplo, cuando su servidor escriba en ese socket, el buffer asociado estará lleno y se bloqueará hasta que ese cliente lea efectivamente. El problema es que si el servidor se bloquea, entonces se bloquea para todos los clientes. Select también se puede usar para evitar escribir en sockets cuyo buffer está lleno. Piense en como usar está característica de select para evitar el bloqueo del servidor con un cliente específico. ¡Cuidado! Implementarlo es complejo. Esa es la gracia del servidor de chat con threads: nunca se bloquea para todos los clientes.

sockets-jo.txt · Última modificación: 2018/08/16 10:04 por lmateu