Go|《Go Web 编程》之第4章 处理请求


《Go Web 编程》之第4章 处理请求

  • 第4章 处理请求
    • 4.1 请求和响应
    • 4.1.1 Request结构
      • 4.1.2 请求URL
      • 4.1.3 请求首部
      • 4.1.4 请求主体
    • 4.2 Go与HTML表单
      • 4.2.1 Form字段
      • 4.2.2 PostForm字段
      • 4.2.3 MultipartForm字段
      • 4.2.4 文件
      • 4.2.5 处理含JSON主体的POST请求
    • 4.3 ResponseWriter
      • 4.3.1 Write方法
      • 4.3.2 WriteHeader方法
      • 4.3.2 Header方法
      • 4.3.4 完整JSON响应
    • 4.4 cookie
      • 4.4.1Go与cookie
      • 4.4.2 发送cookie至浏览器
      • 4.4.3 从浏览器获取cookie
      • 4.4.4 cookie实现闪现消息

第4章 处理请求 4.1 请求和响应 HTTP报文在客户端和服务器间传递消息,分为HTTP请求和HTTP响应,结构如下:
(1)请求行或响应行; (2)零或多个首部; (3)一个空行; (4)可选报文主体。

GET /Protocols/rfc2616/rfc2616.html HTTP/1.1 Host: www.w3.org User-Agent: Mozilla/5.0 (empty line)

