How to send only one UDP packet with swift playgrounds?

hi all


How to send only one UDP packet with swift playgrounds?


Thanks,

Replies

The challenge here is not the Swift playground but sending UDP in general. Apple platforms do not have a nice, high-level API for UDP and thus you have to use BSD Sockets. Using BSD Sockets from Swift is quite challenging. You can find some hints on that topic in this thread.

On the UDP side of things, a good place to start is the UDPEcho sample code. It is in Objective-C, but if you have any questions about how to convert something to Swift I’d be happy to answer them.

Even with all of this info, it’s still a challenge assembling things into a coherent whole. Pasted in below is an example I just created out of the stuff I discussed above.

Share and Enjoy

Quinn “The Eskimo!”
Apple Developer Relations, Developer Technical Support, Core OS/Hardware

let myEmail = "eskimo" + "1" + "@apple.com"
import Foundation

extension sockaddr_storage {

    // not actually in Socket API Helper, but an 'obvious' extension

    init(sa: UnsafeMutablePointer<sockaddr>, saLen: socklen_t) {
        var ss = sockaddr_storage()
        withUnsafeMutableBytes(of: &ss) { ssPtr -> Void in
            let addrBuf = UnsafeRawBufferPointer(start: sa, count: Int(saLen))
            assert(addrBuf.count <= MemoryLayout<sockaddr_storage>.size)
            ssPtr.copyMemory(from: addrBuf)
        }
        self = ss
    }

    // from Socket API Helper

    static func fromSockAddr<ReturnType>(_ body: (_ sa: UnsafeMutablePointer<sockaddr>, _ saLen: inout socklen_t) throws -> ReturnType) rethrows -> (ReturnType, sockaddr_storage) {
        // We need a mutable `sockaddr_storage` so that we can pass it to `withUnsafePointer(to:_:)`.
        var ss = sockaddr_storage()
        // Similarly, we need a mutable copy of our length for the benefit of `saLen`.
        var saLen = socklen_t(MemoryLayout<sockaddr_storage>.size)
        let result = try withUnsafeMutablePointer(to: &ss) {
            try $0.withMemoryRebound(to: sockaddr.self, capacity: 1) {
                try body($0, &saLen)
            }
        }
        return (result, ss)
    }

    // from Socket API Helper

    func withSockAddr<ReturnType>(_ body: (_ sa: UnsafePointer<sockaddr>, _ saLen: socklen_t) throws -> ReturnType) rethrows -> ReturnType {
        // We need to create a mutable copy of `self` so that we can pass it to `withUnsafePointer(to:_:)`.
        var ss = self
        return try withUnsafePointer(to: &ss) {
            try $0.withMemoryRebound(to: sockaddr.self, capacity: 1) {
                try body($0, socklen_t(self.ss_len))
            }
        }
    }
}

func addressesFor(host: String, port: Int) throws -> [sockaddr_storage] {
    var hints = addrinfo()
    hints.ai_socktype = SOCK_DGRAM
    var addrList: UnsafeMutablePointer<addrinfo>? = nil
    let err = getaddrinfo(host, "\(port)", &hints, &addrList)
    guard err == 0, let start = addrList else {
        throw NSError(domain: NSURLErrorDomain, code: NSURLErrorCannotFindHost, userInfo: nil)
    }
    defer { free(addrList) }
    return sequence(first: start, next: { $0.pointee.ai_next} ).map { (addr) -> sockaddr_storage in
        sockaddr_storage(sa: addr.pointee.ai_addr, saLen: addr.pointee.ai_addrlen)
    }
}

func sendExample() {
    guard let addresses = try? addressesFor(host: "fluffy.local.", port: 12345) else {
        print("host not found")
        return
    }
    if addresses.count != 1 {
        print("host ambiguous; using the first one")
    }
    let address = addresses[0]
    let fd = socket(Int32(address.ss_family), SOCK_DGRAM, 0)
    guard fd >= 0 else {
        print("`socket` failed`")
        return
    }
    defer {
        let junk = close(fd)
        assert(junk == 0)
    }
    let message = "Hello Cruel World!\r\n\(Date())\r\n".data(using: .utf8)!
    let messageCount = message.count
    let sendResult = message.withUnsafeBytes { (messagePtr: UnsafePointer<UInt8>) -> Int in
        return address.withSockAddr { (sa, saLen) -> Int in
            return sendto(fd, messagePtr, messageCount, 0, sa, saLen)
        }
    }
    guard sendResult >= 0 else {
        print("send failed")
        return
    }
    print("success")
}

sendExample()

I’ve found this code really useful for an IoT device I’m building, but being new to swift I couldn’t figure out how to create a ReadExample function. I tried modifying your SendExample but using “recvfrom“ but just kept getting compiler errors I couldn’t solve so I was wondering if you could possibly post an addition to this code showing a ReadExample function as well. It would be great help, thanks.

These days I would use Network framework rather than BSD Sockets. It makes this sort of thing much easier. Pasted in below is an example playground that shows this is action. Use

nc -u -l 12345
for the server side.

Share and Enjoy

Quinn “The Eskimo!”
Apple Developer Relations, Developer Technical Support, Core OS/Hardware

let myEmail = "eskimo" + "1" + "@apple.com"
import Network
import Foundation
import PlaygroundSupport

class Main {

    init() {
        let connection = NWConnection(host: "sully.local.", port: 12345, using: .udp)
        self.connection = connection
    }

    let connection: NWConnection

    func start() {
        // Start the connection.
        connection.stateUpdateHandler = self.stateDidChange(to:)
        connection.start(queue: .main)
        self.setupReceive(connection)
        // Start the send timer.
        let sendTimer = DispatchSource.makeTimerSource(queue: .main)
        sendTimer.setEventHandler(handler: self.send)
        sendTimer.schedule(deadline: .now(), repeating: 1.0)
        sendTimer.resume()
        self.sendTimer = sendTimer
    }

    var sendTimer: DispatchSourceTimer?

    func stateDidChange(to state: NWConnection.State) {
        switch state {
        case .setup:
            break
        case .waiting(let error):
            self.connectionDidFail(error: error)
        case .preparing:
            print("Preparing")
        case .ready:
            print("Connected")
        case .failed(let error):
            self.connectionDidFail(error: error)
        case .cancelled:
            break
        }
    }

    func send() {
        let messageID = UUID()
        self.connection.send(content: "\(messageID)\r\n".data(using: .utf8)!, completion: .contentProcessed({ sendError in
            if let error = sendError {
                self.connectionDidFail(error: error)
            } else {
                print("Did send, messageID: \(messageID)")
            }
        }))
    }

    func setupReceive(_ connection: NWConnection) {
        connection.receiveMessage { (data, _, isComplete, error) in
            if let data = data, !data.isEmpty {
                print("Did receive, size: \(data.count)")
            }
            if let error = error {
                self.connectionDidFail(error: error)
                return
            }
            self.setupReceive(connection)
        }
    }

    func connectionDidFail(error: Error) {
        print("Failed, error: \(error)")
        if connection.stateUpdateHandler != nil {
            self.connection.stateUpdateHandler = nil
            connection.cancel()
        }
        exit(0)
    }
}

PlaygroundPage.current.needsIndefiniteExecution = true
let m = Main()
m.start()

Thanks for this code. Unfortunately, I’m having problems with it. Firstly I had to add some if #available(iOSApplicationExtension 12.0, *) lines (but maybe that’s normal).


The main issue I’m having is with receiving still. If I use the nc -u -l on a Mac the playground code works fine. But sending and receiving to and from my IoT device and code causes issues.


I can send data to the IoT device and it receives fine, but if I send data from the device to the playground app nothing is received. Now, I know you’ll say it’s my code on the IoT, but if I run an echo server running in playground the IoT sends and receives fine.


Any idea what could be causing this inconsistency? Is there any kind of special header that your code is expecting? The receive message code isn’t even triggering and then failing. It never seems to trigger at all. Is there anything in the new framework that would error check the message I’m sending and screen it before your code even gets it? I’ve tried sending different types of messages from the IoT, and even just echoed back what your code is sending out, but nothing is ever received. Whereas the Echo UDP playground code echoes back fine.

A clear example. Thanks.

However, when I run the code it does keep sending messages but it never receives one.

Regards,

Ghislain


Catalina 10.15.3

Xcode 11.3

Macbook Air

Same. @eskimo this code here doesn't seem to receive, it's effectively the same code but not using a timer to send, right? Is there something missing?


class UDPClient {
   
    var connection: NWConnection
    var address: NWEndpoint.Host
    var port: NWEndpoint.Port
    var delegate: UDPListener?
    private var listening = true
   
    var resultHandler = NWConnection.SendCompletion.contentProcessed { NWError in
        guard NWError == nil else {
            print("ERROR! Error when data (Type: Data) sending. NWError: \n \(NWError!)")
            return
        }
    }

    init?(address newAddress: String, port newPort: Int32, listener isListener: Bool = true) {
        guard let codedAddress = IPv4Address(newAddress),
            let codedPort = NWEndpoint.Port(rawValue: NWEndpoint.Port.RawValue(newPort)) else {
                print("Failed to create connection address")
                return nil
        }
        address = .ipv4(codedAddress)
        port = codedPort
        listening = isListener
        connection = NWConnection(host: address, port: port, using: .udp)
           
        connect()
    }
   
    func connect() {
        connection.stateUpdateHandler = { newState in
            switch (newState) {
            case .ready:
                print("State: Ready")
                if self.listening { self.listen() }
            case .setup:
                print("State: Setup")
            case .cancelled:
                print("State: Cancelled")
            case .preparing:
                print("State: Preparing")
            default:
                print("ERROR! State not defined!\n")
            }
        }
        connection.start(queue: .global())
    }
   
    func send(_ data: Data) {
        connection.send(content: data, completion: resultHandler)
    }
   
    private func listen() {
        while listening {
            connection.receiveMessage { data, context, isComplete, error in
                print("Receive isComplete: " + isComplete.description)
                guard let data = data else {
                    print("Error: Received nil Data")
                    return
                }
                print("Data Received")
            }
        }
    }
}