什麼是請求引數、表單引數、url引數、header引數、Cookie引數?一文講懂

胡塗阿菌發表於2022-05-22

最近在工作中對 http 的請求引數解析有了進一步的認識,寫個小短文記錄一下。

回顧下自己的情況,大概就是:有點點網路及程式設計基礎,只需要加深一點點對 HTTP 協議的理解就能弄明白了。

先分享一個小故事:我至今仍清晰地記得大三實習時的第一個工作任務,我需要呼叫其他部門提供的 api 去完成某項業務。

那個 api 文件只告訴了我請求引數需要傳什麼,沒有提及用什麼方式傳,比如這樣:

其實如果有經驗的話,直接在請求體或 url 裡填引數試一下就知道了;另一個是新人有時候不太敢問問題,其實只要向同事確認一下就好的。

然而由於當時我掌握的程式設計知識有限,只會用表單提交資料。所以當我下載完同事安利的 api 呼叫除錯工具 postman 後,我就在網上查怎麼用 postman 傳送表單資料,結果折騰了好久 api 還是沒能調通。

當天晚上我向老同學求助,他問我上課是不是又睡過去了?

我說你怎麼知道?

他說當然咯,你上課睡覺不學習又不是一天兩天的事情......

後來他告訴我得好好學一下 http 協議,看看可以在協議的哪些位置放請求引數。

一個簡單的 http 伺服器還原

那麼,在正式講解之前,我們先簡單搭建一個 http 伺服器,阿菌沿用經典的 python 版雲你好伺服器進行講解。

雲你好伺服器的程式碼很簡單,伺服器首先會獲取 name 使用者名稱這個引數,如果使用者傳了這個引數,就返回 Hello xxx,xxx 指的是 name 使用者名稱;如果使用者沒有傳這個引數則返回 Hello World

# 雲你好服務原始碼
from flask import Flask
from flask import request

app = Flask(__name__)

# 雲你好服務 API 介面
@app.get("/api/hello")
def hello():
    # 看使用者是否傳遞了引數 name
    name = request.args.get("name", "")
    # 如果傳了引數就向目標物件打招呼,輸出 Hello XXX,否則輸出 Hello World
    return f"Hello {name}" if name else "Hello World"

# 啟動雲你好服務
if __name__ == '__main__':
    app.run()

為了快速開發(大夥可以下載一個 python 把這個程式碼跑一下,用自己的語言實現一個類似的伺服器也是可以的),阿菌這裡使用了 flask 框架構建後端服務。

在具體獲取引數的時候,我選擇了在 request.args 中獲取引數。這裡提前劇透一下:在 flask 框架中,request.args 指的是從 url 中獲取引數(不過這是我們後面講解的內容,大家有個印象就好)

抓包檢視 http 報文

有了 http 伺服器後,我們開始深入講解 http 協議,em...個人覺得只在學校上課看教材學計算機網路好像還欠缺了點啥,比較推薦大家下載一個像 Wireshark 這樣的網路抓包軟體,動手拆解網路包,深入學習各種網路協議。抓取網路包的示例視訊

為了搞清楚什麼是請求引數、表單引數、url 引數、Header 引數、Cookie 引數,我們先發一個 http 請求,然後抓取這個請求的網路包,看看一份 http 報文會攜帶哪些資訊。

呼應開頭,使用者阿菌是個只會發表單資料的萌新,他使用 postman 向雲你好 api 傳送了一個 post 請求:

劇情發展正常,我們沒能得到 Hello 阿菌(伺服器會到 url 中獲取引數,我們們用表單形式提交,所以獲取不到)

由於我們們對請求體這個概念比較模糊,接下來我們重新發一個一模一樣的請求,並且通過 Wireshark 抓包看一下:

可以看到強大的 Wireshark 幫助我們把請求抓取了下來,並把整個網路包的鏈路層協議,IP層協議,傳輸層協議,應用層協議全都解析好了。

由於我們們小碼農一般都忙於解決應用層問題,所以我們把目光聚焦於高亮的 Hypertext Transfer Protocol 超文字傳輸協議,也就是大名鼎鼎的 HTTP 協議。

首先我們檢視一下 HTTP 報文的完整內容:

可以看到,http 協議大概是這麼組成的:

  • 第一行是請求的方式,比如 GET / POST / DELETE / PUT
  • 請求方式後面跟的是請求的路徑,一般把這個叫 URI(統一資源識別符號)

補充:URL 是統一資源定位符,見名知義,因為要定位,所以要指定協議甚至是位置,比如這樣:http://localhost:5000/api/hello

  • 請求路徑後面跟的是 HTTP 的版本,比如這裡是 HTTP/1.1

完整的第一行如下:

POST /api/hello HTTP/1.1

第二行的 User-Agent 則用於告訴對方發起請求的客戶端是啥,比如我們們用 Postman 發起的請求,Postman 就會自動把這個引數設定為它自己:

User-Agent: PostmanRuntime/7.28.4

第三行的 Accept 用於告訴對方我們希望收到什麼型別的資料,這裡預設是能接受所有型別的資料:

Accept: */*

第四行就非常值得留意,Postman-Token 是 Postman 自己傳的引數,這個我們放到下面講!

Postman-Token: ddd72e1a-0d63-4bad-a18e-22e38a5de3fc

第五行是請求的主機,網路上的一個服務一般用 ip 加埠作為唯一標識:

Host: 127.0.0.1:5000

第六行指定的是我們們請求發起方可以理解的壓縮方式:

Accept-Encoding: gzip, deflate, br

第七行告訴對方處理完當前請求後不要關閉連線:

Connection: keep-alive

第八行告訴對方我們們請求體的內容格式,這個是本文的側重點啦!比如我們這裡指定的是一般瀏覽器的原生表單格式:

Content-Type: application/x-www-form-urlencoded

好了,下面大家要留意了,第九行的 Content-Length 給出的是請求體的大小。

而請求體,會放在緊跟著的一個空行之後。比如本請求的請求體內容是以 key=value 形式填充的,也就是我們表單引數的內容了:

Content-Length: 23

name=%E9%98%BF%E8%8F%8C

看到這裡我們先簡單小結一下,想要告訴伺服器我們傳送的是表單資料,一共需要兩步:

  1. Content-Type 設定為 application/x-www-form-urlencoded
  2. 在請求體中按照 key=value 的形式填寫請求引數

什麼是協議?進一步瞭解 http

好了,接下來我們進一步講解,大家試想一下,網路應用,其實就是端到端的互動,最常見的就是服務端和客戶端互動模型:客戶端發一些引數資料給服務端,通過這些引數資料告訴服務端它想得到什麼或想幹什麼,服務端根據客戶端傳遞的引數資料作出處理。

傳輸層協議通過 ip 和埠號幫我們定位到了具體的服務應用,具體怎麼互動是由我們程式設計師自己定義的。

大概在 30 年前,英國電腦科學家蒂姆·伯納斯-李定義了原始超級文字傳輸協議(HTTP),後續我們的 web 應用大都延續採用了他定義的這套標準,當然這套標準也在不斷地進行迭代。

許多文獻資料會把 http 協議描述得比較晦澀,加上協議這個詞聽起來有點高大上,初學者入門學習的時候往往感覺不太友好。

其實協議說白了就是一種格式,就好比我們寫書信,約定要先頂格寫個敬愛的 xxx,然後寫個你好,然後換一個段落再寫正文,可能最後還得加上日期署名等等。

我們只要按照格式寫信,老師就能一眼看出來我們在寫信;只要我們按協議格式發請求資料,伺服器就能一眼看出來我們想要得到什麼或想幹什麼。

當然,老師是因為老早就學過書信格式,所以他才能看懂書信格式;服務端程式也一樣,我們要預先編寫好 http 協議的解析邏輯,然後我們的伺服器才能根據解析邏輯去獲取一個 http 請求中的各種東西。

當然這個解析 http 協議的邏輯不是誰都能寫出來的,就算能寫出來,也未必寫得好,所以我們會使用厲害的人封裝好的腳手架,比如 java 裡的 spring 全套、Go 語言裡的 Gin 等等。

回到我們開頭給出的示例:

from flask import Flask
from flask import request

app = Flask(__name__)

# 雲你好服務 API 介面
@app.get("/api/hello")
def hello():
    # 看使用者是否傳遞了引數 name
    name = request.args.get("name", "")
    # 如果傳了引數就向目標物件打招呼,輸出 Hello XXX,否則輸出 Hello World
    return f"Hello {name}" if name else "Hello World"

# 啟動雲你好服務
if __name__ == '__main__':
    app.run()

阿菌的示例使用了 python 裡的 flask 框架,在處理邏輯中使用了 request.args 獲取請求引數,而 args 封裝的就是框架從 url 中獲取引數的邏輯。比如我們傳送請求的 url 為:

http://127.0.0.1:5000/api/hello?name=ajun

框架會幫助我們從 url 中的 ? 後面開始擷取,然後把 name=ajun 這些引數存放到 args 裡。

切換一下,假設我們是雲你好服務提供者,我們希望使用者通過表單引數的形式使用雲你好服務,我們只要把獲取 name 引數的方式改成從表單引數裡獲取就可以了,flask 在 request.form 裡封裝了表單引數(關於框架是怎麼在數行 http 請求中封裝引數的,大家可以看自己使用的框架的具體邏輯,估計區別不大,只是存在一些語言特性上的差異):

@app.post("/api/hello")
def hello():
    # 看使用者是否傳遞了引數 name
    name = request.form.get("name", "")
    # 如果傳了引數就向目標物件打招呼,輸出 Hello XXX,否則輸出 Hello World
    return f"Hello {name}" if name else "Hello World"

思考:我們可以在 http 協議中傳遞什麼引數?

最後,我們解釋本文的標題,其實想要明白各種引數之間的區別,我們可以換一個角度思考:

我們們可以在一份 http 報文的哪些位置傳遞引數?

接下來回顧一下一個 http 請求的內容:

POST /api/hello HTTP/1.1
User-Agent: PostmanRuntime/7.28.4
Accept: */*
Postman-Token: fbf75035-a647-46dc-adc0-333751a9399e
Host: 127.0.0.1:5000
Accept-Encoding: gzip, deflate, br
Connection: keep-alive
Content-Type: application/x-www-form-urlencoded
Content-Length: 23

