I remember when AOL instant messenger (AIM) was extremely popular. This page is dedicated to developing a simple chat room. It is an instructive exercise in working with WebSockets and channels in Go. A very rudimentary chatroom, reminiscent of what I saw in the 90's, can be developed in roughly 100 lines of Go code and some basic HTML and JavaScript front-end.
websocket.Upgrader
is used to upgrade an HTTP connection to
a websocket connection, allowing for full-duplex communication between this server
and any clients connected to it.
Client
struct is defined that contains a pointer to a websocket
connection
and a channel to send outgoing messages to any number of clients.
Hub
struct is defined to manage multiple clients.
Hub
contains a a map of clients. At first I used a slice of clients,
but
when I was working on the code shown below I realized that a map would provide for more
efficient lookups used to register and unregister clients, and also avoid duplicate
clients
from being registered.
Hub
also contains a channel used to broadcast messages to all clients,
channels for registering and unregistering clients, and a slice of byte slices, where
each
nested byte slice corresponds to a message that was sent, this supports newly registered
clients
from viewing the chat room history.
package main
import (
"fmt"
"log"
"net/http"
"github.com/gorilla/websocket"
)
var upgrader = websocket.Upgrader{
ReadBufferSize: 1024,
WriteBufferSize: 1024,
}
type Client struct {
conn *websocket.Conn
send chan []byte
}
type Hub struct {
clients map[*Client]bool
broadcast chan []byte
register chan *Client
unregister chan *Client
history [][]byte
}
func newHub() *Hub {
return &Hub{
broadcast: make(chan []byte),
register: make(chan *Client),
unregister: make(chan *Client),
clients: make(map[*Client]bool),
history: make([][]byte, 0),
}
}
Hub
.
register
channel.
The run()
function will then send existing chat history to this client and
add
it to the
map of clients in the Hub
.
unregister
channel
which will
result in its deletion from the map of clients and closes its send
channel.
Hub
on the
broadcast
channel that message is added to the chat room history and sent to all clients over each
client's
send
channel.
send
channel buffer is full (this means 256 messages have
not
been received, see below)
it is assumed something has gone wrong and
the client's send
channel will be closed and the client removed from the
Hub's
map.
func (h *Hub) run() {
for {
select {
case client := <-h.register:
for _, message := range h.history {
client.send <- message
}
h.clients[client] = true
case client := <-h.unregister:
if _, ok := h.clients[client]; ok {
delete(h.clients, client)
close(client.send)
}
case message := <-h.broadcast:
h.history = append(h.history, message)
for client := range h.clients {
select {
case client.send <- message:
default:
close(client.send)
delete(h.clients, client)
}
}
}
}
}
read()
function is used to read messages originating from clients and
send
them to the Hub
>
on the broadcast
channel.
client
and close its
connection
if an error occurs
when the websocket attempts ro read a client's message server-side.
write()
function is used to send messages from the
broadcast
channel
to connected clients. If an error occurs the connection is closed.
serveWS
function is a handler function, that when invoked by an
endpoint
being contacted
by a client will upgrade that client's HTTP connection to a WebSocket.
Client
using the newly upgraded
connection and by
creating a channel capable of containing 256 messages represented as byte slices.
func (c *Client) read(hub *Hub) {
defer func() {
hub.unregister <- c
c.conn.Close()
}()
for {
_, message, err := c.conn.ReadMessage()
if err != nil {
log.Println("error on read:", err)
break
}
hub.broadcast <- message
}
}
func (c *Client) write() {
defer c.conn.Close()
for message := range c.send {
if err := c.conn.WriteMessage(websocket.TextMessage, message); err != nil {
return
}
}
}
func serveWs(hub *Hub, w http.ResponseWriter, r *http.Request) {
conn, err := upgrader.Upgrade(w, r, nil)
if err != nil {
log.Println("error on upgrade:", err)
return
}
client := &Client{conn: conn, send: make(chan []byte, 256)}
hub.register <- client
go client.write()
client.read(hub)
}
main()
function is an entry point for this Go program.
Hub
and invokes the run()
function
associated with it.
/ws
endpoint of the server to the
serveWs()
function described above.
/ws
endpoint
on the server.
sendMessage()
function sends messages from the client's front-end to
the
WebSocket provided the message
is not empty.
func main() {
hub := newHub()
go hub.run()
http.HandleFunc("/ws", func(w http.ResponseWriter, r *http.Request) {
serveWs(hub, w, r)
})
http.Handle("/", http.FileServer(http.Dir("./public")))
fmt.Println("Starting server at https://localhost:8443")
err := http.ListenAndServeTLS(":8443", "cert.pem", "key.pem", nil)
if err != nil {
fmt.Printf("Failed to start server: %v\n", err)
}
}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>WebSocket Chat</title>
</head>
<body>
<h1>WebSocket Chat</h1>
<div id="chat"></div>
<input type="text" id="messageInput" placeholder="Type a message...">
<button onclick="sendMessage()">Send</button>
<script>
let ws = new WebSocket("wss://localhost:8443/ws");
let chat = document.getElementById("chat");
let input = document.getElementById("messageInput");
ws.onmessage = function(event) {
let message = document.createElement("div");
message.textContent = event.data;
chat.appendChild(message);
};
function sendMessage() {
if (input.value.trim() !== "") {
ws.send(input.value);
input.value = "";
}
}
input.addEventListener("keyup", function(event) {
if (event.key === "Enter") {
sendMessage();
}
});
</script>
</body>
</html>
I've used multiple browsers to simulate multiple clients and test that the chat room is able to display historical messages upon new client connections.
I learned a lot from this project but there are a number of improvements I would like to make: