-1

We've implemented a Socket Client in Kotlin to communicate with a Payment Terminal in the same local network. It works like the following:

  1. We open the Socket connection, the Payment Terminal responds with a message indicating its status
  2. We send a message with the Payment Request itself to the Payment Terminal
  3. When the Payment is Completed/Cancelled/Declined/..., the Payment Terminal responds with the Payment Result

Code below.

The TCP Client itself:

import android.os.Build
import android.system.Os
import timber.log.Timber
import java.io.BufferedInputStream
import java.io.BufferedReader
import java.io.BufferedWriter
import java.io.FileInputStream
import java.io.InputStream
import java.io.InputStreamReader
import java.io.OutputStreamWriter
import java.io.PrintWriter
import java.net.InetAddress
import java.net.InetSocketAddress
import java.net.Socket
import java.nio.charset.Charset
import java.nio.charset.StandardCharsets
    
class TcpClient(private val host: String, private val port: Int, private val handlers: Handlers) {
    // While this is true, the client will continue listening
    private var isRunning = false

    // Used to send messages
    private var writeBuffer: PrintWriter? = null

    private fun readString(inputStream: InputStream): String {
        return buildString {
            Timber.e("readString, available: ${inputStream.available()}")
            while (inputStream.available() > 0) {
                val byte = inputStream.read()
                Timber.e("byte: $byte")

                // Filter out invalid characters (such as heartbeat i.e. 0)
                if(byte > 0) {
                    val ch = byte.toChar()
                    Timber.e("ch: $ch")

                    append(ch)
                }
            }
        }
    }

    /*private fun readData(inputStream: InputStream): String {
        val data = ByteArray(4096)
        val count = inputStream.read(data)

        if(count > 0) {
            return String(data, 0, count, StandardCharsets.UTF_8)
        }
        return ""
    }

    private fun readLine(reader: BufferedReader): String {
        //reader.read

    }*/

    /**
     * Sends the message entered by client to the server
     *
     * @param message text entered by client
     */
    fun sendMessage(message: String?) {
        Timber.e("sendMessage: $message")
        if (writeBuffer != null && !writeBuffer!!.checkError()) {
            writeBuffer?.println(message)
            writeBuffer?.flush()
        }
    }

    /**
     * Close the connection and release the members
     */
    fun stopClient(isCancelled: Boolean = false) {
        Timber.e("stopClient: $isCancelled")
        isRunning = false

        if (writeBuffer != null) {
            writeBuffer?.flush()
            writeBuffer?.close()
        }

        writeBuffer = null

        if(isCancelled) {
            handlers.exceptionOccurred(Exception("error.session-cancelled"))
        }
    }

    fun run() {
        Timber.e("run")
        isRunning = true

        try {
            val serverAddress = InetAddress.getByName(host)

            // Create a socket to make the connection with the server
            //val socket = Socket(serverAddress, SERVER_PORT)
            val socket = Socket()
            socket.connect(InetSocketAddress(serverAddress, port), 5000) // Timeout to initially connect to the POS (connect())
            socket.soTimeout = 10000 // Timeout when connection is dropped (read())
            //setKeepAliveSocketOptions(socket, 60, 15, 4)

            try {
                //val readBuffer = BufferedReader(InputStreamReader(socket.inputStream))
                writeBuffer =
                    PrintWriter(BufferedWriter(OutputStreamWriter(socket.outputStream)), true)

                // In this while the client listens for the messages sent by the server
                while (isRunning) {
                    val serverMessage = readString(socket.inputStream)
                    Timber.e("serverMessage: $serverMessage")
                    //val serverMessage = readLine(readBuffer)

                    if(serverMessage != "") {
                        handlers.messageReceived(serverMessage)
                    }
                }

                handlers.sockedClosed()
            } catch (e: Exception) {
                Timber.e("exception1: $e")
                handlers.exceptionOccurred(e)
            } finally {
                Timber.e("finally socket.close()")
                // The socket must be closed. It is not possible to reconnect to this socket
                // after it is closed, which means a new socket instance has to be created.
                socket.close()
            }
        } catch (e: Exception) {
            Timber.e("exception2: $e")
            handlers.exceptionOccurred(e)
        }
    }

