7. SOCKETS En este proyecto nos hemos decantado por los sockets para implementar la comunicación cámara-ordenador. Son responsables del importante papel de enviar los comandos que provocan el movimiento de la cámara, así como de recibir las imágenes que captamos a través de ella. Por ello, creemos necesario dedicarles esta sección. 7.1 ¿QUÉ SON? Los sockets son la interfaz más difundida que hay para la comunicación de procesos. Socket designa un concepto abstracto por el cual dos programas (posiblemente situados en computadoras distintas) pueden intercambiarse cualquier flujo de datos, de manera transparente, sin conocer los detalles de como se transmiten esos datos, y generalmente de manera fiable y ordenada. Para que dos programas puedan comunicarse entre si es necesario que un programa sea capaz de localizar al otro, y además, que ambos programas sena capaces de intercambiarse cualguier secuencia de octetos, es decir, datos relevantes a su finalidad. Para ello son necesarios los tres recursos que originan el concepto de socket, y gracias a los cuales éste queda definido: – Un protocolo de comunicaciones, que permite el intercambio de octetos. Una dirección del Protocolo de Red (dirección IP, si se utiliza el protocolo TCP/IP), que identifica una computadora. – – Un número de puerto, que identifica a un programa dentro de una computadora. 1 De aquí se deduce que la propiedades inherentes a los sockets dependen de las características del protocolo en el que se implementan. El protocolo más utilizado es TCP, gracias al cual los sockets tienen las propiedades de ser orientados a conexión y de garantizar la transmisión de todos los octetos sin errores ni omisiones, y que éstos llegan a su destino en el mismo orden en que se transmitieron. Aunque también puede usarse el protocolo UDP. Éste es un protocolo no orientado a conexión. Sólo se garantiza que si un mensaje llega, llega bien. En ningún caso se garantiza que llege o que lleguen los mensajes en el mismoorden que se mandaron. Esto lo hace adecuado para el envío de mensajes frecuentes pero no demasiado importantes, como por ejemplo, mensajes para los refrescos (actualizaciones) de un gráfico. En los orígenes de Internet, las primeras computadoras en implementar sus protocolos fueron aquellas de la universidad de Berkeley. Dicha implementación tuvo lugar en una variante del sistema operativo Unix conocida como BSD Unix. Pronto se hizo evidente que los programadores necesitarían un medio sencillo y eficaz para escribir programas capaces de intercomunicarse entre sí. Esta necesidad dio origen a la primera especificación e implementación de sockets, también en Unix, en 1981, conocidos como BSD sockets (o Berkeley sockets). Se hicieron para proveer al desarrollador de una API mediante la cual pudiera utilizar el protocolo sin complicaciones. Hoy día son un estándar de facto, y están implementados como bibliotecas de programación para multitud de sistemas operativos. Los sockets se caracterizan por ser una interfaz mediante la cual podemos comunicarnos con otros procesos, utilizando descriptores de ficheros. Es decir, como todo en Unix se realiza escribiendo y leyendo ficheros, los sockets se basan en esto también. Cuando establecemos una comunicación entre dos sockets, cada uno tiene un descriptor de fichero en el que escribe y lee para comunicarse con el otro socket. Figura 7.1: socket entre dos anfitriones 2 Los sockets permiten implementar una arquitectura cliente-servidor. La comunicación ha de ser iniciada por uno de los programas que se denomina programa cliente. El segundo programa espera a que otro inicie la comunicación, por este motivo se denomina programa servidor. Desde el punto de vista de programación, un socket no es más que un fichero que se abre de una manera especial. Así, un socket es un fichero existente en la máquina cliente y en la máquina servidora, que sirve en última instancia para que el programa servidor y el cliente lean y escriban la información. Esta información será la transmitida por las diferentes capas de red. Los sockets, al ser de bajo nivel, no resultan muy cómodos para el programador. Al no permitir el paso directo de argumento, el programador tiene que encargarse de abrir/cerrar los flujos de entrada/salida, colocar en ellos los argumentos que quiere pasar, extraer los resultados, etc. Además están muy ligados a la plataforma donde se ejecutan y el código es difícil de reutilizar. Pero por otro lado los sockets son rápidos, al ser de bajo nivel introducen poca sobrecarga a las aplicaciones, lo que los hace ideales para nuestra aplicación. Y como son tan populares y difundidos, prácticamente todos los lenguajes de programación los soportan, y por supuesto también que aquí se emplea. 7.2 SOCKETS EN C# La programación de sockets en .NET es posible gracias a la clase Socket presente en el espacio de nombres System.Net.Sockets. Esta clase Socket tiene varios métodos y propiedades y un constructor. 7.2.1 Creación El primer paso es crear un objeto de esta clase, usando el constructor para ello. Así es como creamos el socket: move_cam = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp); Código 7.1: declaración de un socket El primer parámetro es la familia de la dirección (AddressFamily) que será usada, en este caso, InterNetwork ( que viene a ser IP versión 4). Con el siguiente especificamos el tipo de socket, escogiendo sockets fiables orientados a conexión y de doble sentido (stream) en vez de no fiables y no orientados a conexión (datagramas). Obviamente, especificamos Stream como tipo y finalmente estaremos usando TCP/IP, por lo que especificamos Tcp como tipo de protocolo. 3 7.2.2 Conexión Una vez hemos creado un socket necesitamos crear una conexión con el servidor. Las dos aplicaciones que necesitan comunicarse tienen primero que crear una conexión entre ellas. Las dos aplicaciones necesitarán identificar a la otra. Veamos como funciona esto en .NET. Para conectar con la computadora remota necesitamos conocer la dirección IP y el puerto al cual conectar. En .NET hay una clase en el espacio de nombres System.Net llamada IPEndPoint, la cual representa una como una dirección IP y un número de puerto. Para conseguir la dirección dada por una cadena de caracteres se usa el método Parse. Una vez el punto final está listo se usa el método Connect de la clase Socket para conectar al punto final (el servidor o computadora remota). IPEndPoint RemoteCam = new System.Net.IPEndPoint(IPAddress.Parse("192.168.1.253"),80); move_cam.Connect(RemoteCam); Código 7.2: conexión de un socket con un servidor conocido Si el servidor está funcionando y escuchando, la conexión tendrá éxito. Si en cambio el servidor no está operativo, será lanzada una excepción llamada SocketException. Asumiendo que la conexión está hecha, ya se puede mandar información al otro lado. 7.2.3 Sockets Síncronos/asíncronos Cuando un lado (cliente o servidor) envía información al otro lado se que éste tiene que leer los datos. Pero, ¿cómo sabe el otro lado que la información ha llegado? Hay dos opciones: la aplicación comprueba regularmente si han llegado datos o alguna clase de mecanismo notifica a la aplicación y ésta puede leer los datos en ese momento. Sockets Síncronos Se manda información de una aplicación a otra usando el método Send. Send bloquea, es decir, espera hasta que los datos hayan sido enviados o hasta que se lance un excepción. De la misma forma que hay un método Send para enviar existe un método Receive para recibir los bytes. Igualmente, Receive bloquea la ejecución del programa hasta que algún tipo de información sea recibida o hasta que se lance una excepción. En este código se muestra como se envía una cadena Txx: todo lo que se mande ha de hacerse en forma de bytes, por lo que previamente hay que convertir los caracteres en bytes. A continuación, se recibe un chorro de bytes proveniente del servidor, que quedan almacenados en el vector Rx, y la longitud de éste en iRx. 4 try { String Txx = "Hello There"; byte[] Txx = System.Text.Encoding.ASCII.GetBytes(Txx); move_cam.Send(Txx); } catch (SocketException se) { MessageBox.Show ( se.Message ); } byte [] Rx = new byte[1024]; int iRx = move_cam.Receive(Rx); Código 7.3: ejemplo de enviar y recibir síncronamente con un socket Sockets Asíncronos La clase Socket de .NET ofrece un método llamado BeginReceive para recibir datos asíncronamente, es decir, de manera que no exista bloqueo. Necesita que se le pase, entre otros parámetros, un buffer, que será donde se almacenen los datos recibidos, y una función callback que (delegado) que será llamada en cualquier que se reciban datos. Esto significa que la función BeginAsyncRead, ha sido completada. A continuación se muestran las signaturas de ambas funciones: Public IAsyncResul BeginReceive (byte[] buffer, int offset, int size, SocketFlags socketFlags, AsyncCallback callback, object state) void AsyncCallback (IAsyncResult ar) Código 7.4: signatura de BeginReceive y su método callback El método callback devuelve void y recibe un parámetro, interfaz IAsyncResult, que contiene el estado de la operación asíncrona. Digamos que hacemos una llamada a BeginReceive y después de un tiempo los datos llegan y nuestra función callback es llamada. Los datos están ahora disponibles en el buffer que se pasó como primer parámetro, cuando se hizo la llamada al método BeginReceive. Pero antes de acceder al buffer es necesario llamar a la función EndReceive sobre el socket. EndReceive devolverá el número de bytes recibidos. No es legal acceder al buffer antes de llamar a EndReceive. El siguiente código muestra como hacer una recepción asíncrona: 5 byte[] m_DataBuffer = new byte [10]; IAsyncResult m_asynResult; public AsyncCallback pfnCallBack ; public Socket m_socClient; // create the socket... public void OnConnect() { m_socClient = new Socket (AddressFamily.InterNetwork,SocketType.Stream ,ProtocolType.Tcp ); // get the remote IP address... IPAddress ip = IPAddress.Parse ("10.10.120.122"); int iPortNo = 8221; //create the end point IPEndPoint ipEnd = new IPEndPoint (ip.Address,iPortNo); //connect to the remote host... m_socClient.Connect ( ipEnd ); //watch for data ( asynchronously )... WaitForData(); } public void WaitForData() { if ( pfnCallBack == null ) pfnCallBack = new AsyncCallback (OnDataReceived); // now start to listen for any data... m_asynResult = m_socClient.BeginReceive (m_DataBuffer,0,m_DataBuffer.Length,SocketFlags.None,pfnCallBack,null); } public void OnDataReceived(IAsyncResult asyn) { //end receive... int iRx = 0 ; iRx = m_socClient.EndReceive (asyn); char[] chars = new char[iRx + 1]; System.Text.Decoder d = System.Text.Encoding.UTF8.GetDecoder(); int charLen = d.GetChars(m_DataBuffer, 0, iRx, chars, 0); System.String szData = new System.String(chars); WaitForData(); } Código 7.5: ejemplo de recepción asíncrona La función OnConnect hace una conexión con el servidor y luego una llamada a WaitForData. Si nos ceñimos a lo que es la recepción de datos, esto es integramente hecho en WaitForData, que crea la función callback y hace una llamada a BeginReceive pasándole un buffer global y la función callback. Cuando los datos llegan OnDataReceive es llamado y, por consiguiente, el método EndReceive del socket, que devolverá el número de bytes recibidos. A partir de aquí habrá que gestionar los datos recibidos, en este caso son copiado en una cadena y se realiza una nueva llamada a WaitForData, que llamará BeginReceive otra vez y así sucesivamente. 6 Realmente, es lo mismo procedimiento que llevamos a cabo en la WiimoteLib para recibir los reports del Wiimote. Un detalle interesante es que el método BeginReceive devuelve una interfaz IAsyncResult, que es lo mismo que se le pasa al método callback. La interfaz IasyncResult tiene varias propiedades. La primera de ellas – AsyncState- es un objeto de la misma naturaleza que el último parámetro que requiere BeginReceive. La segunda propiedad es AsyncWaitHandle que discutiremos en un momento. La tercera propiedad indica si la recepción fue realmente asíncrona o si terminó síncronamente. El siguiente parámetro es IsComplete que indica si la operación ha sido completada o no. En cuanto a AsyncWaitHandle, es de tipo WaitHandle, una clase definida en el espacio de nombres System.Threading. La clase WaitHandle encapsula un manejador y ofrece una forma de esperar para que ese manejador llegue a estar marcado. Para ello la clase tiene varios métodos estáticos como WaitOne (que es similar a WaitForSingleObject), WaitAll (similar a WaitForMultipleObjects con waitAll true), WaitAny, etc. También hay versiones de estas funciones con temporizadores disponibles. El manejador en AsyncWaitHandle (WaitHandle) es marcado cuando la operación de recepción se completa. Así, si esperamos indefinidamente sobre ese manejador seremos capaces de saber cuándo la recepción es completada. Esto significa que si pasamos WaitHandle a un hilo diferente, el hilo diferente puede esperar por el manejador y notificarnos cuándo los datos ya han llegado y podemos leerlos. Esto supone una alternativa al uso de la función callback. Si elegimos usar este mecanismo de WaitHandle, el parámetro de la función callback en la llamada a BeginReceive será null, como se muestra en el siguiente ejemplo: //m_asynResult is declared of type IAsyncResult and assumming that m_socClient has made a connection. m_asynResult = m_socClient.BeginReceive(m_DataBuffer,0,m_DataBuffer.Length,SocketFlags.None,nu ll,null); if ( m_asynResult.AsyncWaitHandle.WaitOne () ) { int iRx = 0 ; iRx = m_socClient.EndReceive (m_asynResult); char[] chars = new char[iRx + 1]; System.Text.Decoder d = System.Text.Encoding.UTF8.GetDecoder(); int charLen = d.GetChars(m_DataBuffer, 0, iRx, chars, 0); System.String szData = new System.String(chars); txtDataRx.Text = txtDataRx.Text + szData; } Código 7.6: método alternativo para una recepción asíncrona 7 En el lado del servidor, la aplicación tiene que enviar y recibir datos. Pero además, el servidor tiene que permitir a los clientes hacer conexiones. El servidor no necesita conocer la dirección IP del cliente. Realmente no le importa dónde está el cliente porque no es él, sino el cliente, el responsable de hacer la conexión. La responsabilidad del servidor es gestionar las conexiones del cliente. En el lado del servidor se tiene un socket llamado oyente que escucha un número de puerto específico para conexiones de cliente. Cuando el cliente hace una conexión, el servidor necesita aceptarla y entonces, se envían y reciben datos a través del socket que han conseguido al aceptar la conexión. El siguiente código ilustra cómo el servidor escucha las conexiones y las acepta: public Socket m_socListener; public void StartListening() { m_socListener = new Socket(AddressFamily.InterNetwork,SocketType.Stream,ProtocolType.Tcp); IPEndPoint ipLocal = new IPEndPoint ( IPAddress.Any ,8221); m_socListener.Bind( ipLocal ); m_socListener.Listen (4); m_socListener.BeginAccept(new AsyncCallback ( OnClientConnect ),null); cmdListen.Enabled = false; } public void OnClientConnect(IAsyncResult asyn) { m_socWorker = m_socListener.EndAccept (asyn); WaitForData(m_socWorker); } Código 7.7: implementación de socket en servidor En realidad, el código es similar al del cliente asíncrono. Primero de todo necesitamos crear el socket oyente y asociarlo a una dirección IP. Como en principio no conocemos cual va a ser esa dirección, usamos Any, para luego asociarle la del cliente mediante el método Bind. En cambio, hemos pasado un número de puerto concreto, que es el puerto por el que este socket escucha. Después hemos llamado a la función Listen. El cuatro indica la máxima longitud de la cola de conexiones pendientes. Luego hacemos una llamada a BeginAccept pasándole un delegado callback. BeginAccept es un método sin bloqueo que, cuando un cliente ha hecho una petición de conexión, propicia una llamada a la rutina callback, donde puede aceptarse la conexión llamando a EndAccept. EndAccept devuelve un objeto socket que representa la conexión entrante. 8