Skip to content

Commit 4737651

Browse files
authored
Merge pull request #17 from zamderax/feat/tcpnodelay
Add TCP_NODELAY socket option support
2 parents 5683c03 + 9366ce9 commit 4737651

File tree

4 files changed

+137
-0
lines changed

4 files changed

+137
-0
lines changed

Sources/Socket/System/Constants.swift

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -328,6 +328,9 @@ internal var _IPPROTO_TCP: CInt { numericCast(IPPROTO_TCP) }
328328
@_alwaysEmitIntoClient
329329
internal var _IPPROTO_UDP: CInt { numericCast(IPPROTO_UDP) }
330330

331+
@_alwaysEmitIntoClient
332+
internal var _TCP_NODELAY: CInt { TCP_NODELAY }
333+
331334
/// Maximum queue length specifiable by listen.
332335
@_alwaysEmitIntoClient
333336
internal var _SOMAXCONN: CInt { SOMAXCONN }

Sources/Socket/System/SocketOptionLevel.swift

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,9 @@ public extension SocketOptionLevel {
1919

2020
@_alwaysEmitIntoClient
2121
static var `default`: SocketOptionLevel { SocketOptionLevel(_SOL_SOCKET) }
22+
23+
@_alwaysEmitIntoClient
24+
static var tcp: SocketOptionLevel { SocketOptionLevel(_IPPROTO_TCP) }
2225
}
2326

2427
#if os(Linux)
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
/// TCP Socket Option ID
2+
@frozen
3+
public struct TCPSocketOption: RawRepresentable, Equatable, Hashable, SocketOptionID, Sendable {
4+
5+
@_alwaysEmitIntoClient
6+
public static var optionLevel: SocketOptionLevel { .tcp }
7+
8+
/// The raw socket option identifier.
9+
@_alwaysEmitIntoClient
10+
public let rawValue: CInt
11+
12+
/// Creates a TCP socket option from a raw option identifier.
13+
@_alwaysEmitIntoClient
14+
public init(rawValue: CInt) { self.rawValue = rawValue }
15+
16+
@_alwaysEmitIntoClient
17+
private init(_ raw: CInt) { self.init(rawValue: raw) }
18+
}
19+
20+
public extension TCPSocketOption {
21+
22+
/// TCP_NODELAY - Disable Nagle's algorithm.
23+
///
24+
/// When enabled, this option disables the Nagle algorithm which delays
25+
/// transmission of data to coalesce small packets. This can improve
26+
/// latency for interactive applications.
27+
@_alwaysEmitIntoClient
28+
static var noDelay: TCPSocketOption { TCPSocketOption(_TCP_NODELAY) }
29+
}
30+
31+
/// TCP Socket Options
32+
public extension TCPSocketOption {
33+
34+
/// Disable Nagle's algorithm (TCP_NODELAY).
35+
///
36+
/// When enabled, this option disables the Nagle algorithm which delays
37+
/// transmission of data to coalesce small packets. This can improve
38+
/// latency for interactive applications at the cost of potentially
39+
/// increased network traffic.
40+
@frozen
41+
struct NoDelay: BooleanSocketOption, Equatable, Hashable, ExpressibleByBooleanLiteral, Sendable {
42+
43+
@_alwaysEmitIntoClient
44+
public static var id: TCPSocketOption { .noDelay }
45+
46+
public var boolValue: Bool
47+
48+
@_alwaysEmitIntoClient
49+
public init(_ boolValue: Bool) {
50+
self.boolValue = boolValue
51+
}
52+
}
53+
}

Tests/SocketTests/SocketTests.swift

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -200,6 +200,84 @@ final class SocketTests: XCTestCase {
200200
}
201201
}
202202
#endif
203+
204+
func testTCPNoDelay() async throws {
205+
// Create a TCP socket
206+
let socket = try await Socket(IPv4Protocol.tcp)
207+
defer { Task { await socket.close() } }
208+
209+
// Test setting TCP_NODELAY option
210+
let nodelay = TCPSocketOption.NoDelay(true)
211+
try socket.setOption(nodelay)
212+
213+
// Test getting TCP_NODELAY option
214+
let retrievedOption = try socket[TCPSocketOption.NoDelay.self]
215+
XCTAssertEqual(retrievedOption.boolValue, true)
216+
217+
// Test disabling TCP_NODELAY
218+
let disableNodelay = TCPSocketOption.NoDelay(false)
219+
try socket.setOption(disableNodelay)
220+
221+
let retrievedDisabled = try socket[TCPSocketOption.NoDelay.self]
222+
XCTAssertEqual(retrievedDisabled.boolValue, false)
223+
224+
// Test with boolean literal
225+
let literalOption: TCPSocketOption.NoDelay = true
226+
try socket.setOption(literalOption)
227+
228+
let retrievedLiteral = try socket[TCPSocketOption.NoDelay.self]
229+
XCTAssertEqual(retrievedLiteral.boolValue, true)
230+
}
231+
232+
func testTCPNoDelayBehavior() async throws {
233+
// This test verifies that TCP_NODELAY option works by testing with a connected socket pair
234+
let port = UInt16.random(in: 8080 ..< .max)
235+
print("Testing TCP_NODELAY behavior on port \(port)")
236+
let address = IPv4SocketAddress(address: .any, port: port)
237+
238+
// Create server
239+
let server = try await Socket(IPv4Protocol.tcp, bind: address)
240+
defer { Task { await server.close() } }
241+
242+
// Start server listening
243+
try await server.listen()
244+
245+
// Connect client and verify TCP_NODELAY can be set
246+
let client = try await Socket(IPv4Protocol.tcp)
247+
defer { Task { await client.close() } }
248+
249+
// Test that we can set TCP_NODELAY before connecting
250+
try client.setOption(TCPSocketOption.NoDelay(true))
251+
let nodeDelayBeforeConnect = try client[TCPSocketOption.NoDelay.self]
252+
XCTAssertEqual(nodeDelayBeforeConnect.boolValue, true)
253+
254+
// Connect to server
255+
do { try await client.connect(to: address) }
256+
catch Errno.socketIsConnected { }
257+
258+
// Accept connection on server side
259+
let serverConnection = try await server.accept()
260+
defer { Task { await serverConnection.close() } }
261+
262+
// Verify TCP_NODELAY is still set after connection
263+
let nodeDelayAfterConnect = try client[TCPSocketOption.NoDelay.self]
264+
XCTAssertEqual(nodeDelayAfterConnect.boolValue, true)
265+
266+
// Test setting TCP_NODELAY on the server's accepted connection
267+
try serverConnection.setOption(TCPSocketOption.NoDelay(false))
268+
let serverNodeDelay = try serverConnection[TCPSocketOption.NoDelay.self]
269+
XCTAssertEqual(serverNodeDelay.boolValue, false)
270+
271+
// Demonstrate that small writes work with TCP_NODELAY
272+
// (The actual latency difference is hard to measure reliably on localhost)
273+
let testData = Data("Hello".utf8)
274+
try await client.write(testData)
275+
276+
let receivedData = try await serverConnection.read(testData.count)
277+
XCTAssertEqual(testData, receivedData)
278+
279+
print("Successfully tested TCP_NODELAY option setting and data transmission")
280+
}
203281
}
204282

205283
var isRunningInCI: Bool {

0 commit comments

Comments
 (0)