    /*protected fun setKeepAliveSocketOptions(
        socket: Socket,
        idleTimeout: Int,
        interval: Int,
        count: Int
    ) {
        //Timber.e("setKeepAliveSocketOptions $idleTimeout $interval $count")
        val SOL_TCP = 6
        val TCP_KEEPIDLE = 4
        val TCP_KEEPINTVL = 5
        val TCP_KEEPCNT = 6

        try {
            socket.keepAlive = true
            val socketFileDescriptor = (socket.inputStream as FileInputStream).fd

            if (socketFileDescriptor != null) {
                if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
                    Os.setsockoptInt(socketFileDescriptor, SOL_TCP, TCP_KEEPIDLE, idleTimeout)
                    Os.setsockoptInt(socketFileDescriptor, SOL_TCP, TCP_KEEPINTVL, interval)
                    Os.setsockoptInt(socketFileDescriptor, SOL_TCP, TCP_KEEPCNT, count)
                } else {
                    // TO FIX
                    /*val libCoreClass = Class.forName("libcore.io.Libcore")
                    val osField: Field = libCoreClass.getDeclaredField("os")
                    osField.setAccessible(true)
                    val libcoreOs: Any = osField.get(libCoreClass)
                    val setSocketOptsMethod = Class.forName("libcore.io.ForwardingOs")
                        .getDeclaredMethod(
                            "setsockoptInt",
                            FileDescriptor::class.java,
                            Int::class.javaPrimitiveType,
                            Int::class.javaPrimitiveType,
                            Int::class.javaPrimitiveType
                        )

                    setSocketOptsMethod.invoke(
                        libcoreOs,
                        socketFileDescriptor,
                        SOL_TCP,
                        TCP_KEEPIDLE,
                        idleTimeout
                    )
                    setSocketOptsMethod.invoke(
                        libcoreOs,
                        socketFileDescriptor,
                        SOL_TCP,
                        TCP_KEEPINTVL,
                        interval
                    )
                    setSocketOptsMethod.invoke(
                        libcoreOs,
                        socketFileDescriptor,
                        SOL_TCP,
                        TCP_KEEPCNT,
                        count
                    )*/
                }
            }
        } catch (e: java.lang.Exception) {
            if (BuildConfig.DEBUG) e.printStackTrace()
        }
    }*/

    // Declare the interface. The method messageReceived(String message) must be implemented in the Activity class
    interface Handlers {
        fun messageReceived(message: String)
        fun exceptionOccurred(exception: Exception)
        fun sockedClosed()
    }
}

Where it's being used:

// Connects using a background task to avoid doing long/network operations on the UI thread
fun connectSocket(host: String, port: Int, onMessage: (message: String) -> Unit, onException: (exception: Exception) -> Unit, onClose: () -> Unit) {
    val executor = Executors.newSingleThreadExecutor()
    val handler = Handler(Looper.getMainLooper())

    executor.execute {
        Timber.e("execute connectSocket")
        tcpClient = TcpClient(host, port, object : TcpClient.Handlers {
            override fun messageReceived(message: String) {
                onMessage(message)
            }

            override fun exceptionOccurred(exception: Exception) {
                onException(exception)
            }

            override fun sockedClosed() {
                onClose()
            }
        })

        tcpClient?.run()

        handler.post {}
    }
}

// Disconnects using a background task to avoid doing long/network operations on the UI thread
fun disconnectSocket(isCancelled: Boolean = false) {
    val executor = Executors.newSingleThreadExecutor()
    val handler = Handler(Looper.getMainLooper())

    executor.execute {
        Timber.e("execute disconnectSocket")
        tcpClient?.stopClient(isCancelled)
        tcpClient = null

        handler.post {}
    }
}

// Sends a message using a background task to avoid doing long/network operations on the UI thread
fun sendSocketMessage(message: String) {
    val executor = Executors.newSingleThreadExecutor()
    val handler = Handler(Looper.getMainLooper())

    executor.execute {
        Timber.e("execute sendSocketMessage $message")
        tcpClient?.sendMessage(message)

        handler.post {}
    }
}

