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:
Receiveblocks until a whole message has arrived and returns aMessage; check its kind withIsText,IsBinaryandIsClose. Pings are answered for you.Pollwaits at most a given time.- A
Hubgroups 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
Hubhas no lock. When several connection threads share it, wrapRegister,UnregisterandBroadcastin a sync mutex. - Keep a copy of the connection for
Unregister. When the other side closes,Receiveclearsconn.fd, andUnregistermatches on that socket number, so unregistering with the closed connection removes nothing. Register a copy taken right afterAcceptand 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.dataandm.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.hdryourself if you need anOriginrule. 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.
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)
endAlways 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.
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)
endIn 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.
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)
endA 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.
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)
endBrowsers 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.
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)
endThe 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
| Job | Server | Client |
|---|---|---|
| Open | Listen, Accept (TLS: LoadWSSConfig, AcceptWSS) | Dial, DialWith (TLS: DialWSS) |
| Send | SendText, SendBinary, Ping | |
| Receive | Receive (waits), Poll (with a time limit), ReceiveText (text only) | |
| Tell messages apart | IsText, IsBinary, IsClose, IsPong, or m.kind | |
| Many clients | NewHub, Register, Broadcast, Unregister | |
| Close | Close, CloseServer | Close |
Types
Server
struct Server| Name | Type | Description |
|---|---|---|
fd | i64 = -1 | The socket descriptor. |
port | int = 0 | The port. |
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
endConn
struct Conn| Name | Type | Description |
|---|---|---|
fd | i64 = -1 | The socket descriptor. |
open | bool = false | The connection is open. |
path | str = "" | The path that was asked for, such as /chat. |
client | bool = false | This is the client side; a client sends its frames masked. |
buf | void* | The receive buffer. |
blen | int = 0 | The length of the data in the buffer. |
bcap | int = 0 | The buffer capacity. |
secure | bool = false | It is encrypted. |
tls_idx | int = -1 | The internal TLS index. |
hdr | str = "" | The handshake headers. |
acc | str = "" | The text of a multi frame message being gathered. |
acc_kind | int = 0 | The kind of the multi frame message. |
fragmented | bool = false | A multi frame message is on its way. |
bin | void* | The buffer of a binary message. |
binlen | int = 0 | Its length. |
bincap | int = 0 | Its capacity. |
The example for this item is shown with the ones above: net-websocket-Echo.salam
Message
struct Message| Name | Type | Description |
|---|---|---|
kind | int = 0 | The message kind; one of the constants above. |
text | str = "" | The text of a text message, cut at the first zero byte. |
data | void* | The bytes of a binary message. They live in the connection buffer and are overwritten by the next binary message. |
len | int = 0 | The length of the data. |
The example for this item is shown with the ones above: net-websocket-Echo.salam
RoomMembers
struct RoomMembers| Name | Type | Description |
|---|---|---|
items | Vector | The connections. |
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)
endHub
struct Hub| Name | Type | Description |
|---|---|---|
rooms | HashMap | The 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 = 0The example for this item is shown with the ones above: net-websocket-Echo.salam
TEXT
const TEXT: i32 = 1The example for this item is shown with the ones above: net-websocket-Echo.salam
BINARY
const BINARY: i32 = 2The example for this item is shown with the ones above: net-websocket-Echo.salam
CLOSE
const CLOSE: i32 = 8The example for this item is shown with the ones above: net-websocket-Echo.salam
PING
const PING: i32 = 9The example for this item is shown with the ones above: net-websocket-Echo.salam
PONG
const PONG: i32 = 10The example for this item is shown with the ones above: net-websocket-Echo.salam
TIMEOUT
const TIMEOUT: i32 = 100The example for this item is shown with the ones above: net-websocket-Echo.salam
Functions
AcceptKey
func AcceptKey(key: str): strimport 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=")
endHandshakeResponse
func HandshakeResponse(request: str): strThe example for this item is shown with the ones above: net-websocket-Handshake.salam
HandshakeRequest
func HandshakeRequest(host: str, path: str, key: str): strThe 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): strThe example for this item is shown with the ones above: net-websocket-Handshake.salam
Listen
func Listen(port: int): ServerPort 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): boolThe example for this item is shown with the ones above: net-websocket-Echo.salam
Port
func Port(srv: Server): intThe example for this item is shown with the ones above: net-websocket-Echo.salam
CloseServer
func CloseServer(srv: Server)The example for this item is shown with the ones above: net-websocket-Echo.salam
Accept
func Accept(srv: Server): ConnIsOpen.The example for this item is shown with the ones above: net-websocket-Echo.salam
LoadWSSConfig
func LoadWSSConfig(cert_file: str, key_file: str)AcceptWSS uses. It is one setting for the whole program.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)
endAcceptWSS
func AcceptWSS(srv: Server): ConnThe example for this item is shown with the ones above: net-websocket-WSS.salam
Dial
func Dial(host: str, port: int, path: str): ConnThe 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\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): ConnThe 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): ConnThe 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): ConnThe example for this item is shown with the ones above: net-websocket-WSS.salam
IsOpen
func IsOpen(c &: Conn): boolThe example for this item is shown with the ones above: net-websocket-Echo.salam
Receive
func Receive(c &: Conn): MessageThe example for this item is shown with the ones above: net-websocket-Echo.salam
Poll
func Poll(c &: Conn, timeout_ms: int): MessageTIMEOUT. 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): strThe example for this item is shown with the ones above: net-websocket-Echo.salam
IsText
func IsText(m: Message): boolThe example for this item is shown with the ones above: net-websocket-Echo.salam
IsBinary
func IsBinary(m: Message): boolThe example for this item is shown with the ones above: net-websocket-Echo.salam
IsClose
func IsClose(m: Message): boolThe example for this item is shown with the ones above: net-websocket-Echo.salam
IsPong
func IsPong(m: Message): boolThe example for this item is shown with the ones above: net-websocket-Echo.salam
SendText
func SendText(c &: Conn, msg: str): intThe example for this item is shown with the ones above: net-websocket-Echo.salam
SendBinary
func SendBinary(c &: Conn, data: str): intThe example for this item is shown with the ones above: net-websocket-Echo.salam
SendBinaryBytes
func SendBinaryBytes(c &: Conn, ptr: void*, n: int): intThe example for this item is shown with the ones above: net-websocket-Echo.salam
Ping
func Ping(c &: Conn, data: str): intThe example for this item is shown with the ones above: net-websocket-Echo.salam
Close
func Close(c &: Conn)The example for this item is shown with the ones above: net-websocket-Echo.salam
Stop
func Stop(c &: Conn)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(): HubThe example for this item is shown with the ones above: net-websocket-Hub.salam
Register
func Register(h &: Hub, room: str, c: Conn)The example for this item is shown with the ones above: net-websocket-Hub.salam
Unregister
func Unregister(h &: Hub, room: str, c: Conn)The example for this item is shown with the ones above: net-websocket-Hub.salam
RoomSize
func RoomSize(h: Hub, room: str): intThe example for this item is shown with the ones above: net-websocket-Hub.salam
Broadcast
func Broadcast(h &: Hub, room: str, msg: str): intThe example for this item is shown with the ones above: net-websocket-Hub.salam
BroadcastBinary
func BroadcastBinary(h &: Hub, room: str, data: str): intThe example for this item is shown with the ones above: net-websocket-Hub.salam