本文将正式开始HTTP协议的解析,主要完成Request结构体的属性组成,HTTP请求报文首部字段以及请求行的解析,难度适中。
项目结构
因为在这一章中,我们会完成客户端请求报文中首部字段的解析,首部字段为一个个键值对,显而易见的是使用map结构进行一个存储,所以增加header.go
文件,其中存在Header
结构体,专门用于存放请求报文中请求行的键值对。此处并非重点也极为简单,所以直接从标准库中拷贝,读者自行理解:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
|
package httpd
type Header map[string][]string
func (h Header) Add(key, value string) {
h[key] = append(h[key], value)
}
// 插入键值对
func (h Header) Set(key, value string) {
h[key] = []string{value}
}
// 获取值,key不存在则返回空
func (h Header) Get(key string) string {
if value, ok := h[key]; ok && len(value) > 0 {
return value[0]
} else {
return ""
}
}
// 删除键
func (h Header) Del(key string) {
delete(h, key)
}
|
今天的项目架构如下:
2
├── go.mod
├── httpd
│ ├── conn.go
│ ├── header.go
│ ├── request.go
│ ├── response.go
│ └── server.go
└── main.go
除了增加header.go外,其他所有的更改都在request.go中。
代码传送门
需求分析
我们需要为Request
结构体增加相应的属性,就应该从http请求报文出发,看看我们需要保存哪些信息。一段http请求报文:
1
2
3
4
5
6
7
8
9
10
|
POST /index?name=gu HTTP/1.1\r\n #请求行
Content-Type: text/plain\r\n #此处至报文主体为首部字段
User-Agent: PostmanRuntime/7.28.0\r\n
Host: 127.0.0.1:8080\r\n
Accept-Encoding: gzip, deflate, br\r\n
Connection: keep-alive\r\n
Cookie: uuid=12314753; tid=1BDB9E9; HOME=1\r\n
Content-Length: 18\r\n
\r\n
hello,I am client! #报文主体
|
http的请求报文分为三部分:
-
请求行(第一行)分别由方法Method
、请求路径URL
以及协议版本Proto
组成。将这三者加入到Request结构即可。
-
首部字段由一个个键值对组成,我们的头部信息就存放在此处。这部分就用上面讲到了的Header
存储。
-
报文主体部分,相较于前面两个更为复杂,可能具有不同的编码方式,长度也可能特别大。平时前端提交的form表单就放置在报文主体部分。仅只有POST
和PUT
请求允许携带报文主体。
本篇只涉及前两部分的解析,读者需知。
除此之外,像cookie以及queryString(如上面的URL中的?name=gufeijun),是日常开发经常使用到的部分,为了方便用户的获取,我们分别用cookies
以及queryString
这两个map去保存解析后的字段。因此Request结构体如下:
1
2
3
4
5
6
7
8
9
10
11
12
|
type Request struct{
Method string //请求方法,如GET、POST、PUT
URL *url.URL //URL
Proto string //协议以及版本
Header Header //首部字段
Body io.Reader //用于读取报文主体
RemoteAddr string // 客户端地址
RequestURI string //字符串形式的url
conn *conn //产生此request的http连接
cookies map[string]string //存储cookie
queryString map[string]string //存储queryString
}
|
Body字段在下一章中讲解,读者只需要知道用户可以对(*Request).Body
读取,进而读出当前请求的报文主体即可。
readRequest
上一章我们将框架的骨干搭好后,这里开始第一个空白处readRequest
的填充,它的作用就是从tcp字节流中解析http报文,从而封装出一个Request
对象。
直接上代码,请对比着上述的http报文观看:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
|
func readRequest(c *conn)(r *Request,err error){
r = new(Request)
r.conn = c
r.RemoteAddr = c.rwc.RemoteAddr().String()
//读出第一行,如:Get /index?name=gu HTTP/1.1
line, err := readLine(c.bufr)
if err != nil {
return
}
// 按空格分割就得到了三个属性
_, err = fmt.Sscanf(string(line), "%s%s%s", &r.Method, &r.RequestURI, &r.Proto)
if err != nil {
return
}
// 将字符串形式的URI变成url.URL形式
r.URL, err = url.ParseRequestURI(r.RequestURI)
if err != nil {
return
}
//解析queryString
r.parseQuery()
//读header
r.Header, err = readHeader(c.bufr)
if err != nil {
return
}
const noLimit = (1<<63)-1
r.conn.lr.N = noLimit //Body的读取无需进行读取字节数限制
//设置body
r.setupBody()
return r, nil
}
|
6~16行为解析请求行,分别读出Method、URI以及Proto。21行解析queryString。23行读取首部字段。30行设置Body。上述代码中有四个辅助函数:readLine
、parseQuery
、readHeader
以及setupBody
,这四个我们一个一个单独讲解。
readLine
上文提到bufio.Reader
具有ReadLine
方法,其存在三个返回参数line []byte, isPrefix bool, err error
,line和err都很好理解,但为什么还多出了一个isPrefix参数呢?这是因为ReadLine会借助到bufio.Reader的缓存切片(见上篇),如果一行大小超过了缓存的大小,这也会无法达到读出一行的要求,这时isPrefix会设置成true,代表只读取了一部分。
因此我们需要对ReadLine方法进行封装,得到readLine
函数,能够保证缓存容量不足的情况下也能读出一行,如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
func readLine(bufr *bufio.Reader) ([]byte, error) {
p, isPrefix, err := bufr.ReadLine()
if err != nil {
return p, err
}
var l []byte
for isPrefix {
l, isPrefix, err = bufr.ReadLine()
if err != nil {
break
}
p = append(p, l...)
}
return p, err
}
|
只要isPrefix一直为true,我们则一直读取,并将读取的部分汇总在一起,直至读到\r\n
。
parseQuery
对于客户端访问的url,如127.0.0.0?name=gu&token=1234
,其中name=gu&token=1234即为queryString字段,将这部分以KV方式存入map中。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
func (r *Request) parseQuery() {
//r.URL.RawQuery="name=gu&token=1234"
r.queryString = parseQuery(r.URL.RawQuery)
}
func parseQuery(RawQuery string) map[string]string {
parts := strings.Split(RawQuery, "&")
queries := make(map[string]string, len(parts))
for _, part := range parts {
index := strings.IndexByte(part, '=')
if index == -1 || index == len(part)-1 {
continue
}
queries[strings.TrimSpace(part[:index])] = strings.TrimSpace(part[index+1:])
}
return queries
}
|
先以&符为分隔得到一个个k-v对,然后以=符为分割分别得到key以及value,存入map即可。
readHeader实现很简单,一直调用readLine
读取一行,如果碰到CR+LF(\r\n\r\n
),这时readLine读取到的该行长度为0,也即代表首部字段的结束。我们将读到的每一行都保存到header这个map中,代码如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
|
func readHeader(bufr *bufio.Reader) (Header, error) {
header := make(Header)
for {
line, err := readLine(bufr)
if err != nil {
return nil, err
}
//如果读到/r/n/r/n,代表报文首部的结束
if len(line) == 0 {
break
}
//example: Connection: keep-alive
i := bytes.IndexByte(line, ':')
if i == -1 {
return nil, errors.New("unsupported protocol")
}
if i == len(line)-1 {
continue
}
k, v := string(line[:i]), strings.TrimSpace(string(line[i+1:]))
header[k] = append(header[k], v)
}
return header, nil
}
|
setupBody
Body的设置是本框架的重难点之一,它是一个提供给用户的读取报文主体的接口,其将涉及到报文主体的解析,将是下一章的内容。我们这里暂时将Body设置成eofReader,让用户目前在Handler中无法从Body中读取任何数据。此处读者可以暂时略过。
1
2
3
4
5
6
7
8
9
10
|
type eofReader struct {}
// 实现了io.Reader接口
func (er *eofReader)Read([]byte)(n int,err error){
return 0,io.EOF
}
func (r *Request)setupBody(){
r.Body=new(eofReader)
}
|
绑定方法
Request结构中的cookie以及queryString字段都是私有属性,因为只希望用户具有查询的权限,而不能够进行删除或者修改。为了让用户去查询这个私有字段,需要绑定相应的公共方法,这就是封装的思想。
除了安全性之外,利用公有方法的方式也能让我们的控制更加灵活,可以实现懒加载(lazy load),从而提升性能。
比如在gin框架中每一个gin.Context
中都会有一个叫做keys的map,用于在HandlerChain中传输数据,用户可以调用Set
方法存数据,Get
方法取数据。显而易见,为了实现这个功能,我们需要在操作keys这个map之前,就为其make分配内存。问题就出现在,如果我在生成一个gin.Context
之初就为这个map进行初始化,但如果用户的Handler中并未使用这个功能怎么办?这个为keys初始化的时间是不是白白浪费了?
所以gin采用了比较高明的方式,在用户使用Set
方法时,Set方法会先检测keys这个map是否为nil,如果为nil,这时我们才为其初始化。这样懒加载就能减少一些不必要的开销。
我们给域名生成的cookie,一旦颁发给用户浏览器之后,浏览器在访问我们域名下的后端接口时都会在请求报文中将这个cookie带上,要是后端接口不关系客户端的cookie,而框架无脑全部提前解析,这就做了徒工。
所以也需要将Cookie的解析滞后,不是在readRequest
中解析,而是在用户接口有需求,调用Cookie方法第一次查询时再进行解析。这就是为什么readRequest
中没有解析cookie代码的原因。
接下来为Request绑定两个公有方法Query
以及Cookie
,分别用于查询queryString以及cookie:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
|
// 查询queryString
func (r *Request) Query(name string) string {
return r.queryString[name]
}
// 查询cookie
func (r *Request) Cookie(name string) string {
if r.cookies == nil { //将cookie的解析滞后到第一次cookie查询处
r.parseCookies()
}
return r.cookies[name]
}
func (r *Request) parseCookies() {
if r.cookies != nil {
return
}
r.cookies = make(map[string]string)
rawCookies, ok := r.Header["Cookie"]
if !ok {
return
}
for _, line := range rawCookies {
//example(line): uuid=12314753; tid=1BDB9E9; HOME=1(见上述的http请求报文)
kvs := strings.Split(strings.TrimSpace(line), ";")
if len(kvs) == 1 && kvs[0] == "" {
continue
}
for i := 0; i < len(kvs); i++ {
//example(kvs[i]): uuid=12314753
index := strings.IndexByte(kvs[i], '=')
if index == -1 {
continue
}
r.cookies[strings.TrimSpace(kvs[i][:index])] = strings.TrimSpace(kvs[i][index+1:])
}
}
return
}
|
解析很简单,就是字符串的处理。
测试
为了方便我们的测试,我们为response.go
进行如下更改:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
type response struct{
c *conn
}
type ResponseWriter interface{
Write([]byte)(n int,err error)
}
func setupResponse(c *conn)*response{
return &response{c:c}
}
func (w *response) Write(p []byte)(int,error){
return w.c.bufw.Write(p)
}
|
为ResponseWriter加入Write方法,同时让response实现这个接口。这样我们可以在Handler中可以使用Write方法与客户端交互,从而手动构造http响应报文。
同时记得要在conn.go文件中对setupResponse进行调用:
1
2
3
|
func (c *conn) setupResponse()*response{
return setupResponse(c)
}
|
测试文件main.go如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
|
package main
import (
"bytes"
"example/httpd"
"fmt"
"io"
)
type myHandler struct {}
func (*myHandler) ServeHTTP(w httpd.ResponseWriter,r *httpd.Request){
// 用户的头部信息保存到buff中
buff:=&bytes.Buffer{}
// 测试Request的解析
fmt.Fprintf(buff,"[query]name=%s\n",r.Query("name"))
fmt.Fprintf(buff,"[query]token=%s\n",r.Query("token"))
fmt.Fprintf(buff,"[cookie]foo1=%s\n",r.Cookie("foo1"))
fmt.Fprintf(buff,"[cookie]foo2=%s\n",r.Cookie("foo2"))
fmt.Fprintf(buff,"[Header]User-Agent=%s\n",r.Header.Get("User-Agent"))
fmt.Fprintf(buff,"[Header]Proto=%s\n",r.Proto)
fmt.Fprintf(buff,"[Header]Method=%s\n",r.Method)
fmt.Fprintf(buff,"[Addr]Addr=%s\n",r.RemoteAddr)
fmt.Fprintf(buff,"[Request]%+v\n",r)
//手动发送响应报文
io.WriteString(w, "HTTP/1.1 200 OK\r\n")
io.WriteString(w, fmt.Sprintf("Content-Length: %d\r\n",buff.Len()))
io.WriteString(w,"\r\n")
io.Copy(w,buff) //将buff缓存数据发送给客户端
}
func main(){
svr:=&httpd.Server{
Addr: "127.0.0.1:8080",
Handler: new(myHandler),
}
panic(svr.ListenAndServe())
}
|
利用curl测试我们的接口:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
# -b发送cookie,-i显示响应报文
$ curl "127.0.0.1:8080?name=gu&token=123" -b "foo1=bar1;foo2=bar2;" -i
输出:
HTTP/1.1 200 OK
Content-Length: 488
[query]name=gu
[query]token=123
[cookie]foo1=bar1
[cookie]foo2=bar2
[Header]User-Agent=curl/7.55.1
[Header]Proto=HTTP/1.1
[Header]Method=GET
[Addr]Addr=127.0.0.1:4179
[Request]&{Method:GET URL:/?name=gu&token=123 Proto:HTTP/1.1 Header:map[Accept:[*/*] Cookie:[foo1=bar1;foo2=bar2;] Host:[127.0.0.1:8080] User-Agent:[curl/7.55.1]] Body:0x489290 RemoteAddr:127.0.0.1:4179 RequestURI:/?name=gu&token=123 conn:0xc00007e690 cookies:map[foo1:bar1 foo2:bar2] queryString:map[name:gu token:123]}
|
这一章我们对Request完成了请求行以及首部字段的解析,同时提供了更简便的查询queryString以及cookie的方法,我们已经完成了除POST
以及PUT
之外请求地解析。下一章我们将对Request的Body属性进行更为细致地设置,即对应POST请求的解析。
系列目录