Support ping bedrock
This commit is contained in:
30 files changed
+736
-77
No files matched your search
@@ -23,19 +23,49 @@ repositories {
|
|||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
implementation("cn.rtast.mcping:mcping:0.0.1")
|
implementation("cn.rtast.mcping:mcping:0.0.2")
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Kotlin example
|
||||||
|
|
||||||
```kotlin
|
```kotlin
|
||||||
fun main() {
|
fun main() {
|
||||||
// return a json respose
|
val sm = SelectorManager(Dispatchers.IO)
|
||||||
val response: String = mcping("org.mc-complex.com", 25565)
|
// context is not required, if not passed, default context will be used
|
||||||
println(response)
|
// ping java server
|
||||||
|
val response: PingResponse =
|
||||||
|
mcping(host = "org.mc-complex.com", port = 25565, type = ServerType.Java, context = PingContext(sm))
|
||||||
|
println(response.content)
|
||||||
|
println(response.latency)
|
||||||
|
|
||||||
|
// ping bedrock
|
||||||
|
val response = mcping("play.wildnetwork.net", 19132, ServerType.Bedrock)
|
||||||
|
println(response.toBedrockResponse())
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
> An example json response can be found at [ping-response-example](example/java-ping-response.json) (Formatted)
|
## Java example
|
||||||
|
|
||||||
|
```java
|
||||||
|
void main() {
|
||||||
|
// ping java server
|
||||||
|
String testJavaHost = "org.mc-complex.com";
|
||||||
|
PingResponse javaResponse = McPing.mcping(testJavaHost, 25565);
|
||||||
|
|
||||||
|
// ping bedrock server
|
||||||
|
String testBedrockHost = "play.wildnetwork.net";
|
||||||
|
PingResponse bedrockResponse = McPing.mcping(testBedrockHost, 19132, ServerType.Bedrock);
|
||||||
|
System.out.println(bedrockResponse.toBedrockResponse());
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
> java consumers should not manually pass the context parameter
|
||||||
|
|
||||||
|
## Other resources
|
||||||
|
|
||||||
|
> An example of java ping json response can be found at [ping-response-example](example/java-ping-response.json) (Formatted),
|
||||||
|
> a raw response of bedrock ping response can be found at [bedrock-pinng-raw-response](example/bedrock-pinng-raw-response.txt)
|
||||||
|
|
||||||
# Open Source
|
# Open Source
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -6,7 +6,7 @@ plugins {
|
|||||||
}
|
}
|
||||||
|
|
||||||
group = "cn.rtast.mcping"
|
group = "cn.rtast.mcping"
|
||||||
version = "0.0.1"
|
version = providers.gradleProperty("libVersion").get()
|
||||||
|
|
||||||
repositories {
|
repositories {
|
||||||
mavenCentral()
|
mavenCentral()
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
MCPE;" &e&lWILD&f&lNETWORK &6&lS24 &8-&e discord.gg/WildPrison\n&a Release: &f531d 18h 19m 18s ago";2169;26.45;393;1500;11173189777763089377;Another Geyser server.;Survival;1;19132;0;
|
||||||
+3
-1
@@ -2,4 +2,6 @@ kotlin.code.style=official
|
|||||||
kotlin.native.ignoreDisabledTargets=true
|
kotlin.native.ignoreDisabledTargets=true
|
||||||
kotlin.native.enableKlibsCrossCompilation=true
|
kotlin.native.enableKlibsCrossCompilation=true
|
||||||
kotlin.daemon.jvmargs=-Xmx2048M
|
kotlin.daemon.jvmargs=-Xmx2048M
|
||||||
org.gradle.jvmargs=-Xmx3g -Dfile.encoding=UTF-8
|
org.gradle.jvmargs=-Xmx3g -Dfile.encoding=UTF-8
|
||||||
|
|
||||||
|
libVersion=0.0.2
|
||||||
@@ -0,0 +1,99 @@
|
|||||||
|
/*
|
||||||
|
* Copyright © 2026 RTAkland
|
||||||
|
* Author: RTAkland
|
||||||
|
* Date: 2026/9/3
|
||||||
|
*/
|
||||||
|
|
||||||
|
|
||||||
|
package cn.rtast.mcping.bedrock
|
||||||
|
|
||||||
|
import cn.rtast.mcping.platform.PlatformBuffer
|
||||||
|
import kotlin.random.Random
|
||||||
|
import kotlin.time.Clock
|
||||||
|
|
||||||
|
private val rakNetMagic = byteArrayOf(
|
||||||
|
0x00, 0xFF.toByte(),
|
||||||
|
0xFF.toByte(), 0x00,
|
||||||
|
0xFE.toByte(), 0xFE.toByte(),
|
||||||
|
0xFE.toByte(), 0xFE.toByte(),
|
||||||
|
0xFD.toByte(), 0xFD.toByte(),
|
||||||
|
0xFD.toByte(), 0xFD.toByte(),
|
||||||
|
0x12, 0x34, 0x56, 0x78
|
||||||
|
)
|
||||||
|
|
||||||
|
internal interface MinecraftBedrockPacket {
|
||||||
|
val packetId: Byte
|
||||||
|
val time: Long
|
||||||
|
val magic: ByteArray
|
||||||
|
|
||||||
|
fun writePayload(buffer: PlatformBuffer)
|
||||||
|
}
|
||||||
|
|
||||||
|
internal data class BedrockRequestPacket(
|
||||||
|
override val time: Long,
|
||||||
|
override val magic: ByteArray = rakNetMagic,
|
||||||
|
val guid: Long = Random.nextLong(),
|
||||||
|
) : MinecraftBedrockPacket {
|
||||||
|
override val packetId: Byte = 0x01
|
||||||
|
|
||||||
|
override fun writePayload(buffer: PlatformBuffer) {
|
||||||
|
buffer.writeByte(packetId)
|
||||||
|
buffer.writeLong(time)
|
||||||
|
buffer.writeBytes(magic)
|
||||||
|
buffer.writeLong(guid)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun equals(other: Any?): Boolean {
|
||||||
|
if (this === other) return true
|
||||||
|
if (other == null || this::class != other::class) return false
|
||||||
|
other as BedrockRequestPacket
|
||||||
|
if (time != other.time) return false
|
||||||
|
if (guid != other.guid) return false
|
||||||
|
if (packetId != other.packetId) return false
|
||||||
|
if (!magic.contentEquals(other.magic)) return false
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun hashCode(): Int {
|
||||||
|
var result = time.hashCode()
|
||||||
|
result = 31 * result + guid.hashCode()
|
||||||
|
result = 31 * result + packetId
|
||||||
|
result = 31 * result + magic.contentHashCode()
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
internal data class BedrockResponsePacket(
|
||||||
|
override val packetId: Byte,
|
||||||
|
override val time: Long,
|
||||||
|
val serverGuid: Long,
|
||||||
|
override val magic: ByteArray,
|
||||||
|
val stringLength: Short,
|
||||||
|
val payload: String,
|
||||||
|
) : MinecraftBedrockPacket {
|
||||||
|
override fun writePayload(buffer: PlatformBuffer) {}
|
||||||
|
|
||||||
|
override fun equals(other: Any?): Boolean {
|
||||||
|
if (this === other) return true
|
||||||
|
if (other == null || this::class != other::class) return false
|
||||||
|
other as BedrockResponsePacket
|
||||||
|
if (packetId != other.packetId) return false
|
||||||
|
if (time != other.time) return false
|
||||||
|
if (serverGuid != other.serverGuid) return false
|
||||||
|
if (stringLength != other.stringLength) return false
|
||||||
|
if (!magic.contentEquals(other.magic)) return false
|
||||||
|
if (payload != other.payload) return false
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun hashCode(): Int {
|
||||||
|
var result = packetId.toInt()
|
||||||
|
result = 31 * result + time.hashCode()
|
||||||
|
result = 31 * result + serverGuid.hashCode()
|
||||||
|
result = 31 * result + stringLength
|
||||||
|
result = 31 * result + magic.contentHashCode()
|
||||||
|
result = 31 * result + payload.hashCode()
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
/*
|
||||||
|
* Copyright © 2026 RTAkland
|
||||||
|
* Author: RTAkland
|
||||||
|
* Date: 2026/9/3
|
||||||
|
*/
|
||||||
|
|
||||||
|
|
||||||
|
package cn.rtast.mcping.bedrock
|
||||||
|
|
||||||
|
import cn.rtast.mcping.platform.PlatformBuffer
|
||||||
|
import cn.rtast.mcping.platform.UdpSocket
|
||||||
|
|
||||||
|
internal fun UdpSocket.sendPacket(packet: MinecraftBedrockPacket): ByteArray {
|
||||||
|
val buf = PlatformBuffer()
|
||||||
|
packet.writePayload(buf)
|
||||||
|
return sendAndReceive(buf.toByteArray())
|
||||||
|
}
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
/*
|
||||||
|
* Copyright © 2026 RTAkland
|
||||||
|
* Author: RTAkland
|
||||||
|
* Date: 2026/9/3
|
||||||
|
*/
|
||||||
|
|
||||||
|
|
||||||
|
package cn.rtast.mcping.bedrock
|
||||||
|
|
||||||
|
import cn.rtast.mcping.PingResponse
|
||||||
|
import cn.rtast.mcping.platform.PingContext
|
||||||
|
import cn.rtast.mcping.platform.UdpSocket
|
||||||
|
import cn.rtast.mcping.platform.wrap
|
||||||
|
import kotlin.time.Clock
|
||||||
|
|
||||||
|
internal fun pingBedrockServer(host: String, port: Int, context: PingContext): PingResponse {
|
||||||
|
val socket = UdpSocket(host, port, context)
|
||||||
|
return try {
|
||||||
|
val sendTime = Clock.System.now().toEpochMilliseconds()
|
||||||
|
val packet = BedrockRequestPacket(sendTime)
|
||||||
|
val responseBytes = socket.sendPacket(packet)
|
||||||
|
val receiveTime = Clock.System.now().toEpochMilliseconds()
|
||||||
|
val buf = responseBytes.wrap()
|
||||||
|
buf.readByte() // packet id
|
||||||
|
buf.readLong() // time
|
||||||
|
buf.readLong() // server guid
|
||||||
|
buf.readBytes(16) // magic refer to `rakNetMagic`
|
||||||
|
val payloadLength = buf.readShort()
|
||||||
|
val payload = buf.readBytes(payloadLength.toInt())
|
||||||
|
PingResponse(payload.decodeToString(), (receiveTime - sendTime).toInt())
|
||||||
|
} finally {
|
||||||
|
socket.close()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,116 @@
|
|||||||
|
/*
|
||||||
|
* Copyright © 2026 RTAkland
|
||||||
|
* Author: RTAkland
|
||||||
|
* Date: 2026/9/3
|
||||||
|
*/
|
||||||
|
|
||||||
|
|
||||||
|
package cn.rtast.mcping.bedrock
|
||||||
|
|
||||||
|
import cn.rtast.mcping.PingResponse
|
||||||
|
|
||||||
|
public enum class BedrockGameMode(public val gameMode: String) {
|
||||||
|
Survival("Survival"),
|
||||||
|
Creative("Creative"),
|
||||||
|
Adventure("Adventure"),
|
||||||
|
Spectator("Spectator"), // reserved?
|
||||||
|
Hardcore("Hardcore"),
|
||||||
|
Unknown(""); // reserved
|
||||||
|
|
||||||
|
public companion object {
|
||||||
|
public fun parse(value: String?): BedrockGameMode {
|
||||||
|
return entries.firstOrNull {
|
||||||
|
it.name.equals(value, ignoreCase = true)
|
||||||
|
} ?: Unknown
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public data class BedrockPingResponse(
|
||||||
|
/**
|
||||||
|
* always be `MCPE`
|
||||||
|
* index 0
|
||||||
|
*/
|
||||||
|
val protocolHeader: String,
|
||||||
|
/**
|
||||||
|
* MOTD line 1
|
||||||
|
* index 1
|
||||||
|
*/
|
||||||
|
val motdLine1: String,
|
||||||
|
/**
|
||||||
|
* protocol version
|
||||||
|
* index 2
|
||||||
|
*/
|
||||||
|
val protocolVersion: Int,
|
||||||
|
/**
|
||||||
|
* game version
|
||||||
|
* index 3
|
||||||
|
*/
|
||||||
|
val gameVersion: String,
|
||||||
|
/**
|
||||||
|
* online players
|
||||||
|
* index 4
|
||||||
|
*/
|
||||||
|
val onlinePlayers: Int,
|
||||||
|
/**
|
||||||
|
* maximum players
|
||||||
|
* index 5
|
||||||
|
*/
|
||||||
|
val maximumPlayers: Int,
|
||||||
|
/**
|
||||||
|
* server GUID
|
||||||
|
* parsed as String -> origin bytes count is 8
|
||||||
|
* index 6
|
||||||
|
*/
|
||||||
|
val serverGUID: String,
|
||||||
|
/**
|
||||||
|
* MOTD line 2
|
||||||
|
* index 7
|
||||||
|
*/
|
||||||
|
val motdLine2: String,
|
||||||
|
/**
|
||||||
|
* game mode
|
||||||
|
* index 8
|
||||||
|
*/
|
||||||
|
val gameMode: BedrockGameMode,
|
||||||
|
/**
|
||||||
|
* Nintendo Limited
|
||||||
|
* Nintendo Switch online restriction flag (1 indicates restriction enabled/processed)
|
||||||
|
* index 9
|
||||||
|
*/
|
||||||
|
val nintendoLimited: Boolean,
|
||||||
|
/**
|
||||||
|
* ipv4 port
|
||||||
|
* if disabled or unconfigured, it will be 0
|
||||||
|
* index 10
|
||||||
|
*/
|
||||||
|
val ipv4Port: Int,
|
||||||
|
/**
|
||||||
|
* ipv6 port
|
||||||
|
* if disabled or unconfigured, it will be 0
|
||||||
|
* index 11
|
||||||
|
*/
|
||||||
|
val ipv6Port: Int,
|
||||||
|
/**
|
||||||
|
* latency ms
|
||||||
|
* reserved not exists in response packet
|
||||||
|
*/
|
||||||
|
val latency: Int
|
||||||
|
)
|
||||||
|
|
||||||
|
internal fun PingResponse.parseBedrockPingResponse(): BedrockPingResponse {
|
||||||
|
try {
|
||||||
|
val fields = content.split(";")
|
||||||
|
return BedrockPingResponse(
|
||||||
|
fields[0], fields[1], fields[2].toInt(),
|
||||||
|
fields[3], fields[4].toInt(),
|
||||||
|
fields[5].toInt(), fields[6],
|
||||||
|
fields[7], BedrockGameMode.parse(fields[8]),
|
||||||
|
fields.getOrNull(9) == "1", fields[10].toInt(),
|
||||||
|
fields[11].toInt(), this.latency // pass through PingResponse.latency
|
||||||
|
)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
e.printStackTrace()
|
||||||
|
throw IllegalStateException("The server respond incorrect response: Missing fields or type mismatch")
|
||||||
|
}
|
||||||
|
}
|
||||||
+4
-4
@@ -4,10 +4,10 @@
|
|||||||
* Date: 2026/9/3
|
* Date: 2026/9/3
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package cn.rtast.mcping
|
package cn.rtast.mcping.java
|
||||||
|
|
||||||
import cn.rtast.mcping.platform.PlatformBuffer
|
import cn.rtast.mcping.platform.PlatformBuffer
|
||||||
import cn.rtast.mcping.platform.PlatformReadChannel
|
import cn.rtast.mcping.platform.ReadChannel
|
||||||
|
|
||||||
|
|
||||||
internal fun PlatformBuffer.writeVarInt(value: Int) {
|
internal fun PlatformBuffer.writeVarInt(value: Int) {
|
||||||
@@ -22,7 +22,7 @@ internal fun PlatformBuffer.writeVarInt(value: Int) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
internal fun PlatformReadChannel.readVarInt(): Int {
|
internal fun ReadChannel.readVarInt(): Int {
|
||||||
var value = 0
|
var value = 0
|
||||||
var position = 0
|
var position = 0
|
||||||
while (true) {
|
while (true) {
|
||||||
@@ -41,7 +41,7 @@ internal fun PlatformBuffer.writeMcString(value: String) {
|
|||||||
this.writeBytes(bytes)
|
this.writeBytes(bytes)
|
||||||
}
|
}
|
||||||
|
|
||||||
internal fun PlatformReadChannel.readMcString(): String {
|
internal fun ReadChannel.readMcString(): String {
|
||||||
val length = this.readVarInt()
|
val length = this.readVarInt()
|
||||||
val bytes = this.readBytes(length)
|
val bytes = this.readBytes(length)
|
||||||
return bytes.decodeToString()
|
return bytes.decodeToString()
|
||||||
+6
-1
@@ -5,7 +5,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
|
|
||||||
package cn.rtast.mcping
|
package cn.rtast.mcping.java
|
||||||
|
|
||||||
import cn.rtast.mcping.platform.PlatformBuffer
|
import cn.rtast.mcping.platform.PlatformBuffer
|
||||||
|
|
||||||
@@ -40,4 +40,9 @@ internal data class HandshakePacket(
|
|||||||
internal data object StatusRequestPacket : MinecraftPacket {
|
internal data object StatusRequestPacket : MinecraftPacket {
|
||||||
override val packetId: Int = 0x00
|
override val packetId: Int = 0x00
|
||||||
override fun writePayload(buffer: PlatformBuffer) {}
|
override fun writePayload(buffer: PlatformBuffer) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
internal data class PingPacket(val currentTime: Long) : MinecraftPacket {
|
||||||
|
override val packetId: Int = 0x01
|
||||||
|
override fun writePayload(buffer: PlatformBuffer) = buffer.writeLong(currentTime)
|
||||||
}
|
}
|
||||||
+3
-3
@@ -5,12 +5,12 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
|
|
||||||
package cn.rtast.mcping
|
package cn.rtast.mcping.java
|
||||||
|
|
||||||
import cn.rtast.mcping.platform.PlatformBuffer
|
import cn.rtast.mcping.platform.PlatformBuffer
|
||||||
import cn.rtast.mcping.platform.PlatformWriteChannel
|
import cn.rtast.mcping.platform.WriteChannel
|
||||||
|
|
||||||
internal fun PlatformWriteChannel.sendPacket(packet: MinecraftPacket) {
|
internal fun WriteChannel.sendPacket(packet: MinecraftPacket) {
|
||||||
val bodyBuffer = PlatformBuffer()
|
val bodyBuffer = PlatformBuffer()
|
||||||
bodyBuffer.writeVarInt(packet.packetId)
|
bodyBuffer.writeVarInt(packet.packetId)
|
||||||
packet.writePayload(bodyBuffer)
|
packet.writePayload(bodyBuffer)
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
/*
|
||||||
|
* Copyright © 2026 RTAkland
|
||||||
|
* Author: RTAkland
|
||||||
|
* Date: 2026/9/3
|
||||||
|
*/
|
||||||
|
|
||||||
|
|
||||||
|
package cn.rtast.mcping.java
|
||||||
|
|
||||||
|
import cn.rtast.mcping.PingResponse
|
||||||
|
import cn.rtast.mcping.platform.PingContext
|
||||||
|
import cn.rtast.mcping.platform.Socket
|
||||||
|
import kotlin.time.Clock
|
||||||
|
|
||||||
|
internal fun pingJavaServer(host: String, port: Int, context: PingContext): PingResponse {
|
||||||
|
val socket = Socket(host, port, context)
|
||||||
|
val receiveChannel = socket.openReadChannel()
|
||||||
|
val sendChannel = socket.openWriteChannel()
|
||||||
|
|
||||||
|
return try {
|
||||||
|
val handshakePacket = HandshakePacket(
|
||||||
|
protocolVersion = -1,
|
||||||
|
serverAddress = host,
|
||||||
|
serverPort = port.toUShort(),
|
||||||
|
nextState = 1
|
||||||
|
)
|
||||||
|
sendChannel.sendPacket(handshakePacket)
|
||||||
|
sendChannel.sendPacket(StatusRequestPacket)
|
||||||
|
|
||||||
|
receiveChannel.readVarInt() // consume a varint
|
||||||
|
val packetId = receiveChannel.readVarInt()
|
||||||
|
val jsonResponse = if (packetId == StatusRequestPacket.packetId) receiveChannel.readMcString()
|
||||||
|
else throw IllegalStateException("Server does not respond correct packet id, expected ${StatusRequestPacket.packetId} but got $packetId")
|
||||||
|
|
||||||
|
val sendTime = Clock.System.now().toEpochMilliseconds()
|
||||||
|
val pingPacket = PingPacket(sendTime)
|
||||||
|
sendChannel.sendPacket(pingPacket)
|
||||||
|
receiveChannel.readVarInt() // consume a varint
|
||||||
|
receiveChannel.readVarInt() // packet id
|
||||||
|
receiveChannel.readLong() // pong packet payload
|
||||||
|
PingResponse(jsonResponse, (Clock.System.now().toEpochMilliseconds() - sendTime).toInt())
|
||||||
|
} finally {
|
||||||
|
socket.close()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -4,32 +4,24 @@
|
|||||||
* Date: 2026/9/3
|
* Date: 2026/9/3
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
@file:JvmName("McPing")
|
||||||
|
|
||||||
|
|
||||||
package cn.rtast.mcping
|
package cn.rtast.mcping
|
||||||
|
|
||||||
import cn.rtast.mcping.platform.PlatformSocket
|
import cn.rtast.mcping.bedrock.pingBedrockServer
|
||||||
|
import cn.rtast.mcping.java.pingJavaServer
|
||||||
|
import cn.rtast.mcping.platform.PingContext
|
||||||
|
import kotlin.jvm.JvmName
|
||||||
|
import kotlin.jvm.JvmOverloads
|
||||||
|
|
||||||
|
@JvmOverloads
|
||||||
public fun mcping(host: String, port: Int): String {
|
public fun mcping(
|
||||||
val socket = PlatformSocket(host, port)
|
host: String,
|
||||||
val receiveChannel = socket.openReadChannel()
|
port: Int,
|
||||||
val sendChannel = socket.openWriteChannel()
|
type: ServerType = ServerType.Java,
|
||||||
|
context: PingContext = PingContext(),
|
||||||
return try {
|
): PingResponse = when (type) {
|
||||||
val handshakePacket = HandshakePacket(
|
ServerType.Java -> pingJavaServer(host, port, context)
|
||||||
protocolVersion = -1,
|
ServerType.Bedrock -> pingBedrockServer(host, port, context)
|
||||||
serverAddress = host,
|
|
||||||
serverPort = port.toUShort(),
|
|
||||||
nextState = 1
|
|
||||||
)
|
|
||||||
sendChannel.sendPacket(handshakePacket)
|
|
||||||
sendChannel.sendPacket(StatusRequestPacket)
|
|
||||||
|
|
||||||
receiveChannel.readVarInt() // consume a varint
|
|
||||||
val packetId = receiveChannel.readVarInt()
|
|
||||||
if (packetId == StatusRequestPacket.packetId) receiveChannel.readMcString()
|
|
||||||
else throw IllegalStateException("Server does not respond correct packet id, expected ${StatusRequestPacket.packetId} but got $packetId")
|
|
||||||
} finally {
|
|
||||||
socket.close()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
/*
|
||||||
|
* Copyright © 2026 RTAkland
|
||||||
|
* Author: RTAkland
|
||||||
|
* Date: 2026/9/3
|
||||||
|
*/
|
||||||
|
|
||||||
|
|
||||||
|
package cn.rtast.mcping
|
||||||
|
|
||||||
|
import cn.rtast.mcping.bedrock.BedrockPingResponse
|
||||||
|
import cn.rtast.mcping.bedrock.parseBedrockPingResponse
|
||||||
|
|
||||||
|
public data class PingResponse(
|
||||||
|
/**
|
||||||
|
* raw response content
|
||||||
|
*/
|
||||||
|
public val content: String,
|
||||||
|
/**
|
||||||
|
* latency ms
|
||||||
|
*/
|
||||||
|
public val latency: Int,
|
||||||
|
) {
|
||||||
|
public fun toBedrockResponse(): BedrockPingResponse = parseBedrockPingResponse()
|
||||||
|
}
|
||||||
@@ -7,11 +7,21 @@
|
|||||||
|
|
||||||
package cn.rtast.mcping.platform
|
package cn.rtast.mcping.platform
|
||||||
|
|
||||||
internal expect class PlatformBuffer() {
|
internal expect class PlatformBuffer {
|
||||||
|
constructor()
|
||||||
|
constructor(bytes: ByteArray)
|
||||||
|
|
||||||
fun writeByte(value: Byte)
|
fun writeByte(value: Byte)
|
||||||
|
fun writeShort(value: Short)
|
||||||
|
fun writeLong(value: Long)
|
||||||
fun writeBytes(bytes: ByteArray)
|
fun writeBytes(bytes: ByteArray)
|
||||||
|
|
||||||
fun readByte(): Byte
|
fun readByte(): Byte
|
||||||
|
fun readShort(): Short
|
||||||
|
fun readLong(): Long
|
||||||
fun readBytes(length: Int): ByteArray
|
fun readBytes(length: Int): ByteArray
|
||||||
fun toByteArray(): ByteArray
|
fun toByteArray(): ByteArray
|
||||||
val size: Int
|
val size: Int
|
||||||
}
|
}
|
||||||
|
|
||||||
|
internal fun ByteArray.wrap(): PlatformBuffer = PlatformBuffer(this)
|
||||||
@@ -7,13 +7,14 @@
|
|||||||
|
|
||||||
package cn.rtast.mcping.platform
|
package cn.rtast.mcping.platform
|
||||||
|
|
||||||
internal expect class PlatformReadChannel {
|
internal expect class ReadChannel {
|
||||||
fun readByte(): Byte
|
fun readByte(): Byte
|
||||||
fun readBytes(length: Int): ByteArray
|
fun readBytes(length: Int): ByteArray
|
||||||
fun readFully(out: ByteArray, start: Int = 0, end: Int = out.size)
|
fun readFully(out: ByteArray, start: Int = 0, end: Int = out.size)
|
||||||
|
fun readLong(): Long
|
||||||
}
|
}
|
||||||
|
|
||||||
internal expect class PlatformWriteChannel {
|
internal expect class WriteChannel {
|
||||||
fun writeFully(value: ByteArray, startIndex: Int = 0, endIndex: Int = value.size)
|
fun writeFully(value: ByteArray, startIndex: Int = 0, endIndex: Int = value.size)
|
||||||
fun flush()
|
fun flush()
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
/*
|
||||||
|
* Copyright © 2026 RTAkland
|
||||||
|
* Author: RTAkland
|
||||||
|
* Date: 2026/9/3
|
||||||
|
*/
|
||||||
|
|
||||||
|
|
||||||
|
package cn.rtast.mcping.platform
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An object class used to pass some parameters
|
||||||
|
* that exists only on native targets
|
||||||
|
*/
|
||||||
|
public expect class PingContext()
|
||||||
@@ -7,8 +7,13 @@
|
|||||||
|
|
||||||
package cn.rtast.mcping.platform
|
package cn.rtast.mcping.platform
|
||||||
|
|
||||||
internal expect class PlatformSocket internal constructor(host: String, port: Int){
|
internal expect class Socket internal constructor(host: String, port: Int, context: PingContext) {
|
||||||
fun openReadChannel(): PlatformReadChannel
|
fun openReadChannel(): ReadChannel
|
||||||
fun openWriteChannel(): PlatformWriteChannel
|
fun openWriteChannel(): WriteChannel
|
||||||
|
fun close()
|
||||||
|
}
|
||||||
|
|
||||||
|
internal expect class UdpSocket internal constructor(host: String, port: Int, context: PingContext) {
|
||||||
|
fun sendAndReceive(data: ByteArray): ByteArray
|
||||||
fun close()
|
fun close()
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
/*
|
||||||
|
* Copyright © 2026 RTAkland
|
||||||
|
* Author: RTAkland
|
||||||
|
* Date: 2026/9/3
|
||||||
|
*/
|
||||||
|
|
||||||
|
|
||||||
|
package cn.rtast.mcping
|
||||||
|
|
||||||
|
public enum class ServerType {
|
||||||
|
Java, Bedrock
|
||||||
|
}
|
||||||
@@ -7,6 +7,7 @@
|
|||||||
|
|
||||||
package test
|
package test
|
||||||
|
|
||||||
|
import cn.rtast.mcping.ServerType
|
||||||
import cn.rtast.mcping.mcping
|
import cn.rtast.mcping.mcping
|
||||||
import kotlinx.coroutines.test.runTest
|
import kotlinx.coroutines.test.runTest
|
||||||
import kotlin.test.Test
|
import kotlin.test.Test
|
||||||
@@ -18,4 +19,11 @@ class TestPing {
|
|||||||
val response = mcping("org.mc-complex.com", 25565)
|
val response = mcping("org.mc-complex.com", 25565)
|
||||||
println(response)
|
println(response)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `test ping bedrock server`() = runTest {
|
||||||
|
val response = mcping("play.wildnetwork.net", 19132, ServerType.Bedrock)
|
||||||
|
println(response)
|
||||||
|
println(response.toBedrockResponse())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -8,28 +8,85 @@ package cn.rtast.mcping.platform
|
|||||||
|
|
||||||
import java.io.ByteArrayOutputStream
|
import java.io.ByteArrayOutputStream
|
||||||
|
|
||||||
internal actual class PlatformBuffer actual constructor() {
|
internal actual class PlatformBuffer {
|
||||||
private val stream = ByteArrayOutputStream()
|
private val outStream = ByteArrayOutputStream()
|
||||||
|
private var readBuffer: ByteArray? = null
|
||||||
private var readOffset = 0
|
private var readOffset = 0
|
||||||
|
|
||||||
actual fun writeByte(value: Byte) = stream.write(value.toInt())
|
actual constructor()
|
||||||
actual fun writeBytes(bytes: ByteArray) = stream.write(bytes)
|
|
||||||
|
actual constructor(bytes: ByteArray) {
|
||||||
|
this.readBuffer = bytes
|
||||||
|
outStream.write(bytes)
|
||||||
|
}
|
||||||
|
|
||||||
|
actual fun writeByte(value: Byte) {
|
||||||
|
outStream.write(value.toInt())
|
||||||
|
}
|
||||||
|
|
||||||
|
actual fun writeShort(value: Short) {
|
||||||
|
val v = value.toInt()
|
||||||
|
outStream.write(v shr 8)
|
||||||
|
outStream.write(v)
|
||||||
|
}
|
||||||
|
|
||||||
|
actual fun writeLong(value: Long) {
|
||||||
|
outStream.write((value shr 56).toInt())
|
||||||
|
outStream.write((value shr 48).toInt())
|
||||||
|
outStream.write((value shr 40).toInt())
|
||||||
|
outStream.write((value shr 32).toInt())
|
||||||
|
outStream.write((value shr 24).toInt())
|
||||||
|
outStream.write((value shr 16).toInt())
|
||||||
|
outStream.write((value shr 8).toInt())
|
||||||
|
outStream.write(value.toInt())
|
||||||
|
}
|
||||||
|
|
||||||
|
actual fun writeBytes(bytes: ByteArray) {
|
||||||
|
outStream.write(bytes)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun ensureReadArray(): ByteArray {
|
||||||
|
var buf = readBuffer
|
||||||
|
if (buf == null) {
|
||||||
|
buf = outStream.toByteArray()
|
||||||
|
readBuffer = buf
|
||||||
|
}
|
||||||
|
return buf
|
||||||
|
}
|
||||||
|
|
||||||
actual fun readByte(): Byte {
|
actual fun readByte(): Byte {
|
||||||
val array = stream.toByteArray()
|
val array = ensureReadArray()
|
||||||
if (readOffset >= array.size) throw IndexOutOfBoundsException("Buffer underflow")
|
if (readOffset >= array.size) throw IndexOutOfBoundsException("Buffer underflow")
|
||||||
return array[readOffset++]
|
return array[readOffset++]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
actual fun readShort(): Short {
|
||||||
|
val b1 = readByte().toInt() and 0xFF
|
||||||
|
val b2 = readByte().toInt() and 0xFF
|
||||||
|
return ((b1 shl 8) or b2).toShort()
|
||||||
|
}
|
||||||
|
|
||||||
|
actual fun readLong(): Long {
|
||||||
|
return (readByte().toLong() and 0xFF shl 56) or
|
||||||
|
(readByte().toLong() and 0xFF shl 48) or
|
||||||
|
(readByte().toLong() and 0xFF shl 40) or
|
||||||
|
(readByte().toLong() and 0xFF shl 32) or
|
||||||
|
(readByte().toLong() and 0xFF shl 24) or
|
||||||
|
(readByte().toLong() and 0xFF shl 16) or
|
||||||
|
(readByte().toLong() and 0xFF shl 8) or
|
||||||
|
(readByte().toLong() and 0xFF)
|
||||||
|
}
|
||||||
|
|
||||||
actual fun readBytes(length: Int): ByteArray {
|
actual fun readBytes(length: Int): ByteArray {
|
||||||
val array = stream.toByteArray()
|
val array = ensureReadArray()
|
||||||
if (readOffset + length > array.size) throw IndexOutOfBoundsException("Buffer underflow")
|
if (readOffset + length > array.size) throw IndexOutOfBoundsException("Buffer underflow")
|
||||||
val result = array.copyOfRange(readOffset, readOffset + length)
|
val result = array.copyOfRange(readOffset, readOffset + length)
|
||||||
readOffset += length
|
readOffset += length
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
actual fun toByteArray(): ByteArray = stream.toByteArray()
|
actual fun toByteArray(): ByteArray = outStream.toByteArray()
|
||||||
|
|
||||||
actual val size: Int
|
actual val size: Int
|
||||||
get() = stream.size()
|
get() = outStream.size()
|
||||||
}
|
}
|
||||||
@@ -7,10 +7,11 @@
|
|||||||
|
|
||||||
package cn.rtast.mcping.platform
|
package cn.rtast.mcping.platform
|
||||||
|
|
||||||
|
import java.io.EOFException
|
||||||
import java.io.InputStream
|
import java.io.InputStream
|
||||||
import java.io.OutputStream
|
import java.io.OutputStream
|
||||||
|
|
||||||
internal actual class PlatformReadChannel {
|
internal actual class ReadChannel {
|
||||||
private val _inputStream: InputStream
|
private val _inputStream: InputStream
|
||||||
|
|
||||||
constructor(inputStream: InputStream) {
|
constructor(inputStream: InputStream) {
|
||||||
@@ -28,9 +29,27 @@ internal actual class PlatformReadChannel {
|
|||||||
bytesRead += read
|
bytesRead += read
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
actual fun readLong(): Long {
|
||||||
|
val bytes = ByteArray(8)
|
||||||
|
var read = 0
|
||||||
|
while (read < 8) {
|
||||||
|
val count = _inputStream.read(bytes, read, 8 - read)
|
||||||
|
if (count == -1) throw EOFException()
|
||||||
|
read += count
|
||||||
|
}
|
||||||
|
return ((bytes[0].toLong() and 0xFF shl 56) or
|
||||||
|
(bytes[1].toLong() and 0xFF shl 48) or
|
||||||
|
(bytes[2].toLong() and 0xFF shl 40) or
|
||||||
|
(bytes[3].toLong() and 0xFF shl 32) or
|
||||||
|
(bytes[4].toLong() and 0xFF shl 24) or
|
||||||
|
(bytes[5].toLong() and 0xFF shl 16) or
|
||||||
|
(bytes[6].toLong() and 0xFF shl 8) or
|
||||||
|
(bytes[7].toLong() and 0xFF))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
internal actual class PlatformWriteChannel {
|
internal actual class WriteChannel {
|
||||||
private val _outputStream: OutputStream
|
private val _outputStream: OutputStream
|
||||||
|
|
||||||
constructor(outputStream: OutputStream) {
|
constructor(outputStream: OutputStream) {
|
||||||
@@ -41,4 +60,15 @@ internal actual class PlatformWriteChannel {
|
|||||||
_outputStream.write(value, startIndex, endIndex - startIndex)
|
_outputStream.write(value, startIndex, endIndex - startIndex)
|
||||||
|
|
||||||
actual fun flush() = _outputStream.flush()
|
actual fun flush() = _outputStream.flush()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun InputStream.readNBytes(length: Int): ByteArray {
|
||||||
|
val buffer = ByteArray(length)
|
||||||
|
var totalRead = 0
|
||||||
|
while (totalRead < length) {
|
||||||
|
val read = this.read(buffer, totalRead, length - totalRead)
|
||||||
|
if (read == -1) throw EOFException()
|
||||||
|
totalRead += read
|
||||||
|
}
|
||||||
|
return buffer
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
/*
|
||||||
|
* Copyright © 2026 RTAkland
|
||||||
|
* Author: RTAkland
|
||||||
|
* Date: 2026/9/3
|
||||||
|
*/
|
||||||
|
|
||||||
|
package cn.rtast.mcping.platform
|
||||||
|
|
||||||
|
/**
|
||||||
|
* No Context for jvm targets
|
||||||
|
*/
|
||||||
|
public actual class PingContext
|
||||||
@@ -6,12 +6,33 @@
|
|||||||
|
|
||||||
package cn.rtast.mcping.platform
|
package cn.rtast.mcping.platform
|
||||||
|
|
||||||
import java.net.Socket
|
import java.net.DatagramPacket
|
||||||
|
import java.net.DatagramSocket
|
||||||
|
import java.net.InetSocketAddress
|
||||||
|
import java.net.Socket as JvmSocket
|
||||||
|
|
||||||
internal actual class PlatformSocket internal actual constructor(host: String, port: Int) {
|
internal actual class Socket internal actual constructor(host: String, port: Int, context: PingContext) {
|
||||||
private val socket = Socket(host, port)
|
private val socket = JvmSocket(host, port)
|
||||||
|
|
||||||
|
actual fun openReadChannel(): ReadChannel = ReadChannel(socket.getInputStream())
|
||||||
|
actual fun openWriteChannel(): WriteChannel = WriteChannel(socket.getOutputStream())
|
||||||
|
actual fun close() = socket.close()
|
||||||
|
}
|
||||||
|
|
||||||
|
internal actual class UdpSocket internal actual constructor(host: String, port: Int, context: PingContext) {
|
||||||
|
private val socket = DatagramSocket().apply {
|
||||||
|
soTimeout = 3000
|
||||||
|
connect(InetSocketAddress(host, port))
|
||||||
|
}
|
||||||
|
|
||||||
|
actual fun sendAndReceive(data: ByteArray): ByteArray {
|
||||||
|
socket.send(DatagramPacket(data, data.size))
|
||||||
|
|
||||||
|
val buf = ByteArray(2048)
|
||||||
|
val receivePacket = DatagramPacket(buf, buf.size)
|
||||||
|
socket.receive(receivePacket)
|
||||||
|
return buf.copyOf(receivePacket.length)
|
||||||
|
}
|
||||||
|
|
||||||
actual fun openReadChannel(): PlatformReadChannel = PlatformReadChannel(socket.getInputStream())
|
|
||||||
actual fun openWriteChannel(): PlatformWriteChannel = PlatformWriteChannel(socket.getOutputStream())
|
|
||||||
actual fun close() = socket.close()
|
actual fun close() = socket.close()
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
/*
|
||||||
|
* Copyright © 2026 RTAkland
|
||||||
|
* Author: RTAkland
|
||||||
|
* Date: 2026/9/3
|
||||||
|
*/
|
||||||
|
|
||||||
|
|
||||||
|
package test;
|
||||||
|
|
||||||
|
import cn.rtast.mcping.McPing;
|
||||||
|
import cn.rtast.mcping.PingResponse;
|
||||||
|
import cn.rtast.mcping.ServerType;
|
||||||
|
import org.junit.Test;
|
||||||
|
|
||||||
|
|
||||||
|
public class TestPingInJava {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testPingJava() {
|
||||||
|
String testJavaHost = "org.mc-complex.com";
|
||||||
|
PingResponse resp = McPing.mcping(testJavaHost, 25565);
|
||||||
|
System.out.println(resp);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testPingBedrock() {
|
||||||
|
String testBedrockHost = "play.wildnetwork.net";
|
||||||
|
PingResponse resp = McPing.mcping(testBedrockHost, 19132, ServerType.Bedrock);
|
||||||
|
System.out.println(resp.getContent());
|
||||||
|
System.out.println(resp.getLatency());
|
||||||
|
System.out.println(resp.toBedrockResponse());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
/*
|
||||||
|
* Copyright © 2026 RTAkland
|
||||||
|
* Author: RTAkland
|
||||||
|
* Date: 2026/9/3
|
||||||
|
*/
|
||||||
|
|
||||||
|
|
||||||
|
package test
|
||||||
|
|
||||||
|
import cn.rtast.mcping.ServerType
|
||||||
|
import cn.rtast.mcping.mcping
|
||||||
|
import cn.rtast.mcping.platform.PingContext
|
||||||
|
import io.ktor.network.selector.*
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.IO
|
||||||
|
import kotlinx.coroutines.test.runTest
|
||||||
|
import kotlin.test.Test
|
||||||
|
|
||||||
|
class TestMingwPing {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `test ping java on mingw with selectorManager`() = runTest {
|
||||||
|
val sm = SelectorManager(Dispatchers.IO)
|
||||||
|
val response =
|
||||||
|
mcping(host = "org.mc-complex.com", port = 25565, type = ServerType.Java, context = PingContext(sm))
|
||||||
|
println(response)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `test ping bedrock on mingw with selectorManager`() = runTest {
|
||||||
|
val sm = SelectorManager(Dispatchers.IO)
|
||||||
|
val response =
|
||||||
|
mcping(host = "play.wildnetwork.net", port = 19132, type = ServerType.Bedrock, context = PingContext(sm))
|
||||||
|
println(response)
|
||||||
|
println(response.toBedrockResponse())
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -4,19 +4,32 @@
|
|||||||
* Date: 2026/9/3
|
* Date: 2026/9/3
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
|
||||||
package cn.rtast.mcping.platform
|
package cn.rtast.mcping.platform
|
||||||
|
|
||||||
import kotlinx.io.Buffer
|
import kotlinx.io.Buffer
|
||||||
import kotlinx.io.readByteArray
|
import kotlinx.io.readByteArray
|
||||||
|
|
||||||
internal actual class PlatformBuffer actual constructor() {
|
internal actual class PlatformBuffer {
|
||||||
private val _delegateBuf = Buffer()
|
private val _delegateBuf: Buffer
|
||||||
|
|
||||||
|
actual constructor() {
|
||||||
|
_delegateBuf = Buffer()
|
||||||
|
}
|
||||||
|
|
||||||
|
actual constructor(bytes: ByteArray) {
|
||||||
|
_delegateBuf = Buffer().apply { write(bytes) }
|
||||||
|
}
|
||||||
|
|
||||||
actual fun writeByte(value: Byte) = _delegateBuf.writeByte(value)
|
actual fun writeByte(value: Byte) = _delegateBuf.writeByte(value)
|
||||||
|
actual fun writeShort(value: Short) = _delegateBuf.writeShort(value)
|
||||||
|
actual fun writeLong(value: Long) = _delegateBuf.writeLong(value)
|
||||||
actual fun writeBytes(bytes: ByteArray) = _delegateBuf.write(bytes)
|
actual fun writeBytes(bytes: ByteArray) = _delegateBuf.write(bytes)
|
||||||
|
|
||||||
actual fun readByte(): Byte = _delegateBuf.readByte()
|
actual fun readByte(): Byte = _delegateBuf.readByte()
|
||||||
|
actual fun readShort(): Short = _delegateBuf.readShort()
|
||||||
|
actual fun readLong(): Long = _delegateBuf.readLong()
|
||||||
actual fun readBytes(length: Int): ByteArray = _delegateBuf.readByteArray(length)
|
actual fun readBytes(length: Int): ByteArray = _delegateBuf.readByteArray(length)
|
||||||
|
|
||||||
actual fun toByteArray(): ByteArray = _delegateBuf.peek().readByteArray()
|
actual fun toByteArray(): ByteArray = _delegateBuf.peek().readByteArray()
|
||||||
|
|
||||||
actual val size: Int
|
actual val size: Int
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ package cn.rtast.mcping.platform
|
|||||||
import io.ktor.utils.io.*
|
import io.ktor.utils.io.*
|
||||||
import kotlinx.coroutines.runBlocking
|
import kotlinx.coroutines.runBlocking
|
||||||
|
|
||||||
internal actual class PlatformReadChannel {
|
internal actual class ReadChannel {
|
||||||
private val _readChannel: ByteReadChannel
|
private val _readChannel: ByteReadChannel
|
||||||
|
|
||||||
constructor(readChannel: ByteReadChannel) {
|
constructor(readChannel: ByteReadChannel) {
|
||||||
@@ -19,9 +19,10 @@ internal actual class PlatformReadChannel {
|
|||||||
actual fun readByte(): Byte = runBlocking { _readChannel.readByte() }
|
actual fun readByte(): Byte = runBlocking { _readChannel.readByte() }
|
||||||
actual fun readBytes(length: Int): ByteArray = runBlocking { _readChannel.readByteArray(length) }
|
actual fun readBytes(length: Int): ByteArray = runBlocking { _readChannel.readByteArray(length) }
|
||||||
actual fun readFully(out: ByteArray, start: Int, end: Int) = runBlocking { _readChannel.readFully(out, start, end) }
|
actual fun readFully(out: ByteArray, start: Int, end: Int) = runBlocking { _readChannel.readFully(out, start, end) }
|
||||||
|
actual fun readLong(): Long = runBlocking { _readChannel.readLong() }
|
||||||
}
|
}
|
||||||
|
|
||||||
internal actual class PlatformWriteChannel {
|
internal actual class WriteChannel {
|
||||||
private val _writeChannel: ByteWriteChannel
|
private val _writeChannel: ByteWriteChannel
|
||||||
|
|
||||||
constructor(writeChannel: ByteWriteChannel) {
|
constructor(writeChannel: ByteWriteChannel) {
|
||||||
|
|||||||
@@ -0,0 +1,23 @@
|
|||||||
|
/*
|
||||||
|
* Copyright © 2026 RTAkland
|
||||||
|
* Author: RTAkland
|
||||||
|
* Date: 2026/9/3
|
||||||
|
*/
|
||||||
|
|
||||||
|
@file:Suppress("PropertyName")
|
||||||
|
|
||||||
|
package cn.rtast.mcping.platform
|
||||||
|
|
||||||
|
import io.ktor.network.selector.*
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.IO
|
||||||
|
|
||||||
|
public actual class PingContext actual constructor() {
|
||||||
|
internal var _selectorManager: SelectorManager = SelectorManager(Dispatchers.IO)
|
||||||
|
internal var _autoCloseSelectorManager: Boolean = false
|
||||||
|
|
||||||
|
public constructor(selectorManager: SelectorManager, autoClose: Boolean = false) : this() {
|
||||||
|
_selectorManager = selectorManager
|
||||||
|
_autoCloseSelectorManager = autoClose
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -6,23 +6,40 @@
|
|||||||
|
|
||||||
package cn.rtast.mcping.platform
|
package cn.rtast.mcping.platform
|
||||||
|
|
||||||
import io.ktor.network.selector.*
|
|
||||||
import io.ktor.network.sockets.*
|
import io.ktor.network.sockets.*
|
||||||
import kotlinx.coroutines.Dispatchers
|
import io.ktor.utils.io.core.*
|
||||||
import kotlinx.coroutines.IO
|
|
||||||
import kotlinx.coroutines.runBlocking
|
import kotlinx.coroutines.runBlocking
|
||||||
|
import kotlinx.io.readByteArray
|
||||||
|
|
||||||
internal actual class PlatformSocket internal actual constructor(host: String, port: Int) {
|
internal actual class Socket internal actual constructor(host: String, port: Int, context: PingContext) {
|
||||||
|
private val ctx = context
|
||||||
|
private val socket = runBlocking { aSocket(ctx._selectorManager).tcp().connect(host, port) }
|
||||||
|
|
||||||
private val sm = SelectorManager(Dispatchers.IO)
|
actual fun openReadChannel(): ReadChannel = ReadChannel(socket.openReadChannel())
|
||||||
private val socket = runBlocking { aSocket(sm).tcp().connect(host, port) }
|
actual fun openWriteChannel(): WriteChannel =
|
||||||
|
WriteChannel(socket.openWriteChannel(autoFlush = true))
|
||||||
actual fun openReadChannel(): PlatformReadChannel = PlatformReadChannel(socket.openReadChannel())
|
|
||||||
actual fun openWriteChannel(): PlatformWriteChannel =
|
|
||||||
PlatformWriteChannel(socket.openWriteChannel(autoFlush = true))
|
|
||||||
|
|
||||||
actual fun close() {
|
actual fun close() {
|
||||||
socket.close()
|
socket.close()
|
||||||
sm.close()
|
if (ctx._autoCloseSelectorManager) ctx._selectorManager.close()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
internal actual class UdpSocket internal actual constructor(host: String, port: Int, context: PingContext) {
|
||||||
|
private val ctx = context
|
||||||
|
|
||||||
|
// use bind to create an unconnected socket
|
||||||
|
private val socket = runBlocking { aSocket(ctx._selectorManager).udp().bind() }
|
||||||
|
private val remoteAddress = InetSocketAddress(host, port)
|
||||||
|
|
||||||
|
actual fun sendAndReceive(data: ByteArray): ByteArray = runBlocking {
|
||||||
|
val packet = buildPacket { writeFully(data) }
|
||||||
|
socket.send(Datagram(packet, remoteAddress))
|
||||||
|
socket.receive().packet.readByteArray()
|
||||||
|
}
|
||||||
|
|
||||||
|
actual fun close() {
|
||||||
|
socket.close()
|
||||||
|
if (ctx._autoCloseSelectorManager) ctx._selectorManager.close()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user