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 chamadasrecv()/send()que no lado do cliente.Quando o cliente fecha,
recv()devolveb"", o ciclo interno termina e o servidor fecha a sua extremidade comclose().
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 únicorecv(4)comb"hihi", ou como doisrecv(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 blocoswithem 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 debind()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.