Práctica 1: sockets en Python Álvaro Navarro anavarro@gsyc.es Jesús M. González-Barahona jgb@gsyc.es Infraestructura de Redes 5 o Ingeniería Informática 08/09 1. Fase 1: Cliente UDP Esta primera fase tiene como objetivo familiarizarnos con el paquete sockets de Python. Para ello construiremos un sencillo cliente UDP que envíe paquetes a un receptor. Para usar librería de sockets basta con importar el paquete sockets de la siguiente forma: import socket Podemos crear un socket mediante la función socket: s = socket.socket (socket.af_inet, socket.sock_stream) AF INET especifica la familia de procolos que usaremos. Las otras dos posibilidades son AF UNIX y AF INET6, pero quedan fuera del alcance de esta práctica. SOCK STREAM define nuestro socket como TCP. Para definir un socket como UDP usaríamos la constante SOCK DGRAM. Existen más tipos de sockets, como por ejemplos sockets RAW, que no usaremos en esta práctica. Una vez creado el socket, podemos realizar envío y recepciones mediante las funciones send y recv, en el caso de haber definido un socket TCP, o mediante sendto y recvfrom para UDP. Para finaliar cerraremos el descriptor del socket mediante el método close. En este punto conviene leer el manual de Python del paquete sockets 1. En él se describe cómo usar métodos importantes como inicializar la conexión mediante el método connect (para TCP) o el método bind para escuchar peticiones en un puerto. Construye un cliente que envíe peticiones UDP a un host. La dirección IP y el puerto destino serán configurados mediante argumentos en línea de comandos. De esta forma podríamos lanzar nuestro cliente: python clienteudp.py 127.0.0.1 4000 1 http://www.python.org/doc/2.5.2/lib/module-socket.html 1
Donde 127.0.0.1 es la IP destino (localhost en este caso) y 4000 el puerto. El programa nos preguntará qué mensaje (string) queremos enviar. El usuario escribirá una frase que será enviada al servidor. El programa finalizará sólo cuando el usuario escriba quit. Para parsear los argumentos de la línea de comandos utilizamos el paquete sys de Python. Nuestro programa recibirá los argumentos en forma de lista, de forma que podemos acceder a cada elemento de la siguiente forma: sys.argv[0] # referencia al nombre del programa sys.argv[1] # referencia al primer argumento sys.argv[2] # referencia al segundo argumento... Para probar nuestro cliente necesitamos un proceso escuchando peticiones en algún puerto determinado. Como todavía no hemos construido ningún servidor, usaremos la herramienta netcat. Para ello, desde consola ejecutamos: nc -u -l -p 4000 -u indica que esperamos datagramas UDP -l pone netcat en modo escucha -p indica el puerto por donde escucharemos peticiones Por último ejecuta el analizador de redes wireshack y estudia los paquetes que están intercambiando cliente y servidor. Ya que wireshack necesita permisos de root, utilizaremos la herramienta sudo para lanzarlo: sudo which wireshack 2. Fase 2: Servidor UDP En esta fase implementaremos un servidor que realice la misma función que netcat en la Fase1. Crearemos un nuevo programa, servidorudp.py, que implemente un servidor UDP. La estructura del programa es similar al clienteudp.py a diferencia del argumento pasado por línea de comandos: python servidorudp.py 4000 Además debemos usar el método bind para escuchar peticiones en el puerto indicado. El servidor implementará un servicio de echo, es decir, devolverá al cliente el mismo string que éste le envíe pero convertido a mayúsculas. Por ejemplo: 2
# lanzamos primero el servidor en un terminal anavarro@host: /$ python servidor UDP.py 4000 Esperando peticiones... mensaje aceptado! # lamzamos el cliente anavarro@host: /$ python clienteudp.py localhost 4000 Escriba mensaje: luke, soy tu padre El servidor dice: LUKE, SOY TU PADRE Por último vuelve a lanzar tu práctica con Wireshack para estudiar qué está pasando. 3. Fase 3: Cálculo RTT En esta tercera fase calcularemos el Roud Trip Time o RTT entre ambos nodos. Para ello necesitamos construir un cliente y un servidor (fases anteriores), que realicen las siguientes tareas: El cliente enviará una petición de tiempo al servidor tomando el timestamp del momento del envío. El servidor recibe la petición de tiempo y responde al cliente. El cliente recibe el la respuesta del servidor y vuelve a tomar un timestamp tras la recepción. El cliente puede calcular el RTT de la siguiente forma: RTT = timestamp_recepcion - timestamp_envio Para calcular un Timestamp podemos usar el paquete time de Python: import time y ejecutar, por ejemplo: t = time.time() Podéis también echarle un vistazo al paquete datetime 2. Este paquete permite manejar fechas y horas de forma muy cómoda permitiendo el formato y el acceso a cada uno de los elementos del timestamp (día, mes, año, hora, minuto...) 2 http://www.python.org/doc/2.5.2/lib/module-datetime.html 3
4. Fase 4: Intercambio RTTs bidireccional En esta última fase construiremos 2 nodos que envíen y reciban tiempos. Para facilitar la construcción de nodos más complejos, nos serviremos del paquete SocketServer 3 de Python: import SocketServer Este paquete facilita la implementación de servidores mediante la delegación de la lógica de la aplicación a métodos handle. Estos métodos están implementados dentro de una clase, que debe heredar de Base RequestHandler y que incluye variables para el manejo de conexiones y clientes. A continuación se incluye un esqueleto para implementar una aplicación basada en este paquete: import SocketServer import threading # heredamos de BaseRequestHandler class MyUDPHandler(SocketServer.ThreadingMixIn, SocketServer.BaseRequestHandler): # constructor def init (self, request, client_address, server): SocketServer.BaseRequestHandler. init (self, request, client_address, server) # metodo handler def handle (self): data = self.request[0].strip() socket = self.request[1] print "%s wrote:" % self.client_address[0] print data socket.sendto (data.upper(), self.client_address) if name == " main ": cnn = ("127.0.0.1", 4000) server = SocketServer.UDPServer (cnn, MyUDPHandler) server_thread = threading.thread (target=server.serve_forever) server_thread.setdaemon (True) server_thread.start() El objetivo de esta fase es crear dos nodos que calculen sus RTTs e intercambien dicho valor entre ellos. De tal forma que ambos nodos mantendrán dos valores: por un lado el RTT que él mismo ha calculado y el RTT que el otro nodo ha calculado. Suponiendo que n1 y n2 sean dos nodos de nuestra red, tendremos dos operaciones que implementar: Cálculo de RTT (fase 3). n1 calcula el RTT respecto a n2. 3 http://docs.python.org/library/socketserver.html 4
Obtención de RTT: n1 pide a n2 el tiempo que n2 ha calculado anteriormente. En el que supuesto que n2 no haya calculado el RTT todavía, puede enviar 0. Por tanto, cada nodo debería recibir tres argumentos: puerto en el que escuchará peticiones dirección IP de otro nodo puerto del otro nodo Si queremos evitar los dos últimos argumentos, podemos hacer que nuestro nodo nos pregunte en tiempo de ejecución la IP y el puerto del nodo sobre el que queremos calcular y obtener el RTT. Aunque la solución se puede abordar de muchas formas, recomendamos implementar un sistema básico de mensajes. Será el método handler de la clase SimpleRequestHandler el que debería implementar la lógica de la aplicación. El siguiente fragmento de código ilustra un ejemplo sobre cómo discriminar el tipo de mensaje entrante: msg = self.request[0].strip() if msg == "GET_TIME": self.request.send(time) elif msg == "CALCULATE_TIME": self.request.send("ack") else: self.request.send("unknown command") A la hora de enviar y recibir datos que no sean strings, necesitaremos hacer una serialización del envío. Para ello usaremos el paquete struct de Python y los métodos pack/unpack. Aquí se muestra un ejemplo muy sencillo: import struct >> packet = struct.pack(!d, 1294.58) >> @\x94:q\xeb\x85\x1e\xb8 >> struct.unpack (!d, packet) >> (1294.58,) El primer argumento del método pack especifica el tipo de dato sobre el que se realizará la conversión. En este ejemplo indicamos que es un tipo float (d) y que además irán por la red (!), evitando así problemas con el endian. Tenéis más información del paquete struct en la página del manual 4 Para probar la práctica recomendamos lanzar cada nodo en máquinas diferentes y, a ser posible, desde campus diferentes. 4 http://www.python.org/doc/2.5.2/lib/module-struct.html 5