Standard library · Network

net/websocket

A two way connection that stays open between browser and server, with rooms for broadcasting.

import net.websocket

A WebSocket is a connection that starts as one HTTP request and then stays open, and either side sends a message whenever it likes: live chat, notifications, dashboards, games. This package has both ends. The server takes connections with Listen and Accept on its own port, and a client connects with Dial; AcceptWSS and DialWSS do the same over TLS. It runs its own listener and does not plug into the net/http server. For the Socket.IO protocol that many JavaScript apps use (events, acknowledgements, reconnection) use net/socketio instead; a plain browser WebSocket talks to this package directly.

Core ideas:

  • Receive blocks until a whole message has arrived and returns a Message; check its kind with IsText, IsBinary and IsClose. Pings are answered for you. Poll waits at most a given time.
  • A Hub groups connections into rooms so one call sends to every member.
  • One connection is read by one thread. To serve many clients, give each connection its own thread.

Things that catch people out:

  • A Hub has no lock. When several connection threads share it, wrap Register, Unregister and Broadcast in a sync mutex.
  • Keep a copy of the connection for Unregister. When the other side closes, Receive clears conn.fd, and Unregister matches on that socket number, so unregistering with the closed connection removes nothing. Register a copy taken right after Accept and unregister with the same copy.
  • Text stops at a zero byte and is not checked as UTF-8. Send binary data as binary and read it from m.data and m.len; that buffer belongs to the connection and is reused by the next binary message, so copy what you keep.
  • The server takes any origin and offers no subprotocols or compression; check conn.hdr yourself if you need an Origin rule. A frame larger than 100 MB closes the connection.

Each example below runs a server and its clients in the same program, the server in a thread started with spawn, on a free port chosen by Listen(0). That keeps them runnable offline; in a real program the clients are browsers using the JavaScript WebSocket API, which speaks the same protocol.

Typed JSON messages

A WebSocket carries text or bytes; the structure is up to you. The common choice is one JSON object per message with a type field, and a server loop that reads a message, looks at the type and answers. Receive waits for the next message, IsText tells text from close or binary frames, and json.Str quotes user text safely when you build a reply by hand.

net-websocket-json.salam
import net.websocket
import encoding.json

mut server := websocket.Server {}

func chat():
    mut conn := websocket.Accept(server)
    mut name := ""
    until websocket.IsOpen(conn):
        msg := websocket.Receive(conn)
        if not websocket.IsText(msg): break end
        kind := json.Get(msg.text, "type")
        if kind == "join":
            name = json.Get(msg.text, "name")
            _ := websocket.SendText(conn, `{"type": "joined", "users": 1}`)
        else kind == "say":
            reply := `{"type": "said", "from": ` + json.Str(name) + `, "text": ` + json.Str(json.Get(msg.text, "text")) + `}`
            _ := websocket.SendText(conn, reply)
        else:
            _ := websocket.SendText(conn, `{"type": "error", "reason": "unknown type"}`)
        end
    end
    websocket.Close(conn)
end

func ask(c &: websocket.Conn, request: str): str:
    _ := websocket.SendText(c, request)
    ret websocket.ReceiveText(c)
end

func main:
    server = websocket.Listen(0)
    t := spawn(chat)
    mut c := websocket.Dial("127.0.0.1", websocket.Port(server), "/chat")
    joined := ask(c, `{"type": "join", "name": "Sara"}`)
    println json.Get(joined, "type"), "users:", json.GetInt(joined, "users")
    said := ask(c, `{"type": "say", "text": "salam \"everyone\""}`)
    println json.Get(said, "from"), "said:", json.Get(said, "text")
    println ask(c, `{"type": "dance"}`)
    websocket.Close(c)
    join(t)
    websocket.CloseServer(server)
end
Outputjoined users: 1 Sara said: salam "everyone" {"type": "error", "reason": "unknown type"}

Always answer an unknown type with an error message instead of closing; clients built later may send types an older server does not know.

Rooms with a Hub

A chat or a live score page sends one message to many connections. A Hub keeps connections by room name: Register adds a connection to a room, Broadcast sends text to every member and returns how many got it, and RoomSize counts the members. A natural room name is the path the client connected to, which the server sees in conn.path.

net-websocket-rooms.salam
import net.websocket
import str

mut server := websocket.Server {}
mut hub := websocket.NewHub()

func acceptor():
    repeat 3:
        conn := websocket.Accept(server)
        room := str.Replace(conn.path, "/rooms/", "")
        websocket.Register(hub, room, conn)
    end
end

func main:
    server = websocket.Listen(0)
    port := websocket.Port(server)
    t := spawn(acceptor)
    mut ali := websocket.Dial("127.0.0.1", port, "/rooms/news")
    mut sara := websocket.Dial("127.0.0.1", port, "/rooms/news")
    mut reza := websocket.Dial("127.0.0.1", port, "/rooms/sport")
    join(t)
    println "news:", websocket.RoomSize(hub, "news"), "sport:", websocket.RoomSize(hub, "sport")
    println "sent to", websocket.Broadcast(hub, "news", "rates are up"), "in news"
    println "sent to", websocket.Broadcast(hub, "sport", "2 to 1 at half time"), "in sport"
    println "ali:", websocket.ReceiveText(ali)
    println "sara:", websocket.ReceiveText(sara)
    println "reza:", websocket.ReceiveText(reza)
    println "nobody in chess:", websocket.Broadcast(hub, "chess", "hello")
    websocket.Close(ali)
    websocket.Close(sara)
    websocket.Close(reza)
    websocket.CloseServer(server)
end
Outputnews: 2 sport: 1 sent to 2 in news sent to 1 in sport ali: rates are up sara: rates are up reza: 2 to 1 at half time nobody in chess: 0

In a real server, read each connection in its own thread and call Unregister when it closes, so broadcasts do not go to connections that are gone. The next section shows that server.

A chat server with a thread per connection

Here every member thread accepts one connection, registers it in the lobby room, and then reads that connection until it closes, broadcasting each line to the room. Several threads may call Accept on the same server; each gets a different client. The hub is shared, so every hub call is wrapped in one mutex. The thread registers entry, a copy taken before any Receive, and unregisters with that same copy; with conn itself the room would still count two members at the end.

net-websocket-chat-server.salam
import net.websocket
import str
import sync

mut server := websocket.Server {}
mut hub := websocket.NewHub()
mut lock := sync.NewMutex()

func announce(text: str):
    sync.Lock(lock)
    _ := websocket.Broadcast(hub, "lobby", text)
    sync.Unlock(lock)
end

func member():
    mut conn := websocket.Accept(server)
    if not websocket.IsOpen(conn): ret end
    name := str.Replace(conn.path, "/chat/", "")
    entry := conn
    sync.Lock(lock)
    websocket.Register(hub, "lobby", entry)
    sync.Unlock(lock)
    _ := websocket.SendText(conn, "welcome, " + name)
    until websocket.IsOpen(conn):
        m := websocket.Receive(conn)
        if not websocket.IsText(m): break end
        announce(name + ": " + m.text)
    end
    sync.Lock(lock)
    websocket.Unregister(hub, "lobby", entry)
    sync.Unlock(lock)
    announce(name + " left")
    websocket.Close(conn)
end

func main:
    server = websocket.Listen(0)
    port := websocket.Port(server)
    a := spawn(member)
    b := spawn(member)
    mut sara := websocket.Dial("127.0.0.1", port, "/chat/sara")
    println "sara got:", websocket.ReceiveText(sara)
    mut reza := websocket.Dial("127.0.0.1", port, "/chat/reza")
    println "reza got:", websocket.ReceiveText(reza)

    _ := websocket.SendText(sara, "lunch at one?")
    println "sara got:", websocket.ReceiveText(sara)
    println "reza got:", websocket.ReceiveText(reza)

    websocket.Close(reza)
    println "sara got:", websocket.ReceiveText(sara)
    websocket.Close(sara)
    join(a)
    join(b)
    println "left in the lobby:", websocket.RoomSize(hub, "lobby")
    websocket.CloseServer(server)
    sync.Destroy(lock)
end
Outputsara got: welcome, sara reza got: welcome, reza sara got: sara: lunch at one? reza got: sara: lunch at one? sara got: reza left left in the lobby: 0

A thread per client is fine for hundreds of connections. Start one member thread per expected client, or have each thread spawn the next one after its Accept returns, so there is always one waiting for the next visitor.

Checking a token during the handshake

A WebSocket starts as an HTTP request, and its headers are kept in conn.hdr, so the server can check a token before it trusts the connection; read it with http.HeaderValue from net/http. On the client side DialWith adds extra header lines. A connection without a valid token gets a short message and is closed, which the client sees as a close message from Poll.

net-websocket-auth.salam
import net.websocket
import net.http

mut server := websocket.Server {}

func gate():
    repeat 2:
        mut conn := websocket.Accept(server)
        token := http.HeaderValue(conn.hdr, "authorization")
        if token == "Bearer sara-token":
            _ := websocket.SendText(conn, "welcome, sara")
            question := websocket.ReceiveText(conn)
            _ := websocket.SendText(conn, "you asked: " + question)
        else:
            _ := websocket.SendText(conn, "who are you?")
        end
        websocket.Close(conn)
    end
end

func attempt(port: int, extra: str):
    mut c := websocket.DialWith("127.0.0.1", port, "/live", extra)
    println "open:", websocket.IsOpen(c), "server says:", websocket.ReceiveText(c)
    if extra != "":
        _ := websocket.SendText(c, "any news?")
        println websocket.ReceiveText(c)
    end
    m := websocket.Poll(c, 2000)
    println "closed by the server:", websocket.IsClose(m)
end

func main:
    server = websocket.Listen(0)
    t := spawn(gate)
    attempt(websocket.Port(server), "Authorization: Bearer sara-token\r\n")
    attempt(websocket.Port(server), "")
    join(t)
    websocket.CloseServer(server)
end
Outputopen: true server says: welcome, sara you asked: any news? closed by the server: true open: true server says: who are you? closed by the server: true

Browsers cannot set an Authorization header on a WebSocket. For browser clients put a short lived token in the URL, such as /live?token=..., and check conn.path instead.

Waiting without blocking: Poll and ping

A client that waits a long time for an answer should know the connection is still alive, and proxies close connections that stay silent. Poll(conn, ms) waits at most that many milliseconds and returns a message of kind TIMEOUT when nothing arrived, which is the moment to send a Ping. The loop below waits for a job report, pinging every 100 ms, and gives up after 50 quiet rounds.

net-websocket-heartbeat.salam
import net.websocket
import time

mut server := websocket.Server {}

func worker():
    mut conn := websocket.Accept(server)
    time.Sleep(400)
    _ := websocket.SendText(conn, "report ready")
    until websocket.IsOpen(conn):
        m := websocket.Receive(conn)
        if websocket.IsClose(m) or m.kind == websocket.NONE: break end
    end
    websocket.Close(conn)
end

func main:
    server = websocket.Listen(0)
    t := spawn(worker)
    mut c := websocket.Dial("127.0.0.1", websocket.Port(server), "/jobs/42")
    mut idle := 0
    mut answer := ""
    until answer == "" and idle < 50:
        m := websocket.Poll(c, 100)
        if m.kind == websocket.TIMEOUT:
            idle += 1
            _ := websocket.Ping(c, "still here")
        else websocket.IsText(m):
            answer = m.text
        else websocket.IsClose(m):
            break
        end
    end
    println "answer:", answer
    println "pinged while waiting:", idle > 0
    websocket.Close(c)
    join(t)
    websocket.CloseServer(server)
end
Outputanswer: report ready pinged while waiting: true

The send functions return 0 on success and a negative number when the connection is gone, so check the result of SendText and Ping in long running loops.

Which function for which job

JobServerClient
OpenListen, Accept (TLS: LoadWSSConfig, AcceptWSS)Dial, DialWith (TLS: DialWSS)
SendSendText, SendBinary, Ping
ReceiveReceive (waits), Poll (with a time limit), ReceiveText (text only)
Tell messages apartIsText, IsBinary, IsClose, IsPong, or m.kind
Many clientsNewHub, Register, Broadcast, Unregister
CloseClose, CloseServerClose

Types

Server

struct Server
A WebSocket server.
NameTypeDescription
fdi64 = -1The socket descriptor.
portint = 0The port.
net-websocket-Echo.salam
import net.websocket

mut server := websocket.Server {}

