Abstract #
Currently, client platforms connect to the server with one of the following methods:
A predefined document, which is common yet cumbersome and prone to errors due to the varying message formats across different routes and
HTTP
methods.An
RPC
(remote procedure call) framework that is hard to getcontext
and requires either:- A third-party language to define shared
interfaces
anddata types
. - A uniform language across all clients and servers, which can be notably restrictive. For example, Java cannot be
used on
browsers
andiOS
.
- A third-party language to define shared
To address these issues, I developed this new RPC
framework named Phone
, based on the increasingly favored
Kotlin Multiplatform
and its web framework Ktor
. Phone
is poised to greatly promote the use of Kotlin Multiplatform
and to be revolutionary together with it.
Phone
offers key advantages:
- It facilitates connections using shared
interfaces
,data types
, and associated logics. - It provides robust support and straightforward configuration for essential features, including various
HTTP
methods,authentication
,WebSocket
,PartialContent
,cryptography
, and automatedserialization
. - It allows for custom extensions through the use of
Route
on servers, andHttpRequestBuilder
andHttpResponse
on clients, which are intuitively powerful due to theirKtor
origin. - It’s compatible with the traditional way. For instance, there may be a client using
C++
. It can connect to the server with the traditionalJson
conversion, manual routes, andHTTP
methods.
It’s not essential to replace allJavaScript
withKotlin-Js
on the browser side, because the generatedPhone.kt
is callable fromJavaScript
.
Core usage sample #
Other platforms are also supported, e.g. iOS
and wasm
.
Setup #
- Kotlin version is required at least
1.9.0
.- Get familiar with Ktor first.
Create three modules of which all could be multiplatform, configure them as below in build.gradle.kts
,
and implement them in server
and each client
.
plugins {
val kt = "1.9.0"
kotlin("multiplatform") version kt
id ("com.google.devtools.ksp") version "$kt-1.0.13"
kotlin("plugin.serialization") version kt
}
repositories {
mavenCentral()
}
kotlin {
jvm{
jvmToolchain(8)
}
js{
browser()
}
// other needed native platforms
sourceSets {
val commonMain by getting{
dependencies{
api("io.github.shawxingkwok:phone-runtime:1.0.0")
implementation("org.jetbrains.kotlinx:kotlinx-serialization-core:1.6.0")
api("org.jetbrains.kotlinx:kotlinx-serialization-json:1.6.0")
}
}
}
}
dependencies {
// other platforms are needless here, because this compiler generates code and copy them to else where.
add("kspJvm", "io.github.shawxingkwok:phone-compiler:1.0.0-1.0.0")
}
ksp{
// set your own local path
arg("phone.server-package-path", "${projectDir.parent}/serverside/src/commonMain/kotlin")
arg("phone.client-package-path", "${projectDir.parent}/clientside/src/commonMain/kotlin")
arg("phone.server-package-name", "pers.shawxingkwok.server.phone")
arg("phone.client-package-name", "pers.shawxingkwok.client.phone")
arg("phone.default-method", "post")
// optional
// arg("phone.jwt-auth-name", "<your jwt auth name>")
// arg("phone.client-phone-modifiers", "internal abstract") // "open" by default
}
plugins {
kotlin("multiplatform") version "1.9.0"
}
repositories {
mavenCentral()
}
kotlin {
jvm{
jvmToolchain(8)
}
// other needed native platforms
sourceSets {
val commonMain by getting{
dependencies{
implementation("io.github.shawxingkwok:phone-runtime:1.0.0"))
api(project(":example:api:shared")) // or the remote
implementation("io.ktor:ktor-server-core:<ktor_version>")
}
}
}
}
plugins {
kotlin("multiplatform") version "1.9.0"
}
repositories {
mavenCentral()
}
kotlin {
jvm{
jvmToolchain(8)
}
js{
browser()
}
// other needed native platforms
sourceSets {
val commonMain by getting{
dependencies{
implementation("io.github.shawxingkwok:phone-runtime:1.0.0"))
api(project(":example:api:shared")) // or the remote
implementation("io.ktor:ktor-server-core:<ktor_version>")
}
}
}
}
Http methods #
The default http method
is indispensable and configured in shared
module build.gradle.kts
.
The options are get
, post
, put
, delete
and patch
which could start with an uppercase char.
The http method
of login
in this case is Post
.
Default methods could be overridden in Phone.Api
. Methods for each function could also be separately set.
Now login
method is Post
, and search
method is Get
.
Calls #
- In this section, all parameters and generic type are not limited.
- Examples run on Jvm. There are multiple
Ktor
ways of handling file stream inbody
. Each platform also handles differently.
Common #
This is the most common case.
Remember to use Unit
if the returned value is needless.
You could also upload files easily as below.
Manual #
With Phone.Call.Manual
, client also gets HttpResponse
which is generally for downloading files.
The generic type is not limited to Pair<String, Long>
.
PartialContent #
Required server plugin:PartialContent
andAutoHeadResponse
Type of the handler
The parameter ranges
is vararg. If you pass no LongRange
, you would get the whole file.
File
/ Stream
/ ByteReadChannel
as before.WebSocket #
Required server and client plugins: WebSockets
You will get the parameter enablesWss
in the Phone
constructor.
Request
I suggest to use onReceivedSuccess
instead.
This function is generated in module clientside
.
You could set isRaw
to true
and get ClientWebSocketSession
and ServerWebSocketSession
on
server and client sides.
Parameter positions #
You may be concerned about the actual parameter positions because URL
is sometimes a bad choice.
Actually, Phone
puts parameters in a form as long as the HTTP
method is not Get
and the HttpRequest body
is empty.
If you set the body before request in this way, the parameters would be put in URL
.
Phone.DemoApi{
setBody(...)
}
...
Crypto #
Cipher #
First you need to provide a Cipher
in module shared
with customed protocol with other
Kotlin multiplatform Crypto libraries.
Targets #
Next you could annotate these symbols with Phone.Crypto
.
- The top
Phone.Crypto
means all messages inDemoApi
will be encrypted. The nextPhone.Crypto
on the functionlogin
means all messages related to this function will be encrypted. - For parameters, annotating it or its type makes the same sense.
- Annotating the returned
Any
makesLoginResult
crypto.
For this login
case, I suggest annotating Phone.Crypto
only on the function.
Serialize third-party types #
Suppose class Time
is from a third-party library and is not adapted with Kotlin Serializable
.
You could set a serializer annotated with Phone.Serializer
.
Note that it’s not as intelligent as the original Kotlin serialization
. A single TimeSerializer
is not enough for cases below.
These are the actually paired serializers.
Extend #
The source code is open
with options.
You could make the generated Phone
internal abstract
instead of open
via ksp args. And then
set a public subclass.
ksp{
arg("phone.client-phone-modifiers", "internal abstract")
}
onStart
is background called as below.
Auth #
Required server plugin:Learn the auth part in Ktor if you are not familiar with it. Then you could get the point in this section easily.Authentication
Required client plugin:Auth
Annotation Phone.Auth
Generated code in the route function.
Attentions
For
JWT
, at first tellPhone
the jwt auth name inbuild.gradle(.kts)
in moduleshared
as below.ksp { arg("phone.jwt-auth-name", "<your-auth-name>") }
Then you could refresh the jwt token on client as before, and put it in
phone
viaphone.refreshJwtToken(token)
. Then each request header needing theJWT
token would get it.For the
form authentication
, avoid usingHttpMethod.Get
.For
WebSockets
,JWT
is more suggested than general authentications. Besides, the configured client authentication info would not be put intoHttpHeader
as commonHttp requests
. If you apply basic authentication on aWebSocket
route, you should extend the request with specific header info before you proceed.phone.CompoundApi{ header(HttpHeaders.Authorization, "Basic " + "jetbrains:foobar".encodeBase64()) } ...
For
OAuth
Shared api
Server side
Polymorphic functions #
For the backward compatibility, polymorphic functions except the first are required to declare the distinct id.
Exception #
You could intercept exceptions and respond a status code with the Ktor plugin StatusPages.
All status codes except HttpStatusCode.OK
means failure. I suggest to only respond 400
or 500
with messages for
exceptions.
Roadmap #
- Documents and api generation for other languages.
- Big binary data as parameters, and limited permission with a new
context
.call.parameters
,call.respond
and some other needless functions are invisible.