Skip to content

Commit c44c708

Browse files
authored
Merge pull request #318 from LossyDragon/websocket-ktor
Replace WebSocket with Ktor
2 parents 4ba8443 + e3502d2 commit c44c708

7 files changed

Lines changed: 167 additions & 170 deletions

File tree

build.gradle.kts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ plugins {
1818

1919
allprojects {
2020
group = "in.dragonbra"
21-
version = "1.6.0-SNAPSHOT"
21+
version = "1.6.0"
2222
}
2323

2424
repositories {
@@ -122,6 +122,7 @@ dependencies {
122122
implementation(libs.okHttp)
123123
implementation(libs.xz)
124124
implementation(libs.protobuf.java)
125+
implementation(libs.bundles.ktor)
125126

126127
testImplementation(libs.bundles.testing)
127128
}

gradle/libs.versions.toml

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,9 @@ protobuf-java = { module = "com.google.protobuf:protobuf-java", version.ref = "p
4444
protobuf-protoc = { module = "com.google.protobuf:protoc", version.ref = "protobuf" }
4545
qrCode = { module = "pro.leaco.qrcode:console-qrcode", version.ref = "qrCode" }
4646
xz = { module = "org.tukaani:xz", version.ref = "xz" }
47+
ktor-client-core = { module = "io.ktor:ktor-client-core", version = "3.0.3" }
48+
ktor-client-cio = { module = "io.ktor:ktor-client-cio", version = "3.0.3" }
49+
ktor-client-websocket = { module = "io.ktor:ktor-client-websockets", version = "3.0.3" }
4750

4851
test-commons-codec = { module = "commons-codec:commons-codec", version.ref = "commonsCodec" }
4952
test-jupiter-api = { module = "org.junit.jupiter:junit-jupiter-api", version.ref = "junit5" }
@@ -71,3 +74,9 @@ testing = [
7174
"test-mockito-core",
7275
"test-mockito-jupiter",
7376
]
77+
78+
ktor = [
79+
"ktor-client-core",
80+
"ktor-client-cio",
81+
"ktor-client-websocket",
82+
]

src/main/java/in/dragonbra/javasteam/networking/steam3/WebSocketCMClient.kt

Lines changed: 0 additions & 100 deletions
This file was deleted.
Lines changed: 136 additions & 66 deletions
Original file line numberDiff line numberDiff line change
@@ -1,105 +1,175 @@
11
package `in`.dragonbra.javasteam.networking.steam3
22

33
import `in`.dragonbra.javasteam.util.log.LogManager
4-
import `in`.dragonbra.javasteam.util.log.Logger
5-
import okhttp3.Response
4+
import io.ktor.client.HttpClient
5+
import io.ktor.client.engine.cio.CIO
6+
import io.ktor.client.plugins.websocket.WebSockets
7+
import io.ktor.client.plugins.websocket.pingInterval
8+
import io.ktor.client.plugins.websocket.webSocketSession
9+
import io.ktor.http.URLProtocol
10+
import io.ktor.http.path
11+
import io.ktor.websocket.Frame
12+
import io.ktor.websocket.WebSocketSession
13+
import io.ktor.websocket.close
14+
import io.ktor.websocket.readBytes
15+
import io.ktor.websocket.readText
16+
import kotlinx.coroutines.CoroutineScope
17+
import kotlinx.coroutines.Dispatchers
18+
import kotlinx.coroutines.Job
19+
import kotlinx.coroutines.SupervisorJob
20+
import kotlinx.coroutines.cancelChildren
21+
import kotlinx.coroutines.channels.consumeEach
22+
import kotlinx.coroutines.delay
23+
import kotlinx.coroutines.isActive
24+
import kotlinx.coroutines.launch
625
import java.net.InetAddress
726
import java.net.InetSocketAddress
8-
import java.net.URI
9-
import java.util.concurrent.atomic.AtomicReference
27+
import kotlin.coroutines.CoroutineContext
28+
import kotlin.time.DurationUnit
29+
import kotlin.time.toDuration
1030

1131
class WebSocketConnection :
1232
Connection(),
13-
WebSocketCMClient.WSListener {
33+
CoroutineScope {
1434

1535
companion object {
16-
private val logger: Logger = LogManager.getLogger(WebSocketConnection::class.java)
17-
18-
private fun constructUri(address: InetSocketAddress): URI =
19-
URI.create("wss://${address.hostString}:${address.port}/cmsocket/")
36+
private val logger = LogManager.getLogger(WebSocketConnection::class.java)
2037
}
2138

22-
private val client = AtomicReference<WebSocketCMClient?>(null)
39+
private val job: Job = SupervisorJob()
2340

24-
private var socketEndPoint: InetSocketAddress? = null
41+
private var client: HttpClient? = null
2542

26-
override fun connect(endPoint: InetSocketAddress, timeout: Int) {
27-
logger.debug("Connecting to $endPoint...")
43+
private var session: WebSocketSession? = null
2844

29-
val serverUri = constructUri(endPoint)
30-
val newClient = WebSocketCMClient(timeout, serverUri, this)
31-
val oldClient = client.getAndSet(newClient)
45+
private var endpoint: InetSocketAddress? = null
3246

33-
oldClient?.let { oldClient ->
34-
logger.debug("Attempted to connect while already connected. Closing old connection...")
35-
oldClient.close()
36-
onDisconnected(false)
37-
}
47+
private var lastFrameTime = System.currentTimeMillis()
3848

39-
socketEndPoint = endPoint
49+
override val coroutineContext: CoroutineContext = Dispatchers.IO + job
4050

41-
newClient.connect()
51+
override fun connect(endPoint: InetSocketAddress, timeout: Int) {
52+
launch {
53+
logger.debug("Trying connection to ${endPoint.hostName}:${endPoint.port}")
54+
55+
try {
56+
endpoint = endPoint
57+
58+
client = HttpClient(CIO) {
59+
install(WebSockets) {
60+
pingInterval = timeout.toDuration(DurationUnit.SECONDS)
61+
}
62+
}
63+
64+
val session = client?.webSocketSession {
65+
url {
66+
host = endPoint.hostName
67+
port = endPoint.port
68+
protocol = URLProtocol.WSS
69+
path("cmsocket/")
70+
}
71+
}
72+
73+
this@WebSocketConnection.session = session
74+
75+
startConnectionMonitoring()
76+
77+
launch {
78+
try {
79+
session?.incoming?.consumeEach { frame ->
80+
when (frame) {
81+
is Frame.Binary -> {
82+
// logger.debug("on Binary ${frame.data.size}")
83+
lastFrameTime = System.currentTimeMillis()
84+
onNetMsgReceived(NetMsgEventArgs(frame.readBytes(), currentEndPoint))
85+
}
86+
87+
is Frame.Close -> disconnect(false)
88+
is Frame.Ping -> logger.debug("Received pong")
89+
is Frame.Pong -> logger.debug("Received pong")
90+
is Frame.Text -> logger.debug("Received plain text ${frame.readText()}")
91+
}
92+
}
93+
} catch (e: Exception) {
94+
logger.error("An error occurred while receiving data", e)
95+
disconnect(false)
96+
}
97+
}
98+
99+
logger.debug("Connected to ${endPoint.hostName}:${endPoint.port}")
100+
onConnected()
101+
} catch (e: Exception) {
102+
logger.error("An error occurred setting up the web socket client", e)
103+
disconnect(false)
104+
}
105+
}
42106
}
43107

44108
override fun disconnect(userInitiated: Boolean) {
45-
disconnectCore(userInitiated)
109+
logger.debug("Disconnect called: $userInitiated")
110+
launch {
111+
try {
112+
session?.close()
113+
client?.close()
114+
} finally {
115+
session = null
116+
client = null
117+
118+
job.cancelChildren()
119+
}
120+
}
121+
122+
onDisconnected(userInitiated)
46123
}
47124

48125
override fun send(data: ByteArray) {
49-
try {
50-
client.get()?.send(data)
51-
} catch (e: Exception) {
52-
logger.debug("Exception while sending data", e)
53-
disconnectCore(false)
126+
launch {
127+
try {
128+
val frame = Frame.Binary(true, data)
129+
session?.send(frame)
130+
} catch (e: Exception) {
131+
logger.error("An error occurred while sending data", e)
132+
disconnect(false)
133+
}
54134
}
55135
}
56136

57-
override fun getLocalIP(): InetAddress? = InetAddress.getByAddress(byteArrayOf(0, 0, 0, 0))
137+
override fun getLocalIP(): InetAddress = InetAddress.getLocalHost()
58138

59-
override fun getCurrentEndPoint(): InetSocketAddress? = socketEndPoint
139+
override fun getCurrentEndPoint(): InetSocketAddress? = endpoint
60140

61141
override fun getProtocolTypes(): ProtocolTypes = ProtocolTypes.WEB_SOCKET
62142

63-
private fun disconnectCore(userInitiated: Boolean) {
64-
logger.debug("User initiated disconnection: $userInitiated")
65-
66-
val oldClient = client.getAndSet(null)
67-
oldClient?.close()
68-
69-
onDisconnected(userInitiated)
143+
/**
144+
* Rudimentary watchdog
145+
*/
146+
private fun startConnectionMonitoring() {
147+
launch {
148+
while (isActive) {
149+
if (client?.isActive == false || session?.isActive == false) {
150+
logger.error("Client or Session is no longer active")
151+
disconnect(userInitiated = false)
152+
}
70153

71-
socketEndPoint = null
72-
}
154+
val timeSinceLastFrame = System.currentTimeMillis() - lastFrameTime
73155

74-
override fun onTextData(data: String) {
75-
// Ignore string messages
76-
logger.debug("Got string message: $data")
77-
}
78-
79-
override fun onData(data: ByteArray) {
80-
if (data.isNotEmpty()) {
81-
onNetMsgReceived(NetMsgEventArgs(data, getCurrentEndPoint()))
82-
}
83-
}
156+
// logger.debug("Watchdog status: $timeSinceLastFrame")
157+
when {
158+
timeSinceLastFrame > 30000 -> {
159+
logger.error("Watchdog: No response for 30 seconds. Disconnecting from steam")
160+
disconnect(userInitiated = false)
161+
break
162+
}
84163

85-
override fun onClose(code: Int, reason: String) {
86-
logger.debug("Connection closed")
87-
}
164+
timeSinceLastFrame > 25000 -> logger.debug("Watchdog: No response for 25 seconds")
88165

89-
override fun onClosing(code: Int, reason: String) {
90-
logger.debug("Closing connection: $code, reason: ${reason.ifEmpty { "No reason given" }}")
91-
// Steam can close a connection if there is nothing else it wants to send.
92-
// For example: AccountLoginDeniedNeedTwoFactor, InvalidPassword, etc.
93-
disconnectCore(code == 1000)
94-
}
166+
timeSinceLastFrame > 20000 -> logger.debug("Watchdog: No response for 20 seconds")
95167

96-
override fun onError(t: Throwable) {
97-
logger.error("Error in websocket", t)
98-
disconnectCore(false)
99-
}
168+
timeSinceLastFrame > 15000 -> logger.debug("Watchdog: No response for 15 seconds")
169+
}
100170

101-
override fun onOpen(response: Response) {
102-
logger.debug("WebSocket connected to $socketEndPoint using TLS: ${response.handshake?.tlsVersion}")
103-
onConnected()
171+
delay(5000)
172+
}
173+
}
104174
}
105175
}

src/main/java/in/dragonbra/javasteam/steam/CMClient.java

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -117,7 +117,12 @@ public CMClient(SteamConfiguration configuration) {
117117

118118
this.configuration = configuration;
119119

120-
heartBeatFunc = new ScheduledFunction(() -> send(new ClientMsgProtobuf<CMsgClientHeartBeat.Builder>(CMsgClientHeartBeat.class, EMsg.ClientHeartBeat)), 5000);
120+
heartBeatFunc = new ScheduledFunction(() -> {
121+
var heartbeat = new ClientMsgProtobuf<CMsgClientHeartBeat.Builder>(
122+
CMsgClientHeartBeat.class, EMsg.ClientHeartBeat);
123+
heartbeat.getBody().setSendReply(true); // Ping Pong
124+
send(heartbeat);
125+
}, 5000);
121126
}
122127

123128
/**

0 commit comments

Comments
 (0)