All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.lightstreamer.kotlin.socket.LightstreamerTlcpSocket.kt Maven / Gradle / Ivy

The newest version!
package com.lightstreamer.kotlin.socket

import com.lightstreamer.kotlin.socket.internal.FastCharSequence
import com.lightstreamer.kotlin.socket.internal.TlcpParser
import com.lightstreamer.kotlin.socket.internal.appendTlcpEncoded
import com.lightstreamer.kotlin.socket.internal.appendTlcpEncodedParameters
import com.lightstreamer.kotlin.socket.message.*
import kotlinx.coroutines.*
import kotlinx.coroutines.channels.*
import kotlinx.coroutines.future.await
import mu.KLogging
import java.io.IOException
import java.net.URI
import java.net.http.HttpClient
import java.net.http.HttpRequest
import java.net.http.HttpResponse
import java.net.http.WebSocket
import java.time.Duration
import java.util.concurrent.CompletionStage
import kotlin.time.Duration.Companion.seconds

public class LightstreamerTlcpSocket
private constructor(
    private val webSocket: WebSocket,
    private val webSocketListener: WebSocketListener
) : LightstreamerSocket, ReceiveChannel by webSocketListener.serverMessageChannel {

    private val sendChannel =
        Channel(256)

    private val sendJob: Job = GlobalScope.launch {
        val sendBuffer = FastCharSequence(128)
        var pendingMessage: LightstreamerClientMessage? = null

        try {
            while (true) {
                val firstMessage = pendingMessage ?: sendChannel.receive()
                sendBuffer.appendTlcpEncoded(firstMessage)
                pendingMessage = null

                // collapse multiple requests
                if (firstMessage.name == LightstreamerClientRequestName.CONTROL || firstMessage.name == LightstreamerClientRequestName.MESSAGE) {
                    while (sendBuffer.length < SEND_THRESHOLD) {
                        pendingMessage = sendChannel.tryReceive().getOrNull() ?: break
                        if (pendingMessage.name != firstMessage.name) break
                        sendBuffer.appendTlcpEncodedParameters(pendingMessage)
                        pendingMessage = null
                    }
                }

                ensureActive()
                logger.trace { "Send to $webSocket: $sendBuffer" }
                webSocket.sendText(sendBuffer, true)?.await()
                sendBuffer.clear()
            }
        } catch (e: Exception) {
            if (e is CancellationException) logger.trace(e) { "Error sending messages to $webSocket" }
            else logger.info { "Error sending messages to $webSocket: $e" }
            throw e
        } finally {
            sendChannel.close()
        }
    }

    override fun disconnect() {
        logger.trace { "Disconnect $webSocket" }
        sendJob.cancel()
        sendChannel.close()
        webSocketListener.close(webSocket)
    }

    override suspend fun join() {
        webSocketListener.closeCauseDeferred.join()
    }

    override suspend fun send(message: LightstreamerClientMessage) {
        logger.debug { "Enqueue message for $webSocket: $message" }
        try {
            sendChannel.send(message)
        } catch (e: IOException) {
            throw e
        } catch (e: Exception) {
            throw IOException(e.message ?: "Error sending message to $webSocket", e)
        }
    }

    override fun toString(): String = webSocket.toString()

    private class WebSocketListener : WebSocket.Listener {

        val serverMessageChannel = Channel(1024)

        private val tlcpParser = TlcpParser()

        val closeCauseDeferred = CompletableDeferred()

        override fun onText(webSocket: WebSocket, data: CharSequence, last: Boolean): CompletionStage? {
            try {
                logger.debug { "$webSocket onText $data" }
                for (message in tlcpParser.parse(data)) {
                    serverMessageChannel.trySendBlocking(message)
                        .onFailure { throw it ?: IllegalStateException("Channel closed") }
                }
                webSocket.request(1)
            } catch (t: Throwable) {
                close(webSocket, t)
            }
            return null
        }

        override fun onClose(webSocket: WebSocket, statusCode: Int, reason: String?): CompletionStage<*>? {
            logger.debug { "$webSocket onClose $statusCode $reason" }
            val exception =
                if (statusCode == WebSocket.NORMAL_CLOSURE) null
                else IOException("WebSocket closed: $statusCode $reason")
            close(webSocket, exception)
            return null
        }

        override fun onError(webSocket: WebSocket, error: Throwable) {
            logger.debug { "$webSocket onError $error" }
            close(webSocket, error)
        }

        fun close(webSocket: WebSocket, cause: Throwable? = null) {
            if (cause !is CancellationException) logger.info { "Close $webSocket: $cause" }
            closeCauseDeferred.complete(cause)
            if (cause == null || cause is ClosedReceiveChannelException) serverMessageChannel.close()
            else serverMessageChannel.close(cause)
            webSocket.abort()
        }
    }

    public companion object : KLogging() {

        @Suppress("MemberVisibilityCanBePrivate", "SpellCheckingInspection")
        public val clientCidCode: String get() = "mgQkwtwdysogQz2BJ4Ji kOj2Bg"

        private const val SEND_THRESHOLD = 8 * 1024

        @Suppress("MemberVisibilityCanBePrivate", "SpellCheckingInspection")
        public val tlcpVersion: String get() = "2.5.0"

        /**
         * Opens a WebSocket connection.
         */
        public suspend fun connect(address: LightstreamerServerAddress, httpClient: HttpClient): LightstreamerSocket {
            logger.debug { "connect($address)" }

            val webSocketListener = WebSocketListener()

            val protocol = if (address.secureConnection) "wss" else "ws"
            val webSocket: WebSocket = httpClient.newWebSocketBuilder()
                .connectTimeout(Duration.ofSeconds(5))
                .subprotocols("TLCP-$tlcpVersion.lightstreamer.com")
                .buildAsync(
                    URI(protocol, null, address.host, address.port.toInt(), "/lightstreamer", null, null),
                    webSocketListener
                )
                .await()

            return LightstreamerTlcpSocket(
                webSocket = webSocket,
                webSocketListener = webSocketListener
            )
        }

        /**
         * Send messages to a LightStreamer server using the "Control Combo Request"
         */
        public suspend fun sendMessages(
            address: LightstreamerServerAddress,
            adapterSetName: String = "DEFAULT",
            userCredential: UsernamePassword? = null,
            httpClient: HttpClient,
            messages: Iterable
        ) {
            val requestBody = buildString {
                appendTlcpEncodedParameters(
                    LightstreamerClientMessage.CreateSession(
                        adapterSetName,
                        userCredential,
                        polling = 0.seconds
                    )
                )
                for (message in messages) {
                    append("LS_message=")
                    appendTlcpEncoded(message)
                    append("&LS_outcome=false")
                    append("\r\n")
                }
            }

            val protocol = if (address.secureConnection) "https" else "http"
            val request = HttpRequest
                .newBuilder(
                    URI(
                        protocol,
                        null,
                        address.host,
                        address.port.toInt(),
                        "/lightstreamer/create_session.txt",
                        "LS_protocol=TLCP-$tlcpVersion",
                        null
                    )
                )
                .POST(HttpRequest.BodyPublishers.ofString(requestBody))
                .build()

            val response = httpClient.sendAsync(request, HttpResponse.BodyHandlers.ofString()).await()

            if (response.statusCode() != 200) throw IOException("Unable to send messages, HTTP status code ${response.statusCode()}")
            when (val serverResponse = TlcpParser().parse(response.body()).firstOrNull()) {
                is LightstreamerServerMessage.ConnectionOk -> Unit
                is LightstreamerServerMessage.ConnectionError ->
                    throw LightstreamerServerException.ConnectionError(serverResponse.code, serverResponse.message)

                else -> throw IOException("Unable to parse response: ${response.body()}")
            }
        }
    }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy