Added documents
This commit is contained in:
30 files changed
+1423
-132
No files matched your search
@@ -0,0 +1,31 @@
|
||||
# 组合上下文API
|
||||
|
||||
```kotlin
|
||||
public val CustomProtocolContext: ProtocolContextBuilder.() -> Unit = {
|
||||
rsaEncryptor = RSA1024Encryptor { key, data -> ... }
|
||||
sha1Hasher = Sha1Hasher { serverId, secretKey, publicKey -> ... }
|
||||
cipherFactory = { key -> ... }
|
||||
authProvider = AuthenticationProvider { url, accessToken, uuid, serverIdHash -> ... }
|
||||
socketEngine = MyCustomTCPSocketImpl()
|
||||
}
|
||||
```
|
||||
|
||||
> 如果使用`libmc-protocol-context`中的`DefaultProtocolContext`作为默认上下文实现,
|
||||
> 请参阅[实现 TCPSocket](Impl-TCP-Socket-zh.md)
|
||||
|
||||
# 修改默认实现
|
||||
|
||||
```kotlin
|
||||
fun main() {
|
||||
val myNewProtocolContext = DefaultProtocolContext.withCustom {
|
||||
rsaEncryptor = RSA1024Encryptor { key, data -> ... }
|
||||
sha1Hasher = Sha1Hasher { serverId, secretKey, publicKey -> ... }
|
||||
cipherFactory = { key -> ... }
|
||||
authProvider = AuthenticationProvider { url, accessToken, uuid, serverIdHash -> ... }
|
||||
socketEngine = MyCustomTCPSocketImpl()
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
> 使用`.withCustom`来修改已有的上下文API实现
|
||||
|
||||
@@ -0,0 +1,79 @@
|
||||
# 创建客户端
|
||||
|
||||
```kotlin
|
||||
public fun main() = runBlocking {
|
||||
val client = createMinecraftClient(
|
||||
"127.0.0.1", 25566, "MyBot",
|
||||
generateOfflineUuid("MyBot"),
|
||||
accessToken = null,
|
||||
context = DefaultProtocolContext.withCustom {
|
||||
socketEngine = KtorNetworkEngine()
|
||||
}
|
||||
)
|
||||
client.launch { client.connect() }
|
||||
while (true) {
|
||||
delay(5.seconds)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
> 在上面的示例代码中, 创建了一个`MinecraftClient`, 这个客户端将会使用`MyBot`作为玩家名称连接到`127.0.0.1:25565`的`离线`
|
||||
> 服务器, 并且将底层TCP Socket实现替换为了基于`ktor-network`实现的TCP Socket.
|
||||
> (有关如何创建`SocketEngine`请参阅[实现TCP Socket](Impl-TCP-Socket-zh.md),
|
||||
> 有关如何创建Context请参阅[需要实现的API](README-zh.md#需要实现的api))
|
||||
> `MinecraftClient`实现了`CoroutineContext`, 使用`client.connect()`后将会切换到后台线程执行, 添加阻塞线程代码以免程序直接退出.
|
||||
|
||||
## 连接到在线服务器
|
||||
|
||||
```kotlin
|
||||
private val accessToken = "eyJraWQiOiIw..."
|
||||
|
||||
public fun main() = runBlocking {
|
||||
val client = createMinecraftClient(
|
||||
// 其他参数
|
||||
username = "RTAkland",
|
||||
uuid = Uuid.parse("bb033844-e68e-4909-a636-1a5d1821ddc4"),
|
||||
accessToken = accessToken,
|
||||
// 其他参数
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
> 上面的示例代码中, 将会使用`RTAkland`作为玩家名称连接到服务器
|
||||
|
||||
### 快速获取AccessToken
|
||||
|
||||
打开[minecraft.net](https://minecraft.net)并登录后按下F12, 在Console内输入
|
||||
|
||||
```javascript
|
||||
console.log(`; ${document.cookie}`.split('; bearer_token=').pop().split(';').shift())
|
||||
```
|
||||
|
||||
> AccessToken的有效期为24小时
|
||||
|
||||
# 监听数据包
|
||||
|
||||
```kotlin
|
||||
// 监听接收到的指定类型的数据包, 必须以Clientbound开头
|
||||
client.onPacket<ClientboundLoginSuccessPacket> {
|
||||
println(it)
|
||||
}
|
||||
// 监听所有接收到的数据包
|
||||
client.on { packet, direction ->
|
||||
println("$direction -> $packet")
|
||||
}
|
||||
|
||||
// 监听被发送出去的数据包, 必须以Serverbound开头
|
||||
cli.onSent<ServerboundPongConfigurationPacket> {
|
||||
println(it)
|
||||
}
|
||||
```
|
||||
|
||||
# 发送数据包
|
||||
|
||||
```kotlin
|
||||
// 必须以Serverbound开头的数据包才可以被发送, 否则将会抛出UnsupportedOperationException异常
|
||||
client.networkChannel.sendPacket(
|
||||
ServerboundChatCommandPacket(command = "say Hello from libmc")
|
||||
)
|
||||
```
|
||||
@@ -0,0 +1,67 @@
|
||||
# Before start
|
||||
|
||||
`libmc-protocol-context` 模块中默认实现了 `AES-128-CFB8`, `RSA1024` 以及 `SHA1`,
|
||||
三种加密/签名算法都基于`cryptography-kotlin`及其对应平台的provider
|
||||
|
||||
> 下面给出的示例代码均基于Java内置的密码学API
|
||||
|
||||
# AES-128-CFB8
|
||||
|
||||
```kotlin
|
||||
class JavaAesCipher(sharedKey: ByteArray) : NetworkCipher {
|
||||
private val encryptCipher = Cipher.getInstance("AES/CFB8/NoPadding").apply {
|
||||
init(Cipher.ENCRYPT_MODE, SecretKeySpec(sharedKey, "AES"), IvParameterSpec(sharedKey))
|
||||
}
|
||||
|
||||
private val decryptCipher = Cipher.getInstance("AES/CFB8/NoPadding").apply {
|
||||
init(Cipher.DECRYPT_MODE, SecretKeySpec(sharedKey, "AES"), IvParameterSpec(sharedKey))
|
||||
}
|
||||
|
||||
override fun encrypt(buffer: ByteArray, offset: Int, length: Int) {
|
||||
encryptCipher.update(buffer, offset, length, buffer, offset)
|
||||
}
|
||||
|
||||
override fun decrypt(buffer: ByteArray, offset: Int, length: Int) {
|
||||
decryptCipher.update(buffer, offset, length, buffer, offset)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
# RSA1024
|
||||
|
||||
```kotlin
|
||||
private fun encrypt(publicKeyBytes: ByteArray, data: ByteArray): ByteArray {
|
||||
val keySpec = X509EncodedKeySpec(publicKeyBytes)
|
||||
val keyFactory = KeyFactory.getInstance("RSA")
|
||||
val publicKey = keyFactory.generatePublic(keySpec)
|
||||
val cipher = Cipher.getInstance("RSA/ECB/PKCS1Padding")
|
||||
cipher.init(Cipher.ENCRYPT_MODE, publicKey)
|
||||
return cipher.doFinal(data)
|
||||
}
|
||||
|
||||
public val CustomProtocolContext: ProtocolContextBuilder.() -> Unit = {
|
||||
rsaEncryptor = RSA1024Encryptor { key: ByteArray, data: ByteArray -> encrypt(key, data) }
|
||||
}
|
||||
```
|
||||
|
||||
# SHA1
|
||||
|
||||
```kotlin
|
||||
private fun minecraftServerIdHash(serverId: String, secretKey: ByteArray, publicKey: ByteArray): String {
|
||||
val serverIdBytes: ByteArray = serverId.encodeToByteArray()
|
||||
for (b in serverIdBytes) require((b.toInt() and 0xFF) <= 0x7F) { "serverId contains non-US-ASCII character" }
|
||||
val data = ByteArray(serverIdBytes.size + secretKey.size + publicKey.size)
|
||||
System.arraycopy(serverIdBytes, 0, data, 0, serverIdBytes.size)
|
||||
System.arraycopy(secretKey, 0, data, serverIdBytes.size, secretKey.size)
|
||||
System.arraycopy(publicKey, 0, data, serverIdBytes.size + secretKey.size, publicKey.size)
|
||||
val digest = MessageDigest.getInstance("SHA-1")
|
||||
val hash = digest.digest(data)
|
||||
return BigInteger(hash).toString(16)
|
||||
}
|
||||
|
||||
public val CustomProtocolContext: ProtocolContextBuilder.() -> Unit = {
|
||||
sha1Hasher = Sha1Hasher { serverId: String, secretKey: ByteArray, publicKey: ByteArray ->
|
||||
minecraftServerIdHash(serverId, secretKey, publicKey)
|
||||
}
|
||||
}
|
||||
```
|
||||
@@ -0,0 +1,45 @@
|
||||
# 前言
|
||||
|
||||
`libmc-protocol-context` 中提供了默认HTTP客户端实现, 如果使用了此模块则不需要手动配置HTTP客户端认证部分
|
||||
|
||||
# 基于Ktor的HTTP客户端
|
||||
|
||||
```kotlin
|
||||
private val httpClient = HttpClient()
|
||||
|
||||
public val CustomProtocolContext: ProtocolContextBuilder.() -> Unit = {
|
||||
authProvider = AuthenticationProvider { url, accessToken, uuid, serverIdHash ->
|
||||
val status = httpClient.post(url) {
|
||||
headers { header("Content-Type", "application/json") }
|
||||
setBody("{\"accessToken\":\"$accessToken\", \"selectedProfile\":\"$uuid\", \"serverId\":\"$serverIdHash\"}")
|
||||
}.status
|
||||
require(status == HttpStatusCode.NoContent)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
# 基于Java8 HttpURLConnection的HTTP客户端
|
||||
|
||||
```kotlin
|
||||
private fun sendJoinRequest(url: String, accessToken: String, uuid: String, serverIdHash: String): Boolean {
|
||||
val connection = (URL(url).openConnection() as HttpURLConnection).apply {
|
||||
requestMethod = "POST"
|
||||
setRequestProperty("Content-Type", "application/json")
|
||||
doOutput = true
|
||||
connectTimeout = 5000
|
||||
readTimeout = 5000
|
||||
}
|
||||
val jsonPayload = """{"accessToken":"$accessToken","selectedProfile":"$uuid","serverId":"$serverIdHash"}"""
|
||||
connection.outputStream.use { os -> os.write(jsonPayload.toByteArray(Charsets.UTF_8)) }
|
||||
val responseCode = connection.responseCode
|
||||
connection.disconnect()
|
||||
return responseCode == HttpURLConnection.HTTP_NO_CONTENT
|
||||
}
|
||||
|
||||
public val CustomProtocolContext: ProtocolContextBuilder.() -> Unit = {
|
||||
authProvider = AuthenticationProvider { url, accessToken, uuid, serverIdHash ->
|
||||
val success = sendJoinRequest(url, accessToken, uuid, serverIdHash)
|
||||
require(success) { "Failed to authenticate with Mojang session server" }
|
||||
}
|
||||
}
|
||||
```
|
||||
@@ -0,0 +1,327 @@
|
||||
# 前言
|
||||
|
||||
如果底层TCP Socket库是非挂起的, 那么**不要**将IO读写操作使用`withContext`包裹,
|
||||
这会导致在网络IO中频繁的切换线程导致程序性能**下降**.
|
||||
|
||||
# 基于Netty的TCP Socket实现
|
||||
|
||||
```kotlin
|
||||
dependencies {
|
||||
implementation("io.netty:netty-transport:4.2.17.Final")
|
||||
implementation("io.netty:netty-buffer:4.2.17.Final")
|
||||
}
|
||||
```
|
||||
|
||||
<details>
|
||||
<summary>点击展开代码</summary>
|
||||
|
||||
```kotlin
|
||||
import cn.rtast.libmc.network.RawSocket
|
||||
import cn.rtast.libmc.network.ReadChannel
|
||||
import cn.rtast.libmc.network.SocketEngine
|
||||
import cn.rtast.libmc.network.WriteChannel
|
||||
import io.netty.bootstrap.Bootstrap
|
||||
import io.netty.buffer.ByteBuf
|
||||
import io.netty.buffer.Unpooled
|
||||
import io.netty.channel.*
|
||||
import io.netty.channel.nio.NioIoHandler
|
||||
import io.netty.channel.socket.SocketChannel
|
||||
import io.netty.channel.socket.nio.NioSocketChannel
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.suspendCancellableCoroutine
|
||||
import kotlinx.coroutines.sync.Mutex
|
||||
import kotlinx.coroutines.sync.withLock
|
||||
import kotlinx.coroutines.withContext
|
||||
import kotlin.coroutines.resume
|
||||
import kotlin.coroutines.resumeWithException
|
||||
import io.netty.channel.Channel as NettyChannel
|
||||
import kotlinx.coroutines.channels.Channel as KotlinChannel
|
||||
|
||||
public class NettyNetworkSocketEngine : SocketEngine {
|
||||
override fun create(host: String, port: Int): RawSocket = NettyNetworkSocket(host, port)
|
||||
|
||||
private class NettyNetworkSocket(private val host: String, private val port: Int) : RawSocket {
|
||||
private val workerGroup: EventLoopGroup = MultiThreadIoEventLoopGroup(0, NioIoHandler.newFactory())
|
||||
private lateinit var channel: NettyChannel
|
||||
private val inboundQueue = KotlinChannel<ByteArray>(KotlinChannel.UNLIMITED)
|
||||
private lateinit var readChannel: NettyReadChannel
|
||||
private lateinit var writeChannel: NettyWriteChannel
|
||||
|
||||
override suspend fun connect() {
|
||||
val bootstrap = Bootstrap()
|
||||
.group(workerGroup)
|
||||
.channel(NioSocketChannel::class.java)
|
||||
.option(ChannelOption.TCP_NODELAY, true)
|
||||
.option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 10000)
|
||||
.handler(object : ChannelInitializer<SocketChannel>() {
|
||||
override fun initChannel(ch: SocketChannel) {
|
||||
ch.pipeline().addLast(object : ChannelInboundHandlerAdapter() {
|
||||
override fun channelRead(ctx: ChannelHandlerContext, msg: Any) {
|
||||
if (msg is ByteBuf) {
|
||||
try {
|
||||
val bytes = ByteArray(msg.readableBytes())
|
||||
msg.readBytes(bytes)
|
||||
inboundQueue.trySend(bytes)
|
||||
} finally {
|
||||
msg.release()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun exceptionCaught(ctx: ChannelHandlerContext, cause: Throwable) {
|
||||
inboundQueue.close(cause)
|
||||
ctx.close()
|
||||
}
|
||||
|
||||
override fun channelInactive(ctx: ChannelHandlerContext) {
|
||||
inboundQueue.close()
|
||||
super.channelInactive(ctx)
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
channel = withContext(Dispatchers.IO) {
|
||||
bootstrap.connect(host, port).suspendAwait()
|
||||
}
|
||||
|
||||
readChannel = NettyReadChannel(inboundQueue)
|
||||
writeChannel = NettyWriteChannel(channel)
|
||||
}
|
||||
|
||||
override fun openReadChannel(): ReadChannel = readChannel
|
||||
override fun openWriteChannel(): WriteChannel = writeChannel
|
||||
|
||||
override fun close() {
|
||||
if (::channel.isInitialized && channel.isOpen) {
|
||||
channel.close()
|
||||
}
|
||||
inboundQueue.close()
|
||||
workerGroup.shutdownGracefully()
|
||||
}
|
||||
}
|
||||
|
||||
private class NettyReadChannel(private val inboundQueue: KotlinChannel<ByteArray>) : ReadChannel {
|
||||
private var currentChunk: ByteArray? = null
|
||||
private var chunkOffset = 0
|
||||
|
||||
private suspend fun fetchNextChunk() {
|
||||
val next = inboundQueue.receiveCatching().getOrNull()
|
||||
?: throw IllegalStateException("Socket/Channel closed while reading")
|
||||
currentChunk = next
|
||||
chunkOffset = 0
|
||||
}
|
||||
|
||||
override suspend fun readByte(): Byte {
|
||||
while (currentChunk == null || chunkOffset >= currentChunk!!.size) {
|
||||
fetchNextChunk()
|
||||
}
|
||||
val chunk = currentChunk!!
|
||||
return chunk[chunkOffset++]
|
||||
}
|
||||
|
||||
override suspend fun readBytes(length: Int): ByteArray {
|
||||
val result = ByteArray(length)
|
||||
readFully(result, 0, length)
|
||||
return result
|
||||
}
|
||||
|
||||
override suspend fun readFully(out: ByteArray, start: Int, end: Int) {
|
||||
var written = start
|
||||
|
||||
while (written < end) {
|
||||
while (currentChunk == null || chunkOffset >= currentChunk!!.size) {
|
||||
fetchNextChunk()
|
||||
}
|
||||
val chunk = currentChunk!!
|
||||
val available = chunk.size - chunkOffset
|
||||
val toCopy = minOf(available, end - written)
|
||||
|
||||
chunk.copyInto(
|
||||
out,
|
||||
destinationOffset = written,
|
||||
startIndex = chunkOffset,
|
||||
endIndex = chunkOffset + toCopy
|
||||
)
|
||||
chunkOffset += toCopy
|
||||
written += toCopy
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private class NettyWriteChannel(private val channel: NettyChannel) : WriteChannel {
|
||||
private val writeMutex = Mutex()
|
||||
|
||||
override suspend fun writeFully(value: ByteArray, startIndex: Int, endIndex: Int) {
|
||||
val length = endIndex - startIndex
|
||||
if (length <= 0) return
|
||||
|
||||
val nettyBuf = Unpooled.copiedBuffer(value, startIndex, length)
|
||||
|
||||
writeMutex.withLock {
|
||||
withContext(Dispatchers.IO) {
|
||||
channel.writeAndFlush(nettyBuf).suspendAwait()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun flush() {
|
||||
writeMutex.withLock {
|
||||
withContext(Dispatchers.IO) {
|
||||
channel.flush()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private suspend inline fun ChannelFuture.suspendAwait(): NettyChannel = suspendCancellableCoroutine { cont ->
|
||||
if (isDone) {
|
||||
if (isSuccess) cont.resume(channel())
|
||||
else cont.resumeWithException(cause() ?: RuntimeException("Netty operation failed"))
|
||||
return@suspendCancellableCoroutine
|
||||
}
|
||||
|
||||
addListener { future ->
|
||||
if (future.isSuccess) {
|
||||
cont.resume(channel())
|
||||
} else {
|
||||
cont.resumeWithException(future.cause() ?: RuntimeException("Netty operation failed"))
|
||||
}
|
||||
}
|
||||
cont.invokeOnCancellation { cancel(false) }
|
||||
|
||||
}
|
||||
```
|
||||
|
||||
> 请一并复制import部分的代码
|
||||
|
||||
</details>
|
||||
|
||||
# 基于ktor-network的TCP Socket实现
|
||||
|
||||
```kotlin
|
||||
dependencies {
|
||||
implementation("io.ktor:ktor-network:3.5.2")
|
||||
}
|
||||
```
|
||||
|
||||
<details>
|
||||
<summary>点击展开代码</summary>
|
||||
|
||||
```kotlin
|
||||
public class KtorNetworkEngine : SocketEngine {
|
||||
override fun create(host: String, port: Int): RawSocket = KtorNetworkSocket(host, port)
|
||||
}
|
||||
|
||||
public class KtorNetworkSocket(private val host: String, private val port: Int) : RawSocket {
|
||||
private val sm = SelectorManager(Dispatchers.IO)
|
||||
private lateinit var socket: Socket
|
||||
|
||||
override suspend fun connect(): Unit = run { socket = aSocket(sm).tcp().connect(host, port) }
|
||||
override fun openReadChannel(): ReadChannel = KtorReadChannel(socket.openReadChannel())
|
||||
override fun openWriteChannel(): WriteChannel = KtorWriteChannel(socket.openWriteChannel())
|
||||
override fun close() {
|
||||
socket.close()
|
||||
sm.close()
|
||||
}
|
||||
}
|
||||
|
||||
public class KtorReadChannel(private val readChannel: ByteReadChannel) : ReadChannel {
|
||||
override suspend fun readByte(): Byte = readChannel.readByte()
|
||||
override suspend fun readBytes(length: Int): ByteArray = readChannel.readByteArray(length)
|
||||
override suspend fun readFully(out: ByteArray, start: Int, end: Int): Unit =
|
||||
readChannel.readFully(out, start, end)
|
||||
}
|
||||
|
||||
public class KtorWriteChannel(private val writeChannel: ByteWriteChannel) : WriteChannel {
|
||||
override suspend fun writeFully(value: ByteArray, startIndex: Int, endIndex: Int) {
|
||||
writeChannel.writeFully(value, startIndex, endIndex)
|
||||
}
|
||||
|
||||
override suspend fun flush(): Unit = writeChannel.flush()
|
||||
}
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
# 基于Java内置Socket的TCP Socket实现
|
||||
|
||||
<details>
|
||||
<summary>点击展开代码</summary>
|
||||
|
||||
```kotlin
|
||||
class JavaSocketEngine : SocketEngine {
|
||||
override fun create(host: String, port: Int): RawSocket = JavaNetworkSocket(host, port)
|
||||
}
|
||||
|
||||
class JavaNetworkSocket(
|
||||
private val host: String,
|
||||
private val port: Int,
|
||||
private val connectTimeoutMs: Int = 10000,
|
||||
) : RawSocket {
|
||||
private lateinit var socket: Socket
|
||||
private lateinit var readChannel: JavaReadChannel
|
||||
private lateinit var writeChannel: JavaWriteChannel
|
||||
|
||||
override suspend fun connect() {
|
||||
val s = Socket()
|
||||
s.tcpNoDelay = true
|
||||
s.connect(InetSocketAddress(host, port), connectTimeoutMs)
|
||||
|
||||
socket = s
|
||||
readChannel = JavaReadChannel(s.getInputStream())
|
||||
writeChannel = JavaWriteChannel(s.getOutputStream())
|
||||
}
|
||||
|
||||
override fun openReadChannel(): ReadChannel = readChannel
|
||||
override fun openWriteChannel(): WriteChannel = writeChannel
|
||||
|
||||
override fun close() {
|
||||
if (::socket.isInitialized && !socket.isClosed) {
|
||||
runCatching { socket.close() }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class JavaReadChannel(private val inputStream: InputStream) : ReadChannel {
|
||||
override suspend fun readByte(): Byte {
|
||||
val b = inputStream.read()
|
||||
if (b == -1) throw IllegalStateException("Socket stream reached EOF while reading byte")
|
||||
return b.toByte()
|
||||
}
|
||||
|
||||
override suspend fun readBytes(length: Int): ByteArray {
|
||||
val buffer = ByteArray(length)
|
||||
readFullyInternal(buffer, 0, length)
|
||||
return buffer
|
||||
}
|
||||
|
||||
override suspend fun readFully(out: ByteArray, start: Int, end: Int) {
|
||||
readFullyInternal(out, start, end - start)
|
||||
}
|
||||
|
||||
private fun readFullyInternal(out: ByteArray, offset: Int, length: Int) {
|
||||
var bytesRead = 0
|
||||
while (bytesRead < length) {
|
||||
val count = inputStream.read(out, offset + bytesRead, length - bytesRead)
|
||||
if (count == -1) {
|
||||
throw IllegalStateException("Socket stream closed unexpectedly (read $bytesRead of $length bytes)")
|
||||
}
|
||||
bytesRead += count
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class JavaWriteChannel(private val outputStream: OutputStream) : WriteChannel {
|
||||
override suspend fun writeFully(value: ByteArray, startIndex: Int, endIndex: Int) {
|
||||
outputStream.write(value, startIndex, endIndex - startIndex)
|
||||
}
|
||||
|
||||
override suspend fun flush() {
|
||||
outputStream.flush()
|
||||
}
|
||||
}
|
||||
```
|
||||
</details>
|
||||
@@ -0,0 +1,28 @@
|
||||
# libmc
|
||||
|
||||
一个轻量级适用于Kotlin Native & Jvm的现代Minecraft客户端协议库, 核心协议库模块使用了以下依赖
|
||||
|
||||
- 标准库 (`kotlin-stdlib`)
|
||||
- IO (`kotlinx-io`) - 高效的将每个数据包封装为一个Buffer
|
||||
- 协程库 (`kotlinx-coroutines`) - 实现高性能异步操作
|
||||
|
||||
## 需要实现的API
|
||||
|
||||
| 名称 | 是否必须实现 | 描述 |
|
||||
|:-------------|:-------------|:-----------------------------------------------------------------------------------------------------------------------------------------------|
|
||||
| TCP Socket | 是 | `protocol` 模块没有内置TCP Socket实现. [实现 TCP Socket](Impl-TCP-Socket-zh.md) |
|
||||
| AES-128-CFB8 | 视情况而定 | 仅在登录开启了正版验证(`online-mode`)的服务器时需要, 用于加密/解密流量. [实现AES-128-CFB8](Impl-Crypto-zh.md#AES-128-CFB8) |
|
||||
| RSA 1024 | 视情况而定 | 仅在登录开启了正版验证(`online-mode`)的服务器时需要, 用于对服务器下发的公钥进行签名. [实现RSA 1024](Impl-Crypto-zh.md#RSA1024) |
|
||||
| SHA1 | 视情况而定 | 仅在登录开启了正版验证(`online-mode`)的服务器时需要, 用于计算服务器的ServerId. [实现SHA1](Impl-Crypto-zh.md#SHA1) |
|
||||
| HTTP 客户端 | 视情况而定 | 仅在登录开启了正版验证(`online-mode`)的服务器时需要, 用于向Mojang的Session服务器发送加入服务器的请求 [实现HTTP 客户端](Impl-HTTP-Client-zh.md) |
|
||||
|
||||
# 开始使用
|
||||
|
||||
> `libmc-protocol`目前正在开发中, 也许存在着某些未被发现的问题.
|
||||
> 它仅支持最新的Minecraft版本 (目前支持的游戏版本: `26.2`, 协议版本: `776`)
|
||||
|
||||
[开始使用libmc-protocol](Get-started-zh.md)
|
||||
|
||||
# 将实现的API组合起来
|
||||
|
||||
[将所有上下文API组合](Assemble-context-zh.md)
|
||||
Reference in New Issue
Block a user