Archivo

Posts Tagged ‘I/O’

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: , ,

Creación de archivos ZIP en .NET


Hace algún tiempo publiqué un artículo sobre cómo manejar flujos de datos: la clase Stream y sus derivadas. Entre lo que expuse en aquella entrada está el uso de las clases GZipStream y DeflateStream como flujos para comprimir datos. Una de las limitantes, sin embargo, de estas clases, es que aunque generan un flujo que puede guardarse eventualmente en disco duro vía FileStream, no son compatibles con programas populares como WinZip, WinRAR o el mismo componente Zip que viene con el Windows Explorer.

O mejor dicho: GZipStream y DeflateStream comprimen un archivo, pero no crean un contenedor para archivos compresos, que al final del día es lo que es un archivo ZIP. En fin, que hasta hace poco, estas clases se podían usar para comprimir archivos de uso propio de la aplicación, pero no para que pudiesen distribuirse los archivos.

Con la salida de .NET 4.5, sin embargo, apareció unas clases en System.IO.Compression que no causaron mucho ruido o atención, pero que a mi entender son súper importantes, porque permiten precisamente comprimir archivos dentro de un contenedor en formato ZIP estándar. Las clases son las siguientes.

1.- ZipArchiveEntry. Representa un archivo compreso dentro de un archivo ZIP (ZipArchive).

2.- ZipArchive. Representa un archivo ZIP, que tiene archivos compresos (ZipArchiveEntry).

3.- ZipFile. Contiene métodos estáticos para trabajar con archivos ZIP y archivos compresos.

4.- ZipFileExtensions. Contiene métodos de extensión para ZipArchive y ZipArchiveEntry.


Veamos entonces algunos ejemplos sobre cómo utilizar estas clases.

Crear un archivo zip

 

using System.IO;
using System.IO.Compression;
…

// 1
FileInfo sourceFile = new FileInfo(@"C:\reporte1.xlsx");
FileStream sourceStream = sourceFile.OpenRead();
// 2
FileStream stream = new FileStream(@"C:\reportes.zip", FileMode.Open);
// 3 
ZipArchive archive = new ZipArchive(stream, ZipArchiveMode.Create);
// 4 
ZipArchiveEntry entry = archive.CreateEntry(sourceFile.Name);
// 5
Stream zipStream = entry.Open();
// 6
sourceStream.CopyTo(zipStream);
// 7
zipStream.Close();
sourceStream.Close();
archive.Dispose();
stream.Close();

 

Bueno, veamos que sucede. Asumimos que en C:\ existe un archivo, reporte1.xlsx, y que tenemos permisos de escritura en C:\. La verdad que escogí ese directorio para no hacer tan largo el ejemplo. En fin. Por tanto, primero abrimos el archivo que queremos añadir a un archivo zip. Para ello, usamos FileInfo y FileStream en modo lectura, como siempre.

El segundo paso es que necesitamos crear un flujo de archivo para nuestro archivo zip contenedor. Volvemos a usar un FileStream tradicional, hasta aquí no hay nada nuevo. Lo nuevo viene en el tercer paso: creamos un ZipArchive y le pasamos como primer parámetro el flujo que hemos creado para nuestro zip (así garantizamos que el ZipArchive escriba sobre éste), y como segundo parámetro pasamos la enumeración ZipArchiveMode.Create para indicarle que vamos a crear entradas. Y ahora sí, viene el cuarto paso: creamos una entrada invocando ZipArchive.CreateEntry, y le pasamos el nombre con el que queremos identificarlo. Para más fácil, le pasamos el nombre original mediante FileInfo.Name. Y ahora a tenemos un ZipArchiveEntry, aunque vacío. En el quinto paso abrimos el flujo de esa entrada y obtenemos un Stream donde podemos escribir. El sexto paso vacía el flujo del archivo que queremos comprimir a la entrada dentro del zip que hemos creado. Aquí es donde realmente ocurre la compresión. Y finalmente, en el paso 7 cerramos flujos y limpiamos memoria. Nota que puedes usar directivas "using" en lugar de invocar a Close y Dispose.

Resumiendo hasta el momento: 1) abrimos archivo fuente, 2 y 3) creamos archivo zip, 4) creamos una entrada vacía en el zip.

Extraer archivo de un zip

 

using System.IO; 
using System.IO.Compression; 
… 

// 1 
FileInfo zipFile = new FileInfo(@"C:\reportes.zip"); 
FileStream zipStream = zipFile.OpenRead(); 
// 2 
ZipArchive archive = new ZipArchive(zipStream, ZipArchiveMode.Read); 
// 3 
ZipArchiveEntry entry = archive.GetEntry("reporte1.xlsx"); 
// 4 
Stream stream = entry.Open(); 
// 5 
FileInfo destFile = new FileInfo(@"C:\reporte1.xlsx"); 
if (file.Exists) { 
    file.Delete(); 
    file.Refresh(); 
} 
FileStream destStream = destFile.OpenWrite(); 
// 6 
stream.CopyTo(destStream); 
// 7 
stream.Close(); 
destStream.Close(); 
archive.Dispose(); 
zipStream.Close();

 

Es un proceso similar, de hecho, pero a la inversa. Veamos. En 1, abrimos un flujo proveniente del archivo ZIP del cual queremos extraer el archivo. Luego, en 2 creamos el ZipArchive, pasándole el flujo que acabamos de abrir, y la bandera ZipArchiveMode.Read, para indicarle que vamos a leer. Esto es de suma importancia, porque las banderas de lectura, escritura y búsqueda de los objetos Stream dependen de esta bandera. Si pones un Read e intentas escribir, seguramente tendrás un InvalidOperationException entre manos.

Bueno bueno, ya. En 3 usamos el método GetEntry, pasándole el nombre del archivo dentro del zip, es decir, de la entrada, y obtenemos un objeto de tipo ZipAchiveEntry. Alternativamente, puedes iterar sobre ZipArchive.Entries para ver todas las entradas existentes. Ya con el ZipArchiveEntry podemos abrir el stream mediante ZipArchiveEntry.Open, como se muestra en 4. Ya tenemos el flujo, ahora sólo debemos guardarlo en disco duro. El paso 5 abre el archivo destino donde guardaremos el archivo descompreso. Antes revisamos que si el archivo existe, lo eliminemos. Y abrimos el flujo de dicho archivo en modo escritura.

Y ahora sí, en 6 vemos cómo copiamos los bytes del flujo del archivo compreso al flujo del archivo en disco duro. Perfecto, ya estamos del otro lado. Lo último que resta es cerrar los objetos invocando a Close y Dispose, y listo.

Comprimir un directorio entero

Muy similar a comprimir un archivo, sólo que nos basamos en DirectoryInfo… Veamos.

 

using System.IO; 
using System.IO.Compression; 
… 

// 1 
DirectoryInfo dir = new DirectoryInfo(@"C:\reportes\"); 
// 2 
FileStream stream = new FileStream(@"C:\reportes.zip", FileMode.Open); 
// 3 
ZipArchive archive = new ZipArchive(stream, ZipArchiveMode.Create); 
// 4 
FileInfo[] sourceFiles = dir.GetFiles(); 
foreach (FileInfo sourceFile in sourceFiles) 
{ 
    // 5 
    Stream sourceStream = sourceFile.OpenRead(); 
    // 6 
    ZipArchiveEntry entry = archive.CreateEntry(sourceFile.Name); 
    // 7 
    Stream zipStream = entry.Open(); 
    // 8 
    sourceStream.CopyTo(zipStream); 
    // 9 
    zipStream.Close(); 
    sourceStream.Close(); 
} 
// 10 
archive.Dispose(); 
stream.Close();

  

Como puedes apreciar, es un código muy similar, sólo que usamos DirectoryInfo.GetFiles para obtener los archivos, e iteramos sobre estos. Veamos. En 1 abrimos el directorio que queremos comprimir. En 2, creamos el archivo donde guardaremos el zip y en 3 asociamos el flujo de éste al archivo zip. En 4 obtenemos los archivos del directorio y comenzamos a iterar para cada uno de ellos. En 5 abrimos el flujo de cada archivo, en 6 creamos la entrada dentro del zip, en 7 abrimos el flujo de la entrada dentro del zip, y en 8 vaciamos los bytes del archivo a la entrada dentro del zip. En 9 limpiamos los flujos locales. Finalmente, en 10 hacemos limpieza del archivo zip y del flujo. ¡Easy Peasy!

Bueno, si no quieres hacer tanto, huevas, puedes usar ZipFile.CreateFromDirectory:

ZipFile.CreateFromDirectory(@"C:\reportes\", @"C:\reportes.zip");

y listo. Pero bueno, lo interesante del código anterior era mostrar cómo hacerlo manual. Ahora que sabes cómo, usa la versión corta. Nota: la versión corta está disponible en .NET 4.5, pero no en .NET para Windows Store Apps. Así que si estabas pensando hacer alguna app para Windows 8 / RT, te la pelas: tendrás que emplear la versión larga.

Extraer los archivos de un zip

 

Vamos ahora a extraer todos los archivos de un zip a un directorio. Sea pues.

 

using System.IO; 
using System.IO.Compression; 
… 

// 1 
FileInfo zipFile = new FileInfo(@"C:\reportes.zip"); 
FileStream zipStream = zipFile.OpenRead(); 
// 2 
ZipArchive archive = new ZipArchive(zipStream, ZipArchiveMode.Read); 
// 3 
foreach (ZipArchiveEntry entry in archive.Entries) 
{ 
    // 4 
    Stream stream = entry.Open(); 
    // 5 
    FileInfo destFile = new FileInfo(@"C:\reporte\" + entry.Name); 
    if (file.Exists) { 
        file.Delete(); 
        file.Update(); 
    } 
    FileStream destStream = destFile.OpenWrite(); 
    // 6 
    stream.CopyTo(destStream); 
    // 7 
    stream.Close(); 
    destStream.Close(); 
} 
// 8 
archive.Dispose(); 
zipStream.Close(); 

Pues easy peasy también, ¿no? Ya nos la sabemos: abrimos archivo zip (1), asociamos el archivo con un zip (2), iteramos por cada entrada del zip (3), abrimos el flujo de una entrada (4), revisamos que el archivo no exista en el directorio destino (5) y abrimos el flujo del archivo destino, luego copiamos el flujo de la entrada del zip hacia el flujo del archivo abierto (6), para por último, cerrar flujos locales (7) y globales (8).

Para los huevas, pueden hacerlo más fácil, si no programan para .NET en Windows Store Apps.

ZipFile.ExtractToFile(@"C:\reportes.zip", @"C:\reportes\");

¡Jo! Bueno, siempre es mejor saber cómo se hacen las cosas por detrás. Sin albur.

Agregar y eliminar archivos a un zip

Y ya como último ejemplo, y creo que cubrimos todos los escenarios más importantes, está éste, el cual muestra cómo añadir un archivo a algún zip existente a la vez que eliminamos una entrada al mismo zip. Aquí la clave está en abrir el ZipArchive en modo ZipArchiveMode.Update en lugar de Read o Create. Lo demás es igual a lo que ya hemos visto.

 

using System.IO; 
using System.IO.Compression; 
… 

// 1 
FileInfo zipFile = new FileInfo(@"C:\reportes.zip"); 
FileStream zipStream = zipFile.Open(FileMode.Open); 
ZipArchive archive = new ZipArchive(zipStream, ZipArchiveMode.Update); 
// 2 
ZipArchiveEntry oldEntry = archive.GetEntry("reporte1.xlsx"); 
oldEntry.Delete(); 
// 3 
FileInfo sourceFile = new FileInfo(@"C:\reporte2.xlsx"); 
FileStream sourceStream = sourceFile.OpenRead(); 
ZipArchiveEntry newEntry = archive.CreateEntry(sourceFile.Name); 
Stream stream = newEntry.Open(); 
sourceStream.CopyTo(stream); 
stream.Close(); 
sourceStream.Close(); 
// 4 
archive.Dispose(); 
zipStream.Close();

  

 

Mucho más compacto porque ya no necesitamos re-explicar miles de cosas, vamos por pasos. En 1 abrimos el archivo y el flujo del mismo. Nota que ahora no usamos OpenRead u OpenWrite, pues necesitamos que el flujo sea tanto de lectura como de escritura, así que usamos Open con FileMode.Open. Luego asociamos el flujo a un zip, pero ojo, aquí pasamos como parámetro ZipArchiveMode.Update como segundo parámetro.

Luego, en 2 utilizamos GetEntry para obtener el archivo reporte1.xlsx, que debe encontrarse en el zip. Esto nos regresa un ZipArchiveEntry. Para eliminarlo, sólo tenemos que invocar su método Delete. Easy peasy esta parte.

Luego continuamos con 3, lo cual es lo mismo que hemos visto: abrir el archivo que queremos añadir al zip (en este caso, el ingeniosamente llamado reporte2.xlsx), abrir el flujo, crear la entrada en el zip, copiar bytes de la fuente a la entrada, y cerrar. Esto no varía en nada, todo el trabajo lo hace ya ZipArchive cuando le dijimos que se abriera en modo Update.

Por último, cerramos con 4 cerrando los objetos que hemos creado, haciendo Close y Dispose. And be done with it.

Conclusiones

Es bueno que Microsoft haya añadido estas clases a .NET. Me parece que hace mucho tiempo que eran necesarias. Hasta ahora uno tenía que utilizar librerías de terceros. Y no es que sean malas. SharpZipLib, de iC# Code, es muy buena, por ejemplo. Sin embargo tienen el estigma "no Microsoft", y eso hace que algunas empresas le pongan peros al momento de aceptar desarrollos que la utilicen. No todas, pero sí las hay. Además, de esta forma se estandariza la forma de hacerlo.

Quizás lo que me llama la atención es que no haya mucha publicidad sobre este tema importante. Es decir, el Task Parallel Library recibió mucha atención cuando salió .NET 4, y la integración de C# con el TPL (i.e. las palabras reservadas async y await) ahora que salió el .NET 4.5. Y estas clases se me hacen muy importantes también. Ojalá sólo sea falta de información de mi parte.

Pero bueno, que ahora ya las conoces y no hay pretexto para comenzar a utilizar estas clases y comenzar a migrar tu código legado… ¿Comentarios, preguntas, dudas?

Categorías:.NET Framework, Apunte, C# Etiquetas: , ,

El misterio de las extensiones de los archivos

septiembre 22, 2010 3 comentarios

Hoy por la mañana un tío en los foros de MSDN hizo una pregunta relacionada con las extensiones de los archivos. En esencia, lo que se preguntaba era que cómo era posible que una aplicación entendiera archivos con extensión totalmente diferente a la original, y que si se podía explicar cómo se codificaban las extensiones de los archivos.

Bien, dado que es un concepto que a veces cuesta trabajo entender (creo que por la naturaleza misma del concepto de extensión) aprovecho para hacer un pequeño apunte al respecto.

Por supuesto, todos conocemos lo que es una extensión de un archivo. Es decir, los documentos de Microsoft Word tienen una extensión .doc, los de Excel .xls, los de texto son .txt y el formato universal para música es .mp3. Pero ¿qué quiere decir esto de extensión? La respuesta no es muy complicada: una extensión no es otra cosa que parte del nombre del archivo. O mejor dicho, forma parte del nombre y son usadas para clasificar a los archivos, de la misma forma en la que nuestros apellidos son nombres para identificar la familia a la que pertenecemos. Pero la extensión no afecta las características de un archivo, así como el nombre no afecta mi ser: yo soy un humano independientemente de si me apellido Gómez, Pérez o Teal’c.

¿Para qué utilizar extensiones entonces? Bueno, el sistema operativo puede hacer búsquedas, filtros, ordenamientos y muchas otras cosas con esta información adicional, de la misma forma en la que una persona en el registro público puede encontrar nuestra información de forma más fácil si le damos nuestro apellido. Pero la extensión de un archivo, repito, no afecta el contenido del mismo.

En esencia, todos los archivos se guardan en un solo formato: binario. Todos, absolutamente todos. Es más, todo lo que está en nuestra máquina se encuentra en binario. En realidad, cuando uno dice que guarda un archivo en formato de texto, de XML, de Word, de PDF, etc., lo que quiere decir es que el arreglo de los bytes está hecho de tal forma que se puede representar como texto, como un XML, o que puede ser interpretado por programas que entiendan el ordenamiento de bytes establecido por Word o Acrobat. Incluso los ejecutables o las librerías de enlace dinámico son binarios, aunque estos son interpretados de forma especial por el sistema operativo.

Luego entonces, la capacidad de un programa de "leer" un tipo de archivo se traduce a si el programa puede o no entender e interpretar el orden de bytes que contiene un determinado archivo. Tomemos como ejemplo al Notepad. La finalidad de este programa consiste en leer y escribir texto simple. La tabla de caracteres ASCII (o UTF, Unicode, etc.) establece una correspondencia entre un byte y un caracter. Por ejemplo, el byte con valor 65 corresponde a la letra latina mayúscula A, mientras que la letra latina minúscula j se corresponde con un byte con valor 106. Y así sucesivamente, para cada caracter. Por ende, el Notepad lo que hace es interpretar estos números y dibujar en pantalla los píxeles correspondientes a cada caracter.

En cambio, un archivo de Word no solamente guarda texto, sino que guarda mucha más información: fuentes, colores, posición de las páginas, encabezados, imágenes, errores ortográficos y un larguísimo etcétera. Si abres un archivo Word con el Notepad verás la interpretación en texto simple del contenido binario. En algunos casos, reconocerás palabras (que se corresponde con el texto que guarda Word, eventualmente), aunque la mayor parte de las veces serán símbolos raros, correspondientes al equivalente en la tabla ASCII de los números que cada byte representa.

En fin, el punto es que la extensión de archivo no hace nada, y de hecho estas son arbitrarias. Normalmente, como programadores escogemos extensiones estándares e intuitivas (como .xml para documentos XML, .cpp y .cs para archivos de código C++ y C#, o incluso .dbf para un archivo que contenga una base de datos), o bien los escogemos por su contenido (por ejemplo, a veces se usa la extensión .dat para indicar un archivo que guarda datos de algún tipo, o .config para indicar que el archivo en cuestión es uno que guarda datos de configuración), o incluso se escogen en base al nombre del programa (por ejemplo, un .psd indica un archivo de imagen de Photoshop, o un .ppt indica un archivo de Power Point). Pero nuevamente, estos son arbitrarios.

Como prueba, hagamos este ejercicio. Abre Microsoft Excel, crea una hoja y mete algunos datos. Luego, guarda el archivo y cierra el programa. Una vez hecho esto, ve y cambia la extensión al archivo, digamos, de prueba.xls a prueba.fer. Abre Excel nuevamente, dale al menú Abrir y navega al directorio donde radica el archivo. Notarás que éste no aparece: esto es así porque Excel filtra los archivos mostrados a aquellos que tengan extensión xls o xlsx (un ejemplo de cómo las extensiones pueden hacer la vida más fácil). En fin, selecciona la opción de “mostrar todos los archivos” y ahora sí, selecciona a prueba.fer. Cuando Excel te pregunte que si deseas abrir el archivo, dile que sí, y voilá: sin problemas mostrará el archivo en cuestión. Esto es así porque solo cambió el nombre del archivo, mas no su contenido. Igual pasa con las extensiones de cualquier otro archivo: al final lo que importa es la interpretación que se le dé al contenido.

Ahora bien, desde el punto de vista de C# y .NET, abrir un archivo cualquiera significa utilizar un objeto de tipo FileInfo y mediante este, obtener un objeto de tipo FileStream (ambos declarados en System.IO). Al FileStream, como a cualquier otro flujo, le podemos leer o escribir bytes (obviamente dependiendo de si abrimos el archivo en modo lectura o escritura). Ahora que podemos usar algunas clases como BinaryReader y BinaryWriter para que nos ayuden a escribir el binario, o bien StreamReader y StreamWriter para que nos ayuden a escribir texto (en este caso, ambas clases se encargan de interpretar el binario como texto). Análogamente tenemos a XmlTextReader y XmlTextWriter. Y así sucesivamente. Bueno, ya en otra ocasión hice una entrada sobre los flujos de datos, ahí hay más información al respecto. Pero el punto es el mismo: lo que se lee y escribe es binario, puros bytes, solo que contamos con algunas clases que nos facilitan la lectura y escritura.

Bueno, pero… ¿qué onda con el formato? Ah bueno. El formato es, digamos, un acuerdo en cómo interpretar los bytes. En el caso de los archivos de texto, la interpretación se hace en base a la tabla ASCII / UTF / Unicode que corresponda. En el caso de un XML, no es otra cosa que un archivo de texto plano que tiene ciertas reglas para crear y acomodar marcas. Por otra parte, un mapa de bits de 32 bits guarda un byte para el color rojo, uno para el verde y uno para el color azul, más otro para la transparencia, aparte de guardar un encabezado con información adicional. Algunos formatos son estándares, como el texto o el XML, algunos son propietarios pero públicos como el JPEG o el GIF, y algunos otros son propietarios y privados, como el PSD o el DOC. De cualquier forma, es necesario conocer el formato de antemano, para leer un archivo. Por ejemplo, en el caso de los mapas de bits, necesitamos leer los primeros 54 bytes del archivo para tener información sobre la profundidad del color, el número de filas, etc.

Por supuesto, tú también puedes crear tus propios formatos. A este proceso se le llama serialización y la mayoría de los lenguajes y plataformas soporta la serialización. En .NET, por ejemplo, basta agregar el atributo [Serializable] a la clase y solito crea el formato necesario para leer y escribir un objeto de ese tipo. Adicionalmente, puedes implementar la interfaz ISerializable y tú mismo controlar qué se serializa y qué no.

En fin, bueno, eso ha sido todo. Espero haber sido claro y no haberte dejado más liado que cuando llegaste. Mi meta es quitar ese como velo de misterio que hay tras las extensiones, y espero haberlo logrado: de esta forma uno puede entender mejor los archivos y como crearlos. Ahí me dices si sí cumplí mi objetivo.

Arrivederci bambini!

Categorías:Apunte Etiquetas: ,

Todo sobre flujos de datos


Los flujos de datos son secuencias de bytes. Es decir, un conjunto de bytes en un orden específico que pueden representar cualquier cosa: texto, un archivo, un objeto, una imagen. Bytes en general. Un flujo de dato sirve de puente entre la fuente de datos y la aplicación (por ejemplo, lectura de un archivo desde el disco duro), o entre la aplicación y el destino (escritura hacia un archivo del disco duro).

Antes, en el antiguo mundo de C, era común crear un puntero con suficiente memoria y guardar ahí algún dato, y luego hacer algún uso de él, como guardarlo en algún archivo. Aunque este enfoque funciona, existe ciertos problemas ya que, por ejemplo, hay que saber las posiciones de los bytes, el tamaño ubicado en memoria podía ser menor al necesario y por ende habíamos de llamar a las funciones calloc y realloc para agrandar nuestro búfer, y no teníamos una forma ordenada de leer y escribir nuestros datos.

Ante estos problemas, la idea de los flujos se fue desarrollando poco a poco. C++ ya incorpora el concepto, y de hecho la librería estándar de C++ hace uso de flujos para archivos (std::cin y std::cout son flujos, por ejemplo, y qué decir de istream y ifstream). Obviamente .NET tenía que incorporar flujos y de hecho lo hace con elegancia, con un enfoque moderno y totalmente orientado a objetos.

La clase base para los flujos es System.IO.Stream, y es abstracta. Esta clase define el comportamiento básico de cualquier flujo en .NET. En esencia, define:

  • Si un flujo puede leer bytes (CanRead, Read).
  • Si un flujo puede escribir bytes (CanWrite, Write).
  • El desplazamiento hacia atrás y hacia adelante en el flujo de bytes (Seek, Position).
  • El tamaño del flujo (Length, SetLength).
  • La forma de descargar los datos en el destino final (Flush).

Algunos ejemplos de flujos son el FileStream, que define un flujo de lectura y escritura sobre un archivo; el GZipStream, que define un flujo sobre un archivo comprimido; el NetworkStream, que define un flujo de lectura y escritura a través de una red; el MemoryStream, que define un flujo de bytes en la propia memoria RAM; el HtmlStream  entre otros.

Lectura y escritura de flujos

 

Por mucho, los métodos más importantes de un Stream son Read y Write, para leer y escribir bytes (que a final de cuentas es el principal propósito de un flujo). Alternativamente existen BeginRead, EndRead, BeginWrite y EndWrite para leer y escribir de forma asíncrona, pero éstos métodos al final se basan en Read y Write, respectivamente.

Así, sin más preámbulos, veamos cómo leer y escribir en un flujo de datos. El siguiente ejemplo muestra la forma básica de leer bytes, aunque de momento no nos importa qué tipo de flujo tratamos ni cómo lo hemos obtenido.

Stream stream = ObtenerStreamDeAlgunaForma();

byte[] buffer;
int singleByte;

// leer en bloques de 1024 bytes
buffer = new byte[1024];
while (stream.Read(buffer, 0, 1024) > 0)
{
  // hacer algo con buffer
}

// leer todo el flujo al mismo tiempo
buffer = new byte[stream.Length];
stream.Read(buffer, 0, buffer.Length);

// leer byte por byte
singleByte = stream.ReadByte();
while (singleByte != -1)
{
  // hacer algo con el byte
}

En las líneas 7 a 11, se leen bloques de 1024 bytes hasta que ya no haya más que leer: Stream.Read copia los bytes en el primer parámetro, y regresa el número de bytes que faltan por leer. Esto es especialmente útil cuando tratamos con flujos muy grandes, de tal suerte que evitamos consumir nuestra memoria RAM. Las líneas 14 y 15 muestran cómo leer todos los bytes de un jalón. Esto es especialmente útil cuando el flujo que queremos leer suele ser un tamaño relativamente pequeño. Y finalmente, las líneas 18 en adelante muestra cómo leer byte por byte. Esta forma es la que menos memoria consume pero la más ineficiente.

Escribir bytes en un flujo es mucho más sencillo.

Stream stream = ObtenerFlujoDeAlgunaForma();

byte[] buffer;
byte singleByte;

// escribir un bloque de 1024 bytes
buffer = ObtenerBufferDeAlgunLado();
stream.Write(buffer, 0, 1024);

// escribir un byte
singleByte = ObtenerAlgunByte();
stream.WriteByte(singleByte);

Decíamos que los flujos se leen/escriben de forma secuencial. Por ello, cuando se leen o escriben bytes, la posición del flujo avanza el número de bytes leídos/escritos. Así, en nuestro ejemplo anterior, cada vez que leo 1024 bytes, el puntero del siguiente byte aumenta en 1024 (especificado por la propiedad Position). Por supuesto, si queremos saltarnos hasta una posición en particular, podemos utilizar el método Seek. Así, en el siguiente ejemplo:

Stream stream = ObtenerFlujoDeAlgunaForma();
stream.Seek(1024, SeekOrigin.Begin);
stream.Seek(1024, SeekOrigin.Current);
stream.Seek(1024, SeekOrigin.End);

tenemos que en la línea 2 nos movemos a la posición 1024 desde el principio. Luego nos movemos otros 1024 bytes desde la posición actual, lo cuál nos ubicaría en la posición 2048. Y finalmente, la cuarta línea nos ubica a 1024 bytes del final del flujo.

Es importante resaltar que un flujo puede ser de lectura y escritura, o de solo lectura o de solo escritura. Eso lo podemos saber gracias a las propiedades CanRead y CanWrite. Si violamos estas limitantes obtendremos una bonita excepción NotSupportedException. Por ejemplo, si abrimos un archivo en forma de solo lectura e intentamos escribir en ella.

Cada tipo de flujo suele definir la forma más eficiente para escribir en su destino (por ejemplo un archivo). Usualmente crearán un búfer en la memoria y solo cuando éste se llene lo escribirán en el destino. De esta forma, se hace más eficiente el proceso de escritura. Podemos, sin embargo, forzar a que el flujo escriba en su fuente llamando al método Flush.

Una vez que terminemos de utilizar un flujo, tenemos que cerrarlo con el método Close, o alternativamente llamando al método Dispose (quien a su vez llamará a Close). Esto es muy importante, porque de no hacerlo el flujo no liberará los recursos que tenga asociados y éstos permanecerán en memoria hasta que finalice la ejecución de la aplicación a la que pertenece, efectivamente causando una fuga de memoria en el mejor de los casos, pudiendo bloquear recursos (como archivos o conexiones de red) y hasta acabarse la memoria RAM, en el peor escenario.

Una forma fácil de asegurarse que esto no pasa es utilizar la cláusula “using” como se muestra a continuación:

Stream stream = ObtenerFlujoDeAlgunaForma();
using (stream)
{
  // hacer algo con el flujo
} // se llama a Dispose de forma automática

O bien lo puedes hacer de forma manual, llamando a Close o a Dispose. Para asegurarnos que Close sea llamado sin importar si surge un error, podemos poner dicha llamada en un bloque finally, como se muestra a continuación.

Stream stream = ObtenerFlujoDeAlgunaForma();
try
{
  // hacer algo con el flujo
}
finally
{
  stream.Close();
}

Por cierto, una vez que llamamos a Close o Dispose, nuestro flujo queda completamente inutilizable. Intentar leer o escribir después de que el flujo se ha cerrado provocará una excepción ObjectDisposedException.

 

Lectura y escritura de texto

 

En muchas ocasiones los datos contenidos dentro del flujo es texto. Pero los flujos solo manejan bytes, por lo que tenemos que realizar la conversión entre tipos de datos. Una forma de hacer esto es codificar la cadena de texto en bytes, o decodificarla, según se requiera. Para ello, podemos utilizar alguna clase derivada de System.Text.Encoding. Esta clase define el método GetBytes para obtener la representación binaria de una cadena de texto, y el método GetString para obtener una cadena de texto dada una representación binaria.

Sin embargo, Encoding es una clase abstracta, por lo que tenemos que utilizar alguna concreta. Encoding tiene algunas propiedades estáticas que nos proveen dichas clases concretas de acuerdo al tipo de codificación que queremos emplear: ASCII, UTF-8, Unicode, etc.

El siguiente ejemplo muestra cómo escribir y leer una cadena de texto con codificación Unicode.

string text;
byte[] buffer;

Stream stream = ObtenerFlujoDeAlgunaForma();
text = "Hola mundo";
buffer = Encoding.Unicode.GetBytes(text);
stream.Write(buffer, 0, buffer.Length);
stream.Close();

Stream stream = ObtenerFlujoDeAlgunaForma();
buffer = new byte[stream.Length];
stream.Read(buffer, 0, buffer.Length);
text = Encoding.Unicode.GetString(buffer);
stream.Close();

La codificación de texto en binario es un tema lo bastante grande como para tratarlo en una entrada por separado. Sin embargo, vale la pena mencionar que el tipo de codificación empleado importa mucho, ya que ésta define el tamaño en bytes a utilizar por cada caracter. En principio, la codificación ASCII utiliza hasta 128 posibles caracteres, por lo que utilizar caracteres acentuados producirá un error al querer codificar o decodificar una cadena de texto. Por otra parte, si quisiera utilizar el alfabeto chino tendría que emplear la codificación Unicode o UTF-32.

Bueno, regresando al tema de los flujos. Evidentemente utilizar las codificaciones es un trabajo mayor: imagínate estar haciendo conversiones a cada rato. Para solventar este problema, el .NET Framework cuenta con el concepto de lectores y escritores de texto. La idea detrás de éstos es que un lector de texto puede leer una serie secuencial de caracteres, y análogamente un escritor de texto puede escribir una serie secuencial de caracteres. Como te darás cuenta, esta definición cuadra con la del flujo, que es una lectura o escritura secuencial de bytes. Por tanto, no sorprende el hecho de que los lectores y escritores lean y escriban flujos de datos.

Las clases que propiamente implementan estos conceptos son TextReader y TextWriter para lectura y escritura, respectivamente. Ambas clases son abstractas, por lo que se tiene que usar alguna clase concreta. Existen muchas para diferentes propósitos. Las clases derivadas diseñadas para trabajar con flujos son StreamReader y StreamWriter, respectivamente.

Comencemos por StreamReader. Esta clase cuenta con varios constructores. Dos llaman especilamente la atención: aquél que toma como parámetro un Stream, y aquél que toma dos parámetros: un Stream y un Encoding, que especifica la codificación a utilizar. El flujo que pasemos como parámetro debe estar habilitado para leerse, evidentemente. Un tercer constructor toma como parámetro una cadena de texto que se encarga de abrir un flujo desde un archivo (el parámetro en cuestión es la dirección del mismo), pero dado que hay otras formas de obtener el flujo de un archivo, y que veremos más adelante, de momento no le haremos caso.

StreamReader cuenta con varios métodos para realizar la lectura de texto desde el flujo. Read lee el siguiente caracter disponible (recordemos que se leen de forma secuencial; si queremos leer un caracter sin que la secuencia avance, utilizamos el método Peek). ReadBlock lee un determinado número de caracteres, especificado por el parámetro que se le pasa. ReadLine lee texto hasta encontrar un caracter de nueva línea (\r\n, por ejemplo). Y finalmente, ReadToEnd lee desde la posición actual hasta el final del flujo.

El siguiente ejemplo muestra cómo leer línea por línea de texto (en codificación Unicode) y guardarlos en un StringBuilder (esta forma gasta menos memoria pero es menos eficiente), y cómo leer todo el flujo de una sola vez (esta forma es más eficiente, pero consume más memoria).

Stream stream;
StreamReader reader;
StringBuilder text;

stream = ObtenerFlujoDeAlgunaForma();
reader = new StreamReader(stream, Encoding.Unicode);
text = new StringBuilder();
while (!reader.EndOfStream)
  text.Append(reader.ReadLine());
reader.Close();

stream = ObtenerFlujoDeAlgunaForma();
reader = new StreamReader(stream, Encoding.Unicode);
text.Append(reader.ReadToEnd());
reader.Close();

Notarás que cuando dejamos de utilizar el lector, mandamos llamar al método StreamReader.Close, y que en cambio no cerramos nuestro Stream. Esto es así porque StreamReader.Close se encarga de cerrar el flujo de datos sobre el cual se basa, así que ya no se necesita llamar a Stream.Close explícitamente.

Análogamente, tenemos que StreamWriter también cuenta con varios constructores, y de igual forma tiene uno que toma el Stream sobre el cual escribirá texto (y que se asume que dicho flujo está habilitado para escritura), y otro que toma el Stream y la codificación a utilizar. El StreamWriter cuenta también con un método Write, que permite escribir texto, números enteros, booleanos, números reales y cualquier otro objeto (a través del método ToString de éstos). Incluso se pueden aplicar formateos de texto. Adicionalmente, está el método WriteLine (y sus varias sobrecargas) que hace lo mismo que Write, solo que pone un caracter de fin de línea (\r\n) al final.

El siguiente ejemplo muestra cómo escribir texto en un flujo.

Stream stream;
StreamWriter writer;

stream = ObtenerFlujoDeAlgunaForma();
writer = new StreamWriter(stream, Encoding.Unicode);
writer.Write("Hola mundo\n");
writer.WriteLine("Hola mundo");
writer.WriteLine("Hola {0}", "mundo");
writer.Close();

 

Lectura y escritura de binario

 

De la misma forma en la que podemos utilizar lectores y escritores para leer y escribir texto en un flujo de datos, podemos utilizar lectores y escritores binarios. Las clases que nos ayudan con esto son BinaryReader y BinaryWriter.

Pero ¿para qué utilizar estas clases si Stream me permite leer y escribir binario? Contestemos la pregunta con un ejemplo. Supongamos que quiero guardar en binario un número entero. Entonces tengo que convertir el entero en binario, y aquí es donde la puerca tuerce el rabo. Podemos utilizar la clase BitConverter:

int i = 64;
byte[] buffer = BitConverter.GetBytes(i);

Stream stream = ObtenerFlujoDeAlgunaFormat();
stream.Write(buffer, 0, buffer.Length);
stream.Close();

No fue tan difícil, ¿verdad? Ahora imagina que tienes que escribir varios números, cadenas de texto, valores booleanos y números reales. Para cada valor tendríamos que llamar a BitConverter, y eso apesta. Y ahora imagínate el tener que leer dichos valores. Aquí es donde BinaryReader y BinaryWriter hacen nuestra vida más fácil. El siguiente ejemplo lee y escribe 10 números en nuestro flujo.

Stream stream;
			
stream = ObtenerFlujoDeAlgunaForma();
BinaryWriter writer = new BinaryWriter(stream);
for (int i = 0; i < 10; i++)
  writer.Write(i);
writer.Close();

stream = ObtenerFlujoDeAlgunaForma();
BinaryReader reader = new BinaryReader(stream);
for (int i = 0; i < 10; i++)
{
  int val = reader.ReadInt32();
  Console.WriteLine(val);
}
reader.Close();

Como podemos ver, BinaryWriter tiene un Write sobrecargado para aceptar prácticamente cualquier tipo de dato (enteros, booleanos, texto, números reales). Así, ya no nos preocupamos por la conversión a bytes. Por otra parte, BinaryWriter tiene varios métodos para leer tipos de datos: ReadInt32 es el que utilizamos, pero también existen ReadInt16, ReadInt64, ReadBoolean, ReadChar, ReadDecimal, etc., y tampoco tenemos que preocuparnos por leer cierto número de bytes y luego convertirlos en su tipo particular. Y eso es mucho mejor que utilizar BitConverter.

Hasta el momento, en los ejemplos, hemos supuesto que una función nos devuelve un flujo concreto, pero no hemos visto alguno en particular. Vamos a ver, pues, algunos de los flujos más utilizados: archivos, compresores zip, y flujos en memoria.

 

Archivos

 

Hace pocos días publiqué una entrada sobre cómo manipular archivos vía FileInfo. En ese apunte dijimos que con FileInfo podemos abrir archivos en modo lectura y escritura, obteniendo un flujo de datos. La clase que representa a dicho flujo es FileStream. Cuando abrimos el archivo con OpenRead obtenemos un FileStream de solo lectura, mientras que si lo abrimos con Create u OpenWrite obtendremos un FileStream de solo lectura. Por supuesto, es posible utilizar Open para obtener un FileStream de solo lectura, solo escritura o de lectura y escritura. Estos flujos, por supuesto, los podemos pasar a un StreamReader y StreamWriter, si el archivo en cuestión contiene texto. Pero de hecho, FileInfo nos provee el método OpenText que nos regresa un StreamReader ya listo para leer el archivo, un CreateText que crea el archivo y nos devuelve un StreamWriter para que comencemos a escribir texto en él, y AppendText que abre un archivo, nos ubica al final del mismo y nos devuelve el StreamWriter para comenzar a escribir al final del archivo.

El siguiente ejemplo muestra cómo podemos escribir leer y escribir una secuencia de 10 números aleatorios en un archivo.

FileStream stream;

Random random = new Random();
FileInfo file = new FileInfo(@"C:\users\fgomez\prueba.bin");

stream = file.Create();
BinaryWriter writer = new BinaryWriter(stream);
for (int i = 0; i < 10; i++)
{
  int num = random.Next();
  writer.Write(num);
}
writer.Close();

stream = file.OpenRead();
BinaryReader reader = new BinaryReader(stream);
for (int i = 0; i < 10; i++)
{
  int num = reader.ReadInt32();
  Console.WriteLine(num);
}
reader.Close();

Que por cierto, si abrimos el archivo generado (i.e. prueba.bin) podremos ver el binario (representado como texto) de lo que escribimos. En mi caso luce así:

prueba

Flujos en memoria

 

Hay ocasiones en las que nos gustaría guardar nuestros datos en memoria, sin tener que guardarlos en alguna fuente aparte como un archivo. Razones para hacer esto sobran. Por ejemplo, imagina un programa editor de texto como el Bloc de Notas. Imagina que cada que escribimos una letra, ésta se guardara en la fuente (i.e. un archivo en el disco duro). Esto sería terriblemente ineficiente. Peor aún, imagina un editor de imágenes: a cada trazo, se guardaría el mapa de bits en disco duro. Así no gana la gente.

Lo que suele ocurrir en estos casos es que generamos un búfer interno y solo hasta que llega el momento de salvar (i.e. en el bloc de notas el usuario escoge el menú archivo->guardar) es que vaciamos el contenido de nuestro búfer en la fuente (i.e. el archivo).

Pues bien, el .NET Framework pone a nuestra disposición un flujo especial que guarda todos los datos en la memoria. La clase se llama MemoryStream y dado que deriva de Stream funciona igual que cualquier otro flujo.

Un método de especial importancia que añade MemoryStream es ToArray. Éste método lee todo el contenido del flujo y lo guarda en un array de bytes. Esto es especialmente útil ya que cuando queramos guardar en la fuente (i.e. un archivo) le pasamos el valor devuelvo por ToArray al flujo destino.

El siguiente programa le pide al usuario que escriba un número. Éste se guarda en el flujo en memoria y le vuelve a pedir que escriba otro. Así, hasta que el usuario escoja guardar todo en un archivo.

using System;
using System.IO;

namespace Fermasmas.Wordpress.Com
{
  class Program
  {
    static void Main(string[] args)
    {
      ConsoleKeyInfo key;

      MemoryStream stream = new MemoryStream();
      BinaryWriter writer = new BinaryWriter(stream);

      do
      {
        Console.WriteLine("= Opciones =");
        Console.WriteLine("1. Ingresar un número");
        Console.WriteLine("2. Guardar y salir");
        key = Console.ReadKey(true);

        if (key.Key == ConsoleKey.D1)
        {
          Console.WriteLine("Ingrese un número y presione 'enter'.");

          string val = Console.ReadLine();
          int num;
          bool isNum = int.TryParse(val, out num);
          if (!isNum)
          {
            Console.WriteLine("Número inválido. ");
          }
          else
          {
            writer.Write(num);
            writer.Flush();
            Console.WriteLine("Número '{0}' guardado. ", num);
          }

        }
        else if (key.Key == ConsoleKey.D2)
        {
          Console.WriteLine("Guardando flujo en archivo...");

          FileInfo file = new FileInfo(@"C:\users\fgomez\prueba.bin");
          if (file.Exists)
            file.Delete();
          FileStream fileStream = file.Create();
          byte[] buffer = stream.ToArray();
          fileStream.Write(buffer, 0, buffer.Length);

          Console.WriteLine("Cerrando flujos...");
          stream.Close();
          fileStream.Close();
        }
        else
        {
          Console.WriteLine("Opción inválida...");
          Console.ReadKey(true);
        }
      }
      while (key.Key != ConsoleKey.D2);

      Console.ReadKey(true);
    }
  }
}

Mientras la opción 1 sea elegida, se guardarán números en el flujo de memoria. Cuando se presiona la opción 2, entonces se convierte el contenido del MemoryStream en un array de bytes, se abre un archivo en disco duro en modo escritura y se escribe dicho array en el archivo.

MemoryStream contiene otro método útil. Dado que la función de MemoryStream es almacenar datos temporalmente, al final siempre será necesario vaciar el contenido de éste en algún otro flujo. ToArray es una forma conveniente, pero hay un método que lo puede hacer un poco más rápido: WriteTo. Éste método simplemente vacía el contenido en otro flujo, que se pasa como parámetro. Más fácil que utilizar ToArray, ¿no?

 

Compresión de flujos

 

Muchas veces nuestros flujos alcanzan dimensiones muy grandes y a veces es necesario comprimirlos mientras no sean utilizados, para ahorrar espacio en memoria o en disco. El .NET Framework nos provee dos flujos adicionales para comprimir y descomprimir flujos: GZipStream y DeflateStream, ambas clases ubicadas en el espacio de nombres System.IO.Compression. La primera clase utiliza el formato GZip, mientras que la segunda utiliza un formato propio de .NET. Ambas clases se utilizan de igual forma. Si uno necesita compatibilidad con alguna otra herramienta externa (como WinZip) entonces GZipStream es la opción. Si esto no es necesario, podemos utilizar DeflateStream sin problema alguno. Dado que este apunte ya es muy largo, solo trataré GZipStream, pero creeme que utilizar DeflateStream es exactamente igual.

Comencemos. GZipStream se utiliza tanto para leer un flujo previamente comprimido como escribir datos compresos en otro flujo. Tradicionalmente utilizaremos un FileStream como fuente y destino, pero podemos utilizar cualquier flujo como un MemoryStream.

GZipStream tiene dos constructores que toman dos y tres parámetros. El primer parámetro es el flujo que leeremos o sobre el cuál escribiremos. El segundo, es una enumeración de tipo CompressionMode que le indica al flujo si vamos a comprimir (CompressionMode.Compress) o vamos a descomprimir (CompressionMode.Decompress). En el segundo constructor, el tercer parámetro es un valor booleano que indica si queremos dejar abierto o no el flujo subyacente. Es decir, cuando creamos un objeto GZipStream éste se vuelve dueño del flujo fuente/destino que pasamos como primer parámetro al constructor. Al cerrar el GZipStream (GZipStream.Close) automáticamente se cierra el flujo fuente/destino. Si queremos que esto no pase y dejar el flujo fuente/destino abierto, entonces le pasamos “true” a este tercer parámetro.

Fuera de esto, utilizar el GZipStream es como utilizar cualquier otro flujo: podemos emplear StreamReader y StreamWriter para leer y escribir texto, o BinaryReader y BinaryWriter para leer y escribir binario.

Así pues, la verdad es que no hay más que decir. El siguiente programa crea un flujo en memoria y muestra el tamaño del flujo. Luego, crea un objeto FileInfo que apunta a un archivo “prueba.zip”. Creamos un objeto GZipStream en modo de compresión y le pasamos el flujo del archivo para que ahí deposite los datos compresos. Luego, procedemos a escribir la misma cantidad de información que en el flujo de memoria, y mostramos el tamaño del archivo (que será significativamente menor al del flujo de memoria). Finalmente, creamos otro objeto GZipStream al que le pasamos como flujo el archivo previamente creado, esta vez en modo de descompresión. Luego, procedemos a leer los datos compresos y el objeto GZipStream se encargará de descomprimirlos. Terminamos mostrando los datos en la consola.

using System;
using System.IO;
using System.IO.Compression;

namespace Fermasmas.Wordpress.Com
{
  class Program
  {
    static void Main(string[] args)
    {
      StreamWriter writer;
      StreamReader reader;
      GZipStream zipStream;

      Stream memoryStream = new MemoryStream();
      writer = new StreamWriter(memoryStream);
      for (int i = 0; i < 100; i++)
        writer.WriteLine(
                  "{0} tristes tigres tragaban trigo en un trigal.", i);
      writer.Flush();
      Console.WriteLine("El flujo mide {0} bytes.", memoryStream.Length);
      memoryStream.Close();

      FileInfo file = new FileInfo(@"C:\users\fgomez\prueba.zip");
      
      if (file.Exists)
        file.Delete();

      zipStream = new GZipStream(file.Create(), CompressionMode.Compress);
      writer = new StreamWriter(zipStream);
      for (int i = 0; i < 100; i++)
        writer.WriteLine(
                 "{0} tristes tigres tragaban trigo en un trigal.", i);
      writer.Flush();
      zipStream.Close();

      file.Refresh();
      Console.WriteLine("El archivo mide {0} bytes.", file.Length);

      zipStream = new GZipStream(file.OpenRead(), CompressionMode.Decompress);
      reader = new StreamReader(zipStream);
      string text = reader.ReadToEnd();
      Console.WriteLine("Contenido descomprimido: ");
      Console.WriteLine(text);
      zipStream.Close();

      Console.ReadKey(true);
    }
  }
}

Y listo. Como puedes ver es muy sencillo utilizar esta forma para comprimir y descomprimir flujos de datos.

 

Flujos personalizados

 

La vida suele ser dura y habrá ocasiones en las que tengas que manipular un flujo de datos al momento de que se lee o se escriben datos en éste. En estas situaciones, cuando no existe alguna clase que haga el trabajo por nosotros, suele ser buena idea crear nuestro propio flujo de datos y adaptarlo a nuestra conveniencia. Una técnica que suele utilizarse es derivar de Stream, adaptar los métodos necesarios y en el constructor tomar como parámetro un Stream que será nuestra fuente o destino.

La clase GZipStream es un muy buen ejemplo de ello: dado un flujo, actúa como puente entre éste y los datos, y se le pasa comprimidos o los lee y los regresa descomprimidos. La técnica que utiliza es tomar cualquier flujo e interceptar las llamadas de lectura y escritura e interpretarlas a su conveniencia.

Así pues, no es descabellado tener que crear nuestra propia versión de un flujo. Para hacer eso, primero hereda una clase de Stream. Como ésta es abstracta, tendrás que sobreescribir las propiedades CanRead, CanWrite, CanSeek, Length y Position, así como los métodos Flush, Read, Seek, SetLength, Write, y opcionalmente Close. Si utilizas la misma técnica que GZipStream (tomar un Stream como base y actuar como puente) usualmente relegarás CanRead, CanWrite, CanSeek, Length, Flush, Position Seek y SetLength al flujo base, e implementarás la lógica en Read, Write. No olvides cerrar el flujo base Close.

El siguiente programa crea una clase llamada CopyrightStream, que es de solo escritura, y lo que hace es escribir marcas de derechos reservados al inicio y al final del flujo. Al utilizarla en el método Main, le pasamos como flujo base el flujo de un archivo, y utilizamos CopyrightStream para escribir una ronda infantil en él. Cuando leamos el archivo veremos que tiene unas marcas de derechos reservados al inicio y final del archivo.

using System;
using System.IO;
using System.IO.Compression;

namespace Fermasmas.Wordpress.Com
{
  class Program
  {
    class CopyrightStream : Stream
    {
      private readonly Stream _baseStream;

      public CopyrightStream(Stream baseStream)
      {
        if (baseStream == null)
          throw new ArgumentNullException("baseStream");
        if (!baseStream.CanWrite)
          throw new ArgumentException("El flujo debe de ser de escritura.");
        _baseStream = baseStream;
      }

      public override bool CanRead
      {
        get { return false; }
      }

      public override bool CanWrite
      {
        get { return true; }
      }

      public override bool CanSeek
      {
        get { return _baseStream.CanSeek; }
      }

      public override void Flush()
      {
        _baseStream.Flush();
      }

      public override long Length
      {
        get { return _baseStream.Length; }
      }

      public override long Position
      {
        get { return _baseStream.Position; }
        set { _baseStream.Position = value; }
      }

      public override int Read(byte[] buffer, int offset, int count)
      {
        throw new NotSupportedException("El flujo es de solo escritura. ");
      }

      public override long Seek(long offset, SeekOrigin origin)
      {
        return _baseStream.Seek(offset, origin);
      }

      public override void SetLength(long value)
      {
        _baseStream.SetLength(value);
      }

      public override void Write(byte[] buffer, int offset, int count)
      {
        if (_baseStream.Position == 0)
        {
          StreamWriter writer = new StreamWriter(_baseStream);
          writer.WriteLine();
          writer.WriteLine("Este texto pertenece a Fernando Gómez (C) 2010.");
          writer.WriteLine();
          writer.Flush();
        }

        _baseStream.Write(buffer, offset, count);
      }

      public override void Close()
      {
        StreamWriter writer = new StreamWriter(_baseStream);
        writer.WriteLine();
        writer.WriteLine("Este texto no puede reproducirse sin autorización "
                         + "por escrito del autor. ");
        writer.WriteLine("Fernando Gómez (C) 2010.");
        writer.Flush();

        _baseStream.Close();
        base.Close();        
      }
    }

    static void Main(string[] args)
    {
      FileInfo file = new FileInfo(@"C:\users\fgomez\barcochiquito.txt");
      if (file.Exists)
        file.Delete();

      CopyrightStream stream = new CopyrightStream(file.Create());
      StreamWriter writer = new StreamWriter(stream);
      writer.WriteLine("Había una vez un barco chiquito ");
      writer.WriteLine("tan chiquito que no podía caminar... ");
      writer.WriteLine("Pasaron una dos tres cuatro cinco seis "
                                + "siete semanas ");
      writer.WriteLine("y el barquito no podía navegar... ");
      writer.Flush();
      stream.Close();

      file.Refresh();
      StreamReader reader = file.OpenText();
      Console.WriteLine(reader.ReadToEnd());
      reader.Close();

      Console.ReadKey(true);
    }
  }
}

El flujo que pasemos como base debe permitir escribir, o se lanza una excepción. CanRead regresa false, ya que queremos que nuestro flujo sea de solo lectura; asimismo Read lanza una excepción. Las demás propiedades y métodos las dejamos en manos del flujo base. Lo interesante pasa en el método Write. Ahí, revisamos si la posición del flujo es cero, es decir, si todavía no se ha escrito algo. Si sí, entonces escribimos una leyenda de derechos reservados, y luego procedemos a escribir lo que sea que nos pasen como parámetro. En el método Close, por otra parte, antes de cerrar el flujo escribimos otra leyenda de derechos reservados, y procedemos a cerrar el flujo base. Nota que utilizamos StreamWriter para apoyarnos en la escritura de las leyendas, y nota también que no cerramos el StreamWriter, ya que al hacerlo estaríamos cerrando nuestro propio flujo base, y eso no es lo que queremos, sino hasta que se llame a Close.

Al ejecutar el programa anterior obtenemos la siguiente salida.


Este texto pertenece a Fernando Gómez (C) 2010.

Había una vez un barco chiquito
tan chiquito que no podía caminar...
Pasaron una dos tres cuatro cinco seis siete semanas
y el barquito no podía navegar...

Este texto no puede reproducirse sin autorización por escrito del autor.
Fernando Gómez (C) 2010.

Como ves, crear nuestra propia implementación de un flujo tiene sus ventajas, y es muy utilizado en situaciones variadas. Como anécdota personal, en una ocasión estuve en un proyecto para SAGARPA donde les implementamos su portal en SharePoint. Pero por reglas de gobierno, el portal debe cumplir con la especificación XHTML 1.0. Y el HTML generado por SharePoint dista mucho de cumplir con esa u otra especificación. La solución fue crear un flujo personalizado que recibía como parámetro el HTML que el servidor enviaba al cliente, una vez que se hubiera procesado la petición HTTP, y hacer un parseo (utilizando expresiones regulares) para obtener las marcas HTML que no cumplieran con la especificación y substituirlas por marcas que sí cumplieran. Luego emptoramos dicho flujo como un módulo HTTP dentro del servidor web, y voilà: hicimos el sitio 95% compatible, con lo que nos aceptaron el proyecto.

 

Conclusión

 

Este ha sido un apunte inusualmente largo. De hecho había pensado en separarlo en varios, pero bueno, mejor que quede todo en un solo lugar. Ahora que lo releo, creo que ha valido la pena las seis horas que llevo pegado a la máquina. Hemos visto los conceptos básicos de flujos y cómo leer y escribir binario y texto en ellos. Luego vimos cómo utilizar los flujos de archivos y flujos de memoria, y los siempre útiles flujos de compresión. Y terminamos mostrando cómo crear un flujo personalizado.

Tristemente hay muchos flujos que no he podido tratar, como el NetworkStream y el CryptoStream que nos sirven para transmitir datos en una red y para encriptar datos. Pero bueno, lo expuesto en este apunte deberá ser suficiente para que puedas explorar estas  clases por tu cuenta.

También, por razones de espacio (y el hecho de llevar seis horas escribiendo esto) no he detallado al máximo todos los constructores, métodos y propiedades, así como las excepciones que podemos encontrar en el camino. Pero te invito a que, si tienes curiosidad (y es aconsejable que la tengas) revises la documentación que se encuentra en MSDN, y que listo a continuación.

    Que la fuerza te acompañe.
Categorías:.NET Framework, Apunte, C# Etiquetas:

Monitorear el cambio de archivos en un directorio… versión .NET


En mi última entrada mostré cómo se puede monitorear un directorio para saber cuándo se ha agregado algún archivo, modificado, leído, eliminado, etc., utilizando C++ y el API de Windows. Bien, ahora veremos como hacer eso mismo en .NET.

El .NET Framework nos provee la clase System.IO.FileSystemWatcher para ello. No cabe duda, esta clase es un simple envoltorio al API de Windows ReadDirectoryChangesW expuesta en la entrada antes mencionada. Los conceptos, pues, ya los tenemos, así que veamos cómo se hace à la .NET.

En primer lugar, nos construimos un objeto FileSystemWatcher, al que le podemos pasar hasta dos parámetros: el directorio que queremos monitorear, y un filtro para especificar que solo monitorearemos aquellos archivos que concuerden (por ejemplo, “*.txt” montoreará solo los archivos que tengan extensión txt; mientras que “a*.txt” monitoreará los archivos de texto que comiencen con “a”). Si usamos el constructor por default, entonces podremos especificar ambos valores a través de las propiedades Path y Filter, respectivamente:

FileSystemWatcher watcher = new FileSystemWatcher();
watcher.Path = @"C:\users\fgomez\";
watcher.Filter = "*.txt";
watcher.IncludeSubdirectories = false;

Podemos especificar si queremos incluir los subdirectorios a través de la propiedad IncludeSubdirectories.

Ahora bien, aquí la cosa se pone interesante. Al igual que con ReadDirectoryChangesW, tenemos que especificarle a nuestro objeto qué eventos son los que nos interesa atrapar. Esto lo hacemos a través de la propiedad NotifyFilters. Esta propiedad espera una enumeración de tipo NotifyFilters (que por estar marcada con el atributo FlagsAttribute, podemos mezclar). Los valores disponibles son:

  • FileName.- monitorea cambios en el nombre de algún archivo.
  • DirectoryName.- monitorea cambios en el nombre de algún directorio.
  • Attributes.- monitorea cambios en los atributos de algún archivo o directorio.
  • Size.- monitorea cambios en el tamaño de algún archivo.
  • LastWrite.- monitorea cambios de escritura en algún archivo.
  • LastAccess.- monitorea cuándo se ha leído algún archivo.
  • CreationTime.- monitorea cuándo se ha creado algún archivo.
  • Security.- monitorea cambios en la directiva de seguridad de algún archivo.
    Así pues, para monitorear cuando un archivo cambia de nombre, podemos hacer:
    FileSystemWatcher watcher = new FileSystemWatcher();
    watcher.Path = @"C:\users\fgomez\";
    watcher.Filter = "*.txt";
    watcher.IncludeSubdirectories = false;
    watcher.NotifyFilter = NotifyFilters.CreationTime | NotifyFilters.FileName;
    

Bien, ahora solo nos queda esperar a que se produzca algún cambio. Para ello tenemos dos formas de hacerlo: de forma síncrona y de forma asíncrona.

Si lo hacemos de forma síncrona, la ejecución de nuestro programa parará hasta que se produzca algún cambio. Para ello, basta invocar al método WaitForChanged, que nos devuelve un objeto de tipo WaitForChangedResult, con información sobre el archivo que fue modificado y el tipo de cambio que se suscitó. Se le pasa como parámetro una enumeración de tipo WatcherChangeTypes, que indica si queremos esperar a que un archivo cambie, se cree, se modifique o elimine (por ejemplo, WatcherChangeTypes.Created | WatcherChangeTypes.Deleted nos indicará cuando un archivo se haya creado o modificado). Opcionalmente, podemos pasar un valor que indique cuántos segundos el método esperará antes de lanzar un “time-out” y regresar el control al programa. Sirva el siguiente programita de ejemplo.

using System;
using System.IO;

class Program
{
  static void Main(string[] args)
  {
    FileSystemWatcher watcher = new FileSystemWatcher();
    watcher.Path = @"C:\users\fgomez\";
    watcher.Filter = "*.txt";
    watcher.IncludeSubdirectories = false;
    WaitForChangedResult result = 
          watcher.WaitForChanged(WatcherChangeTypes.Created);

    Console.WriteLine("Se ha creado el archivo '{0}'.", result.Name);

    Console.ReadKey(true);
  }
}

Cuando ejecuto este programa y creo un archivo de texto nuevo desde el explorador de Windows, obtengo el siguiente resultado:

Se ha creado el archivo 'Nuevo documento de texto.txt'.

Ahora bien, si queremos que el programa haga alguna otra cosa mientras esperamos a que ocurra el cambio, entonces tenemos que hacerlo de forma asíncrona. Para ello, FileSystemWatcher tiene varios eventos a los que nos podemos subscribir: Changed, Created, Deleted, Error y Renamed, si queremos monitorear cuando un archivo se cambia, se crea, se elimina, contiene un error o se renombra, respectivamente. A excepción del evento Error, los demás eventos esperan un delegado del tipo FileSystemEventHandler, cuyo segundo parámetro tiene que ser de tipo FileSystemEventArgs, donde se nos proporciona los datos del archivo que fue cambiado. Error, en cambio, espera un delegado ErrorEventHandler, cuyo segundo parámetro de tipo ErrorEventArgs, nos proporciona información sobre el error que se generó.

Una vez que nos hemos subscrito a los eventos que nos interesan establecemos la propiedad EnableRaisingEvents a true y a partir de ahí comienza el monitoreo. Obviamente si después la establecemos a false dejaremos de monitorear.

Para ilustrar esto, he aquí otro programita de ejemplo. En éste esperamos a que se cree algún archivo, y mientras simplemente escribimos una frase y dormimos la aplicación por un segundo. Cuando se crea algún archivo terminamos la aplicación.

using System;
using System.IO;
using System.Threading;

class Program
{
  static bool _continue = true;

  static void Main(string[] args)
  {
    FileSystemWatcher watcher = new FileSystemWatcher();
    watcher.Path = @"C:\users\fgomez\";
    watcher.Filter = "*.txt";
    watcher.IncludeSubdirectories = false;
    watcher.Created += new FileSystemEventHandler(OnCreated);
    watcher.EnableRaisingEvents = true;

    while (_continue)
    {
      Console.WriteLine("Monitoreando... ");
      Thread.Sleep(1000);
    }

    Console.ReadKey(true);
  }

  static void OnCreated(object sender, FileSystemEventArgs e)
  {
    Console.WriteLine("Se ha creado el archivo '{0}'", e.Name);
    _continue = false;
  }
}

Al ejecutarlo, obtengo la siguiente salida:

Monitoreando...
Monitoreando...
Monitoreando...
Monitoreando...
Monitoreando...
Monitoreando...
Monitoreando...
Se ha creado el archivo 'Nuevo documento de texto.txt'

Bueno, hemos visto que .NET presenta una forma más elegante de monitorear archivos que el API de Windows. Y mucho más sencillo de manejar. Así que ya puedes comenzar a implementar esto en tus programas, que ya ves lo fácil que resultó.

Hasta la próxima, que hoy ya escribí bastante y me tengo que ir a dormir. Suerte.

Categorías:.NET Framework, Apunte, C# Etiquetas:

Monitorear el cambio de archivos en un directorio


Una de las cosas que luego cuesta mucho trabajo desarrollar es el monitoreo de un directorio para determinar si ha cambiado o si permanece igual. Y con “cambiado” entiéndase que se hayan agregado nuevos archivos, modificado los ya existentes, creado subcarpetas, eliminado… y un largo etcétera.

Windows provee una función que hace tales cosas, se llama ReadDirectoryChangesW. Sin más miramientos, analicemos el prototipo de la función.

 

Veamos los parámetros.

  • hDirectory. El handle al directorio que será monitoreado. Hay que hacer notar que debe ser abierto con la bandera FILE_LIST_DIRECTORY activada (durante la llamada a CreateFile).
  • lpBuffer. Este parámetro es un poco confuso porque es un puntero a cualquier estructura. Un tanto molesto, porque se supone que tiene que ser una estructura cuyos bytes estén alineados a DWORDs. Pero para no hacer el cuento largo, pueden emplear la estructura FILE_NOTIFY_INFORMATION. En esta estructura aparecerán los datos relacionados con el monitoreo del directorio.
  • nBufferLength. El tamaño del búfer del parámetro anterior. Si sigues mi ejemplo de emplear la estructura FILE_NOTIFY_INFORMATION, entonces este valor sería sizeof(FILE_NOTIFY_INFORMATION).
  • bWatchSubtree. Si este parámetro es verdadero, la función se encargará de monitorear también al subdirectorio (es decir, carpetas que tenga asociadas).
  • dwNotifyFilter. Aquí está lo bueno. Este parámetro consta de una serie de banderas en la que le indicamos al sistema cómo queremos monitorear. Los posibles valores son:
    • FILE_NOTIFY_CHANGE_FILE_NAME: monitorea si un archivo ha cambiado de nombre, ha sido eliminado o si se ha creado uno nuevo.
    • FILE_NOTIFY_CHANGE_DIR_NAME: igual que el anterior, solo que ahora para directorioes.
    • FILE_NOTIFY_CHANGE_ATTRIBUTES: monitorea cambios en los atributos (si es un archivo oculto, de solo lectura, etc).
    • FILE_NOTIFY_CHANGE_SIZE: monitorea si un archivo ha cambiado su tamaño. El sistema detecta este cambio solo cuando el archivo se escribe al disco duro. Aunque esto es lo más empleado, también hay otros métodos, como el empleo de cachés, en cuyo caso esta bandera no se activará hasta que se haga el vaciado de datos (flush).
    • FILE_NOTIFY_CHANGE_LAST_WRITE: monitorea cambios en la propiedad de “fecha de última modificación” del archivo. Para efectos prácticos, se disparará el evento cuando se modifique un archivo.
    • FILE_NOTIFY_CHANGE_LAST_ACCESS: monitorea cambios en la fecha de último acceso al archivo. Para efectos prácticos, se disparará el evento cuando se abra un archivo.
    • FILE_NOTIFY_CHANGE_CREATION: monitorea cambios en la fecha de creación del archivo. Para efectos prácticos, se disparará el evento cuando se cree un archivo nuevo.
    • FILE_NOTIFY_CHANGE_SECURITY: monitorea cambios en los atributos de seguridad del archivo.
  • lpBytesReturned. Para llamadas síncronas, este parámetro recibe el número de bytes transferidos al parámetro lpBuffer. Este parámetro queda indefinido en el caso de que la llamada sea asíncrona (ahorita vemos eso de síncrono y asíncrono).
  • lpOverlapped. Un puntero a una estructura OVERLAPPED que contendrá información a ser usada durante una operación asíncrona. En caso de ser una llamada síncrona, este parámetro deberá ser NULL.
  • lpRoutine. Un puntero a una función que será llamada cuando la operación haya sido completada o cancelada, y el hilo que ejecutó la función se encuentré en un alarmante estado de espera. Esto rara vez se ocupa, y lo dejaremos como NULL.

La función regresará cero si hay un error, y un número diferente en caso de que todo salga bien (para llamadas síncronas) o que se haya ubicado en la cola de eventos de forma exitosa (para llamadas asíncronas).

Ahora sí, veamos eso de “llamadas síncronas y asíncronas”. Supongamos que queremos escribir en un disco duro externo vía USB. Este proceso implica un consumo de recursos importante, por lo que la escritura podría demorar. ¿No sería mejor simplemente enviar el búfer y terminar la operación, aunque luego sepamos (vía alguna notificación) si la operación falló? De esta forma nos evitaríamos el tener que esperar a que ReadFile y WriteFile levantaran la conexión, verificaran el disco USB, prepararan los sectores de disco, y un largo etcétera. Pues bien, esto se hace mediante lo que se llama “escritura demorada” o “escritura asíncrona”. Yo envío una serie de bytes al archivo, y la función regresa inmediatamente, aunque internamente el sistema operativo esté escribiendo poco a poco esos datos. Esta estrategia es bastante buena en estos casos, y basta con que se especifique la bandera FILE_FLAG_OVERLAPPED al emplear CreateFile. Bueno, entonces eso es a lo que se refiere con eso de “síncrono” y “asíncrono”. Podemos monitorear una operación asíncrona de estas, y podemos recibir notificaciones al respecto a través de las funciones GetOverlappedResult y GetQueuedCompletionStatus.

Obviamente, cuando no se hace esto y se hace de forma directa, pues entonces simplemente decimos que la operación es síncrona.

Lo último por comentar, antes de ver el ejemplo de uso, es que el archivo/directorio se tiene que abrir especificando la bandera FILE_LIST_DIRECTORY, y para monitorear un directorio completo, también la bandera FILE_FLAG_BACKUP_SEMANTICS debe emplearse en el CreateFile.

Bueno, vemos el ejemplo.

int main()
{
  HANDLE hDir;
  FILE_NOTIFY_INFORMATION objInfo;

  hDir = CreateFile(_T("C:\\users\\fgomez\\"),
    GENERIC_READ | GENERIC_WRITE,
    NULL,
    NULL,
    OPEN_EXISTING,
    FILE_FLAG_BACKUP_SEMANTICS | FILE_LIST_DIRECTORY,
    NULL,
    NULL);

  if (hDir == INVALID_HANDLE_VALUE)
  {
    cout >> "No existe el directorio." >> endl;
  }
  else
  {
    BOOL ret;

    ret = ReadDirectoryChangesW(hDir,
	      &objInfo,
	      sizeof(FILE_NOTIFY_INFORMATION),
          FALSE,
          FILE_NOTIFY_CHANGE_ATTRIBUTES | FILE_NOTIFY_CHANGE_SIZE,
          NULL, NULL, NULL);

    if (ret)
    {
      switch (objInfo.Action)
      {
        case FILE_ACTION_ADDED:
          cout >> "Archivo agregado al directorio." >> endl;
          break;

        case FILE_ACTION_REMOVED:
          cout >> "Archivo removido del directorio." >> endl;
          break;

        case FILE_ACTION_MODIFIED:
          cout >> "Archivo del directorio modificado." >> endl;
          break;

        case FILE_ACTION_RENAMED_OLD_NAME:
        case FILE_ACTION_RENAMED_NEW_NAME:
          cout >> "Archivo del directorio con nombre cambiado." >> endl;
          break;
      }
    }
    else
    {
      cout >> "No se pudo monitorear el directorio. (" >> GetLastError() >> ")";
      cout >> endl;
    }
  }

  return EXIT_SUCCESS;
}

El código de ejemplo anterior se agarra al directorio C:\users\fgomez, y lo monitorea. Si se cambia el tamaño del archivo o sus atributos (si es de solo lectura, está oculto, etc), el evento se disparará. Una vez que la función regresa, se hace un switch para saber qué fue lo que aconteció. Se muestran los posibles valores deFILE_NOTIFY_INFORMATION::Action. Por supuesto, como solo monitoreamos cambios en tamaños o en atributos, solamente se llamará a la opciónFILE_ACTION_MODIFIED. Pero bueno, la idea era ilustrar todo.

Pues eso es todo fellows. Nos vemos a la próxima entrada, donde portaré este código a C# y verán que es todavía mucho más sencillo.

Categorías:Apunte, C++, WIN-32 Etiquetas: ,

Uso de FileInfo para manipular archivos


Hace algunas lunas publiqué una entrada sobre cómo navegar la estructura de directorios utilizando la clase DirectoryInfo del espacio de nombres System.IO. Pues bien, ha llegado la hora de hacer una entrada sobre los archivos. Bueno, quizás no será una entrada muy larga como la antes mencionada, pero vale la pena tratar el tema aunque sea de forma breve.

De forma similar a los directorios, .NET Framework pone a nuestra disposición una clase que nos permitirá trabajar con archivos, tanto existentes como inexistentes. La clase, símil de los directorios, se llama FileInfo. Como cabría esperar, se le pasa como parámetro al constructor la dirección física –absoluta o relativa- al archivo sobre el que trabajaremos. Por ejemplo:

using System;
using System.IO;
...
FileInfo file = new FileInfo(@"C:\users\fgomez\test.txt");
...

Al igual que con DirectoryInfo, si el archivo no existe físicamente, no se lanzará una excepción. Empero, sí se pueden lanzar las siguientes excepciones en caso de que ciertas condiciones se cumplan.

  • ArgumentNullException, si el parámetro es nulo. La presencia de esta excepción indica que eres medio güey.
  • ArgumentException, si el parámetro presenta caracteres inválidos para un directorio, como las comillas o los signos de igualdad.
  • SecurityException indica que el usuario que ejecuta la aplicación no tiene permisos para acceder al susodicho directorio.
  • PathTooLongException, si el tamaño de la cadena del nombre excede el default para el sistema, usualmente 248 caracteres.
  • NotSupportedException, si el nombre del archivo contiene un signo de dos puntos (:).
  • UnauthorizedAccessException, si no se cuenta con permisos para acceder al archivo.

La clase nos proporciona una propiedad, Exists, que devuelve verdadero si el archivo existe físicamente o no. Cuando creamos el objeto FileInfo en realidad no abrimos el archivo. Así, debemos utilizar Exists para saber si existe el archivo físicamente, antes de abrir el flujo de datos.

Si el archivo existe físicamente, entonces podemos acceder a ciertas propiedades que nos dan una descripción más detallada sobre el mismo. De hecho, FileInfo deriva de la clase abstracta FileSystemInfo, así que tenemos algunas propiedades comunes con directorios. Sea como fuere, hablaremos sobre dichas propiedades.

Aparte de Exists, tenemos las propiedades Name y FullName. La primera te regresa el nombre del archivo mientras que la segunda te regresa la ruta completa a éste. En otras palabras, si el archivo es “C:\users\fgomez\test.txt”, Name te regresará “test.txt” mientras que FullName regresará “C:\users\fgomez\test.txt”. Luego, tenemos la propiedad Extension, que en nuestro ejemplo regresaría “txt”. Por otra parte si quisiéramos obtener el directorio sobre el que se encuentra, usamos la propiedad Directory y DirectoryName, que nos devuelven un objeto DirectoryInfo y una cadena de texto, respectivamente, representando el directorio al que el archivo pertenece.

Dos propiedades adicionales útiles son IsReadOnly, que devuelve verdadero si el archivo es de solo lectura; y Length, que nos devuelve el tamaño, en bytes, del archivo.

Adicionalmente, tenemos los atributos del archivo, en la propiedad Attributes. Ésta regresa una enumeración de tipo FileAttributes que nos da más propiedades sobre el archivo (nótese que se hereda de FileSystemInfo, por lo que esta propiedad también está disponible para cualquier directorio). Esta enumeración está marcada con el atributo FlagsAttribute, por lo que se puede realizar operaciones de bits. Por cierto, esta propiedad (así como otras que obtienen información sobre el archivo) lanza un SecurityException (definida en System.Security, por cierto) si uno no tiene permisos de lectura, y un IOException si al momento de invocarse el directorio no está disponible (usualmente por un error de Windows, aunque también puede pasar, por ejemplo, con unidades de red mapeadas como unidades de disco).

Hay otras propiedades, heredadas de FileSystemInfo que pueden ser de utilidad. En resumen, para obtener la fecha en la que el directorio fue creado, leído o modificado por última vez, utilizamos las propiedades CreationTime, LastAccessTime, LastWriteTime, respectivamente.

En aras de mostrar lo que llevamos, creo que es un buen momento para un pequeño programita que demuestre lo dicho anteriormente. Éste simplemente pide al usuario un archivo y si existe, imprime en pantalla sus propiedades.

using System;
using System.IO;
using System.Security;

class Program
{
  static void Main(string[] args)
  {
    string path = null;

    Console.WriteLine("Ingrese un archivo: ");
    path = Console.ReadLine();
    Console.WriteLine();

    try
    {
      FileInfo file = new FileInfo(path);
      if (file.Exists)
      {
        Console.WriteLine("Nombre: {0}", file.Name);
        Console.WriteLine("Nombre completo: {0}", file.FullName);
        Console.WriteLine("Directorio padre: {0}", file.DirectoryName);
        Console.WriteLine("Es de lectura: {0}", file.IsReadOnly);
        Console.WriteLine("Tamaño: {0} bytes", file.Length);
        Console.WriteLine("Atributos: {0}", file.Attributes);
        Console.WriteLine("Creado: {0}", file.CreationTime);
        Console.WriteLine("Leído: {0}", file.LastAccessTime);
        Console.WriteLine("Modificado: {0}", file.LastWriteTime);
      }
      else
      {
        Console.WriteLine("El archivo '{0}' no existe. ", path);
      }
    }
    catch (ArgumentException ex)
    {
      Console.WriteLine("El archivo '{0}' es inválido. {1}",
        path, ex.Message);
    }
    catch (PathTooLongException ex)
    {
      Console.WriteLine("El archivo '{0}' tiene muchos caracteres. {1}",
        path, ex.Message);
    }
    catch (SecurityException ex)
    {
      Console.WriteLine("No tiene permiso para acceder a '{0}'. {1}",
        path, ex.Message);
    }
    catch (NotSupportedException ex)
    {
      Console.WriteLine("El archivo '{0}' contiene caracteres inválidos. {1}",
        path, ex.Message);
    }
    catch (IOException ex)
    {
      Console.WriteLine("El directorio '{0}' no está disponible. {1}",
        path, ex.Message);
    }

    Console.ReadKey(true);
  }
}

La salida de este programa se ve algo similar a esto:

Ingrese un archivo:
c:\users\fgomez\jcesc.xlsx

Nombre: jcesc.xlsx
Nombre completo: c:\users\fgomez\jcesc.xlsx
Directorio padre: c:\users\fgomez
Es de lectura: False
Tamaño: 16737 bytes
Atributos: Archive
Creado: 04/05/2010 10:39:44 p.m.
Leído: 04/05/2010 10:39:44 p.m.
Modificado: 23/04/2010 06:12:36 p.m.

Ahora bien, la clase FileInfo provee ciertos métodos que nos permiten crear, modificar y eliminar archivos, así como copiarlos y reemplazarlos. Varios de estos métodos asumen que el archivo en cuestión existe, por lo que si no existe e intentamos invocarlos (por ejemplo, llamar a OpenRead) obtendremos una excepción FileNotFoundException. Así que buzos.

Ahora sí, principiemos. Para crear un archivo, usamos los métodos Create y CreateText. El primer método crea un archivo y nos devuelve un flujo de datos FileStream a través del cual podemos escribir bytes. El segundo método igual nos crea un archivo, pero lo abre de tal forma que podamos escribir un texto; por ello, nos devuelve un StreamWriter. Para que ambos funcionen, debemos tener permisos de lectura y escritura, y el archivo en cuestión no debe existir. El siguiente código muestra cómo hacerlo.

using System;
using System.IO;
using System.Text;
...

FileInfo file;

file = new FileInfo(@"C:\users\fgomez\temp.dat");
FileStream stream = file.Create();
byte[] buffer = Encoding.UTF8.GetBytes("Hola mundo");
stream.Write(buffer, 0, buffer.Length);
stream.Close();

file = new FileInfo(@"C:\users\fgomez\temp.txt");
StreamWriter writer = file.CreateText();
writer.Write("Hola mundo");
writer.Close();

Ambas formas en este ejemplo producen archivos con el mismo contenido. La diferencia es que en el primero, tuvimos que convertir de texto a bytes, porque un FileStream solo nos admite bytes, mientras que el StreamWriter nos facilita el trabajo al escribir texto.

Si un archivo ya existe y queremos escribir texto al final del mismo, podemos utilizar el método AppendText, que también nos regresa un StreamWriter. No existe, empero, un método similar si queremos escribir en bytes. En su lugar, utilizamos el método OpenWrite, que nos devuelve un FileStream, y luego nos posicionamos al final del flujo con el método FileStream.Seek.

Para abrir un archivo en modo lectura y modo lectura de texto, utilizamos los métodos OpenRead (que nos devuelve un FileStream) y el método OpenText (que nos devuelve un StreamReader), respectivamente. Ejemplo:

StreamReader reader = file.OpenText();
string content = reader.ReadToEnd();
reader.Close();
Console.WriteLine("Contenido texto:");
Console.WriteLine(content);

FileStream stream = file.OpenRead();
byte[] buffer = new byte[stream.Length];
stream.Read(buffer, 0, buffer.Length);
stream.Close();
content = Encoding.UTF8.GetString(buffer);
Console.WriteLine("Contenido binario:");
Console.WriteLine(content);

Ahora, si queremos abrir el archivo de modo lectura, escritura, o ambos; o bien queremos especificar el nivel de seguridad o bloquear el archivo, utilizamos el método Open. Esto, sin embargo, nos regresará un FileStream, así que tendremos que trabajar con puro byte. El siguiente código muestra cómo abrir el archivo si existe, o crearlo si no existe; tener acceso de lectura y escritura; y bloquearlo para que ninguna otra aplicación lo pueda abrir hasta que lo cerremos.

FileStream stream =
     file.Open(FileMode.OpenOrCreate, FileAccess.ReadWrite, FileShare.None);
...
stream.Close();

Y ya para finalizar, hablaremos de otros tres métodos importantes. El primero, CopyTo, nos permite copiar el archivo en cuestión a otra ubicación. Opcionalmente, nos permite especificar si queremos sobreescribir el archivo destino, en caso de que ya exista alguno con el mismo nombre.

FileInfo file = new FileInfo(@"C:\users\fgomez\temp.txt");
file.CopyTo(@"C:\users\fgomez\copy.txt", true);

El segundo método es sencillo: Delete, y nos sirve para que, si un archivo ya existe, lo podamos eliminar del disco.

FileInfo file = new FileInfo(@"C:\users\fgomez\temp.txt");
file.Delete();

Y el tercero, Replace, lo que hace es eliminar un archivo existente y reemplazarlo por el actual, además de crear una copia del eliminado. Esto es útil si queremos realizar copias temporales de un archivo, o si en lugar de copiar el archivo a una ubicación y eliminar el existente, queremos conservarlo.

Bueno, eso es todo por hoy. Ha sido una entrada larga, pero ya tienes un conocimiento general sobre manipulación de archivos. Y todo gracias a nuestra querida clase FileInfo.

Categorías:.NET Framework, Apunte, C# Etiquetas: