UDP Client Example in Swift 3.1

UDP stands for User Datagram Protocol. It is one of many protocols in electronic communications for send information. It is ideal for small amounts of information and/or situations where relieability is not a priority, and simplicity. For more information use Wikipedia to get started.

Most simple examples for writing programs using UDP online are in C. But, I wanted to do my example in Swift 3+. Xcode has the ability to translate C-code and Objective-C code into Swift if the appropriate header files are added to a project via an Objective-C Bridging Header. So, adding the needed C-header files to my bridging header, I set out to write a UDP Client class. This was done quick and dirty, and there may be improvements needed, but I got it to work for now and I am pretty stoked! My example project is based on the C example found here.

UDPClient.swift:

import Foundation

/**
 The enumeration defines errors that are throw UDPClient methods
 ````
 noSocket              Error thrown if a socket file descriptor could not be created
 bindSocket            Error thrown if the bind process fails for the socket file descriptor
 badAddress            Error thrown if the address string given is not proper
 alreadyInProgress     Error thrown if beginOperation() is called on a UPDClient object who's receive/send process currently running
 setupForNonBlocking   Error thrown if the init method fails to set the socket for non-blocking operation
 threadLock            Error thrown if the init method fails to obtain a valid thread lock
 
 - parameter localizedDescription: Explanation of the current error
 */
enum UDPClientError: Int, LocalizedError {
    
    case noSocket = -999
    case bindSocket
    case badAddress
    case alreadyInProgress
    case setupForNonBlocking
    case threadLock
    
    var localizedDescription: String {
        
        switch self {
            
        case .alreadyInProgress:
            return "operation in progress"
        case .badAddress:
            return "Address string given is not valid"
        case .bindSocket:
            return "Could not bind socket"
        case .noSocket:
            return "Could not obtain socket"
        case .setupForNonBlocking:
            return "Could not setup socket for non-blocking operation"
        case .threadLock:
            return "Could not obtain thread lock"
        }
        
    }
}

/**
 This class defines objects that can be created to send and receive UDP messages
 */
class UDPClient {
    
    // MARK: - properties
    // socket file descriptor
    private var mySocket: Int32 = 0
    // socket address for the client
    private var myAddress = sockaddr_in()
    // socket address for the target client
    private var otherAddress = sockaddr_in()
    // Holds messages received and read by the Rx/Tx thread method
    private var receiveQueue = [String]()
    // Holds messages to be transmitted in the Rx/Tx thread method
    private var sendQueue = [String]()
    // flag controls the execute loop with the Rx/Tx thread method
    private var okToRun = false
    // thread mutex resource
    private var threadLock = pthread_mutex_t()
    // limit for sent and received message
    private let BufferLimit = 1024
    // name for this client
    let name: String
    
    // MARK: - initialization and de-initialization
    /**
     Designated initializater
     
     - parameters:
        - name:             Client's name
        - port:             The client's port
        - otherIPAddress:   The IPv4 address of the recipient's machine
        - otherPort:        The port the recipient will be listening for the client's message
     
     - throws:A UDPClientError on failure
     */
    init(name: String, port: UInt16, otherIPAddress: String, otherPort: UInt16) throws {
        
        // assign
        self.name = name
        
        // get a socket file descriptor
        mySocket = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP)
        
        // no socket
        if mySocket == -1 {
            
            throw UDPClientError.noSocket
        }
        
        // set the socket as non-blocking, else throw error
        if fcntl(mySocket, F_SETFL, O_NONBLOCK) == -1 {
            
            throw UDPClientError.setupForNonBlocking
        }
        
        // configure the client's socket address
        myAddress.sin_family = sa_family_t(AF_INET)
        myAddress.sin_port = in_port_t(port)
        myAddress.sin_addr.s_addr = in_addr_t(INADDR_ANY)
        
        // bind the socket to the socket address
        let retCode = withUnsafeMutablePointer(to: &myAddress) {
            
            $0.withMemoryRebound(to: sockaddr.self, capacity: 1) {
                
                bind(mySocket, UnsafeMutablePointer<sockaddr>($0), socklen_t(MemoryLayout<sockaddr_in>.size))
            }
        }
        // throw error if the socket could not be bound to the socket address
        if retCode == -1 {
            
            throw UDPClientError.bindSocket
        }
        
