Thanks for your reply. The server receives nothing, none of the protocol has its handleInput() method called. Here are the logs:
# Client
Protocol1 start()
Protocol2 start()
sending 2990 bytes
Protocol2 handleOutput()
# Server
Protocol1 start()
Protocol2 start()
Protocol1 is placed at index 1, Protocol2 is on top at index 0. Instead I would expect this to happen:
# Client
Protocol1 start()
Protocol2 start()
sending 2990 bytes
Protocol2 handleOutput()
Protocol1 handleOutput()
# Server
Protocol1 start()
Protocol2 start()
Protocol1 handleInput()
Protocol2 handleInput()
received 2990 bytes
I insert the protocols on the NWListener (server-side) and the NWConnection (client-side) like so:
let protocol1 = NWProtocolFramer.Options(definition: Protocol1.definition)
let protocol2 = NWProtocolFramer.Options(definition: Protocol2.definition)
let parameters: NWParameters = .tcp
parameters.defaultProtocolStack.applicationProtocols.insert(protocol1, at: 0)
parameters.defaultProtocolStack.applicationProtocols.insert(protocol2, at: 0)
Post
Replies
Boosts
Views
Activity
If that helps, my handleOuptut() method looks like this:
func handleOutput(framer: NWProtocolFramer.Instance, message: NWProtocolFramer.Message, messageLength: Int, isComplete: Bool) {
print("Protocol2 handleOutput()")
var length = Int32(messageLength)
let data = Data(bytes: &length, count: MemoryLayout<Int32>.size)
framer.writeOutput(data: data)
try? framer.writeOutputNoCopy(length: messageLength)
}
If we could pass some context into NWProtocolFramer.Options and access it from the protocol framer, that would also solve my problem. Actually it wouldn't solve my problem, for the listener the options are inserted into NWListener and not NWConnection. Rather I need access to the connection itself from my protocol framer, or to storage/context associated with the connection.
How do people solve that problem? It seems to me this is a pretty basic need but it is seemingly impossible to do, am I missing something here?
Or is there some buffer size limit above which the framer would stop receiving more data? It's as-if it never receives the end of the data even though the other side has sent it. My code can't parse the needed amount and handleInput() stops being called.
Further testing indicates the latter: data is not dropped but the framer fails to receive more data, and even though handleInput() is called again, the buffer does not contain more to parse. Consuming the available (partial) data seems to unblock the situation as the framer is then able to receive the missing piece of the data.
Is this the correct behavior? Am I trying to parse too much data at once? If so, what is the upper limit we can parse without possibly entering this deadlock situation?
I do have a length value in the packet header but I can't call deliverInputNoCopy because my protocol needs to look at all the data (doing custom encryption/decryption, not TLS on purpose). Therefore I am trying to parse the whole length read in the header.
I have seen nothing in the documentation or wwdc sessions about the framer.parseInput() API potentially starving depending on the minimum amount of data requested. Would you be so kind to at least give us the range of values we can expect to pass safely to the API?
Thank you for this reply. It doesn't seem like the TLS security options is what I need since I am implementing my own protocol which needs to pass its own custom options (unless I try to abuse these security options by inserting a pointer into some dispatch data object that could be stashed somewhere in there). But it does seem like what I want is NWParameters but for my protocol, alas the Network API does not seem to allow this.
Right, I wouldn't even dare to write a custom TLS implementation. I am implementing custom encryption that will only work with my application on both ends, it's not meant to interoperate with other things.
I thought I might be able to subclass NWParameters to pass custom parameters to my protocol but alas Swift prevents it: although the class is not marked final, it is also not marked open and therefore subclassing is prevented outside of the Network module.
It does reach the other side. What I'm saying is that I can send data onto the connection much more quickly than it actually goes out on the network, which causes it to enqueue in memory (a lot). I was expecting the connection to have a small buffer and only call the completion handler of the send() method when enough data had left the buffer and went onto the network.
In the current state of things, how would I know how fast the data is coming out of this internal buffer and onto the network? I would certainly need some sort of back pressure information in order to know whether it is ok to give more data to the connection. Or am I missing something?
This is not AV data. I have to admit this is quite of a major let down, very surprised back pressure isn't part of the Network framework when a bare socket does it. I have filed a bug report already (FB9587908). Thank you very much Matt for clarifying the situation and saving me a lot of time and trouble trying to figure this out.
Seems like I have found a solution which is to add .handlesExternalEvents(matching: []) to the WindowGroup.
Thank you Quinn. I did not test your sample code yet, the only difference I can think of for now is that in my case I never stopped receiving data like you're doing here. Could it be that the connection keeps calling the completion handler as long as some data is being sent (and received) and regardless of how fast the data is being received?
Ok Quinn. I made further testing and the problem seems related to me using a protocol framer. Give it a try and let me know what you think.
import Foundation
import Network
class MyProtocol: NWProtocolFramerImplementation {
// Create a global definition of your game protocol to add to connections.
static let definition = NWProtocolFramer.Definition(implementation: MyProtocol.self)
// Set a name for your protocol for use in debugging.
static var label: String { return "MyProtocol" }
// Set the default behavior for most framing protocol functions.
required init(framer: NWProtocolFramer.Instance) { }
func start(framer: NWProtocolFramer.Instance) -> NWProtocolFramer.StartResult {
return .ready
}
func wakeup(framer: NWProtocolFramer.Instance) { }
func stop(framer: NWProtocolFramer.Instance) -> Bool { return true }
func cleanup(framer: NWProtocolFramer.Instance) { }
// Whenever the application sends a message, add your protocol header and forward the bytes.
func handleOutput(framer: NWProtocolFramer.Instance, message: NWProtocolFramer.Message, messageLength: Int, isComplete: Bool) {
try? framer.writeOutputNoCopy(length: messageLength)
}
// Whenever new bytes are available to read, try to parse out your message format.
func handleInput(framer: NWProtocolFramer.Instance) -> Int {
let message = NWProtocolFramer.Message(definition: MyProtocol.definition)
_ = framer.deliverInputNoCopy(length: 1000, message: message, isComplete: true)
return 0
}
}
var listenerRef: NWListener? = nil
var receiveConnectionRef: NWConnection? = nil
func startListener() {
let options = NWProtocolFramer.Options(definition: MyProtocol.definition)
let parameters = NWParameters.tcp
parameters.defaultProtocolStack.applicationProtocols.insert(options, at: 0)
let listener = try! NWListener(using: parameters, on: 12345)
listenerRef = listener
listener.stateUpdateHandler = { state in
print("listener: state did change, new: \(state)")
}
listener.newConnectionHandler = { conn in
if let old = receiveConnectionRef {
print("listener: will cancel old connection")
old.cancel()
receiveConnectionRef = nil
}
receiveConnectionRef = conn
startReceive(on: conn)
conn.start(queue: .main)
}
listener.start(queue: .main)
}
func startReceive(on connection: NWConnection) {
connection.receiveMessage { dataQ, _, isComplete, errorQ in
if let data = dataQ {
print("receiver: did received, count: \(data.count)")
}
if let error = errorQ {
print("receiver: did fail, error: \(error)")
return
}
// if isComplete {
// print("receiver: is complete")
// return
// }
print("receiver: will not start new receive to force back pressure")
}
}
var sendConnectionRef: NWConnection? = nil
var totalSent = 0
func sendRandomData(to connection: NWConnection) {
var bytes = [UInt8](repeating: 0, count: 1000)
let err = SecRandomCopyBytes(kSecRandomDefault, bytes.count, &bytes)
assert(err == errSecSuccess)
let message = NWProtocolFramer.Message(definition: MyProtocol.definition)
let context = NWConnection.ContentContext(identifier: "Data", metadata: [message])
connection.send(content: Data(bytes), contentContext: context, completion: .contentProcessed({ errorQ in
if let error = errorQ {
print("sender: send failed, error: \(error)")
return
}
totalSent += bytes.count
print("sender: did send, total: \(totalSent)")
sendRandomData(to: connection)
}))
}
func startSender() {
let options = NWProtocolFramer.Options(definition: MyProtocol.definition)
let parameters = NWParameters.tcp
parameters.defaultProtocolStack.applicationProtocols.insert(options, at: 0)
let connection = NWConnection(host: "localhost", port: 12345, using: parameters)
sendConnectionRef = connection // Guarantees a long-lived referenced.
connection.stateUpdateHandler = { state in
print("sender: state did change, new: \(state)")
}
sendRandomData(to: connection)
connection.start(queue: .main)
}
func main() {
startListener()
startSender()
dispatchMain()
}
main()
exit(EXIT_SUCCESS)
Sure. Thank you Quinn!
Using a method like this to handle input is roughly the same as calling connection.receive(minimumIncompleteLength: 1, maximumLength: 1000), is there a reason you are not parsing out the length of your frame from the packet header?
Hi Matt, in my app I do parse the length from the packet header. This is a reduced sample code, the simplest possible that demonstrates the problem. I added a protocol framer that basically does nothing (just passing the data along), and it changed the behavior of the writer, which seems wrong to me.
About your code, you need to remove these three lines:
if error == nil {
startReceive(on: connection)
}
Because the whole point is to see what happens when the other side does not read (or rather does not read fast enough). If you remove these three lines, you will see that the sender keeps enqueuing indefinitely into memory explosion.