WebSocket Server in Go
We will create a simple WebSocket server on Go with a tiny WebSocket library gobwas/ws and learn how to handle concurrent client connections.
As a base project, we will use the example
app from First App with Go.
Create component for WebSocket handler
Make directory for new component called ws
. This component will implement an HTTP handler, active connections storage, and queue to send messages.
mkdir -p internal/ws
WebSocket handler
Install WebSocket library to handle HTTP requests:
go get github.com/gobwas/ws
Create new file internal/ws/handler.go
with HTTP handler:
package ws
import (
"io"
"log"
"net"
"net/http"
"github.com/gobwas/ws"
"github.com/gobwas/ws/wsutil"
)
func wsLoop(conn net.Conn) {
client := newClient(conn)
defer client.close()
r := wsutil.NewReader(conn, ws.StateServerSide)
for {
// Wait for next frame from client
hdr, err := r.NextFrame()
if err != nil {
if err != io.EOF {
log.Panicln("failed to read frame", err)
}
return
}
if hdr.OpCode == ws.OpClose {
return
}
// Drop any data
_, _ = io.Copy(io.Discard, r)
}
}
func Handler(w http.ResponseWriter, r *http.Request) {
conn, _, _, err := ws.UpgradeHTTP(r, w)
if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
go wsLoop(conn)
}
The ws.UpgradeHTTP()
function detaches the connection from the HTTP handler and starts the wsLoop()
function in the new goroutine. The wsLoop()
function appends new client to the storage and waits for packets from the client. In this example, we discarded all incoming packets.
Client storage
Create new file internal/ws/client.go
with client definition:
package ws
import (
"log"
"net"
"sync"
"github.com/gobwas/ws"
"github.com/gobwas/ws/wsutil"
)
type wsClient struct {
conn net.Conn
}
var (
lock sync.RWMutex
clients = make(map[*wsClient]struct{})
)
func newClient(conn net.Conn) *wsClient {
c := &wsClient{
conn: conn,
}
lock.Lock()
clients[c] = struct{}{}
lock.Unlock()
return c
}
func (c *wsClient) close() {
c.conn.Close()
lock.Lock()
delete(clients, c)
lock.Unlock()
}
func (c *wsClient) send(data []byte) error {
w := wsutil.NewWriter(c.conn, ws.StateServerSide, ws.OpText)
if _, err := w.Write(data); err != nil {
return err
}
if err := w.Flush(); err != nil {
return err
}
return nil
}
func broadcast(data []byte) {
lock.RLock()
defer lock.RUnlock()
for client := range clients {
if err := client.send(data); err != nil {
log.Println("send to client failed", err)
continue
}
}
}
For client storage, we use map
. It's a nice trick. Appending and removing new objects with map is very simple and effective.
Message Sending
Now we create new public function to send messages to all clients. Create new file internal/ws/send.go
:
package ws
import (
"context"
"log"
)
var queue = make(chan []byte, 1000)
// Send pushes data to the queue
func Send(data []byte) {
select {
case queue <- data:
default:
log.Println("send failed: queue is full")
}
}
// SendLoop is an infinity loop, send messages from queue to WebSocket clients
func SendLoop(ctx context.Context) {
for {
select {
case <-ctx.Done():
return
case msg := <-queue:
broadcast(msg)
}
}
}
HTTP Server
You may append WebSocket handler to your HTTP router. For this example, we will create a basic HTTP server.
Open file internal/app/app.go
in any text editor:
package app
import (
"context"
_ "embed"
"net/http"
"time"
"example/internal/ws"
)
//go:embed index.html
var indexHTML []byte
// webSPA serves index.html
func webSPA(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "text/html; charset=utf-8")
_, _ = w.Write(indexHTML)
}
// exampleBroadcast sends new messages to WebSocket clients each second
func exampleBroadcast(ctx context.Context) {
tick := time.NewTicker(time.Second)
defer tick.Stop()
for {
select {
case <-ctx.Done():
return
case v := <-tick.C:
currentTime := v.Format("2006-01-02 15:04:05")
ws.Send([]byte(currentTime))
}
}
}
func Start() error {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
go ws.SendLoop(ctx)
go exampleBroadcast(ctx)
mux := http.NewServeMux()
mux.HandleFunc("/ws/", ws.Handler)
mux.HandleFunc("/", webSPA)
return http.ListenAndServe(":8080", mux)
}
And finally, create simple HTML file internal/app/index.html
to start WebSocket client:
<!doctype html>
<html>
<head>
<meta charset="UTF-8">
<title>WebSocket Client</title>
</head>
<body>
<h1>WebSocket Client</h1>
<pre id="output"></pre>
<script>
const url = new URL(window.location)
url.pathname = '/ws/'
url.protocol = url.protocol.replace('http', 'ws')
const socket = new WebSocket(url)
socket.addEventListener('message', function (event) {
const codeElement = document.createElement('code')
codeElement.textContent = event.data + '\n'
document.getElementById('output').appendChild(codeElement)
})
</script>
</body>
</html>
Done. WebSocket server ready to launch
Launch
Start your application:
go run ./cmd/example
And open in your browser http://localhost:8080
Check out this project in our repository.