Desenvolvimento - C#

.Net: Trabalhando com Streams

Este artigo tem como principal objetivo explanar a utilização de streams demonstrando as vantagens e desvantagens de trabalhar com métodos síncronos e assíncronos das mesmas.

por Bruno Spinelli Dantas



Uma stream pode ser definida como uma abstração entre o programador e o dispositivo que realmente esta sendo acessado, que pode ser arquivo, memória, rede, impressora e etc. Por intermédio das streams é possível transportar dados como voz, vídeo, imagem, entre outros. As streams suportam leituras e escritas síncronas e assíncronas.

E/S Síncrona

O .Net Framework provê classes para trabalhar com streams, sendo as principais: FileStream, MemoryStream, NetworkStream - derivadas de Stream, e BufferedStream que pode ser utilizada em conjunção com qualquer outra classe Stream. O primeiro passo para obter e gravar dados em arquivos, é invocar os métodos estáticos OpenRead() e OpenWrite(), da classe File, que retornam um Stream. Essas streams servirão como "canais" por onde passarão os dados conforme o exemplo abaixo.

Stream streamEntrada = File.OpenRead(@"c:\ArquivoOriginal.cs");
Stream streamSaida = File.OpenWrite(@"c:\ArquivoCopia.cs");

No exemplo acima foi criado um Stream de Entrada a partir do arquivo "ArquivoOriginal.cs" e um Stream de Saída a partir do arquivo "ArquivoCopia.cs".

Depois de aberto o arquivo para leitura, é iniciada a transferência dos dados através do método Read da classe Stream. A otimização dessa transferência ocorre criando-se um buffer (repositório temporário de dados) para que, uma vez lidos, os dados de um Backstore (Fonte de Dados) possam ser armazenados nesse mesmo buffer e posteriormente manipulados. O exemplo abaixo mostra a leitura do arquivo (c:\ArquivoOriginal.cs) e a escrita no arquivo (c:\ArquivoCopia.cs) através dos métodos Read e Write dos objetos streamEntrada e streamSaída :

while(bytesLidos = streamEntrada.Read(buffer,0,tamanhoBuffer)>0)
{
	streamSaida.Write (buffer,0,bytesLidos);
}

Os parâmetros que o método Write espera são: buffer, que nada mais é do que um array de bytes que irá armazenar os dados; a posição de início de leitura; e o número de bytes a escrever que no caso do exemplo acima sempre será o mesmo número de bytes que foi lido.

Exemplo streams síncrono :

namespace Exemplo
{
	using System;
	using System.IO;

	class ESarquivoSincrono
	{
		const int tamanhoBuffer = 1024;
		public static void Main( )
		{
			EsarquivoSincrono  esArquivoSincrono = new EsarquivoSincrono ( );
			esArquivoSincrono.Executa( );
		}

		private void Executa( )
		{
			// O arquivo que será lido
			Stream streamEntrada = File.OpenRead(@"c:\ArquivoOriginal.cs");

			// O arquivo que será escrito
			Stream streamSaida = File.OpenWrite(@"c:\ArquivoCopia.cs");

			// O buffer que irá armazenar o bytes
			byte[] buffer = new Byte[tamanhoBuffer];
			
			int bytesLidos;

			//escreve no arquivo enquanto houver bytes no buffer a serem escritos 
			while(bytesLidos = streamEntrada.Read(buffer,0,buffer.Lenght)>0)
			{
				streamSaida.Write (buffer,0,bytesLidos);
			}

			//Fecha as streams
			streamSaida.Close();
			streamEntrada.Close( );
		}
	}
}

Acima foi mostrado um exemplo de chamada síncrona, que não é o melhor método de trabalhar com streams, quando levado em consideração fatores importantes como performance, pois ao fazer uma chamada síncrona, enquanto seu programa lê ou escreve, todas as outras atividades são paradas. Pode-se levar muito tempo para obter dados do backstore que pode ser ou um disco muito devagar ou uma rede lenta. Desse modo a melhor alternativa é trabalhar com E/S Assíncrona.

E/S Assíncrona

E/S Assíncrona permite começar uma tarefa de E/S e exercer outras atividades. Isso ocorre através dos métodos BeginRead() e BeginWrite() da classe Stream. O fato de ser dedicado um Thread a cada tarefa de E/S possibilita a execução de tarefas concorrentes. É de praxe, ao iniciarmos uma tarefa de E/S Assíncrona, "agendarmos" um procedimento a ser realizado quando a mesma for concluída. Isso é feito passando-se um método de callback através de um delegate, onde será implementada uma outra atividade, que pode ser o processamento dos dados lidos ou escritos, a notificação que determinada tarefa foi concluída, entre outras.

Leitura Assíncrona :

streamEntrada.BeginRead(
	buffer, //Local onde serão armazenados os dados
	0, // offset
	buffer.Length, // Tamanho do Buffer
	myCallBack, // Delegate "apontando"  para o método de callback 
	null); // Objeto contendo informações sobre estado

Escrita Assíncrona :

streamSaida.BeginWrite(
	buffer, //Local onde serão armazenados os dados
	0, // offset
	buffer.Length, // Tamanho do Buffer
	myCallBack, // Delegate "apontando"  para o método de callback 
	null); // Objeto contendo informações sobre estado do objeto

O exemplo abaixo ilustra uma pequena aplicação para leitura e escrita de dados no modo assíncrono. Inicialmente são declaradas as Streams de entrada e saída, os delegates utilizados para "apontarem" os métodos de callback para as tarefas de leitura e escrita, e um array de bytes que servirá de buffer.

namespace Exemplo
{
	using System;
	using System.IO;
	using System.Threading;
	using System.Text;
	
	public class ESarquivoAssincrono
	{
		private Stream streamEntrada;
		private Stream streamSaida;
		
		// Declara os delegates
		private AsyncCallback callBackEscrita;
		private AsyncCallback callBackLeitura;
		
		// Buffer que irá armazenar os dados
		private byte[] buffer;
		
		// Define o tamanho do buffer
		const int tamanhoBuffer  = 256;

O construtor da classe irá instanciar: as streams de entrada e saída a partir dos métodos estáticos OpenRead e OpenWrite da classe File; o buffer; e os delegates, atribuindo a cada um dos delegates um método que irá efetuar o processamento posterior às respectivas tarefas.

		// Construtor
		ESarquivoAssincrono( )
		{
		
			// Abre a stream de entrada
			streamEntrada = File.OpenRead(@"c:\ArquivoOriginal.cs");
			
			// Abre a stream de saída
			streamSaída = File.OpenWrite(@"c:\ArquivoCopia.cs");
			
			// Instancia o buffer
			buffer = new byte[tamanhoBuffer];
			
			// "Aponta" o Delegate de CallBack para um 
			//determinado método que irá processar 
			   informações  após a leitura ser concluída
			
			callBackLeitura = new AsyncCallback(this.QuandoLeituraForConcluida);
			callBackEscrita = new AsyncCallback(this.QuandoEscritaForConcluida);
		}
		
		public static void Main( )
		{
			ESarquivoAssincrono esArquivoAssincrono =new ESarquivoAssincrono ( );
			
			// invoca o método que irá começar o processo de leitura e escrita
			esArquivoAssincrono. Executa( );
		}

Logo após é iniciada a leitura dos dados, invocando-se o método BeginRead(), que tem como parâmetros de entrada: o buffer, onde serão armazenados os dados lidos; o offset; o tamanho do buffer; o delegate, "apontando" para o método de callback; e um objeto, que serve para indicar o estado.Depois é efetuada uma tarefa qualquer para demonstrar o trabalho assíncrono.

		void Executa( )
		{
		
			streamEntrada.BeginRead(
				buffer, // Onde os dados serão armazenados
				0, // offset
				buffer.Length, // Tamanho do buffer
				callBackLeitura, //Delegate de callback
				null); // Objeto de estado
			
			Console.WriteLine("A leitura assíncrona foi iniciada\n");
			Console.WriteLine("Tarefa concorrente a leitura :\n");
			for (long i = 0; i < 2000; i++)
			{
				Console.WriteLine("*");
			}
		}
	}
}
O próximo passo é criar o método que irá processar as informações quando a leitura for concluída. Esse método verifica se os dados foram lidos por intermédio do método EndRead() que retorna o número de bytes lidos. Caso tenham sido , os mesmos são escritos por um outro método assíncrono BeginWrite(), seguido de uma tarefa qualquer concorrente para demonstrar o trabalho assíncrono.
// Metodo que irá processar as informações quando a leitura for concluída
public void QuandoLeituraForConcluida (IAsyncResult result)
{
	//Se houver bytes lidos escreve eles em outro arquivo
	if (bytesLidos = streamEntrada.EndRead(result) > 0)
	{
		streamSaida.BeginWrite(buffer,0,buffer.Length, callBackEscrita, null);
		
		Console.WriteLine("A escrita assíncrona foi iniciada\n");
		Console.WriteLine("Tarefa concorrente a escrita :\n");
		for (long i = 0; i < 2000; i++)
		{
			Console.WriteLine("/");
		}      
	}
}

Por fim é criado o método que irá processar as informações quando a escrita for concluída. Nesse método invoca-se novamente BeginRead(), criando um ciclo, que quando a leitura é concluída, inicia a escrita, que logo terminada inicia novamente a leitura. Esse processo, que se repete até que não haja mais bytes a serem lidos, é feito através dos métodos de callback, invocados através do delegate, que são passados nos métodos de leitura e escrita.

	// Metodo que irá processar as informações quando a escrita for concluída
	void QuandoEscritaForConcluida (IAsyncResult result)
	{
		streamSaída.EndWrite(result);
		
		streamEntrada.BeginRead(
			buffer, // Onde os dados serão armazenados
			0, // offset
			buffer.Length, // Tamanho do buffer
			callBackLeitura, //Delegate de callback
			null); // Objeto de estado
		
		Console.WriteLine("A leitura assíncrona foi iniciada\n");
		Console.WriteLine("Tarefa concorrente a leitura :\n");
		for (long i = 0; i 

E/S Rede

Escrever para um objeto remoto não é diferente do que escrever para um objeto local, basicamente a maior diferença é o uso de sockets. Os sockets são muito úteis para aplicações cliente-servidor, ponto-a-ponto e quando há necessidade de fazer chamadas a procedimentos remotos. Imagine um socket como uma tomada, um ponto final, para uma determinada comunicação entre processos através de uma rede. O primeiro passo para criar uma aplicação deste tipo é instanciar um socket dando a ele a tarefa de "escutar" em uma determinada porta. O socket irá esperar pacientemente por uma chamada do cliente onde irá interagir com o mesmo. No caso de haver mais de uma chamada por vez, é criada uma fila para que os clientes sejam colocados em espera. É notável, já neste momento, a deficiência deste procedimento, pois se formos tratar múltiplas conexões, este processo de enfileiramento irá causar um grande gargalo. Utiliza-se mais uma vez E/S assíncrona para que seja sanada esta deficiência. No modo assíncrono um socket fica responsável por escutar chamadas de clientes, e outros sockets são instanciados para cada conexão, possibilitando múltiplas conexões. Para que seja criada uma conexão é necessário utilização da classe TCPListener que provê serviços TCP/IP de alto nível.

Para exemplificar a transmissão de dados através de uma rede utilizando streams, iremos criar um servidor e um cliente de streaming utilizando o protocolo TCP/IP através das classes TCPListener e TCPClient. Começaremos com a aplicação Servidor.

Iniciamos criando a classe ServidorArquivoAssincrono que irá representar o nosso servidor de arquivos. Esta classe tem como atributos membros: tamanhoBuffer, que ira armazenar o tamanho do buffer; buffer, um array de bytes que será o buffer utilizado pela classe; streamRede, que será a stream que utilizaremos para representar a rede por onde irão trafegar os dados; streamEntrada, que será a stream que irá representar o arquivo que será lido; callBackLeitura, delegate que irá "apontar" para um método que tomará alguma ação quando a leitura de determinados dados, vindos da rede, for concluída; callBackEscrita , delegate que irá "apontar" para um método que tomará alguma ação quando a escrita na rede de determinados dados, vindos de um arquivo, for concluída; e callBackLeituraArquivo, delegate que irá "apontar" para um método que tomará alguma ação quando a leitura de arquivo for concluída.

using System;
using System.Net.Sockets;
using System.Text;
using System.IO;

namespace Exemplo
{
	public class ServidorArquivoAssincrono
	{
		private const int tamanhoBuffer  = 256;

		private byte[] buffer;

		private Socket socket;

		private NetworkStream streamRede;
		private Stream streamEntrada;

		private AsyncCallback callBackLeitura;
		private AsyncCallback callBackEscrita;
		private AsyncCallback callBackLeituraArquivo;

Criamos o método Main que sera o ponto de entrada da aplicação, que instancia um objeto da classe ServidorArquivoAssincrono e chama seu método Executar.

		public static void Main( )
	{
	ServidorArquivoAssincrono servidorArquivoAssincrono = new ServidorArquivoAssincrono();
	servidorArquivoAssincrono.Executar( );
}

O método Executar instancia um objeto da classe TcpListener, passando para seu construtor o número da porta que ele irá escutar para depois iniciar a escuta através do método Start(). É criado um for "infinito" para que a aplicação fique de prontidão para possíveis conexões feitas por clientes. A conexão é aceita por intermédio do método AcceptSocket que retorna um socket e testada através da propriedade Connected. Esta propriedade retorna um valor booleano, true se conectado e false se não conectado. Caso conectado é instanciado um objeto da classe ManipulaCliente que ira representar a interação entre o servidor e o determinado cliente. Logo após é chamado então o método ObtemNomeArquivo.

private void Executar( )
{
	TcpListener tcpListener = new TcpListener(50000);
	tcpListener.Start( );

	for (;;)
	{
		Socket socket = tcpListener.AcceptSocket( );

		if (socket.Connected)
		{
			ManipulaCliente manipulaCliente = new ManipulaCliente (socket);
			manipulaCliente.ObtemNomeArquivo();
		}
	
	}
}

A classe ManipulaCliente, já citada acima, foi criada para representar a abstração da interação entre o servidor e um determinado cliente conectado. O construtor desta classe recebe o socket do cliente, inicializa a variável membro socket com o mesmo, instancia um array de bytes e instancia um NetworkStream através do socket passado ao construtor de ManipulaCliente. O array de bytes será o buffer que irá conter os dados do arquivo a ser lido e o NetworkStream representará a rede que irá trafegar os dados entre o cliente e o servidor. Os delegates de callback também são instanciados passando os seus respectivos métodos que serão chamados quando determinadas tarefas forem concluídas, no caso leitura de arquivo, rede e escrita na rede.

class ManipulaCliente
{
	public ManipulaCliente (Socket socket)
	{
		this.socket = socket;

		buffer = new byte[256];

		streamRede = new NetworkStream(socket);

		callbackLeituraArquivo =new AsyncCallback(this.QuandoLeituraArquivoForConcluida);

		callbackLeitura = new AsyncCallback(this.QuandoLeituraForConcluida);

		callbackEscrita = new AsyncCallback(this.QuandoEscritaForConcluida);
	}

O nome do arquivo, que é enviado pelo cliente através da rede, é obtido através de uma NetworkStream chamando o método ObtemNomeArquivo da classe ManipulaCliente.

	public void ObtemNomeArquivo ( )
	{
		streamRede.BeginRead (buffer, 0, buffer.Length, callbackLeitura, null);
	}

Quando a leitura da stream de rede for concluída é chamado o método QuandoLeituraForConcluida, através do delegate callbackLeitura, que verifica a quantidade de bytes lidos através da chamada do método EndRead da classe NetworkStream. Os bytes são transformados em uma String que é passada como argumento do método estático OpenRead, da classe File. É iniciada a leitura do arquivo através da stream streamEntrada.

	
private void QuandoLeituraForConcluida ( IAsyncResult result )
{
  int bytesLidos = streamRede.EndRead(result);

  if( bytesLidos> 0 )
  {
    string nomeArquivo = System.Text.Encoding.ASCII.GetString (buffer, 0, bytesLidos);

    streamEntrada = File.OpenRead(nomeArquivo);

    streamEntrada.BeginRead(buffer, 0, buffer.Length, callBackLeituraArquivo, null);
  }
  else
  {
    streamRede.Close( );
    socket.Close( );
    streamRede = null;
    streamRede = null;
  }
}

Quando a leitura do arquivo for concluída , ou seja, o buffer for preenchido, é chamado o método QuandoLeituraArquivoForConcluida, através do delegate de callback, callBackLeituraArquivo. Este método alavancará o processo de escrita dos dados na Rede através do objeto da classe NetworkStream, streamRede.

void QuandoLeituraArquivoForConcluida (IAsyncResult result)
{
  int bytesLidos = streamEntrada.EndRead(result);

  if (bytesLidos> 0)
  {
    streamRede.BeginWrite(buffer, 0, bytesLidos, callbackEscrita, null);
  }
}

Quando o processo de escrita na rede for concluído, novamente é iniciado o processo de leitura do arquivo através do método QuandoEscritaForConcluida, chamado por intermédio do delegate de callback callbackEscrita .

  private void QuandoEscritaForConcluida ( IAsyncResult result)
  {
    streamRede.EndWrite(result);
		
    streamEntrada.BeginRead(buffer, 0,buffer.Length, callBackLeituraArquivo, null); 
  }
 }
 }
}

Este procedimento de leitura e escrita continua até que não existam mais dados para serem lidos. O exemplo de aplicação servidora acima é relativamente simples, onde, primeiramente, o servidor requisita o nome do arquivo a ser lido e logo começa a leitura e escrita assíncrona deste arquivo. O ponto chave de aplicações assíncronas é o encadeamento dos procedimentos através dos delegates de callback. A seguir iremos construir a aplicação cliente para concluir a explanação do assunto. A aplicação cliente sera constituída pela classe ClienteArquivoAssincrono, que será responsável pelo envio de dados do cliente e a recepção de dados vindos do servidor. A classe contem os seguintes atributos membros : tamanhoBuffer, que armazenará o tamanho do buffer; buffer, que armazenara os dados; streamRede, que irá abstrair a rede que está entre o cliente e o servidor; e streamSaida, que será responsável pela escrita dos dados em um determinado arquivo no cliente.

using System;
using System.Net.Sockets;
using System.Threading;
using System.Text;
using System.Runtime.Serialization.Formatters.Binary;

namespace Exemplo
{

	public class ClienteArquivoAssincrono
	{

		private const int tamanhoBuffer = 256;
		private NetworkStream streamRede;
		private Stream streamSaida;

		static public int Main( )
		{
			ClienteArquivoAssincrono clienteArquivoAssincrono =
			new clienteArquivoAssincrono ( );
			return clienteArquivoAssincrono.Executa( );
		}

O construtor desta classe instancia um objeto da classe TcpClient, passando o número da porta e o nome do servidor, que a aplicação irá se tornar cliente. O atributo membro streamRede é instanciado através do método GetStream da classe TcpClient que retorna um Stream.

ClienteArquivoAssincrono ( )
{
	TcpClient tcpClient = new TcpClient("localhost", 50000);
	streamRede = tcpClient.GetStream( );
}
Abaixo vemos o método Executa que envia para o servidor o nome do arquivo através da classe StreamWriter. StreamWriter, por sua vez escreve na streamRede que é a abstração da rede por onde serão enviados os dados para o servidor. Depois de feita a transferência dos dados do servidor para o cliente, o próximo passo é instanciar um objeto da classe Stream (streamSaida) através do método estático OpenWrite, da classe File, que sera utilizado para escrever os dados enviados pelo servidor em um arquivo.
private int Executa( )
{
	string nomeArquivo = "C:\\ArquivoNoServidor.txt";

	StreamWriter writer = new StreamWriter(streamRede);
	writer.Write(nomeArquivo);
	writer.Flush( );

	int bytesLidos = 0;

	streamSaida = File.OpenWrite(@"C:\ArquivoNoCliente.txt")

	while (!byteLidos.Convert.ToBoolean())
	{
		char[] buffer = new char[tamanhoBuffer];

		StreamReader streamEntrada = new StreamReader(streamRede);

		int bytesLidos = streamEntrada.Read(buffer,0,tamanhoBuffer);

		if (bytesLidos== 0
		{
			Continue;
		}
		else 
		{
			streamSaida.Write(buffer,0, tamanhoBuffer);
		}
	}
	streamRede.Close( ); 
	return 0;
}
}

Conclusão:

Este artigo tem como principal objetivo explanar a utilização de streams demonstrando as vantagens e desvantagens de trabalhar com métodos síncronos e assíncronos das mesmas. Nos exemplos foram mostrados trocas de informações em forma de arquivo texto, porém outros tipos de dados como vídeo, imagem, voz, etc também podem ser transportados da mesma forma.

Bruno Spinelli Dantas

Bruno Spinelli Dantas - MCAD, Desenvolvedor da nTime Mobile Solutions, dedicando-se exclusivamente a Tecnologia Net, no desenvolvimento de plataformas para dispositivos móveis.