【Web】基于标准库实现http服务
前言
前面我们已经了解了net/http标准库实现的http服务,其底层实现了端接口监听,连接的创建和http协议的解析等功能,对于上层业务而言,我们仅需要关注不同的URI及其对应的处理逻辑即可。本节将基于net/http库来实现一个简单的http服务。
默认多路复用器
当 http.ListenAndServe()
传入的 handler
为 nil
时,其使用的是默认多路复用器 DefaultServeMux
,通过 http.Handle
或 http.HandleFunc
方法往 DefaultServeMux
注册不同URI
对应的处理逻辑
type CustomHandler struct{}
func (c *CustomHandler) ServeHTTP(http.ResponseWriter, *http.Request) {}
func main() {
http.HandleFunc("/r1", func(http.ResponseWriter, *http.Request) {})
http.Handle("/r2", &CustomHandler{})
http.ListenAndServe(":8080", nil)
}
http.HandleFunc
内部会将第二个参数强制转换成 HandlerFunc
类型,而该类型实现了 ServeHTTP
接口,所以其本质和 http.Handle
是一样的。由之前的分析我们知道net/http实现的路由匹配规则过于简单,很难满足日常的需求开发,由此衍生了许多的web框架,他们基本都会实现一套自己的路由匹配。
自定义多路复用器
http.ListenAndServe
第二个参数不为 nil
时,所使用的就是自定义的多路复用器,需要实现 ServeHTTP
方法。这里仅做演示,不讨论过深的路由算法,因此用个map来简单实现,以后讲具体的web框架时,再深入探讨
type CustomHandler struct {
Routers map[string]func(w http.ResponseWriter, r *http.Request)
}
func (c *CustomHandler) Get(pattern string, handler func(w http.ResponseWriter, r *http.Request)) {
c.Routers["GET_"+pattern] = handler
}
func (c *CustomHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
key := r.Method + "_" + r.URL.Path
if h, ok := c.Routers[key]; ok {
h(w, r)
} else {
w.Write([]byte("404"))
}
}
func main() {
h := &CustomHandler{Routers: make(map[string]func(w http.ResponseWriter, r *http.Request))}
h.Get("/ping", func(w http.ResponseWriter, r *http.Request) { w.Write([]byte("pong")) })
http.ListenAndServe(":8080", h)
}
中间件
有时候我们想在请求受理前,先进行一些处理,例如鉴权,请求日志等。基本主流的一些web框架都会提供这样的功能。这里有多种实现方式,其本质就个函数切片,在执行最终的业务代码前,先一层一层地执行这些注册好的公共函数。
type handleFunc func(w http.ResponseWriter, r *http.Request)
type CustomHandler struct {
middlewares []func(handleFunc) handleFunc
Routers map[string]handleFunc
}
func (c *CustomHandler) Use(m func(handleFunc) handleFunc) {
c.middlewares = append(c.middlewares, m)
}
func (c *CustomHandler) POST(pattern string, handler handleFunc) {
c.Routers["POST_"+pattern] = handler
}
func (c *CustomHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
key := r.Method + "_" + r.URL.Path
h, ok := c.Routers[key]
if !ok {
w.Write([]byte("404"))
return
}
tmp := h
for _, m := range c.middlewares {
tmp = m(tmp)
}
tmp(w, r)
}
func main() {
h := &CustomHandler{Routers: make(map[string]handleFunc)}
h.Use(m1)
h.POST("/ping", func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("pong"))
})
http.ListenAndServe(":8080", h)
}
func m1(next handleFunc) handleFunc {
return func(w http.ResponseWriter, r *http.Request) {
if r.Header.Get("m1") == "m1" {
next(w, r)
} else {
w.Write([]byte("abort"))
}
}
}
因为引入了中间件的概念,部分信息可能需要共享,因此往往都会封装一个结构体(通常取名 context)来进行管理,这里就不过多的展开。
请求的输入和输出
一个请求的处理无非就是关注输入和输出。我们先讨论输入,golang的底层帮助我们解析了http报文,并将数据以 io.ReadCloser
的形式暴露给业务层,通常我们使用最多的就是json
数据,下面介绍这种形式的数据如何进行反序列化。
h.POST("/ping", func(w http.ResponseWriter, r *http.Request) {
var buf = make([]byte, r.ContentLength)
// 不建议使用这个方式,当数据过大,网络不佳时,这里仅会读取到一部分数据
n, err := r.Body.Read(buf)
if (err != nil && err != io.EOF) || (n != int(r.ContentLength)) {
//TODO
return
}
var data struct {
Field1 string `json:"f1"`
Field2 string `json:"f2"`
}
if err := json.Unmarshal(buf, &data); err != nil {
// TODO
return
}
log.Println(data)
})
通过 r.Body.Read
直接读取的方式通常不太建议,由于网络原因,超过一定时间没有从socket缓冲区读取到数据,Read
方法就直接返回了,导致了数据的截断,通常都是通过 for
循环读取直到 io.EOF
或其他错误结束。下面的方法比较常用
h.POST("/ping", func(w http.ResponseWriter, r *http.Request) {
decoder := json.NewDecoder(r.Body)
var data struct {
Field1 string `json:"f1"`
Field2 string `json:"f2"`
}
if err := decoder.Decode(&data); err != nil {
// TODO
return
}
log.Println(data)
})
json.Decoder
底层就是for
循环去读取的,这里不得不吐槽下go得if err
是真的恶心😅。有些请求参数是放在url query里面的(一般是GET请求),通过 r.URL.Query().Get()
可以获取。还有部分信息从header里面读取的 r.Header.Get()
,这类信息通常在中间件使用并处理,对于业务层而言并不用过多关心
输出就比较简单了,把数据序列化以下,调用 Write
方法输出即可。
h.POST("/ping", func(w http.ResponseWriter, r *http.Request) {
var data = map[string]string{
"f1": "a",
"f2": "b",
}
buf, _ := json.Marshal(data)
w.Write(buf)
})
对于输入和输出而言,很多现有的框架都会再封装一下。对于请求参数而言,通常还会通过反射来对参数值做一些统一地校验,这里不过多展开。
HTML 渲染
由于现在很多项目都是前后端分离的架构了,页面渲染这一块功能对于后端而言其实已经变得不那么重要了,更多地沦为了一个数据api的工具人
总结
对于一个web框架而言,我更过的注重于路由,中间件,输入输出这几方面功能的,集成太多的功能反而使系统变得很臃肿,gin 和 Martini 这两个框架都是我比较喜欢的,仅关注路由,数据输入输出这几个方面,对于系统的扩展非常地舒服。
转载自:https://juejin.cn/post/7169573616760619044