Go Web 編程之 響應

概述

上一篇文章中,咱們介紹了請求的結構與處理。本文將詳細介紹如何響應客戶端的請求。其實在前面幾篇文章中,咱們已經使用過響應的功能——經過http.ResponseWriter發送字符串給客戶端。
可是這種方式僅限於發送字符串。本文咱們將介紹如何定製響應的參數。html

ResponseWriter接口

若是你看了我前面幾篇文章,應該對處理器和處理器函數都很是熟悉了。處理器函數即擁有如下簽名的函數:git

func (w http.ResponseWriter, r *http.Request)

這裏的ResponseWriter實際上是定義在net/http包中的一個接口:程序員

// src/net/http/
type ReponseWriter interface {
    Header() Header
    Write([]byte) (int, error)
    WriteHeader(statusCode int)
}

咱們響應客戶端請求都是經過該接口的 3 個方法進行的。例如以前fmt.Fprintln(w, "Hello World")其實底層調用了Write方法。github

收到請求後,多路複用器會自動建立一個http.response對象,它實現了http.ResponseWriter接口,而後將該對象和請求對象做爲參數傳給處理器。那爲何請求對象使用的時結構指針*http.Request,而響應要使用接口呢?golang

實際上,請求對象使用指針是爲了能在處理邏輯中方便地獲取請求信息。而響應使用接口來操做,一方面底層也是對象指針,能夠保存修改。另外一方面,我認爲是爲了擴展性。能夠很方便地用新的實現替換而不用修改應用層代碼,即處理器接口不用修改。例如,Go 標準庫提供了一個測試 HTTP 請求的工具包net/http/httptest。它定義了一個ResponseRecorder結構,該結構實現了接口http.ResponseWriter。這個結構不將寫入的數據發送給客戶端,而是將數據記錄下來,方便測試斷言web

接口ResponseWriter有 3 個方法,下面依次來介紹如何使用:chrome

  • Write
  • WriteHeader
  • Header

Write方法

因爲接口ResponseWriter擁有方法Write([]byte) (int, error),因此實現了ResponseWriter接口的結構也實現了io.Writer接口:shell

// src/io/io.go
type Writer interface {
    Write(p []byte) (n int, err error)
}

這也是爲何http.ResponseWriter類型的變量w能在下面代碼中使用的緣由(fmt.Fprintln的第一個參數接收一個io.Writer接口):編程

fmt.Fprintln(w, "Hello World")

咱們也能夠直接調用Write方法來向響應中寫入數據:json

func writeHandler(w http.ResponseWriter, r *http.Request) {
    str := `<html>
<head><title>Go Web 編程之 響應</title></head>
<body><h1>直接使用 Write 方法<h1></body>
</html>`
    w.Write([]byte(str))
}

mux.HandleFunc("/write", writeHandler)

下面,咱們介紹一個工具curl來測試咱們的 Web 應用。因爲瀏覽器只會展現響應中主體的內容,其它元信息須要進行一些操做才能查看,不夠直觀。curl是一個 Linux 命令行程序,可用來發起 HTTP 請求,功能很是強大,如設置首部/請求體,展現響應首部等。

一般 Linux 系統會自帶curl命令。簡單介紹幾種 Windows 上安裝curl的方式。

  • 直接在curl官網下載可執行程序,下載完成後放在PATH目錄中便可在CmdPowershell界面中使用;
  • Windows 提供了一個軟件包管理工具chocolatey,能夠安裝/更新/刪除 Windows 軟件。安裝chocolatey後,直接在CmdPowershell界面執行如下命令便可安裝curl,也比較方便:
choco install curl
  • 我想做爲程序員,每一個人都應該熟悉git。安裝git for windows後,就能夠直接在Git Bash中使用curl命令。實際上,git for windows使用了mingw來在 Windows 上模擬 Linux 環境。它提供了不少 Linux 命令的 Windows 版本,很是推薦使用。

啓動服務器,使用下面命令測試Write方法:

curl -i localhost:8080/write

選項-i的做用是顯示響應首部。該命令返回:

HTTP/1.1 200 OK
Date: Thu, 19 Dec 2019 13:36:32 GMT
Content-Length: 113
Content-Type: text/html; charset=utf-8

<html>
<head><title>Go Web 編程之 響應</title></head>
<body><h1>直接使用 Write 方法<h1></body>
</html>

能夠看出很清晰地看出響應的各個部分。也能夠繼續使用瀏覽器來測試:

可是若是要查看首部,狀態碼等信息就必須使用瀏覽器的開發者工具了。Chrome 的開發者工具能夠經過 F12 喚出,而後切換到Network標籤,點擊剛剛發送的請求:

咱們看到上面紅色的兩個部分爲響應的元信息,下面的綠色部分爲請求的基本信息。

注意到,若是咱們沒有設置響應碼,則響應碼默認爲200
並且咱們也沒有設置內容類型,可是返回的首部中有Content-Type: text/html; charset=utf-8,說明net/http會自動推斷。net/http包是經過讀取響應體中前面的若干個字節來推斷的,並非百分百準確的。

如何設置狀態碼和響應內容的類型呢?這就是WriteHeaderHeader()兩個方法的做用。

WriteHeader方法

WriteHeader方法的名字帶有一點誤導性,它並不能用於設置響應首部。WriteHeader接收一個整數,並將這個整數做爲 HTTP 響應的狀態碼返回。調用這個返回以後,能夠繼續對ResponseWriter進行寫入,可是不能對響應的首部進行任何修改操做。若是用戶在調用Write方法以前沒有執行過WriteHeader方法,那麼程序默認會使用 200 做爲響應的狀態碼。

若是,咱們定義了一個 API,還未定義其實現。那麼請求這個 API 時,能夠返回一個 501 Not Implemented 做爲狀態碼。

func writeHeaderHandler(w http.ResponseWriter, r *http.Request) {
    w.WriteHeader(501)
    fmt.Fprintln(w, "This API not implemented!!!")
}

mux.HandleFunc("/writeheader", writeHeaderHandler)

使用curl來測試剛剛編寫的處理器:

curl -i localhost:8080/writeheader

返回:

HTTP/1.1 501 Not Implemented
Date: Thu, 19 Dec 2019 14:15:16 GMT
Content-Length: 28
Content-Type: text/plain; charset=utf-8

This API not implemented!!!

Header方法

Header方法其實返回的是一個http.Header類型,該類型的底層類型爲map[string][]string

// src/net/http/header.go
type Header map[string][]string

類型Header定義了 CRUD 方法,能夠經過這些方法操做首部。

func headerHandler(w http.ResponseWriter, r *http.Request) {
    w.Header().Set("Location", "http://baidu.com")
    w.WriteHeader(302)
}

經過第一篇文章咱們知道 302 表示重定向,瀏覽器收到該狀態碼時會再發起一個請求到首部中Location指向的地址。使用curl測試:

curl -i localhost:8080/header

返回:

HTTP/1.1 302 Found
Location: http://baidu.com
Date: Thu, 19 Dec 2019 14:17:49 GMT
Content-Length: 0

如何在瀏覽器中打開localhost:8080/header,網頁會重定向到百度首頁

接下來,咱們看看如何設置自定義的內容類型。經過Header.Set方法設置響應的首部Contet-Type便可。咱們編寫一個返回 JSON 數據的處理器:

type User struct {
    FirstName   string      `json:"first_name"`
    LastName    string      `json:"last_name"`
    Age         int         `json:"age"`
    Hobbies     []string    `json:"hobbies"`
}

func jsonHandler(w http.ResponseWriter, r *http.Request) {
    w.Header().Set("Content-Type", "application/json")
    u := &User {
        FirstName:  "lee",
        LastName:   "darjun",
        Age:        18,
        Hobbies:    []string{"coding", "math"},
    }
    data, _ := json.Marshal(u)
    w.Write(data)
}

mux.HandleFunc("/json", jsonHandler)

經過curl發送請求:

curl -i localhost:8080/json

返回:

HTTP/1.1 200 OK
Content-Type: application/json
Date: Thu, 19 Dec 2019 14:31:03 GMT
Content-Length: 78

{"first_name":"lee","last_name":"darjun","age":18,"hobbies":["coding","math"]}

能夠看到響應首部中類型Content-Type被設置成了application/json。相似的格式還有 xml(application/xml)/pdf(application/pdf)/png(image/png)等等。

cookie

概念

什麼是 cookie?

cookie 的出現是爲了解決 HTTP 協議的無狀態性的。客戶端經過 HTTP 協議與服務器通訊,屢次請求之間沒法記錄狀態。服務器能夠在響應中設置 cookie,客戶端保存這些 cookie。而後每次請求時都帶上這些 cookie,服務器就能夠經過這些 cookie 記錄狀態,辨別用戶身份等。

重要性

整個計算機行業的收入都創建在 cookie 機制之上,廣告領域更是如此。

上面的說法雖然有些誇張,可是可見 cookie 的重要性。

咱們知道廣告是互聯網最多見的盈利方式。其中有一個很厲害的廣告模式,叫作聯盟廣告。你有沒有這樣一種經歷,剛剛在百度上搜索了某個關鍵字,而後打開淘寶或京東後發現相關的商品已經被推薦到首頁或邊欄了。這是因爲這些網站組成了廣告聯盟,只要加入它們,就能夠共享用戶瀏覽器的 cookie 數據。

使用

Go 中 cookie 使用http.Cookie結構表示,在net/http包中定義:

// src/net/http/cookie.go
type Cookie struct {
    Name        string
    Value       string
    Path        string
    Domain      string
    Expires     time.Time
    RawExpires  string
    MaxAge      int
    Secure      bool
    HttpOnly    bool
    SameSite    SameSite
    Raw         string
    Unparsed    []string
}
  • Name/Value:cookie 的鍵值對,都是字符串類型;
  • 沒有設置Expires字段的 cookie 被稱爲會話 cookie臨時 cookie,這種 cookie 在瀏覽器關閉時就會自動刪除。設置了Expires字段的 cookie 稱爲持久 cookie,這種 cookie 會一直存在,直到指定的時間來臨或手動刪除;
  • HttpOnly字段設置爲true時,該 cookie 只能經過 HTTP 訪問,不能使用其它方式操做,如 JavaScript。提升安全性;

注意:

ExpiresMaxAge均可以用於設置 cookie 的過時時間。Expires字段設置的是 cookie 在什麼時間點過時,而MaxAge字段表示 cookie 自建立以後可以存活多少秒。雖然 HTTP 1.1 中廢棄了Expires,推薦使用MaxAge代替。可是幾乎全部的瀏覽器都仍然支持Expires;並且,微軟的 IE6/IE7/IE8 都不支持 MaxAge。因此爲了更好的可移植性,能夠只使用Expires或同時使用這兩個字段。

cookie 須要經過響應的首部發送給客戶端。瀏覽器收到Set-Cookie首部時,會將其中的值解析成 cookie 格式保存在瀏覽器中。下面咱們來具體看看如何設置 cookie:

func setCookie(w http.ResponseWriter, r *http.Request) {
    c1 := &http.Cookie {
        Name:       "name",
        Value:      "darjun",
        HttpOnly:   true,
    }
    c2 := &http.Cookie {
        Name:       "age",
        Value:      18,
        HttpOnly:   true,
    }
    w.Header().Set("Set-Cookie", c1.String())
    w.Header().Add("Set-Cookie", c2.String())
}

mux.HandleFunc("/set_cookie", setCookie)

運行程序,打開瀏覽器輸入localhost:8080/set_cookie,瀏覽器中什麼都沒有顯示,咱們須要經過開發者工具查看 cookie。在 chrome 瀏覽器(其它瀏覽器相似)按下 F12,切換到 Application(應用)標籤,在左側 Cookies 下點擊測試的 URL,右側便可顯示咱們剛剛設置的 cookie:

固然,咱們也可使用curl測試。可是curl返回的結果就只是響應中的Set-Cookie首部:

curl -i localhost:8080/set_cookie
HTTP/1.1 200 OK
Set-Cookie: name=darjun; HttpOnly
Set-Cookie: age=18; HttpOnly
Date: Fri, 20 Dec 2019 14:08:01 GMT
Content-Length: 0

上面構造 cookie 的代碼中,有幾點須要注意:

  • 首部名稱爲Set-Cookie
  • 首部的值須要是字符串,因此調用了Cookie類型的String方法將其轉爲字符串再設置;
  • 設置第一個 cookie 調用Header類型的Set方法,添加第二個 cookie 時調用Add方法。Set會將同名的鍵覆蓋掉。若是第二個也調用Set方法,那麼第一個 cookie 將會被覆蓋。

爲了使用的便捷,net/http包還提供了SetCookie方法。用法以下:

func setCookie2(w http.ResponseWriter, r *http.Request) {
    c1 := &http.Cookie {
        Name:       "name",
        Value:      "darjun",
        HttpOnly:   true,
    }
    c2 := &http.Cookie {
        Name:       "age",
        Value:      "18",
        HttpOnly:   true,
    }
    http.SetCookie(w, c1)
    http.SetCookie(w, c2)
}

mux.HandleFunc("/set_cookie2", setCookie2)

若是收到的響應中有 cookie 信息,瀏覽器會將這些 cookie 保存下來。只有沒有過時,在向同一個主機發送請求時都會帶上這些 cookie。在服務端,咱們能夠從請求的Header字段讀取Cookie屬性來得到 cookie:

func getCookie(w http.ResponseWriter, r *http.Request) {
    fmt.Fprintln(w, "Host:", r.Host)
    fmt.Fprintln(w, "Cookies:", r.Header["Cookie"])
}

mux.HandleFunc("/get_cookie", getCookie)

第一次啓動服務器,請求localhost:8080/get_cookie時,結果以下,沒有 cookie 信息:

先請求一次localhost:8080/set_cookie,而後再次請求localhost:8080/get_cookie,結果以下,瀏覽器將 cookie 傳過來了:

r.Header["Cookie"]返回一個切片,這個切片又包含了一個字符串,而這個字符串又包含了客戶端發送的任意多個 cookie。若是想要取得單個鍵值對格式的 cookie,就須要解析這個字符串。
爲此,net/http包在http.Request上提供了一些方法使咱們更容易地獲取 cookie:

func getCookie2(w http.ResponseWriter, r *http.Request) {
    name, err := r.Cookie("name")
    if err != nil {
        fmt.Fprintln(w, "cannot get cookie of name")
    }
    
    cookies := r.Cookies()
    fmt.Fprintln(w, c1)
    fmt.Fprintln(w, cookies)
}

mux.HandleFunc("/get_cookies", getCookies2)
  • Cookie方法返回以傳入參數爲鍵的 cookie,若是該 cookie 不存在,則返回一個錯誤;
  • Cookies方法返回客戶端傳過來的全部 cookie。

測試新的 URL get_cookie2

有一點須要注意,cookie 是與主機名綁定的,不考慮端口。咱們上面查看 cookie 的圖中有一列Domain表示的就是主機名。能夠這樣來驗證一下,建立兩個服務器,一個綁定在 8080 端口,一個綁定在 8081 端口,先請求localhost:8080/set_cookie設置 cookie,而後請求localhost:8081/get_cookie

func main() {
    mux1 := http.NewServeMux()
    mux1.HandleFunc("/set_cookie", setCookie)
    mux1.HandleFunc("/get_cookie", getCookie)

    server1 := &http.Server{
        Addr:    ":8080",
        Handler: mux1,
    }

    mux2 := http.NewServeMux()
    mux2.HandleFunc("/get_cookie", getCookie)

    server2 := &http.Server {
        Addr:      ":8081",
        Handler: mux2,
    }
    
    wg := sync.WaitGroup{}
    wg.Add(2)

    go func () {
        defer wg.Done()

        if err := server1.ListenAndServe(); err != nil {
            log.Fatal(err)
        }
    }()

    
    go func() {
        defer wg.Done()

        if err := server2.ListenAndServe(); err != nil {
            log.Fatal(err)
        }
    }()

    wg.Wait()
}

發送給端口 8081 的請求一樣能夠獲取 cookie:

建議本身嘗試一下,(^_^)

上面代碼中,不能直接在主 goroutine 中依次ListenAndServe兩個服務器。由於ListenAndServe只有在出錯或關閉時纔會返回。在此以前,第二個服務器永遠得不到機會運行。因此,我建立兩個 goroutine 各自運行一個服務器,而且使用sync.WaitGroup來同步。不然,主 goroutine 運行結束以後,整個程序就退出了。

總結

本文介紹瞭如何響應客戶端的請求和 cookie 的相關知識。相關代碼在Github上,很是建議你們本身編寫運行一遍以便加深印象。

相關文章
相關標籤/搜索