If you are building a standard CRUD application, suppose a to-do list, HTTP is all you need. The client asks the server for the data, the server provides it, and the transaction ends. It's simple, stateless, and easy to scale.
But think what the case will be if we were to build a chatting application, the requirements are fundamentally different. When a client sends a message to a room in the server, every other client connected to that room must receive the message instantly.
If we tried to build this with standard HTTP, we would be forced into polling, essentially the client asks the server, "Do I have a new message?" every second. This is like calling your friend every 5 seconds asking if they have something to say. It wastes bandwidth, hammers your database and still introduces lag.
Long Polling vs Short Polling
See that term polling I have used in the last paragraph, there are two types of it.
Short Polling: The client will request data from the server, if the requested data is available the server will send it right away. If it's not the server will return an empty response. The client will repeat this cycle at a regular interval simulating a real-time experience.
Long Polling: The client will request data from the server, if the requested data is available the server will send it right away, same as before. But if the data is not yet available to the server, the server will hold the connection alive until the data is available and then send it to the client. The cycle will repeat.
Introducing WebSockets
To solve this, we use WebSocket, standardized by IETF as
Something to note is that Sec-WebSocket-Key and Sec-WebSocket-Accept are intended to prevent a caching proxy from re-sending a previous WebSocket conversation, they do not provide any authentication or integrity.
You must verify the authenticity of any incoming WebSocket connection yourself before accepting it, there are several well-adopted mechanisms for this such as using token or ticket based authentication. We will talk about it more later in this article.
Same-Origin Policy:
WebSocket is designed to be compatible with HTTP, at least for the initial handshake. But it doesn't mean that it comes with all the security considerations of HTTP. One such case is that WebSocket requests are not restricted by the same-origin policy. As a result, your WebSocket server must validate the Origin header during the connection establishment to avoid cross-site WebSocket attacks. There are live examples of vulnerabilities caused by this, better be safe.
Basic Example
Our goal in this series of articles is to create a functional chatting application with multiple channels and connected clients. But before we start working on that it's better to write a basic program which will help us understand how to implement a WebSocket server in Go.
While Go has a net/http library, the Gorilla WebSocket is a battle-tested, de-facto standard in the Go community. It handles the nitty-gritty of the WebSocket protocol, things like masking, fragmentation and control frames, so we don't have to. Coder/WebSocket is also another great choice which we will use in the near future.
We will write a websocket echo program just to understand how the thing works.
First create a Go project, create a folder and run these commands in the terminal:
go mod init websocket-echo
go get github.com/gorilla/websocket
As you know, these will create a go module for us to work with, lets create a main.go file and write a basic starter application.
main.go:
package main
import (
"log"
"net/http"
)
func handleIndexFunc(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
w.Write([]byte("Hello, World!"))
}
func main() {
http.HandleFunc("/", handleIndexFunc)
log.Println("Starting server on :8080")
if err := http.ListenAndServe(":8080", nil); err != nil {
log.Fatalf("Could not start server: %s\n", err.Error())
}
}
As you are working with WebSockets, I will assume that you have a working understanding of Go, so I will not explain the basics. What we are doing here is using the default HTTP Handler to create an endpoint / and creating a handleIndexFunc function to handle that endpoint. The function will return HTTP 200 (because we are writing http.StatusOK in the response header) and the text Hello, World! as the body.
Lets run it.
$ go run main.go
Starting server on :8080
Now if we hit the address http://127.0.0.1:8080 we will get the text Hello, World! as a response.
This is a basic HTTP endpoint, and our starting point. We will now make our way through implementing websockets.
Note the line http.HandleFunc("/", handleIndexFunc), we are not specifying the HTTP method here, as a result this function will respond to every HTTP METHOD in existence. If we want to specify a method we can write it like this http.HandleFunc("GET /", handleIndexFunc), this will assign the function to only the HTTP GET method.
Our program so far has one endpoint, an HTTP one, we are not here for that, so lets get started with implementing WebSocket.
The first thing we will need for serving a WebSocket endpoint is an upgrader. What is an upgrader you might ask? Remember how WebSocket starts its life by being a simple HTTP connection and then upgrades itself to WebSocket? The upgrader is responsible for the upgrade, Who would have thunk?
In a general sense it validates the HTTP requests, checks if it contains all the necessary headers, calculates the Sec-WebSocket-Accept parameter and returns with 101 Switching Protocols if everything succeeds.
var websocketUpgrader = websocket.Upgrader{
ReadBufferSize: 1024,
WriteBufferSize: 1024,
}
ReadBufferSize and WriteBufferSize are the size of the memory buffer used for, well, Reading and Writing to/from the network. We are allocating 1KB of memory for the incoming and outgoing data. Keep it in mind that having a buffer of 1KB doesn't mean we can't read/write above this limit, it will just process the message as 1KB chunks. Another thing is that this buffer is allocated for each user, meaning if we have 100,000 connected users, the buffers alone would consume about 200MB of RAM (100k * 2KB).
Remember what we read about websocket doesn't automatically enforce the check-origin policy? We need to implement that in the upgrader. For development purposes we will just return true which will allow connections from all origins.
var websocketUpgrader = websocket.Upgrader{
ReadBufferSize: 1024,
WriteBufferSize: 1024,
CheckOrigin: func(r *http.Request) bool {
return true
},
}
Our upgrader is done, now we will create a handler function for the websocket.
package main
import (
"log"
"net/http"
"github.com/gorilla/websocket"
)
var websocketUpgrader = websocket.Upgrader{
ReadBufferSize: 1024,
WriteBufferSize: 1024,
CheckOrigin: func(r *http.Request) bool {
return true
},
}
func handleWebSocketFunc(w http.ResponseWriter, r *http.Request) {
}
func handleIndexFunc(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
w.Write([]byte("Hello, World!"))
}
func main() {
http.HandleFunc("GET /", handleIndexFunc)
http.HandleFunc("GET /ws", handleWebSocketFunc)
log.Println("Starting server on :8080")
if err := http.ListenAndServe(":8080", nil); err != nil {
log.Fatalf("Could not start server: %s\n", err.Error())
}
}
The websocket handler has the same function signature as any other HTTP handler, because up until now it's just another regular HTTP connection, nothing else.
Now we will use our upgrader and try to upgrade our regular HTTP connection to a websocket one.
func handleWebSocketFunc(w http.ResponseWriter, r *http.Request) {
conn, err := websocketUpgrader.Upgrade(w, r, nil)
if err != nil {
log.Printf("WebSocket upgrade error: %s\n", err.Error())
return
}
log.Println("WebSocket connection established")
}
The upgrader takes three arguments, the response object, request object, and a header, which we are passing nil. The header can be used to set cookies.
If we omit the conn variable and run the code, we will be able to connect to our /ws endpoint by using any API Client software such as Postman.
Let's try it, here is the full main.go code.
package main
import (
"log"
"net/http"
"github.com/gorilla/websocket"
)
var websocketUpgrader = websocket.Upgrader{
ReadBufferSize: 1024,
WriteBufferSize: 1024,
CheckOrigin: func(r *http.Request) bool {
return true
},
}
func handleWebSocketFunc(w http.ResponseWriter, r *http.Request) {
_, err := websocketUpgrader.Upgrade(w, r, nil)
if err != nil {
log.Printf("WebSocket upgrade error: %s\n", err.Error())
return
}
log.Println("WebSocket connection established")
}
func handleIndexFunc(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
w.Write([]byte("Hello, World!"))
}
func main() {
http.HandleFunc("GET /", handleIndexFunc)
http.HandleFunc("GET /ws", handleWebSocketFunc)
log.Println("Starting server on :8080")
if err := http.ListenAndServe(":8080", nil); err != nil {
log.Fatalf("Could not start server: %s\n", err.Error())
}
}
Run it and connect to ws://127.0.0.1:8080/ws from any websocket supported client and you will be greeted with a successful connection like below.
So we successfully built our echo example, here is the complete main.go code, I will suggest you go over everything and make sure you understand it all. In the next chapter we will start to implement a functional chatting program with multiple channels and clients.
package main
import (
"log"
"net/http"
"github.com/gorilla/websocket"
)
var websocketUpgrader = websocket.Upgrader{
ReadBufferSize: 1024,
WriteBufferSize: 1024,
CheckOrigin: func(r *http.Request) bool {
return true
},
}
func handleWebSocketFunc(w http.ResponseWriter, r *http.Request) {
conn, err := websocketUpgrader.Upgrade(w, r, nil)
if err != nil {
log.Printf("WebSocket upgrade error: %s\n", err.Error())
return
}
defer conn.Close()
log.Println("WebSocket connection established")
for {
messageType, message, err := conn.ReadMessage()
if err != nil {
log.Println("Read error:", err)
break
}
log.Printf("Received Message Bytes: %s", message)
log.Printf("Received Message Type: %d", messageType)
echoMessage := append([]byte("Echo: "), message...)
err = conn.WriteMessage(messageType, echoMessage)
if err != nil {
log.Println("Write error:", err)
break
}
log.Printf("Sent Message Bytes: %s", message)
}
}
func handleIndexFunc(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
w.Write([]byte("Hello, World!"))
}
func main() {
http.HandleFunc("GET /", handleIndexFunc)
http.HandleFunc("GET /ws", handleWebSocketFunc)
log.Println("Starting server on :8080")
if err := http.ListenAndServe(":8080", nil); err != nil {
log.Fatalf("Could not start server: %s\n", err.Error())
}
}
Conclusion
We didn't build something meaningful yet, but we have learned the basics of implementing a websocket server in Go using Gorilla WebSocket. In the next chapter we will start building a functional chatting application with multiple channels and clients. Until then happy coding.
This post was originally published on my blog.
SOCIAL SHARE CARD GENERATOR