Inicio > .NET Framework, Apunte, C# > Leer y escribir flujos de datos en la red

Leer y escribir flujos de datos en la red


Hace tiempo escribí un artículo titulado "Todo sobre flujos". En esa entrada hablé y hablé y hablé (o mejor dicho, escribí y escribí y escribí) sobre flujos y flujos y flujos. A pesar de tantos flujos que vimos, no mencioné más que de pasada la lectura de flujos que se conectan por red. Es hora de contar un poco sobre ello.

¿Te acuerdas?

Bien, recordemos que un búfer es un bloque de bytes asignados y reservados en memoria para almacenar un valor. Si los bytes son contiguos, decimos que es un búfer secuencial. Hoy en día, por cierto, prácticamente todos los búferes son secuenciales, y en .NET se representan, en su forma más cruda, como un array de bytes.

Un flujo es un objeto que tiene un búfer secuencial y del cuál mantiene una posición, a partir de la cual puede realizar operaciones de lectura y escritura. Así, mientras en un búfer de 100 bytes uno podría leer los bytes 3, 84 y 42, de forma salteada, en un flujo no pasa lo mismo: comienzo a leer el primer byte, luego el segundo, el tercero, el cuarto, etc., hasta llegar al 42. Como podemos ver, los flujos que se utilizan para obtener bytes en orden aleatorio no son muy eficientes. Pero sí lo son si queremos leer (o escribir) byte a byte. Y ahí es donde mejoran notablemente su rendimiento.

En .NET, un flujo está representado por la clase base abstracta Stream, ubicada en el espacio de nombres System. Dado que ya hablé y hablé y hablé (o mejor dicho, escribí y escribí y escribí) sobre flujos y flujos y flujos, no me parece apropiado repetir y repetir y repetir lo mismo. Por favor, revisa el otro artículo, pero sí ten en cuenta los conceptos importantes. Cualquier Stream podemos utilizarlo junto con StreamReader y StreamWriter, para leer y escribir datos en éste. Esto es independiente del tipo de flujo que estemos manejando: MemoryStream, FileStream, GZipStream, etc.

Como puedes ver, el concepto de flujo tiene un poder de abstracción muy grande. Cualquier lectura/escritura de bytes puede abstraerse de esta forma. Es natural que esta abstracción se aproveche en muchos lados de .NET.

Transmisión de datos en redes

Cuando hablamos de una transmisión de datos en red (es decir, mediante protocolo TCP/IP y afines), tenemos básicamente estos componentes.

1.- Transmisor. Representa el software que envía los datos. El transmisor envía una petición a una dirección IP y a un puerto determinado, donde espera una respuesta. Dependiendo de dicha respuesta generará la siguiente transmisión.

2.- Receptor. Representa el software que recibe los datos. El receptor usualmente abre una conexión a su propia IP y a un puerto determinado, y revisa si existe algún dato. Si no existe dato, termina la conexión, se duerme durante un momento, y vuelve a intentarlo de nuevo. A esto se le llama "escuchar" el puerto. Cuando sí existen datos, los lee y genera una respuesta apropiada.

3.- Protocolo. Durante el proceso de escucha-transmisión, se envían y reciben bytes. Cuando tenemos una regla para el envío/recepción de bytes, se ordena y se estandariza, decimos que tenemos un protocolo de comunicación.

4.- Paquetes. Los datos que se envían por red se hacen en bloques de bytes de tamaño definido, usualmente por el propio protocolo. A estos bloques se les conoce como paquetes.

5.- Enchufe. El concepto de un software que se conecta a una dirección IP y abre un puerto, para transmitir o escuchar, se conoce como un enchufe o sócket. Estos suelen ser componentes genéricos independientes, bien provistos por el sistema operativo, o por el hardware y sus manejadores.

Así pues, podemos decir que en general, el proceso de transmisión de datos se lleva de la siguiente forma.

