Raw Sockets

Estimated reading time: 4 minutes

In addition to HTTP handling for the server and the client, Ktor supports client and server, TCP and UDP raw sockets. It exposes a suspending API that uses NIO under the hoods.

Table of contents:

Sockets

This functionality is exposed through the io.ktor:ktor-network:$ktor_version artifact.

In order to create either server or client sockets, you have to use the aSocket builder, with a mandatory ActorSelectorManager: aSocket(selector). For example: aSocket(ActorSelectorManager(ioCoroutineDispatcher)).

Then use:

  • val socketBuilder = aSocket(selector).tcp() for a builder using TCP sockets
  • val socketBuilder = aSocket(selector).udp() for a builder using UDP sockets

This returns a SocketBuilder that can be used to:

  • val serverSocket = aSocket(selector).tcp().bind(address) to listen to an address (for servers)
  • val clientSocket = aSocket(selector).tcp().connect(address) to connect to an address (for clients)

If you need to control the dispatcher used by the sockets, you can instantiate a selector, that uses, for example, a cached thread pool:

val exec = Executors.newCachedThreadPool()
val selector = ActorSelectorManager(exec.asCoroutineDispatcher())
val tcpSocketBuilder = aSocket(selector).tcp()

Once you have a socket open by either binding or connecting the builder, you can read from or to write to the socket, by opening read/write channels:

val input : ByteReadChannel  = socket.openReadChannel()
val output: ByteWriteChannel = socket.openWriteChannel(autoFlush = true)

You can read the KDoc for ByteReadChannel and ByteWriteChannel for further information on the available methods.

Server

When creating a server socket, you have to bind to a specific SocketAddress to get a ServerSocket:

val server = aSocket(selector).tcp().bind(InetSocketAddress("127.0.0.1", 2323))

The server socket has an accept method that returns, one at a time, a connected socket for each incoming connection pending in the backlog:

val socket = server.accept()

If you want to support multiple clients at once, remember to call launch { } to prevent the function that is accepting the sockets from suspending.

Simple Echo Server:

fun main(args: Array<String>) {
    runBlocking {
        val server = aSocket(ActorSelectorManager(ioCoroutineDispatcher)).tcp().bind(InetSocketAddress("127.0.0.1", 2323))
        println("Started echo telnet server at ${server.localAddress}")
        
        while (true) {
            val socket = server.accept()
            
            launch {
                println("Socket accepted: ${socket.remoteAddress}")
                
                val input = socket.openReadChannel()
                val output = socket.openWriteChannel(autoFlush = true)
                
                try {
                    while (true) {
                        val line = input.readASCIILine()
                        
                        println("${socket.remoteAddress}: $line")
                        output.writeBytes("$line\r\n")
                    }
                } catch (e: Throwable) {
                    e.printStackTrace()
                    socket.close()
                }
            }
        }
    }
}

Then you can connect to it using telnet and start typing:

telnet 127.0.0.1 2323

For each line that you type (you have to press the return key), the server will reply with the same line:

Trying 127.0.0.1...
Connected to 127.0.0.1
Escape character is '^]'.

Hello
Hello
World
World
|

Client

When creating a socket client, you have to connect to a specific SocketAddress to get a Socket:

val socket = aSocket(selector).tcp().connect(InetSocketAddress("127.0.0.1", 2323))

Simple Client Connecting to an Echo Server:

fun main(args: Array<String>) {
    runBlocking {
        val socket = aSocket(ActorSelectorManager(ioCoroutineDispatcher)).tcp().connect(InetSocketAddress("127.0.0.1", 2323))
        val input = socket.openReadChannel()
        val output = socket.openWriteChannel(autoFlush = true)

        output.writeBytes("hello\r\n")
        val response = input.readASCIILine()
        println("Server said: '$response'")
    }
}

Secure Sockets (SSL/TLS)

Ktor supports secure sockets. To enable them you will need to include the io.ktor:ktor-network-tls:$ktor_version artifact, and call the .tls() to a connected socket.

Connect to a secure socket:

runBlocking {
    val socket = aSocket(ActorSelectorManager(ioCoroutineDispatcher)).tcp().connect(InetSocketAddress("google.com", 443)).tls()
    val w = socket.openWriteChannel(autoFlush = false)
    w.write("GET / HTTP/1.1\r\n")
    w.write("Host: google.com\r\n")
    w.write("\r\n")
    w.flush()
    val r = socket.openReadChannel()
    println(r.readASCIILine())
}

You can adjust a few optional parameters for the TLS connection:

suspend fun Socket.tls(
        trustManager: X509TrustManager? = null,
        randomAlgorithm: String = "NativePRNGNonBlocking",
        serverName: String? = null,
        coroutineContext: CoroutineContext = ioCoroutineDispatcher
): Socket