You type take picture, the first JPEG lands perfectly, and every capture after that shows the same photo. The socket is not broken and neither is the camera. TCP delivers one ordered byte stream with no message boundaries, so nothing on the wire marks where image one ends and image two begins - and the classic example code saves every capture into one file it opened once. The fix is framing: fixed-length commands, an 8-byte length prefix in front of every image, and recv() loops that read exactly that many bytes into a new file per capture. This guide gives you the protocol, the server and client code, the buffer arithmetic on a GigE link, and the test that proves image two arrives as image two.
Why the first image arrives intact and the second never does
TCP is a reliable byte stream: bytes arrive complete, in order, and identical to what was sent, but with no structure on top (Transmission Control Protocol). Three consequences break the take-picture pattern:
- One recv() is not one image. recv(1000000) returns whatever is in the buffer right now - half a JPEG, or one and a half. Buffer size does not change this; only a length header tells the receiver where to stop.
- One file opened once collects everything. Open photo.jpg before the loop and every capture appends to it, so image two's bytes land directly after image one's.
- A JPEG viewer stops at the first end-of-image marker. The combined file renders as image one forever while its size grows - the exact symptom of a second take picture showing the first photo.
Streams can also merge or split your commands: two take picture requests 100 ms apart may arrive inside one recv(1024), or one may arrive in pieces. Fixed-length commands remove that problem too.
The framing protocol: fixed command, 8-byte header, payload
Both sides agree on three message types. Every field is either a fixed size or an announced length, so one recv() can never cross a message edge:
| Message | Direction | Size | Content |
|---|---|---|---|
| Command | server to client | 16 bytes | ASCII, space-padded: take picture, exit
|
| Length header | client to server | 8 bytes |
struct.pack('>Q', n): payload length, unsigned 64-bit |
| Payload | client to server | n bytes | one JPEG, encoded at quality 90 |
- The server sends a 16-byte command, space-padded to its fixed length.
- The client encodes one frame with cv2.imencode at JPEG quality 90.
- The client prepends the payload length and calls sendall once, header plus payload together.
- The receiver reads exactly 16 bytes, then exactly 8, then exactly n.
- The receiver writes one new file per capture, named from a counter.
The header comes from struct.pack('>Q', n): > is big-endian network order, Q is an unsigned 64-bit integer, and struct.calcsize('>Q') returns 8. Four bytes would also do - >I caps a single image at 4,294,967,295 bytes, about 4.3 GB - but eight cost nothing. One more subtlety from the way TCP works: even the length prefix itself can arrive split across reads, so every fixed-size read loops too.
Read exactly n bytes: the recv_exact helper
Every read in the protocol goes through one helper. It loops until it holds n bytes, caps the final read at the remainder so it cannot swallow the next message's header, and returns short only when the peer has closed:
import socket
import struct
CHUNK = 4096 # int: max bytes one recv() call may return
def recv_exact(sock: socket.socket, n: int) -> bytes:
# returns exactly n bytes; returns fewer only if the peer closed
parts = bytearray()
while len(parts) < n:
chunk = sock.recv(min(n - len(parts), CHUNK))
if not chunk:
break # recv() returned b'': peer closed, not 'no data yet'
parts.extend(chunk)
return bytes(parts)
That b'' check matters: without it, a vanished client turns the server loop into a busy spin on empty reads.
Server: one file per capture
Three lines fix the classic failure: the output file opens inside the loop under a counter-generated name, every read goes through recv_exact, and a short header or payload breaks the connection instead of writing junk:
HOST = '' # str: all local interfaces
PORT = 8000 # int: same port on both sides
CMD_LEN = 16 # int: fixed-length ASCII command
HDR = 8 # int: bytes in a '>Q' header
server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
server.bind((HOST, PORT))
server.listen(1)
print('listening on port', PORT)
conn, _ = server.accept()
conn.settimeout(30.0) # seconds: recv raises socket.timeout when the line goes silent
shot = 0 # int: capture counter for file names
while True:
cmd = recv_exact(conn, CMD_LEN).rstrip()
if cmd == b'exit':
break
if cmd != b'take picture':
continue
head = recv_exact(conn, HDR)
if len(head) != HDR:
break # peer closed mid-message
(n,) = struct.unpack('>Q', head)
payload = recv_exact(conn, n)
if len(payload) != n:
break # peer closed mid-image: discard, never write a partial file
shot += 1
name = f'capture_{shot:04d}.jpg'
with open(name, 'wb') as f: # a new file per capture
f.write(payload)
print(name, n, 'bytes')
conn.close()
server.close()
Binding HOST = '' accepts connections on every interface. Binding to the one address socket.gethostbyname(socket.gethostname()) returns accepts only traffic addressed to that adapter - the classic way a two-NIC machine looks unreachable.
Client: encode once, send once
The client mirrors the framing and sends each image exactly once. A second sendall of the same bytes puts two images on the wire under one length header, and the receiver writes the overflow into the next image's slot:
import cv2
HOST = '10.0.0.12' # str: the receiver's address
PORT = 8000 # int: same port on both sides
CMD_LEN = 16 # int: matches the server
def send_jpeg(sock: socket.socket, frame) -> None:
ok, enc = cv2.imencode('.jpg', frame, [int(cv2.IMWRITE_JPEG_QUALITY), 90])
if not ok:
raise RuntimeError('JPEG encode failed')
payload = enc.tobytes() # bytes: exactly one image
sock.sendall(struct.pack('>Q', len(payload)) + payload) # header + payload, one call
client = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
client.connect((HOST, PORT))
cap = cv2.VideoCapture(0) # open once, before the loop
while True:
cmd = recv_exact(client, CMD_LEN).rstrip()
if cmd == b'exit':
break
if cmd != b'take picture':
continue
ok, frame = cap.read()
if not ok:
continue
send_jpeg(client, frame)
cap.release()
client.close()
Worked example: a 1,000,000-byte quality-90 JPEG on GigE
Inputs. Payload n = 1,000,000 bytes - the buffer the original receiver reserved for one image, and a realistic size for a quality-90 JPEG from a 1080p-class webcam. Header = 8 bytes (>Q), command = 16 bytes, CHUNK = 4096 bytes. Link = GigE: 1,000,000,000 bit/s line rate, with a quiet link moving TCP payload at roughly 800 Mbit/s, about 100 MB/s.
Rule. A recv() call returns whatever has already arrived, so the payload takes at least ceil(n / CHUNK) calls, and wire time is bits over goodput:
Arithmetic. Bytes on the wire per capture: 8 + 1,000,000 = 1,000,008. Chunk coverage: 4096 x 244 = 999,424 bytes, leaving 1,000,000 - 999,424 = 576 bytes, so the payload needs 245 recv() calls, plus 1 for the header - 246 per capture, and more in practice because recv() returns partial chunks mid-flight. Wire time: 1,000,008 x 8 = 8,000,064 bits; at 800,000,000 bit/s that is 10.0 ms, and at the roughly 940 Mbit/s best case, 8.5 ms.
Decision. One quality-90 capture of this size costs about 10 ms of wire time, so a 50 ms cycle fits it with 40 ms to spare for acquisition and processing. The single recv(1000000) still fails even with a perfectly sized buffer - it returns only what has arrived, in the same 245 pieces - so the header, not the buffer size, is the fix. When captures need to arrive less than 10 ms apart, one GigE link is the limit: add a second NIC or move to 10GigE.
Prove it: two images in one send
The failure mode is coalescing, so the test forces it: both images leave in one sendall, and the framing must still separate them. socket.socketpair() provides an in-memory connection - no camera, no network:
import socket
import struct
def recv_exact(sock, n): # same helper as above
parts = bytearray()
while len(parts) < n:
chunk = sock.recv(n - len(parts))
if not chunk:
break
parts.extend(chunk)
return bytes(parts)
tx, rx = socket.socketpair() # an in-memory connected pair
img_a = bytes(range(256)) * 4 # bytes: 1024 bytes
img_b = bytes(3000) # bytes: 3000 zero bytes
tx.sendall(struct.pack('>Q', len(img_a)) + img_a
+ struct.pack('>Q', len(img_b)) + img_b) # ONE send, both images
(n1,) = struct.unpack('>Q', recv_exact(rx, 8))
got_a = recv_exact(rx, n1)
(n2,) = struct.unpack('>Q', recv_exact(rx, 8))
got_b = recv_exact(rx, n2)
assert (n1, len(got_a), got_a) == (1024, 1024, img_a), 'image one corrupted'
assert (n2, len(got_b), got_b) == (3000, 3000, img_b), 'image two corrupted'
print('framing ok: two intact images from one send')
Running it prints framing ok: two intact images from one send. Remove the header from the send and the same test fails on the first unpack: the receiver reads the first 8 bytes of img_a as a length. End to end, take two shots and check the files:
ls -l capture_0001.jpg capture_0002.jpg
cmp capture_0001.jpg capture_0002.jpg # must report the files differ
python -c 'import cv2; assert cv2.imread("capture_0002.jpg") is not None, "decode failed"'
Two files of similar size that both decode: image two finally arrives as image two.
Mistakes that put two images in one file
- Trusting one recv(). recv(n) means up to n, never exactly n - loop through recv_exact.
- Opening the output file once. With photo.jpg opened before the loop, ten captures write ten JPEGs into one file. Open per capture: capture_0001.jpg, capture_0002.jpg and on.
- Sending the payload twice. A pattern found in published client code calls sendall on the same bytes two times per capture; the header describes only the first copy and the second lands in the next image's header slot.
- Reading past the remainder. A final recv(CHUNK) beyond the payload count eats the next header - the reason the helper reads min(n - len(parts), CHUNK).
- Ignoring the empty bytes. recv() returns b'' only when the peer closed; treat it as a disconnect, close and re-accept.
- Unframed commands. Two commands inside one recv(1024), or one split across two reads, desynchronizes the loop; 16 fixed bytes keep the command boundary exact.
Camera SDK grabs need the same discipline: incomplete frames stay incomplete - discard, requeue, grab a new one.
When the peer is a PLC, a smart camera or a vision camera
The same framing rules hold for three other peers - and one case where you should not write the protocol at all.
A PLC as peer. Controller-side sends often write fixed-size buffers, so a large JPEG must be chunked to fit; one PLC integration hit a 480-byte send buffer, and the fix was the same header-plus-chunks arithmetic (a PLC send instruction with a fixed 480-byte TCP buffer). A worked controller-to-Python TCP example shows the other side of the same exchange (TCP socket communication between a motion controller and a Python client). When a robot controller receives coordinates instead of pixels, the identical fixed-length framing applies, as in send camera offsets to a robot over TCP socket messaging.
A smart camera as peer. If inspection can run on the camera itself, your Python program shrinks to command sender and result reader. The MV-SC5060XM-08M-WBN is a GigE smart camera for exactly that role:
- Interface
- GigE
- Resolution
- 6-10 MP
- Sensor format
- 1/2.4"
- Shutter
- Rolling
The command side of that exchange is the trigger a smart camera over TCP with ASCII commands and read results pattern.
A machine-vision camera as peer. A GigE or USB3 Vision camera does not speak your protocol; you drive it through its SDK against the GenICam standard, which frames the stream for you. The command a GigE Vision camera over Ethernet: capture and save images walkthrough shows that path - the MV-CU020-90GC is a 2.5 MP GigE model in this class:
- Sensor format
- 1/1.7"
- Lens mount
- C-mount
- Interface
- GigE
- Resolution
- Up to 3 MP
Browse area scan cameras and smart cameras, or go straight to the camera finder. Once frames flow, feed them straight to inference without temp files: feed socket-streamed frames to YOLO without temp files.
Checks before you deploy
- Same constants on both ends: CMD_LEN = 16, HDR = 8,
>Qbig-endian. One byte of disagreement turns every image into garbage. - Set a timeout on the accepted connection (
conn.settimeout(30.0)), catch socket.timeout, close and re-accept - a silent peer then costs seconds, not the shift. - Open port 8000 between exactly the two hosts, and log payload length and elapsed time per capture; a run of short reads is your first sign of a half-closed connection.
- Bind to
''on the server unless you mean to serve one NIC only. Run Cat5e or better cabling for a GigE camera link; the Ethernet cables for cameras the shop carries cover that run.
With a triggered machine-vision camera the acquisition half becomes a grab loop; the hardware trigger grab loop in Python: acquire and save each frame guide covers that side. To pick the camera, lens and light around this link, start the configurator; more systems guides sit on the systems topic page, and the contact page is the route to help matching parts.
Questions
Why does recv() return fewer bytes than you requested?
recv() hands you whatever has already arrived at your machine, up to the size you asked for; it is a stream read, not a message read. A 1,000,000-byte image arrives as hundreds of packets, so one call often returns a few thousand bytes. Loop until you have collected exactly the length you unpacked from the header, and cap the final read so it cannot swallow the next image's header.
How many bytes should the length header use?
Four bytes with struct.pack('>I', n) cap a single image at 4,294,967,295 bytes, about 4.3 GB, far above any camera frame. Eight bytes ('>Q') remove the ceiling for 4 extra bytes per capture. Either works; the rule is that both ends use the same format, because a reader using the wrong byte order decodes 256 as 65,536 and then waits forever for bytes that never come.
Can I close the connection after every image instead of sending a header?
Yes. If the sender closes or half-closes the write side with shutdown(socket.SHUT_WR) after each image, the receiver loops recv() until it returns b'' and treats that as the end of one message, so no header is needed. The cost is a TCP handshake per capture, a few milliseconds on a local network, and it only works while the connection carries exactly one image. Keep the length header for a long-lived command socket.
How do I send raw pixel data instead of JPEG?
Encode to a lossless container or send the raw array with its dimensions announced first. Size it before you choose: a 1920 x 1080 monochrome 8-bit frame is 1920 x 1080 = 2,073,600 bytes, about 2.1 MB, while the same frame at JPEG quality 90 is often 10 to 20 times smaller. Raw is right when the receiver needs exact pixels; JPEG is fine for archiving and most visual inspections.
Can two cameras send to one server at the same time?
Yes. Call accept() in a loop and give every connection its own thread or asyncio task, with its own recv_exact, and never share one socket between threads. Prefix the saved file name with a camera ID so sequences do not collide: cam1_capture_0007.jpg. The framing protocol does not change at all; only the server loop grows to hold one state per peer.
Does this protocol work for PNG, numpy arrays or video chunks?
The header carries a length, not a type, so the framing does not care what the bytes mean. For a numpy frame, send width, height and dtype in a short fixed-size text header, then the raw buffer. For mixed traffic on one socket, put a one-byte message type in front of the length header. Keep every field fixed-size so a single recv() can never merge two messages.

