QUIC協議分析


QUIC概述

Quic 全稱 quick udp internet connection,“快速 UDP 互聯網連接”,(和英文 quick 諧音,簡稱“快”)是由 google 提出的使用 udp 進行多路並發傳輸的協議。
Quic 相比現在廣泛應用的 http2+tcp+tls 協議有如下優勢:

  • 減少了 TCP 三次握手及 TLS 握手時間。
  • 改進的擁塞控制。
  • 避免隊頭阻塞的多路復用。
  • 連接遷移。
  • 前向冗余糾錯。

QUIC核心特性連接建立延時低

0RTT 建連可以說是 QUIC 相比 HTTP2 最大的性能優勢。那什么是 0RTT 建連呢?這里面有兩層含義。

1、傳輸層 0RTT 就能建立連接。
2、加密層 0RTT 就能建立加密連接。

比如上圖左邊是 HTTPS 的一次完全握手的建連過程,需要 3 個 RTT。就算是 Session Resumption,也需要至少 2 個 RTT。

而 QUIC 呢?由於建立在 UDP 的基礎上,同時又實現了 0RTT 的安全握手,所以在大部分情況下,只需要 0 個 RTT 就能實現數據發送,在實現前向加密的基礎上,並且 0RTT 的成功率相比 TLS 的 Sesison Ticket 要高很多。

改進的擁塞控制

TCP 的擁塞控制實際上包含了四個算法:慢啟動,擁塞避免,快速重傳,快速恢復。
QUIC 協議當前默認使用了 TCP 協議的 Cubic 擁塞控制算法,同時也支持 CubicBytes, Reno, RenoBytes, BBR, PCC 等擁塞控制算法。

擁塞控制特點:

可插拔

指可以靈活的使⽤擁塞算法,⼀次選擇⼀個或⼏個擁塞算法同時⼯作

  • 在應⽤層實現擁塞算法,⽽以前實現對應的擁塞算法,需要部署到操作系統內核中。現在可以更快
    的迭代升級
  • 不同的平台具有不同的底層和⽹絡環境,現在我們能夠靈活的選擇擁塞控制,⽐如選擇A選擇
    Cubic,B則選擇顯示擁塞控制
  • 應⽤程序不需要停機和升級,我們在服務端進⾏的修改,現在只需要簡單的reload⼀下就能實現不同擁塞控制切換

包編號單調遞增

QUIC使⽤Packet Number,每個Packet Number嚴格遞增,所以如果Packet N丟失了,重傳Packet N的Packet Number已不是N,⽽是⼀個⼤於N的值。 這樣可以確保不會出現TCP中的”重傳歧義“問題。

禁止Reneging

QUIC不允許重新發送任何確認的數據包,也就禁止了接收方丟棄已經接受的內容。

更多ACK幀

TCP只能有3個ACK Block,但是Quic Ack Frame 可以同時提供 256 個 Ack Block,在丟包率⽐較⾼的⽹絡下,更多的 Sack Block可以提升⽹絡的恢復速度,減少重傳量。

更精准的發送延遲

QUIC端點會測量接收到數據包與發送相應確認之間的延遲,使對等⽅可以保持更准確的往返時間估計

多路復用

HTTP2的最⼤特性就是多路復⽤,⽽HTTP2最⼤的問題就是隊頭阻塞。例如,HTTP2在⼀個TCP連接上同時發送3個stream,其中第2個stream丟了⼀個Packet,TCP為了保證數據可靠性,需要發送端重傳丟失的數據包,雖然這時候第3個數據包已經到達接收端,但被阻塞了。

QUIC可以避免這個問題,因為QUIC的丟包、流控都是基於stream的,所有stream是相互獨
⽴的,⼀條stream上的丟包,不會影響其他stream的數據傳輸。

前向糾錯

為了從丟失的數據包中恢復⽽⽆需等待重新傳輸,QUIC可以⽤FEC數據包來補充⼀組數據包。與RAID-4相似,FEC數據包包含FEC組中數據包的奇偶校驗。如果該組中的⼀個數據包丟失,則可以從FEC數據包和該組中的其余數據包中恢復該數據包的內容。發送者可以決定是否發送FEC分組以優化特定場景(例如,請求的開始和結束).
在這⾥需要注意的是:早期QUIC中使⽤的FEC算法是基於XOR的簡單實現,不過IETF的QUIC協議標准中已經沒有FEC的蹤影,猜測是FEC在QUIC協議的應⽤場景中難以被⾼效的使⽤。

頭部和負載的加密

由於使用了TLS 1.3,因此QUIC可以確保數據的可靠性,每次發送的數據都被加密。

更快的網絡交換

QUIC允許更快地進行網絡切換,例如將wifi切換為數據網絡。
為了做到這一點,QUIC的連接標識發生了變化。
任何⼀條 QUIC 連接不再以 IP 及端⼝四元組標識,⽽是以⼀個 64 位的隨機數作為 ID 來標識,這樣就算 IP 或者端⼝發⽣變化時,只要 ID 不變,這條連接依然維持着,上層業務邏輯感知不到變化,不會中斷,也就不需要重連。

啟動切換

端點可以通過發送包含來⾃該地址的⾮探測幀的數據包,將連接遷移到新的本地地址。

響應切換

從包含⾮探測幀的新對等⽅地址接收到數據包表明對等⽅已遷移到該地址。

數據檢測和擁塞控制

當響應后,中間可能會有數據損失和擁塞控制問題:新路徑上的可⽤容量可能與舊路徑上的容量不同。在舊路徑上發送的數據包不應有助於新路徑的擁塞控制或RTT估計。端點確認對等⽅對其新地址的所有權后,應⽴即為新路徑重置擁塞控制器和往返時間估計器。

流量控制

QUIC同樣可以針對接收方的緩沖進行設置,以防止發送方發送過快對接收方造成壓力。

QUIC有兩種控制方法:

  • 流控制:通過限制可以在任何流上發送的數據量來防⽌單個流占⽤整個連接的接收緩沖區。
  • 連接控制:通過限制所有流上以STREAM幀發送的流數據的總字節數,來防⽌發送⽅超出連接的接收⽅緩沖區容量。

QUIC 實現流量控制的原理⽐較簡單:
通過 window_update 幀告訴對端⾃⼰可以接收的字節數,這樣發送⽅就不會發送超過這個數量的數據。
通過 BlockFrame 告訴對端由於流量控制被阻塞了,⽆法發送數據。

Packet格式

QUIC 有四種 packet 類型:

  • Version Negotiation Packets
  • Frame Packets
  • FEC Packets
  • Public Reset Packets

所有的 QUIC packet 大小都應該低於路徑的 MTU, 路徑 MTU 的發現由進程負責實現, QUIC 在IPv6 最大支持 1350 的packet,IPv4最大支持 1370

QUIC編譯與測試

實驗環境:

  • Ubuntu 20.04
  • 磁盤空間300GB
  • 內存8G

獲取源代碼

安裝編譯依賴環境並添加至環境變量

git clone https://chromium.googlesource.com/chromium/tools/depot_tools.git
export PATH="$PATH:${HOME}/depot_tools"

獲取chromium源代碼

mkdir ~/chromium && cd ~/chromium
fetch --nohooks chromium

如果僅僅只是想編譯最新版本源碼(大約24gb)的話,可以添加--no-history能減少一些下載量,但是缺點就是無法切換到舊版本源碼。

安裝構建所需的依賴項

cd src
./build/install-build-deps.sh

如果在已經運行過install-build-deps.sh的機器重新檢出chromium源碼的話,可以不輸入-nohooks這個選項,其會在fetch完畢后自動gclient runhooks。

gclient runhooks

設置並生成編譯目錄

 gn gen out/Default

開始編譯 QUIC client and server

ninja -C out/Default quic_server quic_client

mkdir /tmp/quic-data
cd /tmp/quic-data
wget -p --save-headers https://www.example.org
cd www.example.org

修改index.html

vim index.html

刪除(如果存在):"Transfer-Encoding: chunked"
刪除(如果存在):"Alternate-Protocol: ..."
添加:X-Original-Url:https://www.example.org/

將后面內容全部刪除,改成你想要測試的數據,例如改成json數據

生成CA證書並將其部署至系統

cd net/tools/quic/certs
./generate-certs.sh
cd 
certutil -d sql:$HOME/.pki/nssdb -A -t "C,," -n quic -i chromium/src/net/tools/quic/certs/out/2048-sha256-root.pem

測試運行

運行quic_server,監聽8888端口

cd chromium/src
./out/Default/quic_server \
    --quic_response_cache_dir=/tmp/quic-data/www.example.org \
    --certificate_file=net/tools/quic/certs/out/leaf_cert.pem \
    --key_file=net/tools/quic/certs/out/leaf_cert.pkcs8 \
    --host=127.0.0.1 \
    --port=8888&

運行quic-client客戶端,向本地8888端口發送請求,順便提一句如果沒指定端口的話會默認采用80端口。

./out/Default/quic_client  \
    --host=127.0.0.1 \
    --port=8888 \
    https://www.example.org/ \
  	--allow_unknown_root_cert

未加flag --allow_unknown_root_cert 報錯

測試成功,獲取到了之前下載並修改后的www.example.org的index.html

數據重傳邏輯分析

最新的quic代碼里中的重傳邏輯,實現了兩種處理模式,一個是在connection層實現的重傳,另一個是在session層實現的重傳。在應用中只能啟用一個,要有由connection負責重傳,要么由session負責重傳。session層的重傳的啟動開關是session_decides_what_to_write_。

QuicFrame的定義是:

struct QUIC_EXPORT_PRIVATE QuicFrame {
explicit QuicFrame(QuicStreamFrame* stream_frame);
 QuicFrameType type;
  union {
    // Frames smaller than a pointer are inline.
    QuicPaddingFrame padding_frame;
    QuicMtuDiscoveryFrame mtu_discovery_frame;
    QuicPingFrame ping_frame;

    // Frames larger than a pointer.
    QuicStreamFrame* stream_frame;
    QuicAckFrame* ack_frame;
    QuicStopWaitingFrame* stop_waiting_frame;
    QuicRstStreamFrame* rst_stream_frame;
    QuicConnectionCloseFrame* connection_close_frame;
    QuicGoAwayFrame* goaway_frame;
    QuicWindowUpdateFrame* window_update_frame;
    QuicBlockedFrame* blocked_frame;
  };
}

stream_frame是一個指針形式,但是在新的代碼里,就是個結構體。這樣的改變,就是為了實現這個session層面的重傳邏輯。

struct QUIC_EXPORT_PRIVATE QuicFrame {
  explicit QuicFrame(QuicStreamFrame stream_frame);
      struct {
      QuicFrameType type;

      // TODO(wub): These frames can also be inlined without increasing the size
      // of QuicFrame: QuicStopWaitingFrame, QuicRstStreamFrame,
      // QuicWindowUpdateFrame, QuicBlockedFrame, QuicPathResponseFrame,
      // QuicPathChallengeFrame and QuicStopSendingFrame.
      union {
        QuicAckFrame* ack_frame;
        QuicStopWaitingFrame* stop_waiting_frame;
        QuicRstStreamFrame* rst_stream_frame;
        QuicConnectionCloseFrame* connection_close_frame;
        QuicGoAwayFrame* goaway_frame;
        QuicWindowUpdateFrame* window_update_frame;
        QuicBlockedFrame* blocked_frame;
        QuicApplicationCloseFrame* application_close_frame;
        QuicNewConnectionIdFrame* new_connection_id_frame;
        QuicRetireConnectionIdFrame* retire_connection_id_frame;
        QuicPathResponseFrame* path_response_frame;
        QuicPathChallengeFrame* path_challenge_frame;
        QuicStopSendingFrame* stop_sending_frame;
        QuicMessageFrame* message_frame;
        QuicCryptoFrame* crypto_frame;
        QuicNewTokenFrame* new_token_frame;
      };
    };
}

數據的重傳,首先要判斷數據的丟包。

bool QuicSentPacketManager::OnAckFrameEnd(QuicTime ack_receive_time){
  PostProcessAfterMarkingPacketHandled(last_ack_frame_, ack_receive_time,
                                       rtt_updated_, prior_bytes_in_flight);
}
void QuicSentPacketManager::PostProcessAfterMarkingPacketHandled(
    const QuicAckFrame& ack_frame,
    QuicTime ack_receive_time,
    bool rtt_updated,
    QuicByteCount prior_bytes_in_flight){
     InvokeLossDetection(ack_receive_time);
    }
void QuicSentPacketManager::InvokeLossDetection(QuicTime time) {
	MarkForRetransmission(packet.packet_number, LOSS_RETRANSMISSION);    
 }
 //在這里就進入分叉處理,
void QuicSentPacketManager::MarkForRetransmission(
    QuicPacketNumber packet_number,
    TransmissionType transmission_type) {
  //  記錄要重傳的數據包序號,后續connection層的重傳會用到
  if (!session_decides_what_to_write()) {
    if (!unacked_packets_.HasRetransmittableFrames(*transmission_info)) {
      return;
    }
    if (!QuicContainsKey(pending_retransmissions_, packet_number)) {
      pending_retransmissions_[packet_number] = transmission_type;
    }
    return;
  }
//如果session_decides_what_to_write_開啟,則由session負責重傳。
  HandleRetransmission(transmission_type, transmission_info);    
}

connection負責的重傳:

void QuicConnection::WritePendingRetransmissions()
{
  DCHECK(!session_decides_what_to_write());
  // Keep writing as long as there's a pending retransmission which can be
  // written.
  while (sent_packet_manager_.HasPendingRetransmissions() &&
         CanWrite(HAS_RETRANSMITTABLE_DATA)) {
    const QuicPendingRetransmission pending =
        sent_packet_manager_.NextPendingRetransmission();

    // Re-packetize the frames with a new packet number for retransmission.
    // Retransmitted packets use the same packet number length as the
    // original.
    // Flush the packet generator before making a new packet.
    // TODO(ianswett): Implement ReserializeAllFrames as a separate path that
    // does not require the creator to be flushed.
    // TODO(fayang): FlushAllQueuedFrames should only be called once, and should
    // be moved outside of the loop. Also, CanWrite is not checked after the
    // generator is flushed.
    {
      ScopedPacketFlusher flusher(this, NO_ACK);
      packet_generator_.FlushAllQueuedFrames();
    }
    DCHECK(!packet_generator_.HasQueuedFrames());
    char buffer[kMaxPacketSize];
    packet_generator_.ReserializeAllFrames(pending, buffer, kMaxPacketSize);
  }
}
void QuicPacketCreator::ReserializeAllFrames(
    const QuicPendingRetransmission& retransmission,
    char* buffer,
    size_t buffer_len) {
   SerializePacket(buffer, buffer_len);
  packet_.original_packet_number = retransmission.packet_number;   
   }

這里最終還是去stream中的send_buffer_獲取數據。感興趣的可以閱讀packet_generator_.ReserializeAllFrames以下的處理的邏輯。packet_.original_packet_number記錄數據包上次發送使用的序列號,下次發送的時候回調用QuicSentPacketManager::OnPacketSent將原來記錄的丟失幀信息更新。

bool QuicSentPacketManager::OnPacketSent(
    SerializedPacket* serialized_packet,
    QuicPacketNumber original_packet_number,
    QuicTime sent_time,
    TransmissionType transmission_type,
    HasRetransmittableData has_retransmittable_data) {
    unacked_packets_.AddSentPacket(serialized_packet, original_packet_number,
                                 transmission_type, sent_time, in_flight);  
    }
    void QuicUnackedPacketMap::AddSentPacket(SerializedPacket* packet,
                                         QuicPacketNumber old_packet_number,
                                         TransmissionType transmission_type,
                                         QuicTime sent_time,
                                         bool set_in_flight){
  if (old_packet_number.IsInitialized()) {
    TransferRetransmissionInfo(old_packet_number, packet_number,
                               transmission_type, &info);
  }  
}

session負責的重傳

在session層實現的重傳,就不需要sent_packet_manager_.NextPendingRetransmission()獲取pending中含有的可重傳幀的原理的傳輸序號了(retransmission.packet_number)。也可以說QuicUnackedPacketMap中的記錄的unacked_packets_信息就不太重要了。

void QuicSentPacketManager::HandleRetransmission(
    TransmissionType transmission_type,
    QuicTransmissionInfo* transmission_info) {
   unacked_packets_.NotifyFramesLost(*transmission_info, transmission_type); 
    }
    
void QuicUnackedPacketMap::NotifyFramesLost(const QuicTransmissionInfo& info,
                                            TransmissionType type) {
  DCHECK(session_decides_what_to_write_);
  for (const QuicFrame& frame : info.retransmittable_frames) {
    session_notifier_->OnFrameLost(frame);
  }
}

void QuicSession::OnFrameLost(const QuicFrame& frame){
  QuicStream* stream = GetStream(frame.stream_frame.stream_id);
  if (stream == nullptr) {
    return;
  }
  stream->OnStreamFrameLost(frame.stream_frame.offset,
                            frame.stream_frame.data_length,
                            frame.stream_frame.fin);
}
void QuicStream::OnStreamFrameLost(QuicStreamOffset offset,
                                   QuicByteCount data_length,
                                   bool fin_lost) {
  if (data_length > 0) {
    send_buffer_.OnStreamDataLost(offset, data_length);
  }
}
void QuicStreamSendBuffer::OnStreamDataLost(QuicStreamOffset offset,
                                            QuicByteCount data_length) {
  for (const auto& lost : bytes_lost) {
    pending_retransmissions_.Add(lost.min(), lost.max());
  }
}

再次將數據發送出去

void QuicStream::OnCanWrite() {
  if (HasPendingRetransmission()) {
    WritePendingRetransmission();
    // Exit early to allow other streams to write pending retransmissions if
    // any.
    return;
  }
}
void QuicStream::WritePendingRetransmission() {
      consumed = session()->WritevData(this, id_, pending.length, pending.offset,
                                can_bundle_fin ? FIN : NO_FIN);
}

參考:
· https://blog.csdn.net/u010643777/article/details/89178372
· https://cs.chromium.org/chromium/src/net/third_party/quiche/src/quic
· http://www.chromium.org/quic
· https://zhuanlan.zhihu.com/p/32553477


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM