有趣!一行代碼竟然沒法獲取請求的完整URL

來自公衆號: Gopher指北

緣起

作Web服務的時候,可能會有這樣一個業務場景,獲取一個HTTP請求的完整URL。很巧,老許就碰到了這樣的業務場景。面對如此簡單的需求,CV大法根本沒有展現才能的機會。啪啪啪,獲取請求的完整URL代碼就出來了。html

image

當時離驗證只差一步,老許信心滿滿,很快,打臉來得很快就像龍捲風。。。git

image

從圖中能夠知道,req.URL中的SchemeHost均爲空,因此r.URL.String()沒法獲得完整的請求鏈接。這個結果讓老許一陣激動,萬萬沒想到有一天我也有機會發現Go源碼中可能遺漏的賦值。老許強行按耐住心中的激動,準備好好研究一番,萬一成爲了Go的Contributor呢^ω^。最後發現官方實現沒有問題,所以就有了今天這篇文章。github

HTTP1.1中爲何沒法獲取完整的鏈接

HTTP1.1的Server讀取請求並構建Request.URL對象的邏輯在request.go文件的readRequest方法中,下面老許對其源碼作一個簡單分析總結。segmentfault

  1. 讀取請求的第一行,HTTP請求的第一行又稱爲請求行。
// First line: GET /index.html HTTP/1.0
var s string
if s, err = tp.ReadLine(); err != nil {
    return nil, err
}
  1. 將請求行的內容分別解析爲req.Methodreq.RequestURIreq.Proto
var ok bool
req.Method, req.RequestURI, req.Proto, ok = parseRequestLine(s)
  1. 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.Methodreq.RequestURIreq.Proto(request-target在本文均被譯做請求資源)。ide

image

關於請求方法有哪些想必不用老許在這兒科普了吧。至於經常使用的HTTP版本無非就是HTTP1.1和HTTP2。 下面主要介紹請求資源的幾種形式。url

origin-form

這種形式是請求資源中最多見的形式,其格式定義以下。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-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

authority-form形式的請求資源僅用於CONNECT請求中,其格式定義以下。

authority-form = authority

發送CONNECT請求時,client只能發送URI的authority部分(不包含userinfo和@定界符)做爲請求資源。這樣講比較抽象, 咱們先來看看http-URI的定義。

image

經過上面這張圖大概可以猜出來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

asterisk-form形式的請求資源僅適用於OPTIONS請求且只能爲*,其格式定義以下。

asterisk-form  = "*"

一個asterisk-form形式的請求行例子以下。

OPTIONS * HTTP/1.1

對上面幾種形式的請求資源有所瞭解後,咱們再次回到獲取請求的完整URL這一問題自己。以最經常使用的absolute-form爲例(其餘形式的請求資源咱們在開發中幾乎不用考慮),請求資源中自己就缺乏HostScheme信息,因此一行代碼天然沒法獲取請求的完整URL。難道咱們就沒法獲取到請求的完整URL嘛?固然不是,咱們還能夠經過如下兩種方案獲得完整的URL。

方案一

  1. 經過req.Host獲得Host相關信息。
  2. 若是req.TLS == nil則爲HTTP請求,不然爲HTTPS請求。
  3. 經過步驟一、步驟2並結合請求行信息便可獲得完整的URL。

方案二
在配置文件中配置好服務的Host信息,獲取完整請求時只須要讀取配置文件並拼接req.RequestURI便可。事實上老許採用的就是方案二,由於不少服務都在網關後面。當客戶端使用HTTPS請求網關,網關以HTTP請求服務時使用req.TLS == nil判斷就不合理了。

HTTP2中爲何沒法獲取完整的鏈接

須要注意的是在HTTP2中已經沒有請求行的概念了,取而代之的是請求僞標頭,這一點老許在Go發起HTTP2.0請求流程分析(後篇)——標頭壓縮這篇文章中提到過。

下圖爲一次HTTP2請求的部分Header信息。

image

從圖中能夠發現,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方法中。

  1. 若是是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
}
  1. 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的思考就到這裏。最後,衷心但願本文可以對各位讀者有必定的幫助。

  1. 寫本文時, 筆者所用go版本爲: go1.15.2

參考:

https://tools.ietf.org/html/r...

https://tools.ietf.org/html/r...

相關文章
相關標籤/搜索