name=%E9%98%BF%E8%8F%8C

大家看,我們們的 http 報文,也就是基於傳輸層之上的應用層報文,大概就長上面這樣。

我們考慮兩種情況,第一種情況,我們基於別人已經開發好的腳手架開發 http 伺服器。

由於框架會基於 http 協議進行解析,所以框架會幫助我們解析好請求 url,各種 Header 頭(比如:Cookie 等),以及具體的響應內容都幫我們封裝解析好了(比如按照 key=value 的方式去讀取請求體)。

那當我們開發服務端的時候,就可以指定從 url、header、響應體中獲取引數了,比如:

  • url 引數:指的就是 url 中 ? 後面攜帶的 key value 形式引數
  • header 引數:指的就是各個 header 頭,我們甚至可以自定義 header,比如 Postman-Token 就是 postman 這個軟體自己攜帶的,我們服務端如果需要的話是可以指定獲取這個引數的
  • Cookie 引數:其實就是名字為 Cookie 的請求頭
  • 表單引數:指的就是 Content-Type 為 application/x-www-form-urlencoded 下請求體的內容,如果我們的表單需要傳檔案,還會有其他的 Content-Type
  • json 引數:指的就是 Content-Type 為 application/json 下請求體的內容(當然服務端可以不根據 Content-Type 直接解析請求體,但按照協議的規範工程專案或許會更好維護)

綜上所述,請求引數就是對上面各種型別的引數的一個總稱了。

大家會發現,不管什麼 url 引數、header 引數、Cookie 引數、表單引數,其實就是換著法兒,按照一定的格式把資料放到應用層報文中。關鍵在於我們的服務端程式和客戶端程式按照一種什麼樣的約定去傳遞和獲取這些引數。這就是協議吧~

還有另一種情況,當然這只是開玩笑了,比如以後哪位大佬或者哪家企業定義了一種新的資料傳輸標準,推廣至全球,比如叫 hppt 協議,這樣是完全可以自己給各種形式引數下定義取名字的。這可能就是為啥我們說一流的企業、大佬制定標準,接下來的圍繞標準研發技術,進而是基於技術賣產品,最後是圍繞產品提供服務了。

一旦標準制定了,整個行業都圍繞這個標準轉了,而且感覺影響會越來越深遠......

講解參考連結

相關文章