How to use the jsonrpc codec with websockets in Go
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.
Subscribe to my newsletter
Read articles from Alvaro Leal directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by
Alvaro Leal
Alvaro Leal
Hi, I'm Alvaro. I'm a software engineer working in the space industry. I've worked on many science missions for the European Space Agency like Bepi Colombo, Exomars and Juice. As well as on several satellite telecom projects. Currently I'm working on the Galileo Spacecraft Constellation Control Facility. I have been working on the Galileo program since 2018. It is a global navigation satellite system (GNSS) developed by the European Union (EU) and the European Space Agency (ESA). Its purpose is to provide accurate positioning, navigation, and timing information to users worldwide. In this site I publish articles about programming and software development.