swift-nio

SwiftNIO: Send and receive UDP broadcast


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?


Solution

  • 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()