/
githubmirror
/
node
Обзор
Документация
Войти
/
githubmirror
/
node
Код
Запросы
0
Пакеты
0
Релизы
0
Аналитика
Безопасность
main
src/quic/streams.h
518 строк
20 KB
Tim Perry
quic: fix stop sending behaviour & callback
04 авг 2026, 12:07
Не верифицирован
04 авг 2026, 12:07
13e4e3d
Код
Авторство
О чём код?
#pragma once #if defined(NODE_WANT_INTERNALS) && NODE_WANT_INTERNALS #include <aliased_struct.h> #include <async_wrap.h> #include <base_object.h> #include <dataqueue/queue.h> #include <env.h> #include <memory_tracker.h> #include <node_blob.h> #include <node_bob.h> #include <node_http_common.h> #include <util.h> #include "bindingdata.h" #include "data.h" #include <vector> namespace node::quic { class Session; class Stream; // An elastic ring buffer used by Stream to coalesce received data before // flushing it into the DataQueue. This avoids creating many small V8 // BackingStore allocations from per-QUIC-frame ngtcp2 callbacks. Data is // memcpy'd into the ring buffer and flushed as a single right-sized // BackingStore when the reader pulls or the buffer reaches its flush // threshold. The buffer starts at kMinCapacity and can grow up to a // configured maximum (typically the stream flow control window). class RecvAccumulator final { public: static constexpr size_t kMinCapacity = 64 * 1024; // 64 KB static constexpr size_t kFlushThreshold = kMinCapacity; // flush at 64 KB explicit RecvAccumulator(size_t max_capacity); ~RecvAccumulator() = default; DISALLOW_COPY_AND_MOVE(RecvAccumulator) // Append data. Returns the number of bytes written (may be less than // len if the buffer is full). The caller should flush and retry with // the remaining bytes. size_t Write(const uint8_t* data, size_t len); // Flush accumulated data as a single DataQueue entry backed by a // right-sized V8 BackingStore. Returns nullptr if nothing is // accumulated. Resets the read/write cursors and shrinks the buffer // back toward kMinCapacity if it was expanded. std::unique_ptr<DataQueue::Entry> Flush(Environment* env); // Current number of bytes awaiting flush. size_t available() const { return len_; } // Number of bytes that can still be written before the buffer is full. size_t remaining() const { return buf_.size() - len_; } // True if accumulated data has reached the flush threshold. bool should_flush() const { return len_ >= kFlushThreshold; } // Grow the buffer capacity (doubles, up to max_capacity_). void Grow(); private: std::vector<uint8_t> buf_; size_t max_capacity_; size_t read_pos_ = 0; size_t write_pos_ = 0; size_t len_ = 0; // write_pos_ - read_pos_, accounting for wrap }; using Ngtcp2Source = bob::SourceImpl<ngtcp2_vec>; // When a request to open a stream is made before a Session is able to actually // open a stream (either because the handshake is not yet sufficiently complete // or concurrency limits are temporarily reached) then the request to open the // stream is represented as a queued PendingStream. // // The PendingStream instance itself is owned by the stream created but a // reference sits in a linked list in the session. // // The PendingStream request can be canceled by dropping the PendingStream // instance before it can be fulfilled, at which point it is removed from the // pending stream queue. // // Note that only locally initiated streams can be created in a pending state. class PendingStream final { public: explicit PendingStream(Direction direction, Stream* stream, BaseObjectWeakPtr<Session> session); DISALLOW_COPY_AND_MOVE(PendingStream) ~PendingStream(); // Called when the stream has been opened. Transitions the stream from a // pending state to an opened state. void fulfill(stream_id id); // Called when opening the stream fails or is canceled. Transitions the // stream into a closed/destroyed state. void reject(QuicError error = QuicError()); inline Direction direction() const { return direction_; } private: Direction direction_; Stream* stream_; BaseObjectWeakPtr<Session> session_; bool waiting_ = true; ListNode<PendingStream> pending_stream_queue_; public: using PendingStreamQueue = ListHead<PendingStream, &PendingStream::pending_stream_queue_>; }; // QUIC Stream's are simple data flows that may be: // // * Bidirectional (both sides can send) or Unidirectional (one side can send) // * Server or Client Initiated // // The flow direction and origin of the stream are important in determining the // write and read state (Open or Closed). Specifically: // // Bidirectional Stream States: // +--------+--------------+----------+----------+ // | ON | Initiated By | Readable | Writable | // +--------+--------------+----------+----------+ // | Server | Server | Y | Y | // +--------+--------------+----------+----------+ // | Server | Client | Y | Y | // +--------+--------------+----------+----------+ // | Client | Server | Y | Y | // +--------+--------------+----------+----------+ // | Client | Client | Y | Y | // +--------+--------------+----------+----------+ // // Unidirectional Stream States // +--------+--------------+----------+----------+ // | ON | Initiated By | Readable | Writable | // +--------+--------------+----------+----------+ // | Server | Server | N | Y | // +--------+--------------+----------+----------+ // | Server | Client | Y | N | // +--------+--------------+----------+----------+ // | Client | Server | Y | N | // +--------+--------------+----------+----------+ // | Client | Client | N | Y | // +--------+--------------+----------+----------+ // // All data sent via the Stream is buffered internally until either receipt is // acknowledged from the peer or attempts to send are abandoned. The fact that // data is buffered in memory makes it essential that the flow control for the // session and the stream are properly handled. For now, we are largely relying // on ngtcp2's default flow control mechanisms which generally should be doing // the right thing. From the JavaScript side, the application pushes data into // the stream's outbound queue and ngtcp2 pulls data from that queue as it is // able. The stream outbound has a high watermark. The JS side can choose to // continue writing data even after the high watermark is reached but this // risks using up large amounts of memory if the session is slow to send data // or the peer is slow to acknowledge receipt. The JavaScript side needs to // be aware of this risk and pay proper attention to the backpressure signals. // // A Stream may be in a fully closed state (No longer readable nor writable) // state but still have unacknowledged data in both the inbound and outbound // queues. // // A Stream is gracefully closed when (a) both read and write states are closed, // (b) all queued data has been acknowledged. // // The Stream may be forcefully closed immediately using destroy(err). This // causes all queued outbound data to be cleared, pending JavaScript writes // to be abandoned, the Stream to be immediately closed at the ngtcp2 level // without waiting for any outstanding acknowledgements. Keep in mind, however, // that the peer is not notified that the stream is destroyed and may attempt // to continue sending data and acknowledgements until it is able to determine // that the stream is gone. Any data that has already been received and is in // the inbound queue is preserved and may be read by the application. // // QUIC streams in general do not have headers. Some QUIC applications, however, // may associate headers with the stream (HTTP/3 for instance). As a // convenience, the Stream class will hold onto these headers for the // application. // // Streams may be created in a pending state. This means that while the Stream // object is created, it has not yet been opened in ngtcp2 and therefore has // no official status yet. Certain operations can still be performed on the // stream object such as providing data, adding headers, or destroying the // stream. // // When a stream is created the data source for the stream must be given. // If no data source is given, then the stream is assumed to not have any // outbound data. If the stream was created as bidirectional, the outbound // side will be closed. The data source can be fixed length or may support // streaming. What this means practically is, when a stream is opened, // you must already have a sense of whether that will provide data or // not. When in doubt, specify a streaming data source, which can produce // zero-length output. class Stream final : public AsyncWrap, public Ngtcp2Source, public DataQueue::BackpressureListener { public: using Header = NgHeaderBase<BindingData>; // Acquire a DataQueue from the given value if it is valid. The return // follows the typical V8 rules for Maybe types. If an error occurs, // the Maybe will be empty and an exception will be set on the isolate. static v8::Maybe<std::shared_ptr<DataQueue>> GetDataQueueFromSource( Environment* env, v8::Local<v8::Value> value); // The stream_user_data field is from ngtcp2 and will point to the // Stream instance associated with the stream_id. static Stream* From(void* stream_user_data); JS_CONSTRUCTOR(Stream); JS_BINDING_INIT_BOILERPLATE(); // Creates a new non-pending stream. The directionality of the stream // is inferred from the stream id. static BaseObjectPtr<Stream> Create( Session* session, stream_id id, std::shared_ptr<DataQueue> source = nullptr); // Creates a new pending stream. static BaseObjectPtr<Stream> Create( Session* session, Direction direction, std::shared_ptr<DataQueue> source = nullptr); // The constructor is only public to be visible by MakeDetachedBaseObject. // Call Create to create new instances of Stream. Stream(BaseObjectWeakPtr<Session> session, v8::Local<v8::Object> obj, stream_id id, std::shared_ptr<DataQueue> source); // Creates the stream in a pending state. The constructor is only public // to be visible to MakeDetachedBaseObject. Call Create to create new // instances of Stream. Stream(BaseObjectWeakPtr<Session> session, v8::Local<v8::Object> obj, Direction direction, std::shared_ptr<DataQueue> source); DISALLOW_COPY_AND_MOVE(Stream) ~Stream() override; // While the stream is still pending, the id will be kMaxStreamId, // inidicating the maximum possible stream id is kMaxStreamId - 1. stream_id id() const; // While the stream is still pending, the origin will be invalid. Side origin() const; Direction direction() const; Session& session() const; // Returns the most recent activity timestamp for this stream in // nanoseconds (uv_hrtime). Uses received_at if data has been received, // otherwise falls back to created_at. Returns 0 if neither is set. uint64_t last_activity_timestamp() const; // True if this stream was created in a pending state and is still waiting // to be created. bool is_pending() const; // True if the stream is already destroyed. bool is_destroyed() const; // True if we've completely sent all outbound data for this stream. // Importantly, this does not necessarily mean that we are completely // done with the outbound data. We may still be waiting on outbound // data to be acknowledged by the remote peer. bool is_eos() const; // True if the stream wants to send trailing headers after the body. bool wants_trailers() const; // Marks this stream as having received 0-RTT early data. void set_early(); // True if this stream is still in a readable state. bool is_readable() const; // True if this stream is still in a writable state. bool is_writable() const; // True if an outbound data source has been configured. bool has_outbound() const; // True if a Blob::Reader has been created for the inbound data. bool has_reader() const; // Returns the Blob::Reader for the inbound data, or nullptr. Blob::Reader* reader() const; // Called by the session/application to indicate that the specified number // of bytes have been acknowledged by the peer. void Acknowledge(size_t datalen); // Called by the session/application to indicate that the specified number // of bytes have been transmitted to the peer. This is an initial // indication occuring the first time data is sent. It does not indicate // that the data has been retransmitted due to loss or has been // acknowledged to have been received by the peer. void Commit(size_t datalen, bool fin = false); void EndWritable(); void EndReadable(std::optional<uint64_t> maybe_final_size = std::nullopt); void EntryRead(size_t amount) override; void BeforePull() override; // Pulls data from the internal outbound DataQueue configured for this stream. // This is called by the session/application when it is preparing to send // data to the peer. There is no guarantee that the requested amount of data // will actually be sent. The amount of data actually sent is indicated // by the datalen argument to the Commit() method. int DoPull(bob::Next<ngtcp2_vec> next, int options, ngtcp2_vec* data, size_t count, size_t max_count_hint) override; // Forcefully close the stream immediately. Data already queued in the // inbound is preserved but new data will not be accepted. All pending // writes are abandoned, and the stream is immediately closed at the ngtcp2 // level without waiting for any outstanding acknowledgements. void Destroy(QuicError error = QuicError()); struct ReceiveDataFlags final { // Identifies the final chunk of data that the peer will send for the // stream. bool fin = false; // Indicates that this chunk of data was received in a 0RTT packet before // the TLS handshake completed, suggesting that is is not as secure and // could be replayed by an attacker. bool early = false; }; void ReceiveData(const uint8_t* data, size_t len, ReceiveDataFlags flags); void ReceiveStopSending(QuicError error); void ReceiveStreamReset(uint64_t final_size, QuicError error); // Sends a reset stream to the peer to tell it we will not be sending any // more data for this stream. This has the effect of shutting down the // writable side of the stream for this peer. Any data that is held in the // outbound queue will be dropped. The stream may still be readable. void DoStreamReset(error_code code); // Tells the peer to stop sending data for this stream. This has the effect // of shutting down the readable side of the stream for this peer. Any data // that has already been received is still readable. void SendStopSending(error_code code); // Currently, only HTTP/3 streams support headers. These methods are here // to support that. They are not used when using any other QUIC application. void BeginHeaders(HeadersKind kind); void set_headers_kind(HeadersKind kind); // Returns false if the header cannot be added. This will typically happen // if the application does not support headers, a maximum number of headers // have already been added, or the maximum total header length is reached. bool AddHeader(std::unique_ptr<Header> header); // TODO(@jasnell): Implement MemoryInfo to track outbound_, inbound_, // reader_, headers_, and pending_headers_queue_. SET_NO_MEMORY_INFO() SET_MEMORY_INFO_NAME(Stream) SET_SELF_SIZE(Stream) struct State; struct Stats; // Typed accessors for arena-allocated state/stats. These are defined // in streams.cc where State and Stats are complete types. inline State* state() { return static_cast<State*>(state_slot_.ptr); } inline const State* state() const { return static_cast<const State*>(state_slot_.ptr); } inline Stats* stats() { return static_cast<Stats*>(stats_slot_.ptr); } inline const Stats* stats() const { return static_cast<const Stats*>(stats_slot_.ptr); } private: struct Impl; struct PendingHeaders; class Outbound; // Flushes any data accumulated in the receive ring buffer into the // inbound DataQueue as a single right-sized entry. void FlushAccumulation(); // Gets a reader for the data received for this stream from the peer, BaseObjectPtr<Blob::Reader> get_reader(); void set_final_size(uint64_t amount); void set_outbound(std::shared_ptr<DataQueue> source); // Streaming outbound support void InitStreaming(); void WriteStreamData(const v8::FunctionCallbackInfo<v8::Value>& args); void EndWriting(); bool is_local_unidirectional() const; bool is_remote_unidirectional() const; // JavaScript callouts // Notifies the JavaScript side that the stream has been destroyed. void EmitClose(const QuicError& error); // Notifies the JavaScript side that the stream has been reset. void EmitReset(const QuicError& error); // Notifies the JavaScript side that the peer asked it to stop sending. void EmitStopSending(const QuicError& error); // Notifies the JavaScript side that the application is ready to receive // trailing headers. Any trailing headers must be sent immediately, and // synchronously when this callback is triggered. void EmitWantTrailers(); // Notifies the JavaScript side that sending data on the stream has been // blocked because of flow control restriction. void EmitBlocked(); // Notifies the JavaScript side that the outbound buffer has capacity // for more data. Fires when write_desired_size transitions from 0 to > 0. void EmitDrain(); // Updates the write_desired_size state field based on current flow control // and outbound buffer state. Emits drain if transitioning from 0 to > 0. void UpdateWriteDesiredSize(); // Delivers the set of inbound headers that have been collected. void EmitHeaders(); void NotifyReadableEnded(error_code code); void NotifyWritableEnded(error_code code); // When a pending stream is finally opened, the NotifyStreamOpened method // will be called and the id will be assigned. void NotifyStreamOpened(stream_id id); void EnqueuePendingHeaders(HeadersKind kind, v8::Local<v8::Array> headers, HeadersFlags flags); ArenaSlotBase stats_slot_; ArenaSlotBase state_slot_; BaseObjectWeakPtr<Session> session_; std::unique_ptr<Outbound> outbound_; std::shared_ptr<DataQueue> inbound_; BaseObjectWeakPtr<Blob::Reader> reader_; std::unique_ptr<RecvAccumulator> recv_accumulator_; // If the stream cannot be opened yet, it will be created in a pending state. // Once the owning session is able to, it will complete opening of the stream // and the stream id will be assigned. std::optional<std::unique_ptr<PendingStream>> maybe_pending_stream_ = std::nullopt; std::vector<std::unique_ptr<PendingHeaders>> pending_headers_queue_; error_code pending_close_read_code_ = 0; error_code pending_close_write_code_ = 0; struct StoredPriority { StreamPriority priority = StreamPriority::DEFAULT; StreamPriorityFlags flags = StreamPriorityFlags::NON_INCREMENTAL; bool pending = false; }; StoredPriority priority_; const StoredPriority& stored_priority() const { return priority_; } // The headers_ field holds a block of headers that have been received and // are being buffered for delivery to the JavaScript side. Headers are // stored as C++ objects during collection (AddHeader) and converted to // V8 strings only when emitted (EmitHeaders), avoiding StrongRootAllocator // mutex contention on the per-header hot path. std::vector<std::unique_ptr<Header>> headers_; // The headers_kind_ field indicates the kind of headers that are being // buffered. HeadersKind headers_kind_ = HeadersKind::INITIAL; // The headers_length_ field holds the total length of the headers that have // been buffered. size_t headers_length_ = 0; friend struct Impl; friend class PendingStream; friend class Http3ApplicationImpl; friend class DefaultApplication; public: // The Queue/Schedule here are part of the mechanism used to // determine which streams have data to send on the session. When a stream // potentially has data available, it will be scheduled in the Queue. Then, // when the Session::Application starts sending pending data, it will check // the queue to see if there are streams waiting. If there are, it will grab // one and check to see if there is data to send. When a stream does not have // data to send (such as when it is initially created or is using an async // source that is still waiting for data to be pushed) it will not appear in // the queue. ListNode<Stream> stream_queue_; using Queue = ListHead<Stream, &Stream::stream_queue_>; void Schedule(Queue* queue); void Unschedule(); }; } // namespace node::quic #endif // defined(NODE_WANT_INTERNALS) && NODE_WANT_INTERNALS