1.- El receptor comienza el proceso de escucha mediante la apertura de un sócket local en un puerto determinado.

2.- El transmisor abre un sócket y envía una señal para indicar que va a comenzar la transmisión.

3.- Si el transmisor no recibe respuesta, aborta la operación.

4.- Si el transmisor recibe respuesta (su naturaleza depende del protocolo utilizado, por ejemplo, SMTP, SOAP, HTTP, etc.), entonces procede con el envío de información, enviando paquete por paquete.

5.- El receptor recibe paquete por paquete, y cada uno lo va almacenando, ya sea en un búfer de memoria, ya sea en disco duro directamente.

6.- Cada que se envía un paquete, el transmisor se pone en modo "escucha" para recibir la respuesta del receptor, indicándole que todo salió bien (o al menos esto es lo común; algunos protocolos como UDP no esperan confirmación).

7.- Cada que se recibe un paquete, el receptor envía una señal al receptor para indicarle que puede enviar el siguiente paquete.

8.- Cuando ya no hay nada más por enviar, el transmisor envía alguna señal para hacérselo saber al receptor. Y aquí termina el proceso: el transmisor cierra su sócket, y el receptor hará lo propio (y usualmente volverá a quedar a la escucha de nuevas peticiones).

En estos ocho pasos… ¿notas algo común con lo que hemos hablado? ¿Notas algún tremor en la fuerza? ¡Pues el paso cinco! ¿Te suena? "Recibe paquete por paquete y lo va almacenando", dice. Pues claro, ¡esa es nuestra definición de flujo! En otras palabras, podemos decir que el envío secuencial de paquetes en realidad es un flujo de datos. Por supuesto, hay diferencias puesto que entre cada paquete suele haber envío y recepción de comandos entre emisor y receptor, por lo que a pesar de ser secuencial puede ser que no sea inmediato. Sin embargo no importa, sigue entrando dentro de nuestra abstracción de flujo. Por lo tanto, es factible pensar que la transmisión de datos en red se haga mediante un flujo, y por tanto, que en .NET se implemente a través de alguna clase derivada de Stream.

El objeto NetworkStream

En el espacio de nombres System.Net existe un espacio de nombres adicional donde se declaran todas las clases necesarias para trabajar con sóckets: System.Net.Sockets. La clase que (de momento) nos interesa de ese espacio de nombres es NetworkStream. Esta clase implementa los mecanismos necesarios para la lectura/escritura secuencial de bytes (que nosotros conocemos como paquetes). Analicemos un poco la clase.

Tiene un constructor que toma como parámetro un objeto de tipo Socket. Como te imaginarás, esta clase define los sóckets de los que ya hemos hablado. En particular, implementan sóckets de acuerdo a como fueron definidos por la Universidad de Berkeley, allá en los lejanos 80s, para su sistema operativo Unix BSD. En un momento más trataremos con los sóckets. De momento sólo nos interesa saber que necesitamos un sócket para crear un NetworkStream. Lo cual hace sentido, porque el sócket está a la mitad de la comunicación entre sóckets.

Bueno, sigamos. Si analizamos las propiedades que heredamos de Stream, podemos ver que CanRead y CanWrite se implementan como siempre. CanTimeout, sin embargo, siempre regresará true. En efecto, un timeout se da si se nos cae la conexión de red a la mitad de una operación. Por eso, siempre será true. Pero la que más llama la atención es CanSeek: siempre de los siempres, esta propiedad regresa false. ¿Por qué, puedes adivinar?

