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.

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.
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.
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.
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.
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.
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.