TCP Sticky Packet Issue
TCP is a byte-stream protocol with no concept of message boundaries. When the sender transmits multiple messages in quick succession, or when the receiver does not read data fast enough, multiple messages may be merged into a single receive — this is known as sticky packets (also called TCP packet coalescing).
Why Sticky Packets Occur
- Sender side: The Nagle algorithm merges multiple small, closely timed
sendcalls into a single large packet. - Receiver side:
recv(n)reads at mostnbytes at a time. When multiple messages accumulate in the buffer, onerecvcall may span across multiple messages.
UDP does not have sticky packets: UDP is message-oriented; each datagram has an independent boundary, and recvfrom reads exactly one complete datagram at a time.
When You Need to Solve This
- Must solve: Real-time messaging, command/response protocols — you must know the boundary of each message.
- Not necessary: File transfer — you only need to reassemble all the bytes at the end; message boundaries don’t matter.
Solution: Fixed-Length Header (struct)
struct.pack("i", n) packs the integer n into a fixed 4-byte representation. The receiver first reads 4 bytes to get the message length, then reads that many bytes of data, precisely splitting messages.
Basic Version (Length-Only Header)
Server
import socket
import struct
def run_server(host: str = "127.0.0.1", port: int = 9010) -> None:
server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
server.bind((host, port))
server.listen(5)
print(f"Server listening on {host}:{port}")
conn, addr = server.accept()
print(f"Client connected: {addr}")
while True:
raw_cmd = conn.recv(1024)
if not raw_cmd:
break
cmd = raw_cmd.decode("utf-8")
print(f"Received command: {cmd}")
reply = f"Executed: {cmd}".encode("utf-8")
# Send 4-byte length first, then the actual data
conn.send(struct.pack("i", len(reply)))
conn.sendall(reply)
conn.close()
server.close()
if __name__ == "__main__":
run_server()Client
import socket
import struct
def run_client(host: str = "127.0.0.1", port: int = 9010) -> None:
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as client:
client.connect((host, port))
commands = ["ls -la", "pwd", "date"]
for cmd in commands:
client.send(cmd.encode("utf-8"))
# Read 4 bytes first to get the message length
raw_len = client.recv(4)
msg_len = struct.unpack("i", raw_len)[0]
# Read in a loop by length to handle short recv reads
data = b""
while len(data) < msg_len:
chunk = client.recv(min(1024, msg_len - len(data)))
if not chunk:
break
data += chunk
print(f"Server reply: {data.decode('utf-8')}")
if __name__ == "__main__":
run_client()Advanced Version (JSON Header + Payload)
For complex scenarios (file transfer, multi-field metadata), the header can be designed as a JSON dictionary:
import socket
import struct
import json
from pathlib import Path
def send_with_header(conn: socket.socket, payload: bytes, metadata: dict) -> None:
"""Send: 4-byte header length + JSON header + actual data."""
head_json = json.dumps(metadata, ensure_ascii=False).encode("utf-8")
# Send header length first (4 bytes)
conn.send(struct.pack("i", len(head_json)))
# Then send the JSON header
conn.send(head_json)
# Finally send the actual data
conn.sendall(payload)
def recv_with_header(conn: socket.socket) -> tuple[dict, bytes]:
"""Receive: read header length, then JSON header, then actual data."""
raw = conn.recv(4)
head_len = struct.unpack("i", raw)[0]
head_json = conn.recv(head_len).decode("utf-8")
metadata = json.loads(head_json)
data_len = metadata["size"]
data = b""
while len(data) < data_len:
chunk = conn.recv(min(4096, data_len - len(data)))
if not chunk:
break
data += chunk
return metadata, datastruct Format Quick Reference
import struct
# pack(fmt, value) → bytes, unpack(fmt, bytes) → tuple
struct.pack("i", 12345) # int → 4 bytes
struct.pack("I", 12345) # unsigned int → 4 bytes
struct.pack("q", 9_999_999_999) # long long → 8 bytes (larger range)
tup = struct.unpack("i", struct.pack("i", 12345))
print(tup[0]) # 12345
# calcsize: query the byte size of a format
print(struct.calcsize("i")) # 4
print(struct.calcsize("q")) # 8"i" has a range of approximately ±2.1 billion. For large file transfers, use "q" (±9.2 × 10^18) instead.