func responder():
    mut conn := websocket.Accept(server)
    until websocket.IsOpen(conn):
        m := websocket.Receive(conn)
        if websocket.IsText(m):
            _ := websocket.SendText(conn, "echo: " + m.text)
        else websocket.IsBinary(m):
            _ := websocket.SendBinaryBytes(conn, m.data, m.len)
        else websocket.IsClose(m):
            break
        end
    end
    websocket.Close(conn)
end

func main:
    server = websocket.Listen(0)
    println websocket.Ok(server), websocket.Port(server) > 0
    t := spawn(responder)
    mut c := websocket.Dial("127.0.0.1", websocket.Port(server), "/chat")
    println websocket.IsOpen(c), c.path, c.client
    _ := websocket.SendText(c, "hello")
    println websocket.ReceiveText(c)
    _ := websocket.SendBinary(c, "abc")
    m := websocket.Poll(c, 2000)
    println m.kind == websocket.BINARY, m.len
    _ := websocket.Ping(c, "p")
    m2 := websocket.Poll(c, 2000)
    println websocket.IsPong(m2), m2.kind == websocket.PONG
    m3 := websocket.Poll(c, 50)
    println m3.kind == websocket.TIMEOUT
    websocket.Close(c)
    join(t)
    websocket.CloseServer(server)
    println websocket.NONE, websocket.TEXT, websocket.CLOSE, websocket.PING
end
Outputtrue true true /chat true echo: hello true 3 true true true 0 1 8 9

Conn

struct Conn
One WebSocket connection, on the server or the client side.
NameTypeDescription
fdi64 = -1The socket descriptor.
openbool = falseThe connection is open.
pathstr = ""The path that was asked for, such as /chat.
clientbool = falseThis is the client side; a client sends its frames masked.
bufvoid*The receive buffer.
blenint = 0The length of the data in the buffer.
bcapint = 0The buffer capacity.
securebool = falseIt is encrypted.
tls_idxint = -1The internal TLS index.
hdrstr = ""The handshake headers.
accstr = ""The text of a multi frame message being gathered.
acc_kindint = 0The kind of the multi frame message.
fragmentedbool = falseA multi frame message is on its way.
binvoid*The buffer of a binary message.
binlenint = 0Its length.
bincapint = 0Its capacity.

The example for this item is shown with the ones above: net-websocket-Echo.salam

Message

struct Message
One received message.
NameTypeDescription
kindint = 0The message kind; one of the constants above.
textstr = ""The text of a text message, cut at the first zero byte.
datavoid*The bytes of a binary message. They live in the connection buffer and are overwritten by the next binary message.
lenint = 0The length of the data.

The example for this item is shown with the ones above: net-websocket-Echo.salam

RoomMembers

struct RoomMembers
The members of one room.
NameTypeDescription
itemsVectorThe connections.
net-websocket-Hub.salam
import net.websocket

mut server := websocket.Server {}
mut sides := Vector {} as Vector<websocket.Conn>

func acceptor():
    repeat 2:
        sides.push(websocket.Accept(server))
    end
end

func main:
    server = websocket.Listen(0)
    port := websocket.Port(server)
    t := spawn(acceptor)
    mut a := websocket.Dial("127.0.0.1", port, "/room")
    mut b := websocket.DialWith("127.0.0.1", port, "/room", "X-Name: b\r\n")
    join(t)
    mut hub := websocket.NewHub()
    each side in sides:
        websocket.Register(hub, "lobby", side)
    end
    println websocket.RoomSize(hub, "lobby"), hub.rooms.size()
    println websocket.Broadcast(hub, "lobby", "welcome")
    println websocket.ReceiveText(a), websocket.ReceiveText(b)
    println websocket.BroadcastBinary(hub, "lobby", "xy")
    println websocket.Receive(a).len
    websocket.Unregister(hub, "lobby", sides[0])
    println websocket.RoomSize(hub, "lobby")
    websocket.Stop(a)
    websocket.Close(b)
    websocket.CloseServer(server)
end
Output2 1 2 welcome welcome 2 2 1

Hub

struct Hub
A hub: it groups connections into rooms so one message reaches every member of a room; for chat.
NameTypeDescription
roomsHashMapThe rooms and their members.

The example for this item is shown with the ones above: net-websocket-Hub.salam

Constants and variables

NONE

const NONE: i32 = 0
There is no message.

The example for this item is shown with the ones above: net-websocket-Echo.salam

BINARY

const BINARY: i32 = 2
A binary message.

The example for this item is shown with the ones above: net-websocket-Echo.salam

CLOSE

const CLOSE: i32 = 8
The other side closed the connection.

The example for this item is shown with the ones above: net-websocket-Echo.salam

TIMEOUT

const TIMEOUT: i32 = 100
No message arrived within the timeout.

The example for this item is shown with the ones above: net-websocket-Echo.salam

Functions

AcceptKey

func AcceptKey(key: str): str
Builds the handshake accept key from the client key (RFC 6455).
net-websocket-Handshake.salam
import net.websocket
import str

func main:
    println websocket.AcceptKey("dGhlIHNhbXBsZSBub25jZQ==")
    request := websocket.HandshakeRequest("example.com", "/chat", "dGhlIHNhbXBsZSBub25jZQ==")
    println str.Split(request, "\r\n")[0]
    request2 := websocket.HandshakeRequestWith("example.com", "/chat", "dGhlIHNhbXBsZSBub25jZQ==", "Authorization: Bearer x\r\n")
    println str.Contains(request2, "Authorization: Bearer x")
    response := websocket.HandshakeResponse(request)
    println str.Split(response, "\r\n")[0]
    println str.Contains(response, "Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=")
end
Outputs3pPLMBiTxaQ9kYGzzhZRbK+xOo= GET /chat HTTP/1.1 true HTTP/1.1 101 Switching Protocols true

HandshakeResponse

func HandshakeResponse(request: str): str
The 101 response of the server to one handshake request.

The example for this item is shown with the ones above: net-websocket-Handshake.salam

HandshakeRequest

func HandshakeRequest(host: str, path: str, key: str): str
The text of the client handshake request.

The example for this item is shown with the ones above: net-websocket-Handshake.salam

HandshakeRequestWith

func HandshakeRequestWith(host: str, path: str, key: str, extra: str): str
A handshake request with extra headers.

The example for this item is shown with the ones above: net-websocket-Handshake.salam

Listen

func Listen(port: int): Server
A WebSocket server on the given port; zero means a free port, and then Port tells which one (the port field stays 0).

The example for this item is shown with the ones above: net-websocket-Echo.salam

Ok

func Ok(srv: Server): bool
The server was created properly.

The example for this item is shown with the ones above: net-websocket-Echo.salam

Port

func Port(srv: Server): int
The server port.

The example for this item is shown with the ones above: net-websocket-Echo.salam

CloseServer

func CloseServer(srv: Server)
Closes the server.

The example for this item is shown with the ones above: net-websocket-Echo.salam

Accept

func Accept(srv: Server): Conn
Waits for the next client and completes the WebSocket handshake. If the request is not a valid WebSocket upgrade the connection comes back closed, so check IsOpen.

The example for this item is shown with the ones above: net-websocket-Echo.salam

LoadWSSConfig

func LoadWSSConfig(cert_file: str, key_file: str)
Loads the TLS certificate and key that AcceptWSS uses. It is one setting for the whole program.
net-websocket-WSS.salam
import net.websocket

func main:
    mut a := websocket.DialWSS("echo.example.com", 443, "/")
    mut b := websocket.DialWSSWith("echo.example.com", 443, "/", "Origin: https://salamlang.ir\r\n")
    mut c := websocket.DialWSSUnverified("localhost", 8443, "/", "")
    println websocket.IsOpen(a), websocket.IsOpen(b), websocket.IsOpen(c)
    websocket.LoadWSSConfig("cert.pem", "key.pem")
    s := websocket.Listen(8443)
    mut conn := websocket.AcceptWSS(s)
    _ := websocket.SendText(conn, "hello")
    websocket.Close(conn)
end

AcceptWSS

func AcceptWSS(srv: Server): Conn
Accepts a secure (wss) connection.

The example for this item is shown with the ones above: net-websocket-WSS.salam

Dial

func Dial(host: str, port: int, path: str): Conn
Connects to a WebSocket server.

The example for this item is shown with the ones above: net-websocket-Echo.salam

DialWith

func DialWith(host: str, port: int, path: str, extra: str): Conn
The same, with extra headers; every header has to end with \r\n.

The example for this item is shown with the ones above: net-websocket-Hub.salam

DialWSS

func DialWSS(host: str, port: int, path: str): Conn
A secure (wss) connection.

The example for this item is shown with the ones above: net-websocket-WSS.salam

DialWSSWith

func DialWSSWith(host: str, port: int, path: str, extra: str): Conn
A secure connection with extra headers.

The example for this item is shown with the ones above: net-websocket-WSS.salam

DialWSSUnverified

func DialWSSUnverified(host: str, port: int, path: str, extra: str): Conn
A secure connection without verifying the certificate; for testing with a self signed certificate only.

The example for this item is shown with the ones above: net-websocket-WSS.salam

IsOpen

func IsOpen(c &: Conn): bool
The connection is open.

The example for this item is shown with the ones above: net-websocket-Echo.salam

Receive

func Receive(c &: Conn): Message
Takes the next whole message, waiting as long as it takes. It answers pings by itself and joins fragmented messages; on a close it answers and closes the connection.

The example for this item is shown with the ones above: net-websocket-Echo.salam

Poll

func Poll(c &: Conn, timeout_ms: int): Message
The same, waiting at most the given milliseconds; when nothing arrives the message kind is TIMEOUT. A negative time waits forever.

The example for this item is shown with the ones above: net-websocket-Echo.salam

ReceiveText

func ReceiveText(c &: Conn): str
The text of the next message, or an empty string when the next message is not text.

The example for this item is shown with the ones above: net-websocket-Echo.salam

IsText

func IsText(m: Message): bool
The message is a text one.

The example for this item is shown with the ones above: net-websocket-Echo.salam

IsBinary

func IsBinary(m: Message): bool
The message is a binary one.

The example for this item is shown with the ones above: net-websocket-Echo.salam

IsClose

func IsClose(m: Message): bool
The message is a close.

The example for this item is shown with the ones above: net-websocket-Echo.salam

IsPong

func IsPong(m: Message): bool
The message is a ping reply.

The example for this item is shown with the ones above: net-websocket-Echo.salam

SendText

func SendText(c &: Conn, msg: str): int
Sends a text message; returns 0, or a negative number when the connection is gone.

The example for this item is shown with the ones above: net-websocket-Echo.salam

SendBinary

func SendBinary(c &: Conn, data: str): int
Sends a binary message from the bytes of a string.

The example for this item is shown with the ones above: net-websocket-Echo.salam

SendBinaryBytes

func SendBinaryBytes(c &: Conn, ptr: void*, n: int): int
Sends a binary message from memory.

The example for this item is shown with the ones above: net-websocket-Echo.salam

Ping

func Ping(c &: Conn, data: str): int
Sends a ping; the other side should send a pong back.

The example for this item is shown with the ones above: net-websocket-Echo.salam

Close

func Close(c &: Conn)
Sends the close frame and closes the connection.

The example for this item is shown with the ones above: net-websocket-Echo.salam

Stop

func Stop(c &: Conn)
Sends a close frame if the connection is open and shuts the socket down without freeing the connection, so another thread blocked in Receive on it wakes up. Call Close afterwards to free it.

The example for this item is shown with the ones above: net-websocket-Hub.salam

NewHub

func NewHub(): Hub
An empty hub.

The example for this item is shown with the ones above: net-websocket-Hub.salam

Register

func Register(h &: Hub, room: str, c: Conn)
Adds a copy of a connection to a room. Not thread safe.

The example for this item is shown with the ones above: net-websocket-Hub.salam

Unregister

func Unregister(h &: Hub, room: str, c: Conn)
Takes a connection out of a room, matching on its socket; pass the same copy you registered, since a closed connection has lost its socket number.

The example for this item is shown with the ones above: net-websocket-Hub.salam

RoomSize

func RoomSize(h: Hub, room: str): int
The number of members in a room.

The example for this item is shown with the ones above: net-websocket-Hub.salam

Broadcast

func Broadcast(h &: Hub, room: str, msg: str): int
Sends a text message to every open member of a room and returns how many got it; an unknown room gives 0.

The example for this item is shown with the ones above: net-websocket-Hub.salam

BroadcastBinary

func BroadcastBinary(h &: Hub, room: str, data: str): int
Sends a binary message to every member.

The example for this item is shown with the ones above: net-websocket-Hub.salam