Tp 4 - Facultad de Ingeniería

Anuncio
Trabajo Práctico N°4
Implementación de cliente y servidor TCP (y
UDP) en C# (.NET), utilización de hilos para el
manejo asincrónico de las comunicaciones
Universidad de Buenos Aires
Facultad de Ingeniería
66.48 Seminario de Electrónica
José Carlos León Degreef
Javier Luiso
Diego Morana
Gisela Schmilchu
Índice
Índice ............................................................................................................................ 1
Resumen ....................................................................................................................... 3
La diferencia entre comunicación sincrónica y asincrónica en programación de redes3
Clase Socket ................................................................................................................. 4
Aplicación TCP ............................................................................................................ 5
Implementación del Socket Server ........................................................................... 6
Implementación del Socket Client............................................................................ 9
Como encontrar que cliente envió un mensaje particular....................................... 10
Como responder o enviar mensajes a clientes específicos ..................................... 12
Como saber cuando un cliente en particular se desconecta.................................... 13
Como obtener la lista de los clientes conectados en cualquier momento............... 14
Aplicación para UDP.................................................................................................. 15
Server...................................................................................................................... 16
Cliente..................................................................................................................... 18
Temas de Threading a considerar............................................................................... 21
1 - Las variables son seguras en métodos AsynCallBack? Y los hilos de
sincronización? ....................................................................................................... 21
2 - Shared variables ................................................................................................ 21
3 - Modificando la interfaz GUI ............................................................................. 22
2
Resumen
El objetivo es demostrar que una aplicación de tipo cliente – servidor basada
en sockets que permita la comunicación asincrónica usa hilos a nivel sistema
operativo. El servidor no usa hilos para comunicarse con los múltiples clientes, pero
internamente sí lo hace, gracias al framework de .Net y a C#.
La diferencia entre comunicación sincrónica y asincrónica en
programación de redes
Consideremos una aplicación servidor que esta escuchando en un puerto
específico para recibir datos de clientes. En el método sincrónico, mientras el
servidor está esperando para recibir datos de un cliente, si el “stream” o chorro de
datos esta vació, el hilo principal o “main thread” se bloqueará hasta que el pedido
de datos sea satisfecho. Por lo tanto, el servidor no podrá hacer nada hasta que
reciba los datos del cliente. Si otro cliente tratara de conectarse al servidor al
mismo tiempo, el servidor no podrá procesar el pedido de conexión porque estará
bloqueado por el primer cliente. Esta situación no es aceptable en nuestro caso ni
en el mundo real, donde necesitamos soportar múltiples clientes al mismo tiempo.
En una comunicación asincrónica, cuando el servidor esta escuchando o
recibiendo datos de un cliente, puede procesar pedidos de otros clientes como así
también recibir datos de esos clientes. Cuando el servidor esta recibiendo
asincrónicamente, un hilo diferente (a nivel sistema operativo) escucha el socket e
invoca una función callback (especificada cuando comenzó la recepción asincrónica)
cuando ocurra el evento en el socket. Esta función callback responderá y procesará
el evento del socket. Por ejemplo, si el programa remoto escribe datos en el socket,
un evento de lectura de datos (función callback que hayamos especificado) será
invocado; el mismo sabrá como leer datos del socket en ese punto.
Todo esto puede ser resuelto con múltiples hilos, pero deberíamos
aprovechar el framework de .Net y C# que nos proveen un conjunto de
funcionalidades para lograr la comunicación asincrónica y así obviar la complejidad
del uso de los hilos.
3
Clase Socket
La clase Socket (System.Net.Sockets.Socket) provee un conjunto de
métodos para comunicaciones sincrónicas y asincrónicas. En la convención de .Net,
todos los nombres de los métodos asincrónicos son creados prefijando con “Begin”
o “End” al nombre del método sincrónico. Los métodos prefijados con “Begin” y
“End” representan un par de métodos asincrónicos correspondientes a un solo
método sincrónico, como podemos ver en la siguiente tabla.
Métodos Sincrónicos
Connect()
Métodos Asincrónicos
BeginConnect()
Receive()
EndConnect
BeginReceive()
EndReceive()
4
Aplicación para TCP
La implementamos a través de dos clases, una implementando al servidor
socket y la otra al cliente socket.
5
Implementación del Socket Server
La aplicación Socket Server, esta implementada con en la clase
SocketServer (archivo SocketServer.cs). Esta clase tiene un objeto socket principal
(socketPrincipal) y un ArrayList, tal como se muestra en el siguiente código. (Un
HashTable también funcionaría si quisiéramos usar un string en lugar de un índice
para acceder a los clientes conectados).
Importante: Si queremos correr el servidor por un tiempo de duración
infinito, existe la posibilidad de overflow del valor entero de la variable
contadorCliente. En esos casos, deberíamos y tendríamos que reconsiderar usar
este esquema de numeración para los clientes.
// Usamos un ArrayList para mantener la lista de Sockets activos que son
// designados para comunicarse con cada cliente conectado
private ArrayList socketActivos = new ArrayList();
// La siguiente variable mantiene el total de clientes conectados en
// cualquier momento
private int contadorCliente = 0;
El objeto socketPrincipal escucha por clientes. Una vez que un cliente se
conecta, el objeto Socket principal transfiere la responsabilidad de procesamiento
de las transacciones relacionadas para ese cliente particular a un socket activo.
Luego el objeto socketPrincipal vuelve y continúa escuchando por otros clientes.
BeginAccept() y BeginReceive() son dos métodos importantes en la clase
Socket, usados por la aplicación Socket Server.
6
El método BeginAccept() tiene la siguiente nomenclatura:
public IAsyncResult BeginAccept(
AsyncCallback callback,
object state
// (1) función a llamar cuando un cliente se
//
se conecta
// (2) State object para preserver la info
//
del socket
);
Esencialmente, después de llamar al método Listen() del objeto principal
Socket, llamamos a este método asincrónico y especificamos la función callback
(1), que designamos para hacer el procesamiento relacionado con la conexión del
cliente. El state object (2) puede ser nulo en esta instancia particular.
Al ser un método asincrónico, este volverá inmediatamente y el hilo principal
del servidor estará libre para procesar eventos. En la realidad, un hilo separado
comenzará a escuchar en ese socket particular por conexiones de clientes. Cuando
un cliente requiera un pedido de conexión, la función callback que hayamos
especificado será invocada.
Dentro de esta función callback (en el ejemplo, la función se llama
“onClientConnect()”), se realizarán los procesos correspondientes a la conexión del
cliente.
public void OnClientConnect(IAsyncResult asyn)
{
try
{
// Aqui completamos/terminamos la llamada asincronica BeginAccept()
// llamando a EndAccept() que devuelve la referencia a un nuevo
// objeto Socket
socketActivo[contadorClientes] = socketPrincipal.EndAccept (asyn);
// Incrementamos el contador de clientes para este cliente
// de manera cuidadosa a traves de la clase Interlocked
Interlocked.Increment(ref contadorClientes);
// Agregamos al ArrayList la referencia al socket activo
socketActivos.Add(socketActivo);
// Envio bienvenida al cliente
string msg = "Bienvenido cliente " + contadorClientes + "\n";
EnviarMsg(msg, contadorClientes);
// Actualizo la lista de clientes (thread safe call)
ActualizarControlListaClientes();
// Dejamos al Socket Activo hacer el proceso correspondiente para
// el cliente recien conectado
EsperarPorDatos(socketActivo, contadorClientes);
// Como el Socket principal esta ahora libre, puede volver y
// esperar por otros clientes tratando de conectarse
socketPrincipal.BeginAccept(new AsyncCallback
( OnClientConnect ),null);
}
catch(ObjectDisposedException)
{
System.Diagnostics.Debugger.Log(0,"1","\n OnClientConnection:
Se ha cerrado el socket\n");
7
}
catch(SocketException se)
{
MessageBox.Show ( se.Message );
}
}
Lo primero que hacemos dentro de la función “OnClientConnect()” es llamar
al método EndAccept() en el miembro del objeto socketPrincipal, que devolverá una
referencia a otro objeto Socket. Incrementamos el contador de clientes mediante la
clase Interlocked. Seteamos la referencia del objeto (socketActivo) al arrayList
(socketActivos). Ahora, al tener la referencia a un nuevo objeto Socket que puede
procesar la transacción con el cliente, el Socket principal (socketPrincipal) esta
libre; luego, llamamos a su método BeginAccept() nuevamente para comenzar la
espera de pedidos por parte de otros clientes.
En el socket activo, usamos una estrategia similar para recibir datos del
cliente. En lugar de llamar a BeginAccept() y a EndAccept(), aquí llamamos a
BeginReceive() y a EndReceive(). Esto, en resumen, es la implementación de un
Servidor Socket. Cuando enviamos datos al cliente, el servidor simplemente usa el
objeto Socket activo correspondiente para enviar datos a cada cliente.
8
Implementación del Socket Client
La aplicación Socket Cliente esta implementada en la clase SocketClient
(archivo SocketClient.cs). En comparación con el servidor, donde teníamos al
Socket y al arreglo de sockets activos, aquí solo tenemos un objeto Socket
(socketCliente).
Los dos métodos importantes en la clase Socket usados por la aplicación
cliente son Connect() y BeginReceive(). Connect() es un método sincrónico y es
llamado para conectarse al servidor que esta escuchando pedidos de conexión por
parte de los clientes. Como esta llamada será satisfecha o no inmediatamente,
dependiendo de si hay un servidor activo esperando conexiones o no en la IP
especificada y el puerto especificados, basta con que sea un método sincrónico para
nuestro propósito.
Una vez establecida la conexión, llamamos a la función asincrónica
BeginReceive() para esperar por cualquier actividad de escritura en el socket por
parte del servidor. Aquí, si llamáramos a un método sincrónico, el hilo principal en
la aplicación cliente será bloqueado y no será capaz de enviar ningún dato al
servidor mientras el cliente esta esperando por datos desde el servidor.
Cuando haya cualquier actividad de escritura en el socket desde el servidor,
el hilo interno iniciado por BeginReceive() invocara a la función callback
(“onDataReceived()” en este caso), que se encargara del procesamiento de
cualquier dato escrito por el servidor.
Al momento de enviar datos al servidor, solo llamamos al método Send() en
el objeto socketCliente, que escribirá datos sincrónicamente en el socket.
Esto es todo en una comunicación asincrónica usando múltiples clientes.
9
Como encontrar que cliente envió un mensaje particular
Cuando múltiples clientes están conectados, probablemente deberíamos
diferenciar entre mensajes recibidos por diferentes clientes. Además, tal vez haya
alguna razón para enviar un mensaje a un cliente particular.
Esto se puede resolver manteniendo pistas de cada cliente asignándoles un
número incrementado serialmente ni bien se conecten al servidor tal como se ve en
el siguiente código:
public void OnClientConnect(IAsyncResult asyn)
{
try
{
// Aqui completamos/terminamos la llamada asincrónica BeginAccept()
// llamando a EndAccept() que devuelve la referencia a un nuevo
// objeto Socket
Socket socketActivo = socketPrincipal.EndAccept (asyn);
// Ahora, incrementamos el contador de clientes para este cliente
++contadorClientes;
// Agregamos la referencia al socket activo en el ArrayList
// Usaremos (numeroCliente – 1) como el indice para acceder a este
// socket en el futuro
socketActivos.Add(socketActivo);
//........
// Dejamos al Socket Activo hacer el proceso correspondiente para
// el cliente recien conectado
EsperarPorDatos(socketActivo, contadorClientes);
//........
Dentro de la función EsperarPorDatos(), haremos la llamada asincrónica
para recibir datos del cliente de la siguiente manera:
public void EsperarPorDatos(Socket soc, int num)
{
try
{
if( pfnWorkerCallBack == null )
{
// Especificamos la funcion callback que invocaremos cuando
// haya alguna actividad de escritura en el cliente conectado
pfnWorkerCallBack = new AsyncCallback (OnDataReceived);
}
SocketPacket socketPacket = new SocketPacket (soc, num);
// Empieza la recepcion de datos enviados por el cliente
// asincronicamente
soc.BeginReceive (theSocPkt.dataBuffer,
0,
theSocPkt.dataBuffer.Length,
SocketFlags.None,
pfnWorkerCallBack,
socketPacket);
//........
En el código anterior, la clase definida por el usuario SocketPacket es el
ítem mas critico. Un objeto de esta clase es el ultimo parámetro pasado a la
función asincrónica BeginReceive(). Este objeto contiene cualquier información
que encontremos útil; puede ser usada más tarde, cuando realmente recibamos
datos del cliente. Enviamos (1) el objeto Socket Activo y (2) el número de índice
10
del cliente empacado en ese objeto. Los recuperaremos cuando recibamos datos
de un cliente particular.
Debajo, está la definición de la clase SocketPacket.
public class SocketPacket
{
public Socket socketActual;
public int numeroCliente;
// Buffer para almacenar los datos enviados por el cliente
public byte[] dataBuffer = new byte[1024];
// Constructor que toma un Socket y un numero de cliente
public SocketPacket(Socket socket, int numero)
{
socketActual = socket;
numeroCliente = numero;
}
}
En el código anterior, la clase SocketPacket contiene la referencia a un
socket, un buffer de datos de un tamaño de 1024 bytes, y un número de cliente. El
número de cliente estará disponible cuando empecemos a recibir datos de un
cliente en particular. Usando éste número de cliente, podemos identificar que
cliente esta enviando datos.
Para demostrar esto, en el ejemplo, el servidor responderá al cliente (luego
de convertir en mayúsculas) el mensaje recibido, usando el objeto socket correcto.
11
Como responder o enviar mensajes a clientes específicos
Esto es muy simple de implementar. Como el objeto SocketPacket contiene
la referencia a un socket activo particular, simplemente usamos ese objeto para
responder a un cliente. Adicionalmente, podemos enviar cualquier mensaje a
cualquier cliente usando el objeto Socket Activo, almacenados en el ArrayList.
12
Como saber cuando un cliente en particular se desconecta
Esto es un poco más complicado. Deberíamos usar algún método más
elegante, pero esta es una manera simple y fácil de comprender.
Cuando un cliente se desconecte, habrá una ultima llamada a la función
OnDataReceived(). Si no hace nada en particular, arrojará un SocketException.
Lo que hacemos es mirar dentro de la excepción y ver si fue disparada por la
“desconexión” del cliente. Para ello, observamos si el código de error dentro del
objeto exception es igual a 10054. Si lo es, haremos la acción correspondiente a la
desconexión del cliente. Aquí nuevamente, el objeto SocketPacket nos dará el
número de cliente que se desconectó.
catch(SocketException se)
{
if(se.ErrorCode == 10054)
// Codigo de error para reseteo de
// conexión por el cliente
{
string msg = "Cliente " + socketData.numeroCliente +
" desconectado" + "\n";
txtMsgRecibidos.AppendText(msg);
// Eliminamos la referencia al socket activo del cliente cerrado
// así el objeto será recogido por el recolector de basura
socketActivos[socketData.numeroCliente - 1] = null;
ActualizarListaClientes();
}
else
{
MessageBox.Show (se.Message );
}
}
13
Como obtener la lista de los clientes conectados en cualquier momento
Para ver esto, se muestra una lista dinámica en la interfaz GUI del servidor
que será actualizada (ver la función ActualizarListaClientes()) cuando un cliente
se conecte o desconecte.
14
Aplicación para UDP
15
Server
16
private void btnAbrirCanalUDP_Click(object sender, EventArgs e)
{
try
{
if (txtPuerto.Text == "")
{
MessageBox.Show("Por favor ingrese numero de puerto");
return;
}
socketServer = new Socket(AddressFamily.InterNetwork,
SocketType.Dgram, ProtocolType.Udp);
socketServer.SetSocketOption(SocketOptionLevel.Udp,
SocketOptionName.NoDelay, 1);
socketServer.SetSocketOption(SocketOptionLevel.Socket,
SocketOptionName.ReuseAddress, 1);
socketServer.SetSocketOption(SocketOptionLevel.IP,
SocketOptionName.IpTimeToLive, 20);
IPAddress ip = IPAddress.Parse(txtIP.Text);
int iPuerto = Convert.ToInt16(txtPuerto.Text);
RemoteHostIPEnd = new IPEndPoint(ip, iPuerto);
RemoteEndPoint = (EndPoint)RemoteHostIPEnd;
LocalHostIPEnd = new IPEndPoint(IPAddress.Any, iPuerto);
LocalEndPoint = (EndPoint)LocalHostIPEnd;
socketServer.Bind(LocalHostIPEnd);
ActualizarControlBotones(true);
EsperarPorDatos();
}
catch (SocketException se)
{
MessageBox.Show(se.Message);
}
}
17
Cliente
18
void btnAbrirSocketUDP_Click(object sender, System.EventArgs e)
{
if (txtIP.Text == "" || txtPuerto.Text == "")
{
MessageBox.Show("Se requieren IP y Puerto para conectarse\n");
return;
}
try
{
ActualizarControles(false);
socketCliente = new Socket(AddressFamily.InterNetwork,
SocketType.Dgram, ProtocolType.Udp);
socketCliente.SetSocketOption(SocketOptionLevel.Udp,
SocketOptionName.NoDelay);
socketCliente.SetSocketOption(SocketOptionLevel.Socket,
SocketOptionName.Address);
socketCliente.SetSocketOption(SocketOptionLevel.IP,
SocketOptionName.IpTimeToLive, 20);
IPAddress ip = IPAddress.Parse(txtIP.Text);
int iPuerto = Convert.ToInt16(txtPuerto.Text);
RemoteHostIPEnd = new IPEndPoint(ip, iPuerto);
RemoteEndPoint = (EndPoint)RemoteHostIPEnd;
LocalHostIPEnd = new IPEndPoint(IPAddress.Any, iPuerto);
LocalEndPoint = (EndPoint)LocalHostIPEnd;
socketCliente.Bind(LocalHostIPEnd);
ActualizarControles(true);
EsperarPorDatos();
}
catch (SocketException se)
{
string str;
str = "\nFallo la conexion, esta corriendo el servidor?\n" +
se.Message;
MessageBox.Show(str);
ActualizarControles(false);
}
}
19
void btnEnviar_Click(object sender, System.EventArgs e)
{
try
{
string msg = txtMsgAEnviar.Text;
byte[] byData = System.Text.Encoding.ASCII.GetBytes(msg);
socketCliente.BeginSendTo(byData, 0, msg.Length, SocketFlags.None,
RemoteEndPoint,
new AsyncCallback(SendCallback),
socketCliente);
}
catch (SocketException se)
{
MessageBox.Show(se.Message);
}
}
20
Temas de Threading a considerar
1 - Las variables son seguras en métodos AsynCallBack? Y los hilos de
sincronización?
Cuando usamos llamadas asincrónicas, debemos tener en cuenta que, detrás
de esto, estamos usando hilos (threads) a nivel sistema operativo para alcanzar la
naturaleza asincrónica de estas llamadas.
El grafico siguiente muestra una simple ilustración de la interacción de los
hilos implicados en el ejemplo.
En el grafico anterior, el ítem (1) es el hilo principal del GUI que se inicia
cuando iniciamos la aplicación Servidor. El hilo (2) comienza cuando cualquier
cliente intente conectarse al socket. El hilo (3) comienza cuando haya alguna
actividad de escritura por parte de alguno de los clientes conectados.
En el código de ejemplo, las funciones asincrónicas OnClientConnect() y
OnDataReceived() son llamadas por hilos excepto el hilo principal del GUI.
Cualquier otra función llamada dentro de estas dos funciones serán invocadas por
hilos también, con excepción del hilo principal del GUI.
2 - Shared variables
Cualquier variable compartida que modifiquemos en el interior del código
compartido mencionado anteriormente, deberá ser protegida por estructuras de
sincronización. En el ejemplo, las variables compartidas que modificamos dentro del
código compartido son contadorClientes y socketActivos.
21
Podemos usar estrategias simples para proteger a estas variables. La
variable contadorCliente es un entero y por lo tanto puede ser incrementada
usando el método estático dentro de la clase Interlocked como se ve abajo:
// Incrementamos el contador de clientes de manera correcta
Interlocked.Increment(ref contadorCliente);
De manera similar, protegemos la variable miembro socketActivos de
modificaciones por parte de múltiples clientes al mismo tiempo, creando un
Synchronized ArrayList:
private ArrayList socketActivos = ArrayList.Synchronized(new ArrayList());
3 - Modificando la interfaz GUI
El hilo principal del GUI es quien realmente posee los controles del GUI. Por
lo tanto, no es recomendable ni aconsejable modificar o acceder a ninguno de los
controles del GUI a través de hilos, excepto que sea a través del hilo principal.
Cuando necesitemos actualizar la GUI, debemos hacer que el hilo principal lo haga,
como se ve en el siguiente código:
// Este método puede ser llamado por el hilo principal o cualquiera
// hilos activos
private void ActualizarControlMsgRecibidos (string msg)
{
// Chequea si el método fue llamado por un hilo distinto
// del hilo principal
if (InvokeRequired)
{
// No podemos actualizar la GUI en este hilo.
// Todos los controles GUI deben ser actualizados por
// el hilo principal
// Por lo tanto, usamos el motodo invoke en el control
// que sera llamado cuando el hilo principal este libre
// para actualizar a traves del hilo principal
object[] pList = {msg};
txtMsgRecibidos.BeginInvoke(new
ActualizarMsgRecibidos_Callback(ActualizarMsgRecibidos),
pList);
}
else
{
// Es el hilo principal, por lo tanto actualize directamente
ActualizarMsgRecibidos(msg);
}
}
private void ActualizarMsgRecibidos(string msg)
{
txtMsgRecibidos.AppendText(msg);
}
22
Descargar