quinta-feira, 7 de fevereiro de 2008

POSIX Sockets e Windows Sockets (Parte II)

Seguindo nossa aventura no mundo do sockets, vamos entrar agora no mundo da Winsock.
Nossas intenções continuam as mesmas, realizar um conjunto básico de operações com sockets, que permita criar funcionalidades comuns. Para relembrar, listamos:
  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;
Bem, em muitos aspectos a Winsock é bastante compatível com POSIX. Eu diria que as operações mais básicas, como criar socket, fazer bind, send/recv são 100% compatíveis, com as mesmas estruturas de dados e assinaturas de funções.
Os items de 1 até 4 na nossa lista podem ser implementados na winsock da mesma forma que visto no post anterior, com POSIX. Então não vamos entrar em detalhes nestes items, mas vamos estudar algumas peculiaridades.

Primeiramente, a winsock precisa ser inicializada. Talvez seja herança do Windows noventa e alguma coisa, ou para podermos trabalhar com diversos comportamentos e versões, mas é algo que precisa ser feito.
A função WSAStartup faz o trabalho, recebendo 2 parâmetros que indicam a versão que se quer trabalhar, e retornando a versão que foi inicializada. A versão atual é chamada de Winsock2, introduzida com o Windows 95 se não me engano. E tem varias minor version desde então.

Algo que não devemos ignorar ao estudar a Winsock é são as funções que tem o prefixo WSA. Elas são "melhorias" da versão da função compativel com POSIX (ao menos penso que esta foi a visão dos engenheiros que criaram ela). Estas funções WSA* permitem um nivel mais refinado de controle e uma maior quantidade de recursos, como veremos adiante.

Bem, uma vez inicializada a winsock, podemos fazer as operações da nossa lista praticamente da mesma forma que com sockets POSIX. Mas partindo do item 4 algumas coisas começam a ficar interessantemente diferentes. A função fcntl não existe na API documentada do windows, então definir um socket como não síncrono é feito utilizando IOCTL, que no mundo winsock é feito com a função ioctlsocket, usando a constante FIONBIO como parâmetro. A lista dos códigos IOCTL documentados pode ser encontrada aqui.
Assim temos um socket assíncrono, compatível com POSIX sockets, e que combinado com threads permitem codificar tranquilamente os itens 5 e 6 da mesma forma que POSIX sockets. Ao menos em teoria :)
Ah, e uma função importante é a WSAGetLastError, que como o nome diz, retorna o erro da ultima função de socket chamada. Também é possível retornar o erro por referência (para quando é necessário ser thread-safe) usando as funções com prefixo WSP*.

Mas, lembrando das funções WSA*, temos como fazer processamento assíncrono de várias formas, e que fornece bastante emoção.
A primeira delas, é utilizando a pilha de mensagens do Windows para interpretar e processar as operações com sockets, usando a função WSAAsyncSelect. Quando chamada, a função coloca os socket como não bloqueante, e programa a aplicação para receber notificações de eventos de rede na pilha de mensagens de algum identificador de janela do programa. Isto vale para qualquer operação de socket, seja em uma aplicação cliente, seja uma aplicação servidor.
Funciona da seguinte forma: da mesma maneira que a função select retorna a disponibilidade do socket para trafegar dados, conectar ou aceitar conexões, uma mensagem é recebida pelo programa quando estas operações estiverem disponíveis. Por exemplo, o programa receberá uma mensagem com um código específico para leitura de dados, quando o socket estiver disponível para receber dados.

Uma breve introdução sobre janelas e tratamento de mensagens.

Uma aplicação Windows com janelas, precisa ter obrigatoriamente um laço de processamento de mensagens, o laço principal do programa. Este laço tem 4 parâmetros para trabalhar: o identificador da janela, a mensagem recebida e dois parâmetros inteiros de 32 bits com informações particulares de cada mensagem, conhecidos como wParam e lParam.
Geralmente uma função que processa estes parâmetros (chamada de WindowProc) é associada no momento da criação da janela, e disparada automaticamente quando a janela recebe alguma mensagem.
Este é um aspecto bastante particular e importante na programação para Windows, e muitas ferramentas de desenvolvimento RAD já montam esta estrutura no código, facilitando a vida do programador.

Analisando WSAAsyncSelect

Para trabalhar com sockets assíncronos através da fila de mensagens, nada muda nos passos iniciais de criação e configuração do socket. Ou seja, até o momento da chamada de WSAAsyncSelect, basta criar o socket, configurar porta/endereço, conect/bind/listen, da mesma forma que se faria para trabalhar com processamento assíncrono.

A função WSAAsyncSelect aceita parâmetros que permite ao programador informar como sua função de processamento de mensagens deve se comportar ao receber eventos de socket. Vamos ver sua assinatura (em C, direto da MSDN) :
int WSAAsyncSelect(
__in SOCKET s,
__in HWND hWnd,
__in unsigned int wMsg,
__in long lEvent
);
  • SOCKET s é o identificador do socket que está sendo trabalhado;
  • HWND hWnd é o identificador da janela que processará as mensagens de rede;
  • unsigned int wMsg é o código númerico da mensagem (mensagem passada a janela)
  • long lEvent é a combinação de flags que define quais mensagens a janela em questão estará processando.
