Http3Session 和 Streams

HTTP/3 和 QUIC 协议在 neqo 库中实现。Http3Session、Http3Steam 和 Http3WebTransportStream 被添加到将该库集成到现有的 necko 代码中。

以下类是必要的

  • HttpConnectionUDP - 这是在 nsHttpConnectionMgr 中注册的对象,它也用作套接字事件的异步监听器(它实现了 nsIUDPSocketSyncListener)

  • nsUDPSocket - 代表一个 UDP 套接字并实现 nsASocketHandler。nsSocketTransportService 管理 UDP 和 TCP 套接字,并在套接字遇到错误或有数据要读取等情况时调用相应的 nsASocketHandler。

  • NeqoHttp3Conn 是一个映射到 Rust 对象 Http3Client 的 C++ 对象。

  • Http3Session 管理 NeqoHttp3Conn/Http3Client,并在 Rust 实现和 necko 遗留代码(即 HttpConnectionUDP 和 nsHttpTransaction)之间提供桥梁。

  • Http3Streams 用于将从 nsHttpTransaction 读取和写入数据映射到 NeqoHttp3Conn/Http3Client API(例如,nsHttpTransaction::OnWriteSegment 将调用 Http3Client::read_data)。NeqoHttp3Conn 仅由 Http3Sesson 访问,NeqoHttp3Conn 函数在需要时通过 Http3Session 公开。

graph TD A[HttpConnectionMgr] --> B[HttpConnectionUDP] B --> C[nsUDPSocket] C --> B D[nsSocketTransportService] --> C B --> E[NeqoHttp3Conn] B --> F[Http3Stream] F -->|row| B F --> G[nsHttpTransport] G --> B B --> G

与套接字的交互和驱动 Neqo

此文档 中所述,neqo 不会创建套接字,它会生成(即编码)应作为 UDP 数据包有效负载发送的数据,并使用在 UDP 套接字上接收到的数据。因此,necko 负责创建套接字以及从套接字读取和写入数据。Necko 使用 nsUDPSocket 和 nsSocketTransportService 来实现这一点。UDP 套接字会不断轮询以进行读取。它不会轮询写入,我们让 QUIC 控制以避免网络路径和缓冲区过载。

当 UDP 套接字有可用数据包时,nsSocketTransportService 将从轮询函数返回并调用 nsUDPSocket::OnSocketReady,这将调用 HttpConnectionUDP::OnPacketReceived、HttpConnectionUDP::RecvData 以及进一步的 Http3Session::RecvData。对于写入数据,将调用 HttpConnectionUDP::SendData,这将调用 Http3Session::SendData。

Neqo 需要一个外部计时器。计时器由 Http3Session 管理。当计时器到期时,将执行 HttpConnectionUDP::OnQuicTimeoutExpired,该函数将调用 Http3Session::ProcessOutputAndEvents。

当我们与 neqo 交互时,HttpConnectionUDP::RecvData、HttpConnectionUDP::SendData 或 HttpConnectionUDP::OnQuicTimeoutExpired 必须位于堆栈上。原因是在发生错误时,它们负责进行正确的清理。例如,如果有一个准备读取的慢速读取器,它将调用 Http3Session::TransactionHasDataToRecv 以在列表中注册,并且将调用 HttpConnectionUDP::ForceRecv,这将调用与接收新数据包时相同的函数链,即 HttpConnectionUDP::RecvData 和进一步的 Http3Session::RecvData。另一个例子是,当一个新的 HTTP 事务添加到会话时,事务需要发送数据。事务将在列表中注册,并且将调用 HttpConnectionUDP::ResumeSend,这将进一步调用 HttpConnectionUDP::SendData。

Http3Session 持有一个对 ConnectionHandler 对象的引用,该对象是围绕 HttpConnectionUDP 的包装器对象。ConnectionHandler 的析构函数调用 nsHttpHandler::ReclaimConnection,该函数负责从 nsHttpConnectionMgr 中删除连接。HttpConnectionUDP::RecvData、HttpConnectionUDP::SendData 或 HttpConnectionUDP::OnQuicTimeoutExpired 调用 HttpConnectionUDP::CloseTransaction,这将导致 Http3Session 删除对 ConnectionHandler 对象的引用。ConnectionHandler 对象将被销毁,并且将调用 nsHttpHandler::ReclaimConnection。此行为是历史原因,它也用于 HTTP/2 和旧版本。对于旧版本,nsHttpHandler::ReclaimConnection 可能会重用连接而不是将其从 nsHttpConnectionMgr 中删除。

