quinta-feira, 28 de fevereiro de 2008

POSIX Sockets e Windows Sockets (Parte III)

Para fechar esta seqüência de artigos, vamos ver como trabalhar com sockets assíncronos, usando a winsock, agora em uma aplicação sem janelas.
Lembrando que podemos utilizar estas técnicas para trabalhar com qualquer tipo de comunicação via socket no Windows, tanto para aplicações do tipo cliente como servidor, sockets com e sem conexão (UDP, TCP, etc). Os passos que seguimos para criar estes recursos são:
  1. Criar um tratador para o socket;
  2. Para um servidor, fazer bind, entrar em listen e aceitar conexões;
  3. Para um cliente, conectar;
  4. Enviar e receber dados;
  5. Trabalhar de forma assíncrona e definir timeouts;
  6. Tratar múltiplos clientes em um servidor;
No artigo anterior, observamos que a winsock pode ser bastante compatível com as implementações de sockets POSIX. Porém desde o processamento assíncrono através de mensagens, esta compatibilidade deixa de existir, e o que veremos a seguir é ainda mais particular da winsock.

Para realizar comunicação assíncrona, precisávamos ter antes um tratador de janela no qual a aplicação receberia mensagens pertinentes a operações com sockets. Mas o que fazer quando não temos aplicações com janelas? - tá, Microsoft, Windows, janelas... para que alguém iria querer fazer uma aplicação sem janelas? por vários e bons motivos, podem acreditar.
A solução é utilizar a função WSAEventSelect, que é similar a WSAAsyncSelect, porém não utiliza o tratador de mensagens para processar as operações da Winsock, mas sim um objeto de evento Win32.

Eventos de rede podem ser passados a objetos em uma aplicação, e para isto basta criar um objeto de evento com a função WSACreateEvent, e configurar quais eventos desejamos receber na aplicação com a função WSAEventSelect.
Vamos ver então como utilizar esta função, que tem como assinatura:
int WSAEventSelect(
__in SOCKET s,
__in WSAEVENT hEventObject,
__in long lNetworkEvents
);
o parâmetro s diz respeito ao socket criado (usando o processo e funções já citadas anteriormente, tanto para criar uma aplicação do tipo servidor ou do tipo cliente); hEventObject é o tratador de evento criado pela função WSACreateEvent; e lNetworkEvents é a máscara de eventos que deseja-se receber naquele objeto (similar a máscara usada em WSAAsyncSelect, descrita antes aqui).

Então a coisa funciona assim: crie um tratador de socket, para o fim que quiseres. Depois, crie um objeto de evento com WSACreateEvent. Com este objeto, passe os eventos do socket criado que deseja processar. Mas e daí, como processar os eventos recebidos?
Para isto, um laço de tratamento de eventos precisa ser criado, e dentro dele aguardar que o objeto seja sinalizado pela Winsock, e depois identificar o evento recebido. Para aguardar os eventos, utlizamos a função WSAWaitForMultipleEvents, e para identificar o evento utilizamos a função WSAEnumNetworkEvents.
O pseudo código seria algo assim:

criar socket e prepará-lo como desejado;
criar evento com WSACreateEvent;
...
while(TRUE)
{
WSAWaitForMultipleEvents()
WSAEnumNetworkEvents()
...
processar socket conforme evento recebido
...
}

Vamos ver então como usar estas duas funções.
DWORD WSAWaitForMultipleEvents(
__in DWORD cEvents,
__in const WSAEVENT* lphEvents,
__in BOOL fWaitAll,
__in DWORD dwTimeout,
__in BOOL fAlertable
);
Usar WSAWaitForMultipleEvents é bastante direto, sendo os dois principais parâmetros o número de objetos de eventos que estará sendo trabalhado, cEvents e a lista de objetos de eventos, lphEvents. O parâmetro fWaitAll determina se deseja-se retornar da função quanto todos os objetos forem sinalizados ou qualquer um deles; dwTimeout é o tempo (em milisegundos) a ser aguardado pelo evento, e fAlertable determina se a thread corrente deve ser colocada em estado de wait (normalmente utilizado com completion routines, que vamos falar mais adiante).

A função WSAEnumNetworkEvents é bastante simples, e possui apenas três parâmetros:
int WSAEnumNetworkEvents(
__in SOCKET s,
__in WSAEVENT hEventObject,
__out LPWSANETWORKEVENTS lpNetworkEvents
);

onde o parâmetro s é o socket que recebeu o evento, hEventObject é o objeto de evento que deseja-se reinicializar, e lpNetworkEvents é um ponteiro para a estrutura WSANETWORKEVENTS, que possui a máscara FD* de eventos recebidos e uma lista de erros ocorridos. Apesar do parâmetro hEventObject ser opcional, eu recomendo ser informado na própria função, pelo seguinte fato: uma vez recebido um evento, é necessário reinicializar (reset) o objeto, isto poderia ser feito utilizando WSAResetEvent, porém, passando o objeto para WSAEnumNetworkEvents, a própria função encarrega-se disto, com a vantagem de fazer a operação atomicamente.

Importante observar que a chamada a WSAEventSelect deixa o socket automaticamente como não bloqueante, e as operações de envio/recebimento de dados (ou ainda connect e accept) retornam imediatamente, e WSAGetLastError pode retornar WSAEWOULDBLOCK, significando que não foi possível completar a operação imediatamente, o que parece sensato já que o socket é não bloqueante.
Também vale lembrar que as funções de reativação para WSAEventSelect funcionam da mesma forma que para WSAAsyncSelect.

Então, se projetarmos uma aplicação, seja ela cliente ou servidor onde cada socket possui seu próprio laço de tratamento de eventos em uma thread, conseguimos então que esta aplicação seja capaz de processar comunicação em diversas vias, assincronamente. Vale observar que, no caso de uma aplicação servidor, com sockets conectados, um socket criado através de accept irá receber os eventos no mesmo objeto associado ao socket que estava em listen. Para que seja alterado o objeto de evento para este novo socket, deve-se então chamar novamente WSAEventSelect para ele.
Uma aplicação bem construída com estes recursos pode ter escalabilidade suficiente para o tamanho desejado, construindo um código relativamente simples.

Uma forma interessante de saber se uma operação de envio ou recebimento passou um certo tempo desejado (i.e, saber se deu timeout), pode ser concebida através de eventos de timers, usando a função SetTimer, mas para isto, algum identificador de janelas válido deve existir. Outra forma seria controlar manualmente mesmo, medindo o tempo passado desde a solicitação, e verificando se o laço de processamento de eventos recebe algum evento (como enviar ou receber) ainda dentro de um tempo desejado.

Escalabilidade do jeito Microsoft de ser

Outro recurso interessante disponível na winsock é poder trabalhar com o que é chamado overlapped I/O com sockets, e completion routines. A vantagem de utilizar estes recursos é deixar o Windows se preocupar com as tarefas de enviar e receber dados assincronamente, tirando a responsabilidade da aplicação.
Não entrarei em detalhes sobre a implementação destes recursos, uma vez que pode ser bastante complexo para iniciantes. Talvez escreva um outro artigo sobre isto.
Mas para quem quiser tentar, ai vai algumas dicas:
  • usar o flag WSA_FLAG_OVERLAPPED na criação do socket (último parâmetro da função WSASocket);
  • Criar um completion port com CreateIoCompletionPort, ou um objeto de evento que deverá ser sinalizado quando a operação terminar;
  • usar as funções WSASend e WSAReceive para comunicar. Observar os parâmetros lpOverlapped e lpCompletionRoutine, que devem ser informados para que o processamento assíncrono possa ser tratado corretamente pela aplicação;
Bem, aqui encerro os artigos sobre sockets POSIX/BSD e Winsock. Cada um com sua parcela de facilidade e complexidades. Qual melhor abordagem a usar? Depende muito da sua necessidade, e da aplicação que se vai construir.
É importante ter em mente que nem sempre a abordagem mais complexa gera os melhores resultados.

Até a próxima.

Referências
- Windows Sockets 2: http://msdn2.microsoft.com/en-us/library/ms740673(VS.85).aspx
- Write Scalable Winsock Apps Using Completion Ports: http://msdn2.microsoft.com/en-us/magazine/cc302334.aspx

Nenhum comentário: