TCP Client using Vertx, Kotlin and Gradle build

Project Directory Structure

From command line (or via Windows Explorer, whatever you prefer to use) create a directory for project,for instance vertx-net-client. Since we are using Kotlin, we will place all Kotlin files in src/main/kotlin folder. The src/main/resources folder will contain our logging configuration related files.

cd vertx-net-client
mkdir -p src/main/kotlin
mkdir -p src/main/resources

Project Files

We need to add following files in the project

.gitignore If you want to check your project into git, you may consider adding following .gitignore file at root of your project

.gradle
build/
out/
logs/

# Ignore Gradle GUI config
gradle-app.setting

# Avoid ignoring Gradle wrapper jar file (.jar files are usually ignored)
!gradle-wrapper.jar

# Cache of project
.gradletasknamecache

# IDE
.idea/
*.iml
*.ipr
*.iws

# Vertx Cache
.vertx/

logback.xml This example is using slf4j and logback for logging. If you decide to use it in your project, you may also add following logback.xml file in src/main/resources. Modify it as per your requirements. This example will log on console.

<configuration>
    <appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
        <file>logs/vertx-net-client.log</file>
        <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
            <!-- daily rollover -->
            <fileNamePattern>logs/vertx-net-client.%d{yyyy-MM-dd}.log</fileNamePattern>

            <!-- keep 30 days' worth of history capped at 3GB total size -->
            <maxHistory>30</maxHistory>
            <totalSizeCap>3GB</totalSizeCap>

        </rollingPolicy>

        <encoder>
            <pattern>%-4relative [%thread] %-5level %logger{35} - %msg%n</pattern>
        </encoder>
    </appender>

    <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
        <!-- encoders are assigned the type
             ch.qos.logback.classic.encoder.PatternLayoutEncoder by default -->
        <encoder>
            <pattern>%-4relative [%thread] %-5level %logger{35} - %msg %n</pattern>
        </encoder>
    </appender>

    <appender name="ASYNC" class="ch.qos.logback.classic.AsyncAppender">
        <appender-ref ref="STDOUT" />
    </appender>

    <root level="WARN">
        <appender-ref ref="ASYNC" />
    </root>

    <logger name="info.usmans.blog.vertx" level="INFO" />
</configuration>

Gradle Setup

We will use Gradle build system for this project. If you don’t already have Gradle available on your system, download and unzip gradle in a directory of your choice ($GRADLE_HOME is used here to represent this directory). This gradle distribution will be used as a starting point to create Gradle wrapper scripts for our project. These scripts will allow our project to download and use correct version of gradle distribution automatically without messing up system. Really useful when building your project on CI tool or on any other developer's machine.

Run following command in project's directory

$GRADLE_HOME/bin/gradle wrapper

The above commands will generate following files and directories.

gradle/  gradlew  gradlew.bat

Gradle build file build.gradle

Create (and/or copy and modify) following build.gradle in your project's root directory. Our example gradle build file is using vertx-gradle-plugin.


plugins {
    id 'io.vertx.vertx-plugin' version "0.0.6"
    id "org.jetbrains.kotlin.jvm" version "1.2.10"
}

repositories {
    jcenter()
}

dependencies {
    compile 'io.vertx:vertx-lang-kotlin'

    //kotlin dependencies
    compile "org.jetbrains.kotlin:kotlin-stdlib-jre8"

    //slf4j/logback logging
    compile "ch.qos.logback:logback-classic:1.2.3"

    //test dependencies
    testCompile "org.jetbrains.kotlin:kotlin-test"
    testCompile "org.jetbrains.kotlin:kotlin-test-junit"
}

//by default Kotlin targets 1.6
tasks.withType(org.jetbrains.kotlin.gradle.tasks.KotlinCompile).all {
    kotlinOptions {
        jvmTarget = "1.8"
    }
}

vertx {
    mainVerticle = "info.usmans.blog.vertx.NetClientVerticle"
}

//override generated jar name without version
shadowJar {
    baseName = 'vertx-net-client'
    version = null
}

task wrapper(type: Wrapper) {
    gradleVersion = '4.2'
}

In the project directory, run following command to download local gradle distribution:

./gradlew

if in Windows

.\gradlew.bat

At this stage we should have following file structure. This is also a good time to commit changes if you are working with git.

.gitignore
build.gradle
gradle/wrapper/gradle-wrapper.jar
gradle/wrapper/gradle-wrapper.properties
gradlew
gradlew.bat
src/main/resources/logback.xml

Now that our project structure is ready, time to add the meat of the project. You may use any IDE of your choice. My preference is IntelliJ IDEA.

