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

最近在工作中對 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 是統一資源定位符,見名知義,因為要定位,所以要指定協議甚至是位置,比如這樣://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 為:

//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 協議,這樣是完全可以自己給各種形式參數下定義取名字的。這可能就是為啥我們說一流的企業、大佬制定標準,接下來的圍繞標準研發技術,進而是基於技術賣產品,最後是圍繞產品提供服務了。

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

講解參考鏈接