fun launchSecureSession(data: String, host: String, port: String) {
    try {
        Timber.e("launchSecureSession")
        if(tcpClient != null) {
            throw Exception("error.session-ongoing")
        }

        val requestParts = data.split("|")
        if(requestParts.size < 4) {
            throw Exception("error.request-invalid")
        }

        val key = requestParts[2].trim()
        val amount = requestParts[3]
        var sessionId: String? = null

        connectSocket(host, port.toInt(), { message ->
            Timber.e("connectSocket message: $message")
            val responseParts = message.split("|")

            if(responseParts.size == 4) {
                Timber.e("responseParts.size =4")
                val msgLength = responseParts[0]
                val msgType = responseParts[1]
                val msgCode = responseParts[2]
                val seqTxnNum = responseParts[3]

                if(msgLength == "0014" && msgType == "810" && msgCode == "00") {
                    sessionId = seqTxnNum

                    val messageWithoutLength = "|$sessionId|$decryptedData"
                    val messageLength = messageWithoutLength.length.toString().padStart(4, '0')

                    sendSocketMessage("$messageLength$messageWithoutLength")
                }
            } else if(responseParts.size > 5) {
                Timber.e("responseParts.size >5")
                val msgLength = responseParts[0]
                val seqTxnId = responseParts[1]
                val msgTypeResp = responseParts[2]
                val msgCodeResp = responseParts[3]
                val respCodeResp = responseParts[4]

                if(seqTxnId == sessionId && msgTypeResp == "210" && msgCodeResp == "00") {
                    val status = if(respCodeResp == "00") "success" else "fail"

                    handleSetSecureSessionResponse(key, status, message)

                    disconnectSocket()
                }
            }
        }, { exception ->
            Timber.e("connectSocket exception: $exception")

            if(!handleHasSecureSessionResponse(key)) {
                val status = "fail"

                val messageWithoutLength = "|${sessionId ?: "000000"}|210|00|ND|${exception.message}||||||${amount}|1010|00|00|00|00|00|00|            ||||||||||||"
                val messageLength = messageWithoutLength.length.toString().padStart(4, '0')

                handleSetSecureSessionResponse(key, status, "$messageLength$messageWithoutLength")
            }

            disconnectSocket()
        }, {})
    } catch (e: Error) {
        Timber.e("launchSecureSession error: $e")
        e.message?.let { Timber.e(it) }

        disconnectSocket()
    } catch(e: Exception) {
        Timber.e("launchSecureSession exception: $e")
    }
}

@JavascriptInterface
fun cancelSecureSession() {
    disconnectSocket(true)
}

This code works fine, except sometimes it does not. From time to time (avg. 1,5% of the cases), an exception 'Socket Closed' or 'Stream closed.' is thrown right at the moment we're (about) to read the Payment Result (somewhere in the block where we set the result handleSetSessionResponse(key, data)). It's an issue that's very hard to reproduce but the implications are not to be overseen because the customer paid but we never get that result in our app.

Anyone any idea why the read() operation might interrupt unexpectedly?

UPDATE #1 I added logging to the example code above and added the logs when it goes wrong below -- some sensitive data is replaced by ***** (the Exception at the bottom is only thrown at random times):

CoreActivi...pInterface E  launchSecureSession
CoreActivity            E  execute connectSocket
TcpClient               E  run
System.out              I  [socket]:check permission begin!
TcpClient               E  readString, available: 0
TcpClient               E  serverMessage: 
TcpClient               E  readString, available: 0
TcpClient               E  serverMessage: 
TcpClient               E  readString, available: 0
TcpClient               E  serverMessage: 
TcpClient               E  readString, available: 0
TcpClient               E  serverMessage: 
TcpClient               E  readString, available: 0
TcpClient               E  serverMessage: 
TcpClient               E  readString, available: 0
TcpClient               E  serverMessage: 
TcpClient               E  readString, available: 0
TcpClient               E  serverMessage: 
TcpClient               E  readString, available: 0
TcpClient               E  serverMessage: 
TcpClient               E  readString, available: 0
TcpClient               E  byte: 48
TcpClient               E  ch: 0
TcpClient               E  byte: 48
TcpClient               E  ch: 0
TcpClient               E  byte: 49
.....
TcpClient               E  byte: 51
TcpClient               E  ch: 3
TcpClient               E  serverMessage: 0014|810|00|000273
CoreActivi...ureSession E  connectSocket message: 0014|810|00|000273
CoreActivi...ureSession E  responseParts.size =4
TcpClient               E  readString, available: 0
TcpClient               E  serverMessage: 
TcpClient               E  readString, available: 0
TcpClient               E  serverMessage: 
TcpClient               E  readString, available: 0
TcpClient               E  serverMessage: 
TcpClient               E  readString, available: 0
TcpClient               E  serverMessage: 
TcpClient               E  readString, available: 0
TcpClient               E  serverMessage: 
TcpClient               E  readString, available: 0
TcpClient               E  serverMessage: 
CoreActivity            E  execute sendSocketMessage 0182|000273|200|*****|ecr_default
TcpClient               E  sendMessage: 0182|000273|200|*****|ecr_default
TcpClient               E  readString, available: 0
TcpClient               E  serverMessage: 
TcpClient               E  readString, available: 0
TcpClient               E  serverMessage: 
TcpClient               E  readString, available: 0
TcpClient               E  serverMessage: 
TcpClient               E  readString, available: 0
.....
TcpClient               E  readString, available: 0
TcpClient               E  serverMessage: 
TcpClient               E  readString, available: 0
TcpClient               E  serverMessage: 
TcpClient               E  readString, available: 252
TcpClient               E  byte: 48
TcpClient               E  ch: 0
TcpClient               E  byte: 50
TcpClient               E  ch: 2
TcpClient               E  byte: 52
TcpClient               E  ch: 4
TcpClient               E  byte: 56
TcpClient               E  ch: 8
TcpClient               E  byte: 124
TcpClient               E  ch: |
.....
TcpClient               E  byte: 124
TcpClient               E  ch: |
TcpClient               E  byte: 49
TcpClient               E  ch: 1
TcpClient               E  serverMessage: 0248|000273|210|*****|2024-06-17T10:24:36.5397369+03:00|1
CoreActivi...ureSession E  connectSocket message: 0248|000273|210|*****|2024-06-17T10:24:36.5397369+03:00|1
CoreActivi...ureSession E  responseParts.size >5
CoreActivity            E  handleSetSecureSessionResponse 87HDOUvgF4oZQmhoXoJk { "key": "87HDOUvgF4oZQmhoXoJk", "status": "success", "data": "*****" }
TcpClient               E  readString, available: 0
TcpClient               E  serverMessage: 
TcpClient               E  readString, available: 0
TcpClient               E  serverMessage: 
TcpClient               E  readString, available: 0
TcpClient               E  serverMessage: 
TcpClient               E  readString, available: 0
TcpClient               E  serverMessage: 
TcpClient               E  readString, available: 0
TcpClient               E  serverMessage: 
TcpClient               E  readString, available: 0
TcpClient               E  serverMessage: 
TcpClient               E  readString, available: 0
TcpClient               E  serverMessage: 
TcpClient               E  readString, available: 0
TcpClient               E  serverMessage: 
TcpClient               E  readString, available: 0
TcpClient               E  serverMessage: 
TcpClient               E  readString, available: 0
TcpClient               E  serverMessage: 
TcpClient               E  readString, available: 0
TcpClient               E  serverMessage: 
TcpClient               E  readString, available: 0
CoreActivity            E  execute disconnectSocket
TcpClient               E  stopClient: false
TcpClient               E  exception1: java.io.IOException: Stream closed.
CoreActivi...ureSession E  connectSocket exception: java.io.IOException: Stream closed.
CoreActivity            E  handleSetSecureSessionResponse 87HDOUvgF4oZQmhoXoJk { "key": "87HDOUvgF4oZQmhoXoJk", "status": "fail", "data": "*****" }
TcpClient               E  finally socket.close()
CoreActivity            E  execute disconnectSocket
10
  • 1
    You aren’t testing for end of stream; and available() isn’t a test for end of stream. Just remove this test. read() will block as necessary. But both the errors you mention mean that you have closed the stream and then continued to use it. Is this the only code that uses this socket? If so, why the isRunning variable? If not, where is the rest of the code? Commented Jun 16, 2024 at 13:08
  • And where does sendMessage() get writeBuffer from? Post the real code. Commented Jun 16, 2024 at 13:39
  • Hi @user207421. Thx for your comments. I've updated my post above with the full class. I was experimenting myself and was also suspecting something wrong with the read(). Would replacing readString() with readData() below be a better solution? Also, I don't use a BufferedReader, is that an issue? private fun readData(inputStream: InputStream): String { val data = ByteArray(4096) val count = inputStream.read(data) if(count > 0) { return String(data, 0, count, StandardCharsets.UTF_8) } return "" } Commented Jun 16, 2024 at 13:45
  • 1
    You still aren’t testing for end of stream. If read() returns -1 you must close the socket and you should also null writeBuffer, or better still get rid of it and pass it as a parameter as you do with the input stream; and you should also set isRunning to false, or better still get rid of it as well, as it doesn’t express anything useful. Please post the full stack trace in your question. Commented Jun 16, 2024 at 21:13
  • And you mentioned a ping. Who is doing that? How? Some other piece of code you haven’t shown is closing the socket, and it is probably doing so via the writeBuffer variable, which BTW isn’t a buffer. Commented Jun 17, 2024 at 0:16

1 Answer 1

-1

