Connecting a Ktor web server to a Corda node
The preparation for this blog post began several weeks ago (probably over a month by now). Before I could write about melding Corda and Ktor together, I first needed to lay the groundwork and focus solely on Ktor. That is where my blog post, Ktor – a Kotlin web framework came into existence. If you haven’t used or seen Ktor before, I recommend taking a browse at that post either before or after reading this post. Reading it in advance is probably a better idea, but you are in control of your own life 🤷.
This post will focus on implementing a Ktor web server that connects to a Corda node. I am not going to talk about why you should use Ktor. That decision is up to you. What I am doing is providing you with some information and allowing you to formulate a decision by yourself (like anything you read on the internet 🙄).
Dependencies
buildscript { ext.ktor_version = '1.2.2' ext.kotlin_version_for_app = '1.3.41' repositories { mavenCentral() jcenter() } dependencies { classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version_for_app" } } apply plugin: 'java' apply plugin: 'kotlin' java { disableAutoTargetJvm() } dependencies { compile "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version_for_app" compile "$corda_release_group:corda-jackson:$corda_release_version" compile "$corda_release_group:corda-rpc:$corda_release_version" compile "$corda_release_group:corda:$corda_release_version" compile "io.ktor:ktor-server-netty:$ktor_version" compile "ch.qos.logback:logback-classic:1.2.3" implementation ("io.ktor:ktor-jackson:$ktor_version") { exclude group: 'com.fasterxml.jackson.module', module: 'jackson-module-kotlin' } compile project(":contracts") compile project(":workflows") }
There are a few things to highlight here. First, the kotlin_version_for_app
property. Ktor requires Kotlin 1.3 (as it uses coroutines) but, at the time of writing, Corda only supports Kotlin 1.2. Therefore, different versions need to be used for the web server code and the Corda node. Secondly, jackson-module-kotlin
is excluded as it causes a runtime error due to a version mismatch.
High level look at the code
Below is a small snippet of code showing the starting point of the web server’s implementation:
fun main() { embeddedServer( Netty, port = System.getProperty("server.port").toInt(), module = Application::module ).start().addShutdownHook() } fun Application.module() { val connection: CordaRPCConnection = connectToNode() install(CallLogging) { level = Level.INFO } install(ContentNegotiation) { cordaJackson(connection.proxy) } routing { messages(connection.proxy) } addShutdownEvent(connection) }
This code ties everything together as all the functionality of the server branches out from the functions above.
The contents of module
will be explored in the following sections.
Connecting to the node
I bet you might have a good idea what connectToNode
does. I hope you do anyway… Below are the contents of connectToNode
:
fun connectToNode( host: String = System.getProperty("config.rpc.host"), rpcPort: Int = System.getProperty("config.rpc.port").toInt(), username: String = System.getProperty("config.rpc.username"), password: String = System.getProperty("config.rpc.password") ): CordaRPCConnection { val rpcAddress = NetworkHostAndPort(host, rpcPort) val rpcClient = CordaRPCClient(rpcAddress) return rpcClient.start(username, password) }
If you have seen any of the Corda samples, then you will probably be familiar with this piece of code. Long story short, it connects to the node with the given connection details. I chose to generate the function’s default parameters from the application’s system properties. This implementation is not particularly important, it just connects to the node and could be written in several different ways.
A CordaRPCConnection
is returned from the function. Initially, I wanted to return a CordaRPCOps
as the connection itself doesn’t do too much. But, without returning the connection, there is no way to gracefully disconnect from a node. In other words, there needs to be a way to call notifyServerAndClose
when the server stops. This is explored further down in the post.
Setting up Jackson
Some extra setup needs to be done to properly use Jackson with Corda:
fun ContentNegotiation.Configuration.cordaJackson(proxy: CordaRPCOps) { val mapper: ObjectMapper = JacksonSupport.createDefaultMapper(proxy) mapper.apply { setDefaultPrettyPrinter(DefaultPrettyPrinter().apply { indentArraysWith(DefaultPrettyPrinter.FixedSpaceIndenter.instance) indentObjectsWith(DefaultIndenter(" ", "\n")) }) } val converter = JacksonConverter(mapper) register(ContentType.Application.Json, converter) }
The Corda ObjectMapper
is initialised with createDefaultMapper
, allowing classes like Party
or X509Certificate
to be serialised or deserialised. This can be important depending on what is being returned from your own API.
The rest of the code is stolen from the ktor-jackson
module. It alters the JSON output slightly to be more somewhat more desirable.
Creating the endpoints
HTTP requests are routed to these endpoints:
fun Routing.messages(proxy: CordaRPCOps) { route("/messages") { get("/") { call.respond( HttpStatusCode.OK, proxy.vaultQueryBy<MessageState>().states.map { it.state.data }) } post("/") { val received = call.receive<Message>() try { val message = proxy.startFlow( ::SendMessageFlow, state(proxy, received, UUID.randomUUID()) ).returnValue.getOrThrow().coreTransaction.outputStates.first() as MessageState call.respond(HttpStatusCode.Created, message) } catch (e: Exception) { call.respond(HttpStatusCode.InternalServerError, e.message ?: "Something went wrong") } } } }
Logic wise, there is not much going on here. For a focused explanation of this code, I recommend reading Ktor – a Kotlin web framework as I mentioned earlier.
Gracefully disconnecting from a node
To gracefully disconnect from a node, the web server needs to call CordaRPCConnection.notifyServerAndClose
. Implementing this required a bit of work that I wasn’t expecting. Below is the code that triggers notifyServerAndClose
:
fun NettyApplicationEngine.addShutdownHook() { Runtime.getRuntime().addShutdownHook(Thread { stop(1, 1, TimeUnit.SECONDS) }) Thread.currentThread().join() } fun Application.addShutdownEvent(connection: CordaRPCConnection) { environment.monitor.subscribe(ApplicationStopped) { connection.notifyServerAndClose() } }
A shutdown hook is added to the server. As explained in Graceful shutdown of Ktor applications, subscribing to the ApplicationStopped
event is not enough to execute code when terminating the application. The shutdown hook calls stop
to gracefully close the NettyApplicationEngine
that the server runs upon. Leading to the shutdown event being correctly triggered and executed.
That’s all there is
Yes, really, that is all. Implementing a super basic web server does not require much code at all. There isn’t really anything else to write. I have shown you that there is another web framework that can be used to connect to a Corda node. You don’t have to default to Spring just because the Corda samples use them. If you prefer Ktor, use Ktor. If you don’t, don’t. If you did like the look of Ktor, and if you haven’t already, I recommend looking at Ktor – a Kotlin web framework.
A lot of code was excluded from this post as I focused on the more important aspects of the implementation. If you are interested in the rest of the code, you can find it on my GitHub.
Published on Java Code Geeks with permission by Dan Newton, partner at our JCG program. See the original article here: Connecting a Ktor web server to a Corda node Opinions expressed by Java Code Geeks contributors are their own. |