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 chamadasrecv()/send()do lado do cliente.Quando o cliente fecha,
recv()retornab""; o laço interno termina e o servidor fecha o seu lado comclose().
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 únicorecv(4)deb"hihi", ou como doisrecv(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. Blocoswithem 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 debind()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.