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

第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
	
	Proto      string // "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 {
	Scheme      string
	Opaque      string    // encoded opaque data
	User        *Userinfo // username and password information
	Host        string    // host or host:port
	Path        string    // path (relative paths may omit leading slash)
	RawPath     string    // encoded path hint (see EscapedPath method)
	ForceQuery  bool      // append a query ('?') even if RawQuery is empty
	RawQuery    string    // encoded query values, without '?'
	Fragment    string    // 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 main

import (
	"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 main

import (
	"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表单

POST请求基本通过HTML表单发送:

<form action="/process" method="post">
	<input type="text" name="first_name"/>
	<input type="text" name="last_name"/>
	<input type="submit"/>
</form>

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

<form action="/process" method="post" enctype="application/x-www-form-urlencoded">
	<input type="text" name="first_name"/>
	<input type="text" name="last_name"/>
	<input type="submit"/>
</form>

浏览器至少支持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

<html>
  <head>    
    <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
    <title>Go Web Programming</title>
  </head>
  <body>
    <form action="http://127.0.0.1:8080/process?hello=world&thread=123" method="post" enctype="application/x-www-form-urlencoded">
      <input type="text" name="hello" value="sau sheong"/>
      <input type="text" name="post" value="456"/>
      <input type="submit"/>
    </form>
  </body>
</html>
package main

import (
	"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标签。

<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>
package main

import (
	"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 main

import (
	"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 main

import (
	"fmt"
	"encoding/json"
	"net/http"
)

type Post struct {
	User    string
	Threads []string
}

func writeExample(w http.ResponseWriter, r *http.Request) {
	str := `<html>
<head><title>Go Web Programming</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()
}
curl -i 127.0.0.1:8080/writer

4.3.2 WriteHeader方法

响应状态码,未设置时默认返回200 OK。

package main

import (
	"fmt"
	"encoding/json"
	"net/http"
)

type Post struct {
	User    string
	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 main

import (
	"fmt"
	"encoding/json"
	"net/http"
)

type Post struct {
	User    string
	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 main

import (
	"fmt"
	"encoding/json"
	"net/http"
)

type Post struct {
	User    string
	Threads []string
}

func writeExample(w http.ResponseWriter, r *http.Request) {
	str := `<html>
<head><title>Go Web Programming</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, "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 {
	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
}

会话cookie或临时cookie(未设置Expires字段),浏览器关闭时自动移除。
持久cookie(设置了Expires字段),时间过期或手动删除。
Expires绝对时间,几乎所有浏览器都支持。
MaxAge相对时间,HTTP 1.1推荐使用。

4.4.2 发送cookie至浏览器

package main

import (
	"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 main

import (
	"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 main

import (
	"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 main

import (
    "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_message


curl -i -b b.cookie -c b.cookie http://127.0.0.1:8080/show_message
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值