# How to Implement Length-Prefixed JSON Messages for vsock Communication

> Implement length-prefixed JSON messages over vsock for reliable communication. Learn how to combine a 4-byte header with a JSON body for effective message delimiting.

- Repository: [Lakr/vphone-cli](https://github.com/Lakr233/vphone-cli)
- Tags: how-to-guide
- Published: 2026-09-09

---

**Length-prefixed JSON messaging over vsock combines a 4-byte big-endian header containing the payload size with a UTF-8 encoded JSON body, enabling reliable message delimiting without stream delimiters.**

The **Lakr233/vphone-cli** project demonstrates a robust implementation of **length-prefixed JSON messages for vsock communication** between a macOS host and a guest daemon (`vphoned`). This binary framing protocol ensures that JSON payloads are never misparsed, even when transmitted over raw virtual sockets that lack built-in message boundaries. Understanding this approach is essential for building reliable IPC mechanisms in virtualization environments.

## Protocol Structure and Message Layout

### Header and Payload Format

Each message consists of two distinct parts. The **header** occupies exactly 4 bytes and stores the payload length as an unsigned 32-bit integer in network byte order (big-endian). The **payload** follows immediately, containing a UTF-8 encoded JSON document.

This design eliminates the need for newline or null-terminator delimiters, which could appear inside valid JSON strings. The receiver first reads the fixed-size header, converts it from big-endian to host order, then allocates a buffer of exactly that size to receive the complete JSON object.

### Required and Optional JSON Fields

Every JSON payload must contain at minimum two fields:

- **`"v"`** – Protocol version (integer)
- **`"t"`** – Message type (string)

Optional fields include `"id"` for request identifiers and command-specific data structures. For example, a handshake message looks like `{"v": 1, "t": "hello"}`.

## Implementing the Sender

The `writeMessage(fd:dict:)` function in [`sources/vphone-cli/VPhoneControl.swift`](https://github.com/Lakr233/vphone-cli/blob/main/sources/vphone-cli/VPhoneControl.swift) (lines 45-56) handles serialization and transmission. It converts the `[String: Any]` dictionary to JSON data, prefixes the 4-byte length header, and guarantees complete delivery through `writeFully`.

```swift
let hello: [String: Any] = ["v": 1, "t": "hello"]
guard VPhoneControl.writeMessage(fd: socketFD, dict: hello) else {
    print("Failed to send hello")
}

```

The helper `writeFully(fd:buf:count:)` (lines 88-95) loops over `Darwin.write` until all bytes are transmitted, protecting against short writes common in socket I/O.

## Implementing the Receiver

The `readMessage(fd:)` function (lines 58-74) implements the inverse operation. It first calls `readFully` to obtain exactly 4 bytes for the header, validates the length (ensuring it is greater than 0 and reasonably bounded), then allocates a buffer and reads the remaining payload bytes.

```swift
if let msg = VPhoneControl.readMessage(fd: socketFD) {
    let type = msg["t"] as? String ?? "unknown"
    let version = msg["v"] as? Int ?? 0
    print("Received \(type) (v\(version))")
}

```

The underlying `readFully(fd:buf:count:)` function (lines 76-85) repeatedly invokes `Darwin.read` until the requested byte count is satisfied, handling partial reads that occur when the kernel buffer contains fewer bytes than requested.

## Handling I/O Robustness and Errors

Beyond basic framing, the protocol layer implements **resilient error handling**. If any read or write operation fails, `shutdownSocket` closes the file descriptor and schedules a reconnection attempt. Higher-level async logic uses `armRequestTimeout` and `timeoutForRequest` to enforce per-message-type timeouts, preventing indefinite blocking when the guest daemon becomes unresponsive.

The `writeFully` and `readFully` wrappers are critical for production reliability. Raw POSIX sockets may return after transferring fewer bytes than requested, especially under high load or when network buffers are constrained. These helpers ensure atomic message delivery semantics required by the JSON framing layer.

## Real-World Usage Examples

### Initial Handshake

The host initiates communication by sending a version announcement:

```swift
let hello: [String: Any] = ["v": 1, "t": "hello"]
guard VPhoneControl.writeMessage(fd: socketFD, dict: hello) else {
    // Handle connection failure
}

```

### Asynchronous Request-Response

For operations requiring confirmation, the control layer provides an async API that internally manages request IDs:

```swift
do {
    let (resp, _) = try await control.sendRequest(["t": "version"])
    let guestHash = resp["hash"] as? String ?? "n/a"
    print("Guest version hash: \(guestHash)")
} catch {
    print("Version request failed: \(error)")
}

```

The camera server ([`VPhoneCameraServer.swift`](https://github.com/Lakr233/vphone-cli/blob/main/VPhoneCameraServer.swift)) demonstrates a secondary channel on port 1338 using identical framing helpers to stream video frames, proving the protocol's flexibility for different data types.

## Key Source Files

| File | Role | Location |
|------|------|----------|
| **VPhoneControl.swift** | Core vsock client implementing length-prefixed JSON framing, request/response handling, and auto-update logic. | [`sources/vphone-cli/VPhoneControl.swift`](https://github.com/Lakr233/vphone-cli/blob/main/sources/vphone-cli/VPhoneControl.swift) |
| **VPhoneCameraServer.swift** | Secondary vsock channel (port 1338) using the same framing helpers for streaming camera frames. | [`sources/vphone-cli/VPhoneCameraServer.swift`](https://github.com/Lakr233/vphone-cli/blob/main/sources/vphone-cli/VPhoneCameraServer.swift) |
| **VPhoneMenuCamera.swift** | UI layer triggering camera connections and displaying protocol version information. | [`sources/vphone-cli/VPhoneMenuCamera.swift`](https://github.com/Lakr233/vphone-cli/blob/main/sources/vphone-cli/VPhoneMenuCamera.swift) |

## Summary

- **Length-prefixed framing** uses a 4-byte big-endian header followed by UTF-8 JSON to eliminate delimiter ambiguity.
- **`writeMessage`** and **`readMessage`** in [`VPhoneControl.swift`](https://github.com/Lakr233/vphone-cli/blob/main/VPhoneControl.swift) handle the complete serialization lifecycle.
- **`readFully`** and **`writeFully`** guarantee atomic byte transfer despite POSIX short-read/write behavior.
- All messages require `"v"` (version) and `"t"` (type) fields at minimum.
- The protocol supports multiplexed channels (e.g., control on port 1337, camera on port 1338) using identical binary framing.

## Frequently Asked Questions

### What is the purpose of the 4-byte header in vsock communication?

The 4-byte big-endian header encodes the exact byte length of the subsequent JSON payload. This allows the receiver to precisely allocate buffers and detect message boundaries without scanning for delimiters, which is essential because raw vsock streams are byte-oriented and do not preserve message boundaries.

### How does vphone-cli handle partial reads when receiving JSON messages?

The `readFully(fd:buf:count:)` helper repeatedly calls `Darwin.read` until the exact requested byte count is received. This protects against short reads caused by kernel buffer limitations, ensuring the header and payload are always read completely before JSON deserialization occurs.

### What fields are required in every JSON message sent through the vphone-cli protocol?

Every message must include `"v"` indicating the protocol version (integer) and `"t"` indicating the message type (string). Optional fields such as `"id"` for request tracking or command-specific parameters are appended as needed for specific operations like version checks or camera streaming.

### How does the protocol handle connection timeouts and errors?

The control layer wraps I/O operations in higher-level async logic using `armRequestTimeout` and `timeoutForRequest` to enforce per-operation time limits. If a read or write fails, `shutdownSocket` immediately closes the file descriptor and triggers a reconnection sequence, preventing resource leaks and ensuring system stability.