12.13. TCP sockets

TCP sockets come in two shapes that look different but share the same underlying type: client sockets that connect() to a remote server, and server sockets that bind(), listen(), and accept() incoming connections. Both roles use the same socket class introduced on Socket objects; only the methods called on them differ.

12.13.1. A TCP client

The simplest client opens a connection, sends a request, reads the reply, and closes:

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() runs the three-way handshake covered on TCP – a reliable stream of bytes and returns when the connection is open. send() writes bytes to the connection; recv() reads up to a given number of bytes from it. Once the application is done, close() shuts the connection down.

The same script wrapped in the with-statement idiom from Socket objects, so the socket is closed even if something raises:

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))

12.13.1.1. Reading until done

A single recv() returns up to the requested number of bytes – it can return fewer, because TCP is a stream rather than a sequence of messages. The application has to keep reading until it has the full reply:

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

The loop ends when recv() returns an empty bytes. That happens when the other side has cleanly closed its half of the connection; the application reads “end of stream” as the same as “end of message” in this style of protocol.

12.13.1.2. Sending until done

The opposite caveat applies to send(): it may send fewer bytes than requested, returning the count of bytes actually written. For large payloads, retry the unsent remainder:

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

sendall() does the loop internally, so most code can just call that and avoid the manual retry:

s.sendall(some_big_bytes)

12.13.2. A TCP server

The server side is four steps: claim a port, switch the socket to listening mode, accept connections one by one, talk on each accepted socket. A minimal echo server:

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()

Step by step:

  • bind() claims a host and port on the camera. "0.0.0.0" accepts on any interface; replacing it with a specific IP restricts the listener to that interface.

  • listen() switches the socket from a normal socket to a listening socket. The argument is the backlog – how many pending connections MicroPython will queue while the application is busy. Pick a small number; 1 is fine for most cases.

  • accept() blocks until a client connects, then returns (conn, addr): a new socket representing this one connection, and the client’s address. The listening socket itself stays open to accept more.

  • All the bytes for the conversation flow through conn, the new socket. Reads and writes use the same recv() / send() calls as on the client side.

  • When the client closes, recv() returns b""; the inner loop ends and the server closes its end with close().

The outer while True jumps back to accept() to wait for the next client. The server handles one client at a time in this shape; running multiple clients in parallel needs either threads or asyncio. The latter is the subject of the next page.

12.13.3. Common pitfalls

  • Treating recv() as message-shaped. It is not. Two send(b"hi") calls might arrive as one recv(4) of b"hihi", or as two recv(2)s. The application has to add framing if message boundaries matter – a newline, a length prefix, whatever.

  • Forgetting to retry on short sends. Use sendall() for anything beyond a few hundred bytes.

  • Forgetting to close the accepted socket. Each conn is a separate socket; closing the listening socket does not close the accepted ones. with-blocks on both make this hard to get wrong:

    while True:
        with server.accept()[0] as conn:
            # ... talk on conn ...
    
  • Re-binding to a port still in TIME_WAIT. When a server restarts within a few seconds of closing, bind() may fail with “address in use” because MicroPython is still holding the port for the previous connection. server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) before bind() clears this.

12.13.4. What’s next

Blocking on accept() means the server can serve only one client at a time. Blocking on recv() means a single slow client hangs the whole loop. The standard answer on the camera is asyncio – run each connection as its own task, let the event loop dispatch between them. The next page covers the asyncio versions of everything on this one.