A comunicação em rede no ambiente UNIX é feita através do conceito de sockets. Um socket é uma interface de comunicação bidirecional entre processos. Sockets são representados como descritores de arquivos e permitem a comunicação entre processos distintos na mesma máquina ou em máquinas distintas, através de uma rede. Os sockets são a base da comunicação em redes TCP/IP e também são muito usados em comunicações entre processos no interior de um mesmo computador.
Ao criar um socket, deve ser especificada a forma de comunicação (communication style) a ser usada e que protocolo deverá realizar a comunicação nos níveis mais baixos do sistema. Em UNIX, três formas de comunicação são normalmente usadas:
Essas formas de comunicação são definidas através das constantes inteiras SOCK_STREAM
, SOCK_DGRAM
e SOCK_RAW
, no arquivo sys/socket.h
.
Além da forma de comunicação, também deve ser escolhido um espaço de nomes para nomear ou endereçar o socket criado. O nome de um socket sempre está relacionado a um espaço de nomes, também chamado de domínio (socket domain). Cada espaço de nomes é definido por uma macro na forma PF_*
(que vem do termo Protocol Family). Os principais espaços de nomes em uso no UNIX são:
PF_LOCAL
: indica o espaço de nomes local, no qual os nomes de sockets são válidos somente no escopo do computador local. As macros PF_UNIX
e PF_FILE
são sinônimos desse espaço de nomes.PF_INET
: indica o espaço de nomes IPv4 e seus protocolos associados.PF_INET6
: indica o espaço de nomes IPv6 e seus protocolos associados.
Além dos acima, outros espaços de nome estão disponíveis, embora sejam de uso menos freqüente: PF_NS
(protocolos Xerox NS), PF_ISO
(protocolos OSI/ISO), PF_CCITT
(protocolos do CCITT), PF_IMPLINK
(Internet Message Processors), PF_ROUTE
(protocolos de roteamento), etc. Para cada espaço de nomes, uma macro correspondente AF_*
define o formato dos endereços para aquele espaço.
Por fim, deve ser escolhido o protocolo que irá efetuar a comunicação, ou seja, o mecanismo usado para a transferência dos dados. Cada protocolo é aplicável a um dado espaço de nomes e uma forma de comunicação. Algumas regras são importantes na escolha de um protocolo:
SOCK_STREAM
e no espaço de nomes da Internet.Geralmente a interação entre processos usa o modelo de comunicação Cliente/Servidor. Algumas características importantes desse modelo:
Como servidor e cliente têm comportamentos distintos face à comunicação, suas implementações seguem também padrões distintos. Ambos criam sockets para se comunicar, mas operam de forma diferente.
Um cliente executa normalmente os seguintes passos para estabelecer uma comunicação com um servidor:
socket
connect
read
e write
close
.Um servidor normalmente executa os seguintes passos para oferecer serviço a seus clientes:
socket
bind
.listen
.accept
(que gera um descritor específico para a conexão recebida).read
e write
.close
).close
).Diversas variantes são possíveis no esquema acima: por exemplo, o servidor pode lançar threads ou processos filhos para tratar as conexões recebidas.
#include <sys/socket.h> int socket (int namespace, int style, int protocol)
Cria um socket no namespace
indicado, definindo o style
e o protocol
a ser usado. Retorna o descritor de arquivo do socket criado, ou -1 em caso de erro. Alguns exemplos:
fd = socket (PF_INET, SOCK_DGRAM, 0) ; /* socket UDP */ fd = socket (PF_INET, SOCK_STREAM, 0) ; /* socket TCP */ fd = socket (PF_LOCAL, SOCK_STREAM, 0) ; /* socket stream local */
#include <sys/socket.h> int bind (int socket, struct sockaddr *addr, socklen_t length)
Associa um endereço ao socket
. Os parâmetros addr
e length
especificam o endereço a usar; seu formato depende do espaço de nomes em uso.
#include <sys/socket.h.> int connect (int socket, struct sockaddr *addr, socklen_t length)
Inicia uma conexão entre o socket local socket
(lado cliente) e o socket cujo endereço está especificado nos campos addr
and length
(lado servidor). Bloqueia até obter uma resposta do servidor ou erro. Esta operação só é usada em sockets orientados a conexão (SOCK_STREAM
).
#include <sys/socket.h> int listen (int socket, unsigned int n)
Habilita o socket
a aceitar pedidos de conexão, tornando-o um socket de servidor. O parâmetro n
indica o tamanho da fila de pedidos de conexão pendentes (clientes excedendo a capacidade da fila terão seus pedidos de conexão recusados).
#include <sys/socket.h> int accept (int socket, struct sockaddr *addr, socklen_t *length_ptr)
Recebe um pedido de conexão pendente (espera enquanto a fila estiver vazia). Os valores addr
e length_ptr
retornam informações sobre o cliente que efetuou o pedido de conexão.
Ao aceitar o pedido de conexão, a chamada accept
cria um novo socket local para manter a conexão e retorna seu descritor (ou -1 em caso de erro). O socket original permanece disponível para atender novos pedidos de conexão, até ser fechado.
#include <sys/socket.h> int getsockname (int socket, struct sockaddr *addr, socklen_t *length_ptr)
Retorna o endereço de socket
, nos parâmetros addr
e length_ptr
. (observe que lenght_ptr
é um ponteiro)
#include <sys/socket.h> int getpeername (int socket, struct sockaddr *addr, socklen_t *length_ptr)
Retorna o endereço do socket ao qual socket
está conectado, nos parâmetros addr
e length_ptr
.
#include <sys/socket.h> int send (int socket, void *buffer, size_t size, int flags)
Envia os dados contidos no buffer
de tamanho size
através do socket
. Alguns flags permitem modificar parâmetros do envio; se os flags forem 0 (zero), a chamada write
pode ser usada. Retorna o número de bytes enviados, ou -1 (erro).
#include <sys/socket.h> int recv (int socket, void *buffer, size_t size, int flags)
Recebe dados de socket
, depositando-os no buffer
de tamanho size
. Alguns flags permitem modificar parâmetros do envio; se os flags forem 0 (zero), a chamada read
pode ser usada. Retorna o número de bytes recebidos, ou -1 (erro).
#include <unistd.h> int close (int filedes)
Fecha o socket representado por filedes
. Antes de fechar, o sistema tenta enviar os dados pendentes (ainda não enviados).
#include <sys/socket.h> int shutdown (int socket, int how)
Permite encerrar seletivamente um socket. O parâmetro how
indica a ação a ser efetuada:
Várias das chamadas de sistema relacionadas a sockets usam como parâmetro uma estrutura do tipo struct sockaddr
, definida da seguinte forma:
#include <sys/socket.h> struct sockaddr { unsigned short sa_family; // address family, AF_xxx char sa_data[14]; // 14 bytes of protocol address };
O campo sa_family
indica o tipo de endereço a ser considerado (por exemplo, AF_LOCAL
ou AF_INET
).O campo sa_data
contém o endereço do socket, e sua estrutura interna depende do espaço de nomes usado.
No caso da Internet (espaço de nomes PF_INET
), o endereço de um socket é composto pelo endereço IP da interface de rede à qual ele está associado e do número da porta onde ele está conectado. Para definir esses parâmetros uma estrutura adicional struct sockaddr_in
está definida:
#include <netinet/in.h> struct sockaddr_in { short int sin_family; // Address family unsigned short int sin_port; // Port number (in network byte order) struct in_addr sin_addr; // Internet address (in network byte order) unsigned char sin_zero[8]; // just fill this with zeros };
Como essa estrutura tem o mesmo tamanho de sockaddr
, pode-se fazer casting de ponteiros de uma para outra, nos dois sentidos.
É importante observar que somente processos com UID
= 0 (pertencentes ao usuário root) podem criar sockets AF_INET
com número de porta inferior à constante IPPORT_RESERVED
(geralmente igual a 1024).
A estrutura interna in_addr
(Internet address) tem o seguinte formato em AF_INET
:
#include <netinet/in.h> struct in_addr { unsigned long s_addr; // 32-bit long (4 bytes) };
O campo s_addr
contém um endereço IP armazenado como 4 bytes (em network byte order). Para preenchê-lo pode ser usada a função inet_aton
(vide abaixo).
Para evitar problemas de incompatibilidade entre sistemas big-endian e sistemas little-endian, alguns campos da estrutura de endereço são armazenados em um formato padronizado conhecido como Network Byte Order, ou formato de rede. Quatro funções estão disponíveis para converter dados entre o formato local da máquina e o de rede:
short htons (short)
: converte Host ⇒ Network Shortshort ntohs (short)
: converte Network ⇒ Host Shortlong htonl (long)
: converte Host ⇒ Network Longlong ntohl (long)
: converte Network ⇒ Host LongAlém destas, as funções abaixo também são usadas:
#include <arpa/inet.h> int inet_aton (const char *name, struct in_addr *addr)
Esta função converte um endereço IP no formato “nnn.nnn.nnn.nnn
” para o formato binário exigido pela estrutura in_addr
e o deposita no endereço indicado por addr
. Além disso, algumas constantes pré-definidas podem ser usadas no campo s_addr
:
INADDR_LOOPBACK
: localhost, ou endereço de loopbackINADDR_ANY
: any incoming address, usada em sockets de servidoresINADDR_BROADCAST
: endereço de broadcast da rede localINADDR_NONE
: nenhum endereço, valor retornado como erro em algumas funções#include <arpa/inet.h> char *inet_ntoa (struct in_addr addr)
Efetua a conversão de um endereço no formato binário interno para seu equivalente no formato “nnn.nnn.nnn.nnn
”.
Exemplos: cliente TCP e servidor TCP.
A construção de sistemas comunicando através de sockets UDP é similar à dos exemplos apresentados em TCP, com pequenas alterações:
listen
) nem aguardar conexões (accept
).send
/write
e recv
/read
, normalmente são usadas as chamadas sendto
e recvfrom
, que especificam os parceiros de comunicação na hora do envio/recepção.
As chamadas de sistema sendto
e recvfrom
têm a seguinte sintaxe:
#include <sys/socket.h> int sendto (int socket, void *buffer, size_t size, int flags, struct sockaddr *addr, socklen_t length)
Esta chamada envia a mensagem presente em buffer
através do socket
para o destino especificado em addr
e length
. O parâmetro size
indica o número de bytes da mensagem. Os flags permitem alterar parâmetros do envio (default em 0). O retorno é similar ao de send
, mas somente erros locais são reportados.
É possível executar a chamada connect
em um socket UDP, mas essa ação apenas define um destinatário default para todas as mensagens enviadas por aquele socket. Nesse caso, pode-se usar a chamada send
ou mesmo write
para enviar mensagens através desse socket UDP “conectado”.
#include <sys/socket.h> int recvfrom (int socket, void *buffer, size_t size, int flags, struct sockaddr *addr, socklen_t *length-ptr)
Esta chamada lê uma mensagem de socket
e a deposita em buffer
. O parâmetro size
indica o tamanho do buffer (pacotes maiores que o buffer serão truncados). A identidade do remetente é retornada através dos parâmetros addr
e lenght_ptr
. Os erros possíveis são similares aos de recv
.
Pode-se usar recv
ou mesmo read
caso não haja interesse em identificar o remetente da mensagem.
Exemplos: cliente UDP e servidor UDP.
A criação de sockets no domínio AF_LOCAL
, também conhecidos como sockets UNIX, é bastante similar à construção de sockets TCP ou UDP. As únicas diferenças perceptíveis estão na estrutura de dados que representa o endereço do socket, pois o endereço de um socket local é definido como um nome de arquivo. Nesse caso, uma estrutura adicional struct sockaddr_un
(Unix Name) está definida:
#include <sys/un.h> struct sockaddr_un { short int sun_family; // Address family char sun_path[108]; // socket path in the filesystem };
O campo sun_path
deverá indicar o caminho completo para o socket no sistema de arquivos (por exemplo, “/tmp/socket-423142
”). Isso significa que cliente e servidor devem estar acessando o mesmo sistema de arquivos local. Uma vez fechado, o socket permanece no sistema de arquivos e deverá ser explicitamente removido pelo servidor que o criou (através da chamada unlink
).
Exemplos: cliente UNIX e servidor UNIX.
A maior parte dos serviços de rede oferecidos por um computador é provida por processos com características especiais, denominados daemons (espíritos, em inglês). As principais características de um daemon são:
Os daemons nascem como processos normais, que são em seguida convertidos em daemons. Os passos necessários para a transformação de um processo ordinário em um daemon são:
fork()
e encerrar o processo pai, para liberar o shell que lançou aquele processo.setsid()
para criar uma nova sessão de processos; isso faz com que o filho saia da sessão de seu pai (e do shell), sendo desvinculado do terminal e dos sinais porventura enviados ao shell./
) ou um diretório específico do daemon (por exemplo, o daemon httpd
usa /etc/httpd
como diretório de trabalho).SIGHUP
, que releia as configurações do daemon.Como os daemons não estão associados a terminais, eles não podem gerar mensagens em um terminal. Duas abordagens são possíveis para a geração de mensagens:
O serviço syslog é oferecido por um daemon, que recebe mensagens em um socket local em /dev/log
e também em um socket UDP (porta 514). A partir das configurações descritas no arquivo /etc/syslog.conf
ele decide o que fazer com cada mensagem recebida: enviar a um terminal, avisar o administrador, armazenar em um arquivo, ativar um script externo ou enviar a outro host são as principais possibilidades. As funcionalidades oferecidas pelo sistema syslog e sua forma de acesso estão definidas no manual da GLibC.
Como visto acima, os programas que interagem através da rede usam nomes de sockets para localizar seus interlocutores. Na Internet, os sockets são nomeados através de um endereço IP e um número de porta, ambos definidos na estrutura sockaddr_in. O endereço IP de um socket é definido nessa estrutura por um inteiro long, e sua porta por um inteiro short.
Para os usuários, é muito mais fácil se referenciar a um servidor de rede através de um nome simbólico que usar seu endereço IP. Por isso, costumar ser associados nomes simbólicos aos endereços IP. Isso leva a várias representações para o mesmo endereço de socket, como mostra a tabela a seguir:
nome simbólico string | endereço IP string | representação interna in_addr (long) |
---|---|---|
espec.ppgia.pucpr.br | 200.192.112.139 | 0xC8C0708B ou 0x8B70C0C8 |
Conforme visto anteriormente, as funções inet_aton
e inet_ntoa
permitem converter endereços IP em sua representação interna, ou vice-versa. Mas, como converter nomes simbólicos para endereços IP, ou vice-versa?
Diversos mecanismos de resolução de nomes podem ser usados por um sistema operacional para converter um nome simbólico em um endereço IP, ou o contrário. Os mecanismos mais usuais estão detalhados nesta página. Apesar da profusão de mecanismos, o sistema operacional oferece uma interface padronizada para resolver nomes, que é definida pela seguinte funções:
#include <netdb.h> struct hostent * gethostbyname (const char *name)
Esta função retorna informações sobre o computador cujo nome é name
, ou null se houver erro. Ver também Gethostbyname2
.
#include <netdb.h> struct hostent * gethostbyaddr (const char *addr, size_t length, int format)
Esta função retorna informações sobre o computador a partir de seu endereço. O campo addr
é um ponteiro para um endereço em formato interno, com tamanho size
. O campo format
especifica o tipo de endereço (AF_INET
para IPv4 ou AF_INET6
para IPv6).
Essas funções podem falhar com um dos seguintes erros, indicados na variável h_errno
:
HOST_NOT_FOUND
: o nome ou endereço não foi encontrado.TRY_AGAIN
: um erro temporário (o servidor de nomes não pôde ser contactado, por exemplo).NO_RECOVERY
: um erro não recuperável.NO_ADDRESS
: existe uma entrada para o nome, mas não há endereço associado a ele.
As funções acima retornam um ponteiro para uma estrutura do tipo hostent
, estaticamente alocada, que contém os seguintes campos:
char *h_name
: nome principal do host.char **h_aliases
: aliases (nomes alternativos) do computador, na forma de um vetor de strings.int h_addrtype
: tipo de endereço retornado (AF_INET
, AF_INET6
, etc).int h_length
: tamanho em bytes dos endereços retornados (depende do tipo de endereço).char **h_addr_list
: vetor de endereços associados ao host, terminado por null. Os endereços sempre estão no formato interno, em Network Byte Order.Como essa estrutura é alocada estaticamente, deve ser usada com muito cuidado em programas multi-thread.
Exemplos de resolução de nomes tanto direta quanto reversa.
A chamada de procedimento remoto (RPC - Remote Procedure Call) é uma abstração construída sobre sockets. Ela simplifica a tarefa do programador, liberando-o das seguintes tarefas:
Nessa abstração, o cliente vê o servidor como uma interface constituída por uma ou mais funções e as estruturas de dados necessárias para definir os parâmetros das mesmas. Ao chamar uma função dessa interface, código de comunicação gerado automaticamente estabelece a comunicação com o servidor. O diagrama a seguir ilustra esse mecanismo:
Passo | cliente | biliotecas e runtime RPC | rede | bibliotecas e runtime RPC | servidor |
---|---|---|---|---|---|
1 | cliente() { x = 5 ; y = fatorial(x); | ||||
2 | - recebe a chamada de função - prepara os parâmetros para envio - conecta com o servidor - envia o pedido com seus parâmetros | ||||
3 | ⇒ | ||||
4 | - recebe o pedido e parâmetros - extrai os parâmetros no formato local - invoca a função solicitada | ||||
5 | int fatorial(int n) { … return(fat) ; } |
||||
6 | - recebe o valor de retorno - prepara o retorno para envio - devolve resposta ao cliente | ||||
7 | <= | ||||
8 | - recebe a resposta - extrai os valores de retorno no formato local - retorna a chamada da função | ||||
9 | printf(“%d\n”,y); } |
Somente o código do cliente e do servidor e a definição da interface devem ser escritos pelo programador da aplicação distribuída. Todo o código em rosa é gerado automaticamente pelas ferramentas que compõe o suporte a RPCs.
Em UNIX, o suporte a RPC é provido pelos seguintes componentes:
rpcgen
, o compilador de descrições de interface e gerador de código de comunicação.Um exemplo de RPC está disponível nesta página.
RPC é uma abstração muito usada para implementar diversos serviços do mundo UNIX, entre os quais podem ser citados o NFS (Network File System) e o NIS (Network Information System). O suporte a RPC é padrão no mundo UNIX, e as diversas implementações são compatíveis entre si (mas incompatíveis com as RPCs DCE e Microsoft).
GET /path/file HTTP/1.0 (linha vazia)
Caso o arquivo seja encontrado, o servidor envia uma resposta com a seguinte forma e encerra a conexão:
HTTP/1.0 200 ok Content-type: text/html (linha vazia) ... (conteúdo do arquivo solicitado)
text/html
) e GIF (image/gif
). Os arquivos oferecidos aos clientes devem estar em um sub-diretório webfiles
no diretório corrente do servidor. Para mais informações sobre as mensagens do protocolo HTTP consulte esta página.address = gethostbyname (name1) name2 = gethostbyaddr (address) if (name1 equal name2) name1 is ok else name is false endif
gethostbyaddr
para encontrar o nome de cada cliente conectado.web.log
localizado seu diretório de trabalho.