Remove ktor-network dependency use native socket

This commit is contained in:
2025-09-30 10:50:08 +08:00
parent dceb9953af
commit 52f3756be3
21 files changed
+649 -179

No files matched your search

@@ -0,0 +1,69 @@
/*
* Copyright © 2025 RTAkland
* Author: RTAkland
* Date: 9/30/25
*/
@file:OptIn(ExperimentalForeignApi::class)
@file:Suppress("unused")
package cn.rtast.nmdns
import kotlinx.cinterop.*
import platform.darwin.inet_addr
import platform.posix.*
public actual class Socket1 internal actual constructor() {
/**
* socket fd
*/
private var socket: Int = -1
public actual fun bind(ip: String, port: Int): Socket1 {
socket = socket(AF_INET, SOCK_DGRAM, 0)
if (socket < 0) perror("Failed to create udp socket")
memScoped {
val reuse = alloc<IntVar>()
reuse.value = 1
if (setsockopt(socket, SOL_SOCKET, SO_REUSEADDR, reuse.ptr, sizeOf<IntVar>().convert()) < 0) {
perror("Failed to setsocketopt SO_REUSEADDR")
}
}
memScoped {
val addr = alloc<sockaddr_in>()
addr.sin_family = AF_INET.convert()
addr.sin_port = htons(port.toUShort())
addr.sin_addr.s_addr = htonl(INADDR_ANY)
if (bind(socket, addr.ptr.reinterpret(), sizeOf<sockaddr_in>().convert()) < 0) {
perror("Failed to bind")
close(socket)
}
}
memScoped {
val mreq = alloc<ip_mreq>()
mreq.imr_multiaddr.s_addr = inet_addr("224.0.0.251")
mreq.imr_interface.s_addr = htonl(INADDR_ANY)
if (setsockopt(socket, IPPROTO_TP, IP_ADD_MEMBERSHIP, mreq.ptr, sizeOf<ip_mreq>().convert()) < 0) {
perror("setsockopt IP_ADD_MEMBERSHIP")
}
}
return this
}
public actual fun send(packet: ByteArray) {
memScoped {
val addr = alloc<sockaddr_in>()
addr.sin_family = AF_INET.convert()
addr.sin_port = htons(5353u)
addr.sin_addr.s_addr = inet_addr("224.0.0.251")
val cbuf = packet.refTo(0).getPointer(this)
sendto(socket, cbuf, packet.size.convert(), 0, addr.ptr.reinterpret(), sizeOf<sockaddr_in>().convert())
}
}
public actual fun destroy() {
close(socket)
}
}
@@ -0,0 +1,13 @@
/*
* Copyright © 2025 RTAkland
* Author: RTAkland
* Date: 9/30/25
*/
package cn.rtast.nmdns
@Target(AnnotationTarget.FUNCTION)
@Retention(AnnotationRetention.BINARY)
@RequiresOptIn
public annotation class CExport
+10 -34
View File
@@ -7,40 +7,16 @@
package cn.rtast.nmdns
private val macAddresses = listOf(
"ef:42:0d:76:f1:97",
"b4:91:7c:be:84:7b",
"38:d0:d3:f5:63:d1",
"90:d0:d5:3e:5b:fa",
"4d:84:d4:dd:7e:b4",
"11:1c:fb:06:6a:4a",
"cf:97:be:b3:18:5b",
"0c:34:bf:d8:55:71",
"95:a0:71:10:31:ab",
"4c:33:b8:4a:60:8e",
"fc:89:a5:f2:b6:3c",
"a7:42:99:34:b5:21",
"9a:55:3f:5d:bb:9d",
"2b:35:ce:77:a0:7c",
"7d:51:21:72:c5:b4",
"79:7d:cc:e1:30:9b",
"aa:a2:cf:b5:e3:3c",
"8d:73:3f:8e:ef:d7",
"21:3c:a4:56:80:c5",
"c1:fa:ca:cb:ec:f3",
"51:76:e2:12:2f:af",
"a8:03:22:2d:b9:80",
"2a:84:3b:ad:9c:d9",
"24:79:c3:ab:0f:b5",
"bc:44:e3:c4:9e:be",
"cb:1b:84:c2:94:ba",
"9d:66:ce:d2:6e:4f",
"43:bf:e2:b2:e3:78",
"48:25:03:bb:9d:35",
"c3:c9:1c:3d:ae:dd"
)
import kotlin.experimental.ExperimentalNativeApi
import kotlin.native.CName
import kotlin.random.Random
/**
* get random mac address
* generate random mac address
*/
fun randomMacAddress(): String = macAddresses.random()
@OptIn(ExperimentalNativeApi::class)
@CName("random_mac_address")
public fun randomMacAddress(): String {
val bytes = ByteArray(6) { Random.nextInt(0, 256).toByte() }
return bytes.joinToString(":") { it.toHexString() }
}
@@ -5,19 +5,14 @@
*/
@file:OptIn(ExperimentalNativeApi::class)
package cn.rtast.nmdns
import io.ktor.network.selector.*
import io.ktor.network.sockets.*
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.Dispatchers
import kotlinx.io.Buffer
import kotlin.experimental.ExperimentalNativeApi
import kotlin.native.CName
data class NMDNSAnnouncer(
/**
* coroutine dispatcher for running server
*/
internal val dispatcher: CoroutineDispatcher = Dispatchers.Default,
public data class NMDNSAnnouncer(
/**
* service type
* airplay service type: _airplay._tcp.local.
@@ -50,12 +45,60 @@ data class NMDNSAnnouncer(
/**
* mdns socket server
*/
internal val server: BoundDatagramSocket,
internal val server: Socket1,
/**
* srv packet
*/
internal val packet: Buffer,
)
internal val packet: ByteArray,
) {
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (other == null || this::class != other::class) return false
other as NMDNSAnnouncer
if (port != other.port) return false
if (mdnsPort != other.mdnsPort) return false
if (serviceType != other.serviceType) return false
if (serviceName != other.serviceName) return false
if (hostname != other.hostname) return false
if (ipAddress != other.ipAddress) return false
if (txtRecords != other.txtRecords) return false
if (server != other.server) return false
if (!packet.contentEquals(other.packet)) return false
return true
}
override fun hashCode(): Int {
var result = port
result = 31 * result + mdnsPort
result = 31 * result + serviceType.hashCode()
result = 31 * result + serviceName.hashCode()
result = 31 * result + hostname.hashCode()
result = 31 * result + ipAddress.hashCode()
result = 31 * result + txtRecords.hashCode()
result = 31 * result + server.hashCode()
result = 31 * result + packet.contentHashCode()
return result
}
/**
* broadcast packet
*/
public fun broadcast() {
this.server.send(packet)
}
}
/**
* export for c
*/
@CExport
@CName("broadcast")
public fun broadcast1(server: NMDNSAnnouncer) {
server.server.send(server.packet)
}
/**
* register service
@@ -68,7 +111,8 @@ data class NMDNSAnnouncer(
* "pk=f3769a660475d27b4f6040381d784645e13e21c53e6d2da6a8c3d757086fc336"
* )
*/
suspend fun registerService(
@CName("register_service")
public fun registerService(
serviceType: String,
serviceName: String,
hostname: String,
@@ -78,11 +122,9 @@ suspend fun registerService(
bindAddress: String,
txtRecords: List<String>,
): NMDNSAnnouncer {
val manager = SelectorManager(Dispatchers.Default)
val serverSocket = aSocket(manager).udp()
.bind(InetSocketAddress(bindAddress, mdnsPort))
val server = createSocket()
.bind(bindAddress, port)
return NMDNSAnnouncer(
Dispatchers.Default,
serviceType,
serviceName,
hostname,
@@ -90,18 +132,11 @@ suspend fun registerService(
port,
mdnsPort,
txtRecords,
serverSocket,
server,
buildPacket(serviceType, "$serviceName.$serviceType", hostname, ipAddress, port, txtRecords)
)
}
/**
* broadcast packet
*/
suspend fun NMDNSAnnouncer.broadcast() {
this.server.send(Datagram(packet.copy(), InetSocketAddress("224.0.0.251", 5353)))
}
internal fun buildPacket(
serviceType: String,
serviceName: String,
@@ -109,7 +144,7 @@ internal fun buildPacket(
ip: String,
port: Int,
txtRecords: List<String>,
): Buffer {
): ByteArray {
val records = mutableListOf<ByteArray>()
records += encodeRR(serviceType, 12, encodeName(serviceName))
@@ -133,8 +168,7 @@ internal fun buildPacket(
header[6] = ((records.size shr 8) and 0xFF).toByte()
header[7] = (records.size and 0xFF).toByte()
val buffer = Buffer().apply { write(header + records.reduce { acc, r -> acc + r }) }
return buffer
return header + records.reduce { acc, r -> acc + r }
}
internal fun encodeRR(name: String, type: Int, data: ByteArray): ByteArray {
@@ -11,4 +11,5 @@ package cn.rtast.nmdns
* block current thread
* unit: second
*/
internal expect fun sleep1(time: Int)
public expect fun sleep1(time: Int)
@@ -0,0 +1,38 @@
/*
* Copyright © 2025 RTAkland
* Author: RTAkland
* Date: 9/30/25
*/
@file:Suppress("unused")
package cn.rtast.nmdns
public expect class Socket1 internal constructor() {
/**
* bind to ip
*/
public fun bind(ip: String, port: Int): Socket1
/**
* send packet
*/
public fun send(packet: ByteArray)
/**
* close
*/
public fun destroy()
}
internal fun createSocket(): Socket1 = Socket1()
public fun htons(value: UShort): UShort =
(((value.toInt() and 0xFF) shl 8) or ((value.toInt() ushr 8) and 0xFF)).toUShort()
public fun htonl(value: UInt): UInt =
((value and 0xFFu) shl 24) or
((value and 0xFF00u) shl 8) or
((value and 0xFF0000u) shr 8) or
((value and 0xFF000000u) shr 24)
@@ -6,6 +6,6 @@
package cn.rtast.nmdns
actual fun sleep1(time: Int) {
public actual fun sleep1(time: Int) {
Thread.sleep(time * 1000L)
}
@@ -0,0 +1,32 @@
/*
* Copyright © 2025 RTAkland
* Author: RTAkland
* Date: 9/30/25
*/
package cn.rtast.nmdns
import java.net.DatagramPacket
import java.net.InetAddress
import java.net.InetSocketAddress
import java.net.MulticastSocket
public actual class Socket1 internal actual constructor() {
private lateinit var socket: MulticastSocket
private val group: InetAddress = InetAddress.getByName("224.0.0.251")
public actual fun bind(ip: String, port: Int): Socket1 {
socket = MulticastSocket(InetSocketAddress(ip, port))
socket.joinGroup(group)
return this
}
public actual fun send(packet: ByteArray) {
socket.send(DatagramPacket(packet, packet.size, group, 5353))
}
public actual fun destroy() {
socket.close()
}
}
-66
View File
@@ -1,66 +0,0 @@
/*
* Copyright © 2025 RTAkland
* Author: RTAkland
* Date: 9/29/25
*/
package test
import cn.rtast.nmdns.broadcast
import cn.rtast.nmdns.randomMacAddress
import cn.rtast.nmdns.registerService
import kotlinx.coroutines.delay
import kotlinx.coroutines.runBlocking
import org.junit.Test
class JvmTest {
@Test
fun `test on jvm`() {
runBlocking {
// val manager = SelectorManager(Dispatchers.Default)
// val serverSocket = aSocket(manager).udp()
// .bind(InetSocketAddress("192.168.10.104", 5356))
// val txtRecords = listOf(
// "deviceid=01:23:45:67:89:AB",
// "model=AppleTV3,2C",
// "features=0x5A7FFFF7,0x1E",
// "srcvers=220.68",
// "pk=f3769a660475d27b4f6040381d784645e13e21c53e6d2da6a8c3d757086fc336"
// )
// val packet = buildPacket(
// serviceType = "_airplay._tcp.local.",
// serviceName = "MyService._airplay._tcp.local.",
// hostname = "rtakland.local.",
// ip = "192.168.10.104",
// port = 8080,
// txtRecords = txtRecords
// )
// while (true) {
// serverSocket.send(Datagram(packet.copy(), InetSocketAddress("224.0.0.251", 5353)))
// println("sent")
// delay(2000L)
// }
val service = registerService(
serviceType = "_airplay._tcp.local.",
serviceName = "AP@RTAST",
hostname = "rtakland.local.",
ipAddress = "192.168.10.104",
port = 7001,
bindAddress = "192.168.10.104",
mdnsPort = 5356,
txtRecords = listOf(
"deviceid=${randomMacAddress()}",
"model=AppleTV3,2C",
"features=0x5A7FFFF7,0x1E",
"srcvers=220.68",
"pk=f3769a660475d27b4f6040381d784645e13e21c53e6d2da6a8c3d757086fc336"
)
)
while (true) {
service.broadcast()
delay(2000L)
}
}
}
}
+39
View File
@@ -0,0 +1,39 @@
/*
* Copyright © 2025 RTAkland
* Author: RTAkland
* Date: 9/29/25
*/
package test
import cn.rtast.nmdns.randomMacAddress
import cn.rtast.nmdns.registerService
import cn.rtast.nmdns.sleep1
import org.junit.Test
class TestJvm {
@Test
fun `test on jvm`() {
val service = registerService(
serviceType = "_airplay._tcp.local.",
serviceName = "AP@RTAST",
hostname = "rtakland.local.",
ipAddress = "192.168.10.104",
port = 7001,
bindAddress = "192.168.10.104",
mdnsPort = 5356,
txtRecords = listOf(
"deviceid=${randomMacAddress()}",
"model=AppleTV3,2C",
"features=0x5A7FFFF7,0x1E",
"srcvers=220.68",
"pk=f3769a660475d27b4f6040381d784645e13e21c53e6d2da6a8c3d757086fc336"
)
)
while (true) {
service.broadcast()
sleep1(2)
}
}
}
@@ -0,0 +1,68 @@
/*
* Copyright © 2025 RTAkland
* Author: RTAkland
* Date: 9/30/25
*/
@file:OptIn(ExperimentalForeignApi::class)
@file:Suppress("unused")
package cn.rtast.nmdns
import kotlinx.cinterop.*
import platform.linux.inet_addr
import platform.posix.*
public actual class Socket1 internal actual constructor() {
/**
* socket fd
*/
private var socket: Int = -1
public actual fun bind(ip: String, port: Int): Socket1 {
socket = socket(AF_INET, SOCK_DGRAM, 0)
if (socket < 0) perror("Failed to create udp socket")
memScoped {
val reuse = alloc<IntVar>()
reuse.value = 1
if (setsockopt(socket, SOL_SOCKET, SO_REUSEADDR, reuse.ptr, sizeOf<IntVar>().convert()) < 0) {
perror("Failed to setsocketopt SO_REUSEADDR")
}
}
memScoped {
val addr = alloc<sockaddr_in>()
addr.sin_family = AF_INET.convert()
addr.sin_port = htons(port.toUShort())
addr.sin_addr.s_addr = inet_addr(ip)
if (bind(socket, addr.ptr.reinterpret(), sizeOf<sockaddr_in>().convert()) < 0) {
perror("Failed to bind")
close(socket)
}
}
memScoped {
val mreq = alloc<ip_mreq>()
mreq.imr_multiaddr.s_addr = inet_addr("224.0.0.251")
mreq.imr_interface.s_addr = htonl(INADDR_ANY)
if (setsockopt(socket, IPPROTO_IP, IP_ADD_MEMBERSHIP, mreq.ptr, sizeOf<ip_mreq>().convert()) < 0) {
perror("setsockopt IP_ADD_MEMBERSHIP")
}
}
return this
}
public actual fun send(packet: ByteArray) {
memScoped {
val addr = alloc<sockaddr_in>()
addr.sin_family = AF_INET.convert()
addr.sin_port = htons(5353u)
addr.sin_addr.s_addr = inet_addr("224.0.0.251")
val cbuf = packet.refTo(0).getPointer(this)
sendto(socket, cbuf, packet.size.convert(), 0, addr.ptr.reinterpret(), sizeOf<sockaddr_in>().convert())
}
}
public actual fun destroy() {
close(socket)
}
}
-43
View File
@@ -1,43 +0,0 @@
/*
* Copyright © 2025 RTAkland
* Author: RTAkland
* Date: 9/29/25
*/
package test
import cn.rtast.nmdns.broadcast
import cn.rtast.nmdns.randomMacAddress
import cn.rtast.nmdns.registerService
import cn.rtast.nmdns.sleep1
import kotlinx.coroutines.runBlocking
import kotlin.test.Test
class LinuxTest {
@Test
fun `test on linux`() {
runBlocking {
val service = registerService(
serviceType = "_airplay._tcp.local.",
serviceName = "AP@RTAST",
hostname = "rtakland.local.",
ipAddress = "192.168.10.104",
port = 7001,
bindAddress = "192.168.10.104",
mdnsPort = 5356,
txtRecords = listOf(
"deviceid=${randomMacAddress()}",
"model=AppleTV3,2C",
"features=0x5A7FFFF7,0x1E",
"srcvers=220.68",
"pk=f3769a660475d27b4f6040381d784645e13e21c53e6d2da6a8c3d757086fc336"
)
)
while (true) {
service.broadcast()
sleep1(2)
}
}
}
}
+39
View File
@@ -0,0 +1,39 @@
/*
* Copyright © 2025 RTAkland
* Author: RTAkland
* Date: 9/29/25
*/
package test
import cn.rtast.nmdns.randomMacAddress
import cn.rtast.nmdns.registerService
import cn.rtast.nmdns.sleep1
import kotlin.test.Test
class TestLinux {
@Test
fun `test on linux`() {
val service = registerService(
serviceType = "_airplay._tcp.local.",
serviceName = "AP@RTAST",
hostname = "rtakland.local.",
ipAddress = "192.168.10.104",
port = 7001,
bindAddress = "192.168.10.104",
mdnsPort = 5356,
txtRecords = listOf(
"deviceid=${randomMacAddress()}",
"model=AppleTV3,2C",
"features=0x5A7FFFF7,0x1E",
"srcvers=220.68",
"pk=f3769a660475d27b4f6040381d784645e13e21c53e6d2da6a8c3d757086fc336"
)
)
while (true) {
service.broadcast()
sleep1(2)
}
}
}
+19
View File
@@ -0,0 +1,19 @@
/*
* Copyright © 2025 RTAkland
* Author: RTAkland
* Date: 9/30/25
*/
package test
import cn.rtast.nmdns.randomMacAddress
import kotlin.test.Test
class TestMacAddr {
@Test
fun `test generate mac address`() {
println(randomMacAddress())
}
}
+42
View File
@@ -0,0 +1,42 @@
/*
* Copyright © 2025 RTAkland
* Author: RTAkland
* Date: 9/30/25
*/
package test
import cn.rtast.nmdns.buildPacket
import cn.rtast.nmdns.createSocket
import cn.rtast.nmdns.randomMacAddress
import cn.rtast.nmdns.sleep1
import kotlin.test.Test
class TestNative {
@Test
fun `test native socket`() {
val packet = buildPacket(
serviceType = "_airplay._tcp.local.",
serviceName = "AP@RTAST._airplay._tcp.local.",
hostname = "rtakland.local.",
ip = "192.168.10.104",
port = 7001,
txtRecords = listOf(
"deviceid=${randomMacAddress()}",
"model=AppleTV3,2C",
"features=0x5A7FFFF7,0x1E",
"srcvers=220.68",
"pk=f3769a660475d27b4f6040381d784645e13e21c53e6d2da6a8c3d757086fc336"
)
)
val service = createSocket()
.bind("192.168.10.104", 4546)
while (true) {
service.send(packet.copyOf())
sleep1(2)
}
}
}
@@ -0,0 +1,89 @@
/*
* Copyright © 2025 RTAkland
* Author: RTAkland
* Date: 9/30/25
*/
@file:OptIn(ExperimentalForeignApi::class)
@file:Suppress("unused")
package cn.rtast.nmdns
import kotlinx.cinterop.*
import platform.posix.*
import platform.windows.IPPROTO_UDP
import platform.windows.SOCK_DGRAM
import platform.windows.WSAStartup
import platform.windows.inet_addr
import platform.windows.setsockopt
public actual class Socket1 internal actual constructor() {
/**
* socket fd in windows
*/
private var sock: SOCKET = INVALID_SOCKET
private var bound: Boolean = false
public actual fun bind(ip: String, port: Int): Socket1 = memScoped {
val wsaData = nativeHeap.alloc<WSADATA>()
if (WSAStartup(0x202u, wsaData.ptr) != 0) {
perror("Failed to create socket")
}
sock = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP)
if (sock == INVALID_SOCKET) {
perror("Failed to create socket")
WSACleanup()
}
val reuse = nativeHeap.alloc<IntVar>()
reuse.value = 1
val result = setsockopt(
sock,
SOL_SOCKET,
SO_REUSEADDR,
reuse.ptr.reinterpret<ByteVar>().toKString(),
sizeOf<IntVar>().convert()
)
if (result != 0) println("setsockopt failed")
val addr = alloc<SOCKADDR_IN>()
addr.sin_family = AF_INET.convert()
addr.sin_port = htons(port.toUShort())
addr.sin_addr.S_un.S_addr = inet_addr(ip)
if (bind(sock, addr.ptr.reinterpret(), sizeOf<SOCKADDR_IN>().convert()) == SOCKET_ERROR) {
println("Bind failed")
closesocket(sock)
WSACleanup()
}
bound = true
this@Socket1
}
public actual fun send(packet: ByteArray): Unit = memScoped {
if (!bound) throw IllegalStateException("Socket not bound")
val dest = alloc<SOCKADDR_IN>()
dest.sin_family = AF_INET.convert()
dest.sin_port = 5353u
dest.sin_addr.S_un.S_addr = inet_addr("224.0.0.251")
packet.usePinned { pinned ->
val sent = sendto(
sock, pinned.addressOf(0),
packet.size.convert(),
0, dest.ptr.reinterpret(),
sizeOf<SOCKADDR_IN>().convert()
)
if (sent == SOCKET_ERROR) perror("Send failed")
}
}
public actual fun destroy() {
if (sock != INVALID_SOCKET) {
closesocket(sock)
sock = INVALID_SOCKET
}
WSACleanup()
}
}
+39
View File
@@ -0,0 +1,39 @@
/*
* Copyright © 2025 RTAkland
* Author: RTAkland
* Date: 9/30/25
*/
package test
import cn.rtast.nmdns.randomMacAddress
import cn.rtast.nmdns.registerService
import cn.rtast.nmdns.sleep1
import kotlin.test.Test
class TestWindows {
@Test
fun `test on windows`() {
val service = registerService(
serviceType = "_airplay._tcp.local.",
serviceName = "AP@RTAST",
hostname = "rtakland.local.",
ipAddress = "192.168.10.104",
port = 7001,
bindAddress = "192.168.10.104",
mdnsPort = 5356,
txtRecords = listOf(
"deviceid=${randomMacAddress()}",
"model=AppleTV3,2C",
"features=0x5A7FFFF7,0x1E",
"srcvers=220.68",
"pk=f3769a660475d27b4f6040381d784645e13e21c53e6d2da6a8c3d757086fc336"
)
)
while (true) {
service.broadcast()
sleep1(2)
}
}
}
@@ -0,0 +1,28 @@
/*
* Copyright © 2025 RTAkland
* Author: RTAkland
* Date: 9/30/25
*/
@file:OptIn(ExperimentalNativeApi::class)
package cn.rtast.nmdns
import kotlin.experimental.ExperimentalNativeApi
/**
* create an empty list in c
*/
@CExport
@CName("create_txt_records")
public fun createTXTRecords(): List<String> = listOf()
/**
* add txt record to list
*/
@CExport
@CName("add_txt_record")
public fun addTxtRecord(list: List<String>, value: String) {
list.toMutableList().add(value)
}
@@ -7,7 +7,10 @@
package cn.rtast.nmdns
import platform.posix.sleep
import kotlin.experimental.ExperimentalNativeApi
actual fun sleep1(time: Int) {
@OptIn(ExperimentalNativeApi::class)
@CName("sleep1")
public actual fun sleep1(time: Int) {
sleep(time.toUInt())
}