# How Messages Are Encoded in the Length‑Prefixed JSON vsock Protocol: v, t, and id Fields Explained

> Understand how messages are encoded in the length-prefixed JSON vsock protocol. Learn about the v, t, and id fields for asynchronous request-response matching in vphone-cli.

- Repository: [Lakr/vphone-cli](https://github.com/Lakr233/vphone-cli)
- Tags: deep-dive
- Published: 2026-09-08

---

**Each message in the vphone‑cli vsock protocol consists of a 4‑byte big‑endian length header followed by a UTF‑8 JSON payload that must include the fields `v` (protocol version), `t` (message type), and optionally `id` (request identifier) to enable asynchronous request‑response matching.**

The `Lakr233/vphone-cli` project implements a lightweight host‑guest communication channel over virtio‑socket (vsock) using a length‑prefixed JSON framing scheme. Understanding how this protocol encodes the mandatory `v`, `t`, and `id` fields is essential for extending the toolset or debugging communication failures between the macOS host and the virtualized guest.

## Binary Framing Structure

The protocol uses a simple yet strict binary layout for every message transmitted over the vsock file descriptor. Each outbound or inbound message follows this exact structure:

```

[uint32 big-endian payload length][UTF-8 JSON payload]

```

- The first **4 bytes** encode the length of the JSON payload as an unsigned 32‑bit integer in **big‑endian** order.
- The next *length* bytes contain a UTF‑8 encoded JSON object.

The implementation in [`VPhoneControl.swift`](https://github.com/Lakr233/vphone-cli/blob/main/VPhoneControl.swift) enforces a maximum payload size of 4 MiB (4 × 1024 × 1024 bytes) to prevent memory exhaustion attacks.

## Required JSON Fields

Every JSON payload must carry three standard keys to ensure protocol versioning and message routing.

### Protocol Version (v)

The `v` field specifies the protocol version. Currently, all messages must set `"v": 1`. The guest daemon **vphoned** rejects any message whose version does not match the expected value, ensuring both parties operate on compatible protocol semantics.

### Message Type (t)

The `t` field defines the operation being requested. Valid values include `"hello"`, `"hid"`, `"file_get"`, `"file_put"`, and `"update"`. This type determines how the receiver parses the remaining operation‑specific fields (e.g., `"page"` and `"usage"` for HID events, or `"path"` for file operations).

### Request Identifier (id)

The `id` field is optional but critical for asynchronous operations. When present, the host generates a unique hexadecimal string (e.g., `"a1b2c3"`) and includes it in the request dictionary. The guest echoes the same `id` in its response, allowing the host to match incoming messages to pending `PendingRequest` objects in the `startReadLoop` handler.

## Implementation in VPhoneControl.swift

The host‑side implementation lives in **[`sources/vphone-cli/VPhoneControl.swift`](https://github.com/Lakr233/vphone-cli/blob/main/sources/vphone-cli/VPhoneControl.swift)**, which handles the complete lifecycle of message encoding and decoding.

### Writing Messages

The `writeMessage(fd:dict:)` method constructs the length‑prefixed frame. It serializes the dictionary to JSON, calculates the big‑endian length header, and writes both parts atomically using `Darwin.write`:

```swift
private func writeMessage(fd: Int32, dict: [String: Any]) -> Bool {
    guard let json = try? JSONSerialization.data(withJSONObject: dict) else {
        return false
    }
    let length = UInt32(json.count)
    var header = length.bigEndian
    // Write 4-byte length header
    let headerWritten = Darwin.write(fd, &header, 4) == 4
    // Write JSON payload
    let payloadWritten = Darwin.write(fd, [UInt8](json), json.count) == json.count
    return headerWritten && payloadWritten
}

```

### Reading Messages

The `readMessage(fd:)` method reverses the process. It first reads exactly 4 bytes to decode the length, validates that the size is within bounds (greater than 0 and less than 4 MiB), then reads the full payload and deserializes it:

```swift
private static func readMessage(fd: Int32) -> [String: Any]? {
    var header: UInt32 = 0
    guard readFully(fd: fd, &header, 4) else { return nil }
    
    let length = Int(UInt32(bigEndian: header))
    guard length > 0 && length < 4 * 1024 * 1024 else { return nil }
    
    let buffer = UnsafeMutablePointer<UInt8>.allocate(capacity: length)
    defer { buffer.deallocate() }
    
    guard readFully(fd: fd, buffer, length) else { return nil }
    let data = Data(bytes: buffer, count: length)
    return try? JSONSerialization.jsonObject(with: data) as? [String: Any]
}

```

### Request-Response Cycle

During a normal request‑response cycle, the `sendRequest` method automatically injects the `v` and `id` fields into the caller’s dictionary before transmission:

1. The host generates a unique `id` and adds `v: 1`, `t: <operation>`, and `id: <hex>` to the request.
2. `writeMessage(fd:dict:)` transmits the length‑prefixed JSON over the vsock.
3. The `startReadLoop` receives the response, extracts the `id`, looks up the corresponding `PendingRequest`, and delivers the result.

## Binary Payload Handling

For operations requiring raw binary data (such as file transfers or camera frames), the protocol extends the basic JSON frame. After the initial `"hello"` handshake, if the guest responds with `"need_update": true`, the host sends an `"update"` message containing the standard `v`, `t`, and `id` fields in the JSON header. Immediately following this header, the raw binary payload is streamed directly without additional length prefixes.

## Summary

- The **length‑prefixed JSON vsock protocol** uses a 4‑byte big‑endian length header followed by UTF‑8 JSON payload.
- Every JSON message must include **`v`** (version 1), **`t`** (message type), and optionally **`id`** (request tracking).
- The **`writeMessage`** function in [`VPhoneControl.swift`](https://github.com/Lakr233/vphone-cli/blob/main/VPhoneControl.swift) atomically writes the length header and JSON payload using `Darwin.write`.
- The **`readMessage`** function validates the 4‑byte length, enforces a 4 MiB limit, and deserializes the payload.
- Binary data transfers follow the JSON header directly without additional framing.

## Frequently Asked Questions

### What is the maximum payload size allowed in the length‑prefixed JSON vsock protocol?

The implementation enforces a hard limit of **4 MiB** (4 × 1024 × 1024 bytes) for the JSON payload. The `readMessage` function validates this boundary after decoding the 4‑byte length header to prevent memory exhaustion.

### What happens if the protocol version `v` does not match?

The guest daemon **vphoned** validates the `v` field and rejects messages with non‑matching versions. Both host and guest must agree on version `1`; otherwise, the connection will drop or return an error, ensuring protocol compatibility.

### Is the `id` field required for every message?

No, the `id` field is optional for one‑way notifications but **required** for request‑response pairs. When present, the guest must echo the same identifier in its reply, enabling the host’s `startReadLoop` to correlate asynchronous responses with their original `PendingRequest` objects.

### How does the protocol handle large binary transfers like camera frames?

For large binary transfers, the protocol sends a standard JSON header containing `v`, `t`, and `id` fields using the usual length‑prefixed framing, then streams the raw binary payload immediately afterward without additional length headers. This hybrid approach keeps metadata structured while allowing efficient binary streaming.