In the Information Exchange Model, the client sends a request, and the server responds to the client. The type of data/information exchange is based on a specific format that lets both sides communicate with each other. Presently most applications do not use this model to share but instead call services, as they would call any function.
Remote Procedure Call(RPC) was intended to be the function model for networked systems. Client execute RPC like they call a native function, except client package the function parameter and send them through the network to the server. The server can then unpack this parameter and process the request, executing results to the client.
What is RPC
A Remote Procedure Call is an interprocess communication that exchanges data/information between the sender and receiver in a distributed system. RPC is used in client/server architecture for accessible communication. The primary objective of RPC is to develop the physical and logical layers.
The following list is a basic overview of how the RPC process works:
- The RPC client puts together the function and argument required to be sent to the server
- The RPC server receives them by dialling the connection
- The RPC server executes the remote process
- The RPC server responded to the client with the result
- The RPC client gets the response from the request duly and uses it then, it moves to the next task.
Prerequisites
Before getting started, ensure you have the following prerequisites:
- Basic knowledge of Go
- Go installed on your machine
Step 1: Setting Up Your Project
To begin building your project, follow these steps to set up your project:
Create a new directory for your project, open your terminal, and run the following command:
mkdir -p go-RPC
This will create a new folder name go-RPC
in your current directory.
Navigate to the project director by running the following command:
cd go-RPC
Now you are inside the go-RPC
directory.
Initialize your new project by running the following command:
go mod init go-RPC
This command creates a new go.mod
file, your project's configuration, and the dependency management file.
Following these steps, you will have a new project directory with a go.mod
file ready.
This project will have two sub-directory; follow the steps in the preceding to create the sub-directory. You will have an RPC server and RPC client folder that looks like this.
client-server-RPC
├── client
│ └── main.go
└── server
└── main.go
Step 2: Create The RPC Server
You will create an RPC server to send UTC server time to the RPC client. To build the RPC server, go has two libraries, net/rpc
and net/http
.
package main
import (
"log"
"net"
"net/http"
"net/rpc"
"time"
)
type Args struct{}
type TimeServer int64
func (t *TimeServer) GiveServerTime(args *Args, reply *int64) error {
// Fill reply pointer to send the data back
*reply = time.Now().Unix()
return nil
}
func main() {
// Create a new RPC server
timeserver := new(TimeServer)
// Register RPC server
rpc.Register(timeserver)
rpc.HandleHTTP()
// Listen for requests on port 1234
l, e := net.Listen("tcp", ":2233")
if e != nil {
log.Fatal("listen error:", e)
}
http.Serve(l, nil)
}
Explanation:
The Args
struct holds information about arguments passed from the client (RPC) to the server. TimeServer number to register with the rpc.Register
. Here, the server wishes to export an object of type TimeServer(int64). HandleHTTP registers an HTTP handler for RPC messages to DefaultServer. Then you started a TCP server that listens on port 2233—the http.Serve
function is used to serve it as a running program. GiveServerTime
is the client's function, and the current server time is returned.
Here are a few points to note:
-
Args struct
has no field because the server code is not expecting any argument from the client - GiveServerTime takes the Args objects and replies pointer object
- it sets the reply pointer object
- It serves on port 2233
Step 3: Create The RPC Client
You'll create the client to call your server and handle the response here. It also uses the same net/rpc
library but with a different method to dial the server.
package main
import (
"log"
"net/rpc"
)
type Args struct {
}
func main() {
var reply int64
args := Args{}
client, err := rpc.DialHTTP("tcp", "localhost"+":2233")
if err != nil {
log.Fatal("dialing:", err)
}
err = client.Call("TimeServer.GiveServerTime", args, &reply)
if err != nil {
log.Fatal("arith error:", err)
}
log.Printf("%d", reply)
}
Explanation:
The client DialHTTP
to connect to the RPC server, which is running on port 2233. Call the Remote function with the Name: function format with args
and reply
with the pointer object, the client get the data collected into the reply object and call function is sequential.
Note: The client is running as an independent program. Both programs can be on different machines, and the computing can still be shared. This is the core concept of distributed systems. The tasks are divided and given to various RPC servers. Finally, the client collects the results and uses them for further actions. Custom RPC code is only valid when the client and server are written in Go. So to have the RPC server consumed by multiple services, we need to define the JSON RPC over HTTP. Then, any other programming language can send a JSON string and get JSON.
Now you will start the server.go
to the program:
go run server.go
Open another terminal tab to start the client.go program:
go run client.go
The client console will give an output of the current UTC.
JSON RPC Using Gorilla RPC
Gorilla also provides an RPC library. It makes it easier for the client and server to be in the same file. You can send a request as JSON and receive a response as JSON.
This section will be a real-world use case. You have a JSON file on the server with Twitter profile details (Name, username, followers, and following). The client requests Twitter information by making an HTTP request. When the RPC server receives the request, it reads the file from the filesystem and parses it. If the Name matches any profile, the server returns the information to the client in JSON format.
You will install Gorilla RPC and Mux library with the following command:
go get github.com/gorilla/rpc
go get github.com/gorilla/mux
Your folder structure should look like this.
JSON-RPC
├── go.mod
├── go.sum
├── twitterProfile.json
└── server
└── main.go
The JSON data file
Here is a sample data file with a twitterprofile
details structure in it.
[
{
"name": "Name cannot be black",
"username": "@hackSultan",
"followers": "187k",
"following": "974"
},
{
"name": "Scott Hanselman",
"username": "@shanselman",
"followers": "317k",
"following": "10.5k"
},
{
"name": "Odogwu Machalla",
"username": "@unicodeveloper",
"followers": "97k",
"following": "15.5k"
},
{
"name": "Gbedilodo",
"username": "@olodocoder",
"followers": "252",
"following": "143"
},
{
"name": "Angie Jones",
"username": "@techgirl908",
"followers": "114k",
"following": "626"
}
]
Implementing The RPC JSON Server
In this example, you must make a few changes to the preceding server example. Including Gorilla
libraries in this example.
package main
import (
jsonparse "encoding/json"
"io/ioutil"
"log"
"net/http"
"os"
"github.com/gorilla/mux"
"github.com/gorilla/rpc"
"github.com/gorilla/rpc/json"
)
// Args holds arguments passed to JSON RPC service
type Args struct {
Name string
}
// TwitterProfie struct holds TwitterProfile JSON structure
type TwitterProfile struct {
Name string `json:"name,omitempty"`
Username string `json:"username,omitempty"`
Followers string `json:"followers,omitempty"`
Following string `json:"following,omitempty"`
}
type JSONServer struct{}
// TwitterProfileDetail
func (t *JSONServer) TwitterProfileDetail(r *http.Request, args *Args, reply *TwitterProfile) error {
var twitterprofiles []TwitterProfile
// Read JSON file and load data
raw, readerr := ioutil.ReadFile("./twitterprofile.json")
if readerr != nil {
log.Println("error:", readerr)
os.Exit(1)
}
// Unmarshal JSON raw data into twitterprofiles array
marshalerr := jsonparse.Unmarshal(raw, &twitterprofiles)
if marshalerr != nil {
log.Println("error:", marshalerr)
os.Exit(1)
}
// Iterate over each twitterprofile to find the given twitterprofile
for _, twitterprofile := range twitterprofiles {
if twitterprofile.Id == args.Id {
// If twitterprofile found, fill reply with it
*reply = twitterprofile
break
}
}
return nil
}
func main() {
// Create a new RPC server
s := rpc.NewServer() // Register the type of data requested as JSON
s.RegisterCodec(json.NewCodec(), "application/json")
// Register the service by creating a new JSON server
s.RegisterService(new(JSONServer), "")
r := mux.NewRouter()
r.Handle("/rpc", s)
http.ListenAndServe(":9000", r)
}
Open a terminal to start the server program:
go run main.go
Explanation:
The args
and TwitterProfile
structs contain information about the JSON arguments passed and the profile structure. You define a remote function called TwitterProfileDetail on a resource called JSONServer
. This struct is a service created to register with the RegisterService function of the RPC server. If you notice, you also write the codec as JSON. Whenever you get a request from the client, You load the JSON file called twitterprofile.json
into memory and then into the TwitterProfile struct using JSON's Unmarshal method. jsonparse
is the alias given for the Go package encoding/json
because the JSON package from the Gorilla import has the same Name to remove conflict, use an alias instead.
In the remote function, you set the value of the reply with the matched twiterprofile
. The data is filled if the client's name matches any of the twitterprofiles
in JSON. If there is no match, the RPC server will return empty data. This way, you can create a JSON RPC to make clients universal. Here, no Go client was written. Any client can access data from the service.
Now that the server is running, you must interact with the client. The client can be a CRUL command since the RPC server is serving a request over HTTP. The twitterprofile
name is required to get the details.
Open another terminal with the following command to execute the CRUL request:
curl -X POST \
http: //localhost:9000/rpc \
-H 'cache-control: no-cache' \
-H 'content-type: application/json' \
-d '{
"method": "JSONServer.TwitterProfileDetail",
"params": [
{
"name":"Name cannot be blank"
}
],
"id": "1"
}'
Conclusion
You've learned what an RPC is and how an RPC server and client can be built. The JSON RPC is used as a use case, and the use case JSON RPC uses the Gorilla toolkit. Now you can easily create RPC in your Golang project.
For a better understanding, kindly check more articles on RPC. I hope you enjoy this article. Leave a comment on the part you didn’t understand and smash the like button.
Note:
Prefer JSON RPC when multiple client technologies need to connect to your service.
You can view the source code here.
I'd love to connect with you on. Twitter | Linkedin | Github
Top comments (4)
Cool write up
Thank you boss!!!!
Thank you for the article. I found the examples very useful as starting point for understanding how to build a project with RPC.
To make it even better may I suggest three additions?
In the first example, the command to execute would be ...
go run server/main.go &
go run client/main.go