Support ping bedrock

This commit is contained in:
2026-09-03 22:47:27 +08:00
parent b3583ba48d
commit 8eff6ff9a9
30 files changed
+736 -77

No files matched your search

+35 -5
View File
@@ -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
View File
@@ -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()
+1
View File
@@ -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;
+2
View File
@@ -3,3 +3,5 @@ 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,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()
@@ -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
@@ -41,3 +41,8 @@ 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)
}
@@ -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()
}
}
+16 -24
View File
@@ -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
}
+8
View File
@@ -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) {
@@ -42,3 +61,14 @@ internal actual class PlatformWriteChannel {
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()
} }
+33
View File
@@ -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()
} }
} }