O parâmetro hWnd deve ser o identificador de uma janela já com uma função WindowProc associada. Já wMsg deve ser um número que identifique uma mensagem que não esteja sendo usada. A API do Windows utiliza constantes base para mensagens de usuário, que podem ser valores acima de WM_USER ou acima de WM_APP.

Os flags (parâmetro lEvent) que determinam os tipos de evento de rede que serão processados para o par socket/janela, podem ser FD_READ, FD_WRITE, FD_ACCEPT, FD_CONNECT, FD_CLOSE e mais alguns outros que não são interessantes para nosso propósito no momento.
Estes flags podem ser usado individualmente ou compostos através de OR. Isto quer dizer que para que o programa receba uma mensagem dizendo que existe algo disponível para ser lido, lEvent deve conter FD_READ, para saber quando pode ler e/ou gravar, FD_READ|FD_WRITE, e assim por diante. Chamadas consecutivas de WSAAsyncSelect anulam as últimas chamadas. Por exemplo, realizar
WSAAsyncSelect(..., FD_READ); WSAAsyncSelect(..., FD_WRITE);
a última chamada anulará a primeira, fazendo com que o programa receba apenas eventos correspondentes a FD_WRITE.
Ao processar a mensagem de rede, a função WindowProc recebe em wParam o socket que recebeu o evento, e em lParam o código de erro (caso ocorrido) e o código do evento recebido (flag). O código de erro pode ser extraído com a macro WSAGETSELECTERROR, e o flag com a macro WSAGETSELECTEVENT. Vamos analisar cada um destes flags:
  • FD_READ, recebido quando o socket está pronto para receber dados, e pode chamar funções como recv/recvfrom;
  • FD_WRITE, recebido quando o socket está disponível para enviar dados, com send/sendto;
  • FD_ACCEPT, recebido quando um socket em listen está disponível para aceitar uma conexão;
  • FD_CONNECT, recebido quando uma chamada a connect foi realizada, e o socket está conectado (aplicável a sockets com conexão);
  • FD_CLOSE, recebido quando o socket foi encerrado
O Windows toma cuidado para não sobrecarregar a aplicação com múltiplos eventos de um mesmo tempo. Isto quer dizer que quando é recebido um evento de disponibilidade de leitura (FD_READ), eventos de leitura de dados subsequentes não serão passados a aplicação, até que esta execute alguma função de reativação. Cada tipo de mensagem recebida possue um conjunto de funções de ativação, onde usa-se para
  • FD_READ - recv, recvfrom, WSArecv, WSArecvfrom
  • FD_WRITE - send, sendto, WSAsend, WSASendto
  • FD_ACCEPT - accept ou WSAAccept
  • FD_CLOSE e FD_CONNECT não necessitam de reativação.
Um tratador de socket resultante de um accept, tem as mesmas características do socket que estava em listen e aceitou a conexão. Ou seja, os eventos para aquele socket serão tratados da mesma forma (com os mesmos flags) que o socket que recebeu a conexão.
Vale observar que, para cada mensagem recebida com FD_READ, apenas um recv (ou similar é necessário), mesmo que ainda existam dados no buffer de leitura. Caso queria chamar multiplas vezes um recv, é recomendado desabilitar FD_READ para o socket, pois se ocorrer o acaso de uma mensagem do tipo FD_READ ser recebida pelo programa e um recv estiver sendo executado, este deve falhar com WSAEWOULDBLOCK.

Pra finalizar, a organização do código poderia ficar assim:

HANDLE wnd;
SOCKET s;

#define NETWORK_MSG WM_USER+100
...
/* criar um socket que servira como cliente de conexao */
...

/* configura para receber mensagens quando disponibilizar gravacao, leitura, conexao e fechamento */
WSAAsyncSelect(s, wnd, NETWORK_MSG, FD_READ|FD_WRITE|FD_CONNECT|FD_CLOSE);
...

/* aqui o tratador de mensagens da aplicacao */
LRESULT CALLBACK AppWindowProc(HWND hwnd, unsigned int msg, WPARAM wparam, LPARAM lparam)
{

/* aqui, hwnd é igual a wnd definido anteriormente */
switch(msg)
{
/* provavelmente varias mensagens obrigatorias do programa estarao aqui */
case WM_...:
...
/* agora processamos os eventos do socket */
case FD_READ:
recv(...);
...
case FD_WRITE:
send(...);
...
case FD_ACCEPT:
SOCKET sock = accept(...);
...

}
return DefWindowProc(
}


E a referência clássica na internet:

- Windows Sockets 2: http://msdn2.microsoft.com/en-us/library/ms740673(VS.85).aspx

Nenhum comentário: