A TCP socket networking library for iOS/macOS built on Network.framework, with an API modeled after GCDAsyncSocket. Optimized for consuming streaming data from Linux servers (LLM/AI streams in SSE format).
Available in two versions:
- 🟠 Swift version —
Sources/NWAsyncSocket/ - 🔵 Objective-C version —
ObjC/NWAsyncSocketObjC/(uses Network.framework's C API:nw_connection_t). Class is named GCDAsyncSocket for drop-in replacement of CocoaAsyncSocket.
- ✅ GCDAsyncSocket-compatible API — delegate-based, tag-based read/write
- ✅ Sticky-packet handling (粘包) — multiple messages packed in one TCP segment are correctly split
- ✅ Split-packet handling (拆包) — messages split across TCP segments are reassembled
- ✅ UTF-8 boundary detection — prevents multi-byte character corruption at segment boundaries
- ✅ SSE parser — built-in Server-Sent Events parser for LLM streaming (e.g. OpenAI, Claude)
- ✅ Read-request queue — ordered, non-blocking reads (
toLength,toDelimiter,available) - ✅ TLS support — optional TLS via
enableTLS() - ✅ Streaming text mode — UTF-8 safe string delivery via delegate
- iOS 13.0+ / macOS 10.15+ / tvOS 13.0+ / watchOS 6.0+
- Swift 5.9+ (for Swift version)
- Xcode 15+ (for Objective-C version)
Although Network.framework was introduced in iOS 12 (WWDC 2018), this library requires iOS 13+ for the following reasons:
| Aspect | iOS 12 | iOS 13+ |
|---|---|---|
| Network.framework availability | ✅ Available | ✅ Available |
| Stability & bug fixes | ❌ Known issues with NWConnection callbacks and memory leaks |
✅ Major fixes shipped |
| Continuous read loop reliability | NWConnection.receive() under high-frequency reads |
✅ Stable |
| Swift runtime | ❌ Must be embedded in app bundle | ✅ Built into the OS |
Key details:
- Network.framework maturity — Apple significantly improved
NWConnectionreliability in iOS 13, fixing known issues with callback delivery and memory management that existed in the iOS 12 initial release. - Continuous read loop stability — This library's core architecture uses a high-frequency continuous read loop (
receive()→ buffer → dequeue →receive()). This pattern triggers edge-case bugs on iOS 12 that were resolved in iOS 13. - Swift runtime built-in — Starting from iOS 13, the Swift runtime is bundled with the OS, which reduces app binary size and avoids runtime compatibility issues.
- Platform version alignment — iOS 13 / macOS 10.15 / tvOS 13 / watchOS 6 are all from the same 2019 release cycle, ensuring a consistent and well-tested foundation across all Apple platforms.
Note: If you absolutely need iOS 12 support, changing
.iOS(.v13)to.iOS(.v12)inPackage.swiftwill compile, but thorough testing on iOS 12 devices is strongly recommended — especially for long-lived connections and high-frequency read/write scenarios.
// Package.swift
dependencies: [
.package(url: "https://github.com/dustturtle/NWAsyncSocket.git", from: "1.0.0")
]Copy the files from ObjC/NWAsyncSocketObjC/ into your Xcode project. Add the include/ directory to your Header Search Paths.
Drop-in replacement: The Objective-C class is named
GCDAsyncSocketwith aGCDAsyncSocketDelegateprotocol, so you can replace CocoaAsyncSocket's GCDAsyncSocket by swapping the imported header from"GCDAsyncSocket.h"(CocoaAsyncSocket) to"GCDAsyncSocket.h"(this library).
import NWAsyncSocket
class MyController: NWAsyncSocketDelegate {
let socket = NWAsyncSocket(delegate: self, delegateQueue: .main)
func connect() {
try? socket.connect(toHost: "api.example.com", onPort: 8080)
}
// MARK: - Delegate
func socket(_ sock: NWAsyncSocket, didConnectToHost host: String, port: UInt16) {
print("Connected to \(host):\(port)")
sock.readData(withTimeout: -1, tag: 0)
}
func socket(_ sock: NWAsyncSocket, didRead data: Data, withTag tag: Int) {
print("Received \(data.count) bytes")
sock.readData(withTimeout: -1, tag: 0)
}
func socket(_ sock: NWAsyncSocket, didWriteDataWithTag tag: Int) {
print("Write complete for tag \(tag)")
}
func socketDidDisconnect(_ sock: NWAsyncSocket, withError error: Error?) {
print("Disconnected: \(error?.localizedDescription ?? "clean")")
}
}let socket = NWAsyncSocket(delegate: self, delegateQueue: .main)
socket.enableSSEParsing()
try socket.connect(toHost: "llm-server.example.com", onPort: 8080)
// Delegate receives parsed SSE events automatically:
func socket(_ sock: NWAsyncSocket, didReceiveSSEEvent event: SSEEvent) {
print("Event: \(event.event), Data: \(event.data)")
}// Read any available data
socket.readData(withTimeout: 30, tag: 1)
// Read exactly 1024 bytes
socket.readData(toLength: 1024, withTimeout: 30, tag: 2)
// Read until delimiter (e.g. newline)
socket.readData(toData: "\r\n".data(using: .utf8)!, withTimeout: 30, tag: 3)#import "GCDAsyncSocket.h"
@interface MyController () <GCDAsyncSocketDelegate>
@property (nonatomic, strong) GCDAsyncSocket *socket;
@end
@implementation MyController
- (void)connect {
self.socket = [[GCDAsyncSocket alloc] initWithDelegate:self
delegateQueue:dispatch_get_main_queue()];
NSError *err = nil;
[self.socket connectToHost:@"api.example.com" onPort:8080 error:&err];
}
- (void)socket:(GCDAsyncSocket *)sock didConnectToHost:(NSString *)host port:(uint16_t)port {
NSLog(@"Connected to %@:%u", host, port);
[sock readDataWithTimeout:-1 tag:0];
}
- (void)socket:(GCDAsyncSocket *)sock didReadData:(NSData *)data withTag:(long)tag {
NSLog(@"Received %lu bytes", (unsigned long)data.length);
[sock readDataWithTimeout:-1 tag:0];
}
- (void)socket:(GCDAsyncSocket *)sock didWriteDataWithTag:(long)tag {
NSLog(@"Write complete for tag %ld", tag);
}
- (void)socketDidDisconnect:(GCDAsyncSocket *)sock withError:(NSError *)error {
NSLog(@"Disconnected: %@", error.localizedDescription);
}
@endGCDAsyncSocket *socket = [[GCDAsyncSocket alloc] initWithDelegate:self
delegateQueue:dispatch_get_main_queue()];
[socket enableSSEParsing];
[socket connectToHost:@"llm-server.example.com" onPort:8080 error:nil];
// Optional delegate method:
- (void)socket:(GCDAsyncSocket *)sock didReceiveSSEEvent:(NWSSEEvent *)event {
NSLog(@"Event: %@ Data: %@", event.event, event.data);
}┌─────────────────────────────────────────────────┐
│ Your App │
│ (ViewController) │
├─────────────────────────────────────────────────┤
│ NWAsyncSocket / GCDAsyncSocket │
│ ┌──────────────┐ ┌────────────┐ ┌──────────┐│
│ │ Read Queue │ │ Buffer │ │SSE Parser││
│ │ (ReadRequest) │ │(StreamBuf) │ │ ││
│ └──────┬───────┘ └──────┬─────┘ └────┬─────┘│
│ │ │ │ │
│ ┌──────▼─────────────────▼──────────────▼─────┐│
│ │ Continuous Read Loop ││
│ │ (reads → buffer → dequeue → delegate) ││
│ └──────────────────┬──────────────────────────┘│
├─────────────────────┼───────────────────────────┤
│ NWConnection / nw_connection_t │
│ (Network.framework) │
├─────────────────────┼───────────────────────────┤
│ TCP/IP │
│ (Linux Server) │
└─────────────────────────────────────────────────┘
NWAsyncSocket/
├── Package.swift # SPM configuration
├── README.md
├── Sources/NWAsyncSocket/ # Swift version
│ ├── NWAsyncSocket.swift # Main socket class (NWConnection)
│ ├── NWAsyncSocketDelegate.swift # Delegate protocol
│ ├── StreamBuffer.swift # Byte buffer with UTF-8 safety
│ ├── SSEParser.swift # SSE event parser
│ └── ReadRequest.swift # Read request queue model
├── Examples/SwiftDemo/ # Swift interactive demo
│ └── main.swift # Run: swift run SwiftDemo
├── ObjC/NWAsyncSocketObjC/ # Objective-C version
│ ├── include/ # Public headers
│ │ ├── GCDAsyncSocket.h # Main class (drop-in replacement)
│ │ ├── GCDAsyncSocketDelegate.h # Delegate protocol
│ │ ├── NWStreamBuffer.h
│ │ ├── NWSSEParser.h
│ │ └── NWReadRequest.h
│ ├── GCDAsyncSocket.m # Main socket (nw_connection_t C API)
│ ├── NWStreamBuffer.m
│ ├── NWSSEParser.m
│ └── NWReadRequest.m
├── ObjC/ObjCDemo/ # Objective-C interactive demo
│ └── main.m # Build with clang (see Demo section)
├── ObjC/NWAsyncSocketObjCTests/ # ObjC XCTest cases
│ ├── NWStreamBufferTests.m
│ ├── NWSSEParserTests.m
│ └── NWReadRequestTests.m
└── Tests/NWAsyncSocketTests/ # Swift XCTest cases (71 tests)
├── StreamBufferTests.swift
├── SSEParserTests.swift
└── ReadRequestTests.swift
swift test71 tests covering:
- StreamBuffer: basic ops, read-to-length, read-to-delimiter, sticky/split packets, UTF-8 safety
- SSEParser: single/multi events, CRLF/CR/LF, split chunks, LLM simulation, comments, edge cases
- ReadRequest: all request types
Add the ObjC source and test files to an Xcode project and run the XCTest test suite.
Interactive demos are provided for both Swift and Objective-C to help you verify all core components.
Run the interactive Swift demo via SPM:
swift run SwiftDemoThe demo menu lets you test each component individually or run all at once:
- StreamBuffer — sticky-packet / split-packet handling, delimiter-based reads
- SSEParser — single/multi/split SSE events, LLM streaming simulation, ID/retry fields
- UTF-8 Safety — multi-byte character boundary detection, incomplete sequence handling
- ReadRequest — all read-request queue types with simulated queue processing
- NWAsyncSocket — connection setup and delegate usage pattern (Network.framework only)
Build the ObjC demo on macOS:
clang -framework Foundation \
-I ObjC/NWAsyncSocketObjC/include \
ObjC/NWAsyncSocketObjC/NWStreamBuffer.m \
ObjC/NWAsyncSocketObjC/NWSSEParser.m \
ObjC/NWAsyncSocketObjC/NWReadRequest.m \
ObjC/NWAsyncSocketObjC/GCDAsyncSocket.m \
ObjC/ObjCDemo/main.m \
-o ObjCDemo
./ObjCDemoThe ObjC demo provides the same interactive menu and covers:
- NWStreamBuffer — sticky-packet / split-packet handling, delimiter-based reads
- NWSSEParser — single/multi/split SSE events, LLM streaming simulation, ID/retry fields
- UTF-8 Safety — multi-byte boundary detection with
utf8SafeByteCountForData: - NWReadRequest — all read-request queue types with simulated queue processing
- GCDAsyncSocket — connection setup, delegate implementation, and usage pattern
| GCDAsyncSocket (CocoaAsyncSocket) | NWAsyncSocket (Swift) | GCDAsyncSocket (this library) |
|---|---|---|
initWithDelegate:delegateQueue: |
init(delegate:delegateQueue:) |
initWithDelegate:delegateQueue: |
connectToHost:onPort:error: |
connect(toHost:onPort:) |
connectToHost:onPort:error: |
readDataWithTimeout:tag: |
readData(withTimeout:tag:) |
readDataWithTimeout:tag: |
readDataToLength:withTimeout:tag: |
readData(toLength:withTimeout:tag:) |
readDataToLength:withTimeout:tag: |
readDataToData:withTimeout:tag: |
readData(toData:withTimeout:tag:) |
readDataToData:withTimeout:tag: |
writeData:withTimeout:tag: |
write(_:withTimeout:tag:) |
writeData:withTimeout:tag: |
disconnect |
disconnect() |
disconnect |
isConnected |
isConnected |
isConnected |
MIT