REST to NATS Proxy
As microservices architectures are increasingly optimized for speed and simplicity, developers are increasingly using NATS—a lightweight high-performance, cloud-native messaging system—for inter-service communication. An interesting project developed in the NATS community bridges HTTP and NATS, and is explained in this article by Radomir Sohlic.
Join the DZone community and get the full member experience.
Join For Freethe rest to nats proxy project ( sohlich/nats-proxy ) is the micro framework that provides a bridge between http and nats. to introduce the problem, we first compare the http and nats communication models. the table below represents the matching of http and nats concepts and what do they provide.
http | nats | |
---|---|---|
synchronous communication | request/response | request/reply |
real-time asynchronous communication | websocket | publish/subscribe |
as you can see, nats provides both synchronous and asynchronous communication between clients. the synchronous communication, represented by simple request and response of http protocol, could be matched with the request/reply communication model of nats. as the documentation for “request reply” model describes: each request sent via nats contains reply subject, to which the reply is sent. the asynchronous, let’s say real-time communication, can be represented by websockets on http side.the truth is that it is not really related to http, but if we simplify it, at least the handshake is based on http. for this purpose, the publish/subscribe model could be used.
so the rest to nats project uses this similarity between nats and http communication and tries to implement the bridge between http(websockets) and nats in such way. the library was originally created for the purpose of migrating rest based architecture like this:
...into a nats messaging platform-based one. but as it evolved, it started to grow into some kind of framework, that can be used for the creation of service api and seamless protocol bridging. so one of the many examples of how the system using a nats-proxy framework could look like is in the architecture below:
proxy basics
as the name of the project suggests the function of the library is very similar to basic http proxy. the proxy receives the http request and translates it into a struct, which contains all the information from original request (url, header, body).
type request struct {
url string
method string
header http.header
form url.values
remoteaddr string
body []byte
}
this struct is serialized and sent as a message through nats via request (see http://nats.io/documentation/concepts/nats-req-rep/ ) to ensure synchronous processing. the subject, to which the serialized struct is sent, is constructed from the http request url and method by a very simple rule: slashes in the path are replaced by dots and the method is used as the prefix.
-
let's say we have get request on url
http://example.com/user/info
so the proxy will translate this url to subjectget:user.info
.
the client side is subscribed to the subject
get:user.info
. because of that, it receives the request and writes back the response to the reply subject. the response struct also contains the body, status, and header.
type response struct {
header http.header
statuscode int
body []byte
}
for a better picture of how it works in reality, there is the code of a simple client and proxy.
proxy
the proxy side implements the
http.handler
interface, so it can be used with a built-in
http
package as you can see in the code below. the handler does nothing special. it parses the request and translates it to custom representation which is then serialized to json by built-in
json
package encoder.
import(
"gopkg.in/sohlich/nats-proxy.v1"
"net/http"
"github.com/nats-io/nats"
)
func main() {
proxyconn, _ := nats.connect(nats.defaulturl)
proxy, _ := natsproxy.newnatsproxy(proxyconn)
defer proxyconn.close()
http.listenandserve(":8080", proxy)
}
the proxy itself does not implement any mechanisms to apply filters before the request is passed to the proxy handler as this could be implemented by decorating the proxy handler or other similar techniques. because the implementation does not allow writing data to the
http.responsewriter
after the handler is applied, the proxy provides
natsproxy.hook
interface. this hook is applied on the response before it is written to
http.responsewriter
. the example bellow shows the usage of hook to translate jwt token with all user info to meaningless reference token.
proxyhandler.addhook(".*", func(r *response) {
// exchange the jwt token for
// reference token to hide user information
jwt := r.getheader().get("x-auth")
reftoken := auth.gettokenfor(jwt)
r.getheader().set("x-auth", reftoken)
})
client
the client code uses the nats connection as the constructor argument, so all available options for configuring the connection are accessible. the client itself uses the asynchronous subscription to handle incoming messages, so it’s behavior similar to
http.handlerfunc
. the client api and internals are heavily inspired by
gin gonic
project. the sample code shows how to use the client api.
import(
"gopkg.in/sohlich/nats-proxy.v1"
"net/http"
"github.com/nats-io/nats"
)
func main(){
clientconn, _ := nats.connect(nats.defaulturl)
natsclient, _ := natsproxy.newnatsclient(clientconn)
//subscribe to url /user/info
natsclient.get("/user/info", func(c *natsproxy.context) {
user := struct {
name string
}{
"alan",
}
c.json(200, user)
})
defer clientconn.close()
// waiting for signal to close the client
sig := make(chan os.signal, 1)
signal.notify(sig, syscall.sigint, syscall.sigterm)
fmt.println("press ctrl+c for exit.")
<-sig
}
the client api naturally provides the subscription for other http methods and generic subscription method. the request handler which implements
natsproxy.natshandler
interface uses
natsproxy.context
struct which encapsulates both request and response and provides some useful methods to access request data and to write a response.
//get
natsclient.get("/user/info", func(c *natsproxy.context) {
c.json(200, "hello")
})
//post
natsclient.post("/user/info", func(c *natsproxy.context) {
c.json(200, "hello")
})
//put
natsclient.put("/user/info", func(c *natsproxy.context) {
c.json(200, "hello")
})
//delete
natsclient.delete("/user/info", func(c *natsproxy.context) {
c.json(200, "hello")
})
//general method
natsclient.subscribe("head","/user/info", func(c *natsproxy.context) {
c.json(200, "hello")
})
the client also implements middleware function that provides the means of accessing the request before it is handled by the specific handler. the reason behind this feature is to provide options for security checks, logging, etc. the example shows the implementation of middleware, that logs all incoming requests.
natsclient.use(func logger(c *natsproxy.context) {
log.infof("%s:%s from %s", c.request.method, c.request.url)
})
summary
the first version (v1) of the nats-proxy framework implements only http request/response proxying. because it started as a quick proof of concept, there is some additional optimization work needed, and all the work is done on the proxy side. also, the serialization of structs is done by json encoder, which does not provide very fast serialization. however for the purpose of bridging rest (http) requests to nats messaging platform, it’s enough to make it possible.
currently, the next version (v2) is under development. the v2 should bring some performance improvements because the serialization is done via protocol buffers. also, a lot of work originally done on the proxy side was moved to client(service) side. the next significant feature coming is websocket support.
if you are interested or have some ideas, see the rest to nats proxy project and file an issue.
Published at DZone with permission of Radomir Sohlich. See the original article here.
Opinions expressed by DZone contributors are their own.
Comments