本节讨论如何利用go处理客户端发来的请求。Request结构体作为客户端请求的载体,其中HTML作为Request请求的主体,而传入服务器后需要利用相关的响应方法对请求进行相应。Cookie是辅助响应的手段,用于保存状态。

学习的是如何使用 Go 提供的工具来处理请求,以及如何把响应回传给客户端。

1. 请求方法

1.1 Request结构

Request 结构表示一个由客户端发送的 HTTP 请求报文,包括:

  • URL字段
  • Header字段
  • Body字段
  • Form,PostForm字段和MultipartForm字段

虽然 HTTP 请求报文是由一系列文本行组成的,但 Request 结构并不是完全按照报文逐字逐句定义的。 实际情况是,这个结构只包含了报文在经过语法分析之后,其中较为重要的信息。

1.2 请求URL

Request结构体中包含的URL字段指向了一个url.URL结构体:

type URL struct {
    Scheme   string
    Opaque   string    // 编码后的不透明数据
    User     *Userinfo // 用户名和密码信息
    Host     string    // host或host:port
    ath     string
    RawQuery string // 编码后的查询字符串,没有'?'
     Fragment string // 引用的片段(文档位置),没有'#'
}

URL的一般格式是:

scheme:// [user info@] host/path [?query] [#fragment]

而scheme之后不带斜线的URL会被解释为:

scheme:opaque[?query] [#fragment ]

当我们输入http://www.example.com/post?id=123&thread_id=456这个id=123&thread_id=456就是查询字段RawQuery

1.3 请求头部

请求和响应的首部都使用 Header 类型描述,这种类型使用一个映射来表示 HTTP 首部中的多个键值对。 Header 类型拥有 4 种基本方法,这些方法可以根据给定的键执行添加、 删除、获取和设置值等操作。

下面展示了读取头部的方法:

func headers(w http.ResponseWriter, r *http.Request){
    h := r.Header
    fmt.Fprintln(w,h)
}

func main(){
    server:=http.Server{
        Addr: "127.0.0.1:8080",
    }
    http.HandleFunc("/headers",headers)
    server.ListenAndServe()
}

在浏览器会输出:

map[
Accept:[text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9] 

Accept-Encoding:
[gzip, deflate, br] 

Accept-Language:
[zh,en-US;q=0.9,en;q=0.8,zh-CN;q=0.7] 

Connection:
[keep-alive] 

Cookie:
[_ga=GA1.1.1969723589.1578322265; _xsrf=2|86b77755|fdefd0fe22a4955aac4be46f30abdbfe|1578603653; ...省略] 

Sec-Fetch-Mode:
[navigate] 
Sec-Fetch-Site:
[none] 
Sec-Fetch-User:
[?1] 
Upgrade-Insecure-Requests:
[1] 

User-Agent:
[Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/79.0.3945.88 Safari/537.36]
]

通过这些,我们能知道头部的所有信息,当然我们也能返回特定的信息:

h:=r.Header["Accept-Encoding"]

1.4 请求主体

请求和响应的主体都由 Request 结构的 Body 字段表示,这个字段是一个 io.Read Closer 接口,该接口既包含了:

  • Reader接口:接口拥有 Read 方法,这个方法接受一个字节切片为输入,并在执行之后返回被读取内容的字节数以及一个可选的错误作为结果;
  • Closer接口:这个方法不接受任何参数,但会在出错时返回一个错误。

下面是一个demo:

func body(w http.ResponseWriter, r *http.Request){
    len := r.ContentLength
    body :=make([]byte,len)
    r.Body.Read(body)
    fmt.Fprintln(w,string(body))
}

func main(){
    server:=http.Server{
        Addr: "127.0.0.1:8080",
    }
    http.HandleFunc("/body",body)
    server.ListenAndServe()
}

由于GET请求并不包含BODY,所以我们访问时,控制台没有任何输出。所以如果我们想要测试这个服务器,就需要通过HTML表单发送 POST 请求。这里我们不用表单,用cURL命令来测试:

curl -id "first_name=11&last_name=22" 127.0.0.1:8080/body

在控制台可以看到:

HTTP/1.1 200 OK
Date: Thu, 23 Jan 2020 14:53:52 GMT
Content-Length: 27
Content-Type: text/plain; charset=utf-8

first_name=11&last_name=22

2. HTML表单

2.1 什么是表单

POST 请求都是通过HTML表单发送的,这些表单看上去通常会是下面这个样子:

<form action=”/process ” method=”post”> <input type=”text” name=”first name” /> <input type=”text” name=”last name"/> <input type=”submit”/> </form>

<form>标签可以包围文本行、文本框、单选按钮、复选框以及文件上传等多种HTML表单元素, 而用户则可以把想要传递给服务器的数据输入到这些元素里面。当用户按下发送按钮、又或者通过某种方式触发了表单的发送操作之后,用户在表单中输入的数据就会被发送至服务器。

表单的编码属性由enctype属性的值设置,有两种:

  • application/x-www-form-urlencoded
  • multipart/form-data

第一种,浏览器将把 HTML 表单中的数据编码为一个连续的“长查询字符串”( long query string ):在这个字符串中,不同的键值对将使用&符号分隔,而键值对中的键和值则使用等号=分隔

first_name=sau%20sheong&last_name=chang

第二种,表单中的数据将被转换成一条 MIME 报文 (Multipurpose Internet Mail Extensions):表单中的每个键值对都构成了这条报文的一部分,并且每个键值对都带有它们各自的内容类型以及内容配置( disposition ) 。

------WebKitFormBoundaryMPNjKpe09cLiocMw Content-Disposition : f orm-data ; name=”first name" 
sau sheong ------WebKitFormBoundaryMPNjKpe09cLiocMw Content- Disposition: form-data; name=”l ast name” 
chang ------WebKitFormBoundaryMPNjKpe09cLiocMw--

除了post外也可以通过get发送表单

<form action=”/process ” method=”get"> 
   ...
</form>

2.2 Form字段

为了提取表单的键值对数据,我们需要对表单进行处理,net/http库提供了许多函数能够满足需求。通过调用 Request 结构提供的方法,用户可以将 URL、Body数据提取到该结构的 Form, PostFormMultipartForm 等字段当中。

过程如下:

  1. 调用 ParseForm 方法或者 ParseMultipartForm 方法,对请求进行语法分析。
  2. 根据步骤 l 调用的方法,访问相应的 Form 字段、 PostForm 字段或 MultipartForm 字段。

举个例子:

func process(w http.ResponseWriter, r *http.Request){
    r.ParseForm()
    fmt.Fprintln(w,r.Form)
    fmt.Println("收到了")
}

func main(){
    server:=http.Server{
        Addr: "127.0.0.1:8080",
    }
    http.HandleFunc("/process",process)
    server.ListenAndServe()
}

然后我们创建一个html文件,包含了:

<html>
  <head>    
    <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
    <title>Go Web Programming</title>
  </head>
  <body>
    <form action="http://localhost:8080/process?hello=world&thread=123" method="post" enctype="multipart/form-data">
      <input type="text" name="hello" value="sau sheong"/>
      <input type="text" name="post" value="456"/>
      <input type="file" name="uploaded">
      <input type="submit">
    </form>
  </body>
</html>

用浏览器打开这个html文件,点击提交,可以在浏览器看到:

map[hello:[world] thread:[123]]

在控制台可以看到“收到了”的消息提醒。

2.3 PostForm字段

当我们写入r.Form["post"]时返回一个切片456,但是当我们输入r.Form["hello"]时却返回了world而不是sau sheong。这是因为hello同时出现在表单和 URL 两个地方的键

<input type="text" name="hello" value="sau sheong"/>

action="http://localhost:8080/process?hello=world&thread=123"

面对这种情况,我们需要改为r.PostForm语句。

2.4 MultipartForm字段

前面的字段针对application/x-www-form-urlencoded类型,而对于另一种multipart/form-data类型,需要使用MultipartForm字段。

r.ParseMultipartForm(1024) 
fmt.Fprintln (w,r.MultipartForm)

的第一行代码说明了我们想要从 multipart 编码的表单里面取出多少字节的数据,而第二行语句则会打印请求的 MultipartForm 字段。


MultipartForm字段只包含表单键值对,不包含URL键值对

它也不是一个单映射,而是两个映射的组合:第一个键为字符串,值为字符串组成的切片,第二个映射为空,用来记录用户上传的文件。

此外还有FormValuePostFormValue,他们只会从Form结构中取出给定键的第一个值:

fmt.Fprintln(w, r.ForrnValue ("hello" ))

上面的代码将输出sau sheong


对比:

2.5 文件

multipart/form-data编码,通常用于实现文件上传功能,这种功能需要用到 file 类型的 input 标签:

<html>
  <head>    
    <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
    <title>Go Web Programming</title>
  </head>
  <body>
    <form action="http://localhost:8080/process?hello=world&thread=123" method="post" enctype="multipart/form-data">
      <input type="text" name="hello" value="sau sheong"/>
      <input type="text" name="post" value="456"/>
      <input type="file" name="uploaded">
      <input type="submit">
    </form>
  </body>
</html>

为了能够接收表单上传的文件,处理器函数也需要做相应的修改:

func process(w http.ResponseWriter, r *http.Request){
    r.ParseMultipartForm(1024)
    fileHeader:= r.MultipartForm.File["uploaded"][0]
    file,err := fileHeader.Open()
    if err == nil {
        data, err:=ioutil.ReadAll(file)
        if err == nil{
            fmt.Fprintln(w,string(data))
        }
    }
}

文件处理步骤可以总结为:

  1. 执行ParseMultipartForm方法解析字段;
  2. MultipartForm字段的File字段中提取出文件头FileHeader
  3. 通过open打开这个文件;
  4. ioutil函数将文件内容读取到一个字节数组中。

3. 响应方法

响应时需要用到ResponseWriter接口,处理器可以通过这个接口创建HTTP响应。

我们知道ServeHTTP接收ResponseWriter接口和一个指向Request结构的指针作为参数。之所以要用指针传递而不是值传递,是为了探测对Request结构的修改情况。而ResponseWriter看起来像传值,但实际上它是response这个非导出结构的接口,也是传引用

ResponseWriter 接口拥有以下 3 个方法:

  • Write
  • WriteHeader
  • Header

3.1 Write

Write方法接受一个字节数组作为参数,并将数组中的字节写入HTTP响应的主体中。

func writeExample(w http.ResponseWriter, r *http.Request){
    str:=
`<html>
<head><title>Go Web</title></head>
<body><h1>Hello world</h1><body>
</html>`
    w.Write([]byte(str))
}

func main(){
    server:=http.Server{
        Addr: "127.0.0.1:8080",
    }
    http.HandleFunc("/write",writeExample)
    server.ListenAndServe()
}

注意这段Demo使用 ` 符号,用于多行字符串。我们用命令行输入:

curl -i localhost:8080/write

可以得到响应:

HTTP/1.1 200 OK
Date: Fri, 24 Jan 2020 10:35:59 GMT
Content-Length: 82
Content-Type: text/html; charset=utf-8

<html>
<head><title>Go Web</title></head>
<body><h1>Hello world</h1><body>
</html>

3.2 WriteHeader

注意:WriteHeader并不能用于设置响应的头部(Header)。WriteHeader 方法接受一个代表 HTTP 响应状态码的整数作为参数, 并将这个整数用作 HTTP 响应的返回状态码;在调用这个方法之后,用户可以继续对 ResponseWriter 进行写人,但是不能对响应的首部做任何写入操作。 如果用户在调用 Write 方法之前没有执行过 WriteHeader 方法,那么程序默认会使用 200 OK作为响应的状态码。

注意别忘了使用 HandleFune 方法将新处理器绑定到 DefaultServeMux多路复用器里面!

比如,你编写了一个API,但尚未完全实现,所以希望返回一个 501 Not Implemented 状态码:

func writeExample(w http.ResponseWriter, r *http.Request){
    str:=
`<html>
<head><title>Go Web</title></head>
<body><h1>Hello world</h1><body>
</html>`
    w.Write([]byte(str))
}
func writeHeaderExample(w http.ResponseWriter, r *http.Request){
    w.WriteHeader(501)
    fmt.Fprintln(w, "TO DO")
}
func main(){
    server:=http.Server{
        Addr: "127.0.0.1:8080",
    }
    http.HandleFunc("/write",writeExample)
    http.HandleFunc("/writeheader",writeHeaderExample)
    server.ListenAndServe()
}

通过 cURL 访问刚刚添加的新处理器:

curl -i localhost:8080/writeheader

我们将得到以下响应:

HTTP/1.1 501 Not Implemented
Date: Fri, 24 Jan 2020 10:57:32 GMT
Content-Length: 6
Content-Type: text/plain; charset=utf-8

TO DO

3.3 Header

过调用 Header 方法可以取得一个由首部组成的映射,修改这个映射就可以修改首部,修改后的首部将被包含在 HTTP 响应里面, 并随着响应一向发送至客户端。

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

func main(){
    ...
     http.HandleFunc("/redirect",headerExample)
    server.ListenAndServe()
}

那么cURL响应将得到以下结果:

HTTP/1.1 302 Found
Location: http://google.com
Date: Fri, 24 Jan 2020 11:04:47 GMT
Content-Length: 0

除了将状态码设置成了 302 之外, 它还给响应添加了一个名为 Location 的首部,并将这个首部的值设置成了重定向的目的地。 需要注意的是,因为 WriteHeader 方法在执行完毕之后就不允许再对首部进行写入了,所以用户必须先写入 Location 首部,然后再写入状态码。 现在,如果我们在浏览器里面访问这个处 理器, 那么浏览器将被重定向到 Google。

4. Cookie

cookie 是一种存储在客户端的、体积较小的信息,这些信息最初都是由服务器通过 HTTP 响应报文发送的。 每当客户端向服务器发送一个 HTTP 请求时, cookie 都会随着请求被一同发送至服务器。 cookie 的设计本意是要克服 HTTP 的无状态性,虽然 cookie 并不是完成这一目的的唯一方法,但它却是最常用也最流行的方法之一:整个计算机行业的收入都建立在 cookie机制之上,对互联网广告领域来说,更是如此:)

4.1 Go中的Cookie

cookie 在 Go 语言里面用 Cookie 结构表示:

type Cookie struct {
    Name  string
    Value string

    Path       string    // optional
    Domain     string    // optional
    Expires    time.Time // optional
    RawExpires string    // for reading cookies only

    MaxAge   int
    Secure   bool
    HttpOnly bool
    Raw      string
    Unparsed []string // Raw text of unparsed attribute-value pairs
}

cookie根据是否含有Expires信息分为两种:

  • 会话cookie,或者叫临时cookie。在浏览器关闭的时候就会自动被移除。
  • 持久cookie,存在直到指定的过期时间来临或者被手动删除为止

Expires 宇段和 MaxAge 字段都可以用于设置 cookie 的过期时间:

  • Expires,什么时候到期
  • MaxAge,能活多少秒

4.2 将cookie发送至浏览器

完整的步骤是:

  1. 创建一个cookie结构体
  2. 用Cookie 结构的 String 方法将结构体序列化
  3. 使用set设置cookie
func setCookie(w http.ResponseWriter, r *http.Request){
    c1:= http.Cookie{
        Name:       "first_cookie",
        Value:      "Go web",
        Path:       "",
        Domain:     "",
        Expires:    time.Time{},
        RawExpires: "",
        MaxAge:     0,
        Secure:     false,
        HttpOnly:   true,
        SameSite:   0,
        Raw:        "",
        Unparsed:   nil,
    }
    w.Header().Set("Set-Cookie",c1.String())
    //w. Header() . Add ("Set-Cookie”, c1. String ()) 也可以
}
func main(){
    server:=http.Server{
        Addr: "127.0.0.1:8080",
    }
    http.HandleFunc("/set_cookie",setCookie)
    server.ListenAndServe()
}

除了 Set 方法和 Add 方法之外 ,Go语言还提供了一种更为快捷方便的 cookie 设置方法:

http.SetCookie(w, &c1)

4.3 从浏览器获取cookie

添加:

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

http.HandleFunc("/get_cookie",getCookie)

当我们进入这个网页时,会显示一大堆cookie信息。如果用户想要取得单独的键值对格式的 cookie,就需要进行分析,go提供了一些分析方法:

func getCookie(w http.ResponseWriter, r *http.Request){
    c1,err:=r.Cookie("first_cookie")
    if err!=nil{
        fmt.Fprintln(w,"Can not find the cookie")
    }
    cs := r.Cookies()
    fmt.Fprintln(w,c1)
    fmt.Fprintln(w,cs)
}

func main(){
    server:=http.Server{
        Addr: "127.0.0.1:8080",
    }
    http.HandleFunc("/set_cookie",setCookie)
    http.HandleFunc("/get_cookie",getCookie)
    server.ListenAndServe()
}

先进入网页/set_cookie,然后进入/get_cookie。第一行显示我们设置的cookie,后面的为所有cookie。

first_cookie="Go web"

[_ga=GA1.1.193589.15722265 _xsrf=2|86b77755|fdefd0aac4be46f30abdbfe|1578603653 username-localhost-
...
8889|44:OTJhZmZiMjM5OTJiNGY2Y4MTAxNDdkM2UxM2U=|f8d50574e991203f0303f948b092ad1786a2f79c5ecbee1db10cf4c first_cookie="Go web"]

4.4 利用cookie实现闪现消息

当某个条件被满足时,在页面上显示一条临时出现的消息,然而当用户在刷新页面之后就不会再看见相同的消息了一一我们把这种临时出现的消息称为问现消息( flash message )。比如论坛发帖时,因为某种原因失败了,需要弹出失败消息。

func setMessage(w http.ResponseWriter,r *http.Request){
    msg:=[]byte("hello world!")
    c :=http.Cookie{
        Name:       "flash",
        Value:      base64.URLEncoding.EncodeToString(msg),
    }
    http.SetCookie(w,&c)
}
func showMessage(w http.ResponseWriter,r *http.Request){
    c,err:=r.Cookie("flash")
    if err!=nil{
        if err==http.ErrNoCookie{
            fmt.Fprintln(w,"No message")
        }
    }else{
        rc:=http.Cookie{
            Name:       "flash",
            Expires:    time.Unix(1,0),
            MaxAge:     -1,
        }
        http.SetCookie(w,&rc)
        val,_:=base64.URLEncoding.DecodeString(c.Value)
        fmt.Fprintln(w,string((val)))
    }
}
func main(){
    server:=http.Server{
        Addr: "127.0.0.1:8080",
    }
    http.HandleFunc("/set_message",setMessage)
    http.HandleFunc("/show_message",showMessage)
    server.ListenAndServe()
}

首先set_message这一段,其实跟之前的set_cookie比较类似,主要区别在于编码:对消息使用了 Base64URL 编码,以此来满足响应首部对 cookie 值的 URL 编码要求。

再来看show_message这一段,思路是:

  1. 尝试获取指定的 cookie , 如果没找到就返回错误
  2. 找到后,创建一个同名cookie,将 MaxAge 值设置为负数,并且将 Expires 值也设置成 一个已经过去的时间;
  3. 使用 SetCookie 方法将刚刚创建的同名 cookie 发送至客户端。
  4. 对原来的cookie解码,显示消息。

由于新的消息同名,所以会顶替掉原来的cookie,又因为时间是过期时间,会将这个cookie清除,以此来达到闪现的目的。当我们刷新页面时,hello world消失。