Inicio > .NET Framework, Apunte, C# > Todo sobre flujos de datos

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.
Anuncios
Categorías:.NET Framework, Apunte, C# Etiquetas:
  1. Rober
    agosto 31, 2010 en 4:41 pm

    Estuvo buenisimo

  2. uno
    mayo 29, 2012 en 12:11 pm

    Muy bueno, gracias!

  3. Víctor
    enero 16, 2013 en 11:55 am

    Muy bueno! me ayudo muchisimo.

    • enero 16, 2013 en 12:02 pm

      Gracias Víctor! Hay otros artículos de flujos, como flujo sobre la red o una nuevaclase para crear archivos zip!

  4. Toluco
    febrero 5, 2013 en 8:28 am

    Buena introduccion de flujos, Muchas gracias.

  1. septiembre 28, 2010 en 8:39 am
  2. noviembre 15, 2012 en 1:00 am

Responder

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

Logo de WordPress.com

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

Imagen de Twitter

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

Foto de Facebook

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

Google+ photo

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

Conectando a %s