Chat multihilo con Sockets en Python y en C

La computación concurrente es la capacidad de simultanear en la ejecución de múltiples tareas interactivas. Un proceso es un programa en ejecución, que es gestionado por el Sistema Operativo y compite por los recursos del procesador. Los procesos tienen estado y memoria en ejecución reservada. El mecanismo por el cual un proceso crea otro proceso se denomina bifurcación (fork). Cuando un proceso se bifurca, se crea una copia exacta del proceso en ejecución, independiente del resto y no comparte el espacio de memoria con el proceso que los ha creado ni con otros procesos.

Un hilo, en sistemas operativos, es una característica que permite a una aplicación realizar varias tareas a la vez (concurrentemente). Los distintos hilos de ejecución comparten una serie de recursos tales como el espacio de memoria, o los archivos abiertos. Esta técnica permite simplificar el diseño de una aplicación que debe llevar a cabo distintas funciones simultáneamente. Un hilo es un tarea que se ejecuta en paralelo con otra tarea.

La comunicación entre el cliente y el servidor se hace posible gracias a estructuras abstractas denominadas sockets, mediante las cuales los programas pueden intercambiar flujo de datos e información. Este concepto está asociado al concepto de puerto.

Un puerto es una forma genérica de denominar a una interfaz a través de la cual los diferentes tipos de datos se pueden enviar y recibir. En el protocolo TCP/IP, son de tipo lógico, por ejemplo, los puertos que permiten la transmisión de datos entre diferentes computadores.

Existen dos tipos de sockets, orientados a conexión y no orientados a conexión. La diferencia fundamental es que en los sockets orientados a conexión (TCP) el protocolo garantiza que los datos serán entregados en su destino sin errores y en el mismo orden en que se transmitieron, mientras que en los sockets no orientados a conexión (o UDP) no se garantiza que el mensaje llegue a su destino. Parece claro que si el programa envía un mensaje y no hay nadie escuchando, ese mensaje se pierde. De todas formas, aunque haya alguien escuchando, el protocolo tampoco garantiza que el mensaje llegue. Lo único que garantiza es, que si llega, llega sin errores.

En una comunicación con sockets multihilo, como es el caso de la implementación que nos ocupa, cada conexión es gestionada por un proceso o hilo de ejecución independiente. Así, con cada cliente que conecte con el servidor, se expande un nuevo hilo del lado del servidor, que gestionará las peticiones del cliente asociado a él. Servidores concurrentes pueden ser multiproceso o multihilo. Estos son utilizados para servicios largos con mucha sincronización Cliente-Servidor.

A continuación dejo el código de ambas implementaciones, primero en C++ y después en Python.

Comandos

  • ADD usuario:Introduce un usuario en el chat.
  • LIST: Obtiene la lista de clientes conectados.
  • END: Desconecta y sale de la sesión.
  • TEXT: Envía un mensaje a todos los usuarios conectados.
  • TEXT TO usuario: Envía un mensaje privado a un usuario.

En C++

Servidor
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
#include <stdio.h>
#include <string.h>
#include <sys/types.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <pthread.h>
 
#define SERVER_PORT	6543
#define SERVER_ADDRESS	"127.0.0.1"
#define MAXLINE		512
#define MAXCLIENTS	10
 
int buscarCliente(char*);
void subCadena(char*, char*, int, int);
 
struct vector
{
	int socket;
	char usuario[MAXLINE-4];
	int sign_in;
};
 
struct vector vectorClientes[MAXCLIENTS];
int clientes = 0;
 
int main(int argc, char *argv[])
{
    void* gestionaCliente(void* p);
 
    int socketfd, new_sd;
    socklen_t client_len;
    struct sockaddr_in server_addr, client_addr;
    int i, status, id;
    pthread_t hilos[MAXCLIENTS];
 
    for(i=0;i<MAXCLIENTS;i++)
	strcpy(vectorClientes[i].usuario, " ");
 
    // Open TCP internet STREAM socket
    if ((socketfd = socket(AF_INET, SOCK_STREAM, 0)) < 0)
	perror("server: Can't open stream socket");
 
    // Bind local address to allow the client to connect
    bzero((char *) &server_addr, sizeof(server_addr));
    server_addr.sin_family = AF_INET;
    server_addr.sin_addr.s_addr = htonl(INADDR_ANY);
    server_addr.sin_port = htons(SERVER_PORT);
    if (bind
	(socketfd, (struct sockaddr *) &server_addr,
	 sizeof(server_addr)) < 0)
	perror("server: can't bind local address");
    listen(socketfd, 5);
 
    for (;;) {
	client_len = sizeof(client_addr);
	if((new_sd = accept(socketfd, (struct sockaddr *) &client_addr, &client_len)) < 0) {
		printf("Error aceptando peticiones\n");
		exit(0);
	}	
	else {	
		id = clientes;		
		vectorClientes[id].socket = new_sd;
		vectorClientes[id].sign_in = 0;		
		fflush(stdout);			
		clientes++;
		if ( (status = pthread_create(&hilos[id],NULL,gestionaCliente,(void *)&id)) )
		{
			printf("Error al crear el hilo\n");
			exit(0);
		}
	}
   }
   close(socketfd);	// Close original socket
   return 0;
}
 
void subCadena(char *subCad, char *cad, int inicio, int cuantos)
{
     int i,j=0;
     for(i=inicio;i<inicio+cuantos && cad[i]!='\0';i++)
     {
        subCad[j]=cad[i];
        j++;
     }
     subCad[j]='\0';
}
 
int buscarCliente(char* usuario)
{
	int i;
	for(i=0;i<clientes;i++)
	{
		if(strcmp(vectorClientes[i].usuario,usuario ) == 0 && vectorClientes[i].sign_in == 1)
		return vectorClientes[i].socket;
	}
	return -1;
}
 
void* gestionaCliente(void* p)
{
	int *ide, id;
	ide = (int* ) p;
	id = *ide;
	char buffer[MAXLINE], nombre[MAXLINE-4], temp[MAXLINE-8];
	int i, longitud, destino;
 
	while(1)
	{
	recv(vectorClientes[id].socket,buffer,MAXLINE,0);
	printf("\nid%d\n", id);
 
	if(strstr(buffer, "ADD") && vectorClientes[id].sign_in == 0) {
		longitud = strlen(buffer);
		//Le quitamos el ADD
		subCadena(nombre, buffer, 4, longitud-4);
		strcpy(vectorClientes[id].usuario, nombre);
		//Se informa a todos menos a él mismo y al que se haya ido
		strcpy(buffer, "El usuario ");
		strcat(buffer, nombre);
		strcat(buffer, " ha entrado en el chat.");
		for(i = 0; i < clientes; i++)
			if (i != id && vectorClientes[i].sign_in == 1)
			send(vectorClientes[i].socket,buffer,MAXLINE,0);
		vectorClientes[id].sign_in = 1;
		}
	if(strstr(buffer, "LIST") && vectorClientes[id].sign_in == 1) {
		//Se envia al cliente todos los usuarios menos los que hayan abandonado la sesión
		for(i = 0; i < clientes; i++){
			if(vectorClientes[i].sign_in == 1)
				send(vectorClientes[id].socket, vectorClientes[i].usuario, MAXLINE, 0);			
			}		
		}
	if(strstr(buffer, "END") && vectorClientes[id].sign_in == 1) {
		//Se informa a todos menos a él mismo y al que se haya ido
		strcpy(buffer, "El usuario ");
		strcat(buffer, vectorClientes[id].usuario);
		strcat(buffer, " ha abandonado en el chat.");
		bzero(vectorClientes[id].usuario, MAXLINE);
		for(i = 0; i < clientes; i++)
			if (i != id && vectorClientes[i].sign_in == 1)
				send(vectorClientes[i].socket,buffer,MAXLINE,0);
		vectorClientes[id].sign_in = 0;
		}
	if(strstr(buffer, "TEXT") && !strstr(buffer, "TEXT TO") && vectorClientes[id].sign_in == 1) {
		longitud = strlen(buffer);
		subCadena(temp, buffer, 5, longitud-5);
		//Se envía a todos menos a él mismo y al que se haya ido
		bzero(buffer, MAXLINE);
		strcat(buffer, vectorClientes[id].usuario);
		strcat(buffer, " dice: ");
		strcat(buffer, temp);
		for(i = 0; i < clientes; i++)
			if (i != id && vectorClientes[i].sign_in == 1)
			send(vectorClientes[i].socket,buffer,MAXLINE,0);
		}
	if(strstr(buffer, "TEXT TO") && vectorClientes[id].sign_in == 1) {
		//Le quitamos el TEXT TO
		subCadena(nombre, buffer, 8, MAXLINE-8);
		//Nos quedamos sólo con el nombre, quitando desde el primer espacio en blanco hasta el final
		strtok(nombre," ");
		//Se obtiene el socket destino
		destino = buscarCliente(nombre);
		longitud=strlen(nombre);
		strcpy(nombre, vectorClientes[id].usuario);		
		strcat(nombre, " dice: ");
		//Recortamos el TEXT TO, el nombre, y los dos espacios hasta el mensaje(se suma solo uno (un espacio) 
		//porque empieza a recorrer longitud(instruccion)+longitud(nombre) desde la pos. 0 de la cadena.		
		subCadena(temp, buffer, 8+longitud+1, MAXLINE-(8+longitud+1));
		strcat(nombre, temp);
		if(destino != -1)
			send(destino, nombre, MAXLINE, 0);
		}
	fflush(stdout);
	}
	close(vectorClientes[id].socket);
}
Cliente
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <netdb.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <pthread.h>
#include <string.h>
 
#define SERVER_PORT	6543
#define SERVER_ADDRESS	"127.0.0.1"
#define MAXLINE		512
 
void* recibir(void* p);
void* enviar(void* p);
 
int main()
{
	struct sockaddr_in addr;
	int sd,status;
	pthread_t hilos[2];
 
	addr.sin_family = AF_INET;
	addr.sin_addr.s_addr = inet_addr(SERVER_ADDRESS);
	addr.sin_port = htons(SERVER_PORT);
 
	if((sd = socket (AF_INET, SOCK_STREAM, 0)) == -1)
	{
		printf("Error al crear el socket\n");
		exit(0);	
	}	
	if(connect(sd, (struct sockaddr *)&addr, sizeof(addr)) == -1)
	{
		printf("Error al conectar\n");
		exit(0);
	}
	else
	{	
		if ( (status = pthread_create(&hilos[0],NULL,recibir,(void*)&sd)) )
		{
			printf("Error al crear hilo para recibir\n");
			close(sd);
			exit(0);
		}		
		if ( (status = pthread_create(&hilos[1],NULL,enviar,(void*)&sd)) )
		{
			printf("Error al crear hilo para enviar\n");
			close(sd);
			exit(0);
		}
		pthread_join(hilos[0],NULL);
		pthread_join(hilos[1],NULL);		
	}
	return 1;
}
 
void* recibir(void* p)
{
	int* id;
	char buffer[MAXLINE];
	id = (int*) p;
	while(1)
	{
		recv(*id,buffer,MAXLINE,0);
		printf("%s\n",buffer);
		fflush(stdout);
	}
}
 
void* enviar(void* p)
{
	int* id;
	char buffer[MAXLINE];
	id = (int*) p;
	while(1)
	{	 
		printf("\tIntroduce el mensaje\n-> ");
		fgets(buffer , MAXLINE , stdin);
		strtok(buffer,"\n");
		send(*id,buffer,MAXLINE,0);
		sleep(3);
	}
}

chat-en-c-2

En Python

Servidor
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
#!/usr/bin/python
# -*- coding: utf-8 -*-
#Servidor
 
import string
import threading
import socket
 
clientes = {'nombre':[], 'socket':[]}
 
class gestionaClientes(threading.Thread):
 
    def __init__(self, socket):
        threading.Thread.__init__(self)
	self.conn = socket
	self.conectado = False
	self.data = ''
    def run(self):
	while True:
		self.data = self.conn.recv( 1024 )
 
		if 'ADD' in self.data:
			#Busca si ya ha sido insertado
			if(self.conectado == False):
				self.conectado = True					
				clientes['nombre'].append(self.data[4:])
				clientes['socket'].append(self.conn)
				for i in clientes['socket']:
					if i != self.conn:
						i.send(self.data[4:]+" ha entrado en el chat.")
			else:
				self.conn.send("Ya estás en el chat.")
			print clientes
 
		if ('LIST' in self.data) and (self.conectado == True):
			for i in clientes['nombre']:
					print "enviado a "+str(self.conn)+" "+str(i)
					self.conn.send(i+" ")
			print clientes
 
		if ('END' in self.data):
			if (self.conectado == True):
				for i in clientes['socket']:
					if i == self.conn:
						nombre = clientes['nombre'][clientes['socket'].index(i)]
						clientes['nombre'].remove(nombre)
						clientes['socket'].remove(i)
						self.conectado = False
				for i in clientes['socket']:
					if i != self.conn:		
						i.send(nombre+" ha salido del chat.")
				self.conn.send(" ")
			else:
				self.conn.send(" ")
		if ('TEXT' in self.data and 'TEXT TO' not in self.data) and (self.conectado == True):
			for i in clientes['socket']:
				if i != self.conn:
					i.send(clientes['nombre'][clientes['socket'].index(self.conn)]+" dice: "+self.data[5:])
			print clientes
 
		if ('TEXT TO' in self.data) and (self.conectado == True):
			palabras = self.data[8:].split()
			#busca si existe alguien con ese alias
			for i in clientes['nombre']:
				if i == palabras[0]:
					#Prepara el mensaje
					del palabras[0]
					mensaje = string.join(palabras, ' ')
					clientes['socket'][clientes['nombre'].index(i)].send(clientes['nombre'][clientes['socket'].index(self.conn)]+" dice: "+mensaje)
 
 
	self.conn.close() 
 
#creamos socket pasivo y escuchamos en el puerto 9000
s = socket.socket( socket.AF_INET, socket.SOCK_STREAM )
s.bind( ( socket.gethostname(), 9000 ) )
s.listen( 5 )
while(True):
	conn, addr = s.accept()
	gestionaClientes(conn).start()
Cliente
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
#!/usr/bin/python
# -*- coding: utf-8 -*-
#Cliente
 
from Tkinter import *
import threading
import socket
 
class App:
 
    def __init__(self, master, socket):
 
	self.conn = socket
 
        self.frame = Frame(master)
	self.label = Label(self.frame, text="Cliente chat. Python+TkInter. David López")
 
	self.textarea = Text(self.frame, height=20, width=40)
	self.scroll = Scrollbar(self.frame, command=self.textarea.yview)
	self.textarea.configure(yscrollcommand=self.scroll.set)
 
	self.texto_enviar = StringVar()
	self.text_ent = Entry(self.frame, textvariable=self.texto_enviar)
 
        self.btn_enviar = Button(self.frame, text="Enviar", command=self.enviar)
        self.btn_salir = Button(self.frame, text="Salir", command=self.salir)
 
	self.frame.grid()
	self.label.grid(row=0, column=0, columnspan=3)
	self.textarea.grid(row=1, column=0, columnspan=2)
	self.scroll.grid(row=1, column=2, sticky=N+S)
	self.text_ent.grid(row=2, column=0, columnspan=2, sticky=W+E)
	self.btn_enviar.grid(row=3, column=0)
	self.btn_salir.grid(row=3, column=1)
 
    def enviar(self):
	self.conn.send(self.text_ent.get())
	self.text_ent.delete(0, END)
 
    # Esta es una función de retrollamada.
    def salir(self):
	lee.parar()
	self.conn.send("END")
	self.frame.quit()
 
    def escribir(self, texto):
	self.textarea.insert(END, texto)
 