4.1.1 Request结构
type Request struct { Method string URL *url.URL Protostring // "HTTP/1.0" ProtoMajor int// 1 ProtoMinor int// 0 Header Header Body io.ReadCloser GetBody func() (io.ReadCloser, error) ContentLength int64 TransferEncoding []string Close bool Host string Form url.Values PostForm url.Values MultipartForm *multipart.Form Trailer Header RemoteAddr string RequestURI string TLS *tls.ConnectionState Cancel <-chan struct{} Response *Response ctx context.Context }

4.1.2 请求URL
type URL struct { Schemestring Opaquestring// encoded opaque data User*Userinfo // username and password information Hoststring// host or host:port Pathstring// path (relative paths may omit leading slash) RawPathstring// encoded path hint (see EscapedPath method) ForceQuerybool// append a query ('?') even if RawQuery is empty RawQuerystring// encoded query values, without '?' Fragmentstring// fragment for references, without '#' RawFragment string// encoded fragment hint (see EscapedFragment method) }

//URL一般格式: scheme://[userinfo@]host/path[?query][#fragment] //解析为: scheme:opaque[?query][#fragment]

http://www.example.com/post?id=123&thread_id=456 //RawQuery为 id=123&thread_id=456

浏览器向服务器发送请求时会剔除URL中的片段部分,服务器接收到URL中无Fragment字段。
4.1.3 请求首部
type Header map[string][]string

添加、删除、获取和设置。
package mainimport ( "fmt" "net/http" )func headers(w http.ResponseWriter, r *http.Request) { h := r.Header //h := r.Header["Accept-Encoding"] //h := r.Header.Get("Accept-Encoding") fmt.Fprintln(w, h) }func main() { server := http.Server{ Addr: "127.0.0.1:8080", } http.HandleFunc("/headers", headers) server.ListenAndServe() }

4.1.4 请求主体
Body io.ReadCloser

GET请求无报文主体。
package mainimport ( "fmt" "net/http" )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() }

curl -id "first_name=sausheong&last_name=chang" 127.0.0.1:8080/body

4.2 Go与HTML表单 【Go|《Go Web 编程》之第4章 处理请求】POST请求基本通过HTML表单发送:

表单输入数据以键值形式记录在请求主体中。
决定POST请求发送键值对时格式的HTML表单内容类型(content type),由表单的enctype属性(默认"application/x-www-form-urlencoded")指定。

浏览器至少支持application/x-www-form-urlencoded和multipart/form-data,HTML5还支持text/plain。
  • application/x-www-form-urlencoded
    &分隔的键值对保存在请求主体中。
  • multipart/form-data
    表单数据转换为MIME报文,键值对带有各自内容类型和内容配置(disposition)。
    传送大量数据(如上传文件),可Base64编码,以文本方式传送二进制数据。
HTML表单可发送GET请求,键值对在请求URL中,无主体。
4.2.1 Form字段
URL、主体数据提取到Form、PostForm和MultipartForm字段中。
Request获取表单数据的步骤:
(1)ParseFomr或者ParseMultipartForm方法,对请求进行语法分析;
(2)访问Form、PostForm或者MultipartForm字段。
client.html
Go Web Programming - 锐客网

package mainimport ( "fmt" "net/http" )func process(w http.ResponseWriter, r *http.Request) { r.ParseForm() fmt.Fprintln(w, r.Form) }func main() { server := http.Server{ Addr: "127.0.0.1:8080", } http.HandleFunc("/process", process) server.ListenAndServe() }

//URL http://127.0.0.1:8080/process?hello=world&thread=123 //post主体 hello=sau sheong&post=456//输出 //r.ParseForm()和r.Form包含url和表单键值对 //且表单值总是排在URL值的前面 map[thread:[123] hello:[sau sheong world] post:[456]]

4.2.2 PostForm字段
//r.ParseForm()和r.PostForm(只支持application/x-www-form-urlencoded编码) //只包含表单键值对

//r.ParseForm()和r.Form(multipart/form-data) //只返回URL查询值

4.2.3 MultipartForm字段
获取multipart/form-data编码的表单数据,需要ParseMultipartForm方法(需要时会自行调用ParseFrom方法)和MultipartForm字段。
//从multipart编码表单里取出字节数 r.ParseMultipartForm(1024)//&{map[hello:[sau sheong] post:[456]] map[]} //只包含表单键值对 //第二个空映射map[]用来记录用户上传的文件 fmt.Fprintln(w, r.MultipartForm)//FormValue只取第一个值 //需要时自动调用ParseFrom或ParseMultipartForm fmt.Fprintln(w, r.FormValue("hello"))//PostFormValue只返回表单值 //需要时自动调用ParseFrom或ParseMultipartForm fmt.Fprintln(w, r.PostFormValue("hello"))

FormValue和PostFormValue解析multipart/form-data表单,无结果。
整理:
(1)
表单(application/x-www-form-urlencoded,默认)
+
ParseFrom方法
+
Form字段

URL键值对+表单键值对;
(2)
表单(application/x-www-form-urlencoded,默认)
+
ParseFrom方法
+
PostForm字段

表单键值对;
(3)
表单(multipart/form-data)
+
ParseMultipartFrom方法
+
MultipartFrom字段

表单键值对;
(4)
表单(application/x-www-form-urlencoded,默认)
+
FromValue字段

URL键值对+表单键值对;
(5)
表单(application/x-www-form-urlencoded,默认)
+
PostFromValue字段

表单键值对;
4.2.4 文件
multipart/form-data编码常用于文件上传,需要file类型的input标签。
Go Web Programming - 锐客网

package mainimport ( "fmt" "io/ioutil" "net/http" )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)) } } }func main() { server := http.Server{ Addr: "127.0.0.1:8080", } http.HandleFunc("/process", process) server.ListenAndServe() }

package mainimport ( "fmt" "io/ioutil" "net/http" )func process(w http.ResponseWriter, r *http.Request) { file, _, err := r.FormFile("uploaded") if err == nil { data, err := ioutil.ReadAll(file) if err == nil { fmt.Fprintln(w, string(data)) } } }func main() { server := http.Server{ Addr: "127.0.0.1:8080", } http.HandleFunc("/process", process) server.ListenAndServe() }

4.2.5 处理含JSON主体的POST请求
不同客户端使用不同方式编码POST请求。
  • jQuery使用POST+默认表单编码+首部Content-Type: application/x-www-form-urlencoded
  • Angular使用POST+表单编码application/json
ParseForm方法不接受application/json编码。
4.3 ResponseWriter 处理器通过ResponseWriter接口创建HTTP响应。
ResponseWriter接口内部会使用http.response结构(非导出,nonexported)。
4.3.1 Write方法
字节数组作为参数,写入响应主体。
首部未设置内容类型时,通过写入前512字节决定。
package mainimport ( "fmt" "encoding/json" "net/http" )type Post struct { Userstring Threads []string }func writeExample(w http.ResponseWriter, r *http.Request) { str := ` Go Web Programming - 锐客网 Hello World ` w.Write([]byte(str)) }func main() { server := http.Server{ Addr: "127.0.0.1:8080", } http.HandleFunc("/write", writeExample) server.ListenAndServe() }

curl -i 127.0.0.1:8080/writer

4.3.2 WriteHeader方法
响应状态码,未设置时默认返回200 OK。
package mainimport ( "fmt" "encoding/json" "net/http" )type Post struct { Userstring Threads []string }func writeHeaderExample(w http.ResponseWriter, r *http.Request) { w.WriteHeader(501) fmt.Fprintln(w, "No such service, try next door") }func main() { server := http.Server{ Addr: "127.0.0.1:8080", } http.HandleFunc("/writeheader", writeHeaderExample) server.ListenAndServe() }

curl -i 127.0.0.1:8080/writerheader

4.3.2 Header方法
写入首部映射。
先调用Header方法写入首部,再调用WriteHeader(执行后,不允许修改首部)写入状态码。
package mainimport ( "fmt" "encoding/json" "net/http" )type Post struct { Userstring Threads []string }func headerExample(w http.ResponseWriter, r *http.Request) { w.Header().Set("Location", "http://google.com") w.WriteHeader(302) }func main() { server := http.Server{ Addr: "127.0.0.1:8080", } http.HandleFunc("/redirect", headerExample) server.ListenAndServe() }

curl -i 127.0.0.1:8080/redirect

4.3.4 完整JSON响应
package mainimport ( "fmt" "encoding/json" "net/http" )type Post struct { Userstring Threads []string }func writeExample(w http.ResponseWriter, r *http.Request) { str := ` Go Web Programming - 锐客网 Hello World ` w.Write([]byte(str)) }func writeHeaderExample(w http.ResponseWriter, r *http.Request) { w.WriteHeader(501) fmt.Fprintln(w, "No such service, try next door") }func headerExample(w http.ResponseWriter, r *http.Request) { w.Header().Set("Location", "http://google.com") w.WriteHeader(302) }func jsonExample(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") post := &Post{ User:"Sau Sheong", Threads: []string{"first", "second", "third"}, } json, _ := json.Marshal(post) w.Write(json) }func main() { server := http.Server{ Addr: "127.0.0.1:8080", } http.HandleFunc("/write", writeExample) http.HandleFunc("/writeheader", writeHeaderExample) http.HandleFunc("/redirect", headerExample) http.HandleFunc("/json", jsonExample) server.ListenAndServe() }

curl -i 127.0.0.1:8080/json

4.4 cookie 存储在客户端的、体积较小的信息,最初通过服务器HTTP响应报文发送(set-cookie),之后客户端每次请求发送cookie。
cookie划分为会话cookie和持久cookie。
4.4.1Go与cookie
type Cookie struct { Namestring Value string Pathstring Domainstring Expirestime.Time RawExpires string MaxAgeint Securebool HttpOnly bool SameSite SameSite Rawstring Unparsed []string }

会话cookie或临时cookie(未设置Expires字段),浏览器关闭时自动移除。
持久cookie(设置了Expires字段),时间过期或手动删除。
Expires绝对时间,几乎所有浏览器都支持。
MaxAge相对时间,HTTP 1.1推荐使用。
4.4.2 发送cookie至浏览器
package mainimport ( "fmt" "net/http" )func setCookie(w http.ResponseWriter, r *http.Request) { c1 := http.Cookie{ Name:"first_cookie", Value:"Go Web Programming", HttpOnly: true, } c2 := http.Cookie{ Name:"second_cookie", Value:"Manning Publications Co", HttpOnly: true, } w.Header().Set("Set-Cookie", c1.String()) w.Header().Add("Set-Cookie", c2.String()) }func main() { server := http.Server{ Addr: "127.0.0.1:8080", } http.HandleFunc("/set_cookie", setCookie) server.ListenAndServe() }

package mainimport ( "fmt" "net/http" )func setCookie(w http.ResponseWriter, r *http.Request) { c1 := http.Cookie{ Name:"first_cookie", Value:"Go Web Programming", HttpOnly: true, } c2 := http.Cookie{ Name:"second_cookie", Value:"Manning Publications Co", HttpOnly: true, } http.SetCookie(w, &c1) http.SetCookie(w, &c2) }func main() { server := http.Server{ Addr: "127.0.0.1:8080", } http.HandleFunc("/set_cookie", setCookie) server.ListenAndServe() }

4.4.3 从浏览器获取cookie
package mainimport ( "fmt" "net/http" )func setCookie1(w http.ResponseWriter, r *http.Request) { c1 := http.Cookie{ Name:"first_cookie", Value: "Go Programming", } c2 := http.Cookie{ Name:"second_cookie", Value:"Go Web Programming", HttpOnly: true, } w.Header().Set("Set-Cookie", c1.String()) w.Header().Add("Set-Cookie", c2.String()) fmt.Fprintf(w, "%s\n%s\n", c1.String(), c2.String()) }func setCookie2(w http.ResponseWriter, r *http.Request) { c1 := http.Cookie{ Name:"first_cookie", Value: "Go Programming", } c2 := http.Cookie{ Name:"second_cookie", Value: "Go Web Programming", HttpOnly: true, } http.SetCookie(w, &c1) http.SetCookie(w, &c2) }func getCookie1(w http.ResponseWriter, r *http.Request) { cookie := r.Header["Cookie"] fmt.Fprintf(w, "%s\n", cookie) }func getCookie2(w http.ResponseWriter, r *http.Request) { cookie, err := r.Cookie("first_cookie") if err != nil { fmt.Fprintln(w, "Cannot get Cookie") } cookies := r.Cookies() fmt.Fprintf(w, "%s\n%s\n", cookie, cookies) }func main() { server := http.Server{ Addr: "127.0.0.1:8080", } http.HandleFunc("/set_cookie", setCookie2) http.HandleFunc("/get_cookie", getCookie2) server.ListenAndServe() }

//获取的Set-Cookie保存在a.cookie文件中 curl -i -c a.cookie http://127.0.0.1:8080/set_cookie//发送a.cookie文件中的cookies curl -i -b a.cookie http://127.0.0.1:8080/get_cookie

4.4.4 cookie实现闪现消息
某个条件满足时,页面上显示临时消息(闪现消息,flash message),刷新页面后消失。
package mainimport ( "encoding/base64" "fmt" "net/http" "time" )func set_message(w http.ResponseWriter, r *http.Request) { msg := []byte("Hello World") cookie := http.Cookie{ Name:"flash", //响应首部对空格,百分号,中文等特殊字符的URL编码要求 Value: base64.URLEncoding.EncodeToString(msg), } http.SetCookie(w, &cookie) }func show_message(w http.ResponseWriter, r *http.Request) { cookie, err := r.Cookie("flash") if err != nil { if err == http.ErrNoCookie { fmt.Fprintln(w, "no messages to show") } } else { expire_cookie := http.Cookie{ Name:"flash", MaxAge:-1,//负值 Expires:time.Unix(1, 0), //过去时间 }//MaxAge设置负值,Expires设置过去时间 //SetCookie将同名cookie("flash")发送到客户端 //等价于完全移除这个cookie http.SetCookie(w, &expire_cookie)value, _ := base64.URLEncoding.DecodeString(cookie.Value) fmt.Fprintln(w, string(value)) } }func main() { server := http.Server{ Addr: "127.0.0.1:8080", } http.HandleFunc("/set_message", set_message) http.HandleFunc("/show_message", show_message) server.ListenAndServe() }

curl -i -c b.cookie http://127.0.0.1:8080/set_message curl -i -b b.cookie -c b.cookie http://127.0.0.1:8080/show_messagecurl -i -b b.cookie -c b.cookie http://127.0.0.1:8080/show_message

    推荐阅读