QUIC協議和HTTP3.0研究


1. 什么是QUIC

QUIC(Quick UDP Internet Connections),即快速UDP網絡連接,是被設計用在傳輸層的網絡協議,最初由Google的Jim Roskind提出,最初實現和部署在2012年,截止目前仍然是一個因特網草案,但已經被廣泛應用於Google瀏覽器Google服務器之間。目前ChormeMicrosoft EdgeFirefoxSafari均已經支持QUIC,盡管不常用。

QUIC增加了面向連接的TCP網絡應用程序的性能,它通過使用UDP在兩個端點之間建立一系列多路復用(multiplexing)的連接實現這個目的,它同時被用來代替(obsolesce)TCP在網絡層的作用,因此也被戲稱為TCP/2

QUIC與HTTP/2的多路復用連接緊密結合,允許多個數據流獨立的到達終端,因此一個數據包與其他的數據流傳輸的數據包丟失無關。與之相對的是,TCP如果有任何數據包的丟失或延遲,就會發生隊頭阻塞

QUIC的另一個目標是減少連接和傳輸時候的延遲,以及評估每一個方向的帶寬來避免阻塞。它還將擁塞控制算法移動到兩個端點的用戶空間,而不是內核空間,根據QUIC的實現,這將會提升算法的性能。此外,當遇到預期的錯誤的時候,QUIC協議可以使用前向糾錯(forward error correction)FEC來提升性能。2018年10月,IETF的HTTP和QUIC工作組共同決定將QUIC上的HTTP映射稱為HTTP/3,以使其在全球范圍內標准化。

為什么需要QUIC

傳統的TCP網絡通信協議旨在提供一個接口,然后再兩個端口之間發送數據流。TCP的傳輸需要保證數據報按順序來接收,如果發現接收順序錯誤,就需要使用自動重傳請求來通知發送方重新發送數據包,同時建立連接的三次握手在復雜的網絡環境和地理限制也是一個重要的考慮內容。

此外,由於TCP設計像一個"數據管道",如果單個數據包有問題,后續的所有數據報的發送將會被阻塞。現代社會的應用場景對更低延遲、良好的傳輸性能的要求越來越高,於是提出一個新的解決方案就十分有必要了。

QUIC做了什么

QUIC的目標幾乎等同於TCP連接,但是延遲卻會更少。它通過兩個更改來實現

  • 減少連接期間的開銷
  • 提高網絡交換事件期間的性能。例如從wifi切換到移動網絡能更快的切換

QUIC大致可以通過如下公式概括:

TCP + TLS + HTTP2 = UDP + QUIC + HTTP2’s API

從公式可看出:QUIC協議雖然是基於UDP,但它不但具有TCP的可靠性、擁塞控制、流量控制等,且在TCP協議的基礎上做了一些改進,比如避免了隊首阻塞;另外,QUIC協議具有TLS的安全傳輸特性,實現了TLS的保密功能,同時又使用更少的RTT建立安全的會話。

2. 深入QUIC

QUIC解決了一些現代網站應用的傳輸層和應用層問題,並且只需要一點或根本不需要改變客戶端應用。QUIC非常類似於TCP+TLS+HTTP2,但是是使用UDP實現的。使用QUIC作為一個獨立的協議可以實現現有協議無法實現的創新,因為現有協議往往受客戶端和中間設備的妨礙。

現在存在的硬件以及軟件不足:

  • 路由封殺UDP 443端口( 這正是QUIC 部署的端口)
  • UDP包過多,由於QS限定,會被服務商誤認為是攻擊,UDP包被丟棄;
  • 無論是路由器還是防火牆目前對QUIC都還沒有做好准備

相比於TCP+TLS+HTTP2,QUIC主要在五個方面更具有優勢(特性):

  • 建立連接延遲
  • 改善擁塞控制
  • 沒有隊頭阻塞的多路復用
  • 前向糾錯
  • 連接遷移

下面將一一介紹

2.1 建立連接(Connection Establishment)

建立連接的低延遲可以說是QUIC的核心特性。

發送數據之前,QUIC只需要0RTT就能夠建立連接,而傳統的TCP+TLS則需要1-3RTT才能夠建立連接。具體的QUIC建立連接的方法如下:

QUIC客戶端第一次連接到服務器時,客戶端必須執行1次往返握手,以獲取完成握手所需的信息。客戶端發送早期(empty)客戶端Hello問候(CHLO),服務器發送拒絕(rejection)(REJ),其中包含客戶端前進所需的信息,包括源地址令牌和服務器的證書。客戶端下次發送CHLO時,可以使用以前連接中的緩存憑據來立即將加密的請求發送到服務器。

我們再深入的了解一下QUIC的加密協議。

根據因特網草案所述,QUIC現在使用TLS1.3版本來保證傳輸的安全可靠性。

閱讀TLS1.3文檔,我們可以發現TLS的功能其中一項是支持了零往返時間(0-RTT)模式,節省了往返時間。

我們對比一下TLS1.2和TLS1.3協議:

  1. TLS1.2協議

  1. TLS1.3協議

從上面可以看出,為什么叫0RTT呢?

  1. 當客戶端首次發起QUIC連接時,客戶端想服務器發送一個client hello消息,服務器回復一個server reject消息。該消息中有包括server config,類似於TLS1.3中的key_share交換。這需要產生1-RTT. 事實上,QUIC加密協議的作者也明確指出當前的QUIC加密協議是「注定要死掉的」(destined to die), 未來將會被TLS1.3代替。只是在QUIC提出來的時候,TLS1.3還沒出生?,這只是一個臨時的加密方案。

  2. 當客戶端獲取到server config以后,就可以直接計算出密鑰,發送應用數據了,可以認為是0-RTT。

  3. 因此,QUIC握手除去首次連接需要產生1-RTT,理論上,后續握手都是0-RTT的。

  4. 假設1-RTT=100ms, QUIC建立安全連接連接的握手開銷為0ms, 功能上等價於TCP+TLS, 但是握手開銷比建立普通的TCP連接延遲都低:

    (正常體為首次建立連接的延遲,粗體部分為后續握手的延遲)

總結一下:首次建立連接需要一個RTT,但是后續連接只需要可以直接發送數據,故稱為0RTT

前面圖片是1RTT,后面是0RTT

2.2 擁塞控制(Congestion Control)

讓我們回想一下TCP的擁塞控制:慢啟動,擁塞避免,快重傳,快恢復

QUIC的擁塞控制基於了TCP NewReno。NewReno是基於擁塞窗口的擁塞控制。根據QUIC草案對於擁塞部分的描述

QUIC包括了一些具體的擁塞控制算法:

  • 顯示擁塞控制:如果路徑支持ECN,QUIC會將Congestion Experienced codepoint(CEC)標記視為擁塞信號
  • 慢啟動: 擁塞窗口一直增大直到到達閾值
  • 擁塞避免:如果有一個數據丟失,擁塞窗口減半然后重新設置閾值
  • 恢復期Recovery Period(暫譯):區別於TCP的快恢復,QUIC的恢復期是檢測到丟失的一段時間內擁塞窗口變為1
  • 忽略不可解密的數據包丟失:從上面的建立連接我們可以知道,TLS發送會有一個秘匙,如果某些數據包發送過快的話,而秘匙還沒到,就會造成無法解析這個包的數據。
  • 探測超時:發送一個探測包,如果沒有收到確認,可能擁塞
  • 持續性擁塞:如果收到一個ACK幀,與前一個已經收到的ACK幀相差較大,可能擁塞

從擁塞算法來看,QUIC相比於TCP沒有太大不不同,那么QUIC在什么地方和TCP不同呢?

2.2.1 可拔插的擁塞控制

什么是可拔插?就是可以靈活的使用擁塞算法,一次選擇一個或幾個擁塞算法同時工作

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

2.2.2 單調遞增的包編號

回想TCP,TCP使發送方的發送順序與接收方的發送順序相抵觸,從而導致重傳帶有相同序號的相同數據,從而導致“重傳歧義”

QUIC並沒有使用TCP的基於字節序號及ACK來確認消息的有序到達,QUIC使用的是Packet Number,每個Packet Number嚴格遞增,所以如果Packet N丟失了,重傳Packet N的Packet Number已不是N,而是一個大於N的值。 這樣就很容易解決TCP的重傳歧義問題。

2.2.3 沒有Reneging

什么叫 Reneging 呢?就是接收方丟棄已經接收並且上報給 SACK 選項的內容

QUIC ACK包含類似於TCP SACK的信息,但是QUIC不允許重新發送任何確認的數據包,從而極大地簡化了雙方的實現並減輕了發送方的內存壓力。

2.2.4 更多的ACK幀

QUIC支持許多ACK范圍,與TCP的3 SACK范圍相反。

由於 TCP 頭部最大只有 60 個字節,標准頭部占用了 20 字節,所以 Tcp Option 最大長度只有 40 字節,再加上 Tcp Timestamp option 占用了 10 個字節 ,所以留給 Sack 選項的只有 30 個字節。

每一個 Sack Block 的長度是 8 個,加上 Sack Option 頭部 2 個字節,也就意味着 Tcp Sack Option 最大只能提供 3 個 Block。

但是 Quic Ack Frame 可以同時提供 256 個 Ack Block,在丟包率比較高的網絡下,更多的 Sack Block 可以提升網絡的恢復速度,減少重傳量。

2.2.5 延遲確認的顯式更正

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

2.3 流(Stream)的多路復用(Multiplexing)

HTTP2的最大特性就是多路復用,而HTTP2最大的問題就是隊頭阻塞。

首先了解下為什么會出現隊頭阻塞。比如HTTP2在一個TCP連接上同時發送3個stream,其中第2個stream丟了一個Packet,TCP為了保證數據可靠性,需要發送端重傳丟失的數據包,雖然這時候第3個數據包已經到達接收端,但被阻塞了。這就是所謂的隊頭阻塞。

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

下面簡要的介紹一下流:

QUIC中的流向應用程序提供了輕量級的,有序的字節流抽象。QUIC流的另一種觀點是作為一種彈性的“消息”抽象。

流可以由任一端點創建流,可以同時發送與其他流交錯的數據,並且可以將其取消。

2.3.1 發送流

發送部分的狀態端點啟動的流發送部分(type:客戶端為0和2 ,服務器為1和3)由應用程序打開。在 “就緒”狀態代表一個新創建的數據流,它能夠從應用程序接受數據。流數據可能在此狀態下被緩沖以准備發送。

2.3.2 接受流

接收流的部分的狀態由對等方(type:客戶端的類型1和3 ,或0和2)發起的流的接收部分(對於服務器而言),則在為該流接收到第一個STREAM,STREAM_DATA_BLOCKED或RESET_STREAM時創建。對於由對等方發起的雙向流,接收到MAX_STREAM_DATA或STOP_SENDING幀作為流還創建接收部分。

你可以把流比作為發送和接受數據的數據結構

2.3.3 Connections連接

什么是連接呢?

Connection 可以類比一條 TCP 連接。多路復用意味着在一條 Connetion 上會同時存在多條 Stream。既需要對單個 Stream 進行控制,又需要針對所有 Stream 進行總體控制。

2.4 前向糾錯(Forward Error Correction)

為了從丟失的數據包中恢復而無需等待重新傳輸,QUIC可以用FEC數據包來補充一組數據包。與RAID-4相似,FEC數據包包含FEC組中數據包的奇偶校驗。如果該組中的一個數據包丟失,則可以從FEC數據包和該組中的其余數據包中恢復該數據包的內容。發送者可以決定是否發送FEC分組以優化特定場景(例如,請求的開始和結束).

在這里需要注意的是:早期QUIC中使用的FEC算法是基於XOR的簡單實現,不過IETF的QUIC協議標准中已經沒有FEC的蹤影,猜測是FEC在QUIC協議的應用場景中難以被高效的使用。

2.5 連接遷移(Connection Migration)

QUIC一個令人激動的特性就是連接遷移了,想象一下,當你從wifi切換到數據網絡的時候,客戶端IP會發生變化,這時候需要重新建立TCP連接

那 QUIC 是如何做到連接遷移呢?很簡單,任何一條 QUIC 連接不再以 IP 及端口四元組標識,而是以一個 64 位的隨機數作為 ID 來標識,這樣就算 IP 或者端口發生變化時,只要 ID 不變,這條連接依然維持着,上層業務邏輯感知不到變化,不會中斷,也就不需要重連。

2.5.1 啟動連接遷移

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

2.5.2 響應連接遷移

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

2.5.3 損失檢測和擁塞控制

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

