refactor basic

WIP
This commit is contained in:
2026-03-14 00:12:51 +08:00
parent f6d9d897f6
commit 4a45474e21
35 files changed
+544 -637

No files matched your search

+1
View File
@@ -46,3 +46,4 @@ bin/
.idea/
/src/nativeInterop/cinterop/def/
/src/cinterop/win32_join_multicast_group.def
+13
View File
@@ -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)
Supported platforms:
1. windows64(mingwx64)
2. linuxX64
3. linuxArm64
@@ -14,13 +15,25 @@ Supported platforms:
5. macosX64
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
1. A
2. PTR
3. SRV
4. TXT
Supported features
1. Event hooking(Kotlin only)
> AAAA is not supported
+15 -2
View File
@@ -2,9 +2,10 @@ import com.vanniktech.maven.publish.SonatypeHost
import org.jetbrains.kotlin.gradle.dsl.JvmTarget
plugins {
kotlin("multiplatform") version "2.2.0"
kotlin("multiplatform") version "2.3.10"
id("com.vanniktech.maven.publish") version "0.31.0-rc2"
id("signing")
id("cn.rtast.kdef") version "0.1.1"
}
val libVersion: String by project
@@ -20,7 +21,14 @@ kotlin {
val nativeTargets = listOf(
linuxArm64(),
linuxX64(),
mingwX64(),
mingwX64 {
compilations["main"].cinterops {
@Suppress("UNUSED")
val joinMulticastGroup by creating {
definitionFile = file("src/cinterop/win32_join_multicast_group.def")
}
}
},
macosArm64(),
macosX64()
)
@@ -126,3 +134,8 @@ tasks.register("linkSharedLibrary") {
dependsOn(tasks.named("linkReleaseSharedMacosArm64"))
dependsOn(tasks.named("linkReleaseSharedMingwX64"))
}
kdef {
outputDir = file("src/cinterop")
defFiles.add(file("src/cinterop/template/win32_join_multicast_group.def"))
}
+40
View File
@@ -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;
}
+22
View File
@@ -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())
}
}
+1 -1
View File
@@ -7,7 +7,7 @@
package test
import cn.rtast.nmdns.randomMacAddress
import cn.rtast.nmdns.util.randomMacAddress
import cn.rtast.nmdns.registerService
import cn.rtast.nmdns.sleep1
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()
@@ -5,17 +5,15 @@
*/
package cn.rtast.nmdns
package cn.rtast.nmdns.util
import kotlin.experimental.ExperimentalNativeApi
import kotlin.native.CName
import kotlin.random.Random
/**
* generate random mac address
*/
@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() }
@@ -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)
}
}
+1 -1
View File
@@ -7,7 +7,7 @@
package test
import cn.rtast.nmdns.randomMacAddress
import cn.rtast.nmdns.util.randomMacAddress
import cn.rtast.nmdns.registerService
import cn.rtast.nmdns.sleep1
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())
}
}
+1 -1
View File
@@ -10,7 +10,7 @@ package test
import cn.rtast.nmdns.onBroadcast
import cn.rtast.nmdns.onRegister
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.sleep1
import kotlin.test.Test
+1 -1
View File
@@ -7,7 +7,7 @@
package test
import cn.rtast.nmdns.randomMacAddress
import cn.rtast.nmdns.util.randomMacAddress
import kotlin.test.Test
class TestMacAddr {
+1 -1
View File
@@ -9,7 +9,7 @@ package test
import cn.rtast.nmdns.buildPacket
import cn.rtast.nmdns.createSocket
import cn.rtast.nmdns.randomMacAddress
import cn.rtast.nmdns.util.randomMacAddress
import cn.rtast.nmdns.sleep1
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)
}
}
+1 -1
View File
@@ -7,7 +7,7 @@
package test
import cn.rtast.nmdns.randomMacAddress
import cn.rtast.nmdns.util.randomMacAddress
import cn.rtast.nmdns.registerService
import cn.rtast.nmdns.sleep1
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())
}