造輪子系列之http協議

作為一個程序猿,對造輪子這事情可以說是情有獨鍾,幾乎程序猿內心都存在一個夢想是去將開源的技術都實現一遍,所有從本篇開始,我會開一個造輪子系列。



前言

首先,看看這個,想必大家對下面這種簡歷看得比較多了吧?

  • 精通JAVA,Python,熟練掌握C++
  • 精通Redis,Memcached,Mysql
  • 精通Nginx配置,模塊開發
  • 精通Kafka,ActiveMQ 等消息隊列
  • 精通常用數據結構和算法
  • 精通網絡編程,多線程編程技術,高性能服務器技術
  • 精通tcp/ip協議棧,熟悉內核網絡子系統代碼
  • 精通nginx代碼及模塊開發

上面每一條都涉及好多輪子,每一個都是精通,如果真能做到。那這個人可以說是碼農中的戰鬥機。

那我們現在目標就是去做這個戰鬥機。而這個方法,就是自己去造輪子,造的目的不是為了在項目中使用自己造的輪子,而是為了去了解輪子的構造,然後自己動手去體會造輪子的過程。


後端的輪子們

說起後端的輪子們,大家都可以說出一大串來,我們大致來數一數啊。

  • 抗在最前面的:LVS,F5,HAProxy這類負載均衡
  • 接下來有Nginx,Apache,Lighttpd這類Http服務
  • http服務後則是各種容器,部署著我們的業務邏輯
  • 存儲這邊有Redis,Memcached這一類KV存儲器和緩存系統
  • 如果是多機部署,肯定還有Kafka,ActiveMQ這種負責解耦的消息隊列
  • 為了實現集群通信,肯定少不了Thrift這種RPC框架和Protobuf這種序列化技術
  • 再高端點,到了分佈式領域了,就是更多的輪子了。。zookeeper、raft等等
  • 還有大數據系列hadoop。spark。。。。。

本文主要講http協議。

正文分割線


我們都知道http是基於tcp之上的,那我們現在就自己基於tcp來實現一個最小的http服務,功能非常簡單:

  • 返回輸入參數

先來看請求格式:

造輪子系列之http協議

http的報文大概分為3部分:

  • 請求行
  • 首部
  • 正文部分

此處請求行是格式是固定的,

先寫代碼來看看的:

造輪子系列之http協議

造輪子系列之http協議

可以看到我們讀取的到數據是如上,我們可以看到格式上是符合的。

Ps:上面這個代碼有個小問題,因為tcp連接是字節流的,我們通過readAll方法從連接中讀取數據的是,只要瀏覽器上不主動斷開,會一直阻塞在readaALL上。。。

上面我們將收到的數據稍微整理下

  • 請求行
  • POST / HPPP/1.1\r\n
  • 格式:方法 space URL space 版本 cr lf
  • 首部
  • Host: 127.0.0.1:8080
  • Content-length: 0
  • 格式:首部字段名 space 字段值 cr lf
  • 正文部分
  • 此處為空

上面首部中Content-length: 0可以說是非常關鍵,他告訴了我們應該要在兩個\r\n後繼續讀取多少字節。

下面我們開始來寫解析代碼,先是解析文件頭

造輪子系列之http協議

然後我們再解析首部

造輪子系列之http協議

解析完後,我們在寫返回值,返回報文的格式放下:

造輪子系列之http協議

下面是返回值的代碼:

造輪子系列之http協議

完整的例子可以看GitHub上代碼,歡迎star

https://github.com/zhuanxuhit/go-in-practice/tree/master/wheel/http/v1

我們有了第一版http輪子後,我們能和前面介紹的輪子系列:rpc聯繫起來,在rpc系列中,我們講了設計通信協議來傳遞消息,此處http是通過頭部的url+method的方法來表示我要調用服務端哪個方法,然後分割符是使用 \r\n

,連續兩個\r\n表示後續是消息體,為了高速我們消息體的大小和格式,在header中必須指明content-type和content-length,這些都是在我們在實現http協議的時候遵循的。

那現在寫完最初版代碼,我們回過頭總結下我們之前做的rpc輪子,數據編碼採用了protobuf,然後基於tcp自己定義了一套消息協議,其實做的事情跟http/1.1是一樣的,我們完全可以在http通信的時候,將content-type設置為protobuf,然後通信雙方雙方能夠編解碼即可。

在實現過程中,我們發現如果用http1.1作為通信協議,有什麼問題呢?

  1. 每次傳輸都要完整的http頭,浪費帶寬
  2. 每次一個http請求,一個request,response都要獨佔一條tcp連接,不然不知道response對應哪個request,影響實時性和併發性

那上面這兩點都是要解決的問題,在http2.0中都有相應的方案

  1. 針對每次都需要傳輸http頭,通信雙方建立索引,後續傳輸只用索引
  2. 針對連接佔用,一條連接只能同時有一個請求-響應,http2.0啟動了多路複用,即允許一個連接同時發起多個請求

那怎麼能做到一個連接同時發起多個請求呢?通信雙方就必須對每個請求進行編碼,這樣不同的響應就能和請求對應上了。

具體可以看兩張圖:

造輪子系列之http協議

HTTP 2.0 其實是將三個請求變成三個流,將數據分成幀,亂序發送到一個tcp連接中

造輪子系列之http協議

通過stream對不同請求進行區分,然後在將一個消息拆分為多個幀進行發送。

那http2.0後,還能不能更快了呢?於是就有了QUIC協議,這個協議肯定是為了解決http2.0的某些問題的。

  1. 自定義連接機制:tcp連接三次握手慢,由於在移動端,由於網絡從wifi到移動網絡切換時,必定會導致連接斷開重連,再次需要3次握手,那我們就自定義連接機制,原先tcp一條連接是由4元素組成:分別是源 IP、源端口、目的 IP、目的端口,現在以一個64位隨機數來作為連接標誌,斷開了也沒事,重新建立連接不需要3次握手了。
  2. 自定義重傳機制:tcp是可靠連接,當前面的數據編號沒有收到的時候,後面的數據即使收到了,也不會得到確認,這就必須要重傳
造輪子系列之http協議

重傳有個測不準問題,左邊是1.1,我們發現重發100編號的時候,如果後續收到應答101,我們不知道這個是針對第一次100的應答還是第二次重傳100的應答,http2.0則定義了每次發送數據,編號都需要增加,然後通過offset來標明數據的前後續關係。

  1. 無阻塞多路複用:因為tcp是面向字節流的可靠連接,所以數據之間是有依賴的,因此為了減少依賴,讓不同流之間真的能夠獨立,可以採用udp
  2. 自定義流量控制:tcp的流量控制是通過滑動窗口協議,udp也是滑動窗口,而且是每個stream都有自己的窗口。

總結

首先本文基於tcp自己實現了http1.1的協議,實現中發現這個通信協議和我們之前輪子系列文章rpc都是消息協議,只是對消息體的編碼格式不同而已。

接著我們在自己寫的過程中發現了http1.1的種種問題,針對這些問題有了http2.0,繼而又有了QUIC。

預告:今天講完http2.0後,我會接著講輪子系列:gRpc,這個通信協議使用就是http2.0,歡迎大家關注。

Ps:文章最後關於http2.0和quic的內容主要來自極客時間的趣談網絡協議,寫的真的非常好,大家可以去訂閱的,當然通過我的邀請碼可以有返現的,歡迎加wx: hithangtian

源碼地址:https://github.com/zhuanxuhit/go-in-practice/tree/master/wheel/http/v1


分享到:


相關文章: