Go 语言通过 Request 对象读取 HTTP 请求报文
今天开始,我们将继续开发 Go 语言 Web 开发之旅。
在前面的教程中,学院君给大家介绍了 Go 语言中 HTTP 服务器的实现和内置的路由分发实现,以及第三方的路由器解决方案 —— gorilla/mux,接下来,我们将注意力转移到路由分发之后的业务逻辑,比如 HTTP 请求处理,包括请求参数的解析、表单验证、文件上传等,以及 HTTP 响应发送,包括响应头设置、文件下载、视图模板等。
HTTP 请求报文结构
首先来看 HTTP 请求。了解 HTTP 协议的同学应该都知道,一个完整的 HTTP GET 请求报文结构如下:
包含请求行、请求头(首部字段)和请求实体(请求主体)三部分,请求行中包含了请求方法、URL 和 HTTP 协议版本,请求头中包含了 HTTP 请求首部字段,对于 GET 请求来说,没有提交表单数据,所以请求实体为空,对于 POST 请求来说,会包含包括表单数据的请求实体,对这块不够了解的同学可以网上看下 HTTP 协议或者阅读程序员内功修炼部分的 HTTP 报文简介及组成结构深入探索 HTTP 协议底层原理。
Request 结构体
Go 通过一个 Request
结构体来表示 HTTP 请求报文,这一点,我们在前面的处理器编写时已经看到了,这个结构体位于内置的 net/http
包中,其中包含了 HTTP 请求的所有信息,包括请求 URL、请求头、请求实体、表单信息等,平时常用的、比较重要的一些字段如下所示:
- URL:请求 URL
- Method:请求方法
- Proto:HTTP 协议版本
- Header:请求头(字典类型的键值对集合)
- Body:请求实体(实现了
io.ReadCloser
接口的只读类型) - Form、PostForm、MultipartForm:请求表单相关字段,可用于存储表单请求信息
另外还有很多其他字段,比如 Host、From、ContentLength 等,这里就不一一列举了,感兴趣的同学可以自行去查看。
请求 URL
对于一个客户端 HTTP 请求来说,请求行中的最重要的当属 URL 信息,否则无法对服务器发起请求,比如我们访问 Google 首页进行搜索,需要现在浏览器地址栏输入 Google 首页的 URL:
这里的 https://www.google.com
就是请求 URL。
在 Go 语言的 http.Request
对象中,用于表示请求 URL 的 URL 字段是一个 url.URL
类型的指针:
我们来简单介绍下其中常见的字段:
Scheme
表示 HTTP 协议是 HTTPS 还是 HTTP,在上面的例子中是https
;- 对于一些需要认证才能访问的应用,需要提供
User
信息; Host
字段表示域名/主机信息,如果服务器监听端口不是默认的 80 端口的话,还需要通过:端口号
的方式补充端口信息,在上面的例子中是www.google.com
;Path
表示 HTTP 请求路径,一般应用首页是空字符串,或者/
;Query
相关字段表示 URL 中的查询字符串,也就是 URL 中?
之后的部分;Fragment
表示 URL 中的锚点信息,也就是 URL 中#
之后的部分。
因此,常见的 URL 完整格式如下:
scheme://[user@]host/path[?query][#fragment]
如果不包含 /
的话,URL 解析后的结果如下:
scheme:opaque[?query][#fragment]
例如,对 https://laravel.geekai.co/books/golang-tutorials?page=2#comments
而言,Scheme 值是 https
,Host
值是 xueyuanjun.com
,Path
值是 /books/golang-tutorials
,RawQuery
值是 page=2
,我们后面还会演示如何通过 Form
来解析并获取查询字符串中的参数值,Fragment
值是 comments
。
有趣的是,如果请求是从浏览器发送的话,我们无法获取 URL 中的 Fragment
信息,这不是 Go 的问题,而是浏览器根本没有将其发送到服务端。那为什么还要提供这个字段呢?因为不是所有的请求都是从浏览器发送的,而且 Request
也可以在客户端库中使用。
我们可以编写一段测试代码进行演示,还是以 github.com/xueyuanjun/goblog
为例,在 routes/router.go
中,新增如下测试代码:
package routes
import (
"encoding/json"
"github.com/gorilla/mux"
"log"
"net/http"
)
// 返回一个 mux.Router 类型指针,从而可以当作处理器使用
func NewRouter() *mux.Router {
// 创建 mux.Router 路由器示例
router := mux.NewRouter().StrictSlash(true)
// 应用请求日志中间件
router.Use(loggingRequestInfo)
// 遍历 web.go 中定义的所有 webRoutes
for _, route := range webRoutes {
// 将每个 web 路由应用到路由器
router.Methods(route.Method).
Path(route.Pattern).
Name(route.Name).
Handler(route.HandlerFunc)
}
return router
}
// 记录请求日志信息中间件
func loggingRequestInfo(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// 打印请求 URL 明细
url, _ := json.Marshal(r.URL)
log.Println(string(url))
next.ServeHTTP(w, r)
})
}
我们通过新增一个 loggingRequestInfo
中间件记录所有请求 URL 的明细,这里我们通过 JSON 对 URL 对象进行编码,以便可读性更好。
在 goblog
目录下通过 go run main.go
启动 HTTP 服务器:
然后新开一个 Terminal 窗口,通过 curl 运行几组测试请求:
然后就可以在运行 HTTP 服务器的窗口看到请求日志了:
可以看到,Scheme、Host、Fragment 信息都是空的。Fragment 为空的原因上面已经提到,Scheme 需要根据是否启用 HTTPS 进行设置,Host 为空的原因是没有通过代理访问 HTTP 服务器,并且在本地开发环境中,Host 始终为空。
请求头
请求头和响应头都通过 Header
字段表示,Header
是一个键值对字典,键是字符串,值是字符串切片。Header
提供了增删该查方法用于对请求头进行读取和设置。
读取/打印请求头
要获取某个请求头的值很简单,通过 Header
对象提供的 Get
方法,传入对应的字段名即可,比如要获取请求头中 User-Agent
字段,可以这么做:
r.Header.Get("User-Agent")
要打印完整的请求头,传入整个 r.Header
对象到打印函数即可。
我们修改 routes/router.go
中的中间件函数 loggingRequestInfo
,新增打印请求头代码,并且将原来打印 URL 结构体代码调整为打印 URL 字符串:
// 记录请求日志信息中间件
func loggingRequestInfo(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
fmt.Printf("Request URL: %s\n", r.URL)
fmt.Printf("User Agent: %s\n", r.Header.Get("User-Agent"))
fmt.Printf("Request Header: %v\n", r.Header)
next.ServeHTTP(w, r)
})
重新启动 HTTP 服务器,分别通过命令行 curl 和浏览器请求应用首页,可以看到日志信息如下:
由于 curl 访问没有设置额外请求头,所以信息很少,而浏览器会加上很多请求头,所以信息更丰富。
新增/修改/删除请求头
此外,我们还可以通过 Header
提供的 Add
方法新增请求头:
r.Header.Add("test", "value1")
通过 Header
提供的 Set
方法修改请求头:
r.Header.Set("test", "value2")
以及通过 Header
提供的 Del
方法删除请求头:
r.Header.Del("test")
请求实体
请求实体和响应实体都通过 Body
字段表示,该字段是 io.ReadCloser
接口类型。顾名思义,这个类型实现了 io.Reader
和 io.Closer
接口。
io.Reader
提供了 Read
方法,用于读取传入的字节切片并返回读取的字节数以及错误信息,io.Closer
提供了 Close
方法,因此,你可以在 Body
上调用 Read
方法读取请求实体的内容,调用 Close
方法释放资源。
对于请求实体来说,对应的 Body
访问路径是 http.Request.Body
,下面我们编写一段测试代码来演示请求实体的读取,在 goblog/handlers/post.go
中新增一个 AddPost
处理器方法:
func AddPost(w http.ResponseWriter, r *http.Request) {
len := r.ContentLength // 获取请求实体长度
body := make([]byte, len) // 创建存放请求实体的字节切片
r.Body.Read(body) // 调用 Read 方法读取请求实体并将返回内容存放到上面创建的字节切片
io.WriteString(w, string(body)) // 将请求实体作为响应实体返回
}
由于 GET 请求没有请求实体,所以需要通过 POST/PUT/DELETE 之类的请求进行测试,我们在 routes/web.go
中新增一个 Web 路由:
WebRoute{
"NewPost",
"POST",
"/post/add",
handlers.AddPost,
},
重启 HTTP 服务器,要测试这段代码,需要发起 POST 请求:
-id
是两个选项的组合,-i
表示输出 HTTP 响应的详细报文,-d
表示传递的表单数据。HTTP 响应报文与响应头通过空行进行分隔,可以看到,在响应实体中打印的正是传递的请求实体信息。
通常,我们不会一次性获取所有的请求实体信息,而是通过类似 FormValue
之类的方法获取每个请求参数,我们将在下一篇教程中详细介绍如何获取 HTTP 表单请求数据。
2 Comments
期待下一节,把请求和响应走通,就可以写个爬虫了
嗯 今天会发布