likes
comments
collection
share

【Web】基于标准库实现http服务

作者站长头像
站长
· 阅读数 18

前言

前面我们已经了解了net/http标准库实现的http服务,其底层实现了端接口监听,连接的创建和http协议的解析等功能,对于上层业务而言,我们仅需要关注不同的URI及其对应的处理逻辑即可。本节将基于net/http库来实现一个简单的http服务。

默认多路复用器

http.ListenAndServe() 传入的 handlernil 时,其使用的是默认多路复用器 DefaultServeMux ,通过 http.Handlehttp.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 这两个框架都是我比较喜欢的,仅关注路由,数据输入输出这几个方面,对于系统的扩展非常地舒服。