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.