Skip to content

Commit a66a917

Browse files
committed
Async JS packet handling + native compression.
Native compression is actually slower? So It's disabled at the moment.
1 parent 14dfd67 commit a66a917

12 files changed

+165
-12
lines changed

android/app/build.gradle

+2
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
apply plugin: "com.android.application"
2+
apply plugin: "kotlin-android"
23

34
import com.android.build.OutputFile
45
import org.apache.tools.ant.taskdefs.condition.Os
@@ -260,6 +261,7 @@ android {
260261

261262
dependencies {
262263
implementation fileTree(dir: "libs", include: ["*.jar"])
264+
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlinVersion"
263265

264266
//noinspection GradleDynamicVersion
265267
implementation "com.facebook.react:react-native:+" // From node_modules
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
package com.enderchat
2+
3+
import android.util.Base64
4+
import com.facebook.react.bridge.Promise
5+
import com.facebook.react.bridge.ReactApplicationContext
6+
import com.facebook.react.bridge.ReactContextBaseJavaModule
7+
import com.facebook.react.bridge.ReactMethod
8+
import java.io.ByteArrayOutputStream
9+
import java.util.zip.Deflater
10+
import java.util.zip.Inflater
11+
12+
class CompressionModule(reactContext: ReactApplicationContext)
13+
: ReactContextBaseJavaModule(reactContext) {
14+
15+
override fun getName() = "CompressionModule"
16+
17+
@ReactMethod fun compressData(data: String, promise: Promise) {
18+
val handler = (reactApplicationContext.currentActivity as MainActivity).dataTransformsThreadHandler
19+
val bytes = Base64.decode(data, Base64.DEFAULT)
20+
handler.post {
21+
try {
22+
ByteArrayOutputStream(bytes.size).use {
23+
val deflater = Deflater().apply {
24+
setStrategy(Deflater.DEFAULT_STRATEGY)
25+
setLevel(Deflater.DEFAULT_COMPRESSION)
26+
setInput(bytes)
27+
finish()
28+
}
29+
val buffer = ByteArray(1024)
30+
while (!deflater.finished()) {
31+
val count = deflater.deflate(buffer)
32+
it.write(buffer, 0, count)
33+
}
34+
promise.resolve(Base64.encodeToString(it.toByteArray(), Base64.DEFAULT))
35+
}
36+
} catch (e: Exception) {
37+
promise.reject(e)
38+
}
39+
}
40+
}
41+
42+
@ReactMethod fun decompressData(data: String, promise: Promise) {
43+
val handler = (reactApplicationContext.currentActivity as MainActivity).dataTransformsThreadHandler
44+
val bytes = Base64.decode(data, Base64.DEFAULT)
45+
handler.post {
46+
try {
47+
ByteArrayOutputStream(bytes.size).use {
48+
val inflater = Inflater().apply { setInput(bytes) }
49+
val buffer = ByteArray(1024)
50+
while (!inflater.finished()) {
51+
val count = inflater.inflate(buffer)
52+
it.write(buffer, 0, count)
53+
}
54+
inflater.end()
55+
promise.resolve(Base64.encodeToString(it.toByteArray(), Base64.DEFAULT))
56+
}
57+
} catch (e: Exception) {
58+
promise.reject(e)
59+
}
60+
}
61+
}
62+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
package com.enderchat
2+
3+
import android.view.View
4+
import com.facebook.react.ReactPackage
5+
import com.facebook.react.bridge.NativeModule
6+
import com.facebook.react.bridge.ReactApplicationContext
7+
import com.facebook.react.uimanager.ReactShadowNode
8+
import com.facebook.react.uimanager.ViewManager
9+
10+
class CompressionPackage : ReactPackage {
11+
12+
override fun createViewManagers(
13+
reactContext: ReactApplicationContext
14+
): MutableList<ViewManager<View, ReactShadowNode<*>>> = mutableListOf()
15+
16+
override fun createNativeModules(
17+
reactContext: ReactApplicationContext
18+
): MutableList<NativeModule> = listOf(CompressionModule(reactContext)).toMutableList()
19+
}

android/app/src/main/java/com/enderchat/MainActivity.java

+19
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,16 @@
11
package com.enderchat;
22

33
import android.os.Bundle;
4+
import android.os.Handler;
5+
import android.os.HandlerThread;
6+
import android.os.Process;
47
import com.facebook.react.ReactActivity;
58
import com.facebook.react.ReactActivityDelegate;
69
import com.facebook.react.ReactRootView;
710

811
public class MainActivity extends ReactActivity {
12+
private HandlerThread dataHandlerThread;
13+
private Handler dataTransformsThreadHandler;
914

1015
/**
1116
* Returns the name of the main component registered from JavaScript. This is used to schedule
@@ -42,5 +47,19 @@ protected ReactRootView createRootView() {
4247
@Override
4348
protected void onCreate(Bundle savedInstanceState) {
4449
super.onCreate(null);
50+
dataHandlerThread = new HandlerThread("enderchat-data-handler-thread",
51+
Process.THREAD_PRIORITY_MORE_FAVORABLE);
52+
dataHandlerThread.start();
53+
dataTransformsThreadHandler = new Handler(dataHandlerThread.getLooper());
54+
}
55+
56+
@Override
57+
protected void onDestroy() {
58+
super.onDestroy();
59+
dataHandlerThread.quitSafely();
60+
}
61+
62+
public Handler getDataTransformsThreadHandler() {
63+
return dataTransformsThreadHandler;
4564
}
4665
}

android/app/src/main/java/com/enderchat/MainApplication.java

+1
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ protected List<ReactPackage> getPackages() {
2929
// Packages that cannot be autolinked yet can be added manually here, for example:
3030
// packages.add(new MyReactNativePackage());
3131
packages.add(new NavBarColorPackage());
32+
packages.add(new CompressionPackage());
3233
return packages;
3334
}
3435

android/build.gradle

+2
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import org.apache.tools.ant.taskdefs.condition.Os
44

55
buildscript {
66
ext {
7+
kotlinVersion = '1.6.20'
78
buildToolsVersion = "31.0.0"
89
minSdkVersion = 21
910
compileSdkVersion = 31
@@ -27,6 +28,7 @@ buildscript {
2728
classpath("de.undercouch:gradle-download-task:4.1.2")
2829
// NOTE: Do not place your application dependencies here; they belong
2930
// in the individual module build.gradle files
31+
classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:${project.ext.kotlinVersion}")
3032
}
3133
}
3234

package-lock.json

+14
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

+1
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@
3434
"react-native-tcp": "github:aprock/react-native-tcp",
3535
"react-native-vector-icons": "^7.1.0",
3636
"react-native-webview": "^11.17.1",
37+
"semaphore-async-await": "^1.5.1",
3738
"stream-browserify": "^3.0.0"
3839
},
3940
"devDependencies": {

src/minecraft/connection.ts

+11-7
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
1+
import { InteractionManager } from 'react-native'
2+
import Semaphore from 'semaphore-async-await'
3+
import net from 'react-native-tcp'
4+
import events from 'events'
15
import {
26
Cipher,
37
createCipheriv,
@@ -6,9 +10,6 @@ import {
610
Decipher,
711
publicEncrypt
812
} from 'react-native-crypto'
9-
import { InteractionManager } from 'react-native'
10-
import net from 'react-native-tcp'
11-
import events from 'events'
1213
import {
1314
concatPacketData,
1415
makeBaseCompressedPacket,
@@ -67,8 +68,9 @@ export class ServerConnection extends events.EventEmitter {
6768
data: Buffer,
6869
cb?: ((err?: Error | undefined) => void) | undefined
6970
): Promise<boolean> {
71+
const compressionThreshold = this.compressionThreshold
7072
const packet = this.compressionEnabled
71-
? makeBaseCompressedPacket(this.compressionThreshold, packetId, data)
73+
? await makeBaseCompressedPacket(compressionThreshold, packetId, data)
7274
: makeBasePacket(packetId, data)
7375
const toWrite = this.aesCipher ? this.aesCipher.update(packet) : packet
7476
return this.socket.write(toWrite, cb)
@@ -126,23 +128,24 @@ const initiateConnection = async (opts: ConnectionOptions) => {
126128
if (!resolved) reject(err)
127129
else conn.emit('error', err)
128130
})
131+
const lock = new Semaphore(1)
129132
socket.on('data', newData => {
130133
// Handle timeout after 20 seconds of no data.
131134
if (conn.disconnectTimer) clearTimeout(conn.disconnectTimer)
132135
conn.disconnectTimer = setTimeout(() => conn.close(), 20000)
133136
// Run after interactions to improve user experience.
134-
InteractionManager.runAfterInteractions(() => {
137+
InteractionManager.runAfterInteractions(async () => {
138+
await lock.acquire()
135139
try {
136140
// Note: the entire packet is encrypted, including the length fields and the packet's data.
137141
// https://github.com/PrismarineJS/node-minecraft-protocol/blob/master/src/transforms/encryption.js
138142
let finalData = newData
139143
if (conn.aesDecipher) finalData = conn.aesDecipher.update(newData)
140144
// Buffer data for read.
141145
conn.bufferedData = Buffer.concat([conn.bufferedData, finalData])
142-
// ;(async () => { This would need a mutex.
143146
while (true) {
144147
const packet = conn.compressionEnabled
145-
? parseCompressedPacket(conn.bufferedData)
148+
? await parseCompressedPacket(conn.bufferedData)
146149
: parsePacket(conn.bufferedData)
147150
if (packet) {
148151
// Remove packet from buffered data.
@@ -233,6 +236,7 @@ const initiateConnection = async (opts: ConnectionOptions) => {
233236
} catch (err) {
234237
conn.emit('error', err)
235238
}
239+
lock.release()
236240
}).then(() => {}, console.error)
237241
})
238242
})

src/minecraft/native/compression.ts

+22
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import { NativeModules } from 'react-native'
2+
import zlib from 'zlib'
3+
4+
const { CompressionModule } = NativeModules
5+
const isCompressionModuleAvailable = false
6+
// compressionModule?.compressData && CompressionModule.decompressData
7+
8+
export const compressData = async (data: Buffer): Promise<Buffer> => {
9+
if (isCompressionModuleAvailable) {
10+
return CompressionModule.compressData(data.toString('base64')).then(
11+
(res: string) => Buffer.from(res, 'base64')
12+
)
13+
} else return zlib.deflateSync(data)
14+
}
15+
16+
export const decompressData = async (data: Buffer): Promise<Buffer> => {
17+
if (isCompressionModuleAvailable) {
18+
return CompressionModule.decompressData(data.toString('base64')).then(
19+
(res: string) => Buffer.from(res, 'base64')
20+
)
21+
} else return zlib.unzipSync(data, { finishFlush: 2 })
22+
}

src/minecraft/packet.ts

+7-5
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
1-
import zlib from 'zlib'
21
import { Buffer } from 'buffer'
32
import { toggleEndian, encodeString, readVarInt, writeVarInt } from './utils'
3+
import { compressData, decompressData } from './native/compression'
44

55
export const makeBasePacket = (packetId: number, data: Buffer) => {
66
const finalData = Buffer.concat([writeVarInt(packetId), data])
@@ -9,7 +9,7 @@ export const makeBasePacket = (packetId: number, data: Buffer) => {
99
return Buffer.concat([finalDataLength, finalData])
1010
}
1111

12-
export const makeBaseCompressedPacket = (
12+
export const makeBaseCompressedPacket = async (
1313
threshold: number,
1414
packetId: number,
1515
data: Buffer
@@ -24,7 +24,7 @@ export const makeBaseCompressedPacket = (
2424
/* : await new Promise((resolve, reject) => {
2525
zlib.deflate(finalData, (err, res) => err ? reject(err) : resolve(res))
2626
}) */
27-
const dataToSend = toCompress ? zlib.deflateSync(finalData) : finalData
27+
const dataToSend = toCompress ? await compressData(finalData) : finalData
2828
return makeBasePacket(dataLength, dataToSend)
2929
}
3030

@@ -73,7 +73,9 @@ export const parsePacket = (packet: Buffer): Packet | undefined => {
7373
} catch (e) {} // If the packet is incomplete, readVarInt could error, so no packet parsed.
7474
}
7575

76-
export const parseCompressedPacket = (packet: Buffer): Packet | undefined => {
76+
export const parseCompressedPacket = async (
77+
packet: Buffer
78+
): Promise<Packet | undefined> => {
7779
const dissect = parsePacket(packet)
7880
if (!dissect) return
7981
else if (dissect.id === 0) {
@@ -92,7 +94,7 @@ export const parseCompressedPacket = (packet: Buffer): Packet | undefined => {
9294
const dataLength = dissect.id
9395
let dataWithId: Buffer
9496
try {
95-
dataWithId = zlib.unzipSync(dissect.data, { finishFlush: 2 })
97+
dataWithId = await decompressData(dissect.data)
9698
} catch (e) {
9799
console.error(`problem inflating chunk
98100
uncompressed length: ${dataLength}

yarn.lock

+5
Original file line numberDiff line numberDiff line change
@@ -7568,6 +7568,11 @@
75687568
"loose-envify" "^1.1.0"
75697569
"object-assign" "^4.1.1"
75707570

7571+
"semaphore-async-await@^1.5.1":
7572+
"integrity" "sha1-hXvvXjZEYBykuVcLh+nfXKEpdPo="
7573+
"resolved" "https://registry.npmjs.org/semaphore-async-await/-/semaphore-async-await-1.5.1.tgz"
7574+
"version" "1.5.1"
7575+
75717576
"semver@^5.5.0":
75727577
"integrity" "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ=="
75737578
"resolved" "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz"

0 commit comments

Comments
 (0)