Uma das primeiras formas de se desenvolver aplicações distribuídas em um ou mais computadores foi com o uso de sockets. Com isso foram desenvolvidas diversas aplicações cliente/servidor onde cliente(s) e servidor poderiam estar em máquinas diferentes, distantes umas das outras. Atualmente temos outras ferramentas de linguagem para implementar software distribuído, mas é interessante notar como vários dos conceitos da API de sockets permanecem verdadeiros ainda hoje. No texto a seguir veremos o que é a API de socket, as seus principais funções e procedimentos e uma aplicação exemplo escrita em C. No final do texto você pode pegar o código fonte da aplicação e modificá-lo à vontade. Interface de Programa Aplicativo (API) Aplicativos cliente e servidor utilizam protocolos de transporte para se comunicarem. Quando um aplicativo interage com o software de protocolo, ele deve especificar detalhes, como por exemplo se é um servidor ou um cliente (isto é, se esperará passivamente ou iniciará ativamente a comunicação). Além disso, os aplicativos que se comunicam devem especificar detalhes adicionais (por exemplo, o remetente deve especificar os dados a serem enviados, e o receptor deve especificar onde os dados recebidos devem ser colocados). A interface que um aplicativo usa quando interage com o software de protocolo de transporte é conhecida como Interface de Programa Aplicativo (Application Program Interface, API). Uma API define um conjunto de operações que um aplicativo pode executar quando interage com o software de protocolo. Deste modo, a API determina a funcionalidade que está disponível a um aplicativo e também a dificuldade de se criar um programa para usar aquela funcionalidade. A maioria dos sistemas de programação define uma API dando um conjunto de procedimentos que o aplicativo pode chamar e os argumentos que cada procedimento espera. Normalmente, uma API contém um procedimento separado para cada operação básica. Por exemplo, uma API poderia conter um procedimento que é usado para estabelecer uma comunicação e outro procedimento que é usado para enviar dados. A API de Sockets Os padrões de protocolos de comunicação usualmente não especificam uma API que os aplicativos devem usar para interagir com os protocolos. Em vez disso, os protocolos especificam as operações gerais que devem ser fornecidas e permitem que cada sistema operacional defina a API específica que um aplicativo deve usar para executar as operações. Deste modo, um padrão de protocolo poderia sugerir que uma operação seja necessária para permitir que um aplicativo envie dados, e a API especifica o nome exato da função e o tipo de cada argumento. Embora os padrões de protocolo permitam que os projetistas de sistema operacional escolham uma API, muitos adotaram a API de sockets (sockets). A API de sockets está disponível para muitos sistemas operacionais, incluindo sistema usados em computadores pessoais (por exemplo, Windows NT e Windows 98 da Microsoft) como também vários sistemas UNIX (por exemplo, Solaris da Sun Microsystems). A API de sockets se originou como parte do sistema operacional BSD UNIX. O trabalho foi financiado por uma bolsa do governo americano, através da qual a University of California em Berkeley desenvolveu e distribuiu uma versão de UNIX que continha protocolos de ligação inter-redes TCP/IP. Muitos vendedores de computadores portaram o sistema BSD para seu hardware, e o usuaram como base em seus sistema operacionais comerciais. Deste modo, a API de sockets se tornou o padrão de facto na indústria. Para resumir: A interface entre um programa aplicativo e os protocolos de comunicação em um sistema operacional é conhecido como Interface de Programa Aplicativo ou API. A API de sockets é um padrão de facto. Sockets e Bibliotecas de Socket No UNIX BSD e nos sistemas que derivaram dele, as funções de sockets fazem parte do próprio sistema operacional. Como os sockets se tornaram mais extensamente usados, os vendedores de outros sistemas decidiram acrescentar uma API de sockets a seus sistemas. Em muitos casos, em vez de modificar seu sistema operacional básico, os vendedores criaram uma biblioteca de sockets que fornece a API de sockets. Isto é, o vendedor criou uma biblioteca de procedimentos onde cada procedimento tem o mesmo nome e argumentos que as funções de socket. Do ponto de vista de um programador de aplicativos, uma biblioteca de sockets fornece a mesma semântica que uma implementação de sockets no sistema operacional. O programa chama procedimentos de sockets, que são então providos por procedimentos do sistema operacional ou por rotinas de biblioteca. Deste modo, um aplicativo que usa sockets pode ser copiado para um novo computador, compilado, carregado junto com a biblioteca de sockets do computador e então executado - o código de origem não precisa ser mudado quando o programa for portado de um sistema de computador para outro (na prática, bibliotecas de sockets são raramente perfeitas, e às vezes ocorrem diferenças menores entre a implementação padrão de uma API de sockets e uma biblioteca de sockets - por exemplo, na forma em que os erros são tratados). Apesar de semelhanças aparentes, bibliotecas de socket têm uma implementação completamente diferente que uma API de sockets nativa provida por um sistema operacional. Diferentemente de rotinas de socket nativo, que são parte do sistema operacional, o código para procedimentos de biblioteca de sockets é unido ao programa aplicativo e reside no espaço de endereçamento do aplicativo. Quando um aplicativo chamar um procedimento da biblioteca de sockets, o controle passa para a rotina de biblioteca que, por sua vez, faz uma ou mais chamadas para as funções de sistema operacional subjacente para obter o efeito desejado. É interessante perceber que funções providas pelo sistema operacional subjacente não precisam de maneira alguma se assemelhar à API de sockets - as rotinas na biblioteca de sockets escondem do aplicativo o sistema operacional nativo e apresentam somente uma interface de sockets. Para resumir: Uma biblioteca de sockets pode fornecer a aplicativos uma API de sockets em um sistema de computador que não forneça sockets nativos. Quando um aplicativo chama um dos procedimentos de socket, o controle passa para uma rotina de biblioteca que faz uma ou mais chamadas para o sistema operacional subjacente para implementar a função de socket. Comunicação de Socket e E/S do UNIX Como os sockets foram originalmente desenvolvidos como parte do sistema operacional UNIX, eles empregam muitos conceitos encontrados em outras partes do UNIX. Em particular, sockets são integrados com a E/S - uma aplicação se comunica através de uma rotina similar ao socket que forma um caminho para a aplicação transferir dados para um arquivo. Deste modo, compreender sockets exige que se entenda as facilidades de E/S do UNIX. UNIX usa um paradigma open-read-write-close para toda E/S; o nome é derivado das operações de E/S básicas que se aplicam tanto a dispositivos como a arquivos. Por exemplo, um aplicativo deve primeiro chamar open para preparar um arquivo para acesso. O aplicativo então chama read ou write para recuperar dados do arquivo ou armazenar dados no arquivo. Finalmente, o aplicativo chama close para especificar que terminou de usar o arquivo. Quando um aplicativo abre um arquivo ou dispositivo, a chamada open retorna um descritor, um inteiro pequeno que identifica o arquivo; o aplicativo deve especificar o descritor ao solicitar transferência de dados (isto é, o descritor é um argumento para o procedimento de read ou write). Por exemplo, se um aplicativo chama open para acessar um arquivo de nome foobar, o procedimento de abertura poderia retornar o descritor 4. Uma chamada subsequente para write que especifica o descritor 4 fará com que sejam escritos dados no arquivo foobar; o nome de arquivo não aparece na chamada para write. Sockets, Descritores e E/S de Rede A comunicação de sockets usa também a abordagem de descritor. Antes de uma aplicativo poder usar protocolos para se comunicar, o aplicativo deve solicitar ao sistema operacional que crie um socket que será usado para comunicação. O aplicativo passa o descritor como argumento quando ele chama procedimentos para transferir dados através da rede; o aplicativo não precisa especificar detalhes sobre o destino remoto cada vez que transfere dados. Em uma implementação UNIX, os sockets são completamente integrados com o restante da E/S. O sistema operacional fornece um único conjunto de descritores para arquivos, dispositivos, comunicação entre processos e comunicação de rede. Como resultado, procedimentos como read e write são bastante gerais - uma aplicação pode usar o mesmo procedimento para enviar dados para outro programa, um arquivo ou através de uma rede. Em terminologia corrente, o descritor representa um objeto, e o procedimento write representa o método aplicado àquele objeto. O objeto subjacente determina como o método é aplicado. A vantagem principal de um sistema integrado reside em sua flexibilidade: pode ser escrito um único aplicativo que transfira dados para uma localização arbitrária. Se o aplicativo recebe um descritor que corresponde a um dispositivo, o aplicativo envia dados para o dispositivo. Se o aplicativo recebe um descritor que corresponde a um arquivo, o aplicativo armazena dados no arquivo. Se o aplicativo recebe um descritor que corresponde a um socket, o aplicativo envia dados através de uma inter-rede para uma máquina remota. Para resumir: Quando um aplicativo cria um socket, o aplicativo recebe um descritor; um inteiro pequeno, usado para referenciar o socket. Se um sistema usa o mesmo espaço de descritores para sockets e demais E/S, pode ser usado um único aplicativo para comunicação de rede bem como para transferência local de dados. Parâmetros e a API de Sockets A programação de socket difere da E/S convencional porque um aplicativo deve especificar diversos detalhes para usar um socket. Por exemplo, um aplicativo deve escolher um protocolo de transporte em particular, fornecer o endereço de protocolo de uma máquina remota e especificar se o aplicativo é um cliente ou um servidor. Para acomodar todos os detalhes, cada socket tem diversos parâmetros e opções - um aplicativo pode fornecer valores para cada um deles. Como opções e argumentos deveriam ser representados em uma API? Para evitar que exista uma única função de socket com argumentos separados para cada opção, os projetistas da API de sockets escolheram definir muitas funções. Essencialmente, um aplicativo cria um socket e então invoca funções para especificar em detalhes como será usado o socket. A vantagem da abordagem de sockets é que a maioria das funções tem três ou menos argumentos; a desvantagem é que um programador deve se lembrar de chamar múltiplas funções ao usar sockets. As próximas seções ilustram o conceito. Procedimentos que Implementam a API de Sockets O Procedimento Socket O procedimento socket cria um socket e retorna um descritor inteiro: descriptor = socket( protofamily, type, protocol ) O argumento protofamily especifica a família de protocolos a ser usada com o socket. Por exmeplo, o valor PF_INET é usado para especificar o suíte de protocolo TCP/IP, e PF_DECnet é usado para especificar protocolos da Digital Equipment Corporation. O argumento type especifica o tipo de comunicação que o socket usará. Os dois tipos mais comuns são a transferência de stream orientada à conexão (especificada com o valor SOCK_STREAM) e uma transferência sem conexão orientada a mensagens (especificada com o valor SOCK_DGRAM). O argumento protocol especifica um protocolo de transporte particular usado com o socket. Ter um argumento protocol além de um argumento type permite a um único suíte de protocolo incluir dois ou mais protoclos que forneçam o mesmo serviço. Naturalmente, os valores que podem ser usados com o argumento protocol dependem da família de protocolos. Por exemplo, embora o suíte de protocolos TCP/IP inclui o protocolo TCP, o suíte AppleTalk não o inclue. O Procedimento Close O procedimento close informa ao sistema para terminar o uso de um socket (a interface de Sockets do Windows usa o nome closesocket em vez de close). Ele assume a forma: close(socket) onde socket é o descritor para um socket sendo fechado. Se o socket está usando um protocolo de transporte orientado à conexão, o close termina a conexão antes de fechar o socket. O fechamento de um socket imediatamente termina seu uso - o descritor é liberado, impedindo que o aplicativo envie mais dados, e o protocolo de transporte pára de aceitar mensagens recebidas direcionadas para o socket, impedindo que o aplicativo receba mais dados. O Procedimento bind Quando criado, um socket não tem um endereço local e nem um endereço remoto. Um servidor usa o procedimento bind para prover um número de porta de protocolo em que o servidor esperará por contato. O bind leva três argumentos: bind( socket, localaddr, addrlen ) O argumento socket é o descritor de um socket que foi criado, mas não previamente amarrado (com bind); a chamada é uma requisição que ao socket seja atribuído um número de porta de protocolo particular. O argumento localaddr é uma estrutura que especifica o endereço local a ser atribuído ao socket, e o argumento addrlen é um inteiro que especifica o comprimento do endereço. Como os sockets podem ser usados com protocolos arbitrários, o formato de um endereço depende do protocolo sendo usado. A API de sockets define uma fomra genérica usada para representar endereços, e então exige que cada família de protocolos especifique como seus endereços de protocolo usam a forma genérica. O formato genérico para representar um endereço é definido como uma estrutura sockaddr. Embora várias versões tenham sido liberadas, o código de Berkely mais recente define uma estrutura sockaddr com três campos: struct sockaddr { u_char sa_len; /* comprimento total do endereço */ u_char sa_family; /* família do endereço */ char sa_data[14]; /* o endereço propriamente dito */ }; O campo sa_len consiste em um único octeto que especifica o comprimento do endereço. O campo sa_family especifica a família à qual um endereço pertence (a constante simbólica AF_INET é usada para endereços TCP/IP). Finalmente, o campo sa_data contém o endereço. Cada família de protocolos define o formato exato dos endereços usados com o campo sa_data de uma estrutura sockaddr. Por exemplo, os protocolos TCP/IP usam a estrutura sockaddr_in para definir um endereço: struct sockaddr_in { u_char sin_len; /* comprimento total do endereço */ u_char sin_family; /* família do endereço */ u_short sin_port; /* número de porta de protocolo */ struct in_addr sin_addr; /* endereço IP de computador */ char sin_zero[8]; /* não usado (inicializado com zero) */ }; Os primeiros dois campos da estrutura sockaddr_in correspondem exatamente aos dois primeiros campos da estrutura genérica sockaddr. Os últimos três campos definem a forma exata do endereço que o protocolo TCP/IP espera. Existem dois pontos a serem observados. Primeiro, cada endereço identifica tanto um computador como um aplicativo particular naquele computador. O campo sin_addr contém o endereço IP do computador, e o campo sin_port contém o número da porta de protocolo de um aplicativo. Segundo, embora o TCP/IP necessite somente de seis octetos para armazenar um endereço completo, a estrutura genérica sockaddr reserva catorze octetos. Deste modo, o campo final da estrutura sockaddr_in define um campo de 8 octetos de zeros, que preenchem a estrutura para o mesmo tamanho que sockaddr. Dissemos que um servidor chama bind para especificar o número da porta de protocolo em que o servidor aceitará um contato. Porém, além de um número de porta de protocolo, a estrutura sockaddr_in contém um campo para um endereço IP. Embora um servidor possa escolher preencher o endereço IP ao especificar um endereço, fazer isso causa problemas quando um host tiver múltiplas interfaces (multihomed) porque significa que o servidor aceita apenas requisições enviadas a um endereço específico. Para permitir que um servidor opere em um host com múltiplas interfaces, a API de sockets inclui uma constante simbólica especial, INADDR_ANY, que permite a um servidor usar uma porta específica em quaisquer dos endereços IP do computador. Para resumir: A estrutura sockaddr_in define o formato que o TCP/IP usa para representar um endereço. Embora a estrutura contenha campos para endereços IP e número de porta de protocolo, a API de sockets inclui uma constante simbólica que permite a um servidor especificar uma porta de protocolo em quaisquer dos endereços IP do computador. O Procedimento Listen Depois de especificar uma porta de protocolo, um servidor deve instruir o sistema operacional para colocar um socket em modo passivo para que o socket possa ser usado para esperar pelo contato de clientes. Para fazer isso, um servidor chama o procedimento listen, que toma dois argumentos: listen( socket, queuesize ) O argumento socket é o descritor de um socket que foi criado e amarrado a um endereço local, e o argumento queuesize especifica um comprimento para a fila de requisição do socket. O sistema operacional cria uma fila de requisição separada para cada socket. Inicialmente, a fila está vazia. À medida que chegam requisições de clientes, elas são colocadas na fila; quando o servidor pede para recuperar uma requisição recebida do socket, o sistema retorna a próxima requisição da fila. Se a fila está cheia quando chega uma requisição, o sistema rejeita a requisição. Ter uma fila de requisições permite que o sistema mantenha novas requisições que chegam enquanto o servidor está ocupado tratando de uma requisição anterior. O argumento permite que cada servidor escolha um tamanho máximo de fila que é apropriado para o serviço esperado. O Procedimento Accept Todos os servidores iniciam chamando socket para criar um socket e bind para especificar um número de porta de protocolo. Depois de executar as duas chamadas, um servidor que usa um protocolo de transporte sem conexão está pronto para aceitar mensagens. Porém, um servidor que usa um protocolo de transporte orientado à conexão exige passos adicionais antes de poder receber mensagens: o servidor deve chamar listen para colocar o socket em modo passivo, e deve então aceitar uma requisição de conexão. Uma vez que uma conexão tenha sido aceita, o servidor pode usar a conexão para se comunicar com um cliente. Depois de terminar a comunicação, o servidor fecha a conexão. Um servidor que usa transporte orientado à conexão deve chamar o procedimento accept para aceitar a próxima requisição de conexão. Se uma requisição está presente na fila, accept retorna imediatamente; se nenhuma requisição chegou, o sistema bloqueia o servidor até que um cliente forme uma conexão. A chamada accept tem a forma: newsock = accept( socket, cadddress, caddresslen ) O argumento socket é o descritor de um socket que o servidor criou e amarrou (bound) a uma porta de protocolo específica. O argumento cadddress é o endereço de uma estrutura do tipo sockaddr e caddresslen é um ponteiro para um inteiro. Accept preenche os campos do argumento caddresscaddresslen o comprimento do endereço. Finalmente, accept cria um novo socket para a conexão e retorna o descritor do novo socket para quem chamou. O servidor usa o novo socket para se comunicar com o cliente e então fecha o socket quando terminou. Enquanto isso, o socket original do servidor permanece inalterado - depois de terminar a comunicação com um cliente, o servidor usa o socket original para aceitar a próxima conexão de um cliente. O Procedimento Connnect Os clientes usam o procedimento connect para estabelecer uma conexão com um servidor específico. A forma é: connect( socket, saddress, saddresslen ) O argumento socket é o descritor de um socket no computador do cliente a ser usado para a conexão. O argumento saddress é uma estrutura sockaddr que especifica o endereço do servidor e o número de porta de protocolo (a combinação de um endereço IP e um número de porta de protocolo é às vezes chamado de um endereço de endpoint). O argumento saddresslen especifica o comprimento do endereço do servidor medido em octetos. Quando usado com um protocolo de transporte orientado à conexão como TCP, connect inicia uma conexão em nível de transporte com o servidor especificado. Na essência, connect é o procedimento que um cliente usa para contatar um servidor que tinha chamado accept. É interessante observar que um cliente que usa um protocolo de transporte sem conexão pode também chamar connect. Porém, fazer isso não inicia uma conexão ou faz com que pacotes cruzem a inter-rede. Em vez disso, connect meramente marca o socket conectado e registra o endereço do servidor. Para entender por que faz sentido conectar com um socket que usa transporte sem conexão, lembre que protocolos sem conexão exigem que o remetente especifique um endereço de destino com cada mensagem. Em muitos aplicativos, porém, um cliente sempre contata um único servidor. Deste modo, todas as mensagens vão ao mesmo destino. Em tais casos, um socket conectado fornece uma taquigrafia - o cliente pode especificar o endereço do servidor uma única vez, não sendo necessário especificar o endereço com cada mensagem. A questão é: O procedimento connect, que é chamado por clientes, tem dois usos. Quando usado com transporte orientado à conexão, connect estabelece uma conexão de transporte com um servidor especificado. Quando usado com transporte sem conexão, connect registra o endereço do servidor no socket, permitindo ao cliente enviar muitas mensagens para o mesmo servidor sem exigir que o cliente especifique o endereço de destico em cada mensagem Os Procedimentos Send, Sendto e Sendmsg Tanto os clientes quanto os servidores precisam enviar informações. Normalmente, um cliente envia uma requisição, e um servidor envia uma resposta. Se o socket está conectado, o procedimento send pode ser usado para transferir dados. send tem quatro argumentos: send( socket, data, length, flags ) O argumento socket é o descritor do socket a ser usado, o argumento data é o endereço em memória dos dados a serem enviados, o argumento length é um inteiro que especifica o número de octetos de dados, e o argumento flags contém bits que requisitam opções especiais (muitas opções visam à depuração do sistema e não estão disponíveis para programas convencionais cliente e servidor). Os procedimentos sendto e sendmsg permitem a um cliente ou servidor enviar uma mensagem usando um socket não-conectado; ambos exigem que um destino seja especificado. sendto toma o endereço de destino como um argumento. Ele toma a forma: sendto( socket, data, length, flags, destaddress, addresslen ) Os primeiros quatro argumentos correspondem aos quatro argumentos do procedimento send. Os últimos dois argumentos especificam o endereço de um destino e o comprimento daquele endereço. O formato do endereço no argumento destaddress é a estrutura sockaddr (especificamente, a estrutura sockaddr_in quando usada com TCP/IP). O procedimento sendmsg executa a mesma operação que sendto, mas abrevia os argumentos definindo uma estrutura. A lista de argumentos mais curta pode tornar os programas que usam sendmsg mas fáceis de ler: sendmsg( socket, msgstruct, flags ) O argumento msgstruct é uma estrutura que contém informações sobre o endereço de destino, o comprimento do endereço, a mensagem a ser enviada e o comprimento da mensagem: struct msgstruct { /* estrutura usada por sendmsg */ struct sockaddr *m_saddr; /* pointer para endereço de destino */ struct datavec *m_dvec; /* pointer para mensagem (vetor) */ int m_dvlength; /* num. de itens em vetor */ struct access *m_rights; /* pointer para acessar lista de direitos */ int m_alength; /* num. de itens em lista */ }; Os detalhes da estrutura de mensagem não têm importância - deve ser visto como um modo de combinar muitos argumentos em uma única estrutura. A maioria dos aplicativos usa apenas os primeiros três campos, que especificam um endereço de protocolo de destino e uma lista de itens de dados que inclui a mensagem. Os Procedimentos Recv, Recvfrom e Recvmsg Um cliente e um servidor precisam receber dados enviados pelo outro. A API de sockets fornece vários procedimentos que podem ser usados. Por exemplo, um aplicativo pode chamar recv para receber dados de um socket conectado. O procedimento tem a forma: recv( socket, buffer, length, flags ) O argumento socket é descritor de um socket a partir do qual dados devem ser recebidos. O argumento buffer especifica o endereço em memória em que a mensagem recebida deve ser colocada e o argumento length especifica o tamanho do buffer. Finalmente, o argumento flags permite que se controle detalhes (por exempjlo, para permitir a um aplicativo extrair uma cópia de uma mensagem recebida sem remover a mensagem do socket). Se um socket não está conectado, ele pode ser usado para receber mensagens de um conjunto arbitrário de clientes. Em tais casos, o sistema retorna o endereço do remetente junto com cada mensagem recebida. Os aplicativos usam o procedimento recvfrom para receber uma mensagem e o endereço do seu remetente: recvfrom( socket, buffer, length, flags, sndraddr, saddrlen ) Os primeiro quatro argumentos correspondem aos argumentos de recv; os dois argumentos adicionais, sndraddr e saddrlen, são usados para registrar o endereço IP do remetente. O argumento sndraddr é um ponteiro para uma estrutura sockaddr em que o sistema escreve o endereço do remetente, e o argumento saddrlen é um ponteiro para um inteiro que o sistema usa para registrar o comprimento do endereço. recvfrom registra o endereço do remetente exatamente da mesma forma que sendto espera. Deste modo, se um aplicativo usa recvfrom para receber uma mensagem, enviar uma resposta é fácil - o aplicativo simplesmente usa o endereço registrado como um destino para a resposta. A API de sockets inclui um procedimento de entrada equivalente ao procedimento de saída sendmsg. O procedimento recvmsg opera como recvfrom, mas exige menos argumentos. Ele tem a forma: recvmsg( socket, msgstruct, flags ) onde o argumento msgstruct dá o endereço de uma estrutura que possui o endereço para uma mensagem recebida como também posições para o endereço IP do remetente. O msgstruct registrado por recvmsg usa exatamente o mesmo formato que a estrutura esperada por sendmsg. Deste modo, os dois procedimentos funcionam bem para receber uma mensagem e enviar uma resposta. Ler e Escrever com Sockets Dissemos que a API de sockets foi originalmente projetada para ser parte do UNIX, que usa read e write para E/S. Consequentemente, sockets permitem que aplicativos também usem read e write para transferir dados. Como send e recv, read e write não têm argumentos que permitam a especificação de um destino. Em vez disso, read e write têm cada um três argumentos: um descritor de socket, a localização de um buffer em memória usado para armazenar os dados e o comprimento do buffer de memória. Deste modo, read e write devem ser usados com sockets conectados. A vantagem principal de se usar read e write é a generalidade - um programa aplicativo pode ser criado para transferir dados de ou para um descritor sem saber se o descritor correponde a um arquivo ou a um socket. Deste modo, um programador pode usar um arquivo em disco local para testar um cliente ou servidor antes de tentar se comunicar através de uma rede. A desvantagem principal de se usar read e write é que uma implementação de biblioteca de sockets pode introduzir sobrecarga adicional na E/S de arquivo em qualquer aplicativo que também use sockets. Outros Procedimentos de Socket A API de socktes contém outros procedimentos úteis. Por exemplo, após um servidor chamar o procedimento accept para aceitar uma requisição de conexão recebida, o servidor pode chamar o procedimento getpeername para obter o endereço completo do cliente remoto que iniciou a conexão. Um cliente ou servidor pode chamar também gethostname para obter informações sobre o computador em que ele está executando. Dissemos que um socket tem muitos parâmetros e opções. Dois procedimentos de propósito geral são usados para configurar opções de socket ou obter uma lista de valores correntes. Um aplicativo chama o procedimento setsockopt para armazenar valores em opções de socket, e o procedimento getsockopt para obter os valores correntes das opções. As opções são principalmente usadas para tratar dos casos especiais (por exemplo, para aumentar o desempenho mudando o tamanho do buffer interno que o software de protocolo usa). Dois procedimentos são usados para traduzir entre endereços IP e nomes de computador. O procedimento gethostbyname retorna o endereço IP para um computador dado o seu nome. Os clientes usam frequentemente gethostbyname para traduzir um nome inserido por um usuário em um endereço IP correpondente exigido pelo software de protocolo. O procedimento gethostbyaddr fornece um mapeamento inverso - dado um endereço IP referente a um computador, ele retorna o nome do computador. Os clientes e servidores podem usar gethostbyaddr ao mostrar informações para uma pessoa ler. Sockets, Threads e Herança Uma vez que muitos servidores são concorrentes, a API de sockets é projetada para funcionar com programas concorrentes. Embora os detalhes dependam do sistema operacional subjacente, as implementações da API de sockets aderem ao seguinte princípio: Cada novo thread criado herda uma cópia de todos os sockets abertos do thread que os criou. Para entender como os servidores usam herança de sockets, é importante saber que os sockets usam um mecanismo de contagem de referências. Quando um socket é primeiramente criado, o sistema inicializa a contagem de referências do socket para 1; o socket existe desde que a contagem de referências permaneça positiva. Quando um programa cria um thread adicional, o sistema fornece ao thread uma lista de todos os sockets que o programa possui e incrementa a contagem de referências de cada socket em 1. Quando um thread chama close para um socket, o sistema decrementa a contagem de referências no socket em 1 e remove o socket da lista de threads (se o thread termina antes de fechar os sockets, o sistema chama automaticamente close em cada um dos sockets abertos que o thread tinha). O thread principal de um servidor concorrente cria um socket que o servidor usa para aceitar conexões recebidas. Quando chega uma requisição de conexão, o sistema cria um novo socket para a nova conexão. Logo depois do thread principal criar um thread de serviço para tratar da nova conexão, ambos os threads têm acesso aos sockets novo e velho, e a contagem de referências de cada socket tem o valor 2. Porém, o thread principal não usará o novo socket, e o thread de serviço não usará o socket original. Portanto, o thread principal chama close para o novo socket, e o thread de serviço chama close para o socket original, reduzindo a contagem de referência de cada um para 1. Depois de um thread de serviço terminar, ele chama close no novo socket, reduzindo a contagem de referência para zero e fazendo com que o socket seja apagado. Deste modo, o tempo de vida dos sockets em um servidor concorrente pode ser resumido: O socket que um servidor concorrente usa para aceitar conexões existe desde que o thread servidor principal execute; um socket usado para uma conexão específica existe apenas enquanto existe o thread para tratar daquela conexão.