Resumo

  • O Windows Sockets 1.1 deu aos programadores do Windows uma interface TCP/IP inspirada em Berkeley, mas não podia transplantar sem mudanças o comportamento bloqueante de Berkeley para o Windows de 16 bits.
  • Uma chamada Winsock bloqueada precisava continuar distribuindo mensagens do Windows. O desktop cooperativo seguia respondendo, com limites rigorosos para chamadas aninhadas, reentrância e duração dos buffers.

A chamada de socket deveria esconder a rede

Um socket dá ao programa uma forma estável de enviar e receber dados sem prendê-lo ao fornecedor da pilha de rede subjacente. Era esse o acordo que os desenvolvedores do Windows buscavam. Também queriam começar por um modelo conhecido: os sockets de Berkeley já eram o padrão de programação TCP/IP no Unix.

O projeto Windows Sockets começou em uma sessão Birds of a Feather na Interop ’91, em San José, em 10 de outubro de 1991. A especificação 1.1 registra o que veio depois: um comitê, debates por e-mail, um rascunho que aproveitou amplamente propostas da JSB e da NetManage, uma reunião organizada pela Microsoft em dezembro e um grupo de trabalho que continuou aperfeiçoando o documento. Foi uma interface construída por um grupo, não um protocolo TCP/IP imposto por um único fornecedor.

A versão 1.0 foi publicada para que fornecedores de software de rede e desenvolvedores de aplicativos pudessem começar a criar implementações e programas sob um contrato comum. A versão 1.1, datada de 20 de janeiro de 1993, trouxe esclarecimentos e pequenos ajustes com base na experiência das empresas que haviam implementado a 1.0. A sequência importa: antes de fechar o texto seguinte, o grupo já o havia confrontado com código em execução.

A especificação adotou o modelo de sockets de Berkeley para parecer familiar aos programadores de Unix e facilitar a portabilidade do código-fonte. Definiu a suíte de protocolos da Internet, com sockets de fluxo TCP e sockets de datagrama UDP. Não fingiu que a versão 1.1 padronizava todas as redes possíveis. Esse limite era útil: os desenvolvedores ganhavam uma interface conhecida, sem a promessa de que todo sistema abaixo dela se comportaria igual.

Então recv encontrou o desktop de 16 bits

A parte difícil aparece quando um programa espera. Um recv bloqueante não retorna até que os dados cheguem ou a operação seja resolvida de outra forma. Em um sistema com escalonamento preemptivo, o sistema operacional pode executar outras tarefas enquanto aquele processo espera. O Windows Sockets 1.1 não podia presumir esse tipo de escalonamento no Windows de 16 bits.

A documentação da Microsoft descreve a solução em detalhe incomum. Se uma operação bloqueante não pudesse terminar de imediato, o provedor iniciava a operação e entrava em um loop: distribuía mensagens do Windows, verificava se a chamada de socket havia terminado e repetia. Assim, o aplicativo orientado a mensagens podia continuar processando eventos das janelas enquanto a leitura de rede permanecia pendente.

Esse desenho não transformava um programa de 16 bits em um processo com escalonamento preemptivo. Ele inseria a distribuição de mensagens do Windows em uma chamada que o programador poderia tratar como uma simples espera. Mensagens recebidas durante a espera podiam levar o aplicativo a fazer outra chamada Winsock. A versão 1.1 proibia esse aninhamento: uma tarefa podia ter apenas uma chamada Winsock pendente. Uma segunda chamada aninhada falhava com WSAEINPROGRESS.

A API oferecia duas pequenas saídas. WSAIsBlocking permitia verificar se havia uma chamada bloqueada. WSACancelBlockingCall podia cancelá-la. Aplicativos mais complexos, inclusive os com várias janelas de documentos, podiam instalar WSASetBlockingHook e substituir a rotina padrão de distribuição de mensagens. O aplicativo ganhava controle, junto com a obrigação de processar as mensagens corretamente.

A interface deslocou o trabalho; não o apagou

Chamadas não bloqueantes e assíncronas evitavam o problema de reentrância do bloqueio. Elas traziam outra obrigação. Se uma operação mantivesse um ponteiro para um buffer ou outro objeto de memória, o aplicativo precisava mantê-lo válido até a conclusão. A biblioteca de rede não podia decidir por ele quando aquela memória deixaria de ser necessária.

Essa é a pequena, mas decisiva, lacuna entre uma API familiar e um programa portável. Nomes de funções compatíveis podem reduzir o custo inicial da portabilidade. Não apagam o modelo de escalonamento do sistema anfitrião, seu loop de mensagens ou a vida útil da memória. A Microsoft documentou depois os arquivos de cabeçalho fornecidos para facilitar a portabilidade de código BSD, e manteve as declarações antigas de Winsock.h para compatibilidade com aplicativos existentes após o Winsock 2 introduzir Winsock2.h. A própria compatibilidade virou software que precisava ser carregado adiante.

A versão 1.1 traçou seu limite na interface e nos tipos de socket TCP/IP que definia. Os fornecedores entregavam as implementações; os desenvolvedores de aplicativos escolhiam como esperar, cancelar, distribuir mensagens e proteger a memória. A especificação comum facilitou a conexão dessas peças. Não tornou suas responsabilidades intercambiáveis.

A escolha de engenharia marcante não foi o Windows copiar Berkeley. Foi o grupo preservar vocabulário suficiente de Berkeley para tornar a portabilidade atraente e, ao mesmo tempo, documentar onde o Windows não podia fingir que era Unix. O loop de mensagens foi o recibo: uma interface aberta reduziu um custo e mostrou exatamente onde o próximo apareceu.

Fontes