Networking APIs

Cross-platform TCP, UDP, TLS, DTLS, ENet, and WebSocket sockets plus DNS, exposed as a layered set of zym-flavored namespaces.

Overview

The networking natives form a layered stack. Each namespace covers one transport:

Conventions

The networking natives share a small set of cross-cutting rules:

IP

DNS lookups and local interface enumeration. The IP global is a namespace of static functions; there is no instance type.

IP.resolve(host)

Performs a synchronous DNS lookup. Returns the first address (IPv4 or IPv6) the system resolver yields, or null on NXDOMAIN or lookup error.

IP.resolveAll(host)

Returns all addresses for host as a list of strings. Returns an empty list on NXDOMAIN or lookup error. Order is whatever the resolver returned.

IP.localAddresses()

Returns the textual IPs assigned to local interfaces as a list of strings. Includes loopback (127.0.0.1 / ::1) and any virtual interface (docker0, etc.).

Address format

resolution and interfaces
print(IP.resolve("example.com"))
//  "93.184.216.34"  (or null on a network without DNS)

for addr in IP.resolveAll("dual-stack.example") {
    print(addr)
}
//  "203.0.113.10"
//  "2001:db8::10"

for ip in IP.localAddresses() {
    print(ip)
}
//  "127.0.0.1"
//  "::1"
//  "192.168.1.42"
//  "172.17.0.1"
//  ...
Blocking: resolution is synchronous. Long DNS lookups block the calling script until the OS resolver responds or hits its own timeout, typically 5–30 s depending on /etc/resolv.conf. A server-style script that wants to keep handling traffic during DNS should resolve in advance or lean on the OS resolver's caching.

Literal addresses ("127.0.0.1", "::1") round-trip through IP.resolve cleanly. The engine treats them as already-resolved and returns them unchanged. localAddresses() enumerates all interfaces, including link-local (fe80::) and virtual ones; filter the result list yourself if you only want, say, the first non-loopback IPv4 address.

TCP

Reliable byte streams. Two globals (TCP.connect for clients, TCP.listen for servers); both return instance handles whose methods are documented below.

The connection model is non-blocking under the hood, with timeoutMs-driven helpers layered on top:

TCP statics

TCP.connect(host, port, timeoutMs?)

Resolves host via IP and opens a TCP connection. Without timeoutMs, blocks forever until connected, refused, or errored. With timeoutMs, the wait is bounded; timeoutMs == 0 returns immediately with the partially-connected sock (status() == "connecting") and the caller drives poll() until ready.

Returns: a sock handle, or null on failure.

TCP.listen(host, port)

Binds and listens on host:port. host == "" or "*" binds all interfaces. port == 0 lets the OS assign one (read it back via server.localPort()). Returns null on EADDRINUSE or permission errors.

Returns: a server handle, or null on failure.

Sock instance methods

sock.status()

Returns one of "none", "connecting", "connected", "error". Does not advance state.

sock.poll()

Drives one round of state advancement (handshake progress, EOF detection) and returns the new status. Useful when you want to react to disconnects without reading.

sock.available()

Returns the number of bytes immediately readable. Polls before reporting.

sock.read(n, timeoutMs?)

Reads exactly n bytes. Without timeoutMs, blocks forever. With a deadline, returns "timeout" on partial arrival before the deadline. The bytes that did arrive are lost, so use readSome for partial-friendly reads. Returns "eof" on peer close.

Returns: a Buffer, or a status string.

sock.readSome(n)

Non-blocking read. Returns whatever is immediately available, up to n bytes; the result may be empty (length 0). Returns null on error or when not connected.

sock.readLine(timeoutMs?)

Reads up to and including the next \n, returning the line without the terminator. Strips both \r\n and \n. Returns "eof" if the connection closes before a newline arrives. Any bytes that arrive after the newline in the same available() window are discarded.

Returns: a string, or a status string.

sock.readAll(timeoutMs?)

Reads until peer EOF (or the deadline). Returns the accumulated payload as a Buffer; "timeout" if the deadline hits before EOF; "error" on connection error.

sock.write(buf, timeoutMs?)

Sends the entire payload, or fails. Returns "ok", "timeout", "closed", or "error". Empty buffers always return "ok".

sock.writeSome(buf)

Non-blocking write. Returns a map { sent: number, status: "ok" | "closed" | "error" }. Use this for back-pressure-aware sending.

sock.setNoDelay(b)

Toggles TCP_NODELAY (Nagle's algorithm). Returns null.

sock.localAddress()

Returns the local end of the connection as { host, port }, or null. host is reported as the wildcard form (engine-side limitation); port is the actual local port.

sock.peerAddress()

Returns the remote end of the connection as { host, port }, or null.

sock.close()

Disconnects. Idempotent. After close, status() returns "none" and read/write return "closed".

Server instance methods

srv.accept(timeoutMs?)

Blocks until a client connects, then takes the connection. Without timeoutMs, blocks forever and returns null only if the server has been stopped. With a deadline, returns null on timeout or on shutdown.

Returns: a sock handle, or null.

srv.localPort()

Returns the actually-bound port. Useful when listen was passed 0.

srv.close()

Stops listening. Already-accepted client sockets are unaffected. Idempotent.

client and server
// Client: connect, exchange one line, close.
var c = TCP.connect("example.com", 80, 5000)
c.write(Buffer.fromString("GET / HTTP/1.0\r\nHost: example.com\r\n\r\n"), 5000)
var status_line = c.readLine(5000)
print(status_line)
c.close()

// Server: accept one connection, echo a line.
var srv = TCP.listen("127.0.0.1", 0)
print("listening on", srv.localPort())
var s = srv.accept()
var line = s.readLine(5000)
s.write(Buffer.fromString("got: " + line + "\n"), 5000)
s.close()
srv.close()

UDP

Unreliable datagrams. One static (UDP.bind) returns an instance with send / recv plus the usual lifecycle; UDP.listen adds per-source server-side demultiplexing.

UDP statics

UDP.bind(host, port)

Binds a UDP socket. host == "" or "*" binds all interfaces; port == 0 lets the OS assign one. Returns null on bind failure. The returned handle is non-blocking.

UDP.listen(host, port)

Binds a UDP socket and demultiplexes incoming datagrams per source (host, port). Each new source becomes a pending peer that you accept(...) to get a fresh udp handle bound to that one source. Used for stateful per-client UDP servers and as the entry point for DTLS.accept. Returns null on bind failure.

Returns: a udp-server handle, or null.

UDP instance methods

udp.send(buf, host, port)

Sends buf as one datagram to host:port. host may be a literal IP or a hostname (resolved via IP). Returns "ok", "busy" (kernel send buffer full), or "error".

udp.recv(timeoutMs?)

Without timeoutMs, blocks forever until a datagram arrives. Returns { data: Buffer, host, port } with the source address, or "error" if the socket is unbound. With a deadline, returns "timeout" if no datagram arrives in time.

udp.localPort()

Returns the actually-bound local port.

udp.setBroadcast(b)

Enables or disables SO_BROADCAST. Required to send to 255.255.255.255 or directed-broadcast addresses. Returns null.

udp.close()

Closes the socket. Idempotent.

UDP server instance methods

The handle returned by UDP.listen exposes a small surface, focused on accepting per-source UDP peers. The accepted peer is a regular udp handle with the same instance methods as those returned by UDP.bind.

srv.accept(timeoutMs?)

Blocks until a new source sends a datagram, then returns a fresh udp peer bound to that source. Subsequent datagrams from the same source flow into that peer's queue. With a deadline: 0 returns immediately (null if no pending source); -1 is the same as no argument.

Returns: a udp handle, or null.

srv.localPort()

Returns the actually-bound port, or 0 if not listening.

srv.close()

Stops listening. Already-accepted peers remain usable until they themselves are closed.

datagrams in and out
// Send-and-forget
var u = UDP.bind("0.0.0.0", 0)
u.send(Buffer.fromString("ping"), "192.168.1.1", 9000)
u.close()

// Receive with source address
var s = UDP.bind("0.0.0.0", 9000)
var r = s.recv(5000)
if r != "timeout" {
    print("from", r["host"], r["port"], "got", r["data"].size(), "bytes")
}
s.close()

Sockets

Multi-handle readiness primitive, the closest zym has to a select(2) or poll(2) for scripts.

Sockets.waitAny(handles, mode, timeoutMs)

Waits for any of handles to become ready in the given mode. Returns once at least one handle is ready, or when timeoutMs expires.

Returns: a map { ready, timedOut }, where ready is the subset of handles that became ready and timedOut is true when nothing fired before the deadline.

Mode semantics

Implementation note: waitAny is a poll-with-quantum loop over the existing readiness primitives, not a single multiplexed syscall. For a handful of handles (the typical CLI workload) this is invisible; for hundreds of fds, expect a quantum of latency (~20 ms) on the slowest spin.
accept or service, whichever comes first
var clients = []
var srv = TCP.listen("127.0.0.1", 0)
while true {
    var watch = [srv]
    for c in clients { append(watch, c) }
    var w = Sockets.waitAny(watch, "read", -1)
    if w["timedOut"] { continue }
    for h in w["ready"] {
        if h == srv {
            append(clients, srv.accept(0))
        } else {
            var line = h.readLine(0)
            // ... handle line, including "eof"/"closed"/"error" ...
        }
    }
}

TLS

Encrypted TCP. Client connections (TLS.connect) verify the peer's certificate against the system trust store by default; server-side acceptance (TLS.accept) wraps an already-accepted TCP socket with the caller's CryptoKey and X509Certificate from Crypto. The instance methods on a TLS sock are identical to TCP (status / poll / read / readSome / readLine / readAll / write / writeSome / setNoDelay / localAddress / peerAddress / close) and obey the same status vocabulary, so script code that talks TCP can be retargeted at TLS by swapping the factory call.

TLS statics

TLS.connect(host, port, timeoutMs?, opts?)

Opens a TLS client connection. Without timeoutMs, blocks until the handshake finishes. Verifies the peer certificate against the system CA bundle (loaded at startup); returns null on DNS failure, TCP refusal, handshake failure, or hostname mismatch. With timeoutMs == 0, kicks off the connection and returns the sock immediately while the handshake is still in flight (drive it with sock.poll()); the underlying TCP is given a brief grace (~250 ms) to settle before the handshake call. Pass null for opts to get the defaults.

Returns: a sock handle, or null.

TLS.accept(tcp, opts, timeoutMs?)

Wraps an already-accepted TCP socket as the server side of a TLS handshake. tcp must be a sock returned by srv.accept(...) (i.e. its __tcp__ tag is recognized). Without timeoutMs, blocks until the handshake completes. With timeoutMs == 0, returns the TLS sock immediately in "connecting" state (drive both sides with poll()).

Returns: a sock handle, or null.

Client opts

KeyDefaultNotes
verifytrueWhen true, the client verifies the server's certificate against trustedRoots (or the system trust store if trustedRoots is omitted) and the server's hostname against the SAN/CN. When false, the connection skips chain & hostname validation entirely (an "unsafe client"). Useful for self-signed peers and tests; do not use against untrusted networks.
trustedRootsnullOptional list of X509Certificate instances to use as the trust anchor for this connection. When provided, replaces the system trust store; the listed certs are concatenated and parsed as a single chain.
commonNamenullOverride the hostname used for SAN/CN matching. Defaults to the host argument of TLS.connect. Has no effect when verify is false.

Server opts (for TLS.accept)

KeyRequiredNotes
keyyesA CryptoKey containing the server's private key (e.g. from Crypto.generateRsa(2048) or CryptoKey().load(path)).
certyesAn X509Certificate containing the server's public certificate (e.g. from Crypto.generateSelfSignedCertificate(...) or X509Certificate().load(path)).

TLS instance methods

Identical surface to TCP (see the sock instance methods above); the only behavioural difference is the meaning of "connecting". On a TLS sock, "connecting" covers both the underlying TCP handshake and the TLS handshake, and a sock stays in "connecting" until the TLS handshake completes (or fails into "error").

The four-state TCP vocabulary collapses onto TLS as:

TLS stateStatus string
TLS DISCONNECTED"closed"
TLS HANDSHAKING"connecting"
TLS CONNECTED"connected"
TLS ERROR / ERROR_HOSTNAME_MISMATCH"error"

localAddress / peerAddress / setNoDelay delegate to the underlying TCP transport. TLS itself doesn't introduce its own concept of those.

https get
// HTTPS GET against a public host, full system-CA verification.
var s = TLS.connect("example.com", 443, 8000)
s.write(Buffer.fromString("GET / HTTP/1.0\r\nHost: example.com\r\n\r\n"))
var status_line = s.readLine(5000)
print(status_line)
s.close()
in-process round-trip with a self-signed certificate
// Server + client driven concurrently from the same script via
// non-waiting handshakes.
var c    = Crypto.create()
var key  = c.generateRsa(2048)
var cert = c.generateSelfSignedCertificate(key, "CN=localhost",
                                           "20240101000000",
                                           "20440101000000")

var srv      = TCP.listen("127.0.0.1", 0)
var port     = srv.localPort()
var cli      = TLS.connect("127.0.0.1", port, 0, { verify: false })
var rawSrv   = srv.accept(2000)
var srvTls   = TLS.accept(rawSrv, { key: key, cert: cert }, 0)

// Drive both handshakes to completion.
while cli.status() == "connecting" || srvTls.status() == "connecting" {
    cli.poll()
    srvTls.poll()
    System.sleep(20)
}

cli.write(Buffer.fromString("hello\n"))
print(srvTls.readLine(2000))   // "hello"
srvTls.write(Buffer.fromString("hi back\n"))
print(cli.readLine(2000))      // "hi back"

cli.close()
srvTls.close()
srv.close()

Sockets.waitAny and TLS

TLS sockets are valid handles for Sockets.waitAny, and the readiness primitives apply the same way. A TLS sock counts as readable when its available() is positive or the connection has terminated ("closed" / "error"), and as writable when its status is "connected". Mixing TCP, TLS, UDP, and TCP-server handles in one waitAny call is supported.

Notes

DTLS

Datagram TLS (DTLS) is TLS over UDP. It provides confidentiality and integrity over a UDP association, with the same status and lifecycle shape as TLS, but a datagram-shaped data API (send / recv) instead of TLS's stream-shaped one (read / write). Each dtls.send(buf) call corresponds to exactly one DTLS record on the wire.

Lossy by design: DTLS keeps UDP's lossy semantics for application data. Packets can still be reordered, duplicated, or lost. DTLS does not retransmit your send(...) payloads; it only retransmits its own handshake messages. Use it where you need encryption + authentication on top of UDP, not where you need reliability.

DTLS statics

DTLS.connect(host, port, timeoutMs?, opts?)

Binds an ephemeral local UDP and runs the client-side DTLS handshake to host:port with default options (system-CA verify). Without timeoutMs, blocks forever; returns null on connect or handshake failure. With a deadline: 0 returns immediately in "connecting" so the caller can drive the handshake via dtls.poll(); -1 blocks. opts supplies explicit client options (see below).

Returns: a dtls handle, or null.

DTLS.connectFrom(udp, host, port, timeoutMs?, opts?)

Like DTLS.connect, but the caller supplies the underlying udp (e.g. for source-port pinning). The udp must come from UDP.bind. The destination is set on the udp if it isn't already connected; further plain-udp use is undefined while DTLS is using it.

DTLS.accept(udpServer, opts, timeoutMs?)

Server-side handshake. udpServer is a handle from UDP.listen(host, port). This call drives the entire DTLS-server flow internally, including the cookie exchange (HelloVerifyRequest), and returns a fully-handshaken DTLS peer when one is ready, or null on timeout. Calling it repeatedly on the same udpServer accepts more peers; the server-side DTLSServer state (cookies, in-flight handshakes) is persisted on the udpServer handle for as long as it lives.

Returns: a dtls handle, or null.

Client opts

Same shape as TLS.connect's client options:

KeyDefaultNotes
verifytrueWhen false, disables certificate verification entirely. Use only for self-signed test scenarios.
trustedRootsnullAn X509Certificate, a list of them, or null for the system trust store.
commonName""Override the SNI / CN check.

Server opts (for DTLS.accept)

KeyNotes
keyCryptoKey from Crypto.generateRsa or a loaded PEM.
certX509Certificate matching the key.

DTLS instance methods

DTLS instances expose a UDP-shaped surface (send / recv), not a stream-shaped one. The status vocabulary matches TLS.

dtls.status()

Returns "connecting" (handshake in progress), "connected", "closed", or "error". Hostname-mismatch and other handshake failures collapse onto "error".

dtls.poll()

Advances the DTLS state machine once and returns the new status. Required during handshake on the client side when DTLS.connect(... 0) was used; safe to call any time.

dtls.available()

Returns the number of pending DTLS records (decrypted datagrams ready to read).

dtls.send(buf)

Sends buf as one DTLS record. Returns "ok", "busy" if the handshake hasn't completed yet (caller should poll() and retry), "closed" if the peer is gone, or "error".

dtls.recv(timeoutMs?)

Without timeoutMs, blocks forever until a record arrives, then returns the decrypted payload as a Buffer. There is no source address, because DTLS is point-to-point post-handshake. With a deadline, returns "timeout" on deadline, and "closed" / "error" otherwise.

dtls.peerAddress()

Returns { host, port } of the remote peer: the host string supplied at connect time on the client; the source address of the first ClientHello on the server.

dtls.close()

Sends a close-notify alert and tears down the underlying UDP. Idempotent.

client with system-CA trust
var c = DTLS.connect("dtls.example.com", 5684, 8000)
if (c == null) {
    print("connect / handshake failed")
    return
}
c.send(Buffer.fromString("hello"))
var reply = c.recv(2000)
if (typeof(reply) == "map") {
    print("got", reply.size(), "bytes back")
}
c.close()
in-process self-signed round-trip
// Mint a key + self-signed certificate via the Crypto native.
var crypto = Crypto.create()
var key  = crypto.generateRsa(2048)
var cert = crypto.generateSelfSignedCertificate(
    key,
    "CN=localhost,O=zym,C=US",
    "20230101000000",
    "20330101000000")

// Server: bind a UDP listener for per-source demux.
var udps = UDP.listen("127.0.0.1", 0)
var port = udps.localPort()

// Client: kick off connect (non-waiting), then drive both sides in a
// shared loop until the handshake completes.
var dc = DTLS.connect("127.0.0.1", port, 0, { verify: false })
var sd = null
while (sd == null) {
    dc.poll()
    sd = DTLS.accept(udps, { key: key, cert: cert }, 0)
    System.sleep(5)
}
while (dc.status() != "connected") {
    dc.poll()
    sd.poll()
    System.sleep(5)
}

// Now we have an encrypted datagram channel.
dc.send(Buffer.fromString("hello dtls"))
sd.poll()
print(sd.recv(1000).toString())     // -> "hello dtls"

dc.close()
sd.close()
udps.close()

Sockets.waitAny and DTLS / UDP server

DTLS handles and UDP-server handles are valid Sockets.waitAny arguments. A DTLS sock counts as readable when a record is decrypted-and-ready or the connection terminated, and as writable when its status is "connected". A UDP.listen server counts as readable when at least one new source has arrived. Mixing TCP, TLS, UDP, DTLS, TCP-server and UDP-server handles in one waitAny call is supported.

Notes

ENet

ENet sits on top of UDP and adds reliability (per-packet, optional), packet ordering, multiplexed channels, and connection liveness, all without giving up the datagram model. It is the right tool whenever you want game-style messaging from a script: multi-channel custom protocols, peer-to-peer relay, distributed CLI sync, or anything where TCP's single-stream byte semantics would force you to invent your own framing on top.

ENet is its own wire protocol. ENet endpoints can only talk to other ENet endpoints. It is not a way to add reliability on top of arbitrary UDP services. The bytes on the wire are an ENet-specific framing that other UDP listeners cannot decode. Pair zym's ENet against another zym ENet, or any C/C++ application using the upstream enet library.

ENet statics

ENet.connect(host, port, channels?, opts?)

Opens a connection to an ENet host. Non-blocking: the peer comes back in "connecting" state and the caller must drive host.service(timeoutMs) until the first connect event arrives. If opts.tls is provided, the host is wrapped with a DTLS client (see ENet over DTLS).

Returns: a { host, peer } map, or null.

ENet.listen(host, port, maxPeers?, channels?, opts?)

Binds and starts a host that accepts incoming peers, delivered as connect events from service(). If opts.tls = { key, cert } is provided, every inbound peer must complete a DTLS handshake before it is delivered as a connect event.

Returns: an ENet host, or null.

Host instance methods

host.service(timeoutMs)

Pumps the host and returns the next event, or null if none arrived within timeoutMs (clamped to 0). The event map is { type, peer, data, channel }, where type is one of "connect", "disconnect", "receive", or "error". For "receive", data is a Buffer; otherwise it is the application-defined integer code.

host.flush()

Force-pushes outbound queues onto the wire without waiting for the next service().

host.localPort()

Returns the bound port. Useful with ENet.listen("...", 0) to discover the OS-assigned port.

host.broadcast(buf, channel, mode)

Sends buf to every connected peer on channel. mode is "reliable", "unreliable", or "unsequenced".

Returns: "ok", "error", or "closed".

host.refuseNewConnections(refuse)

When refuse is true, stops accepting new inbound connections; existing peers are unaffected. Useful for draining a server before shutdown, especially with DTLS, where the cookie exchange would otherwise still be served.

host.close()

Tears down the host. Idempotent.

Peer instance methods

peer.status()

Returns "connecting", "connected", "closed", or "error", following the shared status vocabulary described under Conventions.

peer.send(buf, channel, mode)

Sends buf to this peer. channel must lie in [0, channelsAtConnect); an out-of-range index raises a runtime error. mode is as in broadcast.

Returns: "ok", "closed", or "error".

peer.peerAddress()

Returns the remote address as { host, port }.

peer.ping()

Forces a ping packet immediately.

peer.pingMs()

Returns the most recent round-trip-time sample in milliseconds, or 0 until a measurement exists.

peer.disconnect(data?)

Graceful disconnect: queues a disconnect packet that flushes after pending sends. The optional integer data is delivered to the peer's disconnect event.

peer.disconnectNow(data?)

Immediate disconnect: drops everything pending and notifies the peer in one shot.

peer.close()

Local-only reset. Does not notify the remote. Use disconnect or disconnectNow for that.

Service event shapes

TypeOther fields
"connect"peer (handle), data (integer code), channel (always 0)
"disconnect"peer (handle), data (integer code), channel (always 0)
"receive"peer (handle), data (Buffer), channel (the channel the sender used)
"error"(no other fields)

ENet over DTLS

Both ENet.connect and ENet.listen accept an optional trailing opts map whose tls field opts the host into DTLS. When set, every datagram the host sends or receives is wrapped in a DTLS record. The channel, ordering, reliability, and service() semantics are unchanged. Once the handshake completes, scripts use the same host and peer instance methods as for plain ENet.

Still ENet on the wire. ENet+DTLS talks only to other ENet+DTLS peers. The encrypted bytes are an ENet-specific framing wrapped in DTLS records, so generic DTLS endpoints will not understand them. Pair only against another ENet endpoint configured with the same DTLS options.

Client opts.tls accepts the same shape as TLS.connect:

FieldTypeDefaultNotes
verifybooltrueWhen false, the server certificate is accepted without verification (useful for self-signed peers).
trustedRootsX509Certificate or list thereofsystem trust storeRoots used to validate the server certificate. Ignored when verify is false.
commonNamestringthe host argumentHostname used for SNI and certificate verification. Override when connecting by IP literal.

Server opts.tls requires { key, cert }:

FieldTypeNotes
keyCryptoKeyPrivate key matching cert. Required.
certX509CertificateServer certificate. Required.

A missing key or cert raises a runtime error. Both come from Crypto.

Notes

WebSocket

An RFC 6455 WebSocket client and server exposed as a single namespace. WebSocket.connect opens a client to a ws:// or wss:// URL; WebSocket.accept wraps an already-accepted TCP (or TLS) socket as the server side of the handshake. The instance returned by either factory exposes a frame-shaped API that follows the same status-string vocabulary and Buffer byte-currency as the rest of these networking natives.

Frame model

WebSocket statics

WebSocket.connect(url, opts?)

Opens a client to url and returns the sock immediately in "connecting" state; drive the handshake with sock.poll(). Returns null on outright failure, such as a malformed URL or a build without WebSocket support. Pass null for opts to get the defaults.

Returns: a sock handle, or null.

WebSocket.accept(tcp, opts?)

Wraps an already-accepted TCP sock, a handle returned by TCP.listen(...).accept(...), as the server side of a WebSocket handshake. The TCP sock must remain live as long as the returned WebSocket handle is in use. Returns the sock in "connecting" state; drive the handshake with sock.poll(). For wss:// servers, wrap the accepted TCP with TLS.accept(...) first and pass the resulting TLS sock here.

Returns: a sock handle, or null.

Options

All keys are optional; unknown keys are ignored.

KeyDefaultNotes
tlsnullClient-side only. Controls TLS verification when url is wss://. Same shape as TLS.connect's options: verify, trustedRoots, commonName. When verify is false, the handshake skips chain and hostname validation entirely. Ignored on WebSocket.accept, where the underlying stream already carries the TLS layer.
protocols[]Subprotocol names to advertise. On a client these go out as Sec-WebSocket-Protocol; on a server they constrain the set the peer may pick. The negotiated protocol is available after the handshake via sock.selectedProtocol().
headers[]Additional handshake header lines as "Name: value" strings. Appended to the HTTP GET upgrade request on the client, or to the HTTP 101 upgrade response on the server.
inboundBufferSize65535Maximum size in bytes of the inbound packet buffer. Frames larger than this are rejected.
outboundBufferSize65535Maximum size in bytes of the outbound packet buffer.
maxQueuedPackets4096Maximum number of frames that may sit in the receive queue between polls before the peer is forced closed.
heartbeatInterval0Seconds between automatic ping frames. 0 disables heartbeats.

Address forms

Sock instance methods

sock.status()

Returns "connecting", "connected", "closing", or "closed". Does not advance state.

sock.poll()

Drives one round of state advancement (handshake progress, frame parsing, heartbeats) and returns the new status. Required during the handshake when the caller used the non-waiting form of connect or accept. Safe to call at any time.

sock.available()

Returns the number of decoded frames ready to read. Polls before reporting.

sock.send(buf)

Sends buf as one binary WebSocket frame. An empty buffer sends an empty binary frame.

Returns: "ok", "closed" when the peer is no longer open, or "error".

sock.sendText(text)

Sends text as one text WebSocket frame, UTF-8 on the wire. Same status vocabulary as send.

sock.recv(timeoutMs?)

Returns the next frame's payload as a Buffer. Call sock.wasStringPacket() immediately afterwards to find out whether it was a text frame. Without an argument, blocks until a frame arrives. 0 is a non-blocking peek that returns "busy" when no frame is queued; a positive value waits that many milliseconds and returns "timeout" if nothing arrives; -1 blocks, and is the default. Returns "eof" if the peer closes before any frame arrives, or "error" if the connection drops.

sock.wasStringPacket()

Returns true if the most recently received frame was a text frame, false for a binary one. Reflects the last successful recv(...); the value is undefined before the first frame arrives.

sock.selectedProtocol()

Returns the negotiated subprotocol name, or "" if none was selected. Valid once the status is "connected".

sock.requestedUrl()

Server-side, the URL the client used in its GET upgrade; client-side, the URL passed to WebSocket.connect. Valid once the status is "connected".

sock.closeCode()

Returns the 16-bit WebSocket close code reported by the peer, or sent locally, once the close handshake has happened. -1 if no close has been observed yet.

sock.closeReason()

Returns the UTF-8 close reason that accompanied closeCode(), or "" if none was provided.

sock.peerAddress()

Returns the remote address of the underlying TCP or TLS stream as { host, port }, or null before the handshake completes.

sock.setNoDelay(b)

Toggles TCP_NODELAY (Nagle's algorithm) on the underlying transport.

sock.close(code?, reason?)

Sends a close frame and tears down the underlying stream. With no arguments, sends a normal closure (1000, no reason). Idempotent.

Sockets.waitAny and WebSocket

WebSocket socks are valid handles for Sockets.waitAny, and the readiness primitives apply the same way:

Mixing WebSocket socks with TCP, TLS, UDP, DTLS, and server handles in one waitAny call is supported.

Notes

Examples

ENet echo server

func main(argv) {
    var srv = ENet.listen("0.0.0.0", 9000, 32, 4)
    while (true) {
        var ev = srv.service(100)
        if (ev == null) continue
        if (ev["type"] == "receive") {
            ev["peer"].send(ev["data"], ev["channel"], "reliable")
        }
    }
}

ENet client with a handshake loop

pump service() until the peer settles
func main(argv) {
    var r = ENet.connect("127.0.0.1", 9000, 4)
    if (r == null) { print("connect failed\n"); return 1 }
    var host = r["host"]
    var peer = r["peer"]

    while (peer.status() == "connecting") {
        host.service(50)
    }
    if (peer.status() != "connected") { print("handshake failed\n"); return 1 }

    peer.send(Buffer.fromString("hello"), 0, "reliable")
    host.flush()

    var ev = host.service(2000)
    if (ev != null && ev["type"] == "receive") {
        print("got: %s\n", ev["data"].toUtf8())
    }

    peer.disconnect()
    host.flush()
    host.service(50)
    host.close()
    return 0
}

ENet over DTLS, in-process

self-signed handshake driven from one loop
func main(argv) {
    var c = Crypto.create()
    var key = c.generateRsa(2048)
    var cert = c.generateSelfSignedCertificate(key, "CN=zym-test",
                                               "20240101000000",
                                               "20340101000000")

    var srv = ENet.listen("127.0.0.1", 0, 4, 4,
                          { tls: { key: key, cert: cert } })
    var port = srv.localPort()

    var pair = ENet.connect("127.0.0.1", port, 4,
                            { tls: { verify: false } })
    var host = pair["host"]
    var peer = pair["peer"]

    var serverPeer = null
    while (peer.status() == "connecting" || serverPeer == null) {
        var ev = srv.service(20)
        if (ev != null && ev["type"] == "connect") { serverPeer = ev["peer"] }
        host.service(20)
    }

    peer.send(Buffer.fromString("hello dtls"), 0, "reliable")
    host.flush()
    var ev = srv.service(500)
    print("server got: %s\n", ev["data"].toUtf8())

    peer.disconnect()
    host.flush(); host.service(50)
    host.close(); srv.close()
    return 0
}

WebSocket client

connect, exchange one message
var s = WebSocket.connect("ws://echo.websocket.events/")
while s.status() == "connecting" {
    s.poll()
    System.sleep(20)
}
if s.status() != "connected" {
    print("handshake failed:", s.status())
    return
}

s.sendText("hello")
var frame = s.recv(5000)
if typeof(frame) == "string" {
    print("error/timeout:", frame)
} else {
    if s.wasStringPacket() {
        print("text reply:", frame.toString())
    } else {
        print("binary reply:", frame.size(), "bytes")
    }
}
s.close()

WebSocket server: accept one connection and echo

plain ws:// over an accepted TCP sock
var srv = TCP.listen("127.0.0.1", 0)
print("listening on", srv.localPort())

var raw = srv.accept()                  // TCP sock
var ws  = WebSocket.accept(raw)         // upgrade to WebSocket
while ws.status() == "connecting" {
    ws.poll()
    System.sleep(20)
}

var frame = ws.recv(5000)
if typeof(frame) != "string" {
    ws.send(frame)                       // echo back
}
ws.close()
raw.close()
srv.close()

WebSocket server: wss:// with a self-signed certificate

in-process round-trip
var c    = Crypto.create()
var key  = c.generateRsa(2048)
var cert = c.generateSelfSignedCertificate(key, "CN=localhost",
                                           "20240101000000",
                                           "20440101000000")

var srv = TCP.listen("127.0.0.1", 0)
var port = srv.localPort()

// Client side runs concurrently; since zym is single-threaded we drive
// both sides from one loop.
var cli = WebSocket.connect("wss://127.0.0.1:" + port + "/",
                            { tls: { verify: false } })

var rawSrv = srv.accept(2000)
var tlsSrv = TLS.accept(rawSrv, { key: key, cert: cert }, 0)
var wsSrv  = WebSocket.accept(tlsSrv)

while cli.status() == "connecting" || wsSrv.status() == "connecting" {
    cli.poll()
    tlsSrv.poll()
    wsSrv.poll()
    System.sleep(20)
}

cli.sendText("hello wss")
wsSrv.poll()
print(wsSrv.recv(2000).toString())     // "hello wss"

cli.close()
wsSrv.close()
tlsSrv.close()
srv.close()

Selecting a WebSocket subprotocol

client advertises two; server picks the first it knows
var s = WebSocket.connect("ws://chat.example/",
                          { protocols: ["chat.v2", "chat.v1"] })
while s.status() == "connecting" { s.poll(); System.sleep(20) }
print("server picked:", s.selectedProtocol())