28 張圖,搞懂 TCP

來源:https://juejin.cn/user/3931509313252552/postse

做 IT 相關的工作,肯定都離不開網絡,網絡中最重要的協議是 TCP。無論是實際工作還是筆試面試,你看哪裏能少得了 TCP?

我看過 RFC 中與 TCP 相關的文檔,也看過 linux 中 TCP 相關的源碼,也看過不少框架中的 TCP 相關的代碼,對 TCP 是有點感覺了。

一直想找個時間來分享下 TCP 相關的知識,如果大家有疑問,歡迎相互交流。其實,搞透了 TCP 之後,發現它也就那麼回事。

考慮最簡單的情況:兩臺主機之間的通信。這個時候只需要一條網線把兩者連起來,規定好彼此的硬件接口,如都用 USB、電壓 10v、頻率 2.4GHz 等,這一層就是物理層,這些規定就是物理層協議 。

我們當然不滿足於只有兩臺電腦連接,因此我們可以使用交換機把多個電腦連接起來,如下圖:

這樣連接起來的網絡,稱爲局域網,也可以稱爲以太網(以太網是局域網的一種)。在這個網絡中,我們需要標識每個機器,這樣纔可以指定要和哪個機器通信。這個標識就是硬件地址 MAC。硬件地址隨機器的生產就被確定,永久性唯一。在局域網中,我們需要和另外的機器通信時,只需要知道他的硬件地址,交換機就會把我們的消息發送到對應的機器。

這裏我們可以不管底層的網線接口如何發送,把物理層抽離,在他之上創建一個新的層次,這就是數據鏈路層 。

我們依然不滿足於局域網的規模,需要把所有的局域網聯繫起來,這個時候就需要用到路由器來連接兩個局域網:

但是如果我們還是使用硬件地址來作爲通信對象的唯一標識,那麼當網絡規模越來越大,需要記住所有機器的硬件地址是不現實的;同時,一個網絡對象可能會頻繁更換設備,這個時候硬件地址表維護起來更加複雜。這裏使用了一個新的地址來標記一個網絡對象:IP 地址 。

通過一個簡單的寄信例子來理解 IP 地址。

我住在北京市,我朋友 A 住在上海市,我要給朋友 A 寫信:

  1. 寫完信,我會在信上寫好我朋友 A 的地址,並放到北京市郵局(給信息附加目標 IP 地址,併發送給路由器)

  2. 郵局會幫我把信運輸到上海市當地郵局(信息會經過路由傳遞到目標 IP 局域網的路由器)

  3. 上海市當地路由器會幫我把信交給朋友 A(局域網內通信)

因此,這裏 IP 地址就是一個網絡接入地址(朋友 A 的住址),我只需要知道目標 IP 地址,路由器就可以把消息給我帶到。在局域網中,就可以動態維護一個 MAC 地址與 IP 地址的映射關係,根據目的 IP 地址就可以尋找到機器的 MAC 地址進行發送 。

這樣我們不需管理底層如何去選擇機器,我們只需要知道 IP 地址,就可以和我們的目標進行通信。這一層就是網絡層。網絡層的核心作用就是 提供主機之間的邏輯通信 。這樣,在網絡中的所有主機,在邏輯上都連接起來了,上層只需要提供目標 IP 地址和數據,網絡層就可以把消息發送到對應的主機。

一個主機有多個進程,進程之間進行不同的網絡通信,如邊和朋友開黑邊和女朋友聊微信。我的手機同時和兩個不同機器進行通信。那麼當我的手機收到數據時,如何區分是微信的數據,還是王者的數據?那麼就必須在網絡層之上再添加一層:**運輸層 **:

運輸層通過 socket(套接字),將網絡信息進行進一步的拆分,不同的應用進程可以獨立進行網絡請求,互不干擾。這就是運輸層的最本質特點:提供進程之間的邏輯通信 。這裏的進程可以是主機之間,也可以是同個主機,所以在 android 中,socket 通信也是進程通信的一種方式。

現在不同的機器上的應用進程之間可以獨立通信了,那麼我們就可以在計算機網絡上開發出形形式式的應用:如 web 網頁的 http,文件傳輸 ftp 等等。這一層稱爲應用層

應用層還可以進一步拆分出表示層、會話層,但他們的本質特點都沒有改變:**完成具體的業務需求 **。和下面的四層相比,他們並不是必須的,可以歸屬到應用層中。

最後對計網分層進行小結:

  1. 最底層物理層,負責兩個機器之間通過硬件的直接通信;

  2. 數據鏈路層使用硬件地址在局域網中進行尋址,實現局域網通信;

  3. 網絡層通過抽象 IP 地址實現主機之間的邏輯通信;

  4. 運輸層在網絡層的基礎上,對數據進行拆分,實現應用進程的獨立網絡通信;

  5. 應用層在運輸層的基礎上,根據具體的需求開發形形式式的功能。

這裏需要注意的是,分層並不是在物理上的分層,而是邏輯上的分層。通過對底層邏輯的封裝,使得上層的開發可以直接依賴底層的功能而無需理會具體的實現,簡便了開發。

這種分層的思路,也就是責任鏈設計模式,通過層層封裝,把不同的職責獨立起來,更加方便開發、維護等等。okHttp 中的攔截器設計模式,也是這種責任鏈模式。

/   運輸層   /

本文主要是講解 TCP,這裏需要增加一些運輸層的知識。

本質:提供進程通信

在運輸層之下的網絡層,是不知道該數據包屬於哪個進程,他只負責數據包的接收與發送。運輸層則負責接收不同進程的數據交給網絡層,同時把網絡層的數據拆分交給不同的進程。從上往下匯聚到網絡層,稱爲多路複用,從下往上拆分,稱爲多路拆分 。

運輸層的表現,受網絡層的限制。這很好理解,網絡層是運輸層的底層支持。所以運輸層是無法決定自己帶寬、時延等的上限。但可以基於網絡層開發更多的特性:如可靠傳輸。網絡層只負責盡力把數據包從一端發送到另一端,而不保證數據可以到達且完整。

底層實現:socket

前面講到,最簡單的運輸層協議,就是提供進程之間的獨立通信 ,但底層的實現,是 socket 之間的獨立通信 。在網絡層中,IP 地址是一個主機邏輯地址,而在運輸層中,socket 是一個進程的邏輯地址;當然,一個進程可以擁有多個 socket。應用進程可以通過監聽 socket,來獲取這個 socket 接受到的消息。

socket 並不是一個實實在在的東西,而是運輸層抽象出來的一個對象。運輸層增加了端口這個概念,來區分不同的 socket。端口可以理解爲一個主機上有很多的網絡通信口,每個端口都有一個端口號,端口的數量由運輸層協議確定。

不同的運輸層協議對 socket 有不同的定義方式。在 UDP 協議中,使用目標 IP + 目標端口號來定義一個 socket;在 TCP 中使用目標 IP + 目標端口號 + 源 IP + 源端口號來定義一個 socket。我們只需要在運輸層報文的頭部附加上這些信息,目標主機就會知道我們要發送給哪個 socket,對應監聽該 socket 的進程就可獲得信息。

運輸層協議

運輸層的協議就是大名鼎鼎的 TCP 和 UDP。其中,UDP 是最精簡的運輸層協議,只實現了進程間的通信;而 TCP 在 UDP 的基礎上,實現了可靠傳輸、流量控制、擁塞控制、面向連接等等特性,同時也更加複雜。

當然除此之外,還有更多更優秀的運輸層協議,但目前廣爲使用的,就是 TCP 和 UDP。UDP 在後面也會總結到。

/   TCP 協議首部   /

TCP 協議,表現在報文上,就是會在應用層傳輸下來的數據前附加上一個 TCP 首部,這個首部附加了 TCP 信息,先來整體看一下這個首部的結構:

這張圖是來自一位大學老師的課件, 非常好用,所以一直拿來學習。最下面部分表示了報文之間的關係,TCP 數據部分就是應用層傳下來的數據。

TCP 首部固定長度是 20 字節,下面還有 4 字節是可選的。內容很多,但其中有一些我們比較熟悉的:源端口,目標端口。嗯?socket 不是還需要 IP 進行定位嗎?IP 地址在網絡層被附加了。其他的內容後面都會慢慢講解,作爲一篇總結文章,這裏放出查閱表,方便複習:

選項字段中包含以下其他選項:

講完下面內容,再回來看這些字段就熟悉了。

/   TCP 面向字節流特性   /

TCP 並不是把應用層傳輸過來的數據直接加上首部然後發送給目標,而是把數據看成一個字節 流,給他們標上序號之後分部分發送。這就是 TCP 的 面向字節流 特性:

面向字節流的好處是無需一次存儲過大的數據佔用太多內存,壞處是無法知道這些字節代表的意義,例如應用層發送一個音頻文件和一個文本文件,對於 TCP 來說就是一串字節流,沒有意義可言,這會導致粘包以及拆包問題,後面講。

/   可靠傳輸原理   /

前面講到,TCP 是可靠傳輸協議,也就是,一個數據交給他,他肯定可以完整無誤地發送到目標地址,除非網絡炸了。他實現的網絡模型如下:

對於應用層來說,他就是一個可靠傳輸的底層支持服務;而運輸層底層採用了網絡層的不可靠傳輸。雖然在網絡層甚至數據鏈路層就可以使用協議來保證數據傳輸的可靠性,但這樣網絡的設計會更加複雜、效率會隨之降低。把數據傳輸的可靠性保證放在運輸層,會更加合適。

可靠傳輸原理的重點總結一下有:滑動窗口、超時重傳、累積確認、選擇確認、連續 ARQ 。

停止等待協議

要實現可靠傳輸,最簡便的方法就是:我發送一個數據包給你,然後你跟我回復收到,我繼續發送下一個數據包。傳輸模型如下:

這種 “一來一去” 的方法來保證傳輸可靠就是停止等待協議(stop-and-wait)。不知道還記不記得前面 TCP 首部有一個 ack 字段,當他設置爲 1 的時候,表示這個報文是一個確認收到報文。

然後再來考慮一種情況:丟包。網絡環境不可靠,導致每一次發送的數據包可能會丟失,如果機器 A 發送了數據包丟失了,那麼機器 B 永遠接收不到數據,機器 A 永遠在等待。解決這個問題的方法是:超時重傳 。當機器 A 發出一個數據包時便開始計時,時間到還沒收到確認回覆,就可以認爲是發生了丟包,便再次發送,也就是重傳。

但重傳會導致另一種問題:如果原先的數據包並沒有丟失,只是在網絡中待的時間比較久,這個時候機器 B 會受到兩個數據包,那麼機器 B 是如何辨別這兩個數據包是屬於同一份數據還是不同的數據?這就需要前面講過的方法:給數據字節進行編號。這樣接收方就可以根據數據的字節編號,得出這些數據是接下來的數據,還是重傳的數據。

在 TCP 首部有兩個字段:序號和確認號,他們表示發送方數據第一個字節的編號,和接收方期待的下一份數據的第一個字節的編號。前面講到 TCP 是面向字節流,但是他並不是一個字節一個字節地發送,而是一次截取一整段。截取的長度受多種因素影響,如緩存區的數據大小、數據鏈路層限制的幀大小等。

連續 ARQ 協議

停止等待協議已經可以滿足可靠傳輸了,但有一個致命缺點:效率太低。發送方發送一個數據包之後便進入等待,這個期間並沒有幹任何事,浪費了資源。解決的方法是:連續發送數據包。模型如下:

和停止等待最大的不同就是,他會源源不斷地發送,接收方源源不斷收到數據之後,逐一進行確認回覆。這樣便極大地提高了效率。但同樣,帶來了一些額外的問題:

發送是否可以無限發送直到把緩衝區所有數據發送完?不可以。因爲需要考慮接收方緩衝區以及讀取數據的能力。如果發送太快導致接收方無法接受,那麼只是會頻繁進行重傳,浪費了網絡資源。所以發送方發送數據的範圍,需要考慮到接收方緩衝區的情況。這就是 TCP 的流量控制 。解決方法是:滑動窗口 。基本模型如下:

在 TCP 的首部有一個窗口大小字段,他表示接收方的剩餘緩衝區大小,讓發送方可以調整自己的發送窗口大小。通過滑動窗口,就可以實現 TCP 的流量控制,不至於發送太快,導致太多的數據丟失。

連續 ARQ 帶來的第二個問題是:網絡中充斥着和發送數據包一樣數據量的確認回覆報文,因爲每一個發送數據包,必須得有一個確認回覆。提高網絡效率的方法是:**累積確認 **。接收方不需要逐個進行回覆,而是累積到一定量的數據包之後,告訴發送方,在此數據包之前的數據全都收到。例如,收到 1234,接收方只需要告訴發送方我收到 4 了,那麼發送方就知道 1234 都收到了。

第三個問題是:如何處理丟包情況。在停止等待協議中很簡單,直接一個超時重傳就解決了。但,連續 ARQ 中不太一樣。例如:接收方收到了 123 567,六個字節,編號爲 4 的字節丟失了。按照累積確認的思路,只能發送 3 的確認回覆,567 都必須丟掉,因爲發送方會進行重傳。這就是 GBN(go-back-n) 思路。

但是我們會發現,只需要重傳 4 即可,這樣不是很浪費資源,所以就有了:選擇確認 SACK 。在 TCP 報文的選項字段,可以設置已經收到的報文段,每一個報文段需要兩個邊界來進行確定。這樣發送方,就可以根據這個選項字段只重傳丟失的數據了。

可靠傳輸小結

到這裏關於 TCP 的可靠傳輸原理就已經介紹的差不多。最後進行一個小結:

當然,這只是可靠傳輸的冰山一角,感興趣可以再深入去研究(和麪試官聊天已經差不多了 [狗頭])。

/   擁塞控制   /

擁塞控制考慮的是另外一個問題:避免網絡過分擁擠導致丟包嚴重,網絡效率降低 。

拿現實的交通舉例子:

高速公路同一時間可通行的汽車數量是一定的,當節假日時,就會發生嚴重的堵車。在 TCP 中,數據包超時,會進行重傳,也就是會進來更多的汽車,這時候更堵,最後導致的結果就是:丟包 - 重傳 - 丟包 - 重傳。最後整個網絡癱瘓了。

這裏的擁塞控制和前面的流量控制不是一個東西,流量控制是擁塞控制的手段:爲了避免擁塞,必須對流量進行控制。擁塞控制目的是:限制每個主機的發送的數據量,避免網絡擁塞效率下降。就像廣州等地,限制車牌號出行是一個道理。不然大家都堵在路上,誰都別想走。

擁塞控制的解決方法是流量控制,流量控制的實現是滑動窗口,所以**擁塞控制最終也是通過限制發送方的滑動窗口大小來限制流量 **。當然,擁塞控制的手段不只是流量控制,導致擁塞的因素有:路由器緩存、帶寬、處理器處理速度等等。提升硬件能力(把 4 車道改成 8 車道)是其中一個方法,但畢竟硬件提升是有瓶頸的,沒辦法不斷提升,還是需要從 tcp 本身來增加算法,解決擁塞。

擁塞控制的重點有 4 個:慢開始、快恢復、快重傳、擁塞避免。這裏依舊獻祭出大學老師的 ppt 圖片:

Y 軸表示的是發送方窗口大小,X 軸表示的是發送的輪次(不是字節編號)。

通過這個算法,就可以在很大程度上,避免網絡擁擠。

除此之外,還可以讓路由器在緩存即將滿的時候,告知發送方我快滿了,而不是等到出現了超時再進行處理,這是主動隊列管理 AQM。此外還有很多方法,但是上面的算法是重點。

/   面向連接   /

這一小節講的就是無人不曉的 TCP 三次握手與四次揮手這些,經過前面的內容,這一小節其實已經很好理解。

TCP 是面向連接的,那連接是什麼?這裏的連接並不是實實在在的連接,而是通信雙方彼此之間的一個記錄 。TCP 是一個全雙工通信,也就是可以互相發送數據,所以雙方都需要記錄對方的信息。根據前面的可靠傳輸原理,TCP 通信雙方需要爲對方準備一個接收緩衝區可以接收對方的數據、記住對方的 socket 知道怎麼發送數據、記住對方的緩衝區來調整自己的窗口大小等等,這些記錄,就是一個連接。

在運輸層小節中講到,運輸層雙方通信的地址是採用 socket 來定義的,TCP 也不例外。TCP 的每一個連接只能有兩個對象,也就是兩個 socket,而不能有三個。所以 socket 的定義需要源 IP、源端口號、目標 IP、目標端口號四個關鍵因素,纔不會發生混亂。

假如 TCP 和 UDP 一樣只採用目標 IP + 目標端口號來定義 socket,那麼就會出現多個發送方同時發送到同一個目標 socket 的情況。這個時候 TCP 無法區分這些數據是否來自不同的發送方,就會導致出現錯誤。

既然是連接,就有兩個關鍵要點:建立連接、斷開連接。

建立連接

建立連接的目的就是交換彼此的信息,然後記住對方的信息。所以雙方都需要發送彼此的信息給對方:

但前面的可靠傳輸原理告訴我們,數據在網絡中傳輸是不可靠的,需要對方給予我們一個確認回覆,纔可以保證消息正確到達。如下圖:

機器 B 的確認收到和機器 B 信息可以進行合併,減少次數;而且發送機器 B 給機器 A 本身就代表了機器 B 已經收到了消息,所以最後的示例圖是:

步驟如下:

  1. 機器 A 發送 syn 包向機器 B 請求建立 TCP 連接,並附加上自身的接收緩衝區信息等,機器 A 進入 SYN_SEND 狀態,表示請求已經發送正在等待回覆;

  2. 機器 B 收到請求之後,根據機器 A 的信息記錄下來,並創建自身的接收緩存區,向機器 A 發送 syn+ack 的合成包,同時自身進入 SYN_RECV 狀態,表示已經準備好了,等待機器 A 的回覆就可以向 A 發送數據;

  3. 機器 A 收到回覆之後記錄機器 B 的信息,發送 ack 信息,自身進入 ESTABLISHED 狀態,表示已經完全準備好了,可以進行發送和接收;

  4. 機器 B 收到 ACK 數據之後,進入 ESTABLISHED 狀態。

三次消息的發送,稱爲三次握手。

斷開連接

斷開連接和三次握手類似,直接上圖:

  1. 機器 A 發送完數據之後,向機器 B 請求斷開連接,自身進入 FIN_WAIT_1 狀態,表示數據發送完成且已經發送 FIN 包(FIN 標誌位爲 1);

  2. 機器 B 收到 FIN 包之後,回覆 ack 包表示已經收到,但此時機器 B 可能還有數據沒發送完成,自身進入 CLOSE_WAIT 狀態,表示對方已發送完成且請求關閉連接,自身發送完成之後可以關閉連接;

  3. 機器 B 數據發送完成之後,發送 FIN 包給機器 B ,自身進入 LAST_ACK 狀態,表示等待一個 ACK 包即可關閉連接;

  4. 機器 A 收到 FIN 包之後,知道機器 B 也發送完成了,回覆一個 ACK 包,並進入 TIME_WAIT 狀態

TIME_WAIT 狀態比較特殊。當機器 A 收到機器 B 的 FIN 包時,理想狀態下,確實是可以直接關閉連接了;但是:

  1. 我們知道網絡是不穩定的,可能機器 B 發送了一些數據還沒到達(比 FIN 包慢);

  2. 同時回覆的 ACK 包可能丟失了,機器 B 會重傳 FIN 包;

如果此時機器 A 馬上關閉連接,會導致數據不完整、機器 B 無法釋放連接等問題。所以此時機器 A 需要等待 2 個報文生存最大時長,確保網絡中沒有任何遺留報文了,再關閉連接

  1. 最後,機器 A 等待兩個報文存活最大時長之後,機器 B 接收到 ACK 報文之後,均關閉連接,進入 CLASED 狀態

雙方之間 4 次互相發送報文來斷開連接的過程,就是四次揮手。

現在,對於爲什麼握手是三次揮手是四次、一定要三次 / 四次嗎、爲什麼要停留 2msl 再關閉連接等等這些問題,就都解決了。

/   UDP 協議   /

運輸層協議除了 TCP,還有大名鼎鼎的 UDP。如果說 TCP 憑藉他完善穩定的功能獨樹一幟,那 UDP 就是精簡主義亂拳打死老師傅。

UDP 只實現了運輸層最少的功能:進程間通信。對於應用層傳下來的數據,UDP 只是附加一個首部就直接交給網絡層了。UDP 的頭部非常簡單,只有三部分:

所以 UDP 的功能也只有兩個:校驗數據報是否發生錯誤、區分不同的進程通信。

但,TCP 的功能雖然多,但同時也是要付出相對應的代價。例如面向連接的特性,在建立和斷開連接的時候會有開銷;擁塞控制的特性,會限制傳輸的上限等等。下面來羅列一下 UDP 的優缺點:

UDP 的缺點

UDP 的優點

UDP 適用場景

UDP 適用於對傳輸模型需要應用層高度自定義、允許出現丟包、需要高效率的場景、需要廣播;例如

/   其他補充   /

分塊傳輸

我們可以發現,運輸層在傳輸數據的時候,並不是把整個數據包加個首部直接發送過去,而是會拆分成多個報文分開發送;那他這樣做原因是什麼?

有讀者可能會想到:數據鏈路層限制了數據長度只能有 1460。那數據鏈路層爲什麼要這麼限制?他的本質原因就是:網絡是不穩定的。如果報文太長,那麼極有可能在傳輸一般的時候突然中斷了,這個時候就要整個數據重傳,效率就降低了。把數據拆分成多個數據報,那麼當某個數據報丟失,只需要重傳該數據報即可。

那是不是拆分得越細越好?報文中數據字段長度太低,會使得首部的佔比太大,這樣首部就會成爲網絡傳輸最大的負擔了。例如 1000 字節,每個報文首部是 40 字節,如果拆分成 10 個報文,那麼只需要傳輸 400 字節的首部;而如果拆分成 1000 個,那麼需要傳輸 40000 字節的首部,效率就極大地降低了。

路由轉換

先看下圖:

可以看出來,使用路由轉發的好處是:提高網絡的容錯率,本質原因依舊是網絡是不穩定的 。即使壞掉幾個路由器,網絡依舊暢通。但是如果壞掉路由器 6 那就直接導致主機 A 和主機 B 無法通信,所以要避免這種核心路由器的存在。

使用路由的好處還有:分流。如果一條線路太擁堵,可以從別的路線進行傳輸,提高效率。

粘包與拆包

在面向字節流那一小節講過,TCP 不懂這些數據流的意義,他只知道從應用層拿到數據流,切割成一份份報文,然後發送給目標對象。而如果應用層傳輸下來的是兩個數據包,那麼極有可能出現這種情況:

粘包與拆包都是應用層需要解決的問題,可以在每個文件的最後附加上一些特殊的字節,如換行符;或者控制每個報文只包含一個文件的數據,不足的用 0 補充等等。

惡意攻擊

TCP 的面向連接特點可能會被惡意的人利用,對服務器進行攻擊。

前面我們知道,當我們向一個主機發送 syn 包請求創建連接時,服務器會爲我們創建緩衝區等,然後向我們返回 syn+ack 報文;如果我們僞造 IP 和端口,向一個服務器進行海量的請求,會使得服務器創建了大量的創建一半的 TCP 連接,使得其無法正常響應用戶的請求,導致服務器癱瘓。

解決的方法可以有限制 IP 的創建連接數、讓創建一半的 tcp 連接在更短的時間內自行關閉、延緩接收緩衝區內存的分配等等。

長連接

我們向服務器的每一次請求都需要創建一個 TCP 連接,服務器返回數據之後就會關閉連接;如果在短時間內有大量的請求,那麼頻繁創建 TCP 連接關閉 TCP 連接是一個很浪費資源的行爲。所以我們可以讓 TCP 連接不要關閉,在這個期間進行請求,提高效率。

需要注意長連接維持時間、創建條件等,避免被惡意利用創建大量的長連接,消耗殆盡服務器的資源。

/   最後   /

以前學習的時候覺得這些東西好像沒什麼卵用,貌似就是用來考試的。事實上,在沒應用到的時候,對這些知識很難有更深層次的認知,例如現在我看上面的總結,很多隻是表面上的認知,不知道他背後代表的真正含義。

但當我學習的更加廣泛、深入,會對這些知識有越來越深刻的認識。有那麼幾個瞬間覺得:哦原來那個東西是這樣運用,那個東西是這樣的啊,原來學了是真的有用。

現在可能學了之後沒有什麼感覺,但是當用到或者學到相關的應用時,會有一個頓悟感,會瞬間收穫很多。

網絡技術平臺 混跡網絡行業 10 餘年,現爲世界 50 強金融科技公司網絡專家。分享計算機網絡知識,讓學習變得更有趣,讓網絡變得更簡單。

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/RtDWHsNld03-P1ydb75bFQ