|
1 | 1 | # Socket |
2 | | -Swift async/await based socket library |
3 | 2 |
|
4 | | -## Introduction |
| 3 | +A modern Swift library for working with POSIX sockets using async/await. |
5 | 4 |
|
6 | | -This library exposes an idiomatic Swift API for interacting with POSIX sockets via an async/await interface. What makes this library unique (even to the point that Swift NIO is still using a [custom socket / thread pool](https://forums.swift.org/t/swift-5-5-supports-concurrency-is-there-any-change-in-swift-nio/50940/2)) is that it was built exclusively using Swift Concurrency and doesn't use old blocking C APIs, CFSocket, DispatchIO, CFRunloop, GCD, or explicitly create a single thread outside of the Swift's global cooperative thread pool to manage the sockets and polling. |
| 5 | +## Overview |
7 | 6 |
|
8 | | -The result is a Socket API that is optimized for async/await and built from the group up. Additionally, like the System, and Concurrency APIs, the Socket is represented as a `struct` instead of a class, greatly reducing ARC overhead. The internal state for the socket is managed by a singleton that stores both its state, and keeps an array of managed file descriptors so polling is global. |
| 7 | +Socket is a low-level networking library that provides a Swift-native interface for socket programming. It leverages Swift's async/await concurrency model to offer non-blocking I/O operations without the complexity of callbacks or legacy technologies like CFSocket or GCD. |
9 | 8 |
|
10 | | -## Goals |
| 9 | +### Key Features |
11 | 10 |
|
12 | | -- Minimal overhead for Swift Async/Await |
13 | | -- Minimal ARC overhead, keep state outside of `Socket` |
14 | | -- Avoid thread explosion and overcomitting the system |
15 | | -- Use actors to prevent blocking threads |
16 | | -- Optimize polling and C / System API usage |
17 | | -- Low energy usage and memory overhead |
| 11 | +- ✅ **Pure Swift Concurrency** - Built exclusively with async/await |
| 12 | +- ✅ **Cross-Platform** - Supports macOS, iOS, tvOS, watchOS, and Linux |
| 13 | +- ✅ **Multiple Protocols** - TCP, UDP, Unix domain sockets, and raw sockets |
| 14 | +- ✅ **IPv4 & IPv6** - Full support for both IP versions |
| 15 | +- ✅ **Type-Safe** - Leverages Swift's type system for socket options and addresses |
| 16 | +- ✅ **High Performance** - Minimal overhead with value types and efficient polling |
| 17 | +- ✅ **Event Streams** - Monitor socket events with AsyncStream |
| 18 | + |
| 19 | +## Installation |
| 20 | + |
| 21 | +### Swift Package Manager |
| 22 | + |
| 23 | +Add Socket to your `Package.swift` file: |
| 24 | + |
| 25 | +```swift |
| 26 | +// swift-tools-version:5.7 |
| 27 | +import PackageDescription |
| 28 | + |
| 29 | +let package = Package( |
| 30 | + name: "MyApp", |
| 31 | + platforms: [ |
| 32 | + .macOS(.v10_15), |
| 33 | + .iOS(.v13), |
| 34 | + .tvOS(.v13), |
| 35 | + .watchOS(.v6) |
| 36 | + ], |
| 37 | + dependencies: [ |
| 38 | + .package(url: "https://github.com/PureSwift/Socket.git", from: "0.5.0") |
| 39 | + ], |
| 40 | + targets: [ |
| 41 | + .target( |
| 42 | + name: "MyApp", |
| 43 | + dependencies: ["Socket"] |
| 44 | + ) |
| 45 | + ] |
| 46 | +) |
| 47 | +``` |
| 48 | + |
| 49 | +Then run: |
| 50 | +```bash |
| 51 | +swift package resolve |
| 52 | +``` |
| 53 | + |
| 54 | +### Xcode |
| 55 | + |
| 56 | +1. In Xcode, select **File > Add Package Dependencies...** |
| 57 | +2. Enter the repository URL: `https://github.com/PureSwift/Socket` |
| 58 | +3. Select the version you want to use |
| 59 | +4. Add Socket to your target |
| 60 | + |
| 61 | +## Quick Start |
| 62 | + |
| 63 | +### TCP Client |
| 64 | + |
| 65 | +```swift |
| 66 | +import Socket |
| 67 | + |
| 68 | +// Connect to a TCP server |
| 69 | +let socket = try await Socket(IPv4Protocol.tcp) |
| 70 | +let address = IPv4SocketAddress(address: .init(127, 0, 0, 1), port: 8080) |
| 71 | +try await socket.connect(to: address) |
| 72 | + |
| 73 | +// Send data |
| 74 | +let message = "Hello, Server!".data(using: .utf8)! |
| 75 | +try await socket.write(message) |
| 76 | + |
| 77 | +// Receive response |
| 78 | +let response = try await socket.read(1024) |
| 79 | +print("Received: \(String(data: response, encoding: .utf8) ?? "")") |
| 80 | + |
| 81 | +// Close the socket |
| 82 | +await socket.close() |
| 83 | +``` |
| 84 | + |
| 85 | +### TCP Server |
| 86 | + |
| 87 | +```swift |
| 88 | +import Socket |
| 89 | + |
| 90 | +// Create a server socket |
| 91 | +let address = IPv4SocketAddress(address: .any, port: 8080) |
| 92 | +let server = try await Socket(IPv4Protocol.tcp, bind: address) |
| 93 | +try await server.listen(backlog: 10) |
| 94 | + |
| 95 | +print("Server listening on port 8080") |
| 96 | + |
| 97 | +// Accept connections |
| 98 | +while true { |
| 99 | + let client = try await server.accept() |
| 100 | + |
| 101 | + // Handle client in a task |
| 102 | + Task { |
| 103 | + do { |
| 104 | + // Read request |
| 105 | + let data = try await client.read(1024) |
| 106 | + print("Received: \(String(data: data, encoding: .utf8) ?? "")") |
| 107 | + |
| 108 | + // Send response |
| 109 | + let response = "Hello, Client!".data(using: .utf8)! |
| 110 | + try await client.write(response) |
| 111 | + |
| 112 | + await client.close() |
| 113 | + } catch { |
| 114 | + print("Client error: \(error)") |
| 115 | + } |
| 116 | + } |
| 117 | +} |
| 118 | +``` |
| 119 | + |
| 120 | +### UDP Socket |
| 121 | + |
| 122 | +```swift |
| 123 | +import Socket |
| 124 | + |
| 125 | +// Create UDP socket |
| 126 | +let socket = try await Socket(IPv4Protocol.udp) |
| 127 | +let address = IPv4SocketAddress(address: .any, port: 9090) |
| 128 | +try await socket.bind(to: address) |
| 129 | + |
| 130 | +// Send datagram |
| 131 | +let message = "Hello, UDP!".data(using: .utf8)! |
| 132 | +let remoteAddress = IPv4SocketAddress(address: .init(127, 0, 0, 1), port: 9091) |
| 133 | +try await socket.sendMessage(message, to: remoteAddress) |
| 134 | + |
| 135 | +// Receive datagram |
| 136 | +let (data, sender) = try await socket.receiveMessage(1024, fromAddressOf: IPv4SocketAddress.self) |
| 137 | +print("Received from \(sender): \(String(data: data, encoding: .utf8) ?? "")") |
| 138 | +``` |
| 139 | + |
| 140 | +### Unix Domain Socket |
| 141 | + |
| 142 | +```swift |
| 143 | +import Socket |
| 144 | +import System |
| 145 | + |
| 146 | +// Server |
| 147 | +let path = FilePath("/tmp/my.sock") |
| 148 | +let address = UnixSocketAddress(path: path) |
| 149 | +let server = try await Socket(UnixProtocol.stream, bind: address) |
| 150 | +try await server.listen() |
| 151 | + |
| 152 | +// Client |
| 153 | +let client = try await Socket(UnixProtocol.stream) |
| 154 | +try await client.connect(to: address) |
| 155 | +``` |
| 156 | + |
| 157 | +### Using Event Streams |
| 158 | + |
| 159 | +Monitor socket events asynchronously: |
| 160 | + |
| 161 | +```swift |
| 162 | +import Socket |
| 163 | + |
| 164 | +let socket = try await Socket(IPv4Protocol.tcp) |
| 165 | + |
| 166 | +// Monitor events |
| 167 | +Task { |
| 168 | + for await event in socket.event { |
| 169 | + switch event { |
| 170 | + case .read: |
| 171 | + let data = try await socket.read(1024) |
| 172 | + print("Read \(data.count) bytes") |
| 173 | + case .write: |
| 174 | + print("Socket ready for writing") |
| 175 | + case .error(let error): |
| 176 | + print("Socket error: \(error)") |
| 177 | + case .close: |
| 178 | + print("Socket closed") |
| 179 | + break |
| 180 | + default: |
| 181 | + break |
| 182 | + } |
| 183 | + } |
| 184 | +} |
| 185 | + |
| 186 | +// Use the socket... |
| 187 | +``` |
| 188 | + |
| 189 | +### Socket Options |
| 190 | + |
| 191 | +Configure socket behavior with type-safe options: |
| 192 | + |
| 193 | +```swift |
| 194 | +import Socket |
| 195 | + |
| 196 | +let socket = try await Socket(IPv4Protocol.tcp) |
| 197 | + |
| 198 | +// Set socket options |
| 199 | +try socket.setOption(.reuseAddress, true) |
| 200 | +try socket.setOption(.keepAlive, true) |
| 201 | +try socket.setOption(.noDelay, true) // Disable Nagle's algorithm |
| 202 | + |
| 203 | +// Get socket option |
| 204 | +let keepAlive: Bool = try socket[.keepAlive] |
| 205 | +print("Keep-alive enabled: \(keepAlive)") |
| 206 | +``` |
| 207 | + |
| 208 | +### IPv6 Support |
| 209 | + |
| 210 | +```swift |
| 211 | +import Socket |
| 212 | + |
| 213 | +// IPv6 TCP socket |
| 214 | +let socket = try await Socket(IPv6Protocol.tcp) |
| 215 | +let address = IPv6SocketAddress(address: .loopback, port: 8080) |
| 216 | +try await socket.connect(to: address) |
| 217 | + |
| 218 | +// IPv6 address |
| 219 | +let linkLocal = IPv6SocketAddress( |
| 220 | + address: .init("fe80::1"), |
| 221 | + port: 8080 |
| 222 | +) |
| 223 | +``` |
| 224 | + |
| 225 | +## Advanced Usage |
| 226 | + |
| 227 | +### Non-blocking Accept with Timeout |
| 228 | + |
| 229 | +```swift |
| 230 | +import Socket |
| 231 | + |
| 232 | +let server = try await Socket(IPv4Protocol.tcp, bind: address) |
| 233 | +try await server.listen() |
| 234 | + |
| 235 | +// Accept with timeout using tasks |
| 236 | +let acceptTask = Task { |
| 237 | + try await server.accept() |
| 238 | +} |
| 239 | + |
| 240 | +let timeoutTask = Task { |
| 241 | + try await Task.sleep(nanoseconds: 5_000_000_000) // 5 seconds |
| 242 | + acceptTask.cancel() |
| 243 | +} |
| 244 | + |
| 245 | +do { |
| 246 | + let client = try await acceptTask.value |
| 247 | + timeoutTask.cancel() |
| 248 | + // Handle client... |
| 249 | +} catch { |
| 250 | + print("Accept timed out or failed") |
| 251 | +} |
| 252 | +``` |
| 253 | + |
| 254 | +### Broadcasting with UDP |
| 255 | + |
| 256 | +```swift |
| 257 | +import Socket |
| 258 | + |
| 259 | +let socket = try await Socket(IPv4Protocol.udp) |
| 260 | +try socket.setOption(.broadcast, true) |
| 261 | + |
| 262 | +// Broadcast to all hosts on the local network |
| 263 | +let broadcastAddress = IPv4SocketAddress( |
| 264 | + address: .broadcast, |
| 265 | + port: 9999 |
| 266 | +) |
| 267 | +try await socket.sendMessage(data, to: broadcastAddress) |
| 268 | +``` |
| 269 | + |
| 270 | +### Raw Sockets (Requires Privileges) |
| 271 | + |
| 272 | +```swift |
| 273 | +import Socket |
| 274 | + |
| 275 | +// Create raw socket (ICMP) |
| 276 | +let socket = try await Socket(IPv4Protocol.raw) |
| 277 | + |
| 278 | +// Send ICMP packet |
| 279 | +let icmpPacket = createICMPPacket() // Your ICMP packet data |
| 280 | +try await socket.write(icmpPacket) |
| 281 | +``` |
| 282 | + |
| 283 | +## Error Handling |
| 284 | + |
| 285 | +Socket operations throw `SocketError` which conforms to Swift's `Error` protocol: |
| 286 | + |
| 287 | +```swift |
| 288 | +import Socket |
| 289 | + |
| 290 | +do { |
| 291 | + let socket = try await Socket(IPv4Protocol.tcp) |
| 292 | + try await socket.connect(to: address) |
| 293 | +} catch let error as SocketError { |
| 294 | + switch error.code { |
| 295 | + case .connectionRefused: |
| 296 | + print("Connection refused by server") |
| 297 | + case .networkUnreachable: |
| 298 | + print("Network is unreachable") |
| 299 | + case .timeout: |
| 300 | + print("Operation timed out") |
| 301 | + default: |
| 302 | + print("Socket error: \(error)") |
| 303 | + } |
| 304 | +} catch { |
| 305 | + print("Unexpected error: \(error)") |
| 306 | +} |
| 307 | +``` |
| 308 | + |
| 309 | +## Requirements |
| 310 | + |
| 311 | +- Swift 5.7+ |
| 312 | +- macOS 10.15+, iOS 13+, tvOS 13+, watchOS 6+, or Linux |
| 313 | + |
| 314 | +## Contributing |
| 315 | + |
| 316 | +Contributions are welcome! Please feel free to submit a Pull Request. |
| 317 | + |
| 318 | +## License |
| 319 | + |
| 320 | +This project is licensed under the MIT License - see the LICENSE file for details. |
0 commit comments