Golang Websocket Examples [Server and Client]


GO

Author: Tuan Nguyen
Reviewer: Deepak Prasad

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, ...

Golang Websocket Examples [Server and Client]

 

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:

Golang Websocket Examples [Server and Client]

 

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/

 

Tuan Nguyen

Tuan Nguyen

He is proficient in Golang, Python, Java, MongoDB, Selenium, Spring Boot, Kubernetes, Scrapy, API development, Docker, Data Scraping, PrimeFaces, Linux, Data Structures, and Data Mining. With expertise spanning these technologies, he develops robust solutions and implements efficient data processing and management strategies across various projects and platforms. You can connect with him on his LinkedIn profile.

Can't find what you're searching for? Let us assist you.

Enter your query below, and we'll provide instant results tailored to your needs.

If my articles on GoLinuxCloud has helped you, kindly consider buying me a coffee as a token of appreciation.

Buy GoLinuxCloud a Coffee

For any other feedbacks or questions you can send mail to admin@golinuxcloud.com

Thank You for your support!!

Leave a Comment