Índice de Contenidos :. Introducción General :: 1 TCP Vs UDP :: 2 Cliente/Servidor TCP :: 2.1 Cliente/Servidor UDP :: 2.2 Servidores Multicliente :: 3 Gestión de Sockets en Blender :: 3.1 Implementación de Persistencia :: 3.2 Cliente de Chat en Blender :: 3.3 Diseño Multijugador :: 4 Técnicas de Construcción :: 4.1 Gestión Avanzada :: 5 Sesión 7 :: Transp. 2
Introducción General :. Multijugador: Es el área donde se preven más avances. Incremento de la adicción debido al componente humano. Cómo funciona Internet: Red orientada a paquetes; diferentes caminos. Tolerante a fallos; se adapta a las propiedades de la red en cada momento. 2 operaciones fundamentales: División y Unión de paquetes: TCP. Enrutado de los paquetes: IP. Opciones: Protocolos orientados a la conexión. TCP. Protocolos sin conexión. UDP. Sesión 7 :: Transp. 3
Qué es un Socket? :. API para aislar la complejidad del trabajo con redes. Similar a acceder a un fichero; "apertura", "lectura", "escritura"... Socket: dispositivo de entrada/salida que proporciona un canal de comunicación entre dos computadores. Nos proporciona automáticamente el mecanismo de partición y ensamblado de los paquetes en el destino. Según quién proporcione el servicio a quién, distinguimos dos tipos de máquinas: clientes y servidores. Pueden utilizar el protocolo TCP o UDP. Socket Socket Sesión 7 :: Transp. 4
TCP Vs. UDP :. TCP UDP Orientado a la conexión. Paquetes de tamaño variable. Garantiza la recepción. FIFO. Lento. No mantiene la conexión. Paquetes de tamaño fijo. No garantiza nada. En cualquier orden. Rápido. Aplicaciones Juegos con movimientos esenciales; no se puede perder datos. Estrategia. RPGs. Tablero Etc... Aplicaciones Fundamentalmente juegos en tiempo real, con alta tasa de frames por segundo. Acción. Deportivos. Etc... Sesión 7 :: Transp. 5
Cliente Servidor UDP Mínimo :. import socket Ver ClienteUDP.py puerto = 40000 host = "localhost" s = socket.socket(socket.af_inet, socket.sock_dgram) s.sendto ("Primer ejemplo del curso.", (host, puerto)) import socket Ver ServidorUDP.py puerto = 40000 s = socket.socket(socket.af_inet, socket.sock_dgram) s.bind(("", puerto)) print "Esperando tramas..." while 1: # Recibimos 1024 bytes en un datagrama datos, direccion = s.recvfrom(1024) print "Recibido: ", datos, " desde ", direccion Sesión 7 :: Transp. 6
Cliente Servidor TCP Mínimo :. import socket import socket puerto = 40000 host = "localhost" s = socket.socket (socket.af_inet, socket.sock_stream) # Conectamos con el servidor # en el puerto indicado s.connect((host, puerto)) # Enviamos peticion al host s.send("hola servidor!\n") # Recibimos la respuesta del # servidor (1024 bytes) respuesta = s.recv(1024) print respuesta s.close() puerto = 40000 host = "localhost" s = socket.socket(socket.af_inet, socket.sock_stream) s.bind((host, puerto)) # Numero max. de clientes en espera s.listen(5) print "Esperando conexiones..." try: while 1: nuevo_sock, dir = s.accept() print "Conectado desde ", dir # Bucle para servir a cada cliente while 1: datos = nuevo_sock.recv(1024) if not datos: break nuevo_sock.send(datos) nuevo_sock.close() finally: s.close() Ver ClienteTCP.py Ver ServidorTCP.py Sesión 7 :: Transp. 7
Servidores Multicliente :. En escenarios de trabajo reales, se requieren servidores de, al menos 6-8 jugadores. Sin embargo, las primitivas de envío y recepción por sockets son, por defecto, bloqueantes. Debemos tener en cuenta la gestión de la conexión únicamente cuando haya datos que tratar (o no nos importe quedarnos bloqueados). 2 alternativas: Servidores concurrentes. Servidores iterativos con primitivas no bloqueantes. Sesión 7 :: Transp. 8
Servidores Concurrentes :. Lanzamos proceso hijo para cada conexión aceptada. En sistemas UNIX, llamada a Fork, en sistemas Windows CreateThread. Pseudocódigo del Servidor 1. Crear el socket y enlazar con bind el puerto y la dirección IP. 2. Llamada a listen: modo pasivo. 3. Esperamos nueva conexión con accept. 4. Llegada de una nueva conexión: Creación de un nuevo proceso/hilo. 4.1. Proceso padre: Volver al paso 3 4.2. Proceso hijo: 4.2.1. Bucle send-recv con la nueva conexión. 4.2.2. Cuando termine, close() Socket Abierto h1 h3 h2 Sesión 7 :: Transp. 9
Servidores Iterativos :. Un único proceso. Nos aseguramos que no hay bloqueo. Bloqueos: Pedimos leer antes de que lleguen los datos. Leemos menos de lo esperado. Errores, buffers llenos... Flags interesantes... Socket Abierto Socket NO Bloqueante h1 MSG_OOB: Out-Of-Band. Marca especial del cliente que envía como urgente. Se extrae de forma individual. MSG_PEEK: Permite preguntar en el socket sobre los datos que contiene sin leer de él. Sesión 7 :: Transp. 10
Un Ejemplo: Servidor de Chat :. s = socket.socket(socket.af_inet, socket.sock_stream) s.bind(('', 4000)) s.setblocking(0) s.listen(1) while not terminar: # Hasta que manden un "shutdown" u = ProcesaNuevaConexion() # Hay conexiones nuevas? if u: lista_usuarios.append(u) print len(lista_usuarios)," conexion(es)" for u in lista_usuarios: # Estan los usuarios vivos? u.autentificar() try: mensaje = u.conexion.recv(1024) if mensaje: print "Desde",u.nombre,': ['+mensaje+']' u.procesamensaje(mensaje) if mensaje == "shutdown": terminar=1 except: pass for u in lista_usuarios: u.conexion.close() Ver ServidorChat.py (Fragmento de Código 1/3) HOST = '' PORT = 4000 FINLINEA = "\r\n" lista_usuarios = [] terminar = 0 spregnombre = 0 sesperanombre = 1 sconectado = 2 Sesión 7 :: Transp. 11
Un Ejemplo: Servidor de Chat :. class Usuario: def init (self): self.nombre = "" self.direccion = "" Ver ServidorChat.py self.conexion = None (Fragmento de Código 2/3) self.estado = spregnombre def Autentificar(self): if self.estado == spregnombre: self.preguntanombre() def PreguntaNombre(self): self.conexion.send("nombre? ") self.estado = sesperanombre def ProcesaMensaje(self, msg): print "Procesando mensaje: ",msg global lista_usuarios if self.estado == sesperanombre: # Estamos esperando el nombre? Lo guardamos if len(msg) < 2 or msg=="#": return # Quitamos ruido del telnet... print "Cliente conectado: ",msg self.nombre = msg self.estado = sconectado self.conexion.send("--> Hola, "+self.nombre+" bienvenido"+finlinea) broadcast("--> "+self.nombre+" se ha conectado."+finlinea) return broadcast("<"+self.nombre+"> "+msg+finlinea) Sesión 7 :: Transp. 12
Un Ejemplo: Servidor de Chat :. def ProcesaNuevaConexion(): try: conexion, direccion = s.accept() except: return None print "Conexion desde:", direccion conexion.setblocking(0) user = Usuario(); user.conexion = conexion user.direccion = direccion return user Ver ServidorChat.py (Fragmento de Código 3/3) def broadcast(msg): # Enviar un mensaje a todos los usuarios conectados for u in lista_usuarios: u.conexion.send(msg) Implementar en el servidor dos comandos que puedan utilizar los usuarios del chat; "list", que mostrará (al usuario que lanzó la orden) un listado de la gente conectada en ese momento y "quit", que desconectará al usuario. Ejercicio Sesión 7 :: Transp. 13
Ajustando Blender para usar Sockets :. Blender no posee soporte nativo para Sockets. En un subdirectorio "lib", dentro de nuestro proyecto, se pueden incluir librerías de python 2.0 que se deseen utilizar. El ejecutable generado funcionará correctamente si incluimos las DLLs fmod.dll y python20.dll (y se construye con dynamic runtime). Ejemplo, introducimos un plano en la escena y se añaden los siguientes bloques lógicos. Importante: Los ficheros contenidos en la carpeta lib han sido modificados para eliminar las dependencias con otros módulos. No es la distribución oficial de sockets Python. Nombre del script cliente de la ventana de código Ver carpeta lib, client.py, server.py Soporte de Sockets en Blender Sesión 7 :: Transp. 14
Problemas de Persistencia... :. import socket class misocket: class misocket: def init (self): self.sock = socket.socket(socket.af_inet,socket.sock_stream) self.sock.connect(("localhost",4000)) instancia = None def init (self): if not misocket.instancia: misocket.instancia = misocket. misocket() misocket.instancia.sock.setblocking(0) else: pass def enviar(self, msg): return self.instancia.sock.send(msg) def recibir(self): try: return self.instancia.sock.recv(1024) except: pass def desconectar(self): self.instancia.sock.close() Ver misocket.py Sesión 7 :: Transp. 15
Cliente de Chat en Blender :. Añadimos dos planos a la escena, y los colocamos perpendiculares a la cámara. Uno servirá para mostrar el texto que enviamos y otro el que recibimos (los renombramos a "Local" y "Remoto"). En modo de selección de caras F, ajustamos la textura al tamaño de una letra de la imagen (arialbd.tga) previamente cargada. Activamos en los botones de pintado, la propiedad Text y Alpha (para indicar que va a mostrar una fuente con fondo transparente). Sesión 7 :: Transp. 16
Bloques Lógicos y Código :. En el plano Local, ajustamos una propiedad que se tiene que llamar Text. Recogemos todas las pulsaciones de teclado sobre esa variable (sensor guardatext). Cuando se pulse Return, se limpia (Assign ""). Además, cuando se pulse Return, llamamos a nuestra clase persistente de envío de datos "misocket". import GameLogic from misocket import * c = GameLogic.getCurrentController() owner = c.getowner() misocket().enviar(owner.text) Sesión 7 :: Transp. 17
Bloques Lógicos y Código :. De igual forma, el plano Remoto requiere los siguientes bloques lógicos. En este caso, el texto se añade por código Python en recibir. Ejercicio: Añadir los elementos necesarios en el chat anterior para que permita ver, al menos, las dos últimas líneas recibidas. import GameLogic from misocket import * msj = misocket().recibir() c = GameLogic.getCurrentController() owner = c.getowner() owner.text = msj[:-2] Sesión 7 :: Transp. 18
Diseñando Juegos Cliente/Servidor :. Arquitectura típica usada en juegos de poca carga del servidor (nº reducido de clientes) Servidor TCP Envío de Tramas (2ª Fase) Accept Dos fases diferenciadas; carga y transferencia de datos. Qué ocurre si un cliente pierde su conexión?... Debemos localizar el problema, poner un socket en modo "accept" y esperar a que el cliente vuelva a enganchar. Servidor UDP Cliente 1 Cliente 2 Sesión 7 :: Transp. 19
Moviendo Objetos... :. Comencemos por un ejemplo sencillo que cambia la posición de los objetos. Añadimos un Empty, y un cubo (el cubo en una capa oculta). Añadimos el siguiente código y bloques lógicos al Empty. import GameLogic contr = GameLogic.getCurrentController() move = contr.getactuator("move") random = GameLogic.getRandomFloat() move.setdrot(0,(random-0.5)/5,0,1) move.setdloc(random/5,0,0,1) GameLogic.addActiveActuator(move,1) Sesión 7 :: Transp. 20
2 Jugadores en Red con Blender :. Añadimos 3 objetos. Un Empty que servirá para iniciar el juego (en una capa oculta), un objeto (cubo) que representará a nuestro jugador "Yo", y otro objeto (esfera) que representará al contrario "Otro". Bloques Lógicos del objeto Empty: import GameLogic from misocket import * c = GameLogic.getCurrentController() owner = c.getowner() while (owner.jugador == -1): msj = misocket().recibir() if msj: owner.jugador = int(msj) Código de init Sesión 7 :: Transp. 21
2 Jugadores en Red con Blender :. Bloques lógicos del Jugador "Yo": Los sensores de teclado se encargan únicamente de desplazar el objeto (con actuadores Motion, dloc) Código de enviar import GameLogic from misocket import * c = GameLogic.getCurrentController() owner = c.getowner() if (owner.njugador!= -1): n = owner.njugador x,y,z = owner.getposition() mensaje = str(n)+':'+str(x)+':'+str(y)+':'+str(z) misocket().enviar(mensaje) Sesión 7 :: Transp. 22
2 Jugadores en Red con Blender :. Bloques lógicos del Jugador "Otro": import GameLogic from misocket import * Código de recibir c = GameLogic.getCurrentController() owner = c.getowner() actuador = c.getactuator("mover") msj = misocket().recibir() if (msj!=none): njugador, x, y, z = [float(e) for e in msj.split(':')] ox, oy, oz = owner.getposition() actuador.setdloc(x-ox, y-oy, z-oz, 1) else : actuador.setdloc(0,0,0,1) GameLogic.addActiveActuator(actuador,1) Sesión 7 :: Transp. 23
De 2 Jugadores a Multijugador... :. Ejercicio Realizar los cambios en el ejemplo anterior para que soporte un número mayor de clientes. En concreto, realizar una adaptación para 3 clientes (generalizable sin cambios en el código a N clientes). Es necesario realizar cambios en el servidor? Es necesario variar la arquitectura del cliente? Sesión 7 :: Transp. 24
Manejo del Lag... Extrapolación de Datos :. Las redes, a menudo, no son tan rápidas y eficientes como quisiéramos. Hay que intentar eliminar el Lag. Partimos de un conjunto de valores conocidos (N) en el espacio; P0, P1, P2..., y el instante de tiempo en el que han sucedido (T); T0, T1, T2... Extrapolamos con polinomios de grado 2 (curva parabólica) que, además de interpolar la curva, permiten extrapolar valores, y preguntar por trayectorias futuras. Arregla problemas con Lags medios. P(T) = at2+bt+c Posición extrapolada Px(T) = axt2+bxt+c Py(T) = ayt2+byt+c Sesión 7 :: Transp. 25
Técnicas de Aceleración :. Mensajes Jerárquicos: Clasificación del tipo de mensajes, enviando a cada cliente de mayor prioridad a menos según la calidad de su enlace. Envío Único de Cambios de Estado: El cliente se convierte en poco más que un terminal gráfico. Todo el cálculo (totalmente determinista) se propaga al servidor. Los clientes sólo informan de los cambios en su estado. Hay que tener especial cuidado con las tareas que requieren aleatoriedad. Uso de tablas aleatorias precalculadas. Subdivisión Espacial: Especialmente utilizada en Juegos Multijugador Masivos. Establecer, en una estructura de datos, la posición relativa entre grupos de jugadores. Sólo los que estén dentro de un grupo recibirán mensajes de sus vecinos. Problema: Síndrome de Braveheart. Sesión 7 :: Transp. 26