On top of developing runtime application self-protection tools, the Prevoty Engineering team is always looking for better technologies to manage our cloud and on-premise service-oriented architectures. A package of the Go standard library that we use extensively is
net/rpc - this particular package simplifies the approach and LOC when it comes to developing your own RPC. If you're unfamiliar with Go, then I highly recommend that you take the tour - it's worth it.
In this post, I'll walk you through the construction of a primitive key/value cache, complete with client and server implementations that communicate over TCP. We'll also write unit tests to see how our the overall system works. The best part: no external dependencies are required.
This project, called
rpc_tutorial has been pushed to a public GitHub repository.
Before we write a server or client implementation, we need to define a few things - types and functions (found in rpc.go). We'll setup three types below:
RPC structure allows us to encapsulate and re-use cache functions that augment stored keys/values. Within
RPC, we define a field called
cache. For the purpose of this post, we'll stick to using simple in-memory maps. You'll also see the usage of a mutex as we want to make things relatively thread-safe.
CacheItem is a structure that contains the key/value that will shuttled between our clients and servers.
Requests is just a structure of integers that we'll be incrementing from our RPC functions. We'll also be able to return this structure to clients.
Our RPC constructor is straight-forward:
Now that we've defined our primitives and RPC constructor, we'll need to implement the functions we need to make this cache, well, a cache. We'll keep things simple and only support the following functions:
Get - returns a cache item for the given key
Put - stores a cache item, acknowledges storage
Delete - deletes a cache item for the given key, acknowledges deletion
Clear - clears all cache items, acknowledges clear
Stats - returns basic cache statistics
One caveat to using
net/rpc is that your RPC functions have to adhere to a particular signature: only two arguments are allowed, the second argument must be a pointer (used in our code to return acknowledgements/data to clients) and an
error is always returned.
Now that our RPC and its functions have been defined we can develop our first server implementation (see file server.go).
...and that's all it takes to get a minimal server implementation! We register our cache RPC, which is returned back from the constructor we just wrote in
rpc.go. For our example, we'll have our RPC listen via TCP on port 9876.
At this point, you should be able to do a
go build without running into any errors and have a lonely cache server waiting for some action.
Building a client (see client.go) is a little more involved than the server implementation. We'll start by creating a
dsn is going to be in the form of
timeout is going to express a timeout for the client to connect to the server.
Implementing the client's functions are going to mirror what we setup in
Notice how we invoke a remote function in the form of
Type.Function with the variables that correspond to the
net/rpc signature that we covered above.
Testing + Wrap-Up
Now that we've got the server/client pair, let's put it together via some simple tests (see file client_test.go). Here is the test setup:
As you can see, we'll use the newly created RPC client constructor to instantiate a cache client that we'll use for our tests. Instead of showing all of the unit tests, I'll highlight the significant bits:
Since these tests will execute linearly, we've attempted to model a lifecycle: starting with a cold get, which will fail, to a working cache put and a succesful get. Now that we've got these tests, let's start the server and run our tests just by using the `go test` command.
You should see the following in your terminal:
...and there you have it - a fully working cache using
net/rpc! Best part was that we didn't have to handle serialization, de-serialization or even network transport. Those implementation details are handled by Go's standard library.
As mentioned above, this project can be found in our public GitHub repository. Feel free to fork the code or send any pull requests our way.