How to use the jsonrpc codec with websockets in Go

 Encoding JSON-RPC Messages Using WebSockets in Go: A Practical Guide

WebSockets and JSON-RPC are powerful technologies that can significantly enhance real-time communication and data exchange in web applications. WebSockets provide full-duplex communication channels over a single TCP connection, while JSON-RPC is a lightweight remote procedure call protocol based on JSON. In this article, we'll explore how to leverage these technologies together to implement an efficient and scalable server in Go.

Duplex communication


Understanding JSON-RPC

JSON-RPC is a simple yet effective protocol for remote procedure calls over HTTP or other transport layers. It allows clients to invoke methods on a server and receive responses in a structured JSON format. The protocol is lightweight, language-agnostic, and easy to implement, making it an excellent choice for web applications.

We are going to use the version JSON-RPC 2.0 which follows this format:


     {"jsonrpc":"2.0","method":"HelloService.Hello","params":{"msg":"John"},"id":0}
    


Using WebSockets in Go

WebSockets enable bidirectional communication between clients and servers, allowing real-time data transfer. In Go, the standard library provides support for WebSockets through the golang.org/x/net/websocket package. There are other more advanced packages for WebSockets, like the gorilla/websocket package, but I'll stick to the standard one for this example, as it is simpler.


Implementing a JSON-RPC Server using WebSockets in Go

For this example, let's create a simple JSON-RPC server in Go that communicates with clients over WebSockets.

Step 1: Define the service

Make sure you have Go installed and set up a Go workspace. Create a new directory for the project, and inside it, create the Go file named internal/service/hello.go containing the definition of the service that the server is going to expose through a WebSockets interface.


package service

import (
	"log"
)

type HelloService struct{}

type HelloRequest struct {
	Name string
}

type HelloResponse struct {
	Greeting string `json:"string"`
}

func (s *HelloService) Hello(req *HelloRequest, res *HelloResponse) error {
	log.Println("Execute method: HelloService.Hello()")
	res.Greeting = "Hello: " + req.Name
	return nil
}
    

Step 2: Implementing the JSON-RPC Server

Then we create the file cmd/server/main.go file to set up a WebSocket server and handle incoming JSON-RPC requests.


package main

import (
	"bytes"
	"golang.org/x/net/websocket"
	"io"
	"io/ioutil"
	"log"
	"net/http"
	"net/rpc"
	"net/rpc/jsonrpc"

	"go-websocket-jsonrpc/internal/service"
)

func wsHandleRequest(ws *websocket.Conn) {
	for {
		var req []byte
		err := websocket.Message.Receive(ws, &req)
		if err != nil {
			log.Println("ReadMessage:", err)
			return
		}

		log.Println("ServeRequest...")
		var res bytes.Buffer
		err = rpc.ServeRequest(jsonrpc.NewServerCodec(struct {
			io.ReadCloser
			io.Writer
		}{
			ioutil.NopCloser(bytes.NewReader(req)),
			&res,
		}))
		if err != nil {
			log.Println("ServeRequest:", err)
			return
		}

		err = websocket.Message.Send(ws, res.Bytes())
		if err != nil {
			log.Println("WriteMessage:", err)
			return
		}
	}
}

func main() {
	log.Println("Starting http server")

	rpc.Register(&service.HelloService{})

	http.Handle("/ws", websocket.Handler(wsHandleRequest))
	http.ListenAndServe("localhost:8080", nil)
}
    

Step 3: Implementing the JSON-RPC Client

Finally we create the file cmd/clien/main.go to implement a Go WebSocket client that reads strings from the command line, sends them to the server, and prints out the responses.


package main

import (
	"bufio"
	"golang.org/x/net/websocket"
	"log"
	"net/rpc"
	"net/rpc/jsonrpc"
	"os"

	"go-websocket-jsonrpc/internal/service"
)

func sayHello(c *rpc.Client, name string) {
	req := service.HelloRequest{Name: name}
	var res service.HelloResponse

	err := c.Call("HelloService.Hello", req, &res)
	if err != nil {
		log.Fatal("error:", err)
	}
	log.Printf("Response: %s", res.Greeting)
}

func main() {
	ws, err := websocket.Dial("ws://localhost:8080/ws", "", "http://localhost/")
	if err != nil {
		log.Fatal(err)
	}
	defer ws.Close()

	c := jsonrpc.NewClient(ws)

	reader := bufio.NewReader(os.Stdin)
	for {
		text, _ := reader.ReadString('\n')
		sayHello(c, text)
	}
}
    

Step 4: Test the example

Now that we have the server and client ready, we start the server in one terminal, and the client on another one. Every time we type a string in the client terminal and press Enter, the client will send that string to the server, and we will see right away the response from the server.


>./bin/client
./bin/client
john
2023/08/02 05:27:57 Response: Hello: john
lisa
2023/08/02 05:28:07 Response: Hello: lisa
tom
2023/08/02 05:28:08 Response: Hello: tom


Conclusion

WebSockets and JSON-RPC can work harmoniously together, providing an efficient and real-time communication mechanism for web applications. In this article, we explored how to implement a JSON-RPC server using WebSockets in Go. You can build upon this example to create more sophisticated applications with real-time updates and bidirectional data flow. Whether it's real-time chats, live notifications, or dynamic dashboards, the combination of WebSockets and JSON-RPC in Go is a powerful solution for modern web development. 

Popular posts from this blog

How to setup NeoVim configuration file

WebAssembly (Wasm): Fixing the Flaws of Applets

How to query Outlook from PowerShell