Scratch is a I/O library written in Kotlin (multiplatform). Scratch is non-blocking and uses an event loop and coroutines for I/O.
- Event Loop implementations:
- Java: NIO
- Native: libuv
- Implements Clients and Servers
- TCP/UDP Sockets
- HTTP/1 and HTTP/2
- WebSockets
- TLS
- Java: SSLEngine
- Native: openssl
Usage will look similar to the blocking POSIX socket APIs, but blocking calls (read/write/accept) suspend rather than wait on a thread. An AsyncSocket is roughly:
interface AsyncSocket {
// returns false when the stream ends
suspend fun read(buffer: WritableBuffers): Boolean
suspend fun write(buffer: ReadableBuffers)
suspend fun close()
}
To get started, first, create your main event loop:
val loop = AsyncEventLoop()
// this is a helper to create a thread for you.
loop.startThread()
// the loop can also be manually run on a given thread using:
// loop.run()
Then use the loop to start a coroutine and create an echo server:
loop.async {
val server = listen(5555)
// accept sockets one at a time and echo data back
for (socket in server.accept()) {
val buffer = ByteBufferList()
while (socket.read(buffer)) {
// similar to POSIX write, the socket.write method may
// only write part of the buffer if it is large transfer.
// socket.drain ensures everything is written.
socket.drain(buffer)
}
}
}
To connect with a client and send data every 1 second:
loop.async {
val client = connect("localhost", 5555)
while (true) {
val buffer = "hello".createByteBufferList()
client.drain(buffer)
// the loop provides a nonblocking sleep
sleep(1000)
}
}
The above server has a problem: similar to POSIX accept loops, the example above is only handling one socket at a time. Typically every client socket would get its own thread, but Scratch can put each incoming socket in its own coroutine (all subsequent examples have removed the loop boilerplate):
val server = listen(5555)
// accept sockets asynchronously and echo data back
server.acceptAsync {
val buffer = ByteBufferList()
// "this" is the AsyncSocket.
while (read(buffer)) {
drain(buffer)
}
}
// this http server will have no routing.
// requests at any path will echo.
val server = AsyncHttpServer {
// parse the raw request body bytes as a string
val body = it.parse().readString()
// create and return the response.
StatusCode.OK(body = Utf8StringBody(body))
}
server.listen(5555)
val client = AsyncHttpClient()
val response = client(Methods.GET("http://localhost:5555",
body = Utf8StringBody("hello world")))
println("from server: " + response.parse().readString())
val router = AsyncHttpRouter()
// handle websocket requests at https://localhost:5555/websocket
router.webSocket("/websocket").acceptAsync {
for (message in messages) {
if (message.isText)
send(message.text)
}
}
val server = AsyncHttpServer(router::handle)
server.listen(5555).await()
val client = AsyncHttpClient()
val websocket = client.connectWebSocket("http://localhost:5555/websocket")
while (true) {
websocket.send("hello")
sleep(1000)
}
Creating a TLS Server that uses a key/certificate pair is similar to the previous Socket Server and Client example:
// Create an SSLContext
val serverContext = createTLSContext()
// helper to generate a self signed certificate
val keypairCert = createSelfSignedCertificate("TestServer")
// initialize the SSLContext with the private key and public certificate.
serverContext.init(keypairCert.first, keypairCert.second)
// socket server as seen in the socket example, but the incoming
// clients are upgraded to a TLS connection.
val server = server.listenTls(5555, context = serverContext)
server.acceptAsync {
val buffer = ByteBufferList()
// "this" is the AsyncTlsSocket.
while (read(buffer)) {
drain(buffer)
}
}
The TLS client will need to use the same certificate as the server, and is assumed provided in the sample below (ie, keypairCert).
// since this is a self signed cert, the TLS client needs a custom SSLContext.
// this is not necessary when connecting to publicly facing TLS servers
// with valid certificate chains.
val clientContext = createTLSContext()
// initialize the SSLContext with ONLY the public certificate
clientContext.init(keypairCert.second)
// connect to the TLS Server using the self-signed certificate context
val client = connectTls("localhost", 5555, context = clientContext)
while (true) {
val buffer = "hello".createByteBufferList()
client.drain(buffer)
sleep(1000)
}