2.6流量控制

有必要限制接收方可以緩沖的數據量,以防止快速發送方壓倒慢速接收方,或者防止惡意發送

QUCI主要采取兩種流量控制:

1.流控制:

通過限制可以在任何流上發送的數據量來防止單個流占用整個連接的接收緩沖區。

2.連接控制:

通過限制所有流上以STREAM幀發送的流數據的總字節數,來防止發送方超出連接的接收方緩沖區容量。

QUIC 實現流量控制的原理比較簡單:

通過 window_update 幀告訴對端自己可以接收的字節數,這樣發送方就不會發送超過這個數量的數據。

通過 BlockFrame 告訴對端由於流量控制被阻塞了,無法發送數據。

針對Stream:

可用窗口=最大窗口數-接收到的最大偏移數

針對Connection:

可用窗口=Stream1可用窗口+Stream2可用窗口+...+StreamN可用窗口

至此,我們關於QUIC的主要特性就講完了

3. 一個簡單的QUIC通信實現

分析的源碼基於quic-go:quic-go

前置條件:Go1.14版本以上

3.1 源碼下載編譯運行

使用git命令將源代碼clone到本地

git clone https://github.com/lucas-clemente/quic-go.git

接着,根據官方提示,運行

go test ./...

這里我使用了go test -v ./...來獲得更詳細的信息

部分測試截圖

測試成功,源代碼沒有問題,我們開始編寫服務端和客戶端的通信代碼

先簡單的貼出服務端和客戶端的核心代碼:

服務端

listener, err := quic.ListenAddr(addr, generateTLSConfig(), nil)

客戶端

session, err := quic.DialAddr(addr, tlsConf, nil)

3.2 源碼分析

先來分析客戶端:

tlsConf := &tls.Config{
		InsecureSkipVerify: true,
		NextProtos:         []string{"quic-echo-example"},
	}

首先在QUIC配置TLS來保證安全性

session, err := quic.DialAddr(addr, tlsConf, nil)

撥號,即連接指定的IP地址。session類似於TCP/IP的套接字

stream, err := session.OpenStreamSync(context.Background())

創建流,在stream上發送和接收信息。context.Background()類似於管道,相當於給予QUIC一個通信的手段

// 發送數據
stream.Write([]byte(message))

// 接收數據
buf := make([]byte, len(message))
io.ReadFull(stream, buf)

發送和接收數據,至此,一個完整的client就分析完了

繼續分析服務端:

listener, err := quic.ListenAddr(addr, generateTLSConfig(), nil)

監聽addrgenerateTLSConfig()代表TLS的配置,最后一個參數是quic.config一般是nil

sess, err := listener.Accept(context.Background())

sess與上面的session類似

stream, err := sess.AcceptStream(context.Background())

創建stream,接收信息,在server端新建一個context專門對這個連接進行通信

3.3 仿照用例自己實現

分析完上面主要的代碼之后,我們現在自己實現一個簡單的QUIC通信,實現客戶端發送Hello,服務端發送Hi

為了方便,我們把客戶端和服務端寫到一個文件夾里。

const addr = "localhost:6688"

const clientMessage = "Hello"
const serverMessage = "Hi"

首先定義一下本地監聽端口號和要發送的數據

客戶端:

session, err := quic.DialAddr(addr, tlsConf, nil)

為了簡單,我們使用最簡單的TLS配置安全傳輸

stream, err := session.OpenStreamSync(context.Background())

創建個流,使用流傳輸數據

_, err = stream.Write([]byte(clientMessage))

客戶端發送數據

_, err = stream.Read(buf)

讀取服務端發來的信息到buf,buf是一個字節數組

​ 客戶端至此完成

服務端:

listener, err := quic.ListenAddr(addr, generateTLSConfig(), nil)

服務端使用TLS檢測安全,generateTLSConfig()類似於通用配置,可自定義

sess, err := listener.Accept(context.Background())

接收新的連接請求

stream, err := sess.AcceptStream(context.Background())

在剛才的連接請求上創建新的接收流

_, err = stream.Read(buf)

_, err = stream.Write([]byte(serverMessage))

發送和接收數據

完整代碼在附錄

3.4 QUIC通信總結

