9.13. Sockets TCP

Os sockets TCP apresentam-se em duas formas distintas mas partilham o mesmo tipo subjacente: sockets cliente que utilizam connect() para se ligarem a um servidor remoto, e sockets servidor que utilizam bind(), listen() e accept() para aceitar ligações. Ambas as funções utilizam a mesma classe socket apresentada em Objetos socket; apenas diferem os métodos invocados.

9.13.1. Um cliente TCP

O cliente mais simples abre uma ligação, envia um pedido, lê a resposta e fecha:

import socket

s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect(("192.168.1.20", 9000))

s.send(b"hello\n")
reply = s.recv(1024)
print("reply:", reply)

s.close()

connect() executa o handshake de três vias descrito em TCP – um fluxo fiável de bytes e retorna quando a ligação está estabelecida. send() escreve bytes na ligação; recv() lê até um determinado número de bytes a partir dela. Quando a aplicação termina, close() encerra a ligação.

O mesmo script encapsulado no idioma da instrução with de Objetos socket, para que o socket seja fechado mesmo que ocorra uma exceção:

import socket

with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
    s.connect(("192.168.1.20", 9000))
    s.send(b"hello\n")
    print(s.recv(1024))

9.13.1.1. Leitura até ao fim

Um único recv() devolve até ao número de bytes solicitado – pode devolver menos, porque o TCP é um fluxo contínuo e não uma sequência de mensagens. A aplicação tem de continuar a ler até obter a resposta completa:

chunks = []
while True:
    chunk = s.recv(1024)
    if not chunk:                  # empty bytes -> other side closed
        break
    chunks.append(chunk)
reply = b"".join(chunks)

O ciclo termina quando recv() devolve um bytes vazio. Isso acontece quando o outro lado fechou corretamente a sua metade da ligação; neste estilo de protocolo, a aplicação interpreta «fim de fluxo» como «fim de mensagem».

9.13.1.2. Envio até ao fim

A ressalva inversa aplica-se a send(): pode enviar menos bytes do que o solicitado, devolvendo o número de bytes efetivamente escritos. Para payloads de grande dimensão, reenvie o restante ainda não enviado:

payload = some_big_bytes
while payload:
    n = s.send(payload)
    payload = payload[n:]

sendall() executa o ciclo internamente, pelo que a maioria do código pode simplesmente chamá-lo e evitar a repetição manual:

s.sendall(some_big_bytes)

9.13.2. Um servidor TCP

O lado do servidor compreende quatro etapas: reservar uma porta, colocar o socket em modo de escuta, aceitar ligações uma a uma e comunicar através de cada socket aceite. Um servidor de eco mínimo:

import socket

server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
server.bind(("0.0.0.0", 9000))
server.listen(1)
print("listening on port 9000")

while True:
    conn, addr = server.accept()
    print("connection from", addr)

    while True:
        data = conn.recv(1024)
        if not data:
            break
        conn.send(data)            # echo back

    conn.close()

Passo a passo:

  • bind() reserva um endereço e uma porta na câmara. "0.0.0.0" aceita em qualquer interface; substituindo-o por um IP específico, o servidor fica restrito a essa interface.

  • listen() muda o socket de um socket normal para um socket em escuta. O argumento é o backlog – quantas ligações pendentes o MicroPython irá colocar em fila enquanto a aplicação está ocupada. Escolha um número pequeno; 1 é suficiente para a maioria dos casos.

  • accept() bloqueia até um cliente se ligar e, em seguida, devolve (conn, addr): um novo socket que representa esta ligação individual e o endereço do cliente. O socket em escuta permanece aberto para aceitar mais ligações.

  • Todos os bytes da conversa transitam através de conn, o novo socket. As leituras e escritas utilizam as mesmas chamadas recv() / send() que no lado do cliente.

  • Quando o cliente fecha, recv() devolve b"", o ciclo interno termina e o servidor fecha a sua extremidade com close().

O while True exterior retorna a accept() para aguardar o próximo cliente. Nesta configuração, o servidor trata um cliente de cada vez; para executar múltiplos clientes em paralelo são necessárias threads ou asyncio. Este último é o tema da próxima página.

9.13.3. Armadilhas comuns

  • Tratar recv() como orientado a mensagens. Não o é. Dois send(b"hi") podem chegar como um único recv(4) com b"hihi", ou como dois recv(2)s. A aplicação tem de adicionar delimitadores de mensagem se os limites forem importantes – uma nova linha, um prefixo de comprimento, ou outro mecanismo.

  • Esquecer de repetir em envios parciais. Utilize sendall() para qualquer coisa que ultrapasse algumas centenas de bytes.

  • Esquecer de fechar o socket aceite. Cada conn é um socket independente; fechar o socket em escuta não fecha os aceites. Usar blocos with em ambos torna difícil cometer este erro:

    while True:
        with server.accept()[0] as conn:
            # ... talk on conn ...
    
  • Tentar reutilizar uma porta ainda em TIME_WAIT. Quando um servidor reinicia alguns segundos após o fecho, bind() pode falhar com «address in use» porque o MicroPython ainda está a reservar a porta para a ligação anterior. server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) antes de bind() resolve este problema.

9.13.4. O que se segue

Bloquear em accept() faz com que o servidor sirva apenas um cliente de cada vez. Bloquear em recv() faz com que um único cliente lento bloqueie o ciclo completo. A solução padrão na câmara é asyncio – executar cada ligação como uma tarefa independente e deixar o ciclo de eventos distribuir entre elas. A próxima página cobre as versões asyncio de tudo o que está nesta.