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 calls, online 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")
}
/// Starts 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.