4

I'm trying to build a TCP server with SwiftNIO. The server starts in the net, but the clients don't know the ip address. Therefore I want to start an UDP server as well and if the clients comes up, he sends a broadcast message to the net. The server will receive and answer, so that the client now knows the IP address.

Is it possible to build something like this with SwiftNIO?

Lupurus
  • 3,618
  • 2
  • 29
  • 59

1 Answers1

6

Yes, that's possible also there's not much support in SwiftNIO to make this easy. See below for a commented example which will send HELLO WORLD once a second to en0's broadcast address and port 37020.

import NIO

let group = MultiThreadedEventLoopGroup(numberOfThreads: 1)
defer {
    try! group.syncShutdownGracefully()
}

let matchingInterfaces = try System.enumerateInterfaces().filter {
    // find an IPv4 interface named en0 that has a broadcast address.
    $0.name == "en0" && $0.broadcastAddress != nil
}

guard let en0Interface = matchingInterfaces.first, let broadcastAddress = en0Interface.broadcastAddress else {
    print("ERROR: No suitable interface found. en0 matches \(matchingInterfaces)")
    exit(1)
}

// let's bind the server socket
let server = try! DatagramBootstrap(group: group)
    // enable broadast
    .channelOption(ChannelOptions.socket(SOL_SOCKET, SO_BROADCAST), value: 1)
    .bind(to: en0Interface.address)
    .wait()
print("bound to \(server.localAddress!)")

var buffer = server.allocator.buffer(capacity: 32)
buffer.writeString("HELLO WORLD!")

var destAddr = broadcastAddress
destAddr.port = 37020 // we're sending to port 37020

// now let's just send the buffer once a second.
group.next().scheduleRepeatedTask(initialDelay: .seconds(1),
                                  delay: .seconds(1),
                                  notifying: nil) { task in
    server.writeAndFlush(AddressedEnvelope(remoteAddress: destAddr,data: buffer)).map {
        print("message sent to \(destAddr)")
    }.whenFailure { error in
        print("ERROR: \(error)")
        // and stop if there's an error.
        task.cancel()
        server.close(promise: nil)
    }
}

try server.closeFuture.wait()

In case you want to bind to 0.0.0.0 and send to 255.255.255.255 you can use this

import NIO

let group = MultiThreadedEventLoopGroup(numberOfThreads: 1)
defer {
    try! group.syncShutdownGracefully()
}

// let's bind the server socket
let server = try! DatagramBootstrap(group: group)
    // enable broadast
    .channelOption(ChannelOptions.socket(SOL_SOCKET, SO_BROADCAST), value: 1)
    .bind(to: .init(ipAddress: "0.0.0.0", port: 0))
    .wait()
print("bound to \(server.localAddress!)")

var buffer = server.allocator.buffer(capacity: 32)
buffer.writeString("HELLO WORLD!")

// we're sending to port 37020
let destPort = 37020
let destAddress = try SocketAddress(ipAddress: "255.255.255.255", port: destPort)

// now let's just send the buffer once a second.
group.next().scheduleRepeatedTask(initialDelay: .seconds(1),
                                  delay: .seconds(1),
                                  notifying: nil) { task in
    server.writeAndFlush(AddressedEnvelope(remoteAddress: destAddress, data: buffer)).map {
        print("message sent to \(destAddress)")
    }.whenFailure { error in
        print("ERROR: \(error)")
        // and stop if there's an error.
        task.cancel()
        server.close(promise: nil)
    }
}
try server.closeFuture.wait()
thislooksfun
  • 1,049
  • 10
  • 23
Johannes Weiss
  • 52,533
  • 16
  • 102
  • 136
  • Thank you very much! I tried this running on the same machine, unfortunately either the client or the server crashes, because the port is already in use. – Lupurus Oct 25 '19 at 07:46
  • Sorry, please forget it. I had to restart my machine after some other tests ;) now it works. I try to write a complete example and put in on Github – Lupurus Oct 25 '19 at 08:47
  • 1
    @Lupurus fantastic. Btw, if you know your broadcast address you can also remove most of the code and just do `remoteAddress: try! SocketAddress(ipAddress: "192.168.1.255", port: destPort)`. But in most cases, users want to broadcast to an 'interface', therefore I included the code. – Johannes Weiss Oct 25 '19 at 09:06
  • I still need time, because - and I don't know why - it's only working with SwiftNIO < 2.0.0. I could update the broadcast client to 2.9.0, but the server is just receiving if I use SwiftNIO < 2.0.0 (I use the EchoServer example of the SwiftNIO github project) – Lupurus Oct 25 '19 at 10:32
  • By the way, the server needs to be binded to "0.0.0.0" (but as I said, it only works with version < 2.0.0) – Lupurus Oct 25 '19 at 10:47
  • 1
    Updated my answer to include binding to 0.0.0.0 and sending to 255.255.255.255. It definitely works for me with NIO 2.9.0. Are you sure you mean NIO < 2.0.0? – Johannes Weiss Oct 25 '19 at 12:55