Go语言经典库使用分析(四)| Gorilla Handlers 源代码实现分析

飞雪无情 · · 271 次点击 · · 开始浏览    
这是一个创建于 的文章,其中的信息可能已经有所发展或是发生改变。

Go语言经典库使用分析,未完待续,欢迎扫码关注公众号flysnow_org或者网站http://www.flysnow.org/,第一时间看后续系列。觉得有帮助的话,顺手分享到朋友圈吧,感谢支持。

上一篇 Go语言经典库使用分析(三)| Gorilla Handlers 详细介绍 中介绍了Handlers常用中间件的使用,这一篇介绍下这些中间件实现的原理,以了解他们的实现原理,更好的理解Go Http中间件的设计。

LoggingHandler

这是一个实现了记录HTTP Request访问日志的中间件,通过它我们把日志记录到任何实现了io.Writer类型中。

1
2
3
func LoggingHandler(out io.Writer, h http.Handler) http.Handler {
return loggingHandler{out, h}
}

第一个参数out就是我们刚刚提到的,要把日志输出到哪里,第二个就是我们自己的handler,也就是要被拦截包装的那个handler。

该函数返回的是一个loggingHandler类型,所以该loggingHandler类型肯定实现了http.Handler接口。

1
2
3
4
5
6
7
8
9
10
11
12
type loggingHandler struct {
writer io.Writer
handler http.Handler
}
func (h loggingHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) {
t := time.Now()
logger := makeLogger(w)
url := *req.URL
h.handler.ServeHTTP(logger, req)
writeLog(h.writer, req, url, t, logger.Status(), logger.Size())
}

关键在于这个实现http.Handler接口的ServeHTTP,他里面的实现记录了我们HTTP Request的日志。它先通过makeLogger函数创建一个日志记录器,然后获取请求的URL信息,接着就调用原始http.Handler的ServeHTTP方法,这一步非常重要,如果不调用的话,我们原来的Handler将会不起作用,就是因为ServeHTTP的调用,才使得我们可以组成一个Handler中间件处理链,不停的一层层调用下去。

日志的输出是在最后,这样不会影响HTTP Request的响应,用户可以及时得到反馈。

因为我们日志输出所需的信息都收集好了,然后就开始调用writeLog函数输出日志了。在分析这个writeLog之前,我们先看下日志记录器的函数makeLogger

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
func makeLogger(w http.ResponseWriter) loggingResponseWriter {
var logger loggingResponseWriter = &responseLogger{w: w, status: http.StatusOK}
if _, ok := w.(http.Hijacker); ok {
logger = &hijackLogger{responseLogger{w: w, status: http.StatusOK}}
}
h, ok1 := logger.(http.Hijacker)
c, ok2 := w.(http.CloseNotifier)
if ok1 && ok2 {
return hijackCloseNotifier{logger, h, c}
}
if ok2 {
return &closeNotifyWriter{logger, c}
}
return logger
}

该函数本质上是为了获取ResponseWriter返回的内容大小,以便接下来的日志输出。该函数返回一个接口loggingResponseWriter,从代码实现可以看到,该接口有好几个实现,分别是responseLogger,hijackLogger,hijackCloseNotifier以及closeNotifyWriter,这么频繁做的目的,是想保留参数w对应的类型的所有实现接口,不至于因为接口的转换,而丢失实现。

上面的文字可能有点绕,我们通过一个例子说明:

  1. T1结构体实现了接口I1,I2
  2. T2结构体里有个字段,类型是I1,type T2 struct {i1 I1}
  3. 那么T1可以作为T2的成员参数,构建出T2
  4. 但是构建出的T2,只被认为实现了I1,并没有实现I2
  5. 也就是说,在类型转换的过程过,丢失了对I2的接口实现

这就是以上makeLogger这么频繁的判断w到底实现了哪些接口,尽可能的保留所有实现的接口,因为可能会 用到。

但是这么做不好,因为使用的是罗列法,如果有GoSDK升级,该w多实现了一个新接口呢,还要改代码去罗列,如果忘记了呢?岂不是丢失了实现的接口。

现在看看关键的实现计算返回内容大小的逻辑。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
type responseLogger struct {
w http.ResponseWriter
status int
size int
}
func (l *responseLogger) Write(b []byte) (int, error) {
size, err := l.w.Write(b)
l.size += size
return size, err
}
func (l *responseLogger) Size() int {
return l.size
}

通过写入字节的累加实现,最终都保存在size字段里,通过Size方法获取。该responseLogger实现了loggingResponseWriter接口。

有了日志需要的相关信息后,就可以调用writeLog输出日志了,这个格式主要是日志格式信息的拼接以及输出,没有太多值得讲的,大家自己看下源代码,就不一一列举了。

CombinedLoggingHandler

