I want to implement a "simple" SSDP discovering client. Means the client should send out a
M-SEARCH * HTTP/1.1
HOST: 239.255.255.250:1900
MAN: "ssdp:discover"
MX: 1
ST: ssdp:all
and afterwards listen to "the network"(?) to get the list of IP addresses.
To test the implementation I've written a unit test which creates a "fake" MulticastServer
which simply hear to the SSDP IP&Port and, when receive something, send the same message back.
The problem is that this code works on my machine (macOS) most of the time but never on our CI Server (Linux). I (macOS) receive sometimes the same assertion failed error as on the CI. But as I said - only sometimes! Not always. And I don't know why.
This is the implementation on the client side:
interface GatewayDiscoverer {
companion object {
val instance: GatewayDiscoverer = DefaultGatewayDiscoverer()
}
suspend fun discoverGateways(timeoutMillis: Int = 1000): List<String>
}
internal class DefaultGatewayDiscoverer : GatewayDiscoverer {
override suspend fun discoverGateways(timeoutMillis: Int): List<String> {
require(timeoutMillis in 1000..5000) {
"timeoutMillis should be between 1000 (inclusive) and 5000 (inclusive)!"
}
val socket = DatagramSocket()
sendSsdpPacket(socket)
val gateways = receiveSsdpPacket(socket, timeoutMillis)
return gateways
}
private fun sendSsdpPacket(socket: DatagramSocket) {
val packetToSend =
"M-SEARCH * HTTP/1.1\r\nHOST: 239.255.255.250:1900\r\nMAN: \"ssdp:discover\"\r\nMX: 1\r\nST: ssdp:all\r\n\r\n"
val packetToSendAsBytes = packetToSend.toByteArray()
val packet = DatagramPacket(
packetToSendAsBytes,
packetToSendAsBytes.size,
InetAddress.getByName("239.255.255.250"),
1900
)
socket.send(packet)
}
private fun receiveSsdpPacket(socket: DatagramSocket, timeoutInMillis: Int): List<String> {
val gatewayList = mutableListOf<String>()
while (true) {
val receivedData = ByteArray(12)
val packetToReceive = DatagramPacket(receivedData, receivedData.size)
socket.soTimeout = timeoutInMillis
try {
socket.receive(packetToReceive)
packetToReceive.address?.hostName?.let { gatewayList.add(it) }
} catch (socketTimeout: SocketTimeoutException) {
return gatewayList
}
}
}
}
And this the test (includes the MulticastServer
):
class DefaultGatewayDiscovererTest {
@Test
fun `discover gateways should return a list of gateway IPs`() = with(MulticastServer()) {
start()
val list = runBlocking { GatewayDiscoverer.instance.discoverGateways(1000) }
close()
assertThat(list.size).isEqualTo(1)
assertThat(list).contains(InetAddress.getLocalHost().hostAddress)
Unit
}
}
/**
* A "MulticastServer" which will join the
* 239.255.255.250:1900 group to listen on SSDP events.
* They will report back with the same package
* it received.
*/
class MulticastServer : Thread(), Closeable {
private val group = InetAddress.getByName("239.255.255.250")
private val socket: MulticastSocket = MulticastSocket(1900)
init {
// This force to use IPv4...
var netinterface: NetworkInterface? = null
// Otherwise it will (at least on macOS) use IPv6 which leads to issues
// while joining the group...
val networkInterfaces = NetworkInterface.getNetworkInterfaces()
while (networkInterfaces.hasMoreElements()) {
val networkInterface = networkInterfaces.nextElement()
val addressesFromNetworkInterface = networkInterface.inetAddresses
while (addressesFromNetworkInterface.hasMoreElements()) {
val inetAddress = addressesFromNetworkInterface.nextElement()
if (inetAddress.isSiteLocalAddress
&& !inetAddress.isAnyLocalAddress
&& !inetAddress.isLinkLocalAddress
&& !inetAddress.isLoopbackAddress
&& !inetAddress.isMulticastAddress
) {
netinterface = NetworkInterface.getByName(networkInterface.name)
}
}
}
socket.joinGroup(InetSocketAddress("239.255.255.250", 1900), netinterface!!)
}
override fun run() {
while (true) {
val buf = ByteArray(256)
val packet = DatagramPacket(buf, buf.size)
try {
socket.receive(packet)
} catch (socketEx: SocketException) {
break
}
// Print for debugging
val message = String(packet.data, 0, packet.length)
println(message)
socket.send(packet)
}
}
override fun close() = with(socket) {
leaveGroup(group)
close()
}
}
When the test fails it fails on that line:
assertThat(list.size).isEqualTo(1)
The list
is empty.
After some debugging I found out that the MulticastServer
don't receive the message. Therefore the client don't get the response and add the IP address to the list
.
I would expect that the MulticastServer
will always work without that "flakiness". Do I something wrong with the implementation?