paint-brush
Building a UDP Server & Client in Swift with Network.frameworkby@element
324 reads
324 reads

Building a UDP Server & Client in Swift with Network.framework

by Maxim Egorov9mMarch 5th, 2025
Read on Terminal Reader
Read this story w/o Javascript
tldt arrow

Too Long; Didn't Read

This guide explores Apple’s Network.framework for creating a UDP server and client in Swift. Learn why UDP is ideal for real-time applications, how to implement it step by step, and how it compares to TCP. Includes code snippets and setup instructions for building low-latency networking solutions.

Companies Mentioned

Mention Thumbnail
Mention Thumbnail
featured image - Building a UDP Server & Client in Swift with Network.framework
Maxim Egorov HackerNoon profile picture
0-item

Hi, my name is Maxim Egorov, and I am an iOS developer at B2Broker. In this article, I'll show you how to use Network.framework in iOS. We'll also look at the UDP protocol, why it’s useful, and how to make a simple server and client app.

Why work at the Transport Layer?

Network communication is typically structured into layers, as described by the OSI model. At the application layer, iOS developers commonly use URLSession for HTTP-based communication. For lower-level networking tasks, such as working with TCP and UDP, the Network framework provides more direct control over transport layer communication. This allows for greater flexibility and efficiency compared to higher-level APIs like URLSession.


There are cases where working directly at the transport layer is necessary:


  • Reducing overhead associated with HTTP-based communication. For example, VoIP callsonline games, and real-time data streaming benefit from UDP, as it does not require connection establishment like TCP. In TCP, before data transmission can begin, a connection must be established through a process known as the three-way handshake. This involves an exchange of synchronization (SYN) and acknowledgment (ACK) packets between the client and the server, introducing additional latency. In contrast, UDP is a connectionless protocol, meaning data can be sent immediately without establishing a session beforehand. This makes it ideal for real-time applications where speed is critical, where minor packet loss is acceptable but low latency is essential.


  • Developing IoT devices or real-time protocols where low latency and fine-grained control over packet transmission are required.


  • Working with custom network protocols, such as integrating with hardware or server-side systems that do not use HTTP.


  • Optimizing power consumption by managing connections directly, which is crucial for mobile devices.


Using Network.framework, developers can work with TCP, UDP, and TLS, gaining greater flexibility and efficiency in network communication.


My example comes from my experience working at Arrival, where we faced the challenge of sending messages to a test vehicle as quickly as possible to ensure the fastest possible response to user actions in the mobile application. Using the UDP protocol, we achieved near-instantaneous communication, allowing the tester to control the vehicle like a remote-controlled car.

UDPServer

Step 1: Initialize a New Swift Package

First, create a new executable Swift package using the terminal command:

swift package init --type executable

This command sets up an empty CLI project structure.

Step 2: Update Package.swift

Now, open the Package.swift file and modify it to match the following:

let package = Package(
    name: "UDPServer",
    platforms: [
        .macOS(.v10_15),
    ],
    targets: [
        .executableTarget(name: "UDPServer"),
    ]
)

Step 3: Implement UDPServer Class

We will create a class UDPServerImpl to manage UDP connections. First, import the necessary framework:

import Network

final class UDPServerImpl: Sendable {
    private let connectionListener: NWListener
    
    /// Initializes the UDP Server with a given port
    /// - Parameter port: The UDP port to listen on
    init(port: UInt16) throws {
        connectionListener = try NWListener(
            using: .udp,
            on: NWEndpoint.Port(integerLiteral: port)
        )
        // Handle new incoming connections
        connectionListener.newConnectionHandler = { [weak self] connection in
            // Start connection processing in global queue
            connection.start(queue: .global())
            self?.receive(on: connection)
        }
        // Handle listener state changes
        connectionListener.stateUpdateHandler = { state in
            print("Server state: \(state)")
        }
    }
}


Define the Server Protocol. To maintain clean architecture and encapsulation, let's define a protocol:

protocol UDPServer {
    func start()
    func stop()
}

extension UDPServerImpl: UDPServer {
    /// Starts the UDP server
    func start() {
        connectionListener.start(queue: .global())
        print("Server started")
    }
    
    /// Stops the UDP server
    func stop() {
        connectionListener.cancel()
        print("Server stopped")
    }
}

// MARK: - Private Methods
private extension UDPServerImpl {
    /// Handles incoming messages from clients
    /// - Parameter connection: The connection on which to receive data
    func receive(on connection: NWConnection) {
        connection.receiveMessage { data, _, _, error in
            if let error = error {
                print("New message error: \(error)")
            }
            
            if
                let data = data, let message = String(data: data, encoding: .utf8) {
                print("New message: \(message)")
            }
        }
    }
}

Step 4: Update main.swift

Finally, update the main.swift file to initialize and start the server:

import Foundation

do {
    // Listening on port 8888 server.start()
    let server: UDPServer = try UDPServerImpl(port: 8888)
    server.start()
    
    // Keeps the program running to listen for messages
    RunLoop.main.run()
} catch let error {
    print("Failed to initialize listener: \(error)")
    exit(EXIT_FAILURE)
}

Step 5: Build and Run the Server

Execute the following commands in the terminal:

swift build swift run

You should see:

Server started Server state: ready

Step 6: Send a Test Message

To send a test message to the server, use the following command in another terminal window:

echo "Hello, Hackernoon" | nc -u 127.0.0.1 8888

If the server is running correctly, you should see this output:

New message: Hello, Hackernoon


UDPClient

Step 1: Initialize a New Swift Package

First, create a new executable Swift package using the terminal command:

swift package init --type executable

This command sets up an empty CLI project structure.

Step 2: Update Package.swift

Now, open the Package.swift file and modify it to match the following:

let package = Package(
    name: "UDPClient",
    platforms: [
        .macOS(.v10_15),
    ],
    targets: [
        .executableTarget(name: "UDPClient"),
    ]
)

Step 3: Implement the UDPClient Class

import Network

protocol UDPClient {
    func start()
    func stop()
    func send(message: String)
}

final class UDPClientImpl: Sendable {
    private let connection: NWConnection

    /// Initializes the UDP Client 
    /// - Parameters: 
    /// - host: The server's hostname or IP address
    /// - port: The server's UDP port
    /// - initialMessage: The first message to send upon connection 
    init(
        host: String,
        port: UInt16,
        initialMessage: String
    ) { 
        connection = NWConnection(
            host: NWEndpoint.Host(host),
            port: NWEndpoint.Port(integerLiteral: port), 
            using: .udp
        )
        connection.stateUpdateHandler = { [weak self] state in
            print("Client: state = \\(state)") 
            if state == .ready { 
                self?.send(message: initialMessage)
            }  
        }
    }
}

extension UDPClientImpl: UDPClient {
    /// Starts the UDP client
    func start() {
        connection.start(queue: .global())
        print("Client: started")
    }
    
    /// Stops the UDP client
    func stop() {
        connection.cancel()
        print("Client: stopped")
    }
    
    /// Sends a message to the UDP server
    /// - Parameter message: The message to send
    func send(message: String) {
        guard let data = message.data(using: .utf8) else {
            print("Client: encoding message error")
            return
        }

        connection.send(content: data, completion: .contentProcessed { error in
            if let error = error {
                print("Client: failed to send message: \(error)")
            } else {
                print("Client: message sent")
            }
        })
    }
}

Step 4: Update main.swift

Modify main.swift to initialize and start the client:

import Foundation

// Ensure a message is provided as a command-line argument

guard let message = CommandLine.arguments.last else { exit(EXIT_FAILURE) }

// Create and start the UDP client 
let client: UDPClient = UDPClientImpl(
    host: "127.0.0.1", 
    port: 8888, 
    initialMessage: message
)
client.start()

// Keep the client running to allow communication 
RunLoop.main.run()

Step 5: Build and Run the Client

Execute the following commands in the terminal:

swift build 
swift run UDPClient "Hello Hackernoon"


Expected output UDPClient:

Client: started 
Client: state = preparing
Client: state = ready 
Client: message sent


Expected output UDPServer:

New message: Hello Hackernoon


Congratulations! You have created a UDP server and client with Apple's Network framework 🚀🚀🚀

Conclusion

The Network framework offers a powerful and convenient API for handling low-level networking tasks. In this example, I demonstrated how to implement a UDP server and client using NWListener and NWConnection, enabling communication over the UDP protocol. This implementation can be extended with features such as multi-client support, enhanced error handling, and additional connection settings, making it suitable for more complex networking scenarios like real-time applications, multiplayer games, or IoT communications.

References