- From: Peter Thatcher <pthatcher@google.com>
- Date: Thu, 13 Jul 2017 22:36:59 +0000
- To: Lennart Grahl <lennart.grahl@gmail.com>
- Cc: public-ortc@w3.org
- Message-ID: <CAJrXDUGuTR2iFUT9Lt5RvUeORMQ=Knwsm5EjBiX+phzr7_TAdQ@mail.gmail.com>
I think a low-level SCTP primitive would make sense, but SCTP and QUIC are different enough that low-level primitive for SCTP would be very different than a QuicStream. I think it would look something like this: interface SctpAssociation { void send(Uint8Array data, SctpSendParameters parameters); attribute EventHandler onrecv; // SctpReceivedMessage; } dictionary SctpSendParameters { unsigned short sid; unsigned long ppid; boolean unordered; int max_rtx_count; int max_rtx_ms; } dictionary SctpReceivedMessage { unsigned short sid; unsigned long ppid; unsigned short seqnum; unsigned long timestamp; Uint8Array data; } Everything else (data channels, streams, etc) could be built from there. I don't think it's a problem if QUIC and SCTP have different low-level primitives. On Thu, Jul 13, 2017 at 6:02 AM Lennart Grahl <lennart.grahl@gmail.com> wrote: > I really like the concept as I believe that we need a streaming API for > arbitrary data transfer in the WebRTC environment. And I agree that an > optional low-level approach is better than confronting application > developers with low-level buffering techniques when they just don't need > it (this is where the existing data channel API makes sense). > > However, the concept itself is applicable to the SCTP transport as well. > So, personally, I believe it would make sense if we would have a unified > DataStream (renamed from your QuicStream proposal) that can work on top > of either the SctpTransport or the QuicTransport, so we do not have > separate APIs for SCTP and QUIC. The DataChannel would then work on top > of the DataStream. > > Yes, for such an API, we will have to look at technical differences > between SCTP and QUIC. If these are too large for a direct DataStream > interface, the QuicStream and the SctpStream interfaces could be just > another layer in between. But I believe a well-tailored API should be > possible as there are APIs such as the NEAT project's API > (https://neat.readthedocs.io/en/latest/tutorial.html) which unify > transport protocols with even larger differences (granted, they don't > need the concept of streams but both QUIC and SCTP seem to have it). > > Maybe we should even consider creating WHATWG streams in an extended > form (that support the concept of unreliable and unordered data > transfer) directly, so the concept and logic of data streams, buffering, > back-pressure, etc., can be applied to other transport protocols in the > future and they don't need to reinvent the wheel. > > I'd be happy to participate in creating such an API. > > Cheers, > Lennart > > > On 13.07.2017 13:42, Peter Thatcher wrote: > > There has been a lot of interest in adding QUIC-based data channels to > > ORTC, and it seems like a natural fit to layer some new QuicTransport on > > top of an IceTransport to produce data channels the same way we do form > an > > SctpTransport layered on top of a DtlsTransport on top of an > IceTransport. > > > > But as I have spent time thinking about it and designing it, I realized > > that there is a more fundamental primitive that we can expose, slightly > > lower-level than data channels. And if we give that primitive an API, it > > would not only be possible to implement data channels on top, it would be > > possible to build many other things on top in a way that would be more > > simple, powerful, and flexible at the same time. > > > > That powerful fundamental primitive is the QUIC stream. > > > > Here's as simplified version of the API I propose (I slightly more full > one > > a little later), centered around QUIC streams: > > > > [Constructor(IceTransport)] > > partial interface QuicTransport { > > QuicStream createStream(); > > attribute EventHandler onstream; // QuicStream from remote side > > }; > > > > partial interface QuicStream { > > // If there is space in the local buffer, write to the buffer. > > // Otherwise, fail. > > void write(Uint8Array data); > > > > // Will maybe write one last value before finishing > > // and then a "fin" bit. > > void finish(optional Uint8Array data); > > > > // The remote side has acked everything, including the "fin" bit > > // Which means it is safe to close the stream. > > readonly attribute EventHandler onacked; > > > > // Reads all that is in the buffer. > > Uint8Array read(); > > > > // Fills the array exactly from the read buffer. > > // If that amount is not available, it fails. > > void readInto(Uint8Array data); > > > > // Closing on either side trigger on close on both sides. > > // Once closed, all future reads and writes will fail. > > // Nothing will be sent or received any longer. > > // Everything buffered will be thrown away. > > // Closing an already closed stream does nothing. > > void close(); > > attribute EventHandler onclosed; > > } > > > > > > Basically, you can write, read, and close. You can also "finish", know > > when the remote side has "finished" and know when the remote side has > acked > > everything. > > > >>From here, as I'll show with some examples, you can send and receive > small > > message out of order or with time-bounded reliability. You can also send > > and receive large messages with back pressure. You can build data > > channels. Or you can even build WHATWG streams. > > > > But there's just one last piece to the puzzle to explain: buffering. In > > order for back pressure to work properly and easily we need a way of > > causing the app to wait until there is room in a limited-size buffer to > > send and receive. In the examples, you'll see how this is used, and > then > > I'll show the methods to accomplish this. > > > > Example of sending small unreliable/unordered messages with a short > timeout: > > let ice = ...; > > let quic = new QuicTransport(ice); > > let maxMessageSize = 4096; > > let timeout = 5000; > > > > // Write outgoing message > > let data = ...; > > let qstream = quic.createStream(); > > await qstream.waitForWritable(data.byteLength); > > qstream.finish(data); > > setTimeout(() => qstream.close(), timeout); > > > > // Read incoming messages. > > quic.onstream = qstream => { > > await qstream.waitForReadableOrFinished(maxMessageSize); > > let data = qstream.read(); > > // Do something with data. > > }; > > > > > > Example of sending a large message in chunks, with back pressure > > let ice = ...; > > let quic = new QuicTransport(ice); > > > > // Write out in chunks > > let qstream = quic.createStream(); > > let chunks = ...; > > for chunk in chunks { > > await qstream.waitForWritable(chunk.byteLength); > > qstream.write(chunk); > > } > > > > // Read in the chunks > > quic.onstream = qstream => { > > await qstream.waitForReadableOrFinished(); > > while (!qstream.finished) { > > let data = qstream.read(); > > // .... Use the data somewhere > > await qstream.waitForReadableOrFinished(); > > } > > } > > > > > > > > Building reliable, ordered DataChannels on top of streams: > > > > A reliable, ordered DataChannel can be implemented on top of a single > > QuicStream with a simple framing mechanism to insert messages into the > > stream. For example, a framing mechanism like the following could be > used: > > > > DataChannel.send writes the following to the stream: > > - 2 bits to indicate the type (0x01 == string; 0x10 == binary) > > - 2 bits for the number of additional bytes (N) for the length of the > > message payload (0x00 = 0, 0x01 = 1, 0x10 = 2, 0x11 = 4) > > - 4 + (8 * N) bits for the length of the message (M) > > - M bytes for the message > > > > > > Building unreliable or unordered DataChannels: > > > > To send unordered or unreliable messages over QUIC, multiple QUIC > streams > > must be used, perhaps one for each message. A DataChannel could be built > > which creates a new QUIC stream for each message which writes a data > > channel ID as metadata first and then a message write after. However, an > > application will typically simply skip this level of abstraction and just > > use QuicStreams directly for sending and receiving unreliable or > unordered > > messages, since it is more simple that way. > > > > > > WHATWG streams: > > > > WHATWG streams wrap an "underlying source" to create ReadableStreams and > > WritableStreams which provide some convenient functionality. We can > create > > "underlying sources" from QuicStreams with something like this: > > > > var qstream = quic.createStream(); > > new ReadableStream( > > { > > type: "bytes", > > start: function(controller) { > > qstream.onclose = () => controller.close(); > > }, > > pull: async function(controller) { > > if (controller.byobRequest) { > > var data = controller.byobRequest.view; > > await qstream.waitForReadableOrFinished(data.byteLength); > > if (qstream.finished) { > > let finishedData = qstream.read() > > copyArray(finishedData, data); > > controller.byobRequest.respond(finishedData.byteLength); > > } else { > > qstream.readInto(data); > > controller.byobRequest.respond(data.byteLength); > > } > > } else { > > await qstream.waitForReadableOrFinished(); > > controller.enqueue(qstream.read()); > > } > > }, > > cancel: function() { > > qstream.close(); > > } > > }, > > new ByteLengthQueuingStrategy({ highWaterMark: 4096 }) > > ); > > > > new WritableStream( > > { > > type: "bytes", > > start: function (controller) { > > qstream.onclose = () => controller.close(); > > }, > > write: async function(chunk, controller) { > > await qstream.waitForWritableAmount(chunk.byteLength); > > qstream.write(chunk); > > } > > close: async function() { > > qstream.finish(); > > await waitForEvent(qstream.onacked); > > qstream.close(); > > } > > abort: function(reason) { > > qstream.close(); > > } > > }, > > new ByteLengthQueuingStrategy({ highWaterMark: 4096 }) > > ); > > > > > > Finally, here are the two buffering methods I mentioned that are need to > > make the back pressure and buffer work correctly: > > > > partial interface QuicStream { > > // Resolves when the amount can be written or buffered, > > // Up to the maxBufferedAmount (== amount if not given) > > Promise waitForWritable(unsigned long amount, > > optional unsigned long > > maxBufferedAmount); > > > > // Resolves when the amount can be written or buffered, > > // Up to the maxBufferedAmount (== amount if not given) > > // Will also resolved if the stream finished. > > Promise waitForReadableOrFinished( > > unsigned long amount, optional unsigned long maxBufferedAmount); > > } > > > > > > I think this approach of QUIC streams with data channels and other > > abstractions built on top will be flexible, powerful, and about as simple > > as it can get. > > >
Received on Thursday, 13 July 2017 22:37:39 UTC