因為QUIC是在UDP的基礎上實現的,所以大部分與UDP的機制相同,下面是我根據自己理解畫的一張圖

可以看到相較於傳統的UDP協議,還是有很多類似之處的

4. QUIC實現源碼剖析

4.1 客戶端分析

4.1.1 DialAddr

我們從客戶端開始分析:

session, err := quic.DialAddr(addr, tlsConf, nil)

進入DialAddr函數進行查看

func DialAddr(addr string,tlsConf *tls.Config,config *Config,
) (Session, error) {
	return DialAddrContext(context.Background(), addr, tlsConf, config)
}

先來看函數的參數列表:

addr表示服務端的地址,tlsConf表示tls的配置,最后一個config表示QUIC的配置,當填入nil的時候將使用默認配置。

我們來看看config *Config常用的一些選項:

  • HandshakeIdleTimeout:握手延遲
  • MaxIdleTimeout:雙方沒有發送消息的最大時間,超過這個時間則斷開
  • AcceptToken:令牌接收
  • MaxReceiveStreamFlowControlWindow:最大的接收流控制窗口(針對Stream)
  • MaxReceiveConnectionFlowControlWindow:最大的針對連接的可接收的數據窗口(針對一個Connection可以有多少最大的數據窗口)
  • MaxIncomingStreams:一個連接最大有多少Stream

接着看函數的返回值(Session, error)

  • error不必多少,是對於一系列錯誤的管理
  • Session:根據代碼注釋,Session是一個在兩個端點之間的connection即連接。

繼續深入源代碼,我們知道DialAddr只是一個封裝函數,我們繼續向下追溯DialAddrContext,

func DialAddrContext(ctx context.Context,addr string,tlsConf *tls.Config,config *Config,
) (Session, error) {
	return dialAddrContext(ctx, addr, tlsConf, config, false)
}

唯一的不同在於多加了一個context.Context類型,這是什么東西呢?

上下文context.Context是用來設置截止日期、同步信號,傳遞請求相關值的結構體。

我們可以把它理解為一個同步信號,即對信號同步以減少資源浪費。

我們常用的context.Background()返回一個預定義的類型。因為不是協議的重點,所以我們簡略看一下。

我們發現DialAddrContext依然只是一個包裝函數,我們繼續向下追溯DialAddrContext

func dialAddrContext( ctx context.Context,addr string,tlsConf *tls.Config,config *Config,use0RTT bool,
) (quicSession, error) {
 udpAddr, err := net.ResolveUDPAddr("udp", addr)
//...
 udpConn, err := net.ListenUDP("udp", &net.UDPAddr{IP: net.IPv4zero, Port: 0})
 //...
 return dialContext(ctx, udpConn, udpAddr, addr, tlsConf, config, use0RTT, true)
}

到這里就比較清晰了,因為QUIC基於udp,先調用net.ResolveUDPAddr("udp", addr),接着在UDP上監聽net.ListenUDP("udp", &net.UDPAddr{IP: net.IPv4zero, Port: 0}),最后根據返回的結果調用dialContext函數統一處理。

繼續看dialContext函數干了什么,在函數里我們可以找到關鍵的代碼行

//...
packetHandlers, err := getMultiplexer().AddConn(pconn, config.ConnectionIDLength, config.StatelessResetKey, config.Tracer)
//...
c, err := newClient(pconn, remoteAddr, config, tlsConf, host, use0RTT, createdPacketConn)

getMultiplexer建立一個多路復用,在前面我們分析原理的時候提到了多路復用(multiplexing)。接着返回一個新的客戶端結構newClient

至此,我們終於分析完了DialAddr的調用路徑。

4.1.2 session.OpenStreamSync

stream, err := session.OpenStreamSync(context.Background())

OpenStreamSync:打開一個新的雙向的QUIC Stream

追蹤這個函數,我們找到一個函數OpenStreamSync,這個函數里面是一些處理多線程的語句,在最后,有一個openStream,下面列出這個函數

func (m *outgoingBidiStreamsMap) openStream() streamI {
	s := m.newStream(m.nextStream)
	m.streams[m.nextStream] = s
	m.nextStream++
	return s
}

不要忘記,我們的Session是一個connection,它包含多個Stream,這個函數就是新建一個流,然后加入這個Session,我們繼續查看newStream,它返回一個新創建的流(Stream),流也是一個結構體.

type stream struct {
	receiveStream
	sendStream

	completedMutex         sync.Mutex
	sender                 streamSender
	receiveStreamCompleted bool
	sendStreamCompleted    bool

	version protocol.VersionNumber
}

流里面有接收流receiveStream,發送流sendStream,以及同步用的鎖completedMutex

我們可以看看接受流里面有什么:

  • StreamID:流ID
  • io.Reader:讀接口
  • CancelRead:是否禁止接收流
  • SetReadDeadline:讀超時設置

再看看發送流:

  • StreamID:流ID
  • io.Write:寫接口
  • CancelWrite:是否禁止寫
  • Context:上面提到過的用來同步的結構體
  • SetWriteDeadline:設置寫超時

到這里,我們就分析完了OpenStreamSync調用路徑,最后返回一個流

4.1.3 Read

我們再來看看Stream的讀(接收)操作

_, err = stream.Read(buf)

找到具體的Read函數

func (s *receiveStream) Read(p []byte) (int, error) {
	s.mutex.Lock()
	completed, n, err := s.readImpl(p)
	s.mutex.Unlock()
	//...
}

s就是我們基於的接收流的名字,發送具體的接收使用了readImpl函數

看看readImpl函數

if s.currentFrame == nil || s.readPosInFrame >= len(s.currentFrame) {
			s.dequeueNextFrame()
	}

看看還有沒有可用來讀的MaxReceiveStreamFlowControlWindow,有的話讀數據

copy(p[bytesRead:], s.currentFrame[s.readPosInFrame:])

把數據放到我們接收的地方,這里是從s目前的窗口復制到p

4.1.4 Write

接着看Stream的寫(發送)操作

找到Write函數,因為太長,下面分析一些關鍵的代碼

if s.canBufferStreamFrame() && len(s.dataForWriting) > 0{
  //...
}

先檢查還能不能發送

copy(s.nextFrame.Data[l:], s.dataForWriting)

接着講發送的數據放到Frame里面,調用底層發送出去

4.2 服務端分析

4.2.1 ListenAddr

listener, err := quic.ListenAddr(addr, generateTLSConfig(), nil)

同樣的,進入函數具體查看

func ListenAddr(addr string, tlsConf *tls.Config, config *Config) (Listener, error) {
	return listenAddr(addr, tlsConf, config, false)
}

剛才我們在客戶端分析過addr,tls,config的意義,這里不再贅述

繼續進入listenAddr

func listenAddr(addr string, tlsConf *tls.Config, config *Config, acceptEarly bool) (*baseServer, error) {
	udpAddr, err := net.ResolveUDPAddr("udp", addr)
  
	conn, err := net.ListenUDP("udp", udpAddr)

	serv, err := listen(conn, tlsConf, config, acceptEarly)
  
	return serv, nil
}

前兩個函數與客戶端差不多,我們來看看listen函數發生了什么

這個函數返回一個baseServer,這是一個QUIClistener,它是一個數據結構

主要添加了如下的結構體成員

  • sessionQueue:一個客戶端的Session隊列
  • sessionQueueLen:客戶端的Session隊列的長度

接着新建一個線程,不斷地監聽端口,等待一個新的客戶端連接請求

4.2.2 Accept

sess, err := listener.Accept(context.Background())

看看Accept源代碼

func (s *baseServer) Accept(ctx context.Context) (Session, error) {
	return s.accept(ctx)
}

繼續查看accept,關鍵代碼為

atomic.AddInt32(&s.sessionQueueLen, -1)

當收到一個新的請求的時候添加到sessionQueue中,返回一個客戶端Session,用這個Session可以和客戶端進行發送和接收數據

服務端的WriteRead不再分析,同樣和客戶端一樣使用Stream進行發送和接收

5. 與傳統的TCP比較與應用前景

5.1 QUIC性能分析

業界應用情況:

● 騰訊QQ應用情況

● 微博移動端全面支持QUIC協議

5.2 展望

雖然QUIC相比於以前的通信協議有更大的進步,能具有更低的延遲和更好的安全性,但應用落地依然還具有一段距離

  • QUIC現在仍然是草案,雖然ChormiumQUIC-GO是兩個已經落地使用的協議,但距離大規模應用仍然具有距離
  • 由於歷年的潛規則, 很多路由器對於UDP數據包直接丟棄
  • 網絡服務商對UDP持消極態度
  • 硬件的更新是遙遙無期的問題

但基於QUIC的優勢,期待着QUIC正式成為互聯網標准,並且大規模應用落地

我只是簡單的分析了一下QUIC協議,受限於個人的水平,文章依然還有很多不足之處,請多多包涵

參考資料

[1]. https://en.wikipedia.org/wiki/QUIC

[2]. https://blog.csdn.net/chenhaifeng2016/article/details/79011059

[3]. https://docs.google.com/document/d/1gY9-YNDNAB1eip-RTPbqphgySwSNSDHLq9D5Bty4FSU/edit

[4]. https://zhuanlan.zhihu.com/p/44980381

[5]. https://tools.ietf.org/html/draft-ietf-quic-transport-20#page-23

[6]. https://tools.ietf.org/html/draft-ietf-quic-recovery-20#page-4

[7]. https://zhuanlan.zhihu.com/p/32553477

[8]. https://tools.ietf.org/html/draft-ietf-quic-transport-20#ref-HTTP2

[9]. https://github.com/lucas-clemente/quic-go

[10]. https://www.zhihu.com/question/30519570/answer/1400925045

附錄

實現QUIC客戶端和服務端:Go語言

package main

import (
	"context"
	"crypto/rand"
	"crypto/rsa"
	"crypto/tls"
	"crypto/x509"
	"encoding/pem"
	"fmt"
	"github.com/lucas-clemente/quic-go"
	"math/big"
	"time"
)

const addr = "localhost:4242"

const clientMessage = "Hello"
const serverMessage = "Hi"

func main() {
	go func() {
		err := server()
		if err != nil {
			panic(err)
		}
	}()
	err := client()
	if err != nil {
		panic(err)
	}

	// 等待main和go程 執行完,防止server執行完自動結束
	time.Sleep(time.Second * 5)
}

// 客戶端
func client() error {
	tlsConf := &tls.Config{
		InsecureSkipVerify: true,
		NextProtos:         []string{"quic-echo-example"},
	}

	session, err := quic.DialAddr(addr, tlsConf, nil)
	if err != nil {
		return err
	}

	stream, err := session.OpenStreamSync(context.Background())
	if err != nil {
		return err
	}

	fmt.Printf("Client: Sending '%s'\n", clientMessage)
	_, err = stream.Write([]byte(clientMessage))
	if err != nil {
		return err
	}

	buf := make([]byte, 1024)
	_, err = stream.Read(buf)
	if err != nil {
		return err
	}
	fmt.Printf("Client: Got '%s'\n", buf)

	return nil
}

// 服務端
func server() error {
	listener, err := quic.ListenAddr(addr, generateTLSConfig(), nil)
	if err != nil {
		return err
	}
	sess, err := listener.Accept(context.Background())
	if err != nil {
		return err
	}
	stream, err := sess.AcceptStream(context.Background())
	if err != nil {
		panic(err)
	}

	buf := make([]byte, 1024)
	_, err = stream.Read(buf)
	if err != nil {
		return err
	}
	fmt.Printf("Server: Got '%s'\n", buf)

	fmt.Printf("Server: Sending '%s'\n", serverMessage)
	_, err = stream.Write([]byte(serverMessage))
	return err
}

func generateTLSConfig() *tls.Config {
	key, err := rsa.GenerateKey(rand.Reader, 1024)
	if err != nil {
		panic(err)
	}
	template := x509.Certificate{SerialNumber: big.NewInt(1)}
	certDER, err := x509.CreateCertificate(rand.Reader, &template, &template, &key.PublicKey, key)
	if err != nil {
		panic(err)
	}
	keyPEM := pem.EncodeToMemory(&pem.Block{Type: "RSA PRIVATE KEY", Bytes: x509.MarshalPKCS1PrivateKey(key)})
	certPEM := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: certDER})

	tlsCert, err := tls.X509KeyPair(certPEM, keyPEM)
	if err != nil {
		panic(err)
	}
	return &tls.Config{
		Certificates: []tls.Certificate{tlsCert},
		NextProtos:   []string{"quic-echo-example"},
	}
}



免責聲明!

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



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