refactor basic
WIP
This commit is contained in:
35 files changed
+544
-637
No files matched your search
@@ -46,3 +46,4 @@ bin/
|
|||||||
|
|
||||||
.idea/
|
.idea/
|
||||||
/src/nativeInterop/cinterop/def/
|
/src/nativeInterop/cinterop/def/
|
||||||
|
/src/cinterop/win32_join_multicast_group.def
|
||||||
@@ -7,6 +7,7 @@ Also, nmdns can be compiled to shared/static lib for other languages calling, se
|
|||||||
mDNS is usually used for Apple's AirPlay protocol family, there's an example in README.md [How to use](#How-to-use)
|
mDNS is usually used for Apple's AirPlay protocol family, there's an example in README.md [How to use](#How-to-use)
|
||||||
|
|
||||||
Supported platforms:
|
Supported platforms:
|
||||||
|
|
||||||
1. windows64(mingwx64)
|
1. windows64(mingwx64)
|
||||||
2. linuxX64
|
2. linuxX64
|
||||||
3. linuxArm64
|
3. linuxArm64
|
||||||
@@ -14,13 +15,25 @@ Supported platforms:
|
|||||||
5. macosX64
|
5. macosX64
|
||||||
6. jvm(11 or later)
|
6. jvm(11 or later)
|
||||||
|
|
||||||
|
|
||||||
|
Platform implementations are based on:
|
||||||
|
|
||||||
|
- Apple: posix socket
|
||||||
|
- Windows: WinSock
|
||||||
|
- Linux: posix socket
|
||||||
|
- JVM: `java.net.MulticastSocket`
|
||||||
|
|
||||||
|
No external dependencies required.
|
||||||
|
|
||||||
Supported record type
|
Supported record type
|
||||||
|
|
||||||
1. A
|
1. A
|
||||||
2. PTR
|
2. PTR
|
||||||
3. SRV
|
3. SRV
|
||||||
4. TXT
|
4. TXT
|
||||||
|
|
||||||
Supported features
|
Supported features
|
||||||
|
|
||||||
1. Event hooking(Kotlin only)
|
1. Event hooking(Kotlin only)
|
||||||
|
|
||||||
> AAAA is not supported
|
> AAAA is not supported
|
||||||
|
|||||||
+15
-2
@@ -2,9 +2,10 @@ import com.vanniktech.maven.publish.SonatypeHost
|
|||||||
import org.jetbrains.kotlin.gradle.dsl.JvmTarget
|
import org.jetbrains.kotlin.gradle.dsl.JvmTarget
|
||||||
|
|
||||||
plugins {
|
plugins {
|
||||||
kotlin("multiplatform") version "2.2.0"
|
kotlin("multiplatform") version "2.3.10"
|
||||||
id("com.vanniktech.maven.publish") version "0.31.0-rc2"
|
id("com.vanniktech.maven.publish") version "0.31.0-rc2"
|
||||||
id("signing")
|
id("signing")
|
||||||
|
id("cn.rtast.kdef") version "0.1.1"
|
||||||
}
|
}
|
||||||
|
|
||||||
val libVersion: String by project
|
val libVersion: String by project
|
||||||
@@ -20,7 +21,14 @@ kotlin {
|
|||||||
val nativeTargets = listOf(
|
val nativeTargets = listOf(
|
||||||
linuxArm64(),
|
linuxArm64(),
|
||||||
linuxX64(),
|
linuxX64(),
|
||||||
mingwX64(),
|
mingwX64 {
|
||||||
|
compilations["main"].cinterops {
|
||||||
|
@Suppress("UNUSED")
|
||||||
|
val joinMulticastGroup by creating {
|
||||||
|
definitionFile = file("src/cinterop/win32_join_multicast_group.def")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
macosArm64(),
|
macosArm64(),
|
||||||
macosX64()
|
macosX64()
|
||||||
)
|
)
|
||||||
@@ -126,3 +134,8 @@ tasks.register("linkSharedLibrary") {
|
|||||||
dependsOn(tasks.named("linkReleaseSharedMacosArm64"))
|
dependsOn(tasks.named("linkReleaseSharedMacosArm64"))
|
||||||
dependsOn(tasks.named("linkReleaseSharedMingwX64"))
|
dependsOn(tasks.named("linkReleaseSharedMingwX64"))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
kdef {
|
||||||
|
outputDir = file("src/cinterop")
|
||||||
|
defFiles.add(file("src/cinterop/template/win32_join_multicast_group.def"))
|
||||||
|
}
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
#include "win32_join_multicast.h"
|
||||||
|
|
||||||
|
#include <stdint.h>
|
||||||
|
#include <winsock2.h>
|
||||||
|
#include <ws2tcpip.h>
|
||||||
|
|
||||||
|
int join_multicast_group(SOCKET sock, const char *group) {
|
||||||
|
struct ip_mreq mreq;
|
||||||
|
mreq.imr_multiaddr.s_addr = inet_addr(group);
|
||||||
|
mreq.imr_interface.s_addr = INADDR_ANY;
|
||||||
|
|
||||||
|
if (setsockopt(sock, IPPROTO_IP, IP_ADD_MEMBERSHIP, (char *) &mreq, sizeof(mreq)) == SOCKET_ERROR) {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
int socket_sendto(SOCKET fd, const uint8_t *data, int len, const char *host, unsigned short port) {
|
||||||
|
struct sockaddr_in addr;
|
||||||
|
addr.sin_family = AF_INET;
|
||||||
|
addr.sin_port = htons(port);
|
||||||
|
addr.sin_addr.s_addr = inet_addr(host);
|
||||||
|
|
||||||
|
const int sent = sendto(fd, (const char *) data, len, 0, (struct sockaddr *) &addr, sizeof(addr));
|
||||||
|
if (sent == SOCKET_ERROR) {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
int leave_multicast_group(SOCKET sock, const char *group) {
|
||||||
|
struct ip_mreq mreq;
|
||||||
|
mreq.imr_multiaddr.s_addr = inet_addr(group);
|
||||||
|
mreq.imr_interface.s_addr = INADDR_ANY;
|
||||||
|
|
||||||
|
if (setsockopt(sock, IPPROTO_IP, IP_DROP_MEMBERSHIP, (char *) &mreq, sizeof(mreq)) == SOCKET_ERROR) {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
#ifndef WIN32_JOIN_MULTICAST_H
|
||||||
|
#define WIN32_JOIN_MULTICAST_H
|
||||||
|
|
||||||
|
#include <stdint.h>
|
||||||
|
#include <winsock2.h>
|
||||||
|
|
||||||
|
#ifdef __cplusplus
|
||||||
|
extern "C" {
|
||||||
|
|
||||||
|
#endif
|
||||||
|
|
||||||
|
int join_multicast_group(SOCKET sock, const char *group);
|
||||||
|
|
||||||
|
int socket_sendto(SOCKET fd, const uint8_t *data, int len, const char *host, unsigned short port);
|
||||||
|
|
||||||
|
int leave_multicast_group(SOCKET sock, const char *group);
|
||||||
|
|
||||||
|
#ifdef __cplusplus
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#endif // WIN32_JOIN_MULTICAST_H
|
||||||
@@ -1,69 +0,0 @@
|
|||||||
/*
|
|
||||||
* 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 = 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 = inet_addr(ip)
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,60 @@
|
|||||||
|
/*
|
||||||
|
* Copyright © 2026 RTAkland
|
||||||
|
* Author: RTAkland
|
||||||
|
* Date: 2026/3/13
|
||||||
|
*/
|
||||||
|
|
||||||
|
@file:OptIn(ExperimentalForeignApi::class)
|
||||||
|
|
||||||
|
package cn.rtast.nmdns.util
|
||||||
|
|
||||||
|
import kotlinx.cinterop.*
|
||||||
|
import platform.darwin.inet_addr
|
||||||
|
import platform.darwin.sockaddr_in
|
||||||
|
import platform.posix.*
|
||||||
|
|
||||||
|
public actual class MulticastSocket actual constructor() {
|
||||||
|
|
||||||
|
private var fd: Int = -1
|
||||||
|
|
||||||
|
public actual fun bind(port: Int): Unit = memScoped {
|
||||||
|
fd = socket(AF_INET, SOCK_DGRAM, 0)
|
||||||
|
val opt = alloc<IntVar>()
|
||||||
|
setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, opt.ptr, sizeOf<IntVar>().convert())
|
||||||
|
val addr = alloc<sockaddr_in>()
|
||||||
|
addr.sin_family = AF_INET.convert()
|
||||||
|
addr.sin_port = htons(port.toUShort())
|
||||||
|
addr.sin_addr.s_addr = INADDR_ANY
|
||||||
|
bind(fd, addr.ptr.reinterpret(), sizeOf<sockaddr_in>().convert())
|
||||||
|
}
|
||||||
|
|
||||||
|
public actual fun joinMulticastGroup(group: String): Unit = memScoped {
|
||||||
|
val merq = alloc<ip_mreq>()
|
||||||
|
merq.imr_multiaddr.s_addr = inet_addr(group)
|
||||||
|
merq.imr_interface.s_addr = INADDR_ANY
|
||||||
|
setsockopt(fd, IPPROTO_IP, IP_ADD_MEMBERSHIP, merq.ptr, sizeOf<ip_mreq>().convert())
|
||||||
|
}
|
||||||
|
|
||||||
|
public actual fun send(data: ByteArray, host: String, port: Int): Unit = 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(host)
|
||||||
|
data.usePinned {
|
||||||
|
sendto(fd, it.addressOf(0), data.size.convert(), 0, addr.ptr.reinterpret(), sizeOf<sockaddr_in>().convert())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public actual fun receive(buf: ByteArray): Long = memScoped {
|
||||||
|
buf.usePinned { recv(fd, it.addressOf(0), buf.size.convert(), 0) }
|
||||||
|
}
|
||||||
|
|
||||||
|
public actual fun close(): Unit = run { if (fd > 0) close(fd) }
|
||||||
|
|
||||||
|
public actual fun leaveMulticastGroup(group: String): Unit = memScoped {
|
||||||
|
val merq = alloc<ip_mreq>()
|
||||||
|
merq.imr_multiaddr.s_addr = inet_addr(group)
|
||||||
|
merq.imr_interface.s_addr = INADDR_ANY
|
||||||
|
setsockopt(fd, IPPROTO_IP, IP_DROP_MEMBERSHIP, merq.ptr, sizeOf<ip_mreq>().convert())
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -7,7 +7,7 @@
|
|||||||
|
|
||||||
package test
|
package test
|
||||||
|
|
||||||
import cn.rtast.nmdns.randomMacAddress
|
import cn.rtast.nmdns.util.randomMacAddress
|
||||||
import cn.rtast.nmdns.registerService
|
import cn.rtast.nmdns.registerService
|
||||||
import cn.rtast.nmdns.sleep1
|
import cn.rtast.nmdns.sleep1
|
||||||
import kotlin.test.Test
|
import kotlin.test.Test
|
||||||
|
|||||||
Binary file not shown.
@@ -0,0 +1,3 @@
|
|||||||
|
headers = {{PATH}}/clibs/win32_join_multicast.h
|
||||||
|
compilerOpts = -I{{PATH}}/clibs
|
||||||
|
linkerOpts = {{PATH}}/src/cinterop/libwin32_join_multicast_group.a -lws2_32
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright © 2025 RTAkland
|
|
||||||
* Author: RTAkland
|
|
||||||
* Date: 9/30/25
|
|
||||||
*/
|
|
||||||
|
|
||||||
|
|
||||||
package cn.rtast.nmdns
|
|
||||||
|
|
||||||
@Target(AnnotationTarget.FUNCTION)
|
|
||||||
@Retention(AnnotationRetention.BINARY)
|
|
||||||
@RequiresOptIn
|
|
||||||
public annotation class CExport
|
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
/*
|
||||||
|
* Copyright © 2026 RTAkland
|
||||||
|
* Author: RTAkland
|
||||||
|
* Date: 2026/3/14
|
||||||
|
*/
|
||||||
|
|
||||||
|
|
||||||
|
package cn.rtast.nmdns
|
||||||
|
|
||||||
|
public data class MDNSRecord(
|
||||||
|
val name: String,
|
||||||
|
val type: String,
|
||||||
|
val host: String,
|
||||||
|
val port: Int,
|
||||||
|
val txt: Map<String, String> = emptyMap(),
|
||||||
|
)
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
/*
|
||||||
|
* Copyright © 2026 RTAkland
|
||||||
|
* Author: RTAkland
|
||||||
|
* Date: 2026/3/14
|
||||||
|
*/
|
||||||
|
|
||||||
|
|
||||||
|
package cn.rtast.nmdns
|
||||||
|
|
||||||
|
import cn.rtast.nmdns.util.MulticastSocket
|
||||||
|
|
||||||
|
public class MDNSAnnouncer(private val socket: MulticastSocket) {
|
||||||
|
|
||||||
|
public fun register(record: MDNSRecord) {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
public fun unregister(record: MDNSRecord) {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
public fun onQuery(callback: (queryType: String, queryName: String) -> Unit) {}
|
||||||
|
|
||||||
|
public fun start() {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public fun stop() {
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,247 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright © 2025 RTAkland
|
|
||||||
* Author: RTAkland
|
|
||||||
* Date: 9/28/25
|
|
||||||
*/
|
|
||||||
|
|
||||||
|
|
||||||
@file:OptIn(ExperimentalNativeApi::class)
|
|
||||||
|
|
||||||
package cn.rtast.nmdns
|
|
||||||
|
|
||||||
import kotlin.experimental.ExperimentalNativeApi
|
|
||||||
import kotlin.native.CName
|
|
||||||
|
|
||||||
public fun NMDNSAnnouncer.onRegister(callback: () -> Unit) {
|
|
||||||
onRegisterCallback = callback
|
|
||||||
}
|
|
||||||
|
|
||||||
public fun NMDNSAnnouncer.onBroadcast(callback: () -> Unit) {
|
|
||||||
onBroadcastCallback = callback
|
|
||||||
}
|
|
||||||
|
|
||||||
public fun NMDNSAnnouncer.onUnregistered(callback: () -> Unit) {
|
|
||||||
onUnregisterCallback = callback
|
|
||||||
}
|
|
||||||
|
|
||||||
public data class NMDNSAnnouncer(
|
|
||||||
/**
|
|
||||||
* service type
|
|
||||||
* airplay service type: _airplay._tcp.local.
|
|
||||||
*/
|
|
||||||
internal val serviceType: String,
|
|
||||||
/**
|
|
||||||
* service name
|
|
||||||
*/
|
|
||||||
internal val serviceName: String,
|
|
||||||
/**
|
|
||||||
* local hostname
|
|
||||||
*/
|
|
||||||
internal val hostname: String,
|
|
||||||
/**
|
|
||||||
* service ip, lan ip mostly
|
|
||||||
*/
|
|
||||||
internal val ipAddress: String,
|
|
||||||
/**
|
|
||||||
* service port
|
|
||||||
*/
|
|
||||||
internal val port: Int = 7004,
|
|
||||||
/**
|
|
||||||
* mdns server port
|
|
||||||
*/
|
|
||||||
internal val mdnsPort: Int = 3025,
|
|
||||||
/**
|
|
||||||
* txt record content
|
|
||||||
*/
|
|
||||||
internal val txtRecords: List<String>,
|
|
||||||
/**
|
|
||||||
* mdns socket server
|
|
||||||
*/
|
|
||||||
internal val server: Socket1,
|
|
||||||
/**
|
|
||||||
* srv packet
|
|
||||||
*/
|
|
||||||
internal val packet: ByteArray,
|
|
||||||
) {
|
|
||||||
|
|
||||||
internal var onRegisterCallback: (() -> Unit)? = null
|
|
||||||
internal var onBroadcastCallback: (() -> Unit)? = null
|
|
||||||
internal var onUnregisterCallback: (() -> Unit)? = null
|
|
||||||
|
|
||||||
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.onBroadcastCallback?.invoke()
|
|
||||||
this.server.send(packet)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* unregister service
|
|
||||||
*/
|
|
||||||
public fun unregister() {
|
|
||||||
this.onUnregisterCallback?.invoke()
|
|
||||||
this.server.destroy()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* broadcast packet
|
|
||||||
* export for c
|
|
||||||
*/
|
|
||||||
@CExport
|
|
||||||
@CName("broadcast")
|
|
||||||
public fun broadcast1(server: NMDNSAnnouncer) {
|
|
||||||
server.server.send(server.packet)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* unregister service
|
|
||||||
* export for c
|
|
||||||
*/
|
|
||||||
@CExport
|
|
||||||
@CName("unregister")
|
|
||||||
public fun unregister1(server: NMDNSAnnouncer) {
|
|
||||||
server.unregister()
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* register service
|
|
||||||
* airplay2 txt records
|
|
||||||
* listOf(
|
|
||||||
* "deviceid=${randomMacAddress()}",
|
|
||||||
* "model=AppleTV3,2C",
|
|
||||||
* "features=0x5A7FFFF7,0x1E",
|
|
||||||
* "srcvers=220.68",
|
|
||||||
* "pk=f3769a660475d27b4f6040381d784645e13e21c53e6d2da6a8c3d757086fc336"
|
|
||||||
* )
|
|
||||||
*/
|
|
||||||
@CName("register_service")
|
|
||||||
public fun registerService(
|
|
||||||
serviceType: String,
|
|
||||||
serviceName: String,
|
|
||||||
hostname: String,
|
|
||||||
ipAddress: String,
|
|
||||||
port: Int,
|
|
||||||
mdnsPort: Int = 9872,
|
|
||||||
bindAddress: String,
|
|
||||||
txtRecords: List<String>,
|
|
||||||
callback: NMDNSAnnouncer.() -> Unit = {},
|
|
||||||
): NMDNSAnnouncer {
|
|
||||||
val socket = createSocket()
|
|
||||||
.bind(bindAddress, mdnsPort)
|
|
||||||
|
|
||||||
val server = NMDNSAnnouncer(
|
|
||||||
serviceType,
|
|
||||||
serviceName,
|
|
||||||
hostname,
|
|
||||||
ipAddress,
|
|
||||||
port,
|
|
||||||
mdnsPort,
|
|
||||||
txtRecords,
|
|
||||||
socket,
|
|
||||||
buildPacket(serviceType, "$serviceName.$serviceType", hostname, ipAddress, port, txtRecords),
|
|
||||||
)
|
|
||||||
server.callback()
|
|
||||||
server.onRegisterCallback?.invoke()
|
|
||||||
return server
|
|
||||||
}
|
|
||||||
|
|
||||||
internal fun buildPacket(
|
|
||||||
serviceType: String,
|
|
||||||
serviceName: String,
|
|
||||||
hostname: String,
|
|
||||||
ip: String,
|
|
||||||
port: Int,
|
|
||||||
txtRecords: List<String>,
|
|
||||||
): ByteArray {
|
|
||||||
val records = mutableListOf<ByteArray>()
|
|
||||||
records += encodeRR(serviceType, 12, encodeName(serviceName))
|
|
||||||
|
|
||||||
val srvData = ByteArray(6) { 0 }
|
|
||||||
srvData[4] = (port shr 8).toByte()
|
|
||||||
srvData[5] = (port and 0xFF).toByte()
|
|
||||||
records += encodeRR(serviceName, 33, srvData + encodeName(hostname))
|
|
||||||
|
|
||||||
val ipParts = ip.split(".").map { it.toInt().toByte() }
|
|
||||||
records += encodeRR(hostname, 1, ipParts.toByteArray())
|
|
||||||
|
|
||||||
if (txtRecords.isNotEmpty()) {
|
|
||||||
val txtData = encodeTxt(txtRecords)
|
|
||||||
records += encodeRR(serviceName, 16, txtData)
|
|
||||||
}
|
|
||||||
|
|
||||||
val header = ByteArray(12)
|
|
||||||
header[2] = 0x84.toByte()
|
|
||||||
header[3] = 0x00
|
|
||||||
|
|
||||||
header[6] = ((records.size shr 8) and 0xFF).toByte()
|
|
||||||
header[7] = (records.size and 0xFF).toByte()
|
|
||||||
|
|
||||||
return header + records.reduce { acc, r -> acc + r }
|
|
||||||
}
|
|
||||||
|
|
||||||
internal fun encodeRR(name: String, type: Int, data: ByteArray): ByteArray {
|
|
||||||
val nameBytes = encodeName(name)
|
|
||||||
val rr = ByteArray(10)
|
|
||||||
rr[0] = 0x00; rr[1] = type.toByte()
|
|
||||||
rr[2] = 0x00; rr[3] = 0x01
|
|
||||||
rr[4] = 0x00; rr[5] = 0x00; rr[6] = 0x00; rr[7] = 0x78
|
|
||||||
rr[8] = (data.size shr 8).toByte()
|
|
||||||
rr[9] = (data.size and 0xFF).toByte()
|
|
||||||
return nameBytes + rr + data
|
|
||||||
}
|
|
||||||
|
|
||||||
internal fun encodeName(name: String): ByteArray {
|
|
||||||
val parts = name.trimEnd('.').split(".")
|
|
||||||
val bytes = mutableListOf<Byte>()
|
|
||||||
for (p in parts) {
|
|
||||||
bytes += p.length.toByte()
|
|
||||||
bytes += p.encodeToByteArray().toList()
|
|
||||||
}
|
|
||||||
bytes += 0
|
|
||||||
return bytes.toByteArray()
|
|
||||||
}
|
|
||||||
|
|
||||||
internal fun encodeTxt(records: List<String>): ByteArray {
|
|
||||||
val bytes = mutableListOf<Byte>()
|
|
||||||
for (txt in records) {
|
|
||||||
val txtBytes = txt.encodeToByteArray()
|
|
||||||
if (txtBytes.size > 255) continue
|
|
||||||
bytes += txtBytes.size.toByte()
|
|
||||||
bytes += txtBytes.toList()
|
|
||||||
}
|
|
||||||
return bytes.toByteArray()
|
|
||||||
}
|
|
||||||
@@ -1,15 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright © 2025 RTAkland
|
|
||||||
* Author: RTAkland
|
|
||||||
* Date: 9/29/25
|
|
||||||
*/
|
|
||||||
|
|
||||||
|
|
||||||
package cn.rtast.nmdns
|
|
||||||
|
|
||||||
/**
|
|
||||||
* block current thread
|
|
||||||
* unit: second
|
|
||||||
*/
|
|
||||||
|
|
||||||
public expect fun sleep1(time: Int)
|
|
||||||
@@ -1,38 +0,0 @@
|
|||||||
/*
|
|
||||||
* 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)
|
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
/*
|
||||||
|
* Copyright © 2026 RTAkland
|
||||||
|
* Author: RTAkland
|
||||||
|
* Date: 2026/3/13
|
||||||
|
*/
|
||||||
|
|
||||||
|
|
||||||
|
package cn.rtast.nmdns.util
|
||||||
|
|
||||||
|
|
||||||
|
public fun htons(value: UShort): UShort =
|
||||||
|
(((value.toInt() and 0xFF) shl 8) or ((value.toInt() ushr 8) and 0xFF)).toUShort()
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
/*
|
||||||
|
* Copyright © 2026 RTAkland
|
||||||
|
* Author: RTAkland
|
||||||
|
* Date: 2026/3/13
|
||||||
|
*/
|
||||||
|
|
||||||
|
|
||||||
|
package cn.rtast.nmdns.util
|
||||||
|
|
||||||
|
internal class ByteArrayBuilder {
|
||||||
|
private val buffer = mutableListOf<Byte>()
|
||||||
|
|
||||||
|
fun appendByte(b: Int) = apply { buffer.add(b.toByte()) }
|
||||||
|
|
||||||
|
fun appendShort(value: Int) = apply {
|
||||||
|
buffer.add(((value shr 8) and 0xFF).toByte())
|
||||||
|
buffer.add((value and 0xFF).toByte())
|
||||||
|
}
|
||||||
|
|
||||||
|
fun appendInt(value: Int) = apply {
|
||||||
|
buffer.add(((value shr 24) and 0xFF).toByte())
|
||||||
|
buffer.add(((value shr 16) and 0xFF).toByte())
|
||||||
|
buffer.add(((value shr 8) and 0xFF).toByte())
|
||||||
|
buffer.add((value and 0xFF).toByte())
|
||||||
|
}
|
||||||
|
|
||||||
|
fun appendBytes(bytes: ByteArray) = apply { buffer.addAll(bytes.toList()) }
|
||||||
|
|
||||||
|
fun appendName(name: String): ByteArrayBuilder {
|
||||||
|
val labels = name.split(".")
|
||||||
|
for (label in labels) {
|
||||||
|
appendByte(label.length)
|
||||||
|
appendBytes(label.encodeToByteArray())
|
||||||
|
}
|
||||||
|
appendByte(0)
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
fun build(): ByteArray = buffer.toByteArray()
|
||||||
|
}
|
||||||
|
|
||||||
|
internal fun dnsPacket(block: ByteArrayBuilder.() -> Unit) =
|
||||||
|
ByteArrayBuilder().apply(block).build()
|
||||||
+1
-3
@@ -5,17 +5,15 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
|
|
||||||
package cn.rtast.nmdns
|
package cn.rtast.nmdns.util
|
||||||
|
|
||||||
import kotlin.experimental.ExperimentalNativeApi
|
import kotlin.experimental.ExperimentalNativeApi
|
||||||
import kotlin.native.CName
|
|
||||||
import kotlin.random.Random
|
import kotlin.random.Random
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* generate random mac address
|
* generate random mac address
|
||||||
*/
|
*/
|
||||||
@OptIn(ExperimentalNativeApi::class)
|
@OptIn(ExperimentalNativeApi::class)
|
||||||
@CName("random_mac_address")
|
|
||||||
public fun randomMacAddress(): String {
|
public fun randomMacAddress(): String {
|
||||||
val bytes = ByteArray(6) { Random.nextInt(0, 256).toByte() }
|
val bytes = ByteArray(6) { Random.nextInt(0, 256).toByte() }
|
||||||
return bytes.joinToString(":") { it.toHexString() }
|
return bytes.joinToString(":") { it.toHexString() }
|
||||||
@@ -0,0 +1,53 @@
|
|||||||
|
/*
|
||||||
|
* Copyright © 2026 RTAkland
|
||||||
|
* Author: RTAkland
|
||||||
|
* Date: 2026/3/13
|
||||||
|
*/
|
||||||
|
|
||||||
|
|
||||||
|
package cn.rtast.nmdns.util
|
||||||
|
|
||||||
|
internal fun buildMDNSResponse(
|
||||||
|
host: String,
|
||||||
|
service: String,
|
||||||
|
ip: String,
|
||||||
|
port: Int,
|
||||||
|
txtRecords: Map<String, String>,
|
||||||
|
): ByteArray = dnsPacket {
|
||||||
|
// HEADER
|
||||||
|
appendShort(0); appendShort(0x8400)
|
||||||
|
appendShort(0); appendShort(4)
|
||||||
|
appendShort(0); appendShort(0)
|
||||||
|
|
||||||
|
// PTR
|
||||||
|
appendName("_airplay._tcp.local"); appendShort(12)
|
||||||
|
appendShort(0x8001); appendInt(4500)
|
||||||
|
val ptr = dnsPacket ptr@{ this@ptr.appendName(service) }
|
||||||
|
appendShort(ptr.size); appendBytes(ptr)
|
||||||
|
|
||||||
|
// SRV
|
||||||
|
appendName(service); appendShort(33)
|
||||||
|
appendShort(0x8001); appendInt(120);
|
||||||
|
appendShort(0); appendShort(0)
|
||||||
|
appendShort(port)
|
||||||
|
val srv = dnsPacket srv@{ this@srv.appendName(host) }
|
||||||
|
appendShort(srv.size); appendBytes(srv)
|
||||||
|
|
||||||
|
// TXT
|
||||||
|
appendName(service); appendShort(16)
|
||||||
|
appendShort(0x8001); appendInt(4500)
|
||||||
|
val txt = dnsPacket txt@{
|
||||||
|
for ((k, v) in txtRecords) {
|
||||||
|
val entry = "$k=$v".encodeToByteArray()
|
||||||
|
this@txt.appendByte(entry.size); this@txt.appendBytes(entry)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
appendShort(txt.size); appendBytes(txt)
|
||||||
|
|
||||||
|
// A
|
||||||
|
appendName(host); appendShort(1)
|
||||||
|
appendShort(0x8001); appendInt(120)
|
||||||
|
val ipParts = ip.split(".").map { it.toInt() and 0xff }
|
||||||
|
appendShort(4); appendByte(ipParts[0]); appendByte(ipParts[1])
|
||||||
|
appendByte(ipParts[2]); appendByte(ipParts[3])
|
||||||
|
}
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
/*
|
||||||
|
* Copyright © 2026 RTAkland
|
||||||
|
* Author: RTAkland
|
||||||
|
* Date: 2026/3/13
|
||||||
|
*/
|
||||||
|
|
||||||
|
|
||||||
|
package cn.rtast.nmdns.util
|
||||||
|
|
||||||
|
public expect class MulticastSocket internal constructor() {
|
||||||
|
/**
|
||||||
|
* bind to multicast addr and port
|
||||||
|
*/
|
||||||
|
public fun bind(port: Int)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* join multicast group
|
||||||
|
*/
|
||||||
|
public fun joinMulticastGroup(group: String)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* leave multicast group
|
||||||
|
*/
|
||||||
|
public fun leaveMulticastGroup(group: String)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* send binary packet
|
||||||
|
*/
|
||||||
|
public fun send(data: ByteArray, host: String, port: Int)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* receive packet from multicast group
|
||||||
|
*/
|
||||||
|
public fun receive(buf: ByteArray): Long
|
||||||
|
|
||||||
|
/**
|
||||||
|
* close socket
|
||||||
|
*/
|
||||||
|
public fun close()
|
||||||
|
}
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright © 2025 RTAkland
|
|
||||||
* Author: RTAkland
|
|
||||||
* Date: 9/29/25
|
|
||||||
*/
|
|
||||||
|
|
||||||
package cn.rtast.nmdns
|
|
||||||
|
|
||||||
public actual fun sleep1(time: Int) {
|
|
||||||
Thread.sleep(time * 1000L)
|
|
||||||
}
|
|
||||||
@@ -1,32 +0,0 @@
|
|||||||
/*
|
|
||||||
* 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()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,53 @@
|
|||||||
|
/*
|
||||||
|
* Copyright © 2026 RTAkland
|
||||||
|
* Author: RTAkland
|
||||||
|
* Date: 2026/3/13
|
||||||
|
*/
|
||||||
|
|
||||||
|
|
||||||
|
package cn.rtast.nmdns.util
|
||||||
|
|
||||||
|
import java.net.DatagramPacket
|
||||||
|
import java.net.InetAddress
|
||||||
|
import java.net.InetSocketAddress
|
||||||
|
import java.net.NetworkInterface
|
||||||
|
import java.net.MulticastSocket as JvmMulticastSocket
|
||||||
|
|
||||||
|
public actual class MulticastSocket actual constructor() {
|
||||||
|
|
||||||
|
private var socket: JvmMulticastSocket? = null
|
||||||
|
|
||||||
|
public actual fun bind(port: Int) {
|
||||||
|
socket = JvmMulticastSocket(port)
|
||||||
|
socket?.reuseAddress = true
|
||||||
|
}
|
||||||
|
|
||||||
|
public actual fun joinMulticastGroup(group: String) {
|
||||||
|
val groupAddr = InetAddress.getByName(group)
|
||||||
|
val ni = NetworkInterface.getByName(null)
|
||||||
|
socket?.joinGroup(InetSocketAddress(groupAddr, 0), ni)
|
||||||
|
}
|
||||||
|
|
||||||
|
public actual fun send(data: ByteArray, host: String, port: Int) {
|
||||||
|
val addr = InetAddress.getByName(host)
|
||||||
|
val packet = DatagramPacket(data, data.size, addr, port)
|
||||||
|
socket?.send(packet)
|
||||||
|
}
|
||||||
|
|
||||||
|
public actual fun receive(buf: ByteArray): Long {
|
||||||
|
val packet = DatagramPacket(buf, buf.size)
|
||||||
|
socket?.receive(packet)
|
||||||
|
return packet.length.toLong()
|
||||||
|
}
|
||||||
|
|
||||||
|
public actual fun close() {
|
||||||
|
socket?.close()
|
||||||
|
socket = null
|
||||||
|
}
|
||||||
|
|
||||||
|
public actual fun leaveMulticastGroup(group: String) {
|
||||||
|
val groupAddr = InetAddress.getByName(group)
|
||||||
|
val ni = NetworkInterface.getByName(null)
|
||||||
|
socket?.leaveGroup(InetSocketAddress(groupAddr, 0), ni)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -7,7 +7,7 @@
|
|||||||
|
|
||||||
package test
|
package test
|
||||||
|
|
||||||
import cn.rtast.nmdns.randomMacAddress
|
import cn.rtast.nmdns.util.randomMacAddress
|
||||||
import cn.rtast.nmdns.registerService
|
import cn.rtast.nmdns.registerService
|
||||||
import cn.rtast.nmdns.sleep1
|
import cn.rtast.nmdns.sleep1
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
|
|||||||
@@ -1,68 +0,0 @@
|
|||||||
/*
|
|
||||||
* 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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,64 @@
|
|||||||
|
/*
|
||||||
|
* Copyright © 2026 RTAkland
|
||||||
|
* Author: RTAkland
|
||||||
|
* Date: 2026/3/13
|
||||||
|
*/
|
||||||
|
|
||||||
|
|
||||||
|
@file:OptIn(ExperimentalForeignApi::class)
|
||||||
|
|
||||||
|
package cn.rtast.nmdns.util
|
||||||
|
|
||||||
|
import kotlinx.cinterop.*
|
||||||
|
import platform.linux.inet_addr
|
||||||
|
import platform.posix.*
|
||||||
|
|
||||||
|
public actual class MulticastSocket actual constructor() {
|
||||||
|
private var fd = -1
|
||||||
|
|
||||||
|
public actual fun bind(port: Int): Unit = memScoped {
|
||||||
|
fd = socket(fd, SOCK_DGRAM, 0)
|
||||||
|
val opt = alloc<IntVar>()
|
||||||
|
setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, opt.ptr, sizeOf<IntVar>().convert())
|
||||||
|
val addr = alloc<sockaddr_in>()
|
||||||
|
addr.sin_family = AF_INET.convert()
|
||||||
|
addr.sin_port = htons(port.toUShort())
|
||||||
|
addr.sin_addr.s_addr = INADDR_ANY
|
||||||
|
bind(fd, addr.ptr.reinterpret(), sizeOf<sockaddr_in>().convert())
|
||||||
|
}
|
||||||
|
|
||||||
|
public actual fun joinMulticastGroup(group: String): Unit = memScoped {
|
||||||
|
val merq = alloc<ip_mreq>()
|
||||||
|
merq.imr_multiaddr.s_addr = inet_addr(group)
|
||||||
|
merq.imr_interface.s_addr = INADDR_ANY
|
||||||
|
setsockopt(fd, IPPROTO_IP, IP_ADD_MEMBERSHIP, merq.ptr, sizeOf<ip_mreq>().convert())
|
||||||
|
}
|
||||||
|
|
||||||
|
public actual fun send(data: ByteArray, host: String, port: Int): Unit = 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(host)
|
||||||
|
data.usePinned {
|
||||||
|
sendto(
|
||||||
|
fd, it.addressOf(0),
|
||||||
|
data.size.convert(), 0,
|
||||||
|
addr.ptr.reinterpret(),
|
||||||
|
sizeOf<sockaddr_in>().convert()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public actual fun receive(buf: ByteArray): Long = memScoped {
|
||||||
|
buf.usePinned { recv(fd, it.addressOf(0), buf.size.convert(), 0) }
|
||||||
|
}
|
||||||
|
|
||||||
|
public actual fun close(): Unit = run { if (fd > 0) close(fd) }
|
||||||
|
|
||||||
|
public actual fun leaveMulticastGroup(group: String): Unit = memScoped {
|
||||||
|
val merq = alloc<ip_mreq>()
|
||||||
|
merq.imr_multiaddr.s_addr = inet_addr(group)
|
||||||
|
merq.imr_interface.s_addr = INADDR_ANY
|
||||||
|
setsockopt(fd, IPPROTO_IP, IP_DROP_MEMBERSHIP, merq.ptr, sizeOf<ip_mreq>().convert())
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -10,7 +10,7 @@ package test
|
|||||||
import cn.rtast.nmdns.onBroadcast
|
import cn.rtast.nmdns.onBroadcast
|
||||||
import cn.rtast.nmdns.onRegister
|
import cn.rtast.nmdns.onRegister
|
||||||
import cn.rtast.nmdns.onUnregistered
|
import cn.rtast.nmdns.onUnregistered
|
||||||
import cn.rtast.nmdns.randomMacAddress
|
import cn.rtast.nmdns.util.randomMacAddress
|
||||||
import cn.rtast.nmdns.registerService
|
import cn.rtast.nmdns.registerService
|
||||||
import cn.rtast.nmdns.sleep1
|
import cn.rtast.nmdns.sleep1
|
||||||
import kotlin.test.Test
|
import kotlin.test.Test
|
||||||
|
|||||||
@@ -7,7 +7,7 @@
|
|||||||
|
|
||||||
package test
|
package test
|
||||||
|
|
||||||
import cn.rtast.nmdns.randomMacAddress
|
import cn.rtast.nmdns.util.randomMacAddress
|
||||||
import kotlin.test.Test
|
import kotlin.test.Test
|
||||||
|
|
||||||
class TestMacAddr {
|
class TestMacAddr {
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ package test
|
|||||||
|
|
||||||
import cn.rtast.nmdns.buildPacket
|
import cn.rtast.nmdns.buildPacket
|
||||||
import cn.rtast.nmdns.createSocket
|
import cn.rtast.nmdns.createSocket
|
||||||
import cn.rtast.nmdns.randomMacAddress
|
import cn.rtast.nmdns.util.randomMacAddress
|
||||||
import cn.rtast.nmdns.sleep1
|
import cn.rtast.nmdns.sleep1
|
||||||
import kotlin.test.Test
|
import kotlin.test.Test
|
||||||
|
|
||||||
|
|||||||
@@ -1,87 +0,0 @@
|
|||||||
/*
|
|
||||||
* 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 result = setsockopt(
|
|
||||||
sock,
|
|
||||||
SOL_SOCKET,
|
|
||||||
SO_REUSEADDR,
|
|
||||||
"1",
|
|
||||||
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 = htons(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,68 @@
|
|||||||
|
/*
|
||||||
|
* Copyright © 2026 RTAkland
|
||||||
|
* Author: RTAkland
|
||||||
|
* Date: 2026/3/13
|
||||||
|
*/
|
||||||
|
|
||||||
|
|
||||||
|
@file:OptIn(ExperimentalForeignApi::class)
|
||||||
|
|
||||||
|
package cn.rtast.nmdns.util
|
||||||
|
|
||||||
|
import kotlinx.cinterop.*
|
||||||
|
import platform.posix.*
|
||||||
|
import platform.windows.AF_INET
|
||||||
|
import platform.windows.IPPROTO_UDP
|
||||||
|
import platform.windows.SOCK_DGRAM
|
||||||
|
import platform.windows.WSAStartup
|
||||||
|
import platform.windows.setsockopt
|
||||||
|
|
||||||
|
public actual class MulticastSocket actual constructor() {
|
||||||
|
|
||||||
|
private var fd: SOCKET = INVALID_SOCKET
|
||||||
|
|
||||||
|
public actual fun bind(port: Int): Unit = memScoped {
|
||||||
|
val wsaData = alloc<WSADATA>()
|
||||||
|
if (WSAStartup(0x202u, wsaData.ptr) != 0) perror("Failed to create socket")
|
||||||
|
fd = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP)
|
||||||
|
setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, "1", sizeOf<IntVar>().convert())
|
||||||
|
val addr = alloc<SOCKADDR_IN>()
|
||||||
|
addr.sin_family = AF_INET.convert()
|
||||||
|
addr.sin_port = htons(port.toUShort())
|
||||||
|
addr.sin_addr.S_un.S_addr = INADDR_ANY
|
||||||
|
bind(fd, addr.ptr.reinterpret(), sizeOf<SOCKADDR_IN>().convert())
|
||||||
|
}
|
||||||
|
|
||||||
|
public actual fun joinMulticastGroup(group: String): Unit = memScoped {
|
||||||
|
// ref: clibs/win32_join_multicast.c:7
|
||||||
|
win32_join_multicast_group.join_multicast_group(fd, group)
|
||||||
|
}
|
||||||
|
|
||||||
|
public actual fun send(data: ByteArray, host: String, port: Int): Unit = memScoped {
|
||||||
|
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(host)
|
||||||
|
data.usePinned {
|
||||||
|
// ref: clibs/win32_join_multicast.c:18
|
||||||
|
win32_join_multicast_group.socket_sendto(
|
||||||
|
fd, it.addressOf(0).reinterpret(),
|
||||||
|
data.size, host, port.toUShort()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public actual fun receive(buf: ByteArray): Long = memScoped {
|
||||||
|
buf.usePinned { recv(fd, it.addressOf(0), buf.size.convert(), 0) }.toLong()
|
||||||
|
}
|
||||||
|
|
||||||
|
public actual fun close() {
|
||||||
|
closesocket(fd)
|
||||||
|
WSACleanup()
|
||||||
|
}
|
||||||
|
|
||||||
|
public actual fun leaveMulticastGroup(group: String) {
|
||||||
|
// ref: clibs/win32_join_multicast.c:31
|
||||||
|
win32_join_multicast_group.leave_multicast_group(fd, group)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -7,7 +7,7 @@
|
|||||||
|
|
||||||
package test
|
package test
|
||||||
|
|
||||||
import cn.rtast.nmdns.randomMacAddress
|
import cn.rtast.nmdns.util.randomMacAddress
|
||||||
import cn.rtast.nmdns.registerService
|
import cn.rtast.nmdns.registerService
|
||||||
import cn.rtast.nmdns.sleep1
|
import cn.rtast.nmdns.sleep1
|
||||||
import kotlin.test.Test
|
import kotlin.test.Test
|
||||||
|
|||||||
@@ -1,28 +0,0 @@
|
|||||||
/*
|
|
||||||
* 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)
|
|
||||||
}
|
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright © 2025 RTAkland
|
|
||||||
* Author: RTAkland
|
|
||||||
* Date: 9/29/25
|
|
||||||
*/
|
|
||||||
|
|
||||||
package cn.rtast.nmdns
|
|
||||||
|
|
||||||
import platform.posix.sleep
|
|
||||||
import kotlin.experimental.ExperimentalNativeApi
|
|
||||||
|
|
||||||
@OptIn(ExperimentalNativeApi::class)
|
|
||||||
@CName("sleep1")
|
|
||||||
public actual fun sleep1(time: Int) {
|
|
||||||
sleep(time.toUInt())
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user