CanSeek regresa false porque no puede hacerse una llamada al método Seek, por supuesto. Pero ¿Por qué? ¡Pues porque se transmiten paquetes por paquetes! Imagina este escenario. Abres el flujo, y por tanto estás posicionado en el primer byte. Lees los primeros, digamos, 1024 bytes. Si por ejemplo, el flujo se transmite en paquetes de 256 bytes, cuando tú lees los primeros 1024 bytes el flujo, por detrás, hará cuatro peticiones al transmisor, pidiéndole 4 paquetes. Y no pedirá más hasta que tú vuelvas a leer más bytes. Y pasa algo similar en un escenario al revés: supongamos que el flujo transmite de 1024 en 1024 bytes, y tú lees los primeros 256. En la primera lectura, se descargan los 1024 bytes y el flujo te da los 256 bytes. En la segunda lectura, el flujo *no* descarga más bytes, sino que te regresa los segundos 256 bytes de lo que ya había descargado. Y así sucesivamente hasta la quinta llamada, donde volverá a descargar los próximos 1024 bytes.

Ahora bien, supongamos que en medio de estos escenarios, se me ocurre llamar a Seek con un tamaño mayor a lo que ha descargado hasta el momento. ¡Sopas! Pues el flujo no podrá mover el cursor, porque puede ser que la información ni siquiera se haya descargado. Imagínate que pido Seek(10240): tendría que hacer 10 descargas de paquetes. Pero más importante aún: ¡el receptor nunca sabe cuál es el tamaño del búfer! En efecto, lo sabrá sólo cuando el transmisor le envíe la señal de fin. Entonces, en ese caso, una llamada a Seek puede hacer que nos pasemos más allá del final del búfer, sin siquiera saberlo, lo cual sería un desastre. Por eso es que la búsqueda de bytes no está soportada, y Seek lanzará un gran NotSupportedException si la invocas, y CanSeek siempre regresa false.

Por cierto, como decía, no podemos saber de antemano el tamaño del flujo. Un corolario de esto es que por lo mismo, no podremos utilizar la propiedad Length. Si la invocas, tendrás que vértelas con una brillante y reluciente NotSupportedException. Y por lo mismo, también recibirás la misma excepción si invocas a la propiedad Position, la cual debería regresarte tu posición de búsqueda dentro del flujo.

Por lo demás, las propiedades y métodos tradicionales de Stream siguen funcionando: Read, Close, Write, sus variantes Async, Flush, CopyTo, etc.

NetworkStream también cuenta con sus propiedades particulares. De entrada, la propiedad Socket nos regresa -tadáaaa- el sócket que el flujo ocupa. Esta propiedad no pretende ser utilizada por externos, por lo que está declarada como protegida.

Más interesante es la propiedad DataAvailable. Regresando al escenario de arriba, en el que el flujo descarga un paquete de 1024 bytes, pero tú sólo le pides 256. La primera vez que invocas, DataAvailable regresa false, y se descargan 1024 bytes. La segunda vez, ya no se descarga nada porque todavía hay datos en el búfer, y por tanto DataAvailable te regresa true. Y así sucesivamente.

NetworkStream stream = …;
byte[] buffer = new byte[256]

// stream.DataAvailable == false
stream.Read(buffer, 0, buffer.Length); // se descargan 1024 bytes y nos 
                                                            // devuelve 256 bytes. 
// stream.DataAvailable == true
stream.Read(buffer, 0, buffer.Length); // no se descarga nada, nos devuelve
                                                            // bytes 256-512
// stream.DataAvailable == true
stream.Read(buffer, 0, buffer.Length); // no se descarga nada, nos devuelve 
                                                            // bytes 512-768
// stream.DataAvailable == true
stream.Read(buffer, 0, buffer.Length); // no se descarga nada, nos devuelve
                                                            // bytes 768-1024
// stream.DataAvailable == false
stream.Read(buffer, 0, buffer.Length); // se descargan los próximos 1024 bytes 
                                                            // y nos devuelve 256 bytes

Stream define el método Close, con varias sobrecaras, para cerrar un flujo. Pues bien, NetworkStream define un Close con un parámetro numérico, el cual determina a partir de cuántos segundos debe cerrarse el flujo. Es como un cierre a posteriori.

Y bueno, esas son todas las peculiaridades de NetworkStream. Ahora veamos cómo podemos obtener uno de estos objetos.

