How to use the jsonrpc codec with websockets in Go

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.

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/client/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.