        // configure recipient's address
        otherAddress.sin_family = sa_family_t(AF_INET)
        otherAddress.sin_port = in_port_t(otherPort)
        // need a buffer
        var buffer: [Int8] = Array(otherIPAddress.utf8CString)
        // copy address, throw error on failure
        if inet_aton(&buffer, &otherAddress.sin_addr) == 0 {
                
            throw UDPClientError.badAddress
        }
        // obtain mutex and throw error if we fail to obtain one
        if pthread_mutex_init(&threadLock, nil) != 0 {
            
            throw UDPClientError.threadLock
        }
    }
    
    /**
     Necessary cleanup performed in this method
     */
    deinit {

        pthread_mutex_unlock(&threadLock)
        
        pthread_mutex_destroy(&threadLock)
        
        close(mySocket)
    }
    
    // MARK: - methods
    /**
     This method kicks off the receive and transmit thread.
     - throws: A UDPClientError if the method is called and the receive/transmit operation is already in progress
     */
    func beginOperation() throws {
        
        // Rx/Tx thread process already running
        if okToRun {
            
            throw UDPClientError.alreadyInProgress
        }
        
        // set the flag, detach and start Rx/Tx thread
        okToRun = true
        _ = Thread.detachNewThreadSelector(#selector(process), toTarget: self, with: nil)        
    }
    
    /**
     This method stops the receive and transmit operation for the client
     */
    func endOperation() {
        
        okToRun = false
    }
    
    /**
     The method adds the desired message to the send queue
     - parameter message: Desired message to be transmitted
     */
    func send(message: String) {
        
        pthread_mutex_lock(&threadLock)
        
        sendQueue.append(message)
        
        pthread_mutex_unlock(&threadLock)
        
    }
    
    /**
     This method retrieves one message from the receive queue. Message are retreived in a first in, first out manner.
     - returns: The message at the front of the receive queue, or nil if the receive queue is empty
     */
    func getMessage() -> String? {
        
        var message: String?
        
        pthread_mutex_lock(&threadLock)
        // do we have any messages?
        if receiveQueue.isEmpty == false {
            
            message = receiveQueue.remove(at: 0)
        }
        
        pthread_mutex_unlock(&threadLock)
        
        return message
    }
    
    // MARK: - private thread process
    /**
     The method is run in a separate dedicated thread. This method contains a loop that handles both the receive
     and transmit of a message for the client
     */
    @objc private func process() {
        
        // buffers for Rx and Tx
        var sendBuffer = [UInt8](repeating: 0, count: BufferLimit)
        var receiveBuffer = [UInt8](repeating: 0, count: BufferLimit)
        // struct size, needed for revcfrom() and sendto() functions
        var slen = socklen_t(MemoryLayout<sockaddr_in>.size)
        
        print("Process running for " + name)
        
        // holds the number of bytes read and received
        var bytesRead = 0
        var bytesSent = 0
        
        /* MAIN PROCESS LOOP */
        while okToRun {
            
            // are there any messages to send?
            if sendQueue.isEmpty == false {
                // get a single message, from the front
                pthread_mutex_lock(&threadLock)
                
                var str = sendQueue.remove(at: 0)
                
                pthread_mutex_unlock(&threadLock)
                // message cannot be longer than buffer limit
                if str.characters.count > BufferLimit {
                    
                    let index = str.index(str.startIndex, offsetBy: BufferLimit)
                    str = str.substring(to: index)
                    sendBuffer.replaceSubrange(0 ..< BufferLimit, with: str.utf8)
                }
                else {
                    
                    sendBuffer.replaceSubrange(0 ..< str.characters.count, with: str.utf8)
                }
                // send message to recipient
                bytesSent = withUnsafeMutablePointer(to: &otherAddress) {
                    
                    $0.withMemoryRebound(to: sockaddr.self, capacity: 1) {
                        
                        sendto(mySocket, sendBuffer, str.characters.count, 0, $0, slen)
                    }
                }
                // successfully sent message
                if bytesSent != -1 {
                    
                    print("\(name): bytes sent = \(bytesSent)")
                }
                // reset buffer
                sendBuffer = [UInt8](repeating: 0, count: BufferLimit)
            }
            // read message, can be no longer than buffer limit
            bytesRead = withUnsafeMutablePointer(to: &otherAddress) {
                
                $0.withMemoryRebound(to: sockaddr.self, capacity: 1) {
                    
                    recvfrom(mySocket, &receiveBuffer, BufferLimit, 0, $0, &slen)
                }
            }
            // successfully read message
            if bytesRead != -1 {
                
                print("\(name): bytes read = \(bytesRead)")
                // add received message to queue
                pthread_mutex_lock(&threadLock)
                
                receiveQueue.append(String(bytes: receiveBuffer, encoding: .utf8)!)
                
                pthread_mutex_unlock(&threadLock)
                
                slen = socklen_t(MemoryLayout<sockaddr_in>.size)
                // reset buffer
                receiveBuffer = [UInt8](repeating: 0, count: BufferLimit)
            }
            // reset byte counts
            bytesRead = 0
            bytesSent = 0
            
        } // end processing loop
        
    } // end process
    
}

UDPClient-Bridging-Header.h:

#ifndef UDP_Bridging_Header_h
#define UDP_Bridging_Header_h

#include <arpa/inet.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <netinet/in.h>
#include <fcntl.h>
#include <pthread.h>

#endif /* UDP_Bridging_Header_h */

Main.swift for testing out our class:

import Foundation

var client_1: UDPClient?
var client_2: UDPClient?

// attempt to create clients, catch errors
do {
    
    try client_1 = UDPClient(name: "Client A", port: 9990, otherIPAddress: "192.112.171.32", otherPort: 9992)
}
catch {
    
    print(error.localizedDescription)
}

do {
    
    try client_2 = UDPClient(name: "Client B", port: 9992, otherIPAddress: "192.112.171.32", otherPort: 9990)
}
catch {
    
    print(error.localizedDescription)
}
// if both clients created successfully
if client_1 != nil && client_2 != nil {
    
    client_1!.send(message: "Finally got this thing to work")
    client_1!.send(message: "Feels good don't it man!")
    client_1!.send(message: "...and one more for good measure.")
    // attempt to begin Rx/Tx operation, catch error
    do {
        
        try client_1!.beginOperation()
    }
    catch {
        
        print(error.localizedDescription)
    }
    
    do {
        
        try client_2!.beginOperation()
    }
    catch {
        
        print(error.localizedDescription)
    }
    // hold up...
    Thread.sleep(forTimeInterval: 2.0)
    // print first message
    if let msg = client_2!.getMessage() {
        
        print("got something: " + msg)
    }
    
    
    // hold up again...
    Thread.sleep(forTimeInterval: 2.0)
    // print another message...
    if let msg = client_2!.getMessage() {
        
        print("got something else: " + msg)
    }
    // hold up just once more...
    Thread.sleep(forTimeInterval: 2.0)
    // last message
    if let msg = client_2!.getMessage() {
        
        print("got something last message: " + msg)
    }
}

Running main.swift on my machine I get the following in the console view:

This project was done using a late 2016 MacBook Pro and Xcode 8.3.2 (8E2002).