Hello, World!

O primeiro exemplo não poderia ser outro. Um servidor que, ao ser contactado por um cliente, envia uma mensagem Hello, World!. Para mantê-lo realmente mínimo, este “servidor” se desconecta logo depois de enviar a mensagem e encerra seu funcionamento. A ideia é que você possa ver todo o ciclo desde a configuração até o fechamento do socket com o mínimo de ruído adicional. Segue o código completo. Em seguida, explico cada parte.

import socket

# 1. criar um socket
listen_socket = socket.socket()

# 2. configurar o socket como servidor
porta = 9090
listen_socket.bind(('localhost', porta))
listen_socket.listen()

# 3. aguardar conexões
print('Aguarda conexão na porta {}'.format(porta))
client_socket, endereco = listen_socket.accept()
print('Conexão estabelecida de {}:{}'.format(endereco[0], endereco[1]))

# 4. usar a conexão
client_socket.send('Hello, World!\n'.encode('utf-8'))

# 5. fechar os sockets
listen_socket.close()
client_socket.close()

Para executar o código do servidor acima use Python 3. E para se concetar ao servidor, use o comando nc localhost 9090 (nc é a mesma ferramenta netcat que usei no texto sobre sockets). A vídeo abaixo abaixo ilustra o que você deve ver ao executar o servidor e se conectar a ele. Observe que o servidor se desconecta tão logo envie a mensagem e o cliente ao recebê-la.

GIF Vídeo Servidor Hello, World!

Sobre o código

O código acima segue uma sequência que sempre se repetirá para qualquer servidor em Python (de fato, a sequência é muito semelhante a qualquer outra linguagem, dado que a API da linguagem é basicamente uma camada sobre a API proporcionada pelo sistema operacional). A sequência é: 1) criar um socket; 2) configurá-lo para que funcione como servidor; 3) esperar por conexões; 4) quando uma conexão for estabelecida, usá-la para se comunicar com o cliente; e 5) fechar os sockets. O código acima está devidamente comentado e cada trecho numerado de acordo com essa sequência. Vejamos em mais detalhes o que é feito em cada passo.

  1. A simples criação do socket para ouvir as conexões não cria uma conexão. De fato, é isso que caracteriza um servidor. Ele apenas “ouve” uma porta à espera por novas conexões. Todas as conexões são estabelecidas por clientes.

  2. Para que um socket atue como um servidor são necessárias duas operações: 1) vincular um endereço específico ao socket; e 2) fazer o socket “ouvir” solicitações de conexões vindas da rede. A primeira operação (método bind()) vincula o endereço passado como argumento (IP + porta) ao socket. Este passo garante que o serviço terá um endereço conhecido ao qual os clientes possam se conectar. A segunda operação (método listen()) é a que efetivamente caracteriza que o processo é um servidor (relembre que um servidor aguarda passivamente por conexões e que apenas clientes as iniciam). A partir do momento em que o listen() é invocado, o sistema operacional passa coletar solicitações de conexões em uma fila. Cabe, a partir de agora, ao processo decidir quando aceitará as conexões.

  3. O terceiro passo consiste exatamente em “aceitar” conexões (operação accept()). Através dessa operação, o processo solicita ao sistema que estabeleça uma conexão efetiva. Em geral, a operação é bloqueante. Ou seja, se não houver nenhuma solicitação pendente, o processo será bloqueado até a chegada de uma solicitação. Quando uma solicitação for escolhida, o processo será desbloqueado. Observe que a operação accept() retorna um novo objeto socket, para acesso à conexão estabelecida (variável client_socket, no código acima), e o endereço de origem da conexão na forma de um par IP e porta.

    Detalhe. Esse endereço e essa porta são da máquina de origem da conexão. Não se trata de uma porta local na máquina do servidor.

  4. Neste caso, o quarto passo é simplesmente enviar a mensagem através da conexão TCP estabelecida. Para isso, usamos o método send() do socket de conexão. Um detalhe importante aqui: em geral, dizemos que conexões TCP transmitem caracteres. Contudo, seria mais adequado dizermos que transmitem bytes, dado que essa é a verdadeira unidade de dado transmitida. Cabe à aplicação (tanto cliente, quanto servidor) codificar e decodificar os caracteres como bytes, antes e depois do envio, respectivamente. É essa a razão do uso do método encode() no texto a ser enviado. Neste caso, optei por usar a codificação conhecida como UTF-8, embora qualquer fosse possível.

  5. E o último passo não poderia ser outro: depois que os sockets tenham sido usados, é importante fechá-los adequadamente. No caso de Python, isso é feito invocando o método close() nos objetos. Observe que a operação é importante porque o processo irá se comunicar com o sistema operacional, indicando que a conexão e os sockets podem ser devidamente fechados e descartados. Isso não é uma ação rápida, porque o protocolo estabelece que deve haver uma espera de segurança para o caso de serem necessárias retransmissões dos dados enviados, dentre outros detalhes. Assim, é comum que mesmo depois que o processo de um servidor tenha sido terminado, alguns recursos continuem em uso.