Permission is granted to copy, distribute and/or modify this document under the terms of the GNU Free Documentation License, Version 1.1 or any later version published by the Free Software Foundation with no Invariant Sections, no Front-Cover Texts, and no Back-Cover Texts. A copy of the license can be found in GNU Free Documentation License.
Este artículo intenta mostrar de una forma práctica como implementar canales dentro de la arquitectura de .NET Remoting. Para confeccionar este artículo nos vamos a apoyar en el libro "Advanced .NET Remoting", en concreto en el ejemplo de implementación de un canal que viene en el capítulo 10 del mismo. Vamos a intentar reorganizar algunas partes de este mismo capítulo y a hacer hincapié en ciertos aspectos más complejos. Se utilizará también el ejemplo del canal de transporte SMTP/POP ya que obliga a ciertos esfuerzos en la implementación que ayudarán a comprender mejor la problemática de creación de canales.
Además, el artículo lo vamos a centrar sobre la plataforma Mono en GNU/Linux, en especial Mono sobre Debian, por lo que si hay detalles específicos de plataforma, se referirán a esta configuración. Aunque es de destacar que tan sólo cuando hablemos de la instalación de los servidores de SMTP y POP habrá añguna diferencia entre la plataforma GNU/Linux y otras, como Windows, MacOSX o las variantes de BSD. Tan sólo es necesario tener un entorno de desarrollo .NET en la plataforma. Para los lectores de Windows, indicar que cuando hablemos de Mono en el tutorial, es lo mismo que si hablaramos de .NET a efectos del tutorial.
El objetivo final es lograr los conocimientos suficientes para poder implementar el canal de IIOP que será parte del proyecto MonORB.
El artículo cubre aspectos muy específicos y que en general, no serán de gran utilidad para usar Remoting. Si lo que se quiere es aprender a utilizar Remoting, es más recomendable invertir el tiempo leyendo un tutorila de Remoting. Aunque si se quiere dominar de verdad Remoting y entender como funciona todo por dentro, este tutorial desvelará muchas de las intimidades de Remoting.
Finalmente el lector que haya seguido el tutorial tendrá una visión global de lo que es un canal, como se implementan las interfaces del cliente y del servidor, y verá que en realidad no es nada compleja la arquitectura, tan sólo hay que conocer bien las piezas del puzzle.
A lo largo del tutorial de creación de un canal de Remoting, nos vamos a ir enfrentando con distintos conceptos importantes, los cuales es necesario dominar para no perdernos entre los árboles y perdamos la vista del bosque.
A continuación vamos a intentar ir presentando estos conceptos, detallándalos ahora antes de utilizarlos, de forma que se puedan utilizar como referencia en la lectura del tutorial.
Comunicaciones síncronas/asíncronas: dentro de un canal Remoting se debe de intentar dar soporte a ambos tipos de comunicación. Veremos como utilizando un canal asíncrono como es el de SMTP/POP, podemos simular una comunicación síncrona utilizando hebras.
SMTP: Simple Mail Transfer Protocol, protocolo que permite la entrega de mensajes a buzones de correo registrados en una máquina. Los servidores de SMTP utilizan el puerto 25 para recibir las peticiones.
POP: Post Office Protocolo, protocolo que permite la recogida de mensajes en buzones. Los servidores de POP utilizan el puerto 110 para recibir peticiones de recogida de mensajes.
Polling: petición continúa en intervalos de tiempo normalmente constantes para comprobar si se han producido cambios en un determinado estado. Por ejemplo, en el caso del protocolo POP3, haremos Polling sobre los buzones para ver si se han recibido nuevos mensajes. El intervalo veremos que es algo configurable y constante.
Sumidero (Sink): En Remoting se conoce como sumidero a una clase que permite recibir o enviar un mensaje por él. Tenemos principalmente varios tipos de sumideros: los de mensaje, que permiten ir tratando el mensaje que se va a transmitir, los de formateo de datos que permiten serializar los mensajes antes de ser transmitidos y los sumideros de canal, que reciben un mensaje y lo envían al canal.
En realidad hay algún tipo de sumidero más que ya iremos viendo, pero vamos a centrarnos en estos de momento.
La interfaz de un sumidero de mensaje es
public interface IMessageSink { IMessage SyncProcessMessage (IMessage msg); IMessageCtrl AsyncProcessMessage (IMessage msg, IMessageSink replySink); IMessageSink NextSink { get; } }
Esta es la interfaz de un sumidero de formato de datos:
public interface IClientFormatterSink : IMessageSink, IClientChannelSink, IChannelSinkBase { }Como vemos esta es una interfaz de conveniencia que especifica que un sumidero de formateo es un sumidero de mensaje y un sumidero de canal de forma simultánea.
Vemos a continuación la interfaz de un sumidero de canal, que para el cliente es:
public interface IClientChannelSink : IChannelSinkBase { IClientChannelSink NextChannelSink { get; } void AsyncProcessRequest (IClientChannelSinkStack sinkStack, IMessage msg, ITransportHeaders headers, Stream stream); void AsyncProcessResponse (IClientResponseChannelSinkStack sinkStack, object state, ITransportHeaders headers, Stream stream); Stream GetRequestStream (IMessage msg, ITransportHeaders headers); void ProcessMessage (IMessage msg, ITransportHeaders requestHeaders, Stream requestStream, out ITransportHeaders responseHeaders, out Stream responseStream); }Para el cliente la interfaz de un sumidero de canal es:
public interface IServerChannelSink : IChannelSinkBase { IServerChannelSink NextChannelSink { get; } void AsyncProcessResponse (IServerResponseChannelSinkStack sinkStack, object state, IMessage msg, ITransportHeaders headers, Stream stream); Stream GetResponseStream (IServerResponseChannelSinkStack sinkStack, object state, IMessage msg, ITransportHeaders headers); ServerProcessing ProcessMessage (IServerChannelSinkStack sinkStack, IMessage requestMsg, ITransportHeaders requestHeaders, Stream requestStream, out IMessage responseMsg, out ITransportHeaders responseHeaders, out Stream responseStream); }
Dejamos aquí a modo de referencia todas estas interfaces que ya iremos implementando a lo largo de todo el tutorial.
Un canal de transporte de Remoting tan solo tiene que tener la característica de permitir enviar mensajes de forma síncrona y asíncrona. Ni más ni menos que eso, que se resume en poder implementar sobre ese canal la interfaz de un sumidero de canal. Y esto tan sólo nos obliga a implementar los métodos que permiten enviar un mensaje de forma síncrona y asíncrona, por lo que vamos a poder utilizar como canal de transporte de datos casi cualquier protocolo de intercambio de datos: http, smtp/pop, ftp, tcp ...
En este caso nos vamos a centrar en la combinación SMTP/POP, que el funcionamiento básico es que los mensajes se envían por SMTP, se recogen por POP, se procesan, y se vuelven a enviar por SMTP.
Los sumideros de canal estarán asociados a buzones de correo, es decir, cuando queramos transmitir un mensaje por un canal, lo que haremos será enviar un correo electrónico con el mensaje. Este correo se recogerá con el protocolo POP, es decir, que el servidor recogerá el correo con el mensaje para entregarlo al objeto adecuado.
Una vez que se ha procesado el mensaje y se tiene una respuesta, de nuevo se envía a un sumidero asociado a un correo electrónico. El cliente también utilizando el protocolo POP, recogerá este mensaje y lo procesará, entregando el resultado al programa del cliente.
Resumiendo, que hay un intercambio de mensajes por correo electrónico que transportan las invocaciones remotas. Es importante tener clara esta asociación entre direcciones de correo y sumideros, ya que cuando nos pongamos a implementar el código, no haremos mas que reflejar esta idea.
El protocolo de SMTP utiliza el puerto 25, y es un protocolo de entrega de mensajes. Como veremos, el diálogo que debemos de establecer con el servidor de SMTP es relativamente sencillo. Para poder completar esta parte necesitaremos tener en nuestra máquina instalado un servidor de correo. En debian por ejemplo nos valdría postfix, exim o algún otro servidor.
La idea en este momento es lograr crear unas clases que nos permitan trabajar de forma sencilla con SMTP, sin tener que conocer los detalles del protocolo cuando estemos trabajando con la parte de Remoting. Vamos pues a comenzar a mostrar el código de la clases que van a implementar las comunicaciones SMTP.
using System; using System.Collections; using System.Net.Sockets; using System.Net; using System.IO; using System.Text; namespace MonoHispano { namespace SMTPChannel { .... } }
De momento vemos las librerías que vamos a utilizar y el espacio de nombres en el que vamos a encuadrar este código. Como ha nacido este tutorial dentro de MonoHispano, pues le hacemos publicidad incluyéndolo como espacio de nombres principal. Y dentro de este, utilizamos el espacio de nombres SMTPChannel para incluir toda la implementación del canal. De momento no incluímos la licencia del ejemplo ya que es probable que finalmente utilicemos el código completo del propio Ingo Rammer. Lo que vamos a hacer en este tutorial es irlo destripando.
En esta clase vamos a incluir todos los detalles de como establecer una conexión con el servidor de SMTP, que como ya hemos comentado, esta en el puerto 25 escuchando a la espera de la llegada de mensajes.
public class SMTPConnection { private const Int32 _smtpPort = 25; private String _hostname; private TcpClient _smtpConnection; private NetworkStream _smtpStream; private StreamReader _smtpResponse;
De momento declaramos las variables privadas de la clase. Nos vamos a basar en la librería de sockets de Mono que nos permiten de forma sencilla realizar una conexión Tcp utilizando la clase TcpClient. Para establecer esta conexión Tcp, vamos a necesitar el puerto al que nos queremos concetar, el de SMTP que es el 25, y la máquina remota. Aquí podemos ver que el intercambio de mensajes se va a poder producir entre dos máquinas cualquieras conectadas a Internet, ya que es habitual que todos los cortafuegos tengan abierto el puerto 25, el de entrega de correo.
Una vez establecida la conexión Tcp, para poder leer y escribir datos en la conexión utilizaremos Streams, el método más habitual en Mono para leer flujos de datos, algo que es idéntico por ejemplo a como se hacía en Java.
public SMTPConnection(String hostname) { _hostname = hostname; }
El constructor es muy sencillo y lo único que necesita es que lñe especifiquemos la máquina remota a la que nos vamos a conectar, donde deberá de estar funcionando un servidor de correo para poder entregar los mensajes.
private void Connect() { _smtpConnection = new TcpClient (_hostname, _smtpPort); _smtpStream = _smtpConnection.GetStream (); _smtpResponse = new StreamReader (_smtpStream); }
El método Connect es de los primeros en tener ya realmente código interesante. Vemos lo sencillo que es en Mono establecer una conexión Tcp con una máquina y puerto remotos. Una vez que tenemos ya la conexión, con el método "GetStream" obtenemos el flujo (stream) de intercambio de datos a través de la conexión.
De este flujo de la conexión podemos leer y escribir. Para obtener el flujo de lectura, utilizamos la clase StreamReader, que nos permite obtener de un flujo de datos bidireccional, el flujo de lectura. De "_smtpResponse" vamos a poder leer fácilmente las respuestas que nos de el servidor de correo a nuestros comandos.
private void Disconnect() { _smtpResponse.Close (); _smtpStream.Close (); _smtpConnection.Close (); }
Siempre es fundamental cuando terminamos de utilizar recursos, liberarlos. Y en especial si utilizamos recursos limitados como los de red. Cuando nos desconectamos del servidor de SMTP cerramos los flujos de datos que teníamos abiertos y cerramos la conexión.
private void SendCommand(String command, int expectedResponseClass) { command = command +"\r\n"; byte[] cmd = Encoding.ASCII.GetBytes(command); _smtpStream.Write(cmd, 0, cmd.Length); String response = _smtpResponse.ReadLine(); if (expectedResponseClass != 0) { int resp = Convert.ToInt32(response.Substring(0,1)); if (resp > expectedResponseClass) { throw new Exception("El servidor SMTP devolvió algo inesperado: "+ " response:\n'" + response + "'"); } } }
Este es uno de los métodos estrella de la clase SMTPConnection. Nos permite "SendCommand" enviar un comando a través de la conexión con el servidor SMTP. A cada comando que enviemos recibiremos una respuesta del servidor.
Para invocar un comando en el servidor de SMTP, lo pasamos como una string a este método junto con el código de respuesta que esperamos del servidor. En el caso de que la respuesta que nos da el servidor no es la que esperamos, normalmente por haberse producido un error, levantamos una excepción indicando la respuesta no esperada que nos ha dado el servidor de correo. Estas respuestas son diferentes en cada servidor de correo.
Vemos como se utiliza los flujos de datos para enviar un buffer de bytes por el mismo (_smtpStream.Write(cmd, 0, cmd.Length)). Y como, una vez enviado, podemos leer la respuesta que nos envía el servidor utilizando el flujo de datos de lectura ( _smtpResponse).
Veamos un ejemplo utilizando telnet y conectandonos al puerto 25 de una máquina de lo que puede ocurrir cuando hablemos con un servidor de correo, en este caso Postfix.
acs@linex:~/devel/web-xml/mono.es.gnome.org/tutoriales/remoting-canal/code$ telnet localhost 25 Trying 127.0.0.1... Connected to localhost. Escape character is '^]'. 220 localhost ESMTP Postfix (Debian/GNU)
El servidor de correo esta funcionando y se queda a la espera de ver que queremos :)
HELP 502 Error: command not implemented
Vemos que si intentamos enviarle un comando que no existe, nos devuelve un código de error "502" y un mensaje. Pero si le enviamos un mensaje correcto:
HELO localhost 250 localhost
nos contesta de con el código "250".
Para poder utilizar el servidor de SMTP pues necesitamos saber hablar con él, algo que nos cuenta el RFC2821. En realidad necesitamos saber muy poco del protocolo, tal y como vamos a ver a continuación.
public void SendMessage(String from, String to, String text) { try { Connect(); SendCommand("HELO localhost",2); SendCommand("MAIL FROM: <" + from +">",2); SendCommand("RCPT TO: <" + to + ">",2); SendCommand("DATA",3); byte[] bodybytes = Encoding.ASCII.GetBytes(text + "\r\n"); _smtpStream.Write(bodybytes,0,bodybytes.Length); SendCommand(".",3); SendCommand("QUIT",0); } finally { try { Disconnect(); } catch (Exception e) { } } } }
Al fin hemos llegado al metódo clave con el que luego engancharemos Remoting, "SendMessage". Este método se encarga de recibir un texto con el mensaje a transmitir, a quien debe enviarlo y quien debe de aparecer como remitente.
Veremos que hay una estrecha relación entre los buzones de correo y los sumideros de canal del servidor. Un sumidero de canal del servidor estará asociado a una dirección de correo por la que recibe los comandos a ser invocados por el objeto remoto.
De igual forma, los sumideros de canal del cliente estarán ligados a las direcciones de correo del remitente.
En este método podemos ver el protocolo a seguir para enviar un mensaje de correo al servidor de SMTP. Por cada mensaje que enviemos se envía un correo electrónico estableciendo una nueva conexión con el servidor de correo remoto.
Aquí podríamos pensar en optimizar el uso de las conexiones enviando más de un correo sobre una misma conexión, o incluso enviando más de un mensaje en un mismo correo electrónico. Pero como no queremos de momento realizar un canal de alto rendimiento y uso óptimo de recursos, si no aprender como se hace un canal, no vamos a entrar en estos detalles.
Vamos por último a mostrar que todo funciona realmente. Para ello nos creamos una clase que hace uso de SMTPConnection y envía un comando al servidor.
public class TestSMTP { public static void Main (string[] args) { SMTPConnection con; con = new SMTPConnection ("localhost"); con.SendMessage("acs@localhost","acs","Prueba de C#"); } }
Esto provoca el envío de un correo electrónico tal y como nos muestran los logs del servidor de correo.
Aug 31 12:18:10 localhost postfix/smtpd[1207]: connect from localhost[127.0.0.1] Aug 31 12:18:10 localhost postfix/smtpd[1207]: 3024F305E0: client=localhost[127.0.0.1] Aug 31 12:18:10 localhost postfix/cleanup[1208]: 3024F305E0: message-id=<20020831101810.3024F305E0@localhost> Aug 31 12:18:10 localhost postfix/qmgr[1200]: 3024F305E0: from=<acs@localhost>, size=315, nrcpt=1 (queue active) Aug 31 12:18:10 localhost postfix/smtpd[1207]: disconnect from localhost[127.0.0.1] Aug 31 12:18:10 localhost postfix/local[1210]: 3024F305E0: to=<acs@linex>, relay=local, delay=0, status=sent (mailbox)
Podemos ver como se realiza la conexión, como se le asigna un identificador al mensaje que se envía, el remitente y la cuenta de destino y la confirmación de que el mensaje ha sido entregado.
Ahora si vamos al buzón de la cuenta "acs" encontramos el nuevo mensaje.
Mail version 8.1.2 01/15/2001. Type ? for help. "/var/mail/acs": 1 message 1 unread >U 1 acs@localhost Sat Aug 31 12:18 14/415 & 1 Message 1: From acs@localhost Sat Aug 31 12:18:10 2002 Date: Sat, 31 Aug 2002 12:18:10 +0200 (CEST) From: acs@localhost To: undisclosed-recipients:; Prueba de C#
Ahora este mensaje tenemos que recogerlo para entregárselo al sumidero del servidor que se encargará de procesarlo. Para poder recoger todos los mensajes de un buzón, el servidor utilizará el protocolo POP3, cuyo uso paso a analizar a continuación.
Hasta el momento hemos visto una parte del canal, la que permite enviar los mensajes a los buzones de correo, que vienen a identificar a los sumideros dentro del servidor de Remoting. Pero esos mensajes hay que recogerlos, procesarlos y entregarlos al objeto remoto al que se le envía el mensaje, para que este pueda obtener del mensaje el metódo a invocar y los parámetros.
Para recoger los mensajes vamos a utilizar el protocolo POP3, el cual como vamos a ver es más simple que SMTP, pero veremos que la implementación que necesitamos realizar para poder luego utilizar POP3 dentro de Remoting es algo más compleja.
Como ya comentamos en la sección de conceptos, nos vamos a encontrar con dos tipos de comunicaciones: síncronos y asíncronas. La comunicación basada en SMTP y POP3 es asíncrona, por lo que las invocaciones remotas asíncronas de Remoting se van a implementar de forma sencilla sobre este canal. Pero para poder realizar las invocaciones síncronas, vamos a tener que utilizar mecanismos de sincronización basados en hebras, como iremos viendo en esta implementación.
Al igual que hicimos con el protocolo SMTP lo primero es aprender a comunicarnos con el servidor de POP3 para recuperar los mensajes asociados a una cuenta de correo. El protocolo de POP3 se puede consultar en la RFC1939, siendo POP3 un protocolo más sencillo com vamos a ver a continuación.
Los servidores de POP3 escuchan en el puerto 110 y un ejemplo de sesión con un servidor de POP3, en este caso el ipopd de la Universidad de Washington, lo podemos observar a continuación.
acs@linex:~$ telnet localhost 110 Trying 127.0.0.1... Connected to localhost. Escape character is '^]'. +OK POP3 localhost v2001.78 server ready USER acs +OK User name accepted, password please PASS ******* +OK Mailbox open, 1 messages LIST +OK Mailbox scan listing follows 1 382 . RETR 1 +OK 382 octets Return-Path: <acs@localhost> Delivered-To: acs@linex Received: from localhost (localhost [127.0.0.1]) by localhost (Postfix) with SMTP id 3024F305E0 for <acs>; Sat, 31 Aug 2002 12:18:10 +0200 (CEST) Message-Id: <20020831101810.3024F305E0@localhost> Date: Sat, 31 Aug 2002 12:18:10 +0200 (CEST) From: acs@localhost To: undisclosed-recipients:; Status: RO Prueba de C# . DELE 1 +OK Message deleted LIST +OK Mailbox scan listing follows .
Como vemos para obtener los mensajes de un buzón basta con que nos presentemos con el nombre de usuario (USER) y la clase (PASS) y luego pedir el número de mensaje que queremos obtener (RETR). Una vez procesado un mensaje lo borramos con (DELE). Vamos pues a ver como se implementar la clase TcpConnection.
public class POP3Connection { private class MessageIndex { internal int Number; internal int Bytes; internal MessageIndex(int num, int msgbytes) { Number = num; Bytes = msgbytes; } } private String _hostname; private String _username; private String _password; private TcpClient _pop3Connection; private NetworkStream _pop3Stream; private StreamReader _pop3Response; private IDictionary _msgs; public int MessageCount { get { // returns the message count after connecting and // issuing the LIST command return _msgs.Count; } } public POP3Connection(String hostname, String username, String password) { _hostname = hostname; _username = username; _password = password; try { Connect(); } catch (Exception ex) { try { Disconnect(); } catch (Exception e) { } } }
De momento no hemos hecho gran cosa. Tenemos una clase interna que vamos a utilizar para mantener los datos de los mensajes (MessageIndex), variables que vamos a utilizar para ir almacenando los valores necesarios para establecer la conexión y una propiedad, MessageCount, que va a almacenar el número total de mensajes. Además definimos el constructor de la clase, que establece una conexión nada más crear una instancia de la clase. Para ello utiliza el método Connection que pasamos a analizar.
public void Connect() { // initialize the list of messages _msgs = new Hashtable(); // open the connection _pop3Connection = new TcpClient(_hostname,110); _pop3Stream = _pop3Connection.GetStream(); _pop3Response = new StreamReader(_pop3Stream); // ignore first line (server's greeting) String response = _pop3Response.ReadLine(); // authenticate SendCommand("USER " + _username,true); SendCommand("PASS " + _password,true); // retrieve the list of messages SendCommand("LIST",true); response = _pop3Response.ReadLine(); while (response != ".") { // add entries to _msgs dictionary int pos = response.IndexOf(" "); String msgnumStr = response.Substring(0,pos); String bytesStr = response.Substring(pos); int msgnum = Convert.ToInt32(msgnumStr); int bytes = Convert.ToInt32(bytesStr); MessageIndex msgidx = new MessageIndex(msgnum,bytes); _msgs.Add (msgidx,msgnum); response = _pop3Response.ReadLine(); } }
De nuevo utilizamos la clase TcpClient para establecer una conexión Tcp con el puerto de POP3 (110) y, obtenemos los flujos de intercambio de datos utilizando esta conexión. Utilizando el flujo de escritura, una vez leído el mensaje de saludo del servidor POP3, envíamos por el canal el usuario y clave asociados al buzón que queremos consultar.
Una vez autenticados con el servidor de POP3, llega el momento de obtener la lista de todos los mensajes en el buzón (LIST) y vamos analizando cada uno de los mensajes y los añadimos a la tabla hash con todos los mensajes (_msgs). De cada mensaje guardamos su número y el número de octetos (bytes) que contiene.
Para enviar los comandos al servidor POP3 utilizamos la clase "SendCommand". Como vamos a ver es una clase auxiliar que comprueba la contestación del servidor para actuar en caso de errores.
private void SendCommand(String command,bool needOK) { // enviamos un único comando // si needOK es verdadero se comprueba que la respuesta comience // con "+OK" y se levanta una excepción si no es así command = command + "\r\n"; byte[] cmd = Encoding.ASCII.GetBytes(command); // enviar el comando _pop3Stream.Write(cmd,0,cmd.Length); String response = _pop3Response.ReadLine(); // comprobar la respuesta if (needOK) { if (!response.Substring(0,3).ToUpper().Equals("+OK")) { throw new Exception("POP3 Server returned unexpected " + "response:\n'" + response + "'"); } } }
Junto con la conexión tenemos que implementar la desconexión del servidor de POP3, algo que es sencillo de implementar como vemos a continuación ya que basta con enviar "QUIT" al servidor de POP3 para finalizar la comunicación.
public void Disconnect() { // envía QUIT y desconecta try { // send QUIT to commit the DELEs SendCommand("QUIT",false); } finally { // close the connection _pop3Stream.Close(); _pop3Response.Close(); _pop3Connection.Close(); } }
Casi hemos acabado ya con esta clase pero aún nos queda lo más importante: la obtención de mensajes y su borrado. Vamos primero con el borrado de mensajes que es realmente sencillo.
public void DeleteMessage(int msgnum) { // envia el comando DELE para borrar el mensaje especificado SendCommand("DELE " + msgnum,true); }
Y ahora vamos con el método de obtención de mensajes, que ha de ser capaz de procesar un mensaje de correo y obtener de él los datos que interesan de cara a invocar sobre un objeto remoto un método.
En este método ya vamos a comenzar a tener en cuenta algunas necesidades de Remoting y lo que vamos a devolver va a ser un objeto de la clase POP3Msg que se podrá ya tratar de forma sencilla desde Remoting.
public POP3Msg GetMessage(int msgnum) { // creamos el objeto resultante POP3Msg tmpmsg = new POP3Msg(); // obtenemos un sólo mensaje SendCommand("RETR " + msgnum,true); String response = _pop3Response.ReadLine(); // leemos la respuesta linea a linea y vamos rellenando las // propiedades del objeto POP3Msg StringBuilder headers = new StringBuilder(); StringBuilder body = new StringBuilder(); bool headersDone=false; while ((response!= null) && (response != "." )) { // comprobar si hemos terminado de leer las cabeceras if (!headersDone) { if (response.Length >0) { // sólo vamos a procesar las cabeceras relevantes // para .NET Remoting if (response.ToUpper().StartsWith("IN-REPLY-TO:")) { tmpmsg.InReplyTo = response.Substring(12).Trim(); } else if (response.ToUpper().StartsWith("MESSAGE-ID:")) { tmpmsg.MessageId = response.Substring(11).Trim(); } else if (response.ToUpper().StartsWith("FROM:")) { tmpmsg.From = response.Substring(5).Trim(); } else if (response.ToUpper().StartsWith("TO:")) { tmpmsg.To = response.Substring(3).Trim(); } headers.Append(response).Append("\n"); } else { headersDone = true; } } else { // ya hemos leído todas las cabeceras. Viene el cuerpo // Para NET Remoting, nos interesa tener el cuerpo en una línea // para decodificar Base64 por lo que no añadiremos <CR><LF>s body.Append(response); } // leemos la siguiente línea del cuerpo response = _pop3Response.ReadLine(); } // completamos POP3Msg con la cabecera y cuerpo leídos tmpmsg.Body = body.ToString(); tmpmsg.Headers = headers.ToString(); return tmpmsg; }
Para terminar con esta clase vamos a mostrar el contenido de POP3Msg, que en realidad se podría incluso declarar como una estructura de datos para optimizar el uso de recursos, al no ser POP3Msg más que un registro con un conjunto de campos.
public class POP3Msg { public String From; public String To; public String Body; public String Headers; public String MessageId; public String InReplyTo; }
Para concluir con este apartado, ya tenemos resuelto todo el problema de comunicaciones con un servidor POP3 y ya sabemos como obtener mensajes utilizándolo.
Esto nos va a ser realmente muy útil a partir de este momento, ya que a diferencia del uso que hacemos de SMTP, que lo usamos cuando lo necesitamos para enviar un mensaje, en el caso de POP3 no sabemos cuando vamos a recibir mensajes y POP3 no tiene ningún mecanismo para realizar este aviso.
Esto nos va a obligar a realizar polling, consultas continuas en intervalos regulares de tiempo, sobre todos los buzones en los que se puedan recibir mensajes destinados a objetos remotos. Y la implementación de este Polling es justamente lo que vamos a ver en el próximo apartado.
A diferencia de los clientes, que deciden cuando quieren realizar una invocación de un método remoto, el servidor tiene que estar de forma contínua comprobando si se han recibido nuevos mensajes con peticiones a objetos. Para ello, se utiliza el sistema de polling sobre el servidor de POP3 monitorizando todos los buzones.
El código que implementar esta monitorización de los buzones está dentro de la clase POP3Polling y en él, utilizaremos mecanismos de hebras.
using System; using System.Collections; using System.Threading; namespace MonoHispano { namespace SMTPChannel { public class POP3Polling { delegate void HandleMessageDelegate(POP3Msg msg); // si la instancia está asociada a un servidor de remoting // siempre estaremos comprobando la llegada de mensajes internal bool _isServer; // si no es un servidor, esta variable controla cuando se hace recogida internal bool _needsPolling; // si estamos recogiendo ahora mensajes internal bool _isPolling; // el intervalo de recogida internal int _pollInterval; // datos de acceso a los buzones private String _hostname; private String _username; private String _password; internal POP3Polling(String hostname, String username, String password, int pollInterval, bool isServer) { _hostname = hostname; _username = username; _password = password; _pollInterval = pollInterval; _isServer = isServer; if (!_isServer) { _needsPolling = false; } } private void Poll() { if (_isPolling) return; _isPolling = true; do { Thread.Sleep(_pollInterval * 1000); POP3Connection pop = new POP3Connection(_hostname,_username,_password); for (int i =1;i<=pop.MessageCount;i++) { POP3Msg msg = pop.GetMessage(i); HandleMessageDelegate del = new HandleMessageDelegate(SMTPHelper.MessageReceived); del.BeginInvoke(msg,null,null); pop.DeleteMessage(i); } pop.Disconnect(); pop = null; } while (_isServer || _needsPolling); _isPolling = false; } internal void CheckAndStartPolling() { if (_isPolling) return; if (_isServer || _needsPolling) { Thread thr = new Thread(new ThreadStart(this.Poll)); thr.Start(); thr.IsBackground = true; } } } } }
Sin duda, esta clase requiere algo más de explicación. La recogida de correos es necesaria tanto para recibir los mensajes que envía el cliente al servidor como para recoger las respuestas pro parte del cliente. Por ello, vamos a tener dos tipos de recogida (polling) diferentes: el del servidor que estará haciendo siempre recogida y el del cliente que hará recogida cuando esté esperando una respuesta.
El cliente, cuando necesite recoger la respuesta a una invocación, modificará la variable "_needsPolling".
El constructor de la clase es bastante sencillo, recibiendo los datos de conexión al servidor de POP3 para recoger los mensajes, el intervalo de recogida y si es un servidor o un cliente el que está haciendo la recogida.
El inicio de la recogida se realiza con el método "CheckAndStartPolling" que se llamará nada más crear una instancia en los servidores, o cuando sea necesario en los clientes. Este método se encarga de crear una hebra que invoca el método "Poll" cuando se inicia. Esta hebra crea conexiones POP3 utilizando la clase POP3Connection que ya mostramos anteriormente, y recoge los mensajes disponibles a intervalos regulares (pollInterval).
En el método Poll tenemos una llamada clave a un delegado que se encarga de recibir las notificaciones de que un nuevo mensaje se ha recibido, así como el mensaje que ha sido recibido.
HandleMessageDelegate del = new HandleMessageDelegate(SMTPHelper.MessageReceived); del.BeginInvoke(msg,null,null);
Esta invocación al método delegado es clave ya que como veremos, la clase SMTPHelper es la que va a unir a nuestras clases de SMTP y POP3 con Remoting. Cuando se recibe un nuevo mensaje, se realiza un "callback" sobre SMTPHelper para que este a su vez informe a Remoting de la llegada de un nuevo mensaje. Ya volveremos sobre esta llamada un poco más adelante.
Resumideno, la clase POP3Polling no es tampoco especialmente compleja, y veremos que de nuevo, nos separa aún más de los mecanismos específicos de SMTP y POP para el envío de mensajes y nos acerca un poco más a las interfaces de Remoting.
Para poder acceder a objetos remotos y poder invocar sobre ellos métodos, necesitamos conocerlos para poder enviarles los métodos. Este envío se realiza sobre el canal de comunicaciones de Remoting, y en este caso dicho canal se implementa sobre SMTP.
Los servidores que ofrecen los objetos remotos al mundo Remoting se comunican con los clientes a través de SMTP, es decir, a través de buzones de correo. Y es el registro de estos servidor y su relación con cuentas de correo lo que vamos a implementar a continuación.
public class POP3PollManager { // dictionary of polling instances static IDictionary _listeners = Hashtable.Synchronized(new Hashtable()); // number of sent messages for which no response has been received private static int _pendingResponses; private static int _lastPendingResponses; public static void RegisterPolling(String hostname, String username, String password, int pollInterval, bool isServer) { String key = username + "|" + hostname; POP3Polling pop3 = (POP3Polling) _listeners[key]; if (pop3 == null) { // create a new listener pop3 = new POP3Polling(hostname,username,password, pollInterval,isServer); _listeners[key]= pop3; } else { // change to server-mode if needed if (!pop3._isServer && isServer) { pop3._isServer = true; } // check for pollInterval => lowest interval will be taken if (! (pop3._pollInterval > pollInterval)) { pop3._pollInterval = pollInterval; } } pop3.CheckAndStartPolling(); } internal static void RequestSent() { _pendingResponses++; if (_lastPendingResponses<=0 && _pendingResponses > 0) { IEnumerator enmr = _listeners.GetEnumerator(); while (enmr.MoveNext()) { DictionaryEntry entr = (DictionaryEntry) enmr.Current; POP3Polling pop3 = (POP3Polling) entr.Value; pop3._needsPolling = true; pop3.CheckAndStartPolling(); } } _lastPendingResponses = _pendingResponses; } internal static void ResponseReceived() { _pendingResponses--; if (_pendingResponses <=0) { IEnumerator enmr = _listeners.GetEnumerator(); while (enmr.MoveNext()) { DictionaryEntry entr = (DictionaryEntry) enmr.Current; POP3Polling pop3 = (POP3Polling) entr.Value; pop3._needsPolling = false; } } _lastPendingResponses = _pendingResponses; } }
La clase POP3PollManager tiene como objetivo el gestionar las diferentes recogidas de mensajes que se deben de realizar para entregar los métodos remotos a sus correspondientes objetos.
Para ello mantiene una tabla Hash con todos los buzones que se están monitorizando, tabla Hash que al ser accedida por diferentes hebras, tiene controlado el acceso por un monitor (Synchronized).
La clase permite permite dar de alta nuevos servidores (listener) para monitorizar la llegada de mensaje a nuevos buzones con el método "RegisterPolling" que recibe como parámetros la máquina donde reside el buzón, los datos de acceso al buzón, en intervalo de comprobación de llegada de mensajes y si es un servidor lo que estamos dando de alta, o es un cliente que esta a la espera de respuestas.
A la vista de este método, podemos ver que los buzones pueden estar repartidos en diferentes máquinas sin que esto afecte para nada al uso que hace Remoting de este canal.
Este método hace uso de la clase POP3Polling para relizar la recogida de mensajes, y vemos que ya aquí, no necesitamos conocer para nada los detalles de como se hace este seguimiento o como se maneja el protocolo POP3.
Como ya hemos indicado, en los buzones podemos tener mensajes con invocaciones hacia los objetos remotos, o respuestas a la invocaciones remotas sobre los objetos. Para diferenciar entre unas y otras, tenemos los métodos "RequestSent" y "ResponseReceived".
"RequestSent" incrementa el número de respuesta que aún se esperan y en el caso de que sea necesario, levanta las hebras adecuadas para que comiencen a intentar recibir esas respuestas pendientes en el caso de que no lo estuvieran ya (_lastPendingResponses<=0).
"ResponseReceived" decrementa el número de respuestas que se esperan y en el caso de que ya no se esperen más, detiene las hebras que estaban intentando recibir respuestas.
Hasta el momento nos hemos centrado en crear unas clases que nos van a ayudar a abstraernos de como funcionan los protocolos SMTP y POP3, y también, en gran medida, nos van a abstraer del mecanismo de entrega y recogida de mensajes.
Cuando implementemos el canal de IIOP, deberemos de trabajar de forma muy similar hasta este punto, pero teniendo en cuenta todos los detalles del protocolo IIOP a la hora de enviar y recibir invocaciones remotas.
Al fin ya hemos preparado todas las clases necesarias en las que nos vamos a apoyar con el canal de Remoting que estamos creando. Y viene sin duda la parte más interesante desde el punto de vista de Remoting, el cómo enganchar todo el desarrollo anterior con la implementación de las interfaces de Remoting.
En general es habitual que los canales tengan esta clase "Helper" que va a acercar aún más la interfaz del canal a las interfaces de Remoting. Se encargará de tratar con todos los detalles específicos del canal que estamos utilizando para transmitir los mensajes de Remoting. Aquí por ejemplo vamos a tener que gestionar las direcciones de correo, el formato de los correos electrónicos y como introducir en ellos el mensaje a transmitir asi como especificiar en las cabeceras la localización del objeto remoto.
Por último veremos que también se encarga de simular las comunicaciones síncronas sobre unas comunicaciones asíncronas como son las de SMTP/POP3. En definitiva, es una de las clases más importantes que tenemos en nuestra implementación.
public class SMTPHelper { // threads waiting for response private static IDictionary _waitingFor = Hashtable.Synchronized(new Hashtable()); // known servers private static IDictionary _servers = Hashtable.Synchronized(new Hashtable()); // responses received private static IDictionary _responses = Hashtable.Synchronized(new Hashtable());
De momento hemos definido tres tablas hash privadas en las que tendremos a los clientes esperando por respuestas, a los servidores disponibles y a las respuestas que se han recibido.
Vamos ahora con la implementación del envío de mensajes la cual tiene muchos detalles muy interesantes, por lo que vamos a ir comentando el código dentro de la propia función.
// envío de mensajes private static void SendMessage(String ID,String replyToId, String mailfrom, String mailto, String smtpServer, ITransportHeaders headers, Stream stream, String objectURI)
Vemos que se pasan los parámetros del remitente, destinatario y servidor de SMTP al que enviar el mensaje. También se puede pasar un identificador de mensaje y el interesante "replyToId" que usaremos para asociar invocaciones y respuestas.
Finalmente recibimos ya parámetros habituales de Remoting, como son las cabeceras a incluir dentro del mensaje, el "stream" del que leer el mensaje a enviar y quizá el parámetro más importante, el identificador del objeto remoto sobre el que se realizará la invocación.
{ StringBuilder msg = new StringBuilder(); if (ID != null) { msg.Append("Message-Id: ").Append(ID).Append("\r\n"); } if (replyToId != null) { msg.Append("In-Reply-To: ").Append(replyToId).Append("\r\n"); } msg.Append("From: ").Append(mailfrom).Append("\r\n"); msg.Append("To: ").Append(mailto).Append("\r\n"); msg.Append("MIME-Version: 1.0").Append("\r\n"); msg.Append("Content-Type: text/xml; charset=utf-8").Append("\r\n");
La invocación de métodos se va a especificar en XML. Se utilizará el formateador de SOAP para transformar la invocación del método en una cadena de caracteres que se puede transmitir.
msg.Append("Content-Transfer-Encoding: BASE64").Append("\r\n"); // escribimos las cabeceras remotas IEnumerator headerenum = headers.GetEnumerator(); while (headerenum.MoveNext()) { DictionaryEntry entry = (DictionaryEntry) headerenum.Current; String key = entry.Key as String; if (key == null || key.StartsWith("__")) { continue; } msg.Append("X-REMOTING-").Append(key).Append(": "); msg.Append(entry.Value.ToString()).Append("\r\n"); }
Todos los parámetros de la cabecera de Remoting los escribimos en los campos "X-" que se pueden introducir dentro de la cabecera de los mensajes de correo electrónico. Luego se deberán de sacar de este sitio cuando se procese el mensaje.
if (objectURI != null) { msg.Append("X-REMOTING-URI: ").Append(objectURI).Append("\r\n"); } msg.Append("\r\n");
Y como no podía ser de otra forma, incluísmo también el identificador del objeto remoto sobre el que vamos a realizar la invocación.
MemoryStream fs = new MemoryStream(); byte[] buf = new Byte[1000]; int cnt = stream.Read(buf,0,1000); int bytecount = 0; while (cnt>0) { fs.Write(buf,0,cnt); bytecount+=cnt; cnt = stream.Read(buf,0,1000); }
Acabamos de leer el contenido de todo el mensaje a enviar y lo vamos a transmitir en el cuerpo del mensaje de correo electrónico.
// convertimos toda la cadena a codificación Base64 String body = Convert.ToBase64String(fs.GetBuffer(),0,bytecount); // and ensure the maximum line length of 73 characters int linesNeeded = (int) Math.Ceiling(body.Length / 73); for (int i = 0;i<=linesNeeded;i++) { if (i != linesNeeded) { String line = body.Substring(i*73,73); msg.Append(line).Append("\r\n"); } else { String line = body.Substring(i*73); msg.Append(line).Append("\r\n"); } } // enviamos el resultado en el mensaje SMTPConnection con = new SMTPConnection (smtpServer); con.SendMessage(mailfrom,mailto,msg.ToString()); }
Pues ya hemos visto como se puede enviar un mensaje de Remoting sobre SMTP. Hasta el momento habíamos hablado de mensajes de correo electrónico pero en este método ya hemos fijado como se traduce a un correo electrónico el mensaje de Remoting, incluyendo aspectos como el identificador de objeto.
Vamos ahora con métodos que utilizan el método básico de envío de mensajes, pero que ya diferencian entre lo que son peticiones y lo que son respuestas.
internal static void SendRequestMessage(String mailfrom, String mailto, String smtpServer, ITransportHeaders headers, Stream request, String objectURI, out String ID) { ID = "<" + Guid.NewGuid().ToString().Replace("-","") + "@REMOTING>"; SendMessage(ID,null,mailfrom,mailto,smtpServer,headers,request,objectURI); POP3PollManager.RequestSent(); }
Lo más destacable de este método es que aquí se genera el identificador de mensaje, el cual ya veremos como se utiliza luego para asociar peticiones y respuestas, tras lo que se envía el mensaje y se informa al gestor de peticiones (POP3PollManager) de que se ha enviado una petición, lo que provocará que se comience a monitorizar un buzón de correo en el que se recibirá la respuesta a la petición.
internal static void SendResponseMessage(String mailfrom, String mailto, String smtpServer, ITransportHeaders headers, Stream response, String ID) { SendMessage(null,ID,mailfrom,mailto,smtpServer,headers,response,null); }
El envío de una respuesta es mucho más sencillo ya que no hay que monitorizar si hay alguna respuesta a la respuesta ;-) La respuesta será consumida por el cliente que realizó la invocación en cuanto su monitor se de cuenta de que se ha recibido.
Llagamos a uno de los métodos más importantes del canal. En Remoting podemos enviar los mensajes de forma síncrona, es decir, la ejecución de la hebra o el proceso en el que se reliaza la petición, se debe de quedar bloqueada hasta que se reciba la respuesta. Este es el funcionamiento habitual en la invocación de funciones asi que será una de las formas más habituales de usar el canal.
Pero nuestro canal es de naturaleza asíncrona por lo que vamos a tener que realizar la petición y, quedarnos bloqueados a la espera de que nos llegue la respuesta a través de nuestro monitor de buzón de repuesta. El siguiente método cumple precisamente esta labor, la de suspender la ejecución hasta la llegada de la respuesta.
// esperando a las respuestas internal static POP3Msg WaitAndGetResponseMessage(String ID) { // suspend the thread until the message returns _waitingFor[ID] = Thread.CurrentThread; Thread.CurrentThread.Suspend(); // waiting for resume POP3Msg pop3msg = (POP3Msg) _responses[ID]; _responses.Remove(ID); return pop3msg; }
El identificador del mensaje indica a que respuesta estamos esperando. Quizá sería interesante añadir algún tipo de temporizadores para que, si no recibimos respuesta en un determinado intervalo, levantar una excepción. Pero de nuevo, esto es algo de lo que por el momento no nos vamos a preocupar.
La implementación de las peticiones asíncronas es por otro lado más sencilla de implementar en este canal. Lo que haremos es registrar un manejador de respuestas que se encargue de notificar de la llegada de respuestas.
Por último vamos con la implementación de como se lleva a cabo la recepción de respuestas, donde debemos de tener en cuenta si la recepción se reliaza de forma síncrona o asíncrona.
internal static void MessageReceived(POP3Msg pop3msg) { // Cuando un mensaje pop3 se reciba se tratará en este método // comprobar si es una petición o una respuesta if ((pop3msg.InReplyTo == null) && (pop3msg.MessageId != null)) { // es una petición String requestID = pop3msg.MessageId; // petición recibida // comprobar si el servidor está registrado SMTPServerTransportSink snk = (SMTPServerTransportSink) _servers[GetCleanAddress(pop3msg.To)]; if (snk==null) { // No se ha encontrado ningún servidor para esta petición return; } // Enviar el mensaje al serversink, el sumidero del servidor snk.HandleIncomingMessage(pop3msg); }
Hasta ahora hemos cubierto el caso de que el mensaje recibido sea una petición de invocación de un método remoto. Si es así, debemos de buscar que servidor se encarga de procesar la petición utilizando la tabla de servidores registrados, cada uno de los cuales está asociado a una dirección de un buzón de correo. Si no se encuentra el servidor, se debería de levantar una excepción para informar de ello. Aunque creo que Remoting no tiene la posibilidad de enviar excepciones de forma remota actualmente, a diferencia de CORBA, por lo que esta excepción no llegaría al cliente y serviría de poco.
else if (pop3msg.InReplyTo != null) { // las respuestas deben de tener la cabecera String responseID = pop3msg.InReplyTo.Trim(); // comprobar quien está esperando la respuesta Object notify = _waitingFor[responseID]; if (notify as Thread != null) { _responses[responseID] = pop3msg; // Una hebra a la espera. La levantamos _waitingFor.Remove(responseID ); ((Thread) notify).Resume(); POP3PollManager.ResponseReceived(); } else if (notify as AsyncResponseHandler != null) { _waitingFor.Remove(responseID); POP3PollManager.ResponseReceived(); ((AsyncResponseHandler)notify).HandleAsyncResponsePop3Msg( pop3msg); } else { // Nadie está esperando esta repuesta. La ignoramos } } }
En la parte de gestión de respuestas que acabamos de ver se tratan tanto las respuestas síncronas como las asíncronas. En el caso de que sea una respuesta para la que existe una hebra esperando, es que era una invocación síncrona por lo que levantamos a la hebra que estaba tratando esta petición para que puede recoger la respuesta.
En el caso de que lo esté a la espera de esta respuesta no fuera una hebra si no un objeto de la clase "AsyncResponseHandler", se invoca sobre él la notificación de que debe re gestionar una nueva respuesta.
En ambos casos se elimina la respuesta de entre las esperadas y se informa al POP3PollManager de que hemos recibido una respuesta.
Ya estamos en disposición de implementar el método ProcessMessage que ya aparece en las interfaces de Remoting. Este método nos sirve para procesar un mensaje que hemos recibido por POP3 y convertirlo a formato Remoting para poder ser consumido por la plataforma de Remoting, es decir, que los otros sumideros del canal puedan comenzar a tratarlo antes de entregarlo finalmente a la aplicación.
internal static void ProcessMessage(POP3Msg pop3msg, out ITransportHeaders headers, out Stream stream, out String ID) { // Este método parte el mensaje un TransportHeaders y // un objeto Stream y devolverá el "remoting ID" headers = new TransportHeaders(); // primero las cabeceras de remoting (empiezan con "X-REMOTING-") // las extraemos y las metemos en el objeto TransportHeaders String tmp = pop3msg.Headers; int pos = tmp.IndexOf("\nX-REMOTING-"); while (pos >= 0) { int pos2 = tmp.IndexOf("\n",pos+1); String oneline = tmp.Substring(pos+1,pos2-pos-1); int poscolon = oneline.IndexOf(":"); String key = oneline.Substring(11,poscolon-11).Trim(); String headervalue = oneline.Substring(poscolon+1).Trim(); if (key.ToUpper() != "URI") { headers[key] = headervalue; } else { headers["__RequestUri"] = headervalue; } pos = tmp.IndexOf("\nX-REMOTING-",pos2); } String fulltext = pop3msg.Body ; fulltext = fulltext.Trim(); byte[] buffer = Convert.FromBase64String(fulltext);
Aquí deshacemos la conversión que se hizo para poder transmitir el mensaje sobre correo electrónico. De hecho, creo que esta conversión habría que hacerla sobre un sumidero de formateo, aunque es tan sencilla se ha incluido aquí.
stream=new MemoryStream(buffer); ID = pop3msg.MessageId; }
Para leer el contenido de los mensajes vamos a utilizar flujos de datos. Aquí creamos uno sobre el cuerpo del mensaje.
A continuación mostramos unos métodos auxiliares presentes en la clase que permiten tratar las direcciones de correo, las URL y quizá el método más interesante, el que permite registrar servidores asociados a direcciones de correo.
internal static void parseURL(String url, out String email, out String objectURI) { // formato: "smtp:user@host.domain/URL/to/object" // que se divide en: // email = user@host.domain // objectURI = /URL/to/object int pos = url.IndexOf("/"); if (pos > 0) { email = url.Substring(5,pos-5); objectURI = url.Substring(pos); } else if (pos ==-1) { email = url.Substring(5); objectURI =""; } else { email = null; objectURI = url; } }
Este método nos permite recibir una URL que hace referencia a nuestro canal (smtp) y de ella obtener el buzón de correo al que debemos de enviar los mensajes, y la identificación del objeto remoto.
public static String GetCleanAddress(String address) { // cambia direcciones en el formato "someone@host" // "<someone@host>" "<someone@host> someone@host" // al formato genérico "someone@host" address = address.Trim(); int posAt = address.IndexOf("@"); int posSpaceAfter = address.IndexOf(" ",posAt); if (posSpaceAfter != -1) address = address.Substring(0,posSpaceAfter); int posSpaceBefore = address.LastIndexOf(" "); if (posSpaceBefore != -1 && posSpaceBefore < posAt) { address = address.Substring(posSpaceBefore+1); } int posLt = address.IndexOf("<"); if (posLt != -1) { address = address.Substring(posLt+1); } int posGt = address.IndexOf(">"); if (posGt != -1) { address = address.Substring(0,posGt); } return address; }
Y para terminar esta sección, el método que permite registrar servidores asociados a cuentas de correo.
public static void RegisterServer(SMTPServerTransportSink snk, String address) { // Registra un sumidero para un dirección de correo _servers[address] = snk; }
Vemos que los sumideros de canal del servidor están asociados a direcciones de correo. Estos sumideros tienen asociados también objetos que de forma periódica, comprueban la llegada de nuevos mensajes a estos buzones.
Hemos andando un largo camino hasta llegar al nivel de abstracción suficiente para poder al fin enganchar las interfaces de Remoting al canal de "smtp" que estamos creando.
Pues vamos ya a por la clase "SMTPClientChannel.cs" que es ya el cliente de Remoting. Es esta clase la que nos va a permitir hacer uso de los objetos remotos de Remoting a través del canal de smtp.
public class SMTPClientChannel: BaseChannelWithProperties, IChannelSender { IDictionary _properties; IClientChannelSinkProvider _provider; String _name; public SMTPClientChannel (IDictionary properties, IClientChannelSinkProvider clientSinkProvider) { _properties = properties; _provider = clientSinkProvider; _name = (String) _properties["name"]; POP3PollManager.RegisterPolling( (String) _properties["pop3Server"], (String) _properties["pop3User"], (String) _properties["pop3Password"], Convert.ToInt32((String)_properties["pop3PollInterval"]), false); }
Pues de momento comenzamos implementando dos interfaces, IChannelSender que es la que nos permite enviar mensajes al canal a través de la clase, y BaseChannelWithProperties, que indica que esta clase es un canal que se configura a través de propiedades.
Como todo canal que se configura con un fichero XML de propiedades, el constructor recibe dichas propiedades a través de un IDictionary. Junto a estas propiedades, recibe también una referencia a un proveedor de sumideros, el cual usará para obtener los datos a enviar por el canal.
El canal utilizará este proveedor (clientSinkProvider) para engancharse a él, de forma que cuando se escriba en este sumidero del canal, los datos terminen en el sumidero del canal de transporte y sean transmitidos al objeto remoto.
La configuración del canal se realiza con un fichero XML donde se indican las características del canal. Un ejemplo podría ser:
<configuration> <system.runtime.remoting> <application name="helloworld"> <channels> <channel name="smtp" type="SMTPChannel.SMTPChannel, SMTPChannel" senderEmail="client_1@localhost" smtpServer="localhost" pop3Server="localhost" pop3User="client_1" pop3Password="client_1" pop3PollInterval="1" isServer="yes"/> </channels> </application> </system.runtime.remoting> </configuration>
Vemos como en este fichero se especifican todas las características del canal, y todos estos datos llegan al constructor del cliente "en IDictionary properties".
A continuación vienen unos sencillos métodos de la interfaz de IChannel que deben de devolver el nombre del canal y la prioridad del mismo.
public string ChannelName { get { return _name; } } public int ChannelPriority { get { return 0; } } public string Parse(string url, out string objectURI) { String email; SMTPHelper.parseURL(url, out email, out objectURI); if (email == null || email=="" || objectURI == null || objectURI =="") { return null; } else { return "smtp:" + email; } }
La prioridad en los canales es necesaria ya que se puede acceder a un objeto por diferentes canales, y en función de esta prioridad, se utilizará uno u otro. El método "Parse" nos permite obtener a partir de una URL, la parte que es el identificador del objeto. La URL es específica del canal por lo que es el canal el que sabe como tratarla. Es por ello que hace falta este método.
Y llegamos por fin al método de la interfaz "IChannelSender" CreateMessageSink, el cual nos permite crear un sumidero a través del cual transferir mensajes a un objeto remoto. Este método es el que usará el desarrollador que utilice este canal en sus aplicaciones.
public IMessageSink CreateMessageSink(string url, object remoteChannelData, out string objectURI) { if (url == null && remoteChannelData != null && remoteChannelData as IChannelDataStore != null ) { IChannelDataStore ds = (IChannelDataStore) remoteChannelData; url = ds.ChannelUris[0]; }
La URL con la localización del objeto remoto se la podemos pasar directamente o através del objeto "remoteChannelData", que contiene los datos del canal.
// formato: "smtp:user@host.domain/URI/to/object" if (url != null && url.ToLower().StartsWith("smtp:")) { // obtenemos el último sumidero dentro del canal // A este último sumidero engancharemos el sumidero de transporte IClientChannelSinkProvider prov = _provider; while (prov.Next != null) { prov = prov.Next ;}; // Al último sumidero enganchamos el nuevo sumidero de transporte prov.Next = new SMTPClientTransportSinkProvider((String) _properties["senderEmail"], (String) _properties["smtpServer"]); String dummy; SMTPHelper.parseURL(url,out dummy,out objectURI); // Creamos el sumidero IMessageSink msgsink = (IMessageSink) _provider.CreateSink(this,url,remoteChannelData); return msgsink; }
El sumidero de canal "smtp" será el último sumidero de la cadena de sumideros dentro del cliente, por lo que recorremos todos los sumideros del canal anteriores y lo añadimos después del último.
En el caso de que no comience la URL por "smtp:" es que no es un canal de los que estamos implementando aquí, por lo que ignoramos la petición. Quizá sería bueno levantar una excepción al respecto.
else { objectURI =null; return null; } }
Ahora tenemos que analizar la clase "SMTPClientTransportSinkProvider" que es la que se encarga de crear el sumidero de mensajes de Remoting, la cual como vamos a ver utiliza la clase "SMTPClientTransportSink" que es el sumidero Remoting para el cliente.
Por fin hemos llegado al sumidero que permitará utilizar el canal de smtp, el último eslabón en la cadena de Remoting en la parte del cliente. Lo primero que necesitamos es el proveedor de sumideros de Remoting basados en smtp. Este como ya hemos visto, se utiliza dentro de CreateMessageSink para crear el sumidero de mensajes desde los cuales, el usuario de este canal podrá realizar envíos através de él. Esto lo implementamos en la clase "SMTPClientTransportSinkProvider".
public class SMTPClientTransportSinkProvider: IClientChannelSinkProvider { String _senderEmailAddress; String _smtpServer; public SMTPClientTransportSinkProvider(String senderEmailAddress, String smtpServer) { _senderEmailAddress = senderEmailAddress; _smtpServer = smtpServer; } public IClientChannelSink CreateSink(IChannelSender channel, string url, object remoteChannelData) { String destinationEmailAddress; String objectURI; SMTPHelper.parseURL(url,out destinationEmailAddress,out objectURI); return new SMTPClientTransportSink(destinationEmailAddress,_senderEmailAddress,_smtpServer, objectURI); } public IClientChannelSinkProvider Next { get { return null; } set { // ignore as this has to be the last provider in the chain } } }
Esta clase es bastante sencilla. Tan sólo se encarga de recibir los datos del servidor de smtp a utilizar y del remitente de los correos. Junto a estas acciones, implementa el método "CreateSink" que es el que permite crear un sumidero por el que enviar los mensajes al canal. Con los datos que se le pasan a este método se crea un nuevo sumidero, "SMTPClientTransportSink", por el que se podrán enviar mensajes tanto de forma síncrona como asíncrona, como veremos a continuación.
Al ser este el último proveedor de sumideros de la cadena, no tiene siguiente proveedor en la cadena.
Para finalizar, vamos con la última clase del lado del cliente, la que implementa el sumidero del canal por el que se realizarán las invocaciones, que provocarán el envío mensajes. Todo esto se realiza dentro de la clase "SMTPClientTransportSink".
public class SMTPClientTransportSink: BaseChannelSinkWithProperties, IClientChannelSink, IChannelSinkBase { String _destinationEmailAddress; String _senderEmailAddress; String _objectURI; String _smtpServer; public SMTPClientTransportSink(String destinationEmailAddress, String senderEmailAddress, String smtpServer, String objectURI) { _destinationEmailAddress = destinationEmailAddress; _senderEmailAddress = senderEmailAddress; _objectURI = objectURI; _smtpServer = smtpServer; }
De momento, vemos los datos básicos para poder utilizar un sumidero de tipo "smtp": las direcciones de correo de origen y de destino para los mensajes, el servidor de SMTP a utilizar y el identificador del objeto. Parte de estos datos los obtenemos de la configuración XML del canal, mientras que otros como el identificador del objeto remoto, nos los proporciona el usuario del canal.
public void ProcessMessage(IMessage msg, ITransportHeaders requestHeaders, Stream requestStream, out ITransportHeaders responseHeaders, out Stream responseStream) { String ID; String objectURI; String email; // comprobar la URL String URL = (String) msg.Properties["__Uri"]; SMTPHelper.parseURL(URL,out email,out objectURI); if ((email==null) || (email == "")) { email = _destinationEmailAddress; } // enviar el mensaje SMTPHelper.SendRequestMessage(_senderEmailAddress,email,_smtpServer,requestHeaders,requestStream,objectURI, out ID); // esperar la respuesta POP3Msg popmsg = SMTPHelper.WaitAndGetResponseMessage(ID); // procesar la respuesta SMTPHelper.ProcessMessage(popmsg,out responseHeaders,out responseStream,out ID); }
Este método es el que utilizan los sumideros para irse pasando el mensaje entre ellos y poder ir procesándolo, y es también el método que utiliza el desarrollador para pasar mensajes a la plataforma de Remoting. Esta invocación es de tipo síncrono por lo que, una vez realiza la invocación con "SendRequestMessage", la cual envía el mensaje por SMTP, nos quedamos esperando a la espera de la notificación de la llegada de respuesta en el método ".WaitAndGetResponseMessage". Una vez que hemos recibido la respuesta, la procesamos con el método "ProcessMessage".
Vamos ahora con la invocación de métodos asíncronos, que va a ser muy similar a la de métodos síncronos pero con la diferencia de que no nos quedaremos esperando a la respuesta si no que registraremos un manejador de respuestas para que se nos notifique de la llegada de la respuesta.
public void AsyncProcessRequest(IClientChannelSinkStack sinkStack, IMessage msg, ITransportHeaders headers, Stream stream) { String ID; String objectURI; String email; // análisis de la URL String URL = (String) msg.Properties["__Uri"]; SMTPHelper.parseURL(URL,out email,out objectURI); if ((email==null) || (email == "")) { email = _destinationEmailAddress; } // envío del mensaje de petición SMTPHelper.SendRequestMessage(_senderEmailAddress,email,_smtpServer, headers,stream,objectURI, out ID); // crear y registrar un manejador de respuestas asíncronas AsyncResponseHandler ar = new AsyncResponseHandler(sinkStack); SMTPHelper.RegisterAsyncResponseHandler(ID, ar); }
Por último, veamos el método que se encarga de procesar las respuestas recibidas de forma asíncrona. Esto no es necesario dentro de un sumidero de transporte ya que este lo que hace es propagar hacia la parte superior del canal esta respuesta. Este método debe de ser implementado por los sumideros del canal que preceden al de transporte.
public void AsyncProcessResponse(System.Runtime.Remoting.Channels.IClientResponseChannelSinkStack sinkStack, object state, System.Runtime.Remoting.Channels.ITransportHeaders headers, System.IO.Stream stream) { // no es necesario en el sumidero de transporte throw new NotSupportedException(); }
Vamos con el últimos método y la última propiedad que nos restan por implementar de la interfaz de un sumidero.
public Stream GetRequestStream(System.Runtime.Remoting.Messaging.IMessage msg, System.Runtime.Remoting.Channels.ITransportHeaders headers) { // no se puede acceder al stream de forma directa return null; } public System.Runtime.Remoting.Channels.IClientChannelSink NextChannelSink { get { // no hay más sumideros return null; } }
Con esto, hemos completado la implementación del sumidero del cliente, el cual nos va a permitir ya realizar invocaciones síncrona y asíncronas sobre el canal.
A lo largo de toda esta sección hemos ido viendo los pasos necesarios para implementar todas las interfaces que se esperan dentro de un canal de Remoting. La implementación de estas interfaces se ha hecho utilizando todo el desarrollo anterior que preparó el canal smtp para poder ser usado en remoting.
Es de destacar que podemos crear clientes de Remoting que en realidad no hablen con servidores de Remoting. Por ejemplo, en este caso, se podrían recibir los correos en los buzones y tratarlos de forma diferente dentro del servidor sin utilizar una implementación de Remoting. Este esquema aporta una gran flexibilidad a la plataforma, algo que vamos a explotar por ejemplo cuando empecemos a implementar el canal de IIOP, donde inicialmente lo que haremos será implementar un cliente que nos permita hacer invocaciones remotas sobre objetos CORBA.
Y llegamos a la última parte de la implemetación del canal de Remoting sobre smtp, la parte del servidor, el cual se encargará de recibir las invocaciones remotas, localizar al objeto sobre el que se quiere realizar la invocación, ejecutar la invocación, obtener los resultados de la misma y devolverlos al cliente.
Como con el cliente, vamos a comenzar con el análisis de "SMTPServerChannel" para finalizar con el análisis de la implementación del sumidero del lado del servidor, "SMTPServerTransportSink".
Comencemos con el análisis de la clase "SMTPServerChannel", la cual se encargará iniciar los servidores que servirán los diferentes objetos smtp registrados en este servidor.
public class SMTPServerChannel: BaseChannelWithProperties, IChannelReceiver, IChannel { private String _myAddress; private String _name; private String _smtpServer; private String _pop3Server; private String _pop3Username; private String _pop3Password; private int _pop3Pollingtime; private SMTPServerTransportSink _transportSink; private IServerChannelSinkProvider _sinkProvider; private IDictionary _properties; private ChannelDataStore _channelData;
De momento, hemos indicado que vamos a implementar las interfaces "BaseChannelWithProperties", al igual que hicimos en el cliente, y la interfaz IChannelReceiver la podemos ver a
public interface IChannelReceiver : IChannel { object ChannelData { get; } string [] GetUrlsForUri (string objectUri); void StartListening (object data); void StopListening (object data); }
En ella vemos los métodos para arrancar y para la recepción de peticiones, algo que podría ser similar al POA Manager de CORBA, el método para obtener todas las posibles URLs para acceder a un determinado objeto y, la propiedad ChannelData, un objeto que contiene información sobre el canal y que servirá para que los objetos activados por el cliente (CAO) puedan obtener de él al URL la utilizar para acceder al nuevo objeto activado.
Declaramos un conjunto de propiedades, entre las que destacan el sumidero de transporte (_transportSink), el proveedor de sumidero para el servidor que nos permitirá enganchar el sumidero de transporte con la cadena del canal (_sinkProvider) y las propiedades del canal, que se leerán de un fichero de configuración XML. Las demás variables afectan al uso del servicio y cabe destacar la variable "_pop3Pollingtime" que especifica el intervalo de consulta de nuevas peticiones.
Por último tenemos la variable "_channelDara" que contendrá datos sobre el canal como ya iremos viendo.
Vamos con el constructor de la clase, que entre otras cosas, inicializará todas estas variables.
public SMTPServerChannel(IDictionary properties, IServerChannelSinkProvider serverSinkProvider) { _sinkProvider = serverSinkProvider; _properties = properties; _myAddress = (String) _properties["senderEmail"]; _name = (String) _properties["name"]; _pop3Server = (String) _properties["pop3Server"]; _smtpServer = (String) _properties["smtpServer"]; _pop3Username = (String) _properties["pop3User"]; _pop3Password = (String) _properties["pop3Password"]; _pop3Pollingtime = Convert.ToInt32((String) _properties["pop3PollInterval"]);
Hasta el momento lo único que hemos hecho ha sido inicializar las variables de uso del canal con los valores que se han leído del fichero de configuración XML, el cual es similar al que nos encontramos en el cliente.
String[] urls = { this.GetURLBase() }; // lo necesitan los clientes que activan objetos (CAOs) _channelData = new ChannelDataStore(urls); // obtenemos la información de todos los proveedores del canal IServerChannelSinkProvider provider = _sinkProvider; while (provider != null) { provider.GetChannelData(_channelData); provider = provider.Next; } // creamos la cadena de sumideros del canal IServerChannelSink snk = ChannelServices.CreateServerChannelSinkChain(_sinkProvider,this); // añadimos el SMTPServerTransportSink como el primer elemento de la cadena _transportSink = new SMTPServerTransportSink(snk, _smtpServer, _myAddress); // comenzamos a escuchar por la llegada de peticiones this.StartListening(null); }
Sin duda la parte que requiere explicación es la de la creación de la cadena de sumideros por la que pasarán los mensajes que se reciban. En el fichero XML de configuración del canal, se especifican los diferentes sumideros que formarán la cadena. Con esta información, se registra la configuración del canal dentro del sistema de Remoting, información que podemos utilizar utilizando la clase "ChannelServices".
Podemos ver la implementación de este método para entender como se crea la cadena de sumideros dentro del servidor y como se enganchan para poderse ir entregando entre ellos los mensajes que se reciban.
public sealed class ChannelServices { ... public static IServerChannelSink CreateServerChannelSinkChain ( IServerChannelSinkProvider provider, IChannelReceiver channel) { IServerChannelSinkProvider tmp = provider; while (tmp.Next != null) tmp = tmp.Next; tmp.Next = new ServerDispatchSinkProvider (); return provider.CreateSink (channel); } ... }
Lo que se hace aquí es añadir al final de la cadena de sumideros del servidor, el "dispatcher" que se encargará de repartir los métodos que se reciban sobre los objetos adecuados. Tras ello se crea le primer sumidero de canal, el que especifica el fichero de configuración XML.
Delante de este primer sumidero incorporamos un sumidero de transporte, que será al que se entreguen los mensajes recien recogidos de los buzones, y que irán atravesando toda la cadena de sumideros dentro del servidor, hasta llegar al repartidor (dispatcher) que invocará la petición sobre el objeto adecuado.
Dentro del constructor de "SMTPServerChannel", la última acción es el arranque de la recepción de mensajes que se produce con la llamada al método "StartListening" que pasamos a analizar.
public void StartListening(object data) { // register the POP3 account for polling POP3PollManager.RegisterPolling(_pop3Server,_pop3Username, _pop3Password,_pop3Pollingtime,true); // register the email address as a server SMTPHelper.RegisterServer(_transportSink,_myAddress); }
Nos basamos en la clase POP3PollManager para registrar un nuevo recogedor (polling) de mensajes y tras ello, registramos el servidor junto con la dirección de correo a la que esta asociada.
Los demás métodos de esta clase son bastante sencillos ya que son la implementación de la interfaz IChannel que ya hemos visto en otras ocasiones junto con el método GetUrlsForUri.
private String GetURLBase() { return "smtp:" + _myAddress; } public string Parse(string url, out string objectURI) { String email; SMTPHelper.parseURL(url, out email, out objectURI); if (email == null || email=="" || objectURI == null || objectURI =="") { return null; } else { return "smtp:" + email; } } public string ChannelName { get { return _name; } } public int ChannelPriority { get { return 0; } } public string[] GetUrlsForUri(string objectURI) { String[] urls; urls = new String[1]; if (!(objectURI.StartsWith("/"))) objectURI = "/" + objectURI; urls[0] = this.GetURLBase() + objectURI; return urls; } public object ChannelData { get { return _channelData; } }
Por último resaltar que en esta implementación que hacemos del canal, no vamos a realizar ninguna acción en el método "StopListening", es decir, que una vez que nos ponemos a seguir un buzón de correo para recibir mensajes, ya no dejamos de hacerlo.
public void StopListening(object data) { // No implementado }
Vamos con la última clase de la implementación del canal smtp, el sumidero del canal. Este se crea en la llamada de la clase SMTPServerChannel:
// añadimos el SMTPServerTransportSink como el primer elemento de la cadena _transportSink = new SMTPServerTransportSink(snk, _smtpServer, _myAddress);
Es una clase donde vamos a implementar la interfaz IServerChannelSink y donde se van a recibir las invocaciones remotas para ser tratadas. Vamos con ella.
public class SMTPServerTransportSink: IServerChannelSink { // lo usaremos para guardar el estado de las invocaciones asíncronas private class SMTPState { internal String ID; internal String responseAddress; } private String _smtpServer; private String _myAddress; private IServerChannelSink _nextSink; public SMTPServerTransportSink(IServerChannelSink nextSink, String smtpServer, String myAddress) { _nextSink = nextSink; _smtpServer =smtpServer; _myAddress = myAddress; }
Hasta el momento hemos inicializado las variables de la clase con los valores que se nos dan en el constructor. Quizá destacar que cada sumidero está asociado a una dirección de correo, como ya pronosticamos al comienzo de este tutorial.
La interfaz IServerChannelSink a implementar es la siguiente.
public interface IServerChannelSink : IChannelSinkBase { IServerChannelSink NextChannelSink { get; } void AsyncProcessResponse (IServerResponseChannelSinkStack sinkStack, object state, IMessage msg, ITransportHeaders headers, Stream stream); Stream GetResponseStream (IServerResponseChannelSinkStack sinkStack, object state, IMessage msg, ITransportHeaders headers); ServerProcessing ProcessMessage (IServerChannelSinkStack sinkStack, IMessage requestMsg, ITransportHeaders requestHeaders, Stream requestStream, out IMessage responseMsg, out ITransportHeaders responseHeaders, out Stream responseStream); }
A la vista de esta interfaz, no parece claro como se reciben los mensajes dentro de este sumidero. Y es que esto es un detalle de la implementación que en nuestro caso, hemos resuelto dentro de SMTPHelper, en el método MessageReceived, que recordemos que se invocaba cuando una instancia de POP3Polling obtenía un nuevo mensaje de un buzón. El método MessageReceived lo que hacía era comprobar que tipo de mensaje era y en el caso de ser una invocación, no una respuesta, se ejecutaba el código:
// buscamos un servidor registrado al que entregar el mensaje SMTPServerTransportSink snk = (SMTPServerTransportSink) _servers[GetCleanAddress(pop3msg.To)]; if (snk==null) { // No server side sink found for address return; } // Entregamos el mensaje al sumidero del servidor snk.HandleIncomingMessage(pop3msg);
Luego los mensajes los vamos a recibir en el sumidero de transporte mediante una invocación al método HandleIncomingMessage que pasamos a analizar.
public void HandleIncomingMessage(POP3Msg popmsg) { Stream requestStream; ITransportHeaders requestHeaders; String ID; // dividimos el mensaje entre un flujo hacia el cuerpo del mismo y unas cabeceras ITransportHeaders SMTPHelper.ProcessMessage(popmsg,out requestHeaders, out requestStream, out ID);
Para poder pasar el mensaje a otros sumideros de Remoting, necesitamos dividirlo en dos partes, por un lado las cabeceras del mensaje, y por otro lado el cuerpo del mismo. Para que el siguiente sumidero pueda obtener el cuerpo del mensaje, le damos un flujo de datos a este cuerpo del que lo puede leer.
// creamos una pila de sumideros en el servidor para entregar el mensaje ServerChannelSinkStack stack = new ServerChannelSinkStack();
Hemos creado la pila del canal dentro del servidor. En esta pila estarán enganchados todos los sumideros por los que debe de ir pasando el canal. Actualmente (Sep 2002) en Mono aún no está implementada esta clase, pero se va a realizar dentro de muy poco.
// creamos un nuevo objeto de estado y lo completamos con datos SMTPState state = new SMTPState(); state.ID = ID; state.responseAddress = SMTPHelper.GetCleanAddress(popmsg.From ); // añadimos este sumidero a la pila stack.Push(this,state);
Ponemos como sumidero inicial de la pila del canal, al sumidero de transporte.
IMessage responseMsg; Stream responseStream; ITransportHeaders responseHeaders; // pasamos la llamada al siguiente sumidero ServerProcessing proc = _nextSink.ProcessMessage(stack,null,requestHeaders, requestStream,out responseMsg, out responseHeaders, out responseStream); // comprobamos el valor de retorno switch (proc) { // este mensaje se ha tratado de forma síncrona case ServerProcessing.Complete: // enviamos el mensaje de respuesta SMTPHelper.SendResponseMessage(_myAddress, state.responseAddress,_smtpServer,responseHeaders, responseStream,state.ID); break; // este mensaje se ha tratado de forma asíncrona case ServerProcessing.Async: // no necesitamos hacer nada de momento break; // ha sido un mensaja oneway case ServerProcessing.OneWay: // no necesitamos hacer nada por ahora break; } }
Vemos pues como se recibe y se procesa un mensaje dentro del sumidero de transporte, el cual lo pasa al siguiente sumidero de la pila de sumideros del canal. Lo que aún no hemos detallado aquí ha sido es como se envían las repuestas asíncronas. Vamos con ello.
public void AsyncProcessResponse( IServerResponseChannelSinkStack sinkStack, object state, IMessage msg, ITransportHeaders headers, System.IO.Stream stream) { // accedemos al objeto con el estado SMTPState smtpstate = (SMTPState) state; // envíamos el mensaje (correo) de respuesta SMTPHelper.SendResponseMessage(_myAddress, smtpstate.responseAddress,_smtpServer,headers, stream,smtpstate.ID); }
Este método será invocado por el anterior sumidero una vez que la respuesta se haya comenzado a transmitir por la pila del canal del servidor.
Por último, vamos con la implementación de los métodos más sencillos de esta clase.
public IServerChannelSink NextChannelSink { get { return _nextSink; } }
Esta propiedad apunta al próximo sumidero de la pila del canal del servidor, sumidero al que se entregarán los mensajes que se reciban, y del que se recibirán las respuestas a las invocaciones.
public System.Collections.IDictionary Properties { get { // no se necesitan return null; } } public ServerProcessing ProcessMessage( IServerChannelSinkStack sinkStack, IMessage requestMsg, ITransportHeaders requestHeaders, Stream requestStream, out IMessage responseMsg, out ITransportHeaders responseHeaders, out Stream responseStream) { // nunca se llamará en un sumidero de transporte del lado del servidor throw new NotSupportedException(); } public Stream GetResponseStream( IServerResponseChannelSinkStack sinkStack, object state, IMessage msg, ITransportHeaders headers) { // no es posible acceder de forma directa al flujo de datos // ya que aún no está en formato Remoting: cabeceras y cuerpo return null; }
Al fin hemos terminado con la implementación del canal smtp, tanto del lado del cliente como del servidor. Ha llegado el momento de concluir este tutorial.
A lo largo de todo este tutorial se ha mostrado como implementar un canal de Remoting, posiblemente uno de los aspectos más avanzados y laborioros de la arquitectura de Remoting.
El implementar un canal nos ha llevado a conocer en profundidad todos los detalles en los que se basa el funcionamiento de Remoting, así como ver posibilidades tales como tener clientes que no hablen con servidores Remoting, algo básico para proyectos como MonORB o IM#.
El canal sobre el que hemos implementado Remoting no era precisamente el más adecuado y sencillo de utilizar, lo que nos ha permitido enfrentarnos a problemas como el proporcionar llamadas síncronas sobre un canal asíncrono.
A la vista de esta implementación, podemos pronosticar que Remoting se podrá implementar sobre todo tipo de canales de comunicación, ya que hemos sido capaces de implementarlo sobre un canal como smtp, que ha ofrecido muy pocas facilidades.
Los próximos objetivos que se llevarán a cabo basados en la experiencia del desarrollo de este tutorial serán el ayudar en la implementación de Remoting en Mono, el desarrollo de un canal IIOP que permita a clientes Remoting dialogar con objetos CORBA y el desarrollo de canales en IM# que permitan hablar con los diferentes protocolos de mensajería instantánea.
Para terminar de analizar el posible uso que se podría dar a Remoting dentro del proyecto IM#, vamos a analizar una implementación que existe de un canal Remoting que utiliza como protocolo de transporte al protocolo Jabber.
Jabber es una arquitectura de intercambio de mensajes instantáneos. El sistema se basa en tener un servidor central que es capaz de hablar varios protocolos de mensajería instantánea. Los mensajes se transportan por el protocolo Jabber hasta el servidor, el cual luego los entrega a un cliente utilizando el protocolo adecuado. Las respuestas al servidor por cada uno de los canales conectados a diferentes sistemas de mensajería, son retornados al cliente mediante el protocolo jabber.
El objetivo de IM# es utilizar la misma idea del servidor de Jabber, pero incorporando en los clientes el motor de mensajería instantánea. Y una de las ideas que se tiene es la de utilizar implementaciones de canales de transporte de Remoting para cada uno de los protocolos soportados.
Por ello es de especial importancia el analizar el canal de Jabber que ya existe para Remoting, ver lo complejo de su implementación y luego preveer la complejidad de ir implementando otros protocolos.
Los fuentes de la implementación del canal de transporte de Remoting para Jabber son dos ficheros fuentes.
acs@merry:~/devel/web-xml/mono.es.gnome.org/tutoriales/remoting-canal/code/Channel$ ls -l total 44 -rw-rw-rw- 1 acs acs 19015 abr 13 20:54 channel.cs -rw-rw-rw- 1 acs acs 17789 abr 13 20:54 jabber.cs -rw-rw-rw- 1 acs acs 186 abr 13 20:54 makefile
Vamos a comenzar analizando el fichero "channel.cs" el cual utilizará luego los detalles específicos de "jabber.cs".
using System; using System.Diagnostics; using System.IO; using System.Text; using System.Collections; using System.Reflection; using System.Runtime.Remoting; using System.Runtime.Remoting.Channels; using System.Runtime.Remoting.Messaging; using System.Runtime.Serialization.Formatters.Soap ; using System.Threading; using SimonFell.Jabber ; namespace Razorsoft.Runtime.Remoting.Channels.Jabber { public delegate void delegateAsyncWorker(IMessage msgReq, IMessageSink replySink);
De momento aún no sabemos para que vamos a utilizar este delegado, aunque por su nombre parece claro que va a ser para el trabajo en las invocaciones asíncronas.
public class JabberChannel : IChannelSender, IChannelReceiver {
A diferencia de lo que hemos visto en ejemplos anteriores, aquí se crea una gran clase JabberChannel, que implementa tanto el envío como la recepción de mensajes. Esto es algo que se suele hacer así, para que el usuario del canal tenga una imagen de alto nivel y unificada del mismo.
string channelName = "jabber"; string server = "jabber.org"; string serverName = "jabber.org"; int port = 5222; string resource = "remotingChannel"; string username; string password;
Estos son datos específicos del protocolo Jabber, como el nombre del servidor Jabber al que nos conectaremos, el puerto y otras variables, como la autenticación.
// datos del canal IClientChannelSinkProvider clientSinkProvider; // cadena del cliente de proveedores de sumideros IServerChannelSinkProvider serverSinkProvider; // cadena del servidor de proveedores de sumideros ServerTransportSink serverTransSink; ChannelDataStore chnDataStore; int priority = 1; JabberClient jabberClient; // la conexión jabber de bajo nivel RequestResponseHelper jabberReqRes ; // clase de ayuda que gestiona las peticiones/respuestas MEP
Vamos con el constructor del canal que, como ya vimos en el ejemplo de smtp, cumple como labor principal la de leer del fichero de configuración los diferentes parámetros de uso del canal.
// Inicializa el envío con los valores de configuración public JabberChannel( IDictionary properties, IClientChannelSinkProvider clientProviderChain, IServerChannelSinkProvider serverProviderChain) { if (properties.Contains("server")) { server = properties["server"].ToString(); serverName = server ; } if (properties.Contains("serverName")) serverName = properties["serverName"].ToString(); if (properties.Contains("port")) port = Convert.ToInt32(properties["port"]); if (properties.Contains("username")) username = properties["username"].ToString(); if (properties.Contains("password")) password = properties["password"].ToString(); if (properties.Contains("resource")) resource = properties["resource"].ToString(); if (properties.Contains("priority")) priority = Convert.ToInt32(properties["priority"]); InitClientProviders(clientProviderChain); InitServerProviders(serverProviderChain); }
Vemos que, las dos últimas llamadas del constructor del canal de Jabber, hacen referencia a la inicialización de la cadena de sumideros tanto en el servidor como en el cliente. Vamos a analizar como se lleva a cabo dicha inicialización de las cadenas de sumideros. Y nos vamos a centrar en primera instancia en la del cliente.
private void InitClientProviders(IClientChannelSinkProvider clientProviderChain) { clientSinkProvider = clientProviderChain; if (clientSinkProvider == null) clientSinkProvider = new SoapClientFormatterSinkProvider();
En el caso de que no iniciemos la cadena del cliente con algún sumidero, de forma automática se comienza con el sumidero que se encargará de formatear (serializar) los datos utilizando el formato de Soap (XML).
IClientChannelSinkProvider tempSinkProvider = clientSinkProvider; // Nos movemos al final de la lista de proveedores while (tempSinkProvider.Next != null) tempSinkProvider = tempSinkProvider.Next; // Añadimos el sumidero de transporte al final de la cadena tempSinkProvider.Next = new ClientTransportSinkProvider(); }
Como siempre, nos vamos al final de la cadena de proveedores de sumideros, y añadimos el último proveedor, que siempre debe de ser el del sumidero de transporte. Vamos a analizar en que consiste este proveedor de canal de transporte.
internal class ClientTransportSinkProvider : IClientChannelSinkProvider { public IClientChannelSink CreateSink(IChannelSender channel, String url, Object data) { return new ClientTransportSink(channel, url, data); } public IClientChannelSinkProvider Next { get { return null; } set { throw new NotSupportedException(); } } }
Como vemos, la implementación de esta clase siempre es muy similar. Implementa el método "CreateSink", que nos permitirá obtener un sumidero al canal de transporte, por el que podremos enviar los mensajes. Como todo proveedor de sumideros de transporte, no tiene después de él ningún otro proveedor.
Hemos llegado ya a la implementación del sumidero de transporte del cliente, el cual deberá de recoger los mensajes y entregarlos por Jabber en el otro extremo.
internal class AsyncState { internal string corID ; internal IClientChannelSinkStack stack ; }
Esta clase nos sirve para seguir el estado de la peticiones asíncronas.
public class ClientTransportSink : IClientChannelSink {
Como todo sumidero de trsnaporte, se debe de implementar la interfaz IClientChannelSink, que es la que permite el envío de mensajes síncronos y asíncronos.
JabberClient jabberClient ; // nuestra conexión jabber de bajo nivel RequestResponseHelper jabberReqRes ; WaitCallback callback; string dest ; internal ClientTransportSink(IChannelSender channel, String url, object Data) { int p = url.IndexOf("//") ; dest = url.Substring(p+2) ; callback = new WaitCallback(this.ReceiveCallback); jabberClient = ((JabberChannel)channel).Connection ; jabberReqRes = ((JabberChannel)channel).ReqRes ; }
Vemos como en el canal de Jabber, pasamos ya muchos de los datos en el propio canal y, creamos un objeto que se encargará de recibir las respuestas asíncronas, "WaitCallback".
// No hay propiedades public IDictionary Properties { get { return(null); } }
Llega el momento de la chicha de verdad, la implementación de los métodos que permiten enviar mensajes síncronos y asíncronos, asi como la recepción de respuestas asíncronas.
public void ProcessMessage(IMessage msg, ITransportHeaders reqHead, Stream reqStm, out ITransportHeaders respHead, out Stream respStm) { // comprobamos la URL string thisDest = dest; string URL = (string) msg.Properties["__Uri"]; if ( URL.Length > 0 && ( ! URL.StartsWith("jabber://"))) thisDest += URL ;
Comprobamos que efectivamente, la URL a la que queremos acceder apunta a una recurso que se accede por el canal de Jabber.
string sa = reqHead["SOAPAction"].ToString() ;
La acción a llevar a cabo nos viene codificada dentro de las cabeceras para el canal de transporte (ITransportHeaders), en el campo "SOAPAction".
string corId = Guid.NewGuid().ToString() ; jabberReqRes.RegisterExpectedResponseId(corId) ; jabberClient.SendSoap(sa, reqStm, thisDest, corId, false ) ; string res = jabberReqRes.GetResponse(corId) ;
Y aquí llega la chicha de verdad de la implementación del canal. Utilizamos el cliente Jabber, que aún no hemos visto, para enviar la petición Soap y además, envíamos el identificador corId, que nos servirá para diferenciar entre diferentes invocacines. Justo antes, indicamos que vamos a estar a la espera de la respuesta al mensaje de identificador "corId" y utilizamos la clase "RequestResponseHelper" para quedarnos a la espera de recibir la respuesta. No olvidemos que estamos en la invocación síncrona, y cuando salgamos de este método, debemos de hacerlo una vez que hayamos recibido la respuesta.
respStm = new MemoryStream(Encoding.UTF8.GetBytes(res)) ; respHead = new TransportHeaders(); respHead["Content-Type"] = "text/xml" ; }
El final de este método ya si que es idéntico al canal de smtp, devolviendo un stream a la respuesta y las cabeceras.
Vamos ahora con la invocación asíncrona que como sabemos, no se quedará a la espera de la respuesta y volverá de forma inmediata.
public void AsyncProcessRequest(IClientChannelSinkStack stack, IMessage msg, ITransportHeaders headers, Stream stream) { IMethodCallMessage mcm = (IMethodCallMessage)msg; MethodBase methodBase = mcm.MethodBase; bool oneway = RemotingServices.IsOneWay(methodBase);
En este canal vamos incluso a implementar los métodos oneway que son aquellos métodos que se invocan sin esperar ningún tipo de respuesta.
string thisDest = dest ; String URL = (String) msg.Properties["__Uri"]; if ( URL.Length > 0 && ( ! URL.StartsWith("jabber://"))) thisDest += URL ; string sa = headers["SOAPAction"].ToString() ; string corId = Guid.NewGuid().ToString() ;
Esto es idéntico al caso de invocaciones síncronas, obtenemos el método SOAP y generamos un identificador único del mensaje.
if ( ! oneway ) jabberReqRes.RegisterExpectedResponseId(corId) ;
En el caso de que la petición no sea "oneway" indicamos que estamos interesados en la respuesta y registramos el "corId" para cuando se reciba la respuesta, saber a quien le tiene que notificar.
jabberClient.SendSoap ( sa, stream, thisDest, corId, false );
Enviamos la petición y en el caso de que no sea oneway, indicamos que ante la llegada de la respuesta se debe de llamar a "callback" con el parámetro "s", es decir, que se invocará el método "ReceiveCallback".
if ( ! oneway ) { AsyncState s = new AsyncState() ; s.corID = corId ; s.stack = stack ; ThreadPool.QueueUserWorkItem(callback, s); } }
El método ReceiveCallback tiene la siguiente implementación.
private void ReceiveCallback(Object state) { AsyncState s = (AsyncState)state ; string res = jabberReqRes.GetResponse(s.corID) ; TransportHeaders respHead = new TransportHeaders(); respHead["Content-Type"] = "text/xml" ; MemoryStream stm = new MemoryStream(Encoding.UTF8.GetBytes(res)) ; s.stack.AsyncProcessResponse(respHead , stm); }
que lo que hace es provocar el evento de recepción de la respuesta asíncrona dentro del canal del cliente.
Vamos con la implementación de los últimos métodos que nos quedan de la interfaz IChannelSender.
public void AsyncProcessResponse(IClientResponseChannelSinkStack stack, object obj, ITransportHeaders headers, Stream stream) { // el sumidero de transporte no recibe respuestas de otros sumideros throw new NotSupportedException(); } public Stream GetRequestStream(IMessage msg, ITransportHeaders headers) { // no se puede acceder en el sumidero del transporte al mensaje return(null); } IClientChannelSink IClientChannelSink.NextChannelSink { // No hay ningun sumidero detrás del de transporte get { return(null); } }
Mostramos a continuación los métodos habituales a implementar dentro de cualquier canal (IChannel):
public string ChannelName { get { return channelName; } } public int ChannelPriority { get { return priority; } } public string Parse(string url, out string objectURI) { if (url.StartsWith("jabber://")) { int pSlash = url.IndexOf("/", 10 ) ; if ( pSlash > 0 ) { objectURI = url.Substring(pSlash); string bUrl = url.Substring(0,pSlash) ; return bUrl ; } } objectURI = null; return null; }
La explicación de estos métodos es idéntica a la que se hizo para el canal smtp.
A la hora de enviar mensajes y de recibir respuestas en el apartado anterior, nos hemos apoyado en la existencia de las clases "JabberClient" para invocar el método "SendSoap" y la clase RequestResponseHelper, para quedarnos a la espera de la recepción de respuestas a nuestra peticiones. Vamos ahora a desvelar la implementación de estas clases.
Para comenzar, se presentan algunas interfaces y métodos delegados:
// Se lanza al recibir un mensaje SOAP de entrada public delegate void soapMessage ( string msgId, string from, string to, string soapAction, string soapMessage ) ; // interfaz que debemos de implementar para manejar peticiones de primer nivel public interface JabHandler { bool HandleNode ( JabberClient c, XmlReader r ) ; } // interfaz a implementar para manejar subpeticiones IQ public interface SubIqHandler { void HandleIqNode ( JabberClient c, iqHandler iq, XmlReader r ) ; }
Vamos con la clase principal, JabberClient, que es la que permita que luego nos abstraigamos de las comunicaciones con el servidor de Jabber.
{ public event soapMessage OnSoapMessage ; XmlReader xmlrdr ; TcpClient connection ; bool connected = false ; DateTime lastSend = DateTime.Now; System.Timers.Timer lastSend_tmr = new System.Timers.Timer();
Vamos ya preparando variables para la recepción de eventos, la lectura de XML, el establecimiento de conexiones Tcp y algunos temporizadores. Como veremos, esta clase va a ser de lo más interesante desde el punto de vista de las comunicaciones.
public JabberClient() { lastSend_tmr.Elapsed += new System.Timers.ElapsedEventHandler(CheckConnection) ; lastSend_tmr.Interval = 30000 ; } private void CheckConnection(object source, System.Timers.ElapsedEventArgs e) { if ( connected && lastSend.AddSeconds(30) < DateTime.Now ) Send ("\r\n") ; }
Ajustamos en el constructor un temporizador para que cada 30 segundos, comprueba que la conexión está activa, envando por el canal un retorno de carro. Parece que esto es algo típico de Jabber.
public bool Connected { get { return connected ; } } /// Conexión a un servidor jabber en server:port con este usuario y clave public void Connect(string server, int port, string username, string password, string resource) { Connect(server, server, port, username, password, resource) ; } public void Connect(string server, string serverName, int port, string username, string password, string resource) { // abrimos la conexión Tcp connected = false ; connection = new TcpClient() ; connection.Connect(server, port) ; // abrimos el flujo XML, y enviamos el mensaje de login string openStream = String.Format("<stream:stream to='{0}' xmlns='jabber:client' xmlns:stream='http://etherx.jabber.org/streams'>", serverName ) ; Send ( openStream ); Login ( username, password, resource ) ; // Arrancamos la hebra que leerá el flujo de entrada xmlrdr = new XmlTextReader(connection.GetStream()) ; Thread t = new Thread(new ThreadStart(ReaderThread)) ; t.IsBackground = true ; t.Start() ; // activamos el temporizador que monitoriza la conexión lastSend_tmr.Enabled = true ; }
Pues acabamos de ver uno de los métodos principales del cliente de Jabber, el que abre la conexión con el servidor de Jabber. Para cerrar la conexión:
public void Disconnect() { // Paramos el temporizador lastSend_tmr.Enabled = false ; if ( connected ) { Send ( "</stream:stream>" ); connection.GetStream().Flush() ; } }
Hemos utilizado los métodos "Send" y "Login". Veamos su implementación:
void Login ( string username, string password, string resource ) { //todo: cambiar al modo de autenticación digest string id = Guid.NewGuid().ToString() ; string auth = String.Format ("<iq id='{0}' type='set'><query xmlns='jabber:iq:auth'><username>{1}</username><resource>{2}</resource><password>{3}</password></query></iq>", id, username, resource, password ) ; Send ( auth ) ; Pressense() ; } private void Pressense () { const string pres = "<presence><status>ready</status><show>ready</show></presence>" ; Send ( pres ) ; } // Envíos de cadenas public void Send ( string msg ) { byte [] b = Encoding.UTF8.GetBytes(msg) ; lock(this) { connection.GetStream().Write ( b, 0, b.Length ) ; lastSend = DateTime.Now ; } } // Envíos utilizando un flujo de datos public void Send ( Stream s ) { Stream dest = connection.GetStream() ; byte [] buff = new byte[4096] ; int cb = 0 ; lock(this) { do { cb = s.Read ( buff, 0, buff.Length ) ; dest.Write ( buff, 0, cb ) ; } while ( cb > 0 ) ; lastSend = DateTime.Now ; } }
Con estos métodos ya vemos como nos autenticamos con el servidor y como se envían mensajes, tanto con cadenas como con flujos de datos. Vemos que en general el formato de los mensajes es XML.
Veamos ahora como se envía un mensaje Soap a una dirección determinada:
public void SendSoap(string SoapAction, Stream soap, string to, string id, bool isResponse) { string iqType = isResponse ? "result" : "set" ; string hdr = String.Format("<iq type='{0}' to='{1}' id='{2}'><js:soap soapaction='{3}' xmlns='' xmlns:js='jabber:iq:soap'>", iqType, to, id, SoapAction ); const string trailer = "</js:soap></iq>\r\n" ; lock(this) { Send ( hdr ) ; Send ( soap ); Send ( trailer ) ; } }
Vamos a cerrar esta clase con la hebra que se encarga de ir recibiendo los comandos del servidor de Jabber y con el método que es invocado cuando se reciben mensajes para ser procesados.
// El manajeador de soap IQ llama a esta función cuando recibe un mensaje internal void SoapMessage(string id, string from, string to, string soapAction, string msg) { // disparar el delegado if ( OnSoapMessage != null ) { foreach( soapMessage sm in OnSoapMessage.GetInvocationList() ) sm.BeginInvoke(id, from, to, soapAction, msg, null, null); } }
private void ReaderThread() { connected = true ; JabHandler h = null ; try { while ( xmlrdr.Read() ) { switch (xmlrdr.NodeType) { case XmlNodeType.Element: if ( xmlrdr.Name == "iq" ) { h = new iqHandler() ; h.HandleNode(this, xmlrdr) ; } if ( xmlrdr.Name == "error" && xmlrdr.NamespaceURI == "http://etherx.jabber.org/streams" ) { h = new errHandler(); h.HandleNode(this, xmlrdr) ; } break ; } } } catch(Exception e) { StringBuilder err = new StringBuilder() ; err.AppendFormat("Exception in ReaderThread : {0} {1}", e.Message, e.StackTrace ) ; Debug.WriteLine(err) ; } finally { connected = false ; Debug.WriteLine("ReaderThread Stopping!\n"); } }
Ante la llegada de nuevos datos al flujo de lectura de la conexión con el servidor de Jabber, se analiza lo que se ha leído, que será un mensaje en XML. Si es un mensaje "iq" se trata con un iqHandler y en caso contrario, se genera un mensaje de error.
Si analizamos la clase iqHandler:
public class iqHandler : JabHandler { internal string id, from , to , type ; bool JabHandler.HandleNode ( JabberClient c, XmlReader r ) { int depth = r.Depth ; id = r.GetAttribute("id") ; from = r.GetAttribute("from") ; to = r.GetAttribute("to") ; type = r.GetAttribute("type") ; bool done = r.IsEmptyElement; while ( ! done ) { SubIqHandler h = null ; done = (!r.Read() ) ; if ( (! done) && (type != "error" ) ) { if ( r.NodeType == XmlNodeType.Element ) { if ( r.LocalName == "soap" && r.NamespaceURI == "jabber:iq:soap" ) { h = new soapHandler() ; h.HandleIqNode(c, this, r) ; } else if ( r.LocalName == "query" && r.NamespaceURI == "jabber:iq:version" ) { h = new versionHandler() ; h.HandleIqNode(c, this, r ) ; } } } if ( r.Depth == depth ) done =true ; } return true ; } }
Según el tipo de mensaje IQ recibido, lo manejaremos con un soapHandler o con un versionHandler.
class soapHandler : SubIqHandler { void SubIqHandler.HandleIqNode ( JabberClient c, iqHandler iq, XmlReader r ) { string sa = r["soapaction"] ; do { r.Read() ; } while ( r.NodeType != XmlNodeType.Element ); c.SoapMessage ( iq.id, iq.from, iq.to, sa, r.ReadOuterXml() ) ; } }
Es para este tipo de mensajes para los que utilizamos el método SoapMessage que como vamos a ver a continuación es parte de la clase de gestión de respuestas. En el caso de que el mensaje sea un mensaje de versión, lo tratamos con un versionHandler.
class versionHandler : SubIqHandler { void SubIqHandler.HandleIqNode ( JabberClient c, iqHandler iq, XmlReader r ) { StringBuilder res = new StringBuilder() ; res.AppendFormat ( "<iq type='result' from='{0}' to='{1}' id='{2}'><query xmlns='jabber:iq:version'>", iq.to, iq.from, iq.id ) ; res.Append ( "<name>JibberJabber.NET</name>" ) ; res.Append ( "<version>0.2</version>" ) ; res.AppendFormat ( "<os>GNU/Linux {0}</os>", Environment.OSVersion.Version.ToString() ) ; res.Append ( "</query></iq>" ) ; c.Send ( res.ToString() ) ; } }
Vamos por último con la clase RequestResponseHelper
// Esta clase mantiene una lista con los ID de las respuestas esperadas y // permite que esperemos por una public class RequestResponseHelper { // este evento se lanza cuando recibimos un mensaje soap que no estábamos esperando public event soapMessage OnSoapMessage ; SortedList msgs = new SortedList() ; SortedList waits = new SortedList() ; SortedList responseIds = new SortedList() ; public RequestResponseHelper(JabberClient c) { c.OnSoapMessage += new soapMessage(this.SoapMessage) ; } public void RegisterExpectedResponseId(string corId) { lock(this) { responseIds[corId] = 1 ; } }
Toda esta clase nos recuerda mucho a la implementación del canal de smtp. En este, también manteníamos una lista con todas las respuestas que estábamos esperando.
// Encuentra un mensaje SOAP del servidor, dado un id de mensaje // Si aún no se ha recibido, nos bloqueamos esperándolo public string GetResponse(string id) { bool done = false ; do { AutoResetEvent e = null ; lock (this) { if ( msgs.ContainsKey(id) ) { string res = (string)msgs[id] ; msgs.Remove(id) ; responseIds.Remove(id) ; waits.Remove(id) ; return res ; } e = new AutoResetEvent(false) ; waits[id] = e ; } done = ! e.WaitOne(15000, true) ; } while ( ! done ) ; throw new Exception("Timeout esperando la respuesta" ); }
En este método es en el que nos apoyamos para poder realizar invocaciones síncronas en un canal que de nuevo, es de naturaleza asíncrona, como le ocurría al canal de smtp.
internal void SoapMessage(string id, string from, string to, string soapAction, string msg) { // lanzamos el delegado if ( OnSoapMessage != null ) { bool fireDelg = false ; lock(this) { if ( ! responseIds.Contains(id) ) { responseIds.Remove(id) ; fireDelg = true ; } } if ( fireDelg ) { foreach( soapMessage sm in OnSoapMessage.GetInvocationList() ) sm.BeginInvoke(id, from, to, soapAction, msg, null, null); return ; } } lock (this) { msgs[id] = msg ; if ( waits.ContainsKey(id) ) { AutoResetEvent e = (AutoResetEvent)waits[id]; waits.Remove(id) ; e.Set() ; } } } }
Este método es el que va recibiendo los mensajes que se reciben y los va añadiendo a la tabla donde se almacenan a la espera de ser consumidos.
Hasta el momento hemos implementado toda la parte del canal referente a la interacción del cliente con el servidor Jabber.
private void InitServerProviders(IServerChannelSinkProvider serverProviderChain) { chnDataStore = new ChannelDataStore(null); chnDataStore.ChannelUris = new string[1]; chnDataStore.ChannelUris[0] = baseUrl ; serverSinkProvider = serverProviderChain; // Create the default sink chain if one was not passed in if (serverSinkProvider == null) serverSinkProvider = new SoapServerFormatterSinkProvider(); // Collect the rest of the channel data: IServerChannelSinkProvider provider = serverSinkProvider; while (provider != null) { provider.GetChannelData(chnDataStore); provider = provider.Next; } IServerChannelSink next = ChannelServices.CreateServerChannelSinkChain(serverSinkProvider, this); serverTransSink = new ServerTransportSink(next) ; jabberClient = new JabberClient() ; jabberReqRes = new RequestResponseHelper(jabberClient) ; jabberReqRes.OnSoapMessage += new soapMessage(this.recvSoap) ; StartListening(null); }
FIXME: Falta por explicar
private void recvSoap(string id, string from, string to, string soapAction, string msg) { Debug.WriteLine("Got new msg with id " + id ) ; ServerChannelSinkStack stack = new ServerChannelSinkStack(); stack.Push(serverTransSink, null); IMessage responseMsg; ITransportHeaders responseHeaders; Stream responseStream; ITransportHeaders headers = new TransportHeaders() ; headers["Content-Type"] = "text/xml; charset=UTF-8" ; headers["SOAPAction"] = soapAction ; to = "jabber://" + to ; string uri = to.Substring(baseUrl.Length) ; if ( uri.Length == 0 ) uri = "/" ; headers["__RequestUri"] = uri ; Debug.WriteLine("baseUrl = " + baseUrl ) ; Debug.WriteLine("to = " + to ) ; Debug.WriteLine("SOAPAction = " + soapAction + "\r\nUri= " + uri ) ; MemoryStream request = new MemoryStream(Encoding.UTF8.GetBytes(msg)) ; ServerProcessing processing = serverTransSink.NextChannelSink.ProcessMessage(stack, null, headers, request, out responseMsg, out responseHeaders, out responseStream); // handle response switch (processing) { case ServerProcessing.Complete: // Send the response. Call completed synchronously. stack.Pop(serverTransSink); jabberClient.SendSoap("", responseStream, from, id, true) ; break; case ServerProcessing.OneWay: break; case ServerProcessing.Async: stack.StoreAndDispatch(serverTransSink, null); break; } }
FIXME: Falta por explicar
Vamos con la interfaz a implementar por un IChannelReceiver
public string[] GetUrlsForUri(string objuri) { string[] arr = new string[1]; if (!objuri.StartsWith("/")) objuri = "/" + objuri; arr[0] = baseUrl + objuri; return arr; } public void StartListening(object data) { jabberClient.Connect ( server, serverName, port, username, password, resource + "/" + channelName ); } public void StopListening(object data) { jabberClient.Disconnect(); } public object ChannelData { get { return chnDataStore ; } } private string baseUrl { get { return String.Format("jabber://{0}@{1}/{2}/{3}", username, serverName, resource, channelName ) ; } }
De nuevo la explicación para todos estos métodos es idéntica a la que se hizo para el canal "smtp".
Vamos por último con la implementación del ServerTransportSink. Como nos podemos imaginar va a ser una implementación vacía ya que no vamos a utilizar ningún canal en el servidor de Jabber para la recepción de mensajes de Remoting.
Es muy importante que quede esto claro, ya que es lo mismo que va a ocurrir para todos los canales de mensajería que vayamos implementando, asi como para el canal inicial del proyecto MonORB.
Estamos utilizando Remoting únicamente en la parte del cliente para atacar servicios que se ofrecen de una forma totalmente independiente de remoting. Esto es una clara muestra de la flexibilidad de esta plataforma.
internal class ServerTransportSink : IServerChannelSink { private IServerChannelSink next; public ServerTransportSink(IServerChannelSink next) { this.next = next; } public ServerProcessing ProcessMessage( IServerChannelSinkStack sinkStack, IMessage requestMsg, ITransportHeaders requestHeaders, Stream requestStream, out IMessage msg, out ITransportHeaders responseHeaders, out Stream responseStream) { // El sumidero de transporte en el servidor siempre es el primero throw new NotSupportedException(); } public void AsyncProcessResponse( IServerResponseChannelSinkStack sinkStack, object state, IMessage msg, ITransportHeaders headers, Stream stream) { throw new NotSupportedException(); } public Stream GetResponseStream( IServerResponseChannelSinkStack sinkStack, object state, IMessage msg, ITransportHeaders headers) { // No permitimos el acceso al stream de forma directa return null; } public IServerChannelSink NextChannelSink { get { return next; } } public IDictionary Properties { get { return null; } } }
Hemos visto como es perfectamente viable el implementar canales de mensajería instantánea con Remoting. Esta plataforma se puede usar de forma que podemos ofrecer la interfaz de Remoting para el envío de mensajes a servicios de mensajería instantánea de forma transparente al protocolo que vayamos a utilizar.
El objetivo dentro del proyecto IM# podría ser pues el de implementar otros protocolos siguiendo el ejemplo del canal de Jabber.
Libro .NET Remoting Avanzado de Ingo Rammer y el capítulo de ejemplo sobre Remoting en Acción de 62 páginas.
Guía Rápida de Remoting en GotDotNet (de lo más interesante)