Based on the very helpful, to the point and kind input from @user207421, we modified the TCP Client like below. Based on our testing, it seems to be working more reliable.

import timber.log.Timber
import java.io.BufferedReader
import java.io.BufferedWriter
import java.io.InputStreamReader
import java.io.OutputStreamWriter
import java.io.PrintWriter
import java.net.InetAddress
import java.net.InetSocketAddress
import java.net.Socket
    
class TcpClient(private val host: String, private val port: Int, private val handlers: Handlers) {
    // The actual TCP Socket
    private var socket: Socket? = null
    // Used to send messages
    private var printWriter: PrintWriter? = null

    // Helper function to read Data as a String (max chunk size 4096 bytes)
    private fun readLine(reader: BufferedReader): String? {
        val data = CharArray(4096)
        val count = reader.read(data)

        if(count < 0) {
            return null
        }

        return String(data, 0, count)
    }

    // Send a message to the host (as a String)
    fun send(message: String?) {
        printWriter?.let {
            if (!it.checkError()) {
                it.println(message)
                it.flush()
            }
        }
    }

    // Shutdown the connection
    fun cancel() {
        try {
            socket?.shutdownInput()
            socket?.shutdownOutput()

            throw Exception("error.session-cancelled")
        } catch(e: Exception) {
            handlers.exceptionOccurred(e)
        }
    }

    // Start the Client
    fun run() {
        try {
            // Create a socket to make the connection with the server
            this.socket = Socket()
            this.socket?.let {
                it.connect(InetSocketAddress(InetAddress.getByName(host), port), 5000) // Timeout to initially connect to the POS (connect())
                //socket.soTimeout = 10000 // Timeout when connection is dropped (read())

                try {
                    val readBuffer = BufferedReader(InputStreamReader(it.inputStream))
                    printWriter = PrintWriter(BufferedWriter(OutputStreamWriter(it.outputStream)), true)

                    // In this while the client listens for the messages sent by the host
                    var keepListening = true
                    while (keepListening) {
                        val serverMessage = readLine(readBuffer) ?: break
                        keepListening = handlers.messageReceived(serverMessage)
                    }

                    handlers.sockedClosed()
                } catch (e: Exception) {
                    handlers.exceptionOccurred(e)
                } finally {
                    printWriter?.close()
                    printWriter = null
                }
            }
        } catch (e: Exception) {
            handlers.exceptionOccurred(e)
        }
    }

    // Declare the interface. The method messageReceived(String message) must be implemented in the Activity class
    interface Handlers {
        fun messageReceived(message: String): Boolean
        fun exceptionOccurred(exception: Exception)
        fun sockedClosed()
    }
}

Important things to note:

  • If a read() operation returns anything less then 0, the readLine() function returns null. This breaks the while loop listening for server data and closes the Socket (and corresponding WriteBuffer)
  • Instead of using an isRunning variable, the instance using the client should indicate whether the client should listen for more by returning TRUE or FALSE when messageReceived is called
  • To stop the Client (in the case a payment is cancelled from our app and not from the Payment Terminal itself), a shutdown of the input and output is performed; this unblocks read() and again closes the Socket (and corresponding WriteBuffer)
Sign up to request clarification or add additional context in comments.

8 Comments

You can’t ‘clear’ the write buffer (which still isn’t a buffer) after closing the socket. You don’t need that method at all. Just close that variable instead of the socket.
Thx for taking the time to review my answer @user207421. I'm not following completely. Do you mean I should remove the clearWriteBuffer() helper function and instead just do writeBuffer = null (so basically don't do a flush() and close() at all)? Or should I change the order (first do clearWriteBuffer(), subsequently do socket.close()). Or should I remove everything from the finally clause?
No I don't mean that, and I didn't say it. Close the stream. Closing the stream flushes it and closes the socket.
@user207421 So you mean that instead of calling it.close() (it = Socket) and clearWriteBuffer(), I should only close both Streams instead (readBuffer?.close() and writeBuffer?.close())? I was digging into the documentation (docs.oracle.com/javase%2F8%2Fdocs%2Fapi%2F%2F/java/io/…) and it does not explicitly mention that closing the stream closes the Socket (or is this done implicitly)? The documentation (docs.oracle.com/javase%2F8%2Fdocs%2Fapi%2F%2F/java/net/…) does mention that closing a Socket closes its I/O Streams, however.
You don't need to close both, and I didn't say you did. I said to close the writeBuffer stream. The Socket documentation says more about the streams that what you've found so far. Keep reading.
|

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.