來自公衆號: Gopher指北
作Web服務的時候,可能會有這樣一個業務場景,獲取一個HTTP請求的完整URL。很巧,老許就碰到了這樣的業務場景。面對如此簡單的需求,CV大法根本沒有展現才能的機會。啪啪啪,獲取請求的完整URL代碼就出來了。html
當時離驗證只差一步,老許信心滿滿,很快,打臉來得很快就像龍捲風。。。git
從圖中能夠知道,req.URL
中的Scheme
和Host
均爲空,因此r.URL.String()
沒法獲得完整的請求鏈接。這個結果讓老許一陣激動,萬萬沒想到有一天我也有機會發現Go源碼中可能遺漏的賦值。老許強行按耐住心中的激動,準備好好研究一番,萬一成爲了Go的Contributor呢^ω^。最後發現官方實現沒有問題,所以就有了今天這篇文章。github
HTTP1.1的Server讀取請求並構建Request.URL
對象的邏輯在request.go文件的readRequest
方法中,下面老許對其源碼作一個簡單分析總結。segmentfault
// First line: GET /index.html HTTP/1.0 var s string if s, err = tp.ReadLine(); err != nil { return nil, err }
req.Method
、req.RequestURI
和req.Proto
。var ok bool req.Method, req.RequestURI, req.Proto, ok = parseRequestLine(s)
req.RequestURI
解析爲req.URL
。rawurl := req.RequestURI if req.URL, err = url.ParseRequestURI(rawurl); err != nil { return nil, err }
注:當請求方法是CONNECT時,上述流程略有變化
經過上面的流程咱們知道req.URL
的數據來源爲req.RequestURI
,而req.RequestURI
究竟是什麼讓咱們繼續閱讀後文。服務器
根據rfc7230中的定義, 請求行分爲請求方法、請求資源和HTTP版本,分別對應上述的req.Method
、req.RequestURI
和req.Proto
(request-target在本文均被譯做請求資源)。ide
關於請求方法有哪些想必不用老許在這兒科普了吧。至於經常使用的HTTP版本無非就是HTTP1.1和HTTP2。 下面主要介紹請求資源的幾種形式。url
這種形式是請求資源中最多見的形式,其格式定義以下。spa
origin-form = absolute-path [ "?" query ]
當直接向服務器發起請求時,除開CONNECT和OPTIONS請求,只容許發送path和query做爲請求資源。若是請求連接的path爲空,則必須發送/
做爲請求資源。請求連接中的Host信息以Header頭的形式發送。翻譯
以http://www.example.org/where?q=now
爲例,請求行和Host請求頭信息以下代理
GET /where?q=now HTTP/1.1 Host: www.example.org
這種形式目前僅在向代理髮起請求時使用,其格式定義以下。
absolute-form = absolute-URI
根據rfc7230中的定義,目前client僅會向代理髮送這種形式的請求資源,但爲了未來某個HTTP版本可能會轉換爲這種形式的請求資源因此server須要支持這種形式的請求資源。這大概就是爲何req.URL
中大部分字段值爲空卻仍然將URL各部分定義完整的緣由。
一個absolute-form
形式的請求行例子以下。
GET http://www.example.org/pub/WWW/TheProject.html HTTP/1.1
authority-form
形式的請求資源僅用於CONNECT
請求中,其格式定義以下。
authority-form = authority
發送CONNECT
請求時,client只能發送URI的authority部分(不包含userinfo和@定界符)做爲請求資源。這樣講比較抽象, 咱們先來看看http-URI
的定義。
經過上面這張圖大概可以猜出來authority
應該是指Host信息。Very Good!你沒有猜錯!
The origin server for an "http" URI is identified by the authority component, which includes a host identifier and optional TCP port.
上面是rfc7230對於authority的解釋。老許根據本身的翻譯,在這裏單方面宣佈authority
包括主機標識符和可選的端口信息。一個authority-form
形式的請求行例子以下。
CONNECT www.example.com:80 HTTP/1.1
asterisk-form
形式的請求資源僅適用於OPTIONS
請求且只能爲*
,其格式定義以下。
asterisk-form = "*"
一個asterisk-form
形式的請求行例子以下。
OPTIONS * HTTP/1.1
對上面幾種形式的請求資源有所瞭解後,咱們再次回到獲取請求的完整URL這一問題自己。以最經常使用的absolute-form
爲例(其餘形式的請求資源咱們在開發中幾乎不用考慮),請求資源中自己就缺乏Host
和Scheme
信息,因此一行代碼天然沒法獲取請求的完整URL。難道咱們就沒法獲取到請求的完整URL嘛?固然不是,咱們還能夠經過如下兩種方案獲得完整的URL。
方案一:
req.Host
獲得Host相關信息。req.TLS == nil
則爲HTTP請求,不然爲HTTPS請求。方案二:
在配置文件中配置好服務的Host信息,獲取完整請求時只須要讀取配置文件並拼接req.RequestURI
便可。事實上老許採用的就是方案二,由於不少服務都在網關後面。當客戶端使用HTTPS請求網關,網關以HTTP請求服務時使用req.TLS == nil
判斷就不合理了。
須要注意的是在HTTP2中已經沒有請求行的概念了,取而代之的是請求僞標頭,這一點老許在Go發起HTTP2.0請求流程分析(後篇)——標頭壓縮這篇文章中提到過。
下圖爲一次HTTP2請求的部分Header信息。
從圖中能夠發現,HTTP1.1中的請求行已經沒有了。根據rfc7540中的定義,請求的僞標頭字段有:method
、:scheme
、:authority
和:path
。
:method
和:scheme
不須要老許多說,看英文單詞的意思就能夠了。
:authority
: 根據前文的解釋,其值爲主機標識符和可選的端口信息。另外須要注意的是HTTP2中沒有Host
請求頭。
:path
: 若是是OPTIONS
請求,則其值爲*
。其餘狀況該值爲請求URI的path和query,若是path爲空則其值爲/
。
在對HTTP2請求的僞標頭有了一個基本瞭解後,下面咱們來看一下Request.URL
的賦值過程。HTTP2的Server讀取請求並構建Request.URL
對象的邏輯在h2_bundle.go文件的(*http2serverConn).newWriterAndRequestNoBody
方法中。
CONNECT
請求經過:authority
構建url_
,不然經過:path
構建url_
。if rp.method == "CONNECT" { url_ = &url.URL{Host: rp.authority} requestURI = rp.authority // mimic HTTP/1 server behavior } else { var err error url_, err = url.ParseRequestURI(rp.path) if err != nil { return nil, nil, http2streamError(st.id, http2ErrCodeProtocol) } requestURI = rp.path }
url_
賦值給req.URL
。req := &Request{ Method: rp.method, URL: url_, RemoteAddr: sc.remoteAddrStr, Header: rp.header, RequestURI: requestURI, Proto: "HTTP/2.0", ProtoMajor: 2, ProtoMinor: 0, TLS: tlsState, Host: rp.authority, Body: body, Trailer: trailer, }
因爲:path
標頭的值也不包含Host信息,因此HTTP2的server也沒法經過req.URL.String()
獲得請求的完整URL。
在這裏咱們反思一個問題。經過僞標頭字段已經可以獲得完整的URL,爲何仍然只讀取:path
和:authority
中的一個來賦值req.URL
呢?
老許在這裏猜想可能緣由是但願開發者無需關心請求是HTTP1.1仍是HTTP2,避免沒必要要的HTTP版本判斷。
關於獲取請求完整URL的思考就到這裏。最後,衷心但願本文可以對各位讀者有必定的幫助。
注:
- 寫本文時, 筆者所用go版本爲: go1.15.2
參考: