jueves, 14 de marzo de 2013

10 - Juntando todo

Ahora que sabes montar un servidor, conectarte como cliente y escribir/leer paquetes de datos, enviar mensajes. Ahora es tu turno de ponerlo todo junto!
 Comienza tranquilo, no te alteres, esta clase de cosas toman tiempo.

He creado un ejemplo de Chat, para que puedas ver la funcion cliente/servidor. Solo tienes que abrir el servidor, minimizarlo y ejecutar 2 clientes. Cada cliente representa un usuario asi que lo que escriba cada cliente se encia al servidor, y el servidor lo envía a los demás clientes. Se trata de una sala da chat.






GRACIAS POR LEER

Si encuentras errores de ortografia o quieres ampliar un poco en algo, ponte en contacto conmigo.
39DLL Tutorial en linea
LUKE ESCUDE, 2010

Traducción: Silver_light (comunidadgm.org)


9 - El Cliente

Aquí es dónde esta el juego ahora. El cliente es el que se ejecuta en la computadora del usuario. Éste solo hace una conexión, la conexión al servidor. Tengo una base de cliente que cree para ti, es el esquema de un cliente, pero es bueno trabajar con él, puesto que cuenta con los objetos necesarios y los objetos requeridos.

Bien, el cliente tendrá 2 objetos que serán utilizados para la conexión, "Online_connect" que hará la conexión inicial y "online_receive" que recibirá todos los datos desde el servidor. Este tiene el mismo "switch()" que el servidor, porque podremos poner el byte al comienzo del mensaje.
 Aquí esta el código para el evento create del objeto "online_connect":
dllinit(0, true, false);global.tcp = tcpconnect("127.0.0.1",5123, 1);
if (global.tcp > 0) { room_goto_next();}else{ 
show_message("No fue posible conectarse."); 
game_end();
 En el código anterior, "dllinit()" es el mismo que el servidor. Abajo está nuestra variable global "global.tcp". Comienza con la asignación de una TCP ID devuelta por la función "tcpconnect()". El primer parámetro para "tcpconnect()" es la dirección IP del servidor al cual deseamos conectarnos, la cual debe ser una cadena.
Nota: colocar "127.0.0.1" es lo mismo que decir "LocalHost".

El segundo argumento es el número de puerto. Esta establecido en 5123 pero debe ser cambiado por el puerto con el que opera el servidor (Es decir, 8080). El tercer argumento es el bloqueo establecido en off.

El resto se explica por si solo, si "global.tcp" es mayor que 0 entonces el cliente establece la conexión con el servidor, continuemos...

Nota: El objeto "online_connect" debe tener un room propio. El primer room en tu juego debe estar vació (A excepción de este objeto). Es el objeto "online_receive() el que debería estar en el room del juego.

Muy bien, ahora es turno del objeto "online_receive". Este objeto debe ser colocado en cualquier room que necesite transferir datos online.
 Solo tendrá el evento step, porque queremos verificar si hay a la espera, mensajes nuevos.



While(1){ msize = receivemessages(global.tcp)
 if (msize == 0) { // Para cuando se desconecte de imprevisto el servidor  show_message("Desconexión inesperada");  game_end(); } 
 if (msize <= 0) break; // Resetea el ciclo 
 mid = readbyte(); switch(mid) {  case 0:  break; 
  case 1:  break; } 
}
Observa como están estrechamente relacionados el código del evento step del cliente con el código en el evento step del objeto player.
 Observa también el comando "receivemessage()" tiene "global.tcp" en el objeto player, en vez de "TCP".

Todo esto es fácil de entender si comprendes al objeto player que está en el servidor.

8 - Enviando un mensaje

Enviar un mensaje es bastante simple una vez que tienes los datos escritos en él.
 ¿Recuerdas que el TCP ID de cada objeto player en el servidor? Esta ID representa al cliente específico. Es le sirve al Servidor.
 Por otro lado, en el cliente, cada vez que establece una conexión con el servidor, este almacena esa TCP ID en una variable global, "global.tcp".

Esto significa que si eres el servidor y quieres enviar un mensaje al cliente, usarás la función "sendmessage(TCP) mientras que el cliente para envían un mensaje al servidor deberá usar "sendmessage(global.tcp). Más detalles a continuación.

clearbuffer()writebyte(0)sendmessage(global.tcp) // o sendemessage(global.tcp) si eres cliente

 El comando "Clearbuffer()" debe ser llamado cada vez que escribas datos en un mensaje, "clearbuffer()" limpia la memoria de lectura/escritura. Realmente no quisieras que se muestren datos viejos, verdad?

Luego de que el comando "clearbuffer()" es llamado, usa los comando de escritura para escribir los datos que quieras. En este caso el mensaje tiene solo un byte, cuyo valor es 0 (cero).

El comando "sendmessage()" envía el mensaje a la conexión especificada en el argumento. EN el cliente solo puede haber una conexión, verdad? La conexión al servidor entonces usarás "global.tcp" (o el nombre que hayas escogido para la variable). EN el servidor, la TCP ID es local, no global. Es por ello que solo utilizamos "TCP".

Nota: Sé que no hemos hablado del cliente aún, pero ya nos acercamos, Ya casi lo hemos terminado! 

7- Escribiendo y leyendo un mensaje

Existe un conjunto de comandos para bytes, cadenas, etc. en un mensaje, y luego leer los datos del mensaje.
Si envías un mensaje al cliente, puedes usar los comandos de escritura para escribir datos en él y luego enviarlo. Luego, el servidor los lee usando los comandos de lectura.
  • Comandos de Escritura
writebyte (numero de 0 a 255) 
writestring (Una cadena de caracteres entre comillas "")
writeint (un entero)
writedouble (Un decimal, con mucha precisión)
writefloat (Un decimal, con precisión regular)

La diferencia entre double y float está en que los double tienen más precisión de número decimales a la derecha, a diferencia de los float.
  • Comandos de Lectura
readbyte() - devuelve un entero
readstring()- devuelve una cadena
readint()- devuelve un entero
readdouble()- devuelve un decimal del tipo double
readfloat()-devuelve un decimal del tipo float

Estos son solo los comandos típicos para escribir/leer. Hay muchas más, como enteros largos o cortos (long y short) pero no son necesarios, así los puedes memorizar. Solo los más básicos están aquí.
 Usando los comandos de escritura, puedes escribir datos en un mensaje, y luego enviarlo a la parte receptora. Tantos los comandos de escritura como los de lectura sirven para el cliente y el servidor.



6 - Resumen

Hemos aprendido que los clientes se conectan a un servidor. El servidor es el centro de todas las operaciones. Los datos que quieres visibles para todos los clientes deben ser enviados hacia el servidor, luego el servidor se ocupará desde allí en enviarlos.

Un objeto "Server_init" requiere de cargar las funciones de la DLL y escuchar en un puerto, si hay conexiones entrantes. La conexión se realiza en el evento Step del mismo objeto, luego un objeto player es creado y se le asigna una TCP ID que es única por cada cliente que se ha conectado.

Este objeto player es único para cada cliente. La variable local "TCP" del objeto player, es la misma TCPID del cliente al unirse.

Cuando enviamos un mensaje, tiene que ser exactamente en el mismo orden en que debe ser recibido, con los datos siguientes que proceden al byte que enumera el paquete de datos. Este Byte determina la forma en que el mensaje debe ser leído y cómo debe ser tratado.

5 - La estrucutra de los mensajes

La forma en que se estructura un mensaje es importante (desde y hacia, un cliente o un servidor). Es decir, como pasar los datos entre estos.
 Los datos deben ser enviados en el orden en que serán leídos!!! Esta sección listará los comandos para escribir información en un mensaje que enviarás desde el cliente al servidor.
Un "mensaje" es un paquete de datos que tu envías, puede contener cadenas, enteros, bytes, doubles, etc. Los datos que escribes en un mensaje desde el cliente , debe leerse en el mismo orden desde el servidor, por ejemplo:
(El cliente envía el siguiente mensaje al servidor:
Nota: todos estos datos están en el mismo mensaje)


1 (es el byte del que hablaba anteriormente, le dice al servidor como debe leer el mensaje).

"Hello my nake is Luke"
8675309

23.87
"The cake is a Lie!!!"

(Ahora... aquí el servidor esta recibiendo el mensaje apropiadamente)

byte (Oh! mira, el primer byte, 1, nos dice que vamos a leer en este orden: una cadena, un entero, un decimal (double) y otra cadena)
string

integer
double
string

Esto es quizá difícil de entender al principio. Básicamente, el cliente está enviando un mensaje con los siguientes datos: cadena, entero, decimal, cadena. El servidor no sabe lo que hay dentro del mensaje, asi que por ello utilizamos un byte al principio, para permitir que el servidor sepa qué leer en el mensaje que recibe. Por supuesto tienes que programar en el objeto player, qué hacer cuando se recibe éste byte. Puesto que dijimos que cada objeto player es un representante de un cliente real, y es quien recibe y envía los paquetes de datos. ( y no el objeto Server, en cuestión).

Entonces, ese byte 1, hace que el switch del objeto player(en el servidor) vaya hacia el case 1 lo que le permite saber que espera una cadena, un entero, un decimal, y otra cadena.
 Esta es la forma en que siempre se estructura. El byte al comienzo es el case   en la estructura switch. Entonces, volvemos al código:



mid = readbyte(); 
switch(mid){ case 0: break; 
 case 1: break; // nada por debajo}
La variable "mid" toma el valor del primer byte recibido, entonces se dirige a la instrucción case que se asemeje al formato del mensaje.Tiene sentido? Voy a proporcionar un ejemplo de un Chat, al final de este tutorial.
Nota: Los comandos de la actual DLL se usan para agregar datos, serán explicados después del resumen.

4- El Servidor


Ahora deberías haber comprendido la conexión TCP. Ya con esto es hora de poner manos a la obra.
  Primero que nada, comienza con una base GMK con todas las funciones ya cargadas:


Si observas la rica colección de Scripts, verá miles de cosas.
 Para nuestro juego Online vamos a atrabajar con los comandos TCP (o Scripts que se encuentran en la carpeta Sockets/TCP).

El objeto Server_init inicia el servidor, es decir, lo pone en marcha. En el evento Create, necesitamos colocar el siguiente código:
dllinit(0, true, true);
servertcp = tcpliste(8080,10,1);
En el código anterior, dllinit() es la función requerida para inicializar la DLL, el primer argumento, 0 (cero), tan solo significa que vamos a utilizar el nombre estándar de la DLL "39dll.dll".
Luego, el segundo argumento, un valor booleano, true significa que queremos cargar las funciones para sockets. Los cuales básicamente son esenciales en una conexión TCP. El último parámetro, false, es para cargar las utilidades (Otras funciones, las cuales no entraré en detalles).

Luego creamos la variable "Servertcp", ésta contiene el valor del puerto TCP al cual estamos escuchando. Vemos la funcion tcplisten(8080,10,1), el primer argumento es el puerto que vamos a abrir. Por ejemplo, yo he colocado 8080. Por favor cámbialo, algunas aplicaciones usan dicho número de puerto.
El segundo parámetro, 10, es el máximo de conexiones permitidas. El tercer parámetro:
Se encarga de definir el modo en que funcionará el script al establecer la conexión, tiene 3 modos: - Modo de Bloqueo (Valor del argumento: 0): Al intentar conectar y alestablecer conexión, se congelará la ejecución del juego

- Modo de No Bloqueo [1] (Valor del argumento: 1): Al intentar conectar se pararála ejecución del juego, pero al establecer conexión volverá a la ejecución normal

- Modo de No Bloqueo [2] (Valor del argumento: 2): Nunca se parará laejecución del juego, ni al intentar conectar ni al establecer conexión[...]
Ahora, nuestro objeto Server_init debe tener un evento Step, por cada step verificará si hay una conexión de algún cliente.

 stcp = tcpaccept(servertcp, true);
 if (stcp) {
stsync (stcp,0);
o = instnace_create(x,y,player);
o.tcp = stcp;
setsync(stcp,1); 
 
El  código anterior es ejecutado cada Step. La variable "stcp" es una variable temporal que almacena el ID TCP del cliente. Entraremos en detalle luego...
tcpaccept() acepta la conexión entrante, el primer argumento es el puerto abierto al cual escuchamos si hay conexiones nuevas, el segundo argumento es el modo de bloqueo.


Luego, el comando "Setsync" coloca el bloqueo en off. No tienes que preocuparte mucho por esto.

Nuestro siguiente comando crea un nuevo jugador(No importa donde se cree, este es solo un representante dentro del servidor).
Importante: en el objeto player, cada función del servidor lleva a cabo para cada cliente, cada objeto jugador representa un cliente en la vida real, es decir a otra computadora conectada. O sea, si tienes 13 personas conectadas, debe haber 13 objetos player en el Servidor.

Nota: El cliente, cada vez que envíe datos al servidor, lo estará haciendo al objeto player dentro del servidor ,le corresponda. Este objeto player será quien reciba y envíe los datos. Esto se hará evidente más adelante.

El comando o.tcp = stcp le da al jugador un TCP ID. Esta TCP ID es para identificar como único al objeto jugador (es la forma en que se identifica como único, cada objeto player).
El objeto sabe que es único con esta TCP ID. Haré otro diagrama para ayudarles a visualizar esto.
 Otra vez setsync() coloca el bloqueo en off.




Si comprendes totalmente todo hasta ahora, estás listo para seguir con el resto del tutorial. Si aún tienes problemas lee nuevamente las secciones anteriores.



(Nota como los clientes no se conectan en orden. Primero se conecta el de más arriba y luego el del último. Sin embargo, observa que cada objeto tiene una TCP ID única)


Ahora que entiendes los fundamentos de crear un Hosting (dicho de otra forma, poner en funcionamiento un Servidor) y a esperar conexiones, vamos a echar un vistazo al objeto player.

Lo que hay en el evento Create no tiene mucho sentido ahora.
El evento Step del objeto player es en donde realmente está la gracia, el funcionamiento. Escribiré el código debajo y luego lo explicaré. Por favor si no estás familiarizado con la estructura de control switch y la sentencia break lee el manual de Game Maker.


while(1){ // Loop infinito
msize = receivemessage(tcp);
if (msize == 0) instance_destroy(); 
if (msize <= 0) break;
Switch(mid)
{
case 0: 
break;


case 1:
break; 
}
}
Primero, un while(1) o while(true) es un bucle infinito (o ciclo infinito). Nuestro primer comando "receivemessage()" almacena el mensaje en espera, en la variable "mszie".
Los mensajes que se reciben provienen de tcp, que si recuerdas, lo definimos y declaramos en en "o.tcp = stcp"

El if que le sigue, verifica si "msize" es igual a 0, esto solo ocurrirá si el jugador se desconecta de forma espontánea sin razón. Si no se controla esto los objetos player se acumularían, lo cual no queremos que suceda.

Luego las instrucciones "if (msize <= 0) break;" básicamente verifica si no hay  mensajes que estén en espera. Entonces, si no hay mensajes nuevos, el bucle se reinicia con "break;" luego al comenzar de nuevo el ciclo, verifica si hay nuevos mensajes.

Bien... ahora es donde comienzan los malos entendidos. La declaración "mid = readbyte();" almacena (o guarda) el primer elemento del mensaje en espera, que es un byte Este byte, el primer byte que encabeza le dice al programa qué debe leer como mensaje. No necesariamente debe ser un byte, sin embargo es necesario que el cliente o el servidor conozca el orden de los datos dentro del mensaje. Sigue leyendo hasta la siguiente sección para saber a que se refiere exactamente.