Golang HTTP编程及源码解析-请求/响应处理
1. HTTP协议
HTTP 协议是 Hyper Text Transfer Protocol(超文本传输协议)的缩写,基于TCP/IP通信协议来传递数据(HTML 文件、图片文件、查询结果等)。
- HTTP 是无连接的:无连接的含义是限制每次连接只处理一个请求,服务器处理完客户的请求,并收到客户的应答后,即断开连接,采用这种方式可以节省传输时间。
- HTTP是独立于媒体的:只要客户端和服务器都知道如何处理数据内容,任何类型的数据都可以通过HTTP发送。客户端和服务器都需要使用适当的 MIME 类型 指定内容类型。
- HTTP是无状态的:HTTP 协议是无状态协议,无状态是指协议对于事务处理没有记忆能力,缺少状态意味着如果后续处理需要前面的信息,则它必须重传,这样可能导致每次连接传送的数据量增大,另一方面,在服务器不需要先前信息时它的应答就较快。
2. HTTP请求报文格式
HTTP请求报文包含以下三个部分。
- 请求行
- 请求头
- 请求体
基本结构如下图所示
3. HTTP响应报文格式
与请求报文结构类型,HTTP响应报文包含以下三个部分
- 状态行
- 响应头
- 响应体
基本结构如下图所示。
下面通过代码结合实例来看Golang的HTTP的请求处理和响应处理。
4. Golang HTTP请求处理
4.1 请求行
func requestLineHandler(w http.ResponseWriter, r *http.Request) {
// 请求行
fmt.Fprintf(w, "\nresp method:%v url:%v Proto:%v\n", r.Method, r.URL, r.Proto)
// URL参数获取
fmt.Fprintf(w, "resp URL查询参数:%v\n", r.URL.Query())
w.WriteHeader(http.StatusOK)
}
func main() {
// 1. 新建路由解码器
h := http.NewServeMux()
// 2. 路由注册
h.HandleFunc("/reqline", requestLineHandler)
// 3. 服务启动 阻塞监听
http.ListenAndServe(":8000", h)
}
通过curl
发起POST
请求,通过-v
打印请求报文和响应报文。
$ curl -v -X POST http://localhost:8000/reqline?name=jack
* Trying 127.0.0.1:8000...
* Connected to localhost (127.0.0.1) port 8000 (#0)
> POST /reqline?name=jack HTTP/1.1
> Host: localhost:8000
> User-Agent: curl/7.86.0
> Accept: */*
>
* Mark bundle as not supporting multiuse
< HTTP/1.1 200 OK
< Date: Sat, 04 Mar 2023 12:13:25 GMT
< Content-Length: 94
< Content-Type: text/plain; charset=utf-8
<
resp method:POST url:/reqline?name=jack Proto:HTTP/1.1
resp URL查询参数:map[name:[jack]]
4.2 请求头
func requestHeaderHandler(w http.ResponseWriter, r *http.Request) {
// 头部
fmt.Fprintf(w, "header:\n")
for key, val := range r.Header {
fmt.Fprintf(w, "%v:%v\n", key, val)
}
fmt.Fprintf(w, "Get(\"Content-Type\"):%v\n", r.Header.Get("Content-Type"))
}
func main() {
// 1. 新建路由解码器
h := http.NewServeMux()
// 2. 路由注册
h.HandleFunc("/reqheader", requestHeaderHandler)
// 3. 服务启动 阻塞监听
http.ListenAndServe(":8000", h)
}
通过curl
发起POST
请求
可以看出curl
的请求报文和HTTP服务端获取到的请求Header都包含version:1.1.1
$ curl -v -d "age=18" -H "Accept-Language:en-US" -H "version:1.1.1" http://localhost:8000/reqheader
* Trying 127.0.0.1:8000...
* Connected to localhost (127.0.0.1) port 8000 (#0)
> POST /reqheader HTTP/1.1
> Host: localhost:8000
> User-Agent: curl/7.86.0
> Accept: */*
> Accept-Language:en-US
> version:1.1.1
> Content-Length: 6
> Content-Type: application/x-www-form-urlencoded
>
* Mark bundle as not supporting multiuse
< HTTP/1.1 200 OK
< Date: Sat, 04 Mar 2023 12:21:13 GMT
< Content-Length: 208
< Content-Type: text/plain; charset=utf-8
<
header:
Version:[1.1.1]
Content-Length:[6]
Content-Type:[application/x-www-form-urlencoded]
User-Agent:[curl/7.86.0]
Accept:[*/*]
Accept-Language:[en-US]
Get("Content-Type"):application/x-www-form-urlencoded
4.3 请求体
以下代码大致说明了Content-Type
为application/x-www-form-urlencoded
和application/json
的处理方法
func requestBodyHandler(w http.ResponseWriter, r *http.Request) {
contentType := r.Header.Get("Content-Type")
fmt.Fprintf(w, "Content-Type:%v\n", contentType)
// URL参数获取
fmt.Fprintf(w, "URL查询参数:%v\n", r.URL.Query())
// 解析 Content-Type为multipart/form-data的请求体
switch contentType {
case "application/x-www-form-urlencoded": // 表单默认的提交数据的格式
// 解析 URL查询参数 和 POST、PUT、PATCH的请求体参数
// 并将结果放入r.Form, 且POST、PUT、PATCH的请求体参数的优先级比URL查询参数高
// r.PostForm只存放POST、PUT、PATCH的请求体参数
r.ParseForm()
fmt.Fprintf(w, "PostForm:%v\n", r.PostForm)
fmt.Fprintf(w, "Form:%v\n", r.Form)
case "application/json": // json数据格式
json, _ := ioutil.ReadAll(r.Body)
fmt.Fprintf(w, "Json:%v", string(json))
default:
fmt.Fprintf(w, "unkown Content-Type")
}
}
通过curl
发起Content-Type
为application/x-www-form-urlencoded
的POST
请求,
URL
查询参数和请求体都包含name=
,从Form:map[age:[18] name:[juli jack]]
可以看出POST的请求体参数优先级更高。
$ curl -v http://localhost:8000/reqbody?name=jack -d "age=18" -d "name=juli"
* Trying 127.0.0.1:8000...
* Connected to localhost (127.0.0.1) port 8000 (#0)
> POST /reqbody?name=jack HTTP/1.1
> Host: localhost:8000
> User-Agent: curl/7.86.0
> Accept: */*
> Content-Length: 16
> Content-Type: application/x-www-form-urlencoded
>
* Mark bundle as not supporting multiuse
< HTTP/1.1 200 OK
< Date: Sat, 04 Mar 2023 12:35:15 GMT
< Content-Length: 151
< Content-Type: text/plain; charset=utf-8
<
Content-Type:application/x-www-form-urlencoded
URL查询参数:map[name:[jack]]
PostForm:map[age:[18] name:[juli]]
Form:map[age:[18] name:[juli jack]]
通过curl
发起Content-Type
为application/json
的请求。
$ curl -v http://localhost:8000/reqbody -H "Content-Type: application/json" -d "{"name":"jack","age":"18"}"
* Trying 127.0.0.1:8000...
* Connected to localhost (127.0.0.1) port 8000 (#0)
> POST /reqbody HTTP/1.1
> Host: localhost:8000
> User-Agent: curl/7.86.0
> Accept: */*
> Content-Type: application/json
> Content-Length: 18
>
* Mark bundle as not supporting multiuse
< HTTP/1.1 200 OK
< Date: Sat, 04 Mar 2023 12:38:34 GMT
< Content-Length: 75
< Content-Type: text/plain; charset=utf-8
<
Content-Type:application/json
URL查询参数:map[]
Json:{name:jack,age:18}
5. Golang HTTP响应处理
func respHandler(w http.ResponseWriter, r *http.Request) {
// 设置响应头
w.Header().Set("name", "jack")
// 设置cookie
ck := &http.Cookie{
Name: "testCookie",
Value: "cookieval",
Path: "/",
HttpOnly: true,
}
http.SetCookie(w, ck)
// 设置响应体
w.Write([]byte("hello i'm jack"))
// 设置响应状态码
w.WriteHeader(http.StatusOK)
}
func main() {
// 1. 新建路由解码器
h := http.NewServeMux()
// 2. 路由注册
h.HandleFunc("/resp", respHandler)
// 3. 服务启动 阻塞监听
http.ListenAndServe(":8000", h)
}
值得注意的是,设置Header、设置Cookie必须先于设置响应体,否则不会生效
$ curl -v http://localhost:8000/resp
* Trying 127.0.0.1:8000...
* Connected to localhost (127.0.0.1) port 8000 (#0)
> GET /resp HTTP/1.1
> Host: localhost:8000
> User-Agent: curl/7.86.0
> Accept: */*
>
* Mark bundle as not supporting multiuse
< HTTP/1.1 200 OK
< Name: jack
< Set-Cookie: testCookie=cookieval; Path=/; HttpOnly
< Date: Sat, 04 Mar 2023 13:15:15 GMT
< Content-Length: 14
< Content-Type: text/plain; charset=utf-8
<
hello i'm jack