9.13. Sockets TCP¶
Los sockets TCP se presentan en dos formas que parecen distintas pero que comparten el mismo tipo subyacente: los sockets cliente, que se conectan con connect() a un servidor remoto, y los sockets servidor, que usan bind(), listen() y accept() para aceptar conexiones entrantes. Ambos roles utilizan la misma clase socket presentada en Objetos socket; lo único que difiere son los métodos que se invocan sobre ellos.
9.13.1. Un cliente TCP¶
El cliente más sencillo abre una conexión, envía una solicitud, lee la respuesta y se cierra:
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() ejecuta el saludo de tres vías descrito en TCP – un flujo fiable de bytes y retorna cuando la conexión está abierta. send() escribe bytes en la conexión; recv() lee hasta un número dado de bytes de ella. Una vez que la aplicación termina, close() cierra la conexión.
El mismo script envuelto en el modismo de la sentencia with de Objetos socket, de modo que el socket se cierra incluso si algo lanza una excepción:
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. Leer hasta terminar¶
Una sola llamada a recv() retorna hasta el número de bytes solicitado, y puede retornar menos, porque TCP es un flujo en vez de una secuencia de mensajes. La aplicación tiene que seguir leyendo hasta obtener la respuesta completa:
chunks = []
while True:
chunk = s.recv(1024)
if not chunk: # empty bytes -> other side closed
break
chunks.append(chunk)
reply = b"".join(chunks)
El bucle termina cuando recv() retorna un objeto bytes vacío. Eso ocurre cuando el otro extremo ha cerrado limpiamente su mitad de la conexión; en este estilo de protocolo, la aplicación interpreta el «fin de flujo» igual que el «fin de mensaje».
9.13.1.2. Enviar hasta terminar¶
La advertencia opuesta se aplica a send(): puede enviar menos bytes de los solicitados, retornando el número de bytes realmente escritos. Para cargas útiles grandes, reintenta con el resto sin enviar:
payload = some_big_bytes
while payload:
n = s.send(payload)
payload = payload[n:]
sendall() hace el bucle internamente, de modo que la mayoría del código puede simplemente llamarlo y evitar el reintento manual:
s.sendall(some_big_bytes)
9.13.2. Un servidor TCP¶
El lado del servidor consta de cuatro pasos: reclamar un puerto, cambiar el socket a modo de escucha, aceptar conexiones una a una y comunicarse en cada socket aceptado. Un 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()
Paso a paso:
bind()reclama un host y un puerto en la cámara."0.0.0.0"acepta en cualquier interfaz; al reemplazarlo por una IP específica se restringe el oyente a esa interfaz.listen()cambia el socket de un socket normal a un socket de escucha. El argumento es el backlog, es decir, cuántas conexiones pendientes pondrá MicroPython en cola mientras la aplicación está ocupada. Elige un número pequeño;1está bien para la mayoría de los casos.accept()se bloquea hasta que un cliente se conecta y entonces retorna(conn, addr): un nuevo socket que representa esta conexión concreta y la dirección del cliente. El propio socket de escucha permanece abierto para aceptar más.Todos los bytes de la conversación fluyen a través de
conn, el nuevo socket. Las lecturas y escrituras usan las mismas llamadasrecv()/send()que en el lado del cliente.Cuando el cliente se cierra,
recv()retornab""; el bucle interno termina y el servidor cierra su extremo conclose().
El while True exterior vuelve a accept() para esperar al siguiente cliente. En esta forma, el servidor atiende un cliente a la vez; ejecutar varios clientes en paralelo requiere hilos o asyncio. Esto último es el tema de la página siguiente.
9.13.3. Errores comunes¶
Tratar recv() como si tuviera forma de mensaje. No la tiene. Dos llamadas
send(b"hi")podrían llegar como un únicorecv(4)deb"hihi", o como dosrecv(2). La aplicación tiene que añadir un encuadrado si los límites de los mensajes importan: un salto de línea, un prefijo de longitud, lo que sea.Olvidar reintentar en envíos cortos. Usa
sendall()para cualquier cosa que supere unos pocos cientos de bytes.Olvidar cerrar el socket aceptado. Cada
connes un socket independiente; cerrar el socket de escucha no cierra los aceptados. Los bloqueswithen ambos hacen difícil equivocarse:while True: with server.accept()[0] as conn: # ... talk on conn ...
Volver a vincularse a un puerto que sigue en TIME_WAIT. Cuando un servidor se reinicia pocos segundos después de cerrarse,
bind()puede fallar con «address in use» porque MicroPython sigue reteniendo el puerto de la conexión anterior.server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)antes debind()soluciona esto.
9.13.4. Qué sigue¶
Bloquearse en accept() significa que el servidor solo puede atender un cliente a la vez. Bloquearse en recv() significa que un único cliente lento cuelga todo el bucle. La respuesta estándar en la cámara es asyncio: ejecuta cada conexión como su propia tarea y deja que el bucle de eventos reparta el trabajo entre ellas. La página siguiente cubre las versiones con asyncio de todo lo de esta.