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)
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
}
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字段。
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()
}
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
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。
HTML表单可发送GET请求,键值对在请求URL中,无主体。
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]]
//r.ParseForm()和r.PostForm(只支持application/x-www-form-urlencoded编码)
//只包含表单键值对
//r.ParseForm()和r.Form(multipart/form-data)
//只返回URL查询值
获取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字段
👇
表单键值对;
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()
}
不同客户端使用不同方式编码POST请求。
ParseForm方法不接受application/json编码。
处理器通过ResponseWriter接口创建HTTP响应。
ResponseWriter接口内部会使用http.response结构(非导出,nonexported)。
字节数组作为参数,写入响应主体。
首部未设置内容类型时,通过写入前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 := `
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
响应状态码,未设置时默认返回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
写入首部映射。
先调用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
package main
import (
"fmt"
"encoding/json"
"net/http"
)
type Post struct {
User string
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
存储在客户端的、体积较小的信息,最初通过服务器HTTP响应报文发送(set-cookie),之后客户端每次请求发送cookie。
cookie划分为会话cookie和持久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推荐使用。
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()
}
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
某个条件满足时,页面上显示临时消息(闪现消息,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