class leer(threading.Thread):
    def __init__(self, socket):
        threading.Thread.__init__(self)
	self.mensaje = ''
	self.conn = socket
	self.stop = False
    def run(self):
	while (self.stop == False):
		self.mensaje = self.conn.recv( 1024 )
		app.escribir(self.mensaje+'\n')
	self.conn.close()
    def parar(self):
	self.stop = True
 
miSocket = socket.socket( socket.AF_INET, socket.SOCK_STREAM )
miSocket.connect( (socket.gethostname(), 9000 ) )
 
lee = leer(miSocket)
 
lee.start()
 
root = Tk()
root.title('Chat Python-TkInter - David López')
app = App(root, miSocket)
 
root.mainloop()

 

 

chatgrafico

Llamadas a procedimiento remoto (RPC): Un servidor horario

RPC (Remote Procedure Call) es una tecnología, tradicionalmente empleada en ambiente UNIX, que permite el desarrollo de sistemas de procesamiento distribuido basados en el paradigma procedimental.

Una llamada a un procedimiento (función o subrutina) es un método bien conocido para transferir el control de una parte del programa a otra, con un retorno del control a la primera. Asociado con la llamada a un procedimiento están el pase de argumentos y el retorno de uno o varios resultados. Cuando el código que invoca a un procedimiento y dicho procedimiento están en un mismo proceso en un computador dado, se dice que ha ocurrido una llamada a un procedimiento local.

Por el contrario, en una llamada a un procedimiento remoto (RPC, Remote Proceure Call) el sistema local invoca, a través de la red, a una función alojada en otro sistema. Lo que se pretende es hacerle parecer al programador que está ocurriendo una simple llamada local. Este es el flujo de datos en el modelo RPC:

rpc

Para este ejemplo se va a asumir que el entorno de trabajo ahora mismo es linux con gcc y rpcgen instalado.Los pasos para hacer que todo funcione son 4, y se describen a continuación:

  1. La definición del XDR, usando ese lenguaje para especificar qué funciones quiere “publicar” el servidor, y cómo se han de llamar por parte del cliente.
  2. La implementación de los métodos remotos, para tener un código que ejecutar cuando nos hagan una llamada remota.
  3. La implementación de un cliente que invoque los métodos del servidor.
  4. La compilación de todo esto y una ejecución de prueba.

XDR

XDR (External Data Representation) es un estándar para la descripción y codificación de datos que utiliza un lenguaje cuya sintaxis es similar a la del lenguaje de programación C. Es empleado principalmente en la transferencia de información entre diferentes arquitecturas computacionales y se ubica dentro de la capa de presentación del modelo ISO. Involucra un mecanismo de tipificado implícito (Implicit Typing), es decir que sólo viaja el valor de la variable por la red.

Es importante resaltar que XDR no es un lenguaje de programación, sino una especificación que incluye un lenguaje de descripción de datos el cual es extendido por ONC-RPC para la definición de procedimientos remotos. Los tipos de datos de XDR presentan cierta similitud con los tipos de datos de C. Algunos de estos son:

  • int
  • unsigned int
  • enum
  • bool
  • hyper
  • unsigned hyper
  • float
  • double
  • quadruple (punto flotante de cuádruple precisión)
  • opaque (de longitud fija o variable)
  • string
  • array (de longitud fija o variable)
  • struct
  • union (uniones discriminadas)
  • void
  • constant

El lenguaje especificado por RPC es idéntico al lenguaje de XDR, excepto que agrega la definición de «programa», cuya gramática es mostrada a continuación:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
program-def:
     "program" identifier "{"
          version-def
          version-def *
     "}" "=" constant ";"
 
version-def:
     "version" identifier "{"
          procedure-def
          procedure-def *
     "}" "=" constant ";"
 
procedure-def:
     type-specifier identifier
          "(" type-specifier("," type-specifier )* ")" "=" constant ";"

Lo primero es definir qué métodos está ofreciendo el servidor así como los parámetros que admite y el tipo de retorno. Esto lo hacemos en un fichero .x como el siguiente:

1
2
3
4
5
program HORA_PROG {
version HORA_VERS {
string HORA(void) = 1; /*devuelve una cadena*/
} = 1;
} = 0x31230000;

Las partes importantes del fichero son la definición de la estructura Operandos (en el anterior ejemplo no la hay) y el program { version { con las definiciones de las funciones. La definición de Operandos permite definir estructuras prácticamente arbitrarias como tipos para parámetros y retornos. Un ejemplo de fichero XDR con definición de operandos sería el siguiente, aunque no se usa en esta ocasión:

1
2
3
4
5
6
7
8
9
struct hora {
char arg[90];
};
 
program HORA_PROG {
version HORA_VERS {
hora HORA(void) = 1; /*devuelve una cadena*/
} = 1;
} = 0x31230000;

Los program son una manera de agrupar primitivas (he usado el convenio de notación de Sun para nombrarlo). El valor 0×31230000 es un identificador único para el servicio, de manera que que pueda ser localizado por el cliente y el servidor.

Dentro de program, tenemos una version que es la que contiene los métodos. Esto es así porque RPC admite varias versiones de los métodos para un mismo programa (por motivos de compatibilidad hacia atrás, entre otros). En este caso tenemos una única (la 1).

Las definiciones de las funciones son prácticamente iguales a las que haríamos en C, con una consideración adicional (para este caso): a la hora de devolver una cadena no devolvemos char*, sino string. El convenio de Sun que impone poner aquí los nombres de las funciones en mayúscula. Si devolvemos un operando definido, simplemente se escribe antes del nombre de la función (hora). Por tanto, una vez terminada esta parte, he definido un servicio con una primitiva que devuelve la hora.

Implementación del servidor

El siguiente paso es la implementación del servidor; necesitamos crear el código que se ejecutará efectivamente en la máquina remota. Lo bueno de este sistema es que podemos hacer esencialmente lo que queramos: consultar bases de datos, reaizar cálculos, invocar otros servicios remotos, etc. De este modo, los procedimientos RPC pueden ser usados como rutinas de computación, de administración/gestion u obtención de datos.

En este caso, por tener un ejemplo concreto, la implementación en sí de esas funciones es trivial. Lo que interesa ahora es la sintaxis (su significado), la manera en que se nos ofrecen los datos y cómo devolver nuestros resultados. El siguiente código utiliza la primera definición de XDR (sin operandos definidos)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#include <rpc/rpc.h>
#include "time.h"
 
char **hora_1_svc(struct svc_req *rqstp) {
 
	static long timeval; // debe ser una variable estática
	static char *ptr; // debe ser una variable estática
 
	timeval = time((long *) 0);
 
	ptr = ctime(&timeval);
 
	return(&ptr);
}
 
int hora_prog_1_freeresult(SVCXPRT *transp, xdrproc_t xdr_result, caddr_t result) {
	xdr_free(xdr_result, result);
	return (1);
}

El tipo de retorno es un puntero al tipo que habíamos definido en el XDR (notar que string=char*). Además, a la hora de devolver, devolvemos referencias a datos estáticos (ya que si devolviésemos referencias a la pila éstas no serían válidas al salir de la función). Este comportamiento es configurable en las opciones de rpcgen.

El tipo de los argumentos pueden ser también punteros a los tipos originales (Operandos) (no hay en el código de arriba), y además se añade un nuevo parámetro que es siempre struct svc_req*. El motivo de lo primero tiene que ver con cómo nos llegan los datos remotos (y con cómo los podemos modificar). El segundo parámetro contiene información sobre la petición que nos han hecho y el cliente que la ha realizado.

Con respecto al nombre de las funciones, estas se corresponden con el nombre descrito en el fichero XDR, añadiendo _i_svc, donde i es el número de versión que estamos implementando. Esto es así porque podemos tener diferentes versiones de una misma función, y con este sistema evitamos colisiones en los nombres.

Implementación del cliente

Para completar la parte de implementación, tenemos que escribir un código de cliente que haga uso de los procedimientos remotos. Éste se escribe como un programa local normal, salvo por que hacemos llamadas a ciertas funciones que son las que se encargan de hacer el trabajo de RPC.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
#include <rpc/rpc.h>
#include <stdio.h>
#include "hora.h"
 
int main(int argc, char *argv[]){
	CLIENT *cl;
	char *nombre_servidor;
	char **hora;
 
	if(argc < 2){
		perror("Uso: ./client nombreservidor");
	}
 
	nombre_servidor = argv[1];
 
	cl = clnt_create(nombre_servidor, HORA_PROG, HORA_VERS, "tcp");
 
	if((hora = hora_1(NULL, cl)) == NULL) {
		clnt_sperror(cl, argv[1]);
	}
 
	printf("\nLa hora del servidor %s es %s", nombre_servidor, *hora);
 
	clnt_destroy(cl);
 
	exit(0);
}

El #include se hace sobre un fichero que todavía no hemos creado, hora.h. Éste se genera automáticamente con el comando rpcgen, y suele ser el nombre del fichero .x intercambiando la extensión.

La instanciación se hace con la sentencia

1
cl = clnt_create(nombre_servidor, HORA_PROG, HORA_VERS, "tcp");

Esta llamada devuelve un stub, que es una estructura con toda la información que necesita nuestra biblioteca de rpc. Los parámetros que admiten son el nombre de host del servidor (localhost si hacéis las pruebas en modo local, o una ip, y dos constantes que básicamente tienen que ser el nombre que pusimos después de program y después de version en el fichero .x del principio. El último parámetro indica el protocolo de transporte que queremos usar para nuestra petición.

Las llamadas en sí son muy sencillas, y utilizan exactamente el mismo interfaz que implementamos en el servidor. El segundo parámetro es el stub del cliente. Como esta función no tiene parámetros, se introduce NULL.

Compilación

rpcgen es un programa que toma como argumentos un fichero .x y genera el código de los stubs.

$ rpcgen hora.x

Esta línea ha debido generar los siguientes ficheros: hora.h, hora_svc.c, hora_clnt.c y hora_xdr.c (en el caso de haber definido una estructura de operandos en el XDR).

El primero, hora.h, están las definiciones de tipos, constantes y funciones generales comunes tanto al servidor como al cliente. Es por eso por lo que hemos hecho el include en nuestras implementaciones.

Los ficheros hora_clnt.c y hora_svc.c contienen respectivamente el código los stubs de cliente y de servidor. Dichos stubs son los que se encargan efectivamente de que la comunicación sea transparente: transforman los argumentos y retornos que usamos en las funciones publicadas como servicios y también las decodifican en el otro extremo.

hora_xdr.c es el módulo que contiene información sobre los tipos que ha definido el usuario. Como sólo hemos utilizado tipos básicos, no ha hecho falta.

Lo único que falta es compilar el programa cliente y el servidor como si de cualquier otro programa se tratase. Para el cliente, como hemos hecho una implementación en C, usamos gcc como compilador:

$ gcc client.c hora_clnt.c -o client

Para el servidor:

$ gcc server.c hora_svc.c -o server

Ejecución

Si todo lo anterior ha ido bien, basta con ejecutar el servidor:

$ ./server

y ejecutar el cliente:

$ ./client localhost
La hora del servidor localhost es Thu Jul 21 17:31:56 2011

 

Nota

En recientes versiones de Ubuntu, tras ejecutar el servidor se obtiene un mensaje como este:

Cannot register service: RPC: Unable to receive; errno = Connection refused
unable to register (HORA_PROG, HORA_VERS, udp).

Esto es porque es necesario instalar el paquete portmap, lo cual puede realizarse con el siguiente comando en consola:

sudo apt-get install portmap

Espero que el tema os haya gustado. Dejo abajo el enlace al código del ejemplo.

rpc_time.tar.gz
code