三个主要负责驱动 neqo 的 neqo 函数是 process_input、process_output 和 next_event。它们由以下函数调用:

  • Http3Session::ProcessInput,

  • Http3Session::ProcesOutput 和,

  • Http3Session::ProcessEvents。

**ProcessInput** 在此函数中,我们从 UDP 套接字获取数据并调用 NeqoHttp3Conn::ProcessInput,该函数映射到 Http3Client::process_input。从套接字读取数据包,直到套接字缓冲区为空。

**ProcessEvents** 此函数处理所有可用的 neqo 事件。它仅在发生致命错误时才提前返回。它调用 NeqoHttp3Conn::GetEvent,该函数映射到 Http3Client::next_event。事件及其处理将在下面解释。

**ProcessOutput** 当 necko 对 neqo 执行某些操作时(例如,添加新的 HTTP 事务、完成证书验证等)或计时器到期时,将调用此函数。在这两种情况下,necko 都希望检查 neqo 是否有数据要发送或更改其状态。此函数调用 NeqoHttp3Conn::ProcessOutput,该函数映射到 Http3Client::process_output。在 Http3Session::ProcessOutput 函数中,重复调用 NeqoHttp3Conn::ProcessOutput,这会将数据包从 neqo 发送到套接字,直到 neqo 返回回调计时器、发出空闲信号或返回致命错误。

**Http3Session::RecvData** 执行以下步骤

  • ProcessSlowConsumers - 下面解释。

  • ProcessInput - 处理新数据包。

  • ProcessEvents - 查看是否有新事件

  • ProcessOutput - 查看数据包到达后(例如,发送确认)或由于事件处理(例如,流已取消)我们是否有新数据包要发送。

**Http3Session::SendData** 执行以下步骤

  • 处理(HTTP 和 WebTransport)有数据要写入的流。

  • ProcessOutput - 查看流向 neqo 提供数据后是否有新数据包要发送。

**Http3Session::ProcessOutputAndEvents** 执行以下步骤

  • ProcessOutput - 超时后,neqo 最有可能有数据要重传,或者它将发送 ping

  • ProcessEvents - 查看连接的状态是否已更改,即连接是否超时

HTTP 和 WebTransport 流读取数据

下图显示了如何从 HTTP 流中读取数据。WebTransport 流的图表将在稍后添加。

flowchart TD A1[nsUDPSocket::OnSocketReady] --> |HttpConnectionUDP::OnPacketReceived| C[HttpConnectionUDP] A[HttpConnectionUDP::ResumeRecv calls] --> C B[HttpConnectionUDPForceIO] --> |HttpConnectionUDP::RecvData| C C -->|1. Http3Session::RecvData| D[Http3Session] D --> |2. Http3Stream::WriteSegments|E[Http3Stream] E --> |3. nsHttpTransaction::WriteSegmentsAgain| F[nsHttpTransaction] F --> |4. nsPipeOutputStream::WriteSegments| G["nsPipeOutputStream"] G --> |5. nsHttpTransaction::WritePiipeSegnemt| F F --> |6. Http3Stream::OnWriteSegment| E E --> |"7. 返回响应头或调用 Http3Session::ReadResponseData"|D D --> |8. NeqoHttp3Conn::ReadResponseDataReadResponseData| H[NeqoHHttp3Conn]

当有新的承载流数据的包到达 QUIC 连接时,将调用 nsUDPSocket::OnSocketReady,这将调用 Http3Session::RecvData。Http3Session::RecvData 和 ProcessInput 将从套接字读取新数据包并将其提供给 neqo 进行处理。在下一步中,将调用 ProcessEvent,该函数将有一个 DataReadable 事件,并且将调用 Http3Stream::WriteSegments。Http3Stream::WriteSegments 重复调用 nsHttpTransaction::WriteSegmentsAgain,直到从 QUIC 流中读取所有数据或管道无法接受更多数据为止。后者可能发生在 HTTP 事务或 WebTransport 流的监听器速度较慢,并且无法足够快地读取 HTTP3/WebTransport 流上所有可用数据时。

当管道无法接受更多数据时,nsHttpTransaction 将调用 nsPipeOutputStream::AsyncWait 并等待 nsHttpTransaction::OnOutputStreamReady 回调。当调用 nsHttpTransaction::OnOutputStreamReady 时,将执行 Http3Stream/Session::TransactionHasDataToRecv,并执行以下操作

  • 将相应的流添加到列表 (mSlowConsumersReadyForRead) 中,并且

  • 调用 nsHttpConnection::ResumeRecv(即,它强制执行与套接字有数据要接收时相同的代码路径,以便可以像前面解释的那样正确处理错误)。

这些流将在 ProcessSlowConsumers 中处理,该函数由 Http3Session::RecvData 调用。

HTTP 和 WebTransport 流写入数据

下图显示了如何从 HTTP 流中发送数据。WebTransport 流的图表将在稍后添加。

flowchart TD A[HttpConnectionUDP::ResumeSend calls] --> C[HttpConnectionUDP] B[HttpConnectionUDPForceIO] --> |HttpConnectionUDP::SendData| C C -->|1. Http3Session::SendData| D[Http3Session] D --> |2. Http3Stream::ReadSegments|E[Http3Stream] E --> |3. nsHttpTransaction::ReadSegmentsAgain| F[nsHttpTransaction] F --> |4. nsPipeInputStream::ReadSegments| G["nsPipeInputStream(Request stream)"] G --> |5. nsHttpTransaction::ReadRequestSegment| F F --> |6. Http3Stream::OnReadSegment| E E --> |7. Http3Session::TryActivating/SendRequestBody|D D --> |8. NeqoHttp3Conn::Fetch/SendRequestBody| H[NeqoHHttp3Conn]

当 nsHttpTransaction 新添加到事务中或 nsHttpTransaction 有更多数据要写入时,将调用 Http3Session::StreamReadyToWrite(在后一种情况下通过 Http3Session::TransactionHasDataToWrite),它执行以下操作

  • 将相应的流添加到列表 (mReadyForWrite) 中,并且

  • 调用 HttpConnectionUDP::ResumeSend

Http3Session::SendData 函数遍历 mReadyForWrite 并为每个流调用 Http3Stream::ReadSegments。

Neqo 事件

对于 **HeaderReady** 和 **DataReadable**,将调用相应流的 Http3Stream::WriteSegments 函数。上面流程图中显示的代码路径将调用流提供的 nssHttpTransaction 来获取标头和数据。

**DataWritable** 表示流之前无法接受更多数据,并且流控制现在允许发送更多数据。Http3Sesson 将标记流为可写(通过调用 Http3Session::StreamReadyToWrite)以验证流是否要写入更多数据。

**Reset** 和 **StopSending** 事件将传播到流,并且流将关闭。

当 QUIC 连接由于过去的流控制而无法接受新流,并且流控制增加并且流再次可创建时,将发布 **RequestsCreatable** 事件。Http3Session::ProcessPendingProcessPending 将触发排队流的激活。

当需要证书验证时,将发布 **AuthenticationNeeded** 和 **EchFallbackAuthenticationNeeded**。

ZeroRttRejected 事件在零 RTT 数据被拒绝时发布。

ResumptionToken 事件在新的恢复令牌可用时发布。

ConnectionConnectedGoawayReceivedConnectionClosingConnectionClosed 事件反映了连接状态的变化。ConnectionClosingConnectionClosed 之间的区别在于,在 ConnectionClosed 之后,连接可以立即关闭,而在 ConnectionClosing 之后,我们将保留连接对象一段时间,直到收到 ConnectionClosed 事件。在此期间,如果关闭帧丢失,我们将重新传输它们。

WebTransport 事件

Negotiated - 只有在从服务器收到 HTTP/3 设置帧后,才会协商 WebTransport。此时,会发布 Negotiated 事件以通知应用程序。

当 WebTransport 会话成功协商时,会发布 Session 事件。

当连接正常或异常关闭时,会发布 SessionClosed 事件。

当对等方打开新的流时,会发布 NewStream 事件。