In the previous chapter, we have provided you with some examples of working with the HHTP server and client in Golang. In today's article, we will talk about how WebSockets are used, and how they are different from standard HTTP requests with gorilla/websocket package
gorilla/websocket:
Gorilla WebSocket is a Go implementation of the WebSocket protocol.
Introduction to Websockets
WebSocket is a computer communications protocol, that provides full-duplex communication channels over a single TCP connection. The WebSocket protocol was standardized by the IETF as RFC 6455 in 2011. The current API specification allowing web applications to use this protocol is known as WebSockets. It is a living standard maintained by the WHATWG and a successor to The WebSocket API from the W3C.
WebSocket is distinct from HTTP. Both protocols are located at layer 7 in the OSI model and depend on TCP at layer 4. Although they are different, RFC 6455 states that WebSocket "is designed to work over HTTP ports 443 and 80 as well as to support HTTP proxies and intermediaries", thus making it compatible with HTTP. To achieve compatibility, the WebSocket handshake uses the HTTP Upgrade header to change from the HTTP protocol to the WebSocket protocol.
The WebSocket protocol enables interaction between a web browser (or other client application) and a web server with lower overhead than half-duplex alternatives such as HTTP polling, facilitating real-time data transfer from and to the server. This is made possible by providing a standardized way for the server to send content to the client without being first requested by the client, and allowing messages to be passed back and forth while keeping the connection open. In this way, a two-way ongoing conversation can take place between the client and the server.
The communications are usually done over TCP port number 443 (or 80 in the case of unsecured connections), which is beneficial for environments that block non-web Internet connections using a firewall. Similar two-way browser–server communications have been achieved in non-standardized ways using stopgap technologies such as Comet or Adobe Flash Player.
Most browsers support the protocol, including Google Chrome, Firefox, Microsoft Edge, Internet Explorer, ...
Advantages of websocket protocol
The advantages of the WebSocket protocol include the following:
- A WebSocket connection is a full-duplex, bidirectional communications channel. This means that a server does not need to wait to read from a client to send data to the client and vice versa.
- WebSocket connections are raw TCP sockets, which means that they do not have the overhead required to establish an HTTP connection.
- WebSocket connections can also be used for sending HTTP data. However, plain HTTP connections cannot work as WebSocket connections.
- WebSocket connections live until they are killed, so there is no need to reopen them all the time.
- WebSocket connections can be used for real-time web applications.
- Data can be sent from the server to the client at any time, without the client even requesting it.
- WebSocket is part of the HTML5 specification, which means that it is supported by all modern web browsers.
Setting up Golang websocket Server
Simple HTTP Server
Because WebSockets are built on top of HTTP, we'll first create a simple HTTP server that can accept client connections and deliver messages with Golang net/http package:
package main
import (
"fmt"
"net/http"
)
func main() {
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "This is GolinuxCloud's websocket server")
})
http.ListenAndServe(":8080", nil)
}
Output: Access http://localhost:8080/ with any browsers:
Define the Upgrader
The websocket.Upgrader
method of the gorilla/websocket
package upgrades an HTTP server connection to the WebSocket protocol and allows you to define the parameters of the upgrade. After that, your HTTP connection is a WebSocket connection, which means that you will not be allowed to execute statements that work with the HTTP protocol.
We have to first define a WebSocket UpGrader. This will store details about our WebSocket connection, such as the Read and Write buffer sizes:
var upgrader = websocket.Upgrader{
ReadBufferSize: 1024,
WriteBufferSize: 1024,
}
Upgrading the HTTP Connection
Now that we have the upgrader, we can try to upgrade the incoming HTTP connection. Input the Response Writer and the pointer to the HTTP Request into the UpGrade()
function, which will return a pointer to a WebSocket connection or an error if the upgrade was unsuccessful:
websocket, err := upgrader.Upgrade(w, r, nil)
if err != nil {
log.Println(err)
return
}
Listening on the connection continuously
After that, we'll want to put in place a function that keeps an eye out for any messages that come in via that WebSocket connection. Call the connection's WriteMessage
and ReadMessage
methods to send and receive messages as a slice of bytes:
func listen(conn *websocket.Conn) {
for {
// read a message
messageType, messageContent, err := conn.ReadMessage()
timeReceive := time.Now()
if err != nil {
log.Println(err)
return
}
// print out that message
fmt.Println(string(messageContent))
// reponse message
messageResponse := fmt.Sprintf("Your message is: %s. Time received : %v", messageContent, timeReceive)
if err := conn.WriteMessage(messageType, []byte(messageResponse)); err != nil {
log.Println(err)
return
}
}
}
Full WebSocket server code
The code below shows the full version of the WebSocket server in Golang. For testing purposes, we will use Postman as the WebSocket client. The server will send back the content of request and time it received the message:
package main
import (
"fmt"
"log"
"net/http"
"time"
"github.com/gorilla/websocket"
)
func main() {
var upgrader = websocket.Upgrader{
ReadBufferSize: 1024,
WriteBufferSize: 1024,
}
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
websocket, err := upgrader.Upgrade(w, r, nil)
if err != nil {
log.Println(err)
return
}
log.Println("Websocket Connected!")
listen(websocket)
})
http.ListenAndServe(":8080", nil)
}
func listen(conn *websocket.Conn) {
for {
// read a message
messageType, messageContent, err := conn.ReadMessage()
timeReceive := time.Now()
if err != nil {
log.Println(err)
return
}
// print out that message
fmt.Println(string(messageContent))
// reponse message
messageResponse := fmt.Sprintf("Your message is: %s. Time received : %v", messageContent, timeReceive)
if err := conn.WriteMessage(messageType, []byte(messageResponse)); err != nil {
log.Println(err)
return
}
}
}
Output:
Setting up Golang Websocket Client
This subsection shows how to program a WebSocket client in Go. The client reads user data that sends it to the server and reads the server response. As with the WebSocket server, the gorilla/websocket package is going to help us develop the WebSocket client.
Read from input
The code of ./client/client.go is as follows:
package main
import (
"bufio"
"fmt"
"log"
"net/url"
"os"
"os/signal"
"syscall"
"time"
"github.com/gorilla/websocket"
)
var SERVER = ""
var PATH = ""
var TIMESWAIT = 0
var TIMESWAITMAX = 5
var in = bufio.NewReader(os.Stdin)
The in
 variable is just a shortcut for bufio.NewReader(os.Stdin)
.
func getInput(input chan string) {
result, err := in.ReadString('\n')
if err != nil {
log.Println(err)
return
}
input <- result
}
The getInput()
 function, which is executed as a goroutine, gets user input
that is transferred to the main()
 function via the input channel. Each time the program reads some user input, the old goroutine ends and a new getInput()
 goroutine begins in order to get new input.
func main() {
arguments := os.Args
if len(arguments) != 3 {
fmt.Println("Need SERVER + PATH!")
return
}
SERVER = arguments[1]
PATH = arguments[2]
fmt.Println("Connecting to:", SERVER, "at", PATH)
interrupt := make(chan os.Signal, 1)
signal.Notify(interrupt, os.Interrupt)
Handle Unix Interrupts
The WebSocket client handles UNIX interrupts with the help of the interrupt channel. When the appropriate signal is caught (syscall.SIGINT
), the WebSocket connection with the server is closed with the help of the websocket.CloseMessage
message.
input := make(chan string, 1)
go getInput(input)
URL := url.URL{Scheme: "ws", Host: SERVER, Path: PATH}
c, _, err := websocket.DefaultDialer.Dial(URL.String(), nil)
if err != nil {
log.Println("Error:", err)
return
}
defer c.Close()
The WebSocket connection begins with a call to websocket.DefaultDialer.Dial()
. Everything that goes to the input channel is transferred to the WebSocket server using the WriteMessage()
 method.
done := make(chan struct{})
go func() {
defer close(done)
for {
_, message, err := c.ReadMessage()
if err != nil {
log.Println("ReadMessage() error:", err)
return
}
log.Printf("Received: %s", message)
}
}()
Reading message
Another goroutine, which this time is implemented using an anonymous Go function, is responsible for reading data from the WebSocket connection using the ReadMessage()
 method.
for {
select {
case <-time.After(4 * time.Second):
log.Println("Please give me input!", TIMESWAIT)
TIMESWAIT++
if TIMESWAIT > TIMESWAITMAX {
syscall.Kill(syscall.Getpid(), syscall.SIGINT)
}
The syscall.Kill(syscall.Getpid(), syscall.SIGINT)
 statement sends the interrupt signal to the program using Go code. According to the logic of client.go
, the interrupt signal makes the program close the WebSocket connection with the server and terminate its execution. This only happens if the current number of timeout periods is bigger than a predefined global value.
case <-done:
return
case t := <-input:
err := c.WriteMessage(websocket.TextMessage, []byte(t))
if err != nil {
log.Println("Write error:", err)
return
}
TIMESWAIT = 0
If you get user input, the current number of the timeout periods (TIMESWAIT) is reset and new input is read.
go getInput(input)
case <-interrupt:
log.Println("Caught interrupt signal - quitting!")
err := c.WriteMessage(websocket.CloseMessage, websocket.FormatCloseMessage(websocket.CloseNormalClosure, ""))
Add Close Message
Just before we close the client connection, we send websocket.CloseMessage
 to the server in order to do the closing the right way.
if err != nil {
log.Println("Write close error:", err)
return
}
select {
case <-done:
case <-time.After(2 * time.Second):
}
return
}
}
}
Full websocket client code
Here is the complete golang websocket client code:
package main
import (
"bufio"
"fmt"
"log"
"net/url"
"os"
"os/signal"
"syscall"
"time"
"github.com/gorilla/websocket"
)
var SERVER = ""
var PATH = ""
var TIMESWAIT = 0
var TIMESWAITMAX = 5
var in = bufio.NewReader(os.Stdin)
func getInput(input chan string) {
result, err := in.ReadString('\n')
if err != nil {
log.Println(err)
return
}
input <- result
}
func main() {
arguments := os.Args
if len(arguments) != 3 {
fmt.Println("Need SERVER + PATH!")
return
}
SERVER = arguments[1]
PATH = arguments[2]
fmt.Println("Connecting to:", SERVER, "at", PATH)
interrupt := make(chan os.Signal, 1)
signal.Notify(interrupt, os.Interrupt)
input := make(chan string, 1)
go getInput(input)
URL := url.URL{Scheme: "ws", Host: SERVER, Path: PATH}
c, _, err := websocket.DefaultDialer.Dial(URL.String(), nil)
if err != nil {
log.Println("Error:", err)
return
}
defer c.Close()
done := make(chan struct{})
go func() {
defer close(done)
for {
_, message, err := c.ReadMessage()
if err != nil {
log.Println("ReadMessage() error:", err)
return
}
log.Printf("Received: %s", message)
}
}()
for {
select {
case <-time.After(4 * time.Second):
log.Println("Please give me input!", TIMESWAIT)
TIMESWAIT++
if TIMESWAIT > TIMESWAITMAX {
syscall.Kill(syscall.Getpid(), syscall.SIGINT)
}
case <-done:
return
case t := <-input:
err := c.WriteMessage(websocket.TextMessage, []byte(t))
if err != nil {
log.Println("Write error:", err)
return
}
TIMESWAIT = 0
go getInput(input)
case <-interrupt:
log.Println("Caught interrupt signal - quitting!")
err := c.WriteMessage(websocket.CloseMessage, websocket.FormatCloseMessage(websocket.CloseNormalClosure, ""))
if err != nil {
log.Println("Write close error:", err)
return
}
select {
case <-done:
case <-time.After(2 * time.Second):
}
return
}
}
}
Output:
# go run client.go localhost:8080 ws Connecting to: localhost:8080 at ws Hello there! 2022/10/31 22:59:57 Received: Your message is: Hello there! . Time received : 2022-10-31 22:59:57.100625575 +0530 IST m=+7.521723707 2022/10/31 23:00:01 Please give me input! 0 How are you? 2022/10/31 23:00:04 Received: Your message is: How are you? . Time received : 2022-10-31 23:00:04.272236618 +0530 IST m=+14.693334766 2022/10/31 23:00:08 Please give me input! 0 2022/10/31 23:00:12 Please give me input! 1 2022/10/31 23:00:16 Please give me input! 2 2022/10/31 23:00:20 Please give me input! 3 2022/10/31 23:00:24 Please give me input! 4 2022/10/31 23:00:28 Please give me input! 5 2022/10/31 23:00:28 Caught interrupt signal - quitting! 2022/10/31 23:00:28 ReadMessage() error: websocket: close 1000 (normal)
Interacting with the WebSocket server produces the same kind of output as you can see we received for Hello there!
and How are you?
. The last lines show how the automatic timeout process works.
Summary
We were able to go over some of the fundamentals of WebSockets in this tutorial, along with how to create a straightforward Go application that uses WebSockets. WebSocket gives you an alternative way of creating services. As a rule of thumb, WebSocket is better when we want to exchange lots of data, and we want the connection to remain open all the time and exchange data in full-duplex. However, if you are not sure about what to use, begin with a TCP/IP service and see how it goes before upgrading it to the WebSocket protocol. Additionally, WebSocket is handy when you must transfer lots of data.
References
https://en.wikipedia.org/wiki/WebSocket
https://pkg.go.dev/net/http
https://www.postman.com/