Trabajando con sóckets

Bueno, la forma de obtener un NetworkStream es a través de sóckets, como ya hemos dicho. La verdad es que la programación con sóckets es un poco complicada, sobre todo, porque necesitas definir tu propio protocolo de comunicación, lo cual no suele ser tarea trivial. Es mejor basarse en algún protocolo estándar, como HTTP, SMTP, FTP, SOAP o algo así. En fin, que no es propósito de esta entrada ver cómo mantener comunicaciones par-a-par (peer-to-peer). Pero veamos un ejemplo. Supongamos que hay algún sócket abierto en alguna parte que está a la escucha de una petición, y que tras hacérsela, nos regresará un archivo con información. Lo primero que habríamos que hacer es abrir un sócket propio. Y ya con eso, usamos alguno de los constructores de NetworkStream.

En el siguiente ejemplo, creamos un sócket y asumimos que hay algo en la IP 192.168.26.1 en el puerto 42, que nos va a enviar un bloque de bytes con el siguiente formato: una cadena de texto de 200 caracteres que representa el nombre de un empleado (asumimos que la cadena viene prefijada con su longitud y su codificación), un entero que representa su clave, y un número flotante que representa su sueldo.

 

using System;
using System.IO;
using System.Net;
using System.Net.Sockets;
…

Socket socket = null;
string ip = "192.168.26.1";
int port = 42;

IPHostEntry host = Dns.GetHostEntry(ip);
foreach (IPAddress address in host.AddressList)
{
    IPEndPoint endpt = new IPEndPoint(address, port);
    Socket temp = new Socket(endpt.AddressFamily, SocketType.Stream, 
        ProtocolType.Tcp);
    temp.Connect(endpt);
    if (temp.Connected)
    {
        socket = temp;
        break;
    }
}

if (socket == null)
    throw new Exception("No pudimos conectarnos a " + ip);

NetworkStream stream = new NetworkStream(socket, true);
if (!stream.CanRead)
    throw new Exception("No podemos leer de este flujo.");

// ¡uju! ¡ya tenemos el stream! podríamos leer bloques de
// bytes, pero… vamos, que estamos en .NET, hay que aprovechar
// la abstracción y usar algún lector de flujos…

BinaryReader reader = new BinaryReader(stream);
string name = reader.ReadString();
int id = reader.ReadInt32();
double salary = reader.ReadDouble();
reader.Close();

// reader.Close cierra el Stream, y éste a su vez cierra el sócket,
// porque le pasamos como parámetro al constructor del NetworkStream
// un true, que quiere decir que el flujo es el dueño del sócket y que
// lo cierre al terminar. Si hubiéramos pasado false, tendríamos que
// cerrar el sócket nosotros mismos.

Console.WriteLine("Empleado {0} - {1}:\t ${2}", name, id, salary);

 

Bueno, como podemos ver, es más lío abrir el sócket que usar el flujo. Al final, usar el NetworkStream nos facilita mucho las cosas, puesto que hereda de Stream lo podemos usar en infinidad de lugares. Por ejemplo, en lugar de usar un BinaryReader, podríamos haber creado un FileStream para crear un archivo, y usar NetworkStream.CopyTo para vaciar el contenido en el FileStream y por tanto, guardando la información en el archivo mismo. ¡Easy peasy!

Por cierto, que la clase Socket tiene métodos Send y Receive para enviar y recibir bytes. En efecto, puedes hacerlo como si no fuera un flujo, pero es muuuucho más engorroso. Mira este ejemplo que saqué del MSDN para hacer una petición a un servidor web, usando sóckets.

 

string request = "GET / HTTP/1.1\r\nHost: " + server + 
    "\r\nConnection: Close\r\n\r\n";

Byte[] bytesSent = Encoding.ASCII.GetBytes(request);
Byte[] bytesReceived = new Byte[256];

// Create a socket connection with the specified server and port.
Socket s = ConnectSocket(server, port);
if (s == null)
    return ("Connection failed");

// Send request to the server.
s.Send(bytesSent, bytesSent.Length, 0);
// Receive the server home page content. int bytes = 0;
string page = "Default HTML page on " + server + ":\r\n";
// The following will block until te page is transmitted. 

do {
    bytes = s.Receive(bytesReceived, bytesReceived.Length, 0);
    page = page + Encoding.ASCII.GetString(bytesReceived, 0, bytes);
}
while (bytes > 0);

 

Vamos, tienes que llevar tu control de bytes leídos, escritos, etc., además de tener que interpretar los bytes por tu cuenta. Checa cómo hay que saber si la cadena se codifica como ASCII, Encoding, etc., mientras que con el BinaryReader y BinaryWriter, eso se hace en automático. Entonces, verás que trabajar así no está cool.

Trabajando con servidores de Internet

Como decía, es muy difícil que en la vida trabajemos con sóckets. En realidad son muy contados los escenarios en los que nos interesarán. La mayoría de las veces nos va a interesar trabajar con protocolos de más alto nivel, estándares y definidos. Uno de estos protocolos es el HTTP.

HTTP se construye sobre TCP/IP. El ejemplo anterior que puse, que copié del MSDN, muestra cómo usar sóckets para realizar una petición HTTP. En realidad, este protocolo define una serie de intercambios de textos. Por ejemplo, el texto:

 

GET / HTTP/1.1
Host: 192.168.26.1
Connection: Close

envía una petición sencilla para obtener la página default del servidor web 192.168.26.1. Esto ilustra de forma rápida cómo funciona HTTP. Básicamente, se le envía un encabezado como el anterior, y el servidor responde con un encabezado de respuesta, más el contenido solicitado, que suele ser un binario o un documento HTML.

Bueno, afortunadamente para nosotros, cuando queramos trabajar con HTTP no hace falta que conozcamos todo el protocolo, ni hacen falta sockets. Para hacer peticiones y responder, tenemos un objeto muy conocido por los programadores de ASP.NET, pero que no siempre saben que podemos usar en aplicaciones que no son ASP.NET. Me refiero, por supuesto, a HttpWebRequest y HttpWebResponse.

Estas clases heredan de WebRequest y WebResponse, respectivamente. Estas clases sirven como base para hacer peticiones basadas en TCP/IP. De hecho, soporta tres protocolos: HTTP (http://), cuyas clases derivadas ya vimos; FTP (ftp://) mediante las clases FtpWebRequest y FtpWebResponse; y el protocolo FILE (file://) mediante FileWebRequest y FileWebResponse.

Sin embargo, la mayoría de las veces, sobre todo si estamos haciendo aplicaciones de escritorio o móviles, no vamos a querer lidiar con los aspectos técnicos de los WebRequest y WebResponse. Afotrunadamente, .NET nos ofrece una alternativa mucho más sencilla: utilizar la clase WebClient.

La clase WebClient contiene métodos que nos permiten enviar y recibir información mediante cualquier protocolo web. De hecho, lo interesante es que WebClient se basa en la URI, la cual representa un recurso web, y con base en eso resuelve si ha de usar HTTP, FTP o FILE. De tal suerte que no tenemos que preocuparnos (mucho) por la fuente que estamos intentando acceder.

El siguiente ejemplo muestra cómo usar un WebClient para descargar la página de Microsoft.

 

using System;
using System.Net;
using System.IO;
… 

string userAgent = "Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.2; .NET CLR"
    + "1.0.3705;)";

WebClient client = new WebClient();
client.Headers.Add("user-agent", userAgent);

Stream stream = client.OpenRead("http://www.microsoft.com");
StreamReader reader = new StreamReader(stream);
string html = reader.ReadToEnd();
reader.Close();

Console.WriteLine("HTML recibido: \n\n");
Console.WriteLine(html);

 

Mucho más fácil que hacerlo con sóckets, ¿no crees?

En fin, WebClient puede hacer varias cosas, pero hay otras que no. Si necesitas más sofisticación por el motivo que sea, deberás hacer uso de los WebRequest y WebResponse antes mencionados. Todo comienza con WebRequest.Create. A este método le pasas la URI a la que te quieres conectar. Usualmente saber qué protocolo vas a utilizar, así que puedes hacer la conversión hacia el WebRequest correspondiente. Después, lo que haces es llenar tu WebRequest con información adicional: encabezados, credenciales, etc. Cuando estés listo, ejecutas el método GetResponse. Al hacerlo, digamos que ejecutas la transacción y se va la petición al servidor. El resultado: un WebResponse precisamente con la respuesta del servidor.

Para ilustrar lo anterior, veamos este ejemplo. Lo que hace es leer un archivo de texto cualquiera, y –tadáaa– nos conectamos a un hipotético servidor FTP y subimos dicho archivo. Easy peasy.

using System;
using System.Net;
using System.IO;
… 

// 1
byte[] fileBuffer = File.ReadAllBytes("C:\\archivo.txt");
// 2
var request = WebRequest.Create("ftp://ftp.miservidor.com") as FtpWebRequest;
request.Credentials = new NetworkCredentials("anonymous", "fer@miservidor.com");
request.ContentLength = fileBuffer.Length;
// 3
Stream stream = request.GetRequestStream();
stream.Write(fileBuffer, 0, fileBuffer.Length);
stream.Close();
// 4
var response = request.GetResponse() as FtpWebResponse;
Console.WriteLine("Estado: {0}", response.StatusDescription);
response.Close();

Repasemos. En 1, obtenemos los bytes que componen al hipotético archivo que queremos subir al FTP. En 2 creamos un FtpWebRequest (nota la conversión que hacemos de WebRequest a FtpWebRequest), establecemos las credenciales y el tamaño del contenido que vamos a enviar. En 3, obtenemos el flujo del FTP como tal y escribimos el contenido del archivo que vamos a enviar. En 4, finalmente, invocamos a GetResponse, método que hará el envío de información al servidor y nos regresará un objeto FtpWebResponse (también nota que se hace conversión) con los resultados, entre otras cosas, el StatusDescription. Por último, cerramos el objeto y terminamos.

 

Conclusiones

 

Bueno, hemos llegado lejos. Creo que ha quedado claro que la representación dle un intercambio de paquetes por red como un flujo de trabajo es perfecta. La abstracción hace muy sencillo el hacer trabajar NetworkStream con lectores y escritores y otras clases que ofrece .NET. También vimos cómo aprovechar las clases para intercambiar archivos entre pares y servidores.

Eso es todo por ahora. ¿Quién se echa un peer-to-peer para compartir archivos? Open-mouthed smile

Categorías:.NET Framework, Apunte, C# Etiquetas: , ,
  1. Victor
    octubre 9, 2013 a las 9:23 am

    Gracias por tus aportes, tengo una pregunta sobre sockets, podria estar escuchando el socket de manera indefinida la recepcion de datos enviada a cualquier momento desde una placa arduino conectada con xbee shield, y como podria especificar el puerto al que quiero mandar los datos ya que son cerca de 20 placas mandando informacion :( … Espero puedas ayudarme .

  1. noviembre 27, 2012 a las 7:21 pm

Responder

Introduce tus datos o haz clic en un icono para iniciar sesión:

Logo de WordPress.com

Estás comentando usando tu cuenta de WordPress.com. Cerrar sesión / Cambiar )

Imagen de Twitter

Estás comentando usando tu cuenta de Twitter. Cerrar sesión / Cambiar )

Foto de Facebook

Estás comentando usando tu cuenta de Facebook. Cerrar sesión / Cambiar )

Google+ photo

Estás comentando usando tu cuenta de Google+. Cerrar sesión / Cambiar )

Conectando a %s