Sources/NIOPosix/Docs.docc/GSO-GRO-Linux.md
Use Generic Segmentation Offload (GSO) and Generic Receive Offload (GRO) on a per-message basis for fine-grained control over UDP datagram segmentation and aggregation.
Generic Segmentation Offload (GSO) and Generic Receive Offload (GRO) are Linux kernel features that enable efficient handling of UDP datagrams by offloading segmentation and aggregation work to the kernel or network interface card (NIC).
SwiftNIO provides per-message APIs for both features, allowing dynamic control over segmentation and aggregation on a datagram-by-datagram basis. This offers more flexibility than channel-wide configuration, which requires static segment sizes known ahead of time.
GSO allows you to send a single large buffer (a "superbuffer") that the kernel automatically splits into multiple UDP datagrams of a specified segment size. Instead of your application creating many small datagrams, you write one superbuffer and let the kernel handle the segmentation efficiently.
Benefits:
GRO is the reverse of GSO: the kernel aggregates multiple received UDP datagrams into a single larger buffer (again, a "superbuffer") before delivering it to your application. When enabled with per-message metadata, you receive information about the original segment size used for aggregation.
Benefits:
The per-message GSO API allows you to specify segmentation parameters for each datagram write, rather than configuring a static segment size for the entire channel.
To use per-message GSO, set the segmentSize field in AddressedEnvelope.Metadata when writing datagrams:
import NIOCore
import NIOPosix
// Create a large buffer to send (10 segments of 1000 bytes each)
let segmentSize = 1000
let segmentCount = 10
var largeBuffer = channel.allocator.buffer(capacity: segmentSize * segmentCount)
largeBuffer.writeRepeatingByte(1, count: segmentSize * segmentCount)
// Write with per-message GSO metadata
let envelope = AddressedEnvelope(
remoteAddress: destinationAddress,
data: largeBuffer,
metadata: .init(
ecnState: .transportNotCapable,
packetInfo: nil,
segmentSize: segmentSize // Enable GSO with 1000-byte segments
)
)
try await channel.writeAndFlush(envelope)
The kernel will automatically split largeBuffer into 10 separate UDP datagrams of 1000 bytes each.
You can freely mix writes with and without per-message GSO on the same channel:
// Write with GSO
let gsoEnvelope = AddressedEnvelope(
remoteAddress: destinationAddress,
data: largeBuffer,
metadata: .init(ecnState: .transportNotCapable, packetInfo: nil, segmentSize: 1000)
)
// Write without GSO (normal datagram)
let normalEnvelope = AddressedEnvelope(
remoteAddress: destinationAddress,
data: smallBuffer
)
let write1 = channel.write(gsoEnvelope)
let write2 = channel.write(normalEnvelope)
channel.flush()
The per-message GRO API provides segment size information for each received aggregated datagram through the same AddressedEnvelope.Metadata.segmentSize field used for GSO.
To enable per-message GRO segment size reporting, you must:
ChannelOptions.datagramReceiveOffloadChannelOptions.datagramReceiveSegmentSizeimport NIOCore
import NIOPosix
// Enable GRO on the channel
try await channel.setOption(.datagramReceiveOffload, value: true)
// Enable per-message segment size reporting
try await channel.setOption(.datagramReceiveSegmentSize, value: true)
// Configure a larger receive buffer to accommodate aggregated datagrams
let largeBufferAllocator = FixedSizeRecvByteBufferAllocator(capacity: 65536)
try await channel.setOption(.recvAllocator, value: largeBufferAllocator)
When you receive an aggregated datagram, the segmentSize field in the metadata contains the original segment size:
// In your channel handler
func channelRead(context: ChannelHandlerContext, data: NIOAny) {
let envelope = self.unwrapInboundIn(data)
// Check if this is an aggregated datagram
if let segmentSize = envelope.metadata?.segmentSize {
print("Received aggregated datagram:")
print(" Total size: \(envelope.data.readableBytes) bytes")
print(" Original segment size: \(segmentSize) bytes")
print(" Approximate segment count: \(envelope.data.readableBytes / segmentSize)")
} else {
print("Received normal datagram: \(envelope.data.readableBytes) bytes")
}
}
When using GRO, ensure your receive buffer allocator provides buffers large enough to hold aggregated datagrams. The default datagram channel allocator uses 2048-byte fixed buffers, which may be too small:
// Instead of the default 2048-byte buffers, use larger buffers
let allocator = FixedSizeRecvByteBufferAllocator(capacity: 65536) // 64KB buffers
try await channel.setOption(.recvAllocator, value: allocator)
If the receive buffer is too small, the kernel will not be able to aggregate as many datagrams, reducing the effectiveness of GRO.
Per-message GSO and GRO are only supported on Linux. Attempting to use these features on other platforms will result in errors:
segmentSize set will fail the write promise with ChannelError.operationUnsupportedChannelOptions.datagramReceiveSegmentSize will fail with ChannelError.operationUnsupportedCheck for GSO and GRO support at runtime using the System APIs:
import NIOPosix
if System.supportsUDPSegmentationOffload {
print("GSO is supported on this platform")
// Use per-message GSO
} else {
print("GSO is not supported, falling back to normal writes")
}
if System.supportsUDPReceiveOffload {
print("GRO is supported on this platform")
// Enable per-message GRO
} else {
print("GRO is not supported")
}
Generally speaking, error handling for GSO and GRO is very similar to the error handling without them. An important note is that a single promise cannot handle individualised errors for the datagrams within the superbuffer. The kernel delivers only one return code for a given send or receive, which affects multiple datagrams within the superbuffer. However, it may not affect all datagrams within the superbuffer, as the writes can be split. The result is that if the final superbuffer write completes successfully the promise will be succeeded, even if errors occurred earlier.
In the event that you do not follow the steps above and attempt to use GSO on platforms that do not support it, the promise will fail with .operationUnsupported and no writes will be attempted.