9.13. Sockets TCP

Os sockets TCP aparecem em duas formas que parecem diferentes, mas compartilham o mesmo tipo subjacente: sockets cliente, que usam connect() para se conectar a um servidor remoto, e sockets servidor, que usam bind(), listen() e accept() para aceitar conexões de entrada. Ambos os papéis usam a mesma classe socket apresentada em Objetos socket; apenas os métodos chamados sobre eles diferem.

9.13.1. Um cliente TCP

O cliente mais simples abre uma conexão, envia uma requisição, 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 abordado em TCP – um fluxo confiável de bytes e retorna quando a conexão está aberta. send() grava bytes na conexão; recv() lê até um determinado número de bytes a partir dela. Uma vez que a aplicação termina, close() encerra a conexão.

O mesmo script encapsulado no idiomatismo da instrução with de Objetos socket, de modo que o socket é fechado mesmo que algo levante 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. Lendo até o fim

Uma única chamada a recv() retorna até o número de bytes solicitado – ela pode retornar menos, porque o TCP é um fluxo e não uma sequência de mensagens. A aplicação precisa continuar lendo 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 laço termina quando recv() retorna um bytes vazio. Isso ocorre quando o outro lado fechou de forma limpa a sua metade da conexão; a aplicação interpreta “fim do fluxo” como o mesmo que “fim da mensagem” nesse estilo de protocolo.

9.13.1.2. Enviando até o fim

A ressalva oposta aplica-se a send(): ele pode enviar menos bytes do que o solicitado, retornando a contagem de bytes efetivamente gravados. Para cargas úteis grandes, reenvie o restante não enviado:

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

sendall() faz o laço internamente, de modo que a maioria dos códigos 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 tem quatro etapas: reivindicar uma porta, colocar o socket em modo de escuta, aceitar conexões uma a uma e conversar em cada socket aceito. 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() reivindica um host e uma porta na câmera. "0.0.0.0" aceita em qualquer interface; substituí-lo por um IP específico restringe o ouvinte àquela interface.

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

  • accept() bloqueia até que um cliente se conecte e, então, retorna (conn, addr): um socket novo representando essa conexão específica e o endereço do cliente. O próprio socket de escuta permanece aberto para aceitar mais.

  • Todos os bytes da conversa fluem através de conn, o novo socket. As leituras e gravações usam as mesmas chamadas recv() / send() do lado do cliente.

  • Quando o cliente fecha, recv() retorna b""; o laço interno termina e o servidor fecha o seu lado com close().

O while True externo volta para accept() para aguardar o próximo cliente. Nessa forma, o servidor atende um cliente de cada vez; executar múltiplos clientes em paralelo requer threads ou asyncio. Este último é o assunto da próxima página.

9.13.3. Armadilhas comuns

  • Tratar recv() como orientado a mensagens. Não é. Duas chamadas send(b"hi") podem chegar como um único recv(4) de b"hihi", ou como dois recv(2)s. A aplicação precisa adicionar enquadramento caso os limites das mensagens importem – uma quebra de linha, um prefixo de comprimento, o que for.

  • Esquecer de reenviar em envios curtos. Use sendall() para qualquer coisa além de algumas centenas de bytes.

  • Esquecer de fechar o socket aceito. Cada conn é um socket separado; fechar o socket de escuta não fecha os aceitos. Blocos with em ambos tornam difícil errar nisso:

    while True:
        with server.accept()[0] as conn:
            # ... talk on conn ...
    
  • Re-vincular a uma porta ainda em TIME_WAIT. Quando um servidor reinicia poucos segundos após o fechamento, bind() pode falhar com “address in use” porque o MicroPython ainda está mantendo a porta para a conexão anterior. server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) antes de bind() resolve isso.

9.13.4. O que vem a seguir

Bloquear em accept() significa que o servidor só pode atender um cliente de cada vez. Bloquear em recv() significa que um único cliente lento trava o laço inteiro. A resposta padrão na câmera é asyncio – executar cada conexão como sua própria tarefa e deixar o laço de eventos despachar entre elas. A próxima página aborda as versões asyncio de tudo o que há nesta.