WebSockets are a powerful communication protocol that enables real-time, full-duplex communication between a client and a server over a single, long-lived connection. Unlike traditional HTTP requests, which follow a request-response model, WebSockets facilitate bidirectional communication, allowing data to be transmitted from both the client and the server without the need for continuous polling. In this chapter, we'll explore how to implement WebSockets in Go, from the basics to more advanced concepts.
WebSockets provide a standardized way for browsers and servers to communicate in real-time. They are particularly useful for applications that require low-latency communication, such as chat applications, online gaming, or live data feeds.
Unlike traditional HTTP, which follows a request-response model, WebSockets start with an HTTP handshake, after which the connection is upgraded to a WebSocket connection. Once established, both the client and server can send messages asynchronously over the same connection.
Let’s start by setting up a basic WebSocket server in Go using the gorilla/websocket
package, a popular WebSocket library in the Go ecosystem.
package main
import (
"log"
"net/http"
"github.com/gorilla/websocket"
)
var upgrader = websocket.Upgrader{
CheckOrigin: func(r *http.Request) bool {
return true
},
}
func handler(w http.ResponseWriter, r *http.Request) {
conn, err := upgrader.Upgrade(w, r, nil)
if err != nil {
log.Println(err)
return
}
defer conn.Close()
for {
messageType, p, err := conn.ReadMessage()
if err != nil {
log.Println(err)
return
}
if err := conn.WriteMessage(messageType, p); err != nil {
log.Println(err)
return
}
}
}
func main() {
http.HandleFunc("/", handler)
log.Fatal(http.ListenAndServe(":8080", nil))
}
gorilla/websocket
package, which provides utilities for working with WebSockets.In real-world applications, we often need to broadcast messages from one client to multiple clients. Let’s extend our WebSocket server to support broadcasting.
type Client struct {
conn *websocket.Conn
}
var (
clients = make(map[*Client]bool)
broadcast = make(chan []byte)
register = make(chan *Client)
unregister = make(chan *Client)
)
func handleMessages() {
for {
message := <-broadcast
for client := range clients {
if err := client.conn.WriteMessage(websocket.TextMessage, message); err != nil {
log.Println(err)
return
}
}
}
}
func main() {
go handleMessages()
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
conn, err := upgrader.Upgrade(w, r, nil)
if err != nil {
log.Println(err)
return
}
defer conn.Close()
client := &Client{conn: conn}
register <- client
defer func() { unregister <- client }()
for {
_, message, err := conn.ReadMessage()
if err != nil {
log.Println(err)
return
}
broadcast <- message
}
})
log.Fatal(http.ListenAndServe(":8080", nil))
}
Client
struct to represent WebSocket clients.handleMessages
function continuously listens for messages on the broadcast
channel and sends them to all connected clients.In many cases, transmitting data over WebSocket connections requires an extra layer of security, especially when dealing with sensitive information. Transport Layer Security (TLS) provides encryption and authentication to ensure data confidentiality and integrity. Let’s enhance our WebSocket server to use TLS for secure communication.
package main
import (
"log"
"net/http"
"github.com/gorilla/websocket"
)
var upgrader = websocket.Upgrader{
CheckOrigin: func(r *http.Request) bool {
return true
},
}
func handler(w http.ResponseWriter, r *http.Request) {
conn, err := upgrader.Upgrade(w, r, nil)
if err != nil {
log.Println(err)
return
}
defer conn.Close()
for {
messageType, p, err := conn.ReadMessage()
if err != nil {
log.Println(err)
return
}
if err := conn.WriteMessage(messageType, p); err != nil {
log.Println(err)
return
}
}
}
func main() {
http.HandleFunc("/", handler)
// Serve using TLS
certFile := "server.crt"
keyFile := "server.key"
log.Fatal(http.ListenAndServeTLS(":8080", certFile, keyFile, nil))
}
main
function to use http.ListenAndServeTLS
instead of http.ListenAndServe
, enabling TLS support.certFile
and keyFile
specify the paths to the TLS certificate and key files. You’ll need to generate these files or obtain them from a certificate authority.While our examples so far have focused on sending text-based messages, WebSockets also support binary data transmission. Let’s modify our WebSocket server to handle binary messages.
func handler(w http.ResponseWriter, r *http.Request) {
conn, err := upgrader.Upgrade(w, r, nil)
if err != nil {
log.Println(err)
return
}
defer conn.Close()
for {
messageType, data, err := conn.ReadMessage()
if err != nil {
log.Println(err)
return
}
if err := conn.WriteMessage(messageType, data); err != nil {
log.Println(err)
return
}
}
}
We’ve made no changes to the handler function itself, but it’s important to note that the messageType
returned by conn.ReadMessage()
indicates whether the message is text or binary. We use the same messageType
when writing the message back to ensure consistency.
In real-world applications, you may need to implement more advanced authentication mechanisms to secure WebSocket connections. This could involve using JSON Web Tokens (JWT), OAuth, or other authentication protocols. Let’s briefly discuss how you might integrate JWT-based authentication into our WebSocket server.
// Example JWT-based authentication middleware
func authenticate(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
tokenString := r.Header.Get("Authorization")
if tokenString == "" {
http.Error(w, "Unauthorized", http.StatusUnauthorized)
return
}
// Verify JWT token
token, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) {
// Validate the token signing method etc.
})
if err != nil {
http.Error(w, "Unauthorized", http.StatusUnauthorized)
return
}
// If token is valid, proceed to the next handler
next.ServeHTTP(w, r)
})
}
// Usage
func main() {
http.HandleFunc("/", handler)
http.Handle("/secured", authenticate(http.HandlerFunc(securedHandler)))
log.Fatal(http.ListenAndServe(":8080", nil))
}
authenticate
that extracts the JWT token from the request header, verifies its validity, and either proceeds to the next handler or returns an “Unauthorized” error.authenticate
middleware is applied to a specific route (/secured
), indicating that only authenticated clients can access that endpoint.main
function, we use http.Handle
to associate the authenticate
middleware with the /secured
route.By incorporating TLS support, handling binary data, and implementing advanced client authentication, we've elevated our WebSocket server to meet the demands of real-world applications. These advanced concepts provide a solid foundation for building secure, high-performance WebSocket-based communication systems in Go. Experiment with these techniques and adapt them to your specific use cases to create robust and scalable applications. Happy coding !❤️