STREAM¶
Guide Home | Previous: Actor | Next: Session and Actor
View in another language — C++ · C#/.NET · Java · Kotlin · Node/TypeScript
What you get from this chapter
You can let a program outside the mesh attach over one connection, send requests and receive answers. The code comes from the tutorial README in the examples repository; follow its Download, Build, and Run sections to reproduce the results below.
A client that reaches a STREAM node uses its language's stream connector. Client stream connector covers installation for Unity, Unreal, browser, Node, .NET, Java, and C++ clients, and the stream connector guide for that language covers connection and packet handling.
Every call so far ran between nodes inside the mesh. A game client or an app is outside it and references no Framework at all. STREAM is where such a program attaches, and while the one connection is open both sides send to each other. This chapter goes as far as accepting a connection and answering one packet; binding that connection to an Actor is covered by Session and Actor.
1. The Role of STREAM¶
STREAM is the boundary where a client outside the mesh exchanges packets with it over one connection. Channels, Spots and Actors are all paths called from inside the mesh. The caller has to be a member of the mesh and to reference the Framework. A client running on a player's device meets neither condition.
STREAM narrows that boundary to a single connection. The client needs one address and the packet names, and it never learns how many nodes the mesh has.
One session object stands for one connection. Packets arriving on that connection are handled one at a time in that session, so a session's fields are the place to keep the state of that connection.
2. Deciding the Packets¶
These are no different from the contracts of a mesh call. They travel to a client outside the mesh, though, so the names have to be the ones that client uses.
// Exchanged over the external TCP connection, not between mesh nodes. The
// stream codec wants 64-bit integers as decimal strings, so the timestamp is
// carried as text rather than as a Long.
data class Ping(val sentAtUnixMs: String)
data class Pong(val sentAtUnixMs: String)
3. The Receiving Side — the Node That Accepts Connections¶
The receiving process
The code in this section goes into the process that accepts the connection.
3.1 Writing the Session¶
A session stands for one connection. Connect, disconnect, error and packet arrival all come as callbacks, and the callbacks for one connection run in order.
// One connected game client. Callbacks for the same connection run in order.
// The dispatcher is handed in by the Framework and holds the handlers the
// stream node registered.
class GameSession(
private val context: ZLinkSessionContext,
private val handlers: ZLinkSessionPacketDispatcher<ZLinkSessionContext>,
) : ZLinkSuspendingSession() {
override fun context(): ZLinkSessionContext = context
override suspend fun onConnectedSuspending() {
println("client connected: ${context.sessionId()}")
}
override suspend fun onDisconnectedSuspending() {
println("client disconnected: ${context.sessionId()}")
}
override suspend fun onErrorSuspending(error: ZLinkStreamError) {
println("stream error on ${context.sessionId()}: $error")
}
// Every inbound packet arrives here first. Registering a handler is not
// enough on its own; this method is what routes the packet to it.
override suspend fun onDispatchSuspending(
dispatch: ZLinkSessionDispatchContext,
payload: ZLinkMessage,
) {
if (handlers.tryHandle(context, dispatch, payload).await()) return
// A slotted packet uses its dispatch Actor. An unslotted packet can use
// the connection only while exactly one Actor is bound.
val player =
dispatch.actor()
?: run {
val bound = context.actors().bound()
when (bound.size) {
1 -> bound[0]
0 -> error("Authenticate an Actor before sending player packets.")
else -> error("Select an Actor handle when more than one Actor is bound.")
}
}
player.kotlin().relay(dispatch, payload).await()
}
}
The packet-arrival callback is the gate. Registering handlers is not enough on its own; nothing is handled unless this callback routes the packet to them.
Where handlers are registered differs by language
.NET and Node register from the session; Java and Kotlin register alongside the stream node. C++ has no registration surface at all — every packet arrives at one callback, and telling them apart by name is the application's own code.
3.2 Writing a Packet Handler¶
Instead of returning a value, a handler writes the answer itself. A packet that arrived as a request is answered with reply; pushing to a client that is waiting for none uses send.
// The first type argument is the session context, not the session class. A
// suspending session handler names its packet itself, which the Java surface
// takes from messageType() alone.
class PingHandler : ZLinkSuspendingTypedSessionPacketHandler<ZLinkSessionContext, Ping> {
override fun packetName(): String = "Ping"
override fun messageType(): Class<Ping> = Ping::class.java
override suspend fun handle(
context: ZLinkSessionContext,
dispatch: ZLinkSessionDispatchContext,
message: Ping,
) {
// reply answers a request. To push to a client that is not waiting for
// one, use client().send instead.
context.client().kotlin().reply(Pong(message.sentAtUnixMs)).await()
}
}
3.3 Registration¶
A stream node opens a port and takes one session type. It is a registration of its own, separate from a mesh node.
// The port game clients connect to. One session type per stream node,
// and actor dispatch must be on for a session to relay to its player.
// Session handlers are registered here rather than inside the session,
// and addSessionPacketHandler takes a plain Class, so the suspending
// handlers go in by type.
options
.addStreamNode("client-stream")
.bind("tcp://127.0.0.1:7621")
.enableActorDispatch()
.registerSession<GameSession>()
.addSessionPacketHandler<PingHandler>()
.addSessionPacketHandler<AuthenticateHandler>()
4. The Attaching Side — a Client Outside the Mesh¶
The attaching process
The code in this section goes into the process that attaches from outside the mesh. It pairs with the section above.
This process references no Framework. It references one separately published connector and nothing else.
// A game client outside the mesh. It references the connector only, never the
// Framework, and speaks to the port the stream node opened.
val connector =
ZLinkStreamConnectorFactory.create(
ZLinkStreamConnectorOptions(
URI.create("tcp://127.0.0.1:7621"),
ZLinkStreamDispatchMode.IMMEDIATE,
Duration.ofSeconds(5),
1,
)
)
.kotlin()
connector.connect().await()
println("connected: ${connector.isConnected}")
// A request waits for its reply. Use send for one-way traffic; the server
// then answers with client().send rather than reply.
val sentAt = System.currentTimeMillis()
val pong =
connector.request<Pong>(Ping(sentAt.toString())).timeout(Duration.ofSeconds(5)).await()
println("round trip: ${System.currentTimeMillis() - pong.sentAtUnixMs.toLong()}ms")
The Node connector attaches over a WebSocket
The connector in the other four languages speaks TCP and names a tcp:// endpoint. The Node
connector is published for the browser and speaks WebSocket, so both the stream node and the
client name a ws:// endpoint.
5. What You See When You Run It¶
With the tutorial Server running as the README's Run section specifies, start StreamClient with the command below: the request it sends and response it receives appear on the StreamClient process's stdout, while connection and handler records appear on the Server process's stdout or in server.log.
connected turning true means the connection was established, and the round trip is measured by
the server echoing back the instant the client sent. Those two lines are the outside and the
inside joined.
6. The Difference from a Mesh Call — the Connection Is the Target¶
In a mesh call the caller is a member of the mesh and the target is a name or an id. In STREAM the connection itself is the target. Nothing chooses among clients, and the server can send first.
When the connection drops that session ends. To carry state beyond one connection, bind the connection to an Actor — covered by Session and Actor.
7. Related Documents¶
- Binding a connection to an entity — Session and Actor
- State objects called by id — Spot · Actor
- The whole session lifecycle and its options — STREAM
- A running version of this chapter's code — the tutorial README in the examples repository