diff --git a/Sources/SwiftNetwork/Protocols/BaseProtocolLinkages.swift b/Sources/SwiftNetwork/Protocols/BaseProtocolLinkages.swift index 8e32812..bbddd08 100644 --- a/Sources/SwiftNetwork/Protocols/BaseProtocolLinkages.swift +++ b/Sources/SwiftNetwork/Protocols/BaseProtocolLinkages.swift @@ -433,6 +433,48 @@ public struct BaseOutboundDatagramLinkage: OutboundDatagramLinkage { } } + public func getDatagramToSend( + minimumDatagramSize: Int, + for instance: InstanceIdentifier, + in eventContext: inout NetworkContext.EventContext + ) throws(NetworkError) -> Frame? { + switch protocolType { + case .udp(let index): + return try storage!.udpInstances[index].getDatagramToSend( + minimumDatagramSize: minimumDatagramSize, + for: instance, + in: &eventContext + ) + case .ip(let index): + return try storage!.ipInstances[index].getDatagramToSend( + minimumDatagramSize: minimumDatagramSize, + for: instance, + in: &eventContext + ) + case .bridgeDatagram(let index): + return try storage!.bridgeDatagramInstances[index].getDatagramToSend( + minimumDatagramSize: minimumDatagramSize, + for: instance, + in: &eventContext + ) + #if NETWORK_RTKIT + case .netif(let box): + var protocolInstance = box.instance + return try protocolInstance.getDatagramToSend( + minimumDatagramSize: minimumDatagramSize, + for: instance, + in: &eventContext + ) + #endif + default: + return try getDatagramToSendFromBatch( + minimumDatagramSize: minimumDatagramSize, + for: instance, + in: &eventContext + ) + } + } + public func sendDatagrams( _ datagrams: consuming FrameArray, from instance: InstanceIdentifier, diff --git a/Sources/SwiftNetwork/Protocols/BottomProtocol.swift b/Sources/SwiftNetwork/Protocols/BottomProtocol.swift index e333ab0..e3f7ca0 100644 --- a/Sources/SwiftNetwork/Protocols/BottomProtocol.swift +++ b/Sources/SwiftNetwork/Protocols/BottomProtocol.swift @@ -234,6 +234,15 @@ where LinkageType: OutboundDatagramLinkage, LinkageType.PairedUpperLinkage == Up in eventContext: inout NetworkContext.EventContext ) throws(NetworkError) -> FrameArray? + /// Returns a single datagram frame the upper protocol can use to send. + /// + /// Protocols can implement this function to customize behavior; the default implementation + /// asks `getDatagramsToSend` for a batch of one. + mutating func getDatagramToSend( + minimumDatagramSize: Int, + in eventContext: inout NetworkContext.EventContext + ) throws(NetworkError) -> Frame? + /// Sends datagrams created by the upper protocol. /// /// Protocols can implement this function to customize behavior. @@ -413,6 +422,34 @@ extension BottomDatagramProtocol where Self: ~Copyable { in: &eventContext ) } + public mutating func getDatagramToSend( + minimumDatagramSize: Int, + in eventContext: inout NetworkContext.EventContext + ) throws(NetworkError) -> Frame? { + guard + var datagrams = try self.getDatagramsToSend( + maximumDatagramCount: 1, + minimumDatagramSize: minimumDatagramSize, + in: &eventContext + ) + else { + return nil + } + let datagram = datagrams.popFirst() + // A batch of one must hold at most one frame; anything left here would trap in + // `Frame.deinit` instead of reaching the driver. + precondition(datagrams.isEmpty) + return datagram + } + public mutating func getDatagramToSend( + minimumDatagramSize: Int, + for instance: InstanceIdentifier, + in eventContext: inout NetworkContext.EventContext + ) throws(NetworkError) -> Frame? { + do { try validate(upper: instance, #function) } catch { throw NetworkError.posix(EINVAL) } + guard isConnected(in: &eventContext) else { throw NetworkError.posix(ENOTCONN) } + return try self.getDatagramToSend(minimumDatagramSize: minimumDatagramSize, in: &eventContext) + } public mutating func sendDatagrams( _ datagrams: consuming FrameArray, from instance: InstanceIdentifier, diff --git a/Sources/SwiftNetwork/Protocols/BridgeProtocol.swift b/Sources/SwiftNetwork/Protocols/BridgeProtocol.swift index 98119e8..eb15832 100644 --- a/Sources/SwiftNetwork/Protocols/BridgeProtocol.swift +++ b/Sources/SwiftNetwork/Protocols/BridgeProtocol.swift @@ -269,15 +269,8 @@ public struct BridgeDatagramProtocol: NetworkProtocol { minimumDatagramSize: Int, in eventContext: inout NetworkContext.EventContext ) throws(NetworkError) -> FrameArray? { - if datagramDrops?.blockPacketGeneration ?? false { - if datagramDrops?.shouldDropPacket() ?? false { - log.datapath("blocking \(maximumDatagramCount) datagrams to port: \(self.remoteEndpoint!.port)") - self.async(in: &eventContext) { eventContext in - self.log.datapath("unblocking outbound data") - self.upper.deliverOutboundRoomAvailableEvent(from: self.identifier, in: &eventContext) - } - return nil - } + if blocksPacketGeneration(datagramCount: maximumDatagramCount, in: &eventContext) { + return nil } let frameSize = min(minimumDatagramSize, self.maximumOutputSize) @@ -289,6 +282,33 @@ public struct BridgeDatagramProtocol: NetworkProtocol { return frameArray } + public func getDatagramToSend( + minimumDatagramSize: Int, + in eventContext: inout NetworkContext.EventContext + ) throws(NetworkError) -> Frame? { + if blocksPacketGeneration(datagramCount: 1, in: &eventContext) { + return nil + } + return Frame(count: min(minimumDatagramSize, self.maximumOutputSize)) + } + + /// Whether the configured drops withhold the next `datagramCount` datagrams. When they do, the upper + /// protocol is told it has room again on the next turn, so that it retries. + private func blocksPacketGeneration( + datagramCount: Int, + in eventContext: inout NetworkContext.EventContext + ) -> Bool { + guard datagramDrops?.blockPacketGeneration ?? false, datagramDrops?.shouldDropPacket() ?? false else { + return false + } + log.datapath("blocking \(datagramCount) datagrams to port: \(self.remoteEndpoint!.port)") + self.async(in: &eventContext) { eventContext in + self.log.datapath("unblocking outbound data") + self.upper.deliverOutboundRoomAvailableEvent(from: self.identifier, in: &eventContext) + } + return true + } + public func sendDatagrams( _ datagrams: consuming FrameArray, in eventContext: inout NetworkContext.EventContext diff --git a/Sources/SwiftNetwork/Protocols/IPProtocol.swift b/Sources/SwiftNetwork/Protocols/IPProtocol.swift index 33499c1..3746402 100644 --- a/Sources/SwiftNetwork/Protocols/IPProtocol.swift +++ b/Sources/SwiftNetwork/Protocols/IPProtocol.swift @@ -986,9 +986,13 @@ public struct IPProtocol: NetworkProtocol { inboundFrames.add(frames: processedFrames) } + func prepareOutboundFrame(_ outboundFrame: inout Frame) { + _ = outboundFrame.claim(fromStart: IPv4Instance.headerLength) + } + func prepareOutboundFrames(_ outboundFrames: inout FrameArray) { outboundFrames.iterateMutableFrames { frame in - _ = frame.claim(fromStart: IPv4Instance.headerLength) + prepareOutboundFrame(&frame) return true } } @@ -1832,18 +1836,22 @@ public struct IPProtocol: NetworkProtocol { inboundFrames.add(frames: processedFrames) } + func prepareOutboundFrame(_ outboundFrame: inout Frame) { + if flags.useMinimumMTU { + var trailerClaim = 0 + let frameLength = outboundFrame.unclaimedLength + if frameLength > IPv6Instance.minimalMTU { + trailerClaim = frameLength - IPv6Instance.minimalMTU + } + _ = outboundFrame.claim(fromStart: IPv6Instance.headerLength, fromEnd: trailerClaim) + } else { + _ = outboundFrame.claim(fromStart: IPv6Instance.headerLength) + } + } + func prepareOutboundFrames(_ outboundFrames: inout FrameArray) { outboundFrames.iterateMutableFrames { frame in - if flags.useMinimumMTU { - var trailerClaim = 0 - let frameLength = frame.unclaimedLength - if frameLength > IPv6Instance.minimalMTU { - trailerClaim = frameLength - IPv6Instance.minimalMTU - } - _ = frame.claim(fromStart: IPv6Instance.headerLength, fromEnd: trailerClaim) - } else { - _ = frame.claim(fromStart: IPv6Instance.headerLength) - } + prepareOutboundFrame(&frame) return true } } @@ -2232,6 +2240,30 @@ public struct IPProtocol: NetworkProtocol { } } + func getDatagramToSend( + minimumDatagramSize: Int, + in eventContext: inout NetworkContext.EventContext + ) throws(NetworkError) -> Frame? { + switch self.instanceType { + case .ipv4(let instance): + let outboundFrame = try invokeGetDatagramToSend( + minimumDatagramSize: instance.incrementByHeaderLength(minimumDatagramSize), + in: &eventContext + ) + guard var outboundFrame else { return nil } + instance.prepareOutboundFrame(&outboundFrame) + return outboundFrame + case .ipv6(let instance): + let outboundFrame = try invokeGetDatagramToSend( + minimumDatagramSize: instance.incrementByHeaderLength(minimumDatagramSize), + in: &eventContext + ) + guard var outboundFrame else { return nil } + instance.prepareOutboundFrame(&outboundFrame) + return outboundFrame + } + } + mutating func sendDatagrams( _ datagrams: consuming FrameArray, in eventContext: inout NetworkContext.EventContext diff --git a/Sources/SwiftNetwork/Protocols/ManyToManyProtocol.swift b/Sources/SwiftNetwork/Protocols/ManyToManyProtocol.swift index 9971783..020002f 100644 --- a/Sources/SwiftNetwork/Protocols/ManyToManyProtocol.swift +++ b/Sources/SwiftNetwork/Protocols/ManyToManyProtocol.swift @@ -2189,6 +2189,20 @@ extension ManyToManyOutboundDatagramProtocol where Path: AutomaticLowerDatagramP ) } + /// Fetches a single datagram to send using an event context the caller already holds. + public func getDatagramToSend( + path pathID: MultiplexingPathIdentifier, + minimumDatagramSize: Int, + in eventContext: inout NetworkContext.EventContext + ) throws(NetworkError) -> Frame? { + guard let path = self.path(for: pathID) else { throw NetworkError.posix(EINVAL) } + return try path.lower.invokeGetDatagramToSend( + minimumDatagramSize: minimumDatagramSize, + for: path.identifier, + in: &eventContext + ) + } + public func enqueueOutboundDatagrams( path pathID: MultiplexingPathIdentifier, datagrams: consuming FrameArray diff --git a/Sources/SwiftNetwork/Protocols/OneToOneProtocol.swift b/Sources/SwiftNetwork/Protocols/OneToOneProtocol.swift index 05be225..bed6335 100644 --- a/Sources/SwiftNetwork/Protocols/OneToOneProtocol.swift +++ b/Sources/SwiftNetwork/Protocols/OneToOneProtocol.swift @@ -256,6 +256,15 @@ where UpperProtocol: InboundDatagramLinkage, LowerProtocol: OutboundDatagramLink in eventContext: inout NetworkContext.EventContext ) throws(NetworkError) -> FrameArray? + /// Returns a single datagram frame the upper protocol can use to send. + /// + /// Protocols can implement this function to customize behavior; the default implementation + /// asks `getDatagramsToSend` for a batch of one. + mutating func getDatagramToSend( + minimumDatagramSize: Int, + in eventContext: inout NetworkContext.EventContext + ) throws(NetworkError) -> Frame? + /// Sends datagrams created by the upper protocol. /// /// Protocols can implement this function to customize behavior. @@ -694,6 +703,18 @@ extension OneToOneProtocolHandler where Self: ~Copyable, LowerProtocol: Outbound ) } + @inline(always) + public func invokeGetDatagramToSend( + minimumDatagramSize: Int, + in eventContext: inout NetworkContext.EventContext + ) throws(NetworkError) -> Frame? { + try lower.invokeGetDatagramToSend( + minimumDatagramSize: minimumDatagramSize, + for: effectiveSelfInstance, + in: &eventContext + ) + } + public func invokeSendDatagrams( _ datagrams: consuming FrameArray, in eventContext: inout NetworkContext.EventContext @@ -727,6 +748,34 @@ extension OneToOneDatagramProtocol where Self: ~Copyable { in: &eventContext ) } + public mutating func getDatagramToSend( + minimumDatagramSize: Int, + in eventContext: inout NetworkContext.EventContext + ) throws(NetworkError) -> Frame? { + guard + var datagrams = try self.getDatagramsToSend( + maximumDatagramCount: 1, + minimumDatagramSize: minimumDatagramSize, + in: &eventContext + ) + else { + return nil + } + let datagram = datagrams.popFirst() + // A batch of one must hold at most one frame; anything left here would trap in + // `Frame.deinit` instead of reaching the driver. + precondition(datagrams.isEmpty) + return datagram + } + public mutating func getDatagramToSend( + minimumDatagramSize: Int, + for instance: InstanceIdentifier, + in eventContext: inout NetworkContext.EventContext + ) throws(NetworkError) -> Frame? { + do { try validate(upper: instance, #function) } catch { throw NetworkError.posix(EINVAL) } + guard passthroughEvents || isConnected(in: &eventContext) else { throw NetworkError.posix(ENOTCONN) } + return try self.getDatagramToSend(minimumDatagramSize: minimumDatagramSize, in: &eventContext) + } public mutating func sendDatagrams( _ datagrams: consuming FrameArray, from instance: InstanceIdentifier, diff --git a/Sources/SwiftNetwork/Protocols/ProtocolLinkage.swift b/Sources/SwiftNetwork/Protocols/ProtocolLinkage.swift index 8c8d0b4..6940e3c 100644 --- a/Sources/SwiftNetwork/Protocols/ProtocolLinkage.swift +++ b/Sources/SwiftNetwork/Protocols/ProtocolLinkage.swift @@ -407,6 +407,16 @@ public protocol OutboundDatagramLinkage: OutboundDataLinkage where PairedUpperLi in eventContext: inout NetworkContext.EventContext ) throws(NetworkError) -> FrameArray? + /// Returns a single datagram frame the upper protocol can use to send. + /// + /// Linkages whose protocols can hand back one frame without building a container should implement this + /// function; the default implementation asks `getDatagramsToSend` for a batch of one. + func getDatagramToSend( + minimumDatagramSize: Int, + for instance: InstanceIdentifier, + in eventContext: inout NetworkContext.EventContext + ) throws(NetworkError) -> Frame? + func sendDatagrams( _ datagrams: consuming FrameArray, from instance: InstanceIdentifier, @@ -445,6 +455,48 @@ extension OutboundDatagramLinkage { } } + public func getDatagramToSend( + minimumDatagramSize: Int, + for instance: InstanceIdentifier, + in eventContext: inout NetworkContext.EventContext + ) throws(NetworkError) -> Frame? { + try getDatagramToSendFromBatch(minimumDatagramSize: minimumDatagramSize, for: instance, in: &eventContext) + } + + /// Takes a single frame from `getDatagramsToSend`, for protocols with no way to hand one back directly. + func getDatagramToSendFromBatch( + minimumDatagramSize: Int, + for instance: InstanceIdentifier, + in eventContext: inout NetworkContext.EventContext + ) throws(NetworkError) -> Frame? { + guard + var datagrams = try getDatagramsToSend( + maximumDatagramCount: 1, + minimumDatagramSize: minimumDatagramSize, + for: instance, + in: &eventContext + ) + else { + return nil + } + let datagram = datagrams.popFirst() + // A batch of one must hold at most one frame; anything left here would trap in + // `Frame.deinit` instead of reaching the driver. + precondition(datagrams.isEmpty) + return datagram + } + + public func invokeGetDatagramToSend( + minimumDatagramSize: Int, + for instance: InstanceIdentifier, + in eventContext: inout NetworkContext.EventContext + ) throws(NetworkError) -> Frame? { + guard !identifier.isNone else { return nil } + return try identifier.handleCallFromUpperProtocol(in: &eventContext) { eventContext throws(NetworkError) in + try self.getDatagramToSend(minimumDatagramSize: minimumDatagramSize, for: instance, in: &eventContext) + } + } + public func invokeSendDatagrams( _ datagrams: consuming FrameArray, from instance: InstanceIdentifier, diff --git a/Sources/SwiftNetwork/Protocols/UDPProtocol.swift b/Sources/SwiftNetwork/Protocols/UDPProtocol.swift index 1ba00a6..cd8988a 100644 --- a/Sources/SwiftNetwork/Protocols/UDPProtocol.swift +++ b/Sources/SwiftNetwork/Protocols/UDPProtocol.swift @@ -424,6 +424,30 @@ public struct UDPProtocol: NetworkProtocol { return outputFrames } + mutating func getDatagramToSend( + minimumDatagramSize: Int, + in eventContext: inout NetworkContext.EventContext + ) throws(NetworkError) -> Frame? { + if self.flags.contains(.flowControlled) { + // Wait until UDP flow is allowed + self.flags.insert(.outputPending) + return nil + } + + guard + var outputFrame = try invokeGetDatagramToSend( + minimumDatagramSize: incrementByUDPHeaderLength(minimumDatagramSize), + in: &eventContext + ) + else { + return nil + } + // The lower protocol can hand back a frame too short for the header; the caller + // reads the remaining length and adapts, as it does for a batch. + _ = outputFrame.claim(fromStart: UDPProtocol.headerLength) + return outputFrame + } + mutating func sendDatagrams( _ datagrams: consuming FrameArray, in eventContext: inout NetworkContext.EventContext diff --git a/Sources/SwiftNetwork/QUIC/QUICConnection.swift b/Sources/SwiftNetwork/QUIC/QUICConnection.swift index 59a166b..35c7065 100644 --- a/Sources/SwiftNetwork/QUIC/QUICConnection.swift +++ b/Sources/SwiftNetwork/QUIC/QUICConnection.swift @@ -4136,13 +4136,11 @@ public final class QUICConnection: ManyToManyApplicationStreamProtocol, outFrame = batchFrame } else { guard - var outFrames = try? getDatagramsToSend( + let newFrame = try? getDatagramToSend( path: path.pathIdentifier, - maximumDatagramCount: 1, minimumDatagramSize: requestedFrameLength, in: &eventContext - ), - let newFrame = outFrames.popFirst() + ) else { // Note: handleOutboundRoomAvailableEvent will restart sending log.debug("QUIC path failed to get a frame")