Create a new package under src/main/kotlin. The package name should be adapted from the following section of build.gradle

vertx {
    mainVerticle = "info.usmans.blog.vertx.NetClientVerticle"
}

From the above example, the package name is info.usmans.blog.vertx

Add a new Kotlin Class/file in src/main/kotlin/info/usmans/blog/vertx as NetClientVerticle.kt

The contents of this class is as follows

package info.usmans.blog.vertx

import io.vertx.core.AbstractVerticle
import io.vertx.core.Vertx
import io.vertx.core.net.NetClient
import io.vertx.core.net.NetClientOptions
import org.slf4j.LoggerFactory

fun main(args: Array<String>) {
    //hack for windows - netty cause dns resolver error
    if (System.getProperty("os.name").toLowerCase().indexOf("win") >= 0) {
        System.setProperty("vertx.disableDnsResolver", "true")
    }

    //quick way to run the verticle in IDE.
    Vertx.vertx().deployVerticle(NetClientVerticle())
    println("Running NetClientVerticle!")
}

/**
 * A Vertx Verticle to connect to our custom server
 */
class NetClientVerticle : AbstractVerticle() {
    private val logger = LoggerFactory.getLogger("info.usmans.blog.vertx.NetClientVerticle")
    private val serverHost = System.getProperty("serverHost", "127.0.0.1")
    private val serverPort = System.getProperty("serverPort", "8888").toIntOrNull() ?: 8888
    private val connectMessage = System.getProperty("connectMessage", "hello")

    override fun start() {
        val options = NetClientOptions().apply {
            isSsl = true //required if server is using SSL Socket as well.
            connectTimeout = 10000
        }
        val client = vertx.createNetClient(options)

        fireReconnectTimer(client)
    }

    //wait for 5 seconds before attempting to connect
    private fun fireReconnectTimer(client: NetClient) {
        vertx.setTimer(5000, {
            reconnect(client)
        })
    }

    private fun reconnect(client: NetClient) {
        logger.info("Connecting to $serverHost:$serverPort")

        client.connect(serverPort, serverHost, { event ->
            if (event.succeeded()) {
                logger.info("Connected")
                val socket = event.result()
                //send pass phrase ...
                socket.write(connectMessage)

                socket.handler({ data ->
                    logger.info("Data received: ${data}")
                    //TODO: Do the work here ...
                })

                socket.closeHandler({
                    logger.info("Socket closed")
                    fireReconnectTimer(client)
                })
            } else {
                logger.info("Connection attempt failed. ${event.cause().message}")
                fireReconnectTimer(client)
            }
        })
    }


}

Explaining the Code

The fun main(args: Array<String>) is not strictly required, it quickly allows running the Vert.x verticle from within IDE. You will also notice a small hack in the method for setting system property vertx.disableDnsResolver which is to avoid a Netty bug that I observed when running on Windows machine and remote server is down. Of course, since we are using vertx-gradle-plugin, we can also use gradle vertxRun to run our verticle. In this case the main method will not get called.

The override fun start() method calls fireReconnectTimer which in turn calls reconnect method. reconnect method contains the connection logic to server as well as it calls fireReconnectTimer if it is unable to connect to server or disconnects from server. In reconnect method the socket.handler gets called when server send message to client.

socket.handler({ data ->
    logger.info("Data received: ${data}")
    //TODO: Do the work here ...
})

Distributing the project

To create redistributable jar, use ./gradlew shadowJar command. Or if using IntelliJ: from Gradle projects, Tasks, shadow, shadowJar (right click run). This command will generate ./build/libs/vertx-net-client-fat.jar.

Executing the client

The client jar can be executed using following command:

java -DserverHost=127.0.0.1 -DserverPort=8888 -DconnectMessage="hello" -jar vertx-net-client-full.jar

If you wish to use SLF4J for Vert.x internal logging, you need to pass system property vertx.logger-delegate-factory-class-name with value of io.vertx.core.logging.SLF4JLogDelegateFactory. The final command would look like:

java -DserverHost=127.0.0.1 -DserverPort=8888 -DconnectMessage="hello" -Dvertx.logger-delegate-factory-class-name="io.vertx.core.logging.SLF4JLogDelegateFactory" -jar vertx-net-client-full.jar

You can configure Vert.x logging levels in logback.xml file if required.

Conclusion

This post describes how easy it is to create a simple TCP client using Vert.x, Kotlin and Gradle build system. Hopefully the techniques shown here will serve as a starting point for your next DIY project.

About Me

I am Daiki, Blockchain Engineer, specializing in the EVM, Solana Web3 ecosystem and web3 fronternd. if i resonate with you. follow me on

My GitHub

See you on the next Part