Remove ktor-network dependency use native socket
This commit is contained in:
21 files changed
+649
-179
No files matched your search
@@ -1,7 +1,6 @@
|
|||||||
# native-mdns
|
# native-mdns
|
||||||
|
|
||||||
A Kotlin multiplatform library for mdns announcer(mdns server), only broadcast is supported,
|
A Kotlin multiplatform library for mdns announcer(mdns server), only broadcast is supported,
|
||||||
it depends on ktor-network.About
|
|
||||||
|
|
||||||
Also, nmdns can be compiled to shared/static lib for other language calling, see [build.gradle.kts](build.gradle.kts)
|
Also, nmdns can be compiled to shared/static lib for other language calling, see [build.gradle.kts](build.gradle.kts)
|
||||||
|
|
||||||
@@ -63,4 +62,51 @@ while (true) {
|
|||||||
service.broadcast()
|
service.broadcast()
|
||||||
delay(2000L) // or use cn.rtast.nmdns.sleep1(2) sleep 2 seconds
|
delay(2000L) // or use cn.rtast.nmdns.sleep1(2) sleep 2 seconds
|
||||||
}
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
# C++
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
#include <csignal>
|
||||||
|
#include <cstdio>
|
||||||
|
#include "libnmdns_api.h"
|
||||||
|
|
||||||
|
volatile sig_atomic_t keepRunning = 1;
|
||||||
|
|
||||||
|
void handle_sigint(int sig) {
|
||||||
|
std::printf("\nCaught signal %d, exiting...\n", sig);
|
||||||
|
keepRunning = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
int main() {
|
||||||
|
libnmdns_kref_kotlin_collections_List txt = create_txt_records();
|
||||||
|
|
||||||
|
add_txt_record(txt, "deviceid=ef:42:0d:76:f1:97");
|
||||||
|
add_txt_record(txt, "model=AppleTV3,2C");
|
||||||
|
add_txt_record(txt, "features=0x5A7FFFF7,0x1E");
|
||||||
|
add_txt_record(txt, "srcvers=220.68");
|
||||||
|
add_txt_record(txt, "pk=f3769a660475d27b4f6040381d784645e13e21c53e6d2da6a8c3d757086fc336");
|
||||||
|
|
||||||
|
libnmdns_kref_cn_rtast_nmdns_NMDNSAnnouncer service = register_service(
|
||||||
|
"_airplay._tcp.local.",
|
||||||
|
"AP@RTAST",
|
||||||
|
"rtakland.local.",
|
||||||
|
"192.168.10.104",
|
||||||
|
7001, 3652, "192.168.10.104",
|
||||||
|
txt
|
||||||
|
);
|
||||||
|
|
||||||
|
std::signal(SIGINT, handle_sigint);
|
||||||
|
std::signal(SIGTERM, handle_sigint);
|
||||||
|
std::signal(SIGHUP, handle_sigint);
|
||||||
|
std::signal(SIGQUIT, handle_sigint);
|
||||||
|
|
||||||
|
std::printf("Running... Press Ctrl+C to exit\n");
|
||||||
|
|
||||||
|
while (keepRunning) {
|
||||||
|
broadcast(service);
|
||||||
|
sleep1(2);
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
```
|
```
|
||||||
+7
-3
@@ -1,12 +1,12 @@
|
|||||||
import org.jetbrains.kotlin.gradle.dsl.JvmTarget
|
import org.jetbrains.kotlin.gradle.dsl.JvmTarget
|
||||||
|
|
||||||
plugins {
|
plugins {
|
||||||
kotlin("multiplatform") version "2.2.10"
|
kotlin("multiplatform") version "2.2.0"
|
||||||
id("maven-publish")
|
id("maven-publish")
|
||||||
}
|
}
|
||||||
|
|
||||||
group = "cn.rtast.nmdns"
|
group = "cn.rtast.nmdns"
|
||||||
version = "0.0.2"
|
version = "0.1.0"
|
||||||
|
|
||||||
repositories {
|
repositories {
|
||||||
mavenCentral()
|
mavenCentral()
|
||||||
@@ -32,9 +32,13 @@ kotlin {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
compilerOptions { freeCompilerArgs.addAll("-Xexpect-actual-classes") }
|
||||||
|
|
||||||
|
explicitApi()
|
||||||
|
|
||||||
sourceSets {
|
sourceSets {
|
||||||
commonMain.dependencies {
|
commonMain.dependencies {
|
||||||
implementation("io.ktor:ktor-network:3.2.3")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
commonTest.dependencies {
|
commonTest.dependencies {
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -7,40 +7,16 @@
|
|||||||
|
|
||||||
package cn.rtast.nmdns
|
package cn.rtast.nmdns
|
||||||
|
|
||||||
private val macAddresses = listOf(
|
import kotlin.experimental.ExperimentalNativeApi
|
||||||
"ef:42:0d:76:f1:97",
|
import kotlin.native.CName
|
||||||
"b4:91:7c:be:84:7b",
|
import kotlin.random.Random
|
||||||
"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"
|
|
||||||
)
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 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() }
|
||||||
|
}
|
||||||
+63
-29
@@ -5,19 +5,14 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
|
|
||||||
|
@file:OptIn(ExperimentalNativeApi::class)
|
||||||
|
|
||||||
package cn.rtast.nmdns
|
package cn.rtast.nmdns
|
||||||
|
|
||||||
import io.ktor.network.selector.*
|
import kotlin.experimental.ExperimentalNativeApi
|
||||||
import io.ktor.network.sockets.*
|
import kotlin.native.CName
|
||||||
import kotlinx.coroutines.CoroutineDispatcher
|
|
||||||
import kotlinx.coroutines.Dispatchers
|
|
||||||
import kotlinx.io.Buffer
|
|
||||||
|
|
||||||
data class NMDNSAnnouncer(
|
public data class NMDNSAnnouncer(
|
||||||
/**
|
|
||||||
* coroutine dispatcher for running server
|
|
||||||
*/
|
|
||||||
internal val dispatcher: CoroutineDispatcher = Dispatchers.Default,
|
|
||||||
/**
|
/**
|
||||||
* service type
|
* service type
|
||||||
* airplay service type: _airplay._tcp.local.
|
* airplay service type: _airplay._tcp.local.
|
||||||
@@ -50,12 +45,60 @@ data class NMDNSAnnouncer(
|
|||||||
/**
|
/**
|
||||||
* mdns socket server
|
* mdns socket server
|
||||||
*/
|
*/
|
||||||
internal val server: BoundDatagramSocket,
|
internal val server: Socket1,
|
||||||
/**
|
/**
|
||||||
* srv packet
|
* 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
|
* register service
|
||||||
@@ -68,7 +111,8 @@ data class NMDNSAnnouncer(
|
|||||||
* "pk=f3769a660475d27b4f6040381d784645e13e21c53e6d2da6a8c3d757086fc336"
|
* "pk=f3769a660475d27b4f6040381d784645e13e21c53e6d2da6a8c3d757086fc336"
|
||||||
* )
|
* )
|
||||||
*/
|
*/
|
||||||
suspend fun registerService(
|
@CName("register_service")
|
||||||
|
public fun registerService(
|
||||||
serviceType: String,
|
serviceType: String,
|
||||||
serviceName: String,
|
serviceName: String,
|
||||||
hostname: String,
|
hostname: String,
|
||||||
@@ -78,11 +122,9 @@ suspend fun registerService(
|
|||||||
bindAddress: String,
|
bindAddress: String,
|
||||||
txtRecords: List<String>,
|
txtRecords: List<String>,
|
||||||
): NMDNSAnnouncer {
|
): NMDNSAnnouncer {
|
||||||
val manager = SelectorManager(Dispatchers.Default)
|
val server = createSocket()
|
||||||
val serverSocket = aSocket(manager).udp()
|
.bind(bindAddress, port)
|
||||||
.bind(InetSocketAddress(bindAddress, mdnsPort))
|
|
||||||
return NMDNSAnnouncer(
|
return NMDNSAnnouncer(
|
||||||
Dispatchers.Default,
|
|
||||||
serviceType,
|
serviceType,
|
||||||
serviceName,
|
serviceName,
|
||||||
hostname,
|
hostname,
|
||||||
@@ -90,18 +132,11 @@ suspend fun registerService(
|
|||||||
port,
|
port,
|
||||||
mdnsPort,
|
mdnsPort,
|
||||||
txtRecords,
|
txtRecords,
|
||||||
serverSocket,
|
server,
|
||||||
buildPacket(serviceType, "$serviceName.$serviceType", hostname, ipAddress, port, txtRecords)
|
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(
|
internal fun buildPacket(
|
||||||
serviceType: String,
|
serviceType: String,
|
||||||
serviceName: String,
|
serviceName: String,
|
||||||
@@ -109,7 +144,7 @@ internal fun buildPacket(
|
|||||||
ip: String,
|
ip: String,
|
||||||
port: Int,
|
port: Int,
|
||||||
txtRecords: List<String>,
|
txtRecords: List<String>,
|
||||||
): Buffer {
|
): ByteArray {
|
||||||
val records = mutableListOf<ByteArray>()
|
val records = mutableListOf<ByteArray>()
|
||||||
records += encodeRR(serviceType, 12, encodeName(serviceName))
|
records += encodeRR(serviceType, 12, encodeName(serviceName))
|
||||||
|
|
||||||
@@ -133,8 +168,7 @@ internal fun buildPacket(
|
|||||||
header[6] = ((records.size shr 8) and 0xFF).toByte()
|
header[6] = ((records.size shr 8) and 0xFF).toByte()
|
||||||
header[7] = (records.size and 0xFF).toByte()
|
header[7] = (records.size and 0xFF).toByte()
|
||||||
|
|
||||||
val buffer = Buffer().apply { write(header + records.reduce { acc, r -> acc + r }) }
|
return header + records.reduce { acc, r -> acc + r }
|
||||||
return buffer
|
|
||||||
}
|
}
|
||||||
|
|
||||||
internal fun encodeRR(name: String, type: Int, data: ByteArray): ByteArray {
|
internal fun encodeRR(name: String, type: Int, data: ByteArray): ByteArray {
|
||||||
@@ -11,4 +11,5 @@ package cn.rtast.nmdns
|
|||||||
* block current thread
|
* block current thread
|
||||||
* unit: second
|
* 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
|
package cn.rtast.nmdns
|
||||||
|
|
||||||
actual fun sleep1(time: Int) {
|
public actual fun sleep1(time: Int) {
|
||||||
Thread.sleep(time * 1000L)
|
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()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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())
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
|
package cn.rtast.nmdns
|
||||||
|
|
||||||
import platform.posix.sleep
|
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())
|
sleep(time.toUInt())
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user