If you’re interested in building a real-time chat application using Go (Golang) and WebSockets, you’re in the right place. In this tutorial, I’ll walk you through the process step by step. We’ll cover setting up the project, implementing the server and client, and testing everything out. Let’s get started!
What are WebSockets? Link to heading
Before we dive in, let’s briefly talk about WebSockets. WebSockets provide a way to open a persistent connection between a client and a server, allowing for real-time, two-way communication. Unlike HTTP, where the client has to constantly poll the server for updates, WebSockets allow the server to send updates to the client as soon as they happen. This makes them perfect for real-time applications like chat apps, live notifications, and games.
1. Setting Up the Project Link to heading
First things first, let’s set up our Go project. Create a new directory for our chat application and initialize a new Go module.
mkdir chat
cd chat
go mod init chat
Next, we need to add the WebSocket package we’ll be using. Run this command to get the package:
go get golang.org/x/net/websocket
Your go.mod
file should look like this:
module chat
go 1.22.3
require golang.org/x/net v0.26.0
2. Implementing the Server Link to heading
Now, let’s build our server. The server will handle WebSocket connections, manage clients, and broadcast messages. Create a new directory called server
and add two files: server.go
and hub.go
.
server.go Link to heading
This file will be the main entry point for our server application. Here’s the code:
package main
import (
"fmt"
"golang.org/x/net/websocket"
"log"
"net/http"
)
func main() {
h := newHub()
mux := http.NewServeMux()
mux.Handle("/", websocket.Handler(func(conn *websocket.Conn) {
wsHandler(conn, h)
}))
server := http.Server{
Addr: ":9686",
Handler: mux,
}
err := server.ListenAndServe()
if err != nil {
log.Fatalln(err)
}
}
func wsHandler(conn *websocket.Conn, h *hub) {
go h.run()
h.addClientChn <- conn
for {
var m Message
err := websocket.JSON.Receive(conn, &m)
if err != nil {
h.removeClientChn <- conn
fmt.Println("error in Receive Message: ", err.Error())
continue
}
h.broadcastChn <- m
}
}
hub.go Link to heading
This file will handle managing clients and broadcasting messages. Here’s what it looks like:
package main
import (
"golang.org/x/net/websocket"
)
type Message struct {
Text string `json:"text"`
}
type hub struct {
clients map[string]*websocket.Conn
addClientChn chan *websocket.Conn
removeClientChn chan *websocket.Conn
broadcastChn chan Message
}
func newHub() *hub {
return &hub{
clients: make(map[string]*websocket.Conn),
addClientChn: make(chan *websocket.Conn),
removeClientChn: make(chan *websocket.Conn),
broadcastChn: make(chan Message),
}
}
func (h *hub) run() {
for {
select {
case conn := <-h.addClientChn:
h.addClient(conn)
case conn := <-h.removeClientChn:
h.removeClient(conn)
case m := <-h.broadcastChn:
h.broadcast(m)
}
}
}
func (h *hub) addClient(conn *websocket.Conn) {
h.clients[conn.RemoteAddr().String()] = conn
fmt.Println("Clients, ", h.clients)
}
func (h *hub) removeClient(conn *websocket.Conn) {
delete(h.clients, conn.RemoteAddr().String())
}
func (h *hub) broadcast(m Message) {
for _, conn := range h.clients {
err := websocket.JSON.Send(conn, m)
if err != nil {
fmt.Println("error in Broadcast Message: ", err.Error())
continue
}
}
}
In the addClient
function, we are using the client’s IP address as a unique identifier. This is why in client.go
, we create random IP addresses for clients using the CreateDemoIp
function. In a real-world application, you would typically use a user ID or username from your database instead of an IP address.
Why Use a Hub and Channels? Link to heading
You might be wondering why we need a hub and channels. The hub acts as the central point for managing all WebSocket connections. It keeps track of clients and handles broadcasting messages to everyone. Using channels (addClientChn
, removeClientChn
, and broadcastChn
) ensures that our application can handle multiple clients concurrently without any race conditions or other issues.
Why Use an HTTP Mux and wsHandler? Link to heading
We use http.NewServeMux
to create an HTTP request multiplexer, which allows us to route incoming HTTP requests to different handlers. In our case, we route all requests to the WebSocket handler. The wsHandler
function handles WebSocket connections, registers them with the hub, and listens for incoming messages.
3. Implementing the Client Link to heading
Next, let’s build a simple client that connects to our server, sends messages, and receives broadcast messages. Create a new directory called client
and add a client.go
file.
client.go Link to heading
Here’s the code for our client application:
package main
import (
"bufio"
"fmt"
"golang.org/x/net/websocket"
"log"
"math/rand"
"os"
"time"
)
type Message struct {
Text string `json:"text"`
}
func main() {
conn, err := websocket.Dial("ws://localhost:9686", "", CreateDemoIp())
if err != nil {
log.Fatalln(err)
return
}
defer conn.Close()
go receive(conn)
send(conn)
}
func CreateDemoIp() string {
var arry [4]int
for i := 0; i < len(arry); i++ {
rand.Seed(time.Now().UnixNano())
arry[i] = rand.Intn(256)
}
return fmt.Sprintf("http://%d.%d.%d.%d", arry[0], arry[1], arry[2], arry[3])
}
func receive(conn *websocket.Conn) {
for {
var m Message
err := websocket.JSON.Receive(conn, &m)
if err != nil {
log.Fatalln("Error in Receive Data: ", err)
continue
}
fmt.Println("Message from Server: ", m.Text)
}
}
func send(conn *websocket.Conn) {
scanner := bufio.NewScanner(os.Stdin)
for scanner.Scan() {
text := scanner.Text()
m := Message{
Text: text,
}
err := websocket.JSON.Send(conn, m)
if err != nil {
fmt.Println("Error in Send Data: ", err)
continue
}
}
}
4. Running the Application Link to heading
Now it’s time to run our chat application. We’ll start the server and then run the client to connect and chat.
Running the Server Link to heading
Navigate to the server
directory and run:
go run .
The server will start listening on port 9686.
Running the Client Link to heading
Open a new terminal window, navigate to the client
directory, and run:
go run client.go
You can open multiple terminal windows and run the client in each one to create multiple clients for testing.
5. Testing the Application Link to heading
To test our chat application:
- Start the server by running
go run .
in theserver
directory. - Open multiple terminal windows.
- In each terminal window, navigate to the
client
directory and rungo run client.go
. - Type messages in each client terminal. You should see the messages broadcasted to all connected clients.
Conclusion Link to heading
And there you have it! We’ve built a simple chat application using Go and WebSockets. We set up the project, implemented the server and client, and tested it all out. This should give you a solid foundation to build more complex real-time applications.
Feel free to explore and expand on this example. You could add features like user authentication, message persistence, or even a web-based client. Happy coding!