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 公开。
与套接字的交互和驱动 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 流的图表将在稍后添加。
当有新的承载流数据的包到达 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 流的图表将在稍后添加。
当 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 事件在新的恢复令牌可用时发布。
ConnectionConnected、GoawayReceived、ConnectionClosing 和 ConnectionClosed 事件反映了连接状态的变化。ConnectionClosing 和 ConnectionClosed 之间的区别在于,在 ConnectionClosed 之后,连接可以立即关闭,而在 ConnectionClosing 之后,我们将保留连接对象一段时间,直到收到 ConnectionClosed 事件。在此期间,如果关闭帧丢失,我们将重新传输它们。
WebTransport 事件¶
Negotiated - 只有在从服务器收到 HTTP/3 设置帧后,才会协商 WebTransport。此时,会发布 Negotiated 事件以通知应用程序。
当 WebTransport 会话成功协商时,会发布 Session 事件。
当连接正常或异常关闭时,会发布 SessionClosed 事件。
当对等方打开新的流时,会发布 NewStream 事件。