7. SOCKETS

Anuncio
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
Descargar