CombinedLoggingHandler和LoggingHandler一样,只不过多了一些UA等额外信息。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
func writeLog(w io.Writer, req *http.Request, url url.URL, ts time.Time, status, size int) {
buf := buildCommonLogLine(req, url, ts, status, size)
buf = append(buf, '\n')
w.Write(buf)
}
func writeCombinedLog(w io.Writer, req *http.Request, url url.URL, ts time.Time, status, size int) {
buf := buildCommonLogLine(req, url, ts, status, size)
buf = append(buf, ` "`...)
buf = appendQuoted(buf, req.Referer())
buf = append(buf, `" "`...)
buf = appendQuoted(buf, req.UserAgent())
buf = append(buf, '"', '\n')
w.Write(buf)
}

两个日志的输出函数非常相似,writeCombinedLog多了Request的Referer和UA信息。其他的就和LoggingHandler一样了,这里就不一一介绍了。

CompressHandler

这是一个压缩响应内容的中间件,他可以根据请求的Accept-Encoding头信息中的编码信息进行压缩,默认情况采用默认压缩级别。

1
2
3
func CompressHandler(h http.Handler) http.Handler {
return CompressHandlerLevel(h, gzip.DefaultCompression)
}

CompressHandlerLevel函数是整个处理的压缩中间件处理的核心,他会替换使用一个自定义的http.ResponseWriter替换原来的,达到压缩的目的。

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
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
func CompressHandlerLevel(h http.Handler, level int) http.Handler {
if level < gzip.DefaultCompression || level > gzip.BestCompression {
level = gzip.DefaultCompression
}
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
L:
for _, enc := range strings.Split(r.Header.Get("Accept-Encoding"), ",") {
switch strings.TrimSpace(enc) {
case "gzip":
w.Header().Set("Content-Encoding", "gzip")
w.Header().Add("Vary", "Accept-Encoding")
gw, _ := gzip.NewWriterLevel(w, level)
defer gw.Close()
h, hok := w.(http.Hijacker)
if !hok { /* w is not Hijacker... oh well... */
h = nil
}
f, fok := w.(http.Flusher)
if !fok {
f = nil
}
cn, cnok := w.(http.CloseNotifier)
if !cnok {
cn = nil
}
w = &compressResponseWriter{
Writer: gw,
ResponseWriter: w,
Hijacker: h,
Flusher: f,
CloseNotifier: cn,
}
break L
case "deflate":
w.Header().Set("Content-Encoding", "deflate")
w.Header().Add("Vary", "Accept-Encoding")
fw, _ := flate.NewWriter(w, level)
defer fw.Close()
h, hok := w.(http.Hijacker)
if !hok { /* w is not Hijacker... oh well... */
h = nil
}
f, fok := w.(http.Flusher)
if !fok {
f = nil
}
cn, cnok := w.(http.CloseNotifier)
if !cnok {
cn = nil
}
w = &compressResponseWriter{
Writer: fw,
ResponseWriter: w,
Hijacker: h,
Flusher: f,
CloseNotifier: cn,
}
break L
}
}
h.ServeHTTP(w, r)
})
}

Go语言经典库使用分析,未完待续,欢迎扫码关注公众号flysnow_org或者网站http://www.flysnow.org/,第一时间看后续系列。觉得有帮助的话,顺手分享到朋友圈吧,感谢支持。

首先判断压缩级别,保证是系统可以支持的压缩级别。其次是构建的一个返回的Handler,里面的核心是For循环,For循环从请求头里取Accept-Encoding信息,因为只有客户端支持,我们才可以进行压缩,需要需要从这个头信息取值。

1
2
3
4
5
6
7
8
9
10
11
12
13
L:
for _, enc := range strings.Split(r.Header.Get("Accept-Encoding"), ",") {
switch strings.TrimSpace(enc) {
case "gzip":
break L
case "deflate":
break L
}
}
h.ServeHTTP(w, r)

有了编码信息后,判断是gzip还是deflate,目前只支持这两种编码的压缩方式。如果满足其中任意一个,就马上跳出for循环,执行h.ServeHTTP(w, r),调用我们正常处理业务的Handler,但是注意,这里的w已经不是原来的w了,他已经被替换为一个压缩的ResponseWriter,这就是compressResponseWriter,下面以case "gzip"举例。

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
case "gzip":
w.Header().Set("Content-Encoding", "gzip")
w.Header().Add("Vary", "Accept-Encoding")
gw, _ := gzip.NewWriterLevel(w, level)
defer gw.Close()
h, hok := w.(http.Hijacker)
if !hok { /* w is not Hijacker... oh well... */
h = nil
}
f, fok := w.(http.Flusher)
if !fok {
f = nil
}
cn, cnok := w.(http.CloseNotifier)
if !cnok {
cn = nil
}
w = &compressResponseWriter{
Writer: gw,
ResponseWriter: w,
Hijacker: h,
Flusher: f,
CloseNotifier: cn,
}
break L

前面两行头信息设置,告诉客户端内容已经使用了gzip编码,接着生成一个gzipio.Writer,以供我们压缩内容使用。

接着的Hijacker、Flusher、CloseNotifier,和我们在讲LoggingHandler一样,尽可能保持原ResponseWriter实现的方法,不至于因为类型转换而丢失实现的方法。

最后会通过一个新生成的compressResponseWriter对原来的w重新赋值修改。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
type compressResponseWriter struct {
io.Writer
http.ResponseWriter
http.Hijacker
http.Flusher
http.CloseNotifier
}
func (w *compressResponseWriter) Write(b []byte) (int, error) {
h := w.ResponseWriter.Header()
if h.Get("Content-Type") == "" {
h.Set("Content-Type", http.DetectContentType(b))
}
h.Del("Content-Length")
return w.Writer.Write(b)
}

最终的压缩实现还是靠Write方法,因为我们会调用这个方法向客户端写响应的内容,所以这里通过进行了拦截压缩。

注意这里的w.Writer.Write(b),这个Writer就是gzip生成可以压缩的Writer,通过gzip.NewWriterLevel生成的,所以就这种达到了压缩返回内容的目的。

ContentTypeHandler

这个中间件就更简单了,意思就是服务端只支持这些内容类型的请求,其他的一律提示不支持。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
func ContentTypeHandler(h http.Handler, contentTypes ...string) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if !(r.Method == "PUT" || r.Method == "POST" || r.Method == "PATCH") {
h.ServeHTTP(w, r)
return
}
for _, ct := range contentTypes {
if isContentType(r.Header, ct) {
h.ServeHTTP(w, r)
return
}
}
http.Error(w, fmt.Sprintf("Unsupported content type %q; expected one of %q", r.Header.Get("Content-Type"), contentTypes), http.StatusUnsupportedMediaType)
})
}

从代码的逻辑就可以看出来,首先只支持POST,PUT,PATCH这三种方法,如果不是,那么和正常的请求没有两样,该中间件等于失效。

如果是这三个方法,并且是支持的类型,正常处理。如果不是支持的类型,就返回HTTP错误了,不支持的类型错误。

判断是否支持的类型函数是isContentType,他的逻辑就是从头信息读取内容类型,然后对比和我们支持的是否相等。

1
2
3
4
5
6
7
func isContentType(h http.Header, contentType string) bool {
ct := h.Get("Content-Type")
if i := strings.IndexRune(ct, ';'); i != -1 {
ct = ct[0:i]
}
return ct == contentType
}

CanonicalHost

域名重定向,把一个URL的域名换成另外一个,其他保持不变,然后重新重定向发起网络请求。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
func CanonicalHost(domain string, code int) func(h http.Handler) http.Handler {
fn := func(h http.Handler) http.Handler {
return canonical{h, domain, code}
}
return fn
}
type canonical struct {
h http.Handler
domain string
code int
}

canonical是一个Handler,重定向域名的逻辑在他的ServeHTTP方法里。

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
func (c canonical) ServeHTTP(w http.ResponseWriter, r *http.Request) {
dest, err := url.Parse(c.domain)
if err != nil {
// Call the next handler if the provided domain fails to parse.
c.h.ServeHTTP(w, r)
return
}
if dest.Scheme == "" || dest.Host == "" {
// Call the next handler if the scheme or host are empty.
// Note that url.Parse won't fail on in this case.
c.h.ServeHTTP(w, r)
return
}
if !strings.EqualFold(cleanHost(r.Host), dest.Host) {
// Re-build the destination URL
dest := dest.Scheme + "://" + dest.Host + r.URL.Path
if r.URL.RawQuery != "" {
dest += "?" + r.URL.RawQuery
}
http.Redirect(w, r, dest, c.code)
return
}
c.h.ServeHTTP(w, r)
}

目的域名是空的话,则不进行重定向;重定向的域名不能和原域名一样,不然重定向就没有意义了。

小结

这一篇对Gorilla Handlers的源代码分析,了解HTTP中间件实现的原理,以便我们更好的使用他们,甚至自定义自己的中间件。这里注意的是,如果对ResponseWriter进行类型转换,一定要保留原来实现的方法,尽可能的保证转换的完整,因为保留实现的方法,就意味着保留了一种接口实现。不能原来的ResponseWriter是一个A接口类型,转换后就不是了。

相关文章推荐

Go语言经典库使用分析(三)| Gorilla Handlers 详细介绍
Go语言经典库使用分析(二)| Gorilla Context
Go语言经典库使用分析(一)| 开篇

Go语言经典库使用分析,未完待续,欢迎扫码关注公众号flysnow_org或者网站http://www.flysnow.org/,第一时间看后续系列。觉得有帮助的话,顺手分享到朋友圈吧,感谢支持。

扫码关注


有疑问加站长微信联系(非本文作者)

入群交流(和以上内容无关):加入Go大咖交流群,或添加微信:liuxiaoyan-s 备注:入群;或加QQ群:1006366459

271 次点击  
加入收藏 微博
暂无回复
添加一条新回复 (您需要 登录 后才能回复 没有账号 ?)
  • 请尽量让自己的回复能够对别人有帮助
  • 支持 Markdown 格式, **粗体**、~~删除线~~、`单行代码`
  • 支持 @ 本站用户;支持表情(输入 : 提示),见 Emoji cheat sheet
  • 图片支持拖拽、截图粘贴等方式上传