Framework de Servidor Socket Assíncrono Multithread Extensível EMTASS 2.0

No desenvolvimento de software e aplicações práticas, a implementação de um servidor para recebimento de pacotes de dados via Socket constitui um desafio clássico. Tal tarefa requer conhecimento em programação de rede (principalmente Socket) e está intimamente ligada à lógica de processamento de negócios (como as regras de composição de pacotes), devendo, ao mesmo tempo, garantir estabilidade, eficiência, segurança e gerenciamento do sistema em execução. Em aplicações específicas, com base nos requisitos da lógica de negócios, existem diferentes ênfases: algumas necessitam considerar concorrência e eficiência, outras enfatizam estabilidade e confiabilidade. Embora a tecnologia assíncrona IOCP (I/O Completion Ports) no .NET Framework 2.0 possa resolver eficazmente problemas de concorrência, o modelo totalmente assíncrono carece de certa flexibilidade em termos de controle, como operações de pausa em Sockets.

Este artigo apresenta uma solução tradicional para servidores de pacotes de dados via Socket, derivada de um projeto do autor referente a um centro de servidor de dados de fluxo de tráfego rodoviário provincial (DSC) no final de 2005. Na época, o .NET Framework 2.0 e o Visual Studio 2005 foram lançados recentemente, e o autor possuía pouca experiência com C#. Assim, realizou pesquisas nacional e internacional na web, buscando ideias e código para resolver problemas de comunicação por Socket usando C#. Finalmente, encontrou dois artigos de grande auxílio: um escrito por um desenvolvedor chinês em março de 2005, sobre um framework de receptor de Socket — implementando a comunicação C/S de um serviço de rede TCP usando programação assíncrona de Socket em C# (dividido em duas partes). Este artigo empregou o conceito de sessão de Socket do cliente. O outro, de um autor norte-americano, propôs uma solução técnica para recebimento de pacotes de dados segmentado e multithread, descrevendo muitos detalhes de implementação de Sockets assíncronos e multithread. Este artigo consolidou a adoção, pelo autor, de uma abordagem multithread e assíncrona para o processamento do receptor de Socket. A primeira versão, EMTASS 1.0 (EMTASS - Extensible Multi-Thread Asynchronous Socket Server), foi concluída e posta em uso no início de 2006.

Este verão, o autor modificou o código original do servidor receptor de Socket, resultando no EMTASS 1.1. Recentemente, conforme os requisitos de extensibilidade e reutilizabilidade do framework, o EMTASS foi novamente concebido e projetado, dando origem ao EMTASS 2.0. A apresentação a seguir está dividida em seis seções:

  1. Conceito Geral e Arquitetura

  2. Tecnologias de Implementação Chave

  3. Introdução ao Uso do Framework

  4. Resultados Gerais dos Testes

  5. Conclusão e Perspectivas

  6. Versões e Código-Fonte

  7. Conceito Geral e Arquitetura


1.1 Conceito Geral

A concepção principal considera três aspectos: multithreading, Sockets assíncronos e extensibilidade.

1) Três Threads Principais

Em aplicações Socket no ambiente Internet, clientes e a rede podem apresentar exceções, exigindo a liberação de recursos de Socket de conexões encerradas anormalmente. Considerando a alta capacidade de concorrência do servidor, adota-se geralmente a estratégia de separar o recebimento e o processamento de pacotes: os pacotes recebidos são adicionados a uma fila de pacotes e, em seguida, processados a partir dessa fila. Certamente, a escuta de requisições de conexão de clientes remotos pode utilizar o método assíncrono AcceptAsync() do Socket (iniciando assim o IOCP). Considerando operações síncronas de pausa e encerramento, ainda se utiliza uma thread. Dessa forma, limpeza de recursos, processamento de pacotes e escuta de requisições de conexão de clientes constituem as três threads principais da arquitetura EMTASS, gerenciadas pelo pool de threads do .NET:

  • Thread de Escuta de Conexões de Clientes IniciarEscutaServidor(): Em loop, escuta requisições de conexão Socket de clientes remotos. Se existirem, após a verificação de conformidade adequada, cria um objeto de sessão de cliente TClasseBaseSessao (na verdade, um objeto de uma classe derivada dessa) e invoca o método assíncrono de recebimento de dados do Socket da sessão, BeginReceive(). Os pacotes de dados recebidos são armazenados na fila de pacotes do objeto de sessão. O novo objeto TClasseBaseSessao é adicionado à fila de sessões m_tabelaSessoes (um objeto genérico Dictionary<>), que é o objeto de iteração para as threads de limpeza e processamento.
  • Thread de Processamento de Pacotes VerificarFilaDatagramas(): Em loop, verifica os objetos de sessão na fila TClasseBaseSessao, invocando os métodos relevantes do objeto para completar tarefas como análise de pacotes, verificação de tipo, armazenamento de dados, etc.
  • Thread de Verificação da Tabela de Sessões VerificarTabelaSessoes(): Em loop, verifica cada objeto de sessão na tabela de sessões m_tabelaSessoes, limpando em etapas os objetos de sessão expirados, inválidos ou anormais, limpando os buffers do objeto de sessão e liberando seus recursos de Socket.

2) Modo de Processamento Assíncrono

O Socket no .NET Framework possui capacidade completa de processamento assíncrono: escuta assíncrona (AcceptAsync()), recebimento assíncrono de dados (BeginReceive()), envio assíncrono de dados (BeginSend()), etc. O framwork EMTASS adota os modos de recebimento e envio assíncronos, encapsulados na classe TClasseBaseSessao. Nas versões 1.0 e 1.1 do EMTASS, esses métodos eram implementados na classe principal TClasseBaseServidorSocket, o que claramente não respeita o princípio de encapsulamento de classes.

3) Extensibilidade do Sistema

A extensibilidade considera principalmente diferentes lógicas de processamento de negócios e cenários de aplicação, ou seja: formato dos pacotes, métodos de armazenamento de dados, servidores de banco de dados, etc. A extensibilidade do framework EMTASS é refletida no design genérico e abstrato das classes, em métodos virtuais e protegidos:

  • Classes Abstratas: A classe base de sessão TClasseBaseSessao e a classe base de banco de dados TClasseBaseBancoDados são classes abstratas que fornecem métodos virtuais para análise e verificação de pacotes e para armazenamento de dados, respectivamente.
  • Classes Genéricas: A classe base principal TClasseBaseServidorSocket possui dois parâmetros de restrição genérica: TClasseBaseSessao e TClasseBaseBancoDados. Ao especializar suas classes derivadas, pode-se produzir tipos concretos de servidores.
  • Métodos Abstratos (abstract): O método de análise de pacotes AnalisarDatagrama() de TClasseBaseSessao e o método de abertura do banco de dados Abrir() de TClasseBaseBancoDados são abstratos e devem ser sobrescritos nas classes derivadas com base na lógica de negócios e no tipo de banco de dados.
  • Métodos Protegidos (protected): Todos os métodos relacionados ao tratamento de eventos e à lógica de processamento de negócios são métodos protected, podendo ser sobrescritos conforme a situação.

1.2 Arquitetura de Classes

1) Estrutura Hierárquica Principal das Classes

(Figura 1: Relação Hierárquica das Classes Principais)

Por categoria de aplicação, o EMTASS possui quatro grupos principais de classes: Classes de Servidor Socket, Classes de Sessão, Classes de Banco de Dados e Tipos de Enumeração.

  • Classes de Servidor Socket
    • Classe Genérica do Servidor TClasseBaseServidorSocket: Esta classe inclui um objeto Socket do servidor, um objeto derivado de TClasseBaseBancoDados, uma lista (objeto genérico Dictionary<>) de objetos derivados de TClasseBaseSessao. Encapsula todos os eventos de TClasseBaseBancoDados e TClasseBaseSessao, fornecendo uma interface pública e eventos unificados.
    • Parâmetros Genéricos: A classe base do servidor TClasseBaseServidorSocket tem dois parâmetros genéricos: TClasseBaseSessao e TClasseBaseBancoDados. Ao especializar suas classes derivadas, determina-se a versão concreta da classe genérica.
  • Classes de Sessão do Cliente
    • Classe de Membros Centrais da Sessão TClasseCoreSessao: É a classe base de TClasseBaseSessao. Inclui campos centrais da sessão: horário de login, horário da última atividade, endereço IP, nome do cliente, estado do objeto, etc. É uma das classes base para a lista de sessões e argumentos de evento. Todos os campos de membros desta classe são protected e devem receber valores concretos nas classes derivadas.
    • Classe Abstrata de Sessão TClasseBaseSessao: Encapsula o Socket do cliente, buffers de recepção de dados, buffers de pacotes, filas de pacotes e outras estruturas de dados. Inclui todos os métodos relacionados à comunicação com o cliente: recebimento e envio de dados, processamento de pacotes, etc.
  • Classes de Banco de Dados
    • Classe Base Abstrata de Banco de Dados TClasseBaseBancoDados: Encapsula métodos para abertura e fechamento do banco de dados, tratamento de eventos de exceção, etc. A propriedade de conexão ConexaoDb é uma propriedade virtual, e o método de abertura do banco de dados Abrir() é abstrato, precisando ser sobrescrito nas classes derivadas.
    • Classe Base TClasseBaseSqlServer: Derivada de TClasseBaseBancoDados. Utiliza tipos relacionados ao SqlServer do namespace System.Data.SqlClient para redefinir a propriedade de conexão ConexaoDb e sobrescreve o método Abrir().
    • Classe Base TClasseBaseBancoDadosOleDb: Derivada de TClasseBaseBancoDados. Utiliza tipos relacionados ao acesso a dados OleDb do namespace System.Data.OleDb para redefinir a propriedade de conexão da classe base ConexaoDb e sobrescreve o método Abrir().
  • Tipos de Enumeração
    • Tipo de Estado da Sessão TSessaoEstado: Assume 4 valores: Valido, Invalido, Desligando e Fechado. Valido indica uma sessão válida, Invalido indica que a sessão será limpa, Desligando indica que o Socket da sessão está sendo encerrado, Fechado indica que a sessão foi encerrada e seus recursos foram limpos.
    • Tipo de Desconexão da Sessão TDesconexaoTipo: Assume 3 valores: Normal, Timeout e Exceção, representando conexão normal, desconexão por tempo limite e desconexão por exceção, respectivamente. O tempo limite ocorre quando o intervalo entre os últimos dois recebimentos de dados da sessão excede o tempo limite acordado, evitando que algumas sessões ocupem recursos por muito tempo.

2) Diagrama de Estrutura de Tipos de Argumento de Evento

(Figura 2: Relação Hierárquica das Classes de Argumentos de Evento)

Os eventos do framework EMTASS encluem três categorias: primeiro, eventos comuns, como: início e parada do servidor; segundo, eventos de exceção, exceções de recebimento/envio de dados, exceções de conexão/armazenamento em banco de dados, etc.; terceiro, eventos relacionados à sessão, como: adição de um objeto de sessão, recebimento de um pacote de dados válido, etc. A combinação de exceção e sessão é o evento de exceção de sessão. Através de delegações genéricas EventHandler, podem ser definidos eventos da classe, cujos tipos de argumentos de evento são:

  • Classe de Argumento de Evento de Exceção TArgumentosEventoExceção: Encapsula o valor Message do objeto Exception.
  • Classe de Argumento de Evento de Sessão TArgumentosEventoSessao: Encapsula um objeto TClasseCoreSessao.
  • Classe de Argumento de Evento de Exceção de Sessão TArgumentosEventoExceçãoSessao: Derivada de TArgumentosEventoSessao, inclui o campo Mensagem para a mensagem de exceção.
  1. Tecnologias de Implementação Chave

A seguir, são apresentados os principais métodos de implementação das classes TClasseBaseServidorSocket, TClasseBaseSessao e TClasseBaseBancoDados.

2.1 Classe TClasseBaseServidorSocket

Esta classe contém todas as interfaces públicas e eventos. Sua implementação principal cobre as três threads descritas anteriormente.

1) Pool de Threads e Sinais de Sincronização

O .NET fornece o método de pool de threads ThreadPool.QueueUserWorkItem(), que adiciona automaticamente objetos de delegação ao pool de threads do sistema. O código de implementação é o seguinte:

if (!ThreadPool.QueueUserWorkItem(this.IniciarEscutaServidor)) return false;
if (!ThreadPool.QueueUserWorkItem(this.VerificarFilaDatagramas)) return false;
if (!ThreadPool.QueueUserWorkItem(this.VerificarTabelaSessoes)) return false;

Os métodos de escuta de requisições de conexão de clientes IniciarEscutaServidor(), verificação da fila de pacotes VerificarFilaDatagramas() e verificação da tabela de sessões VerificarTabelaSessoes() utilizam todos um modo de processamento em loop, cuja condição é _servidorFechado ser false. Somente o método Fechar() desta classe pode interromper estas três threads. No método Fechar(), ao configurar a variável _servidorFechado como true para terminar as threads, também é necessário considerar o problema de sincronização da saída das threads, utilizando objetos de sinalização de evento manual ManualResetEvent. Consulte o código do método da thread de verificação da fila de pacotes a seguir:

private void VerificarFilaDatagramas(object estado)
{
    _resetEventoVerificarFilaDatagramas.Reset();

    while (!_servidorFechado)
    {
        lock (_dicionarioSessoes)
        {
            // ... outro código
        }
    }

    _resetEventoVerificarFilaDatagramas.Set();
}

O código acima não é seguro; geralmente, é necessário um bloco try{}finally{} para garantir que o Reset() e o Set() do objeto de sinalização de evento correspondam. No entanto, os três métodos de thread no EMTASS têm suas próprias formas de tratamento de exceções e não lançarão exceções. A seguir, o código principal do método de encerramento do servidor Fechar(). Após configurar a variável _servidorFechado como true, utiliza-se o aguardo de três sinais de evento para sincronizar a finalização normal das três threads.

private void Fechar()
{
    if (_servidorFechado)
    {
        return;
    }
    
    _servidorFechado = true;
    _escutaServidorPausada = true;
    
    _resetEventoVerificarEscutaServidor.WaitOne();  // Aguarda as 3 threads
    _resetEventoVerificarTabelaSessoes.WaitOne();
    _resetEventoVerificarFilaDatagramas.WaitOne();
	
    // ... outro código
}

2) Thread de Limpeza por Etapas

Após a criação de um objeto de sessão, três situações podem exigir o encerramento da sessão: 1) Encerramento do servidor; 2) Exceção na sessão; 3) Expiração da sessão. O primeiro caso forçará o encerramento da sessão; os segundo e terceiro casos necessitam que a thread de limpeza encerre a sessão e libere seus recursos. Para evitar exceções causadas pelo fechamento imediato do Socket, o sistema completa o processo em 3 etapas: 1) Marca a sessão como Invalido; 2) Invoca o método Desligar(): Desliga o Socket da sessão e marca seu estado como Desligando; 3) Invoca o método Fechar(): Limpa os buffers e a fila de pacotes da sessão, libera os recursos do Socket e remove o objeto da tabela de sessões. A operação específica pode ser consultada no método VerificarTabelaSessoes() da classe TClasseBaseServidorSocket.

3) Propagação e Publicação de Eventos

Todos os eventos do framework EMTASS, incluindo os eventos das classes TClasseBaseSessao e TClasseBaseBancoDados, são publicados externamente pela classe do servidor TClasseBaseServidorSocket. Ao criar objetos de sessão ou banco de dados, seus eventos são diretamente propagados para os eventos de delegação correspondentes de TClasseBaseServidorSocket, como mostra o código abaixo:

    sessao.DatagramaAceito += new EventHandler(this.OnDatagramaAceito);
    sessao.DatagramaTratado += new EventHandler(this.OnDatagramaTratado);

No código acima, dois eventos do objeto sessao (derivado de TClasseBaseSessao) são vinculados (usando o operador +=) diretamente ao objeto atual TClasseBaseServidorSocket. O código de implementação específico pode ser consultado no método de inicialização Iniciar() e no método de adição de objeto de sessão AdicionarSessao() de TClasseBaseServidorSocket.

2.2 Classe TClasseBaseSessao

Esta classe abstrata inclui membros como o Socket do cliente, buffers de recepção de dados e filas de pacotes. Encapsula todos os métodos relacionados à comunicação por Socket. A classe também inclui métodos de processamento de pacotes: o método protegido de análise de buffer da sessão ResolverBufferSessao() e o método virtual de análise de pacotes AnalisarDatagrama().

1) Buffer da Sessão e Fila de Pacotes

TClasseBaseSessao inclui dois buffers de recepção de dados e uma fila de pacotes:

  • Buffer _bufferRecepcao: Buffer para recebimento de dados do Socket do cliente. Se um pacote de dados for mais longo que este buffer, o Socket realizará (assincronamente) várias leituras, utilizando o método CopiarParaBufferDatagrama para transferir temporariamente para o buffer de pacotes _bufferDatagrama.
  • Buffer _bufferDatagrama: Se o _bufferRecepcao receber um pacote de dados incompleto, este buffer é usado para armazenamento temporário até que um pacote completo seja obtido. Normalmente, configura-se _bufferRecepcao maior que o comprimento do pacote de dados, permitindo o recebimento de um pacote completo de uma vez; neste caso, este buffer permanece vazio. Além disso, o tamanho deste buffer cresce dinamicamente de acordo com o comprimento do pacote de dados.
  • Fila de Pacotes _filaDatagramas: Uma fila de arrays de bytes (Queue<> genérica), que salva os pacotes de dados (arrays de bytes) da sessão atual, aguardando análise e processamento pela thread de processamento. Nas versões 1.0 e 1.1 do EMTASS, esta estrutura de fila era encapsulada dentro de TClasseBaseServidorSocket. Este design aumentou o acoplamento entre as classes TClasseBaseSessao e TClasseBaseServidorSocket.

2) Método de Análise de Pacotes ResolverBufferSessao()

Este método é protected e pode ter seu código sobrescrito de acordo com a estrutura dos pacotes de dados e a lógica de negócios específica. A regra de composição de pacotes implementada na classe TClasseBaseSessao é: caractere inicial '<' e caractere final '>'. É importante destacar que na comunicação por Socket há dois problemas notórios a serem considerados:

  • Problema de Limite de Pacote: Como definir o limite de uma string (array de bytes) de pacote de dados? No protocolo de comunicação Socket do Ministério dos Transportes, '<' e '>' são usados como caracteres de início e fim de um pacote, respectivamente.
  • Problema de Descontinuidade e Sobreposição de Pacotes: Devido a falhas na rede ou no equipamento, um pacote de dados pode ser recebido em duas vezes. Outra situação é o recebimento contínuo de múltiplos pacotes de dados. No primeiro caso, é necessário armazenar em buffer o pacote recebido até que um pacote completo seja obtido. No segundo caso, é necessário decompor os pacotes um a um com base nos caracteres delimitadores de pacote.

3) Método de Análise de Pacotes AnalisarDatagrama()

Este método abstrato é o que deve ser sobrescrito pela classe TClasseBaseSessao. É também a principal interface de extensão do framework EMTASS e deve completar as seguintes tarefas básicas:

  • Verificar a validade do pacote de dados e seu tipo;
  • Decompor os dados de cada campo do pacote;
  • Validar o pacote e seus dados;
  • Enviar uma mensagem de confirmação ao cliente (invocando o método EnviarDatagrama());
  • Armazenar os dados do pacote no banco de dados;
  • Se o pacote de dados contiver o nome ou número do cliente, preencher o campo _nome.

2.3 Classe TClasseBaseBancoDados

Esta classe abstrata define 3 eventos de tratamento de exceções de banco de dados: ExceçãoAberturaBancoDados, ExceçãoFechamentoBancoDados e ExceçãoBancoDados, e 4 métodos public: Abrir(), Fechar(), Limpar() e Armazenar(). Dentre eles, Abrir() é um método abstrato; nas classes derivadas, pode-se adicionar seu próprio código (ver a seção de implementação do demo). O método Fechar() fecha a conexão com o banco de dados. O método Limpar() é invocado em Fechar() — para limpar recursos relacionados antes de fechar o banco de dados. O método virtual Armazenar() é usado para o armazenamento de dados. O framework EMTASS fornece duas classes derivadas desta classe base: TClasseBaseSqlServer e TClasseBaseBancoDadosOleDb, capazes de atender a necessidades gerais de aplicação de banco de dados.

  1. Introdução ao Uso do Framework

3.1 Passos Gerais

O uso do framework EMTASS inclui os seguintes passos:

  • Especializar uma classe derivada de TClasseBaseSqlServer ou TClasseBaseBancoDadosOleDb que atenda às necessidades:
    • Adicionar campos de membros, como: DbCommand, DbDataAdapter, etc.;
    • Sobrescrever o método virtual Armazenar() da classe TClasseBaseBancoDados, escrevendo o código de implementação para salvar pacotes de dados no banco de dados;
    • No método AnalisarDatagrama() de TClasseBaseSessao, invocar diretamente ou indiretamente o método Armazenar().
  • Especializar uma classe derivada de TClasseBaseSessao que atenda à lógica de processamento de negócios:
    • Sobrescrever o método ResolverBufferSessao() para extrair textos de pacotes de dados do buffer de acordo com as regras do pacote e armazená-los na fila de pacotes;
    • Sobrescrever o método AnalisarDatagrama(), adicionando funcionalidades conforme os requisitos anteriores.
  • Especializar uma classe derivada de TClasseBaseServidorSocket que atenda às necessidades:
    • Definir uma classe derivada da classe genérica TClasseBaseServidorSocket (este passo pode ser omitido);
    • Criar um objeto da classe derivada da classe genérica, fornecendo a string de conexão do banco de dados e a porta TCP de comunicação no construtor;
    • Configurar parâmetros como o comprimento máximo de pacotes de dados do objeto da classe derivada;
    • Implementar métodos de tratamento de eventos relevantes do objeto da classe derivada.

3.2 Construtores, Propriedades, Métodos e Eventos de TClasseBaseServidorSocket

A classe genérica TClasseBaseServidorSocket fornece todas as interfaces públicas (propriedades, métodos e eventos) do framwork EMTASS, incluindo as propriedades e eventos públicos dos objetos inline TClasseBaseSessao e TClasseBaseBancoDados.

1) Construtor de TClasseBaseServidorSocket

Existem duas versões sobrecarregadas, com a porta padrão sendo 3130. Considerando a extensibilidade, a string de conexão do banco de dados deve ser fornecida, como mostra o código a seguir:

public TClasseBaseServidorSocket(string strConexaoDb)
{
    this.Iniciar(strConexaoDb);
}

public TClasseBaseServidorSocket(int portaTcp, string strConexaoDb)
{
    _portaServidor = portaTcp;
    this.Iniciar(strConexaoDb);
}

O método Iniciar() no construtor conclui as tarefas específicas de inicialização.

2) Propriedades Públicas de TClasseBaseServidorSocket

  • PortaServidor: Número da porta do servidor, valor padrão 3130.
  • Fechado: O servidor já foi encerrado.
  • EscutaPausada: O servidor está temporariamente pausado para requisições de conexão de clientes.
  • TempoEsperaLoop: Tempo de espera (ms) no método Socket.Listen, valor padrão 25ms.
  • TamanhoMaximoDatagrama: Comprimento máximo permitido para pacotes de dados, valor padrão 1024K.
  • ComprimentoMaximoFilaEscuta: Comprimento máximo da fila de escuta, valor padrão 16.
  • TamanhoMaximoBufferRecepcao: Comprimento máximo permitido para o buffer de recepção de pacotes, valor padrão 16K.
  • ContagemMaxMesmoIP: Número máximo de Sockets de sessão permitidos para o mesmo endereço IP, valor padrão 64.
  • ComprimentoMaximoTabelaSessoes: Comprimento máximo permitido para a tabela de sessões, valor padrão 1024.
  • TimeoutMaximoSessao: Intervalo máximo de tempo limite permitido para sessões (s), valor padrão 120s.
  • ContagemDatagramasErro: Número de pacotes de dados com erro.
  • ContagemDatagramasRecebidos: Número de pacotes de dados recebidos.
  • ContagemExcecoesServidor: Número de exceções do servidor.
  • ContagemSessoes: Número atual de sessões.
  • ContagemExcecoesSessao: Número de exceções de sessão.
  • ListaCoreInfoSessoes: Lista de informações da tabela de sessões atual.

3) Métodos Públicos de TClasseBaseServidorSocket

  • Iniciar(): Iniciar o servidor.
  • Parar(): Encerrar o servidor.
  • PausarEscuta(): Pausar a escuta de requisições de conexão.
  • RetomarEscuta(): Retomar a escuta de requisições de conexão.
  • Descartar(): Encerrar o servidor e liberar recursos do sistema.
  • FecharSessao(): Fechar uma sessão.
  • FecharTodasSessoes(): Fechar todas as sessões.
  • EnviarParaSessao(): Enviar mensagem para uma sessão.
  • EnviarParaTodasSessoes(): Enviar mensagem para todas as sessões.

4) Eventos de TClasseBaseServidorSocket

  • ExceçãoFechamentoBancoDados: Exceção de fechamento do banco de dados.
  • ExceçãoBancoDados: Exceção do banco de dados.
  • ExceçãoAberturaBancoDados: Exceção de abertura do banco de dados.
  • DatagramaAceito: Um pacote de dados completo foi aceito.
  • ErroDelimitadorDatagrama: Erro no delimitador do pacote de dados.
  • ErroDatagrama: Erro no pacote de dados.
  • DatagramaTratado: Um pacote de dados foi processado.
  • ErroTamanhoExcedidoDatagrama: Erro de pacote de dados acima do tamanho.
  • ServidorIniciado: Após o início do servidor.
  • ServidorEncerrado: Após o encerramento do servidor.
  • EscutaServidorPausada: Após a pausa nas requisições de conexão do servidor.
  • EscutaServidorRetomada: Após a retomada das requisições de conexão do servidor.
  • ExceçãoServidor: Exceção do servidor.
  • SessaoRejeitada: Requisição de conexão foi rejeitada.
  • SessaoConectada: Uma conexão de sessão foi estabelecida.
  • SessaoDesconectada: Uma sessão foi desconectada.
  • ExceçãoRecepcaoSessao: Exceção de recepção de dados da sessão.
  • ExceçãoEnvioSessao: Exceção de envio de dados da sessão.
  • TimeoutSessao: Tempo limite da sessão.

3.3 Introdução ao Demo do Pacote de Download

O pacote de download inclui o código-fonte do framework EMTASS e um Demo. O arquivo de solução do Demo para VS2005 é EMTASS.sln, contendo dois projetos: projeto do servidor e projeto do cliente. Os arquivos compilados na pasta /bin/ podem ser executados diretamente: primeiro iniciar o servidor e, em seguida, executar o cliente.

1) Demo do Lado do Servidor

O lado do servidor inclui duas partes: primeiro, o programa de formulário do servidor receptor; segundo, um banco de dados Access. O programa de formulário do lado do servidor contém a seguinte implementação:

  • Classe derivada de TClasseBaseSessao, TTesteSessao: Sobrescreve os métodos de tratamento de eventos OnErroDelimitadorDatagrama() e OnErroTamanhoExcedidoDatagrama(). Sobrescreve o método de análise de pacotes AnalisarDatagrama() e adiciona um método personalizado Armazenar().
  • Classe derivada de TClasseBaseBancoDados, TTesteBancoDadosAccess: Adiciona um campo OleDbCommand _comando. Sobrescreve os métodos Abrir() e Armazenar(). No método Abrir() sobrescrito, cria o objeto _comando e seus objetos de parâmetro, fornecendo o código SQL da instrução Insert do banco de dados. No método Armazenar() sobrescrito, salva o IP, nome da sessão (NomeSessao) e comprimento do pacote de dados no banco de dados Access. O método Armazenar() será invocado pelo método AnalisarDatagrama() de TTesteSessao().
  • Objeto TClasseBaseServidorSocket<>: Após fornecer a string de conexão do banco de dados, utiliza as duas classes derivadas definidas anteriormente para criar o objeto da classe genérica. Em seguida, registra os métodos de implementação dos eventos do objeto. Os métodos de eventos registrados incluem: exibir a contagem de estatísticas do servidor, exibir o estado de execução do servidor.

O código principal do lado do servidor é o seguinte:

public partial class DemoServidorSocket : Form
{
    TClasseBaseServidorSocket<TTesteSessao, TTesteBancoDadosAccess> _servidorSocket;

    public DemoServidorSocket()
    {
        InitializeComponent();
    }

    private void DemoServidorSocket_Load(object sender, EventArgs e)
    {
        cb_tamanhoMaximoDatagrama.SelectedIndex = 1;
        
        // String de conexão do banco de dados
        string strConexao = "Provider=Microsoft.Jet.OLEDB.4.0; Data Source = DemoBancoDadosAccess.mdb;";
        
        _servidorSocket = new TClasseBaseServidorSocket<TTesteSessao, TTesteBancoDadosAccess>(strConexao);  // Objeto servidor
        _servidorSocket.TamanhoMaximoDatagrama = 1024 * int.Parse(cb_tamanhoMaximoDatagrama.Text);  // Tamanho máximo do pacote

        this.AnexarEventosServidor();  // Anexa todos os eventos do servidor
    }

    private void DemoServidorSocket_FormClosing(object sender, FormClosingEventArgs e)
    {
        _servidorSocket.Descartar();  // Encerra o processo do servidor
    }

    private void AnexarEventosServidor()
    {
        _servidorSocket.ServidorIniciado += this.ServidorSocket_Iniciado;
        _servidorSocket.ServidorEncerrado += this.ServidorSocket_Parado;
        _servidorSocket.EscutaServidorPausada += this.ServidorSocket_Pausado;
        _servidorSocket.EscutaServidorRetomada += this.ServidorSocket_Retomado;
        _servidorSocket.ExceçãoServidor += this.ServidorSocket_Exceção;

        _servidorSocket.SessaoRejeitada += this.ServidorSocket_SessaoRejeitada;
        _servidorSocket.SessaoConectada += this.ServidorSocket_SessaoConectada;
        _servidorSocket.SessaoDesconectada += this.ServidorSocket_SessaoDesconectada;
        _servidorSocket.ExceçãoRecepcaoSessao += this.ServidorSocket_ExceçãoRecepcaoSessao;
        _servidorSocket.ExceçãoEnvioSessao += this.ServidorSocket_ExceçãoEnvioSessao;

        _servidorSocket.ErroDelimitadorDatagrama += this.ServidorSocket_ErroDelimitadorDatagrama;
        _servidorSocket.ErroTamanhoExcedidoDatagrama += this.ServidorSocket_ErroTamanhoExcedidoDatagrama;
        _servidorSocket.DatagramaAceito += this.ServidorSocket_DatagramaRecebido;
        _servidorSocket.ErroDatagrama += this.ServidorSocket_ErroDatagrama;
        _servidorSocket.DatagramaTratado += this.ServidorSocket_DatagramaTratado;

        _servidorSocket.ExceçãoAberturaBancoDados += this.ServidorSocket_ExceçãoAberturaBancoDados;
        _servidorSocket.ExceçãoFechamentoBancoDados += this.ServidorSocket_ExceçãoFechamentoBancoDados;
        _servidorSocket.ExceçãoBancoDados += this.ServidorSocket_ExceçãoBancoDados;

        _servidorSocket.MostrarMensagemDebug += this.ServidorSocket_MostrarMensagemDebug;
    }
    //... outro código
}

Abaixo, uma imagem da execução do Demo do lado do servidor.

2) Demo do Cliente

Cria um objeto TcpClient para simular a comunicação de um cliente remoto com o servidor. Abaixo, uma imagem da execução do Demo do cliente:

  1. Resultados Gerais dos Testes

  • Configuração da Máquina: CPU dual-core E2140, frequência de 1.6G, RAM de 1G (incluindo memória da placa de vídeo).
  • Teste de Correção
    • Método de Teste: Os pacotes de dados do cliente contêm uma string de comprimento; o lado do servidor detecta e compara, para julgar a correção da transmissão do pacote de dados.
    • Situação do Teste: Inicia-se o servidor em uma máquina única, executam-se 7 programas clientes, dos quais 3 são fontes de interferência — realizando operações contínuas de conexão/desconexão, e os outros 4 enviam pacotes de dados continuamente. Após cerca de 80 minutos de operação, verificou-se que o servidor recebia em média 15 pacotes por segundo, sem exceções ocorridas, pacotes errados ou crescimento significativo na fila de pacotes de dados (estável abaixo de 3).
  • Teste de Velocidade
    • Método de Teste: Executa-se o servidor em uma máquina única, depois executam-se 20 programas clientes, enviando pacotes de dados a uma velocidade de 10-50ms.
    • Situação do Teste: Após 30 minutos de operação, verificou-se que o servidor recebia em média 60 pacotes por segundo, sem apresentar crescimento significativo na fila de pacotes de dados.
  • Teste de Estabilidade: O Demo do cliente contém uma operação contínua de desconexão imediatamente após a conexão. Nessa situação, o lado do servidor não apresentou exceções. Além disso, o autor executou o servidor por 1 hora com 30 sessões de cliente, sem exceções do lado do servidor, embora alguns clientes tenham encerrado anormalmente.
  • Teste de Concorrência: Foram executados simultaneamente 30 Demos de cliente, sem ocorrência de exceções do servidor, e o limite superior da fila de pacotes de dados não ultrapassou 5.
  • Conclusão dos Testes: Em uma máquina, o EMTASS 2.0 realizou recebimento e processamento corretos, podendo processar mais de 60 pacotes por segundo, com operação estável e confiável, apresentando boa capacidade de processamento concorrente. Os principais problemas encontrados durante os testes foram:
    • Alta taxa de uso de CPU: Obviamente causada pelas operações em loop das três threads. Nas versões 1.0 e 1.1 do EMTASS, usava-se Thread.Sleep(_tempoEspera) para aguardar por um período. O objetivo do design deste framework é ser um servidor Socket dedicado, e esta operação foi omitida para aumentar a capacidade de processamento. Se necessário, esta funcionalidade será adicionada em versões futuras;
    • Servidor rejeita requisições de conexão após iniciar: Especialmente após o encerramento e reinício, esse fenômeno é propenso a ocorrer; após múltiplos ciclos de reinício/encerramento, retorna ao normal. O autor supõe que seja um problema de sincronização de liberação de recursos do objeto _socketServidor;
    • Requisição de conexão de cliente rejeitada: Às vezes, após ocorrer um erro no cliente, ao tentar reconectar, a requisição é rejeitada. Existem duas possibilidades: a primeira é o problema do objeto do servidor mencionado acima; a segunda é que o Socket do cliente é bloqueado durante recebimento, envio e desconexão, cuja causa específica aguarda análise. Quando essa situação ocorre, ainda é possível conectar-se ao servidor após múltiplas operações de conexão/desconexão.

Claramente, este ambiente de teste e seus resultados necessitam de verificação adicional, mas existe amplo espaço para melhorias. Em particular, o valor máximo da fila de pacotes de dados geralmente não excede 5, indicando que o servidor processa os pacotes imediatamente após o recebimento, apresentando bom desempenho de concorrência.

  1. Conclusão e Perspectivas

O EMTASS 2.0 apresentado neste artigo é um resumo do trabalho do autor e também um pequeno resumo do aprendizado sobre design de classes baseado em .NET, design de componentes e design de padrões. O objetivo do autor é continuar a modificar e aperfeiçoar, projetar e implementar um framework de servidor de recebimento de pacotes de dados Socket confiável e estável, com boa extensibilidade e fácil utilização. Como o código e as ideias iniciais vieram de arquiteturas de código aberto e concepções de design de terceiros, o EMTASS também segue práticas comuns de código aberto: publicando o código-fonte e os conceitos de design.

Um servidor baseado no EMTASS 1.0 possui um registro de 30 dias de operação contínua sem problemas. Embora o framework EMTASS 2.0 possua extensibilidade e tenha passado por testes gerais, ainda não foi colocado em operação real, necessitando de tempo para verificação e validação prática. Atualmente, o .NET 3.0 e o 3.5 Framework fornecem IOCP (I/O Completion Ports) com melhor capacidade de processamento concorrente assíncrono. O autor pretende, combinando com a nova plataforma de execução, aperfeiçoar e atualizar o EMTASS, publicando os planos de aperfeiçoamento e atualização. Se algum leitor encontrar problemas ao usar o EMTASS 2.0, ou tiver sugestões ou ideias melhores, por favor, não hesite em apontar correções.

  1. Versões e Código-Fonte

  • EMTASS 1.0, dezembro de 2005.

  • EMTASS 1.1, setembro de 2008.

  • EMTASS 2.0, 27 de outubro de 2008.

    • Redesenhou os nomes das classes e suas relações, aumentando a extensibilidade do framework. Refatorou grande parte do código.
    • A fila de pacotes de dados de TClasseBaseSessao substituiu a fila de pacotes de dados de TClasseBaseServidorSocket.
    • Todos os métodos de comunicação foram encapsulados em TClasseBaseSessao.
  • EMTASS 2.1, 9 de novembro de 2008.

    • Adicionou a classe de gerenciamento de buffers GerenciadorBuffers, gerenciando dois buffers de envio/recepção reutilizáveis.
    • TClasseBaseServidorSocket adicionou as seguintes propriedades:
      • TamanhoBufferRecepcao: Tamanho do buffer de recepção (padrão: 16K).
      • TamanhoBufferEnvio: Tamanho do buffer de envio (padrão: 16K).
      • IntervaloTempoVerificarFilaDatagramas: Intervalo de tempo (Sleep) da thread de processamento de pacotes (padrão: 100ms).
      • IntervaloTempoVerificarTabelaSessoes: Intervalo de tempo (Sleep) da thread de limpeza de recursos da sessão (padrão: 100ms).
    • TClasseBaseServidorSocket adicionou vários construtores, incluindo número máximo de tarefas e tamanho do buffer.
    • TClasseBaseServidorSocket utilizou a classe de exclusão mútua Mutex para evitar a criação de dois servidores na mesma máquina.
    • TClasseBaseSessao, após o recebimento/envio assíncrono ser concluído, invocou o método IAsyncResult.AsyncWaitHandle.Close().
    • TClasseBaseSessao, ao receber dados, utilizou o buffer público BufferRecepcao do GerenciadorBuffers.
    • TClasseBaseSessao, ao enviar dados, se o comprimento dos dados for menor que o buffer de envio, utilizou BufferEnvio; caso contrário, solicitou um array de bytes para envio.
    • Resultados do teste: 10 clientes de interferência (100ms de conexão/desconexão contínua) e 15 clientes de dados (3 de 100K/100ms, 3 de 100K/50ms, 4 de 100K/20ms, 2 de 1M/1s, 2 de 1M/500ms, 1 de 1M/10s). Taxa de uso de CPU de 70-90%, velocidade de 40/s, sem pacotes errados, número de conexões exibidas diretamente entre 30-50.
  • Baixar código-fonte do EMTASS 1.1: 1) Usuários registrados no csdn, 2) Download direto de rede internacional.

  • Baixar código-fonte e demo do EMTASS 2.0: 1) Usuários registrados no csdn, 2) Download direto de rede internacional.

  • Baixar código-fonte e demo do EMTASS 2.1: Usuários csdn (com bugs), 2) Download direto de rede internacional (corrigiu bugs e a classe GerenciadorBuffers).

Tags: C# .NET socket EMTASS IOCP

Publicado em 6-16 19:05 por Thomas