Implementing a Concurrent Websocket Server in Go

Table of Contents

  1. Overview
  2. Prerequisites
  3. Setting Up
  4. Creating the Websocket Server
  5. Handling Incoming Connections
  6. Sending and Receiving Messages
  7. Testing the Server
  8. Conclusion

Overview

In this tutorial, we will learn how to implement a concurrent websocket server in Go. Websockets provide a persistent connection between a client and a server, allowing real-time communication. By the end of this tutorial, you will be able to create a websocket server that can handle multiple client connections concurrently.

Prerequisites

To follow this tutorial, you should have a basic understanding of the Go programming language. Familiarity with networking concepts and concepts related to concurrent programming would be helpful but not necessary.

Setting Up

Before we start, make sure you have Go installed on your machine. You can download it from the official website and follow the installation instructions for your operating system.

Creating the Websocket Server

Let’s start by creating a new Go module called “websocket-server”.

$ go mod init websocket-server

Next, create a new file called “main.go” and open it in your preferred code editor.

$ touch main.go

Add the following code to “main.go”:

package main

import (
	"fmt"
	"log"
	"net/http"

	"github.com/gorilla/websocket"
)

func main() {
	http.HandleFunc("/ws", handleWebsocketConnection)
	log.Fatal(http.ListenAndServe(":8080", nil))
}

func handleWebsocketConnection(w http.ResponseWriter, r *http.Request) {
	// Upgrade HTTP connection to websocket
	upgrader := websocket.Upgrader{}
	conn, err := upgrader.Upgrade(w, r, nil)
	if err != nil {
		log.Println("Failed to upgrade connection:", err)
		return
	}

	// Start a new goroutine to handle the connection
	go handleMessages(conn)
}

func handleMessages(conn *websocket.Conn) {
	defer conn.Close()

	for {
		// Read incoming message from the websocket connection
		_, message, err := conn.ReadMessage()
		if err != nil {
			log.Println("Failed to read message:", err)
			break
		}

		// Print the received message
		fmt.Println("Received message:", string(message))

		// Send a response back to the client
		response := []byte("Server received your message")
		err = conn.WriteMessage(websocket.TextMessage, response)
		if err != nil {
			log.Println("Failed to send response:", err)
			break
		}
	}
}

In the code above, we import the necessary packages, including the “gorilla/websocket” package for WebSocket functionality. We define the main function, which registers a handler for the “/ws” path and starts the HTTP server. The handleWebsocketConnection function upgrades the HTTP connection to a WebSocket connection and starts a new goroutine to handle the connection. The handleMessages function reads incoming messages from the WebSocket connection and sends a response back to the client.

Handling Incoming Connections

To handle incoming WebSocket connections, we need to implement the handleWebsocketConnection function. This function is called when a client requests a WebSocket connection to the “/ws” path.

Inside the handleWebsocketConnection function, add the following code:

func handleWebsocketConnection(w http.ResponseWriter, r *http.Request) {
	// Upgrade HTTP connection to websocket
	upgrader := websocket.Upgrader{}
	conn, err := upgrader.Upgrade(w, r, nil)
	if err != nil {
		log.Println("Failed to upgrade connection:", err)
		return
	}

	// Start a new goroutine to handle the connection
	go handleMessages(conn)
}

In the code above, we use the websocket.Upgrader struct to upgrade the HTTP connection to a WebSocket connection. We pass the Upgrader along with the response writer and the request to the Upgrade function. If the upgrade is successful, we start a new goroutine to handle the WebSocket connection.

Sending and Receiving Messages

To handle sending and receiving messages, we need to implement the handleMessages function. This function reads incoming messages from the WebSocket connection and sends a response back to the client.

Inside the handleMessages function, add the following code:

func handleMessages(conn *websocket.Conn) {
	defer conn.Close()

	for {
		// Read incoming message from the websocket connection
		_, message, err := conn.ReadMessage()
		if err != nil {
			log.Println("Failed to read message:", err)
			break
		}

		// Print the received message
		fmt.Println("Received message:", string(message))

		// Send a response back to the client
		response := []byte("Server received your message")
		err = conn.WriteMessage(websocket.TextMessage, response)
		if err != nil {
			log.Println("Failed to send response:", err)
			break
		}
	}
}

In the code above, we use a loop to continuously read incoming messages from the WebSocket connection. We print the received message and send a response back to the client.

Testing the Server

To test our WebSocket server, we can use a WebSocket client such as “wscat” (WebSocket cat). If you don’t have “wscat” installed, you can install it using NPM:

$ npm install -g wscat

Start the server by running the following command:

$ go run main.go

In a separate terminal window, connect to the server using “wscat”:

$ wscat -c ws://localhost:8080/ws

You can now send messages to the server and see the responses.

Conclusion

In this tutorial, we learned how to implement a concurrent WebSocket server in Go. We covered the basic setup, handling incoming connections, and sending/receiving messages using the “gorilla/websocket” package. You can further extend this server to handle more complex interactions and integrate it with other systems. Websockets are a powerful tool for real-time communication in web applications, and Go provides a convenient way to implement WebSocket servers.

Remember to check out the official Go documentation for more details on the packages and functions used in this tutorial.