---
title: 'Real-Time Communication'
source: 'https://academia.sh/en/courses/go-applications/real-time-communication'
course: 'The Standard Library and Network Applications'
language: en
updated: '2026-09-14T10:59:30+00:00'
license: 'CC BY-SA 4.0'
objectives:
    - 'Models a long-lived connection without a real network, with a buffer sharing io.Reader and io.Writer, and builds the data-push pattern'
    - 'Compares by running it that fixed-size reads miss the message boundary in an unframed stream, while a length prefix gives the boundary exactly in a framed stream'
    - 'Shows that a correctly parsed frame body still remains untyped, and that type requires a separate schema'
---

# Real-Time Communication

A long-lived connection is modeled, without opening a real port, with a byte buffer implementing io.Reader and io.Writer. The server pushes three messages one after another, without waiting for a request between them. In an unframed stream, the three pushes merge into a single byte sequence and fixed-size reads cannot correctly separate any message (0/3); in a stream framed with a length prefix, all three parse back correctly (3/3). The limiting measure: framing gives where a message ends, not the type of the value inside it.

The previous lesson measured a boundary where the two sides make a single exchange: the
sender wrote once, the receiver read once, the two sides never ran at the same time. This
lesson removes that assumption. When a connection **lasts a long time** — carrying many
crossings one after another without opening and closing — the only thing crossing the
boundary is no longer a byte sequence's **content**, it is **where** that byte sequence
ends. The course's question stays the same, but this time it is asked not at a single
value's boundary, but at a stream stretching out over time.

A real network connection is never opened in this lesson. Instead, a `bytes.Buffer` is used
— the standard library's byte buffer, implementing both the `io.Reader` and `io.Writer`
interfaces, exactly as a real network connection would. This lesson **models** the
connection with this buffer; the code being measured uses only these two interfaces, and the
same code would run unchanged over a real connection too, because both satisfy the same
contract. No address, port, or duration is printed anywhere, because none of them exist at
all in this model.

This continues the strategy followed by the course's HTTP Server and HTTP Client lessons.
Both of those lessons measured with a test helper, without opening a real port; here the
same idea is applied at a different layer. HTTP's own test helper was enough to model a
single request–response pair, because HTTP's boundary was already clear — a request starts,
a response ends, the connection either closes or waits ready for the next request. A
long-lived connection has no boundary like this; the server can write whenever it wants, the
client can read whenever it wants, and precisely because of this, what this lesson measures
is not "what a request carries" but "where in the stream a message ends."

This difference also determines the choice of measurement tool. The HTTP lessons built a
real client and server object and measured a single exchange between them; this lesson uses
a single shared byte buffer as the common ground for both the writing and the reading side,
because what is meant to be measured is not the behavior of two separate sides, but the
structure of the **stream itself** — how many messages arrive one after another, and how
these messages are distinguished within the stream.

## The Connection Is Modeled: Same Interface, Different Concrete Implementation

**ND33.** `bytes.Buffer` implements both `io.Reader` and `io.Writer` — exactly the two
interfaces Go's standard library defines for a network connection. The System and Data
Access topic's first lesson will measure the fact that these two interfaces are shared
between a file, a buffer, and a string; this lesson sets up the connection side of that same
fact in advance, without ever opening the network. The procedures below write a message
first as a four-byte big-endian length prefix, then as the message's own bytes; the reading
side first reads these four bytes to learn the length, then asks for exactly that many
bytes.

This model has a limit, and it needs to be stated explicitly: on a real network connection,
when a `Write` call reaches the other side, and into how many pieces a single call gets
split as it proceeds, can depend on factors like latency and ordering. This lesson models
none of that — a write over `bytes.Buffer` completes instantly, and the next read always
sees the most current state. What is measured is not latency or ordering, only **framing
itself**: how the messages embedded inside a stream get parsed by the side reading the
stream. This narrow scope is deliberate; an extended model is built on top of the same
framing rule, it does not change it.

```go
// framing.go — measuring how framing carries the message boundary in a long-lived connection
package main

import (
	"bytes"
	"encoding/binary"
	"fmt"
	"io"
)

func writeFrame(conn *bytes.Buffer, message string) {
	var length [4]byte
	binary.BigEndian.PutUint32(length[:], uint32(len(message)))
	conn.Write(length[:])
	conn.WriteString(message)
}

func readFrame(conn *bytes.Buffer) (string, bool) {
	var length [4]byte
	if _, err := io.ReadFull(conn, length[:]); err != nil {
		return "", false
	}
	size := binary.BigEndian.Uint32(length[:])
	body := make([]byte, size)
	if _, err := io.ReadFull(conn, body); err != nil {
		return "", false
	}
	return string(body), true
}

func main() {
	pushed := []string{"welcome", "data", "closing"}

	var unframedConn bytes.Buffer
	for _, m := range pushed {
		unframedConn.WriteString(m)
	}
	fmt.Println("-- in an unframed connection, three pushes merge into a single byte sequence --")
	correctlyParsed := 0
	for {
		chunk := make([]byte, 6)
		n, err := unframedConn.Read(chunk)
		if n == 0 {
			break
		}
		chunkText := string(chunk[:n])
		fmt.Printf("read chunk: %q\n", chunkText)
		for _, m := range pushed {
			if chunkText == m {
				correctlyParsed++
			}
		}
		if err != nil {
			break
		}
	}
	fmt.Printf("correctly parsed message: %d/%d\n", correctlyParsed, len(pushed))

	var framedConn bytes.Buffer
	for _, m := range pushed {
		writeFrame(&framedConn, m)
	}
	fmt.Println()
	fmt.Println("-- in a framed connection, all three pushes parse back with the boundary the server wrote --")
	correctlyParsedFramed := 0
	for i := 0; i < len(pushed); i++ {
		m, ok := readFrame(&framedConn)
		if !ok {
			break
		}
		fmt.Printf("read message: %q\n", m)
		if m == pushed[i] {
			correctlyParsedFramed++
		}
	}
	fmt.Printf("correctly parsed message: %d/%d\n", correctlyParsedFramed, len(pushed))

	var numericPush bytes.Buffer
	writeFrame(&numericPush, "42")
	body, _ := readFrame(&numericPush)
	fmt.Println()
	fmt.Println("-- limiting measure: the frame gives the correct body, but not the body's type --")
	fmt.Printf("body: %q, type: %T\n", body, body)
}
```

```
-- in an unframed connection, three pushes merge into a single byte sequence --
read chunk: "welcom"
read chunk: "edatac"
read chunk: "losing"
correctly parsed message: 0/3

-- in a framed connection, all three pushes parse back with the boundary the server wrote --
read message: "welcome"
read message: "data"
read message: "closing"
correctly parsed message: 3/3

-- limiting measure: the frame gives the correct body, but not the body's type --
body: "42", type: string
```

**ND34.** The three messages in the `pushed` slice are written one after another to
`unframedConn` and `framedConn` — with no read, no "request" between them. This differs from
the request–response pattern established in the HTTP server and client lessons: there, every
response followed a request; here, the server side **pushes** all three times on its own
initiative. This is exactly a long-lived connection's push property — being able to write
without a trigger arriving from the other side.

How a server handles a request was already built as a concept across eight courses of the
Backend Development curriculum, and is not repeated here; this lesson's share is only
whether the two interfaces in Go's standard library (`io.Reader`, `io.Writer`) can carry a
push like this. The answer here is yes: the `bytes.Buffer.Write` call depends on no request
at all, it only appends to the end of the buffer the moment it is called — this shows how
simple, at the standard library level, it is for a server to send data to a client without
notice beforehand. The complexity is elsewhere: the other side needs to know where these
pushes end, and the thing that carries this information is not `Write` itself, it is the
framing measured shortly.

## Unframed Stream: Three Pushes Merge into a Single Byte Sequence

**ND35.** The three messages written to `unframedConn` (`"welcome"`, `"data"`, `"closing"`)
are appended directly one after another with no separator between them — the result is a
single eighteen-byte sequence. The reading side **has no way of knowing** where one message
ends and the next begins in this sequence; all it knows is to read fixed-size (six-byte)
chunks. None of these chunks matches any of the three original messages — `"welcom"`,
`"edatac"`, `"losing"` — because the six-byte boundary is a completely arbitrary split with
no relationship at all to the message boundaries. The measurement gives this as `0/3`: a
fixed-size read carries **no** guarantee at all of preserving message boundaries.

The choice of six bytes is not a detail of the measurement, it was chosen precisely to show
this arbitrariness: none of the three messages' lengths (7, 4, 7) divide evenly by six, so
no read stops exactly at a message's boundary. If the read size were four, or eight, the
result would still be `0/3`, because the problem is not the read size's magnitude, it is
that the read size is a number chosen from outside, **independent** of the message lengths.
This points to a fundamental property of a long-lived connection: a `Read` call on an
`io.Reader` returns as many bytes as were requested (or however many are available at that
moment) — it carries no information at all about which bytes are the start of a message and
which are the middle. If this information is not in the stream itself, the reading side has
to **invent** it; a fixed-size read is exactly this kind of invention, and this measurement
shows why it does not work.

## Framed Stream: Parses Back Correctly with Boundary Information

**ND36.** Before every message written to `framedConn` stands a four-byte prefix carrying
that message's length. The reading side no longer guesses: it first reads four bytes to
learn the length, then does a read of exactly that length — neither short nor long. All
three messages come back correctly, byte for byte, `3/3`. The difference is not the read
size's magnitude (in the unframed trial six bytes was longer than some messages and shorter
than others); the difference is that the read size **comes from the message itself every
time**, it is not imposed as a fixed number from outside.

`readFrame` uses `io.ReadFull` in both of its reads, not a plain `Read` call. This choice is
not arbitrary: the `io.Reader` interface's contract permits returning **less** than the
requested byte count, even if more data is available — `bytes.Buffer` always gave exactly
what was requested in this measurement, but that is not part of the interface's general
contract, it is the behavior of this particular concrete type. `io.ReadFull` repeats the
read until the requested byte count is exactly met (either the stream ends or an error
occurs); this is a guarantee against the possibility of the four-byte length prefix or the
variable-length body arriving piecemeal, incomplete, in a single `Read` call. Just as much
as the framing rule itself, the code reading it paying attention to this detail is also part
of parsing correctly.

A length prefix is not the only possible form of framing — a special byte marking the end
of a message (a delimiter) could do the same job. But delimiter-based framing carries the
risk of the same byte as the delimiter itself appearing inside a message's content: the
`"data"` message ends with the letter `"i"`; had `"i"` been chosen as the delimiter, this
message would have looked like it was cut short within itself. A length prefix carries no
such risk, because it never looks at the message's content at all — it first says "how many
bytes are coming," then reads that many bytes, whatever is inside them, as a single unit.
This lesson does not compare these two strategies, it only measures the second one, but this
is the reasoning behind the choice.

The course's full distribution of thirteen boundary crossings (1 preserved, 5 rebuilt, 7
lost; the number built in Lesson 03) solves a separate sub-problem that lies outside these
thirteen crossings: that distribution counted what happens to a single value's type as it
crosses a boundary, while this lesson determines **which part** of a stream is a value in
the first place. Without the answer to this question, neither the previous lessons' JSON
decoder nor the schema generator could work at all — both need, from the start, the
information "a message ended here," and the only thing that carries this information is
framing.

The Computer Networks curriculum's Application Layer Protocols course and the Frontend
Development curriculum's Application Architecture: Routing, State, and Data course already
built long-lived connections and real-time data streaming as concepts; that is not repeated
here, what is measured is only how Go's standard library carries a byte stream at the
interface level.

**ND37.** The final measurement is a warning: the body written with `writeFrame` and read
back with `readFrame` is the string `"42"` — framing works flawlessly here too, the body is
correct byte for byte. But `body`'s Go type is `string`, not `int`. Framing says **where** a
message ends, it does not say whether the bytes inside it represent an integer, a string, or
a struct. Answering this question requires a schema of the kind the previous lesson
measured — framing and schema are two separate layers that complete each other in a
long-lived connection: one draws the boundary, the other names what is inside it.

The importance of this distinction shows up in the fact that a body like `"42"` can be read
**both ways**. If the other side tries to convert this body to an integer (with
`strconv.Atoi`), it succeeds and gets the number `42`; if it keeps it as a string without
converting at all, it also gets a valid value. Framing leaves both interpretations **equally
possible**, because the body's bytes alone do not say which interpretation is correct. The
contract-based schema seen in the previous lesson was exactly what closed this ambiguity: by
fixing a field name and its type in advance, it bound both sides to the same decision about
how the body would be read. In a long-lived connection these two layers are usually set up
together — framing carries the boundary of every message, the schema carries the meaning of
the fields inside every message — but this lesson deliberately separated them, because the
two can succeed or fail independently of each other: a stream can be framed correctly and
still be interpreted incorrectly.

## Summary

- A long-lived connection was modeled with a `bytes.Buffer` implementing `io.Reader` and
  `io.Writer`, without opening a real network connection; because the measured code uses
  only these two interfaces, the same logic would run unchanged over a real connection too.
- The server side pushed three messages without waiting for a request between them — a
  long-lived connection's push property differs from the previous lessons' request–response
  pattern in exactly this way.
- In an unframed stream, three messages merged into a single byte sequence and fixed-size
  reads could not correctly separate any of them (`0/3`); in a stream framed with a length
  prefix, all three came back correct byte for byte (`3/3`), because the read size came
  from the message itself every time.
- Using `io.ReadFull` was a guarantee for cases where the `io.Reader` contract permits
  returning less than the requested byte count — the framing rule working correctly also
  depended on the code reading it observing this detail.
- The limiting measure: framing gave the correct body but did not give the body's type —
  the string `"42"` parsed correctly, but whether this is an integer or a string is a
  separate schema question that lies outside framing.
- Framing and schema are two separate layers completing each other in a long-lived
  connection: one carries the message's boundary, the other carries the meaning of what is
  inside that boundary.

## Next Step

This lesson measured the single piece of information that determines which part of a
stream is a message — framing — and showed that this information carries no **type** of a
message at all: what is inside the same frame still requires a schema. This course's topic
(Networking and Data Formats) measured, across its six lessons, values crossing the boundary
always over the network — a request, a response, a template's output, a schema shared by two
processes, a long-lived connection. The next topic asks the same question at the program's
own boundaries: a file, a buffer, and an embedded resource, too, just like this lesson's
connection, speak at the byte level — and there too, on which side of the boundary type
information stands, and on which side it does not, will be measured again.
