gin 源码阅读(1) - gin 与 net/http 的关系
gin 是目前 Go 里面使用最广泛的框架之一了,弄清楚 gin 框架的原理,有助于我们更好的使用 gin. 这个系列 gin 源码阅读会逐步讲明白 gin 的原理。
gin 概览
想弄清楚 gin, 需要弄明白以下几个问题:
- request数据是如何流转的
- gin框架到底扮演了什么角色
- 请求从gin流入net/http, 最后又是如何回到gin中
- gin的context为何能承担起来复杂的需求
- gin的路由算法
- gin的中间件是什么
- gin的Engine具体是个什么东西
- net/http的requeset, response都提供了哪些有用的东西
从gin的官方第一个demo入手.
package main
import "github.com/gin-gonic/gin"
func main() {
    r := gin.Default()
    r.GET("/ping", func(c *gin.Context) {
        c.JSON(200, gin.H{
          "message": "pong",
        })
    })
    r.Run() // listen and serve on 0.0.0.0:8080
}
r.Run() 的源码:
func (engine *Engine) Run(addr ...string) (err error) {
    defer func() { debugPrintError(err) }()
    address := resolveAddress(addr)
    debugPrint("Listening and serving HTTP on %s\n", address)
    err = http.ListenAndServe(address, engine)
    return
}
看到开始调用的是 http.ListenAndServe(address, engine), 这个函数是net/http的函数,
然后请求数据就在net/http开始流转.
Request 数据是如何流转的
先不使用gin, 直接使用net/http来处理http请求
func main() {
    http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
        w.Write([]byte("Hello World"))
    })
    if err := http.ListenAndServe(":8000", nil); err != nil {
        fmt.Println("start http server fail:", err)
    }
}
在浏览器中输入localhost:8000, 会看到Hello World. 下面利用这个简单demo看下request的流转流程.
HTTP是如何建立起来的
简单的说一下http请求是如何建立起来的(需要有基本的网络基础, 可以找相关的书籍查看, 推荐看UNIX网络编程卷1:套接字联网API)


在TCP/IP五层模型下, HTTP位于应用层, 需要有传输层来承载HTTP协议. 传输层比较常见的协议是TCP,UDP, SCTP等. 由于UDP不可靠, SCTP有自己特殊的运用场景, 所以一般情况下HTTP是由TCP协议承载的(可以使用wireshark抓包然后查看各层协议)
使用TCP协议的话, 就会涉及到TCP是如何建立起来的. 面试中能够常遇到的名词三次握手, 四次挥手就是在这里产生的. 具体的建立流程就不在陈述了, 大概流程就是图中左半边
所以说, 要想能够对客户端http请求进行回应的话, 就首先需要建立起来TCP连接, 也就是socket. 下面要看下net/http是如何建立起来socket?
net/http 是如何建立 socket 的
从图上可以看出, 不管server代码如何封装, 都离不开bind,listen,accept这些函数. 就从上面这个简单的demo入手查看源码.
func main() {
    http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
        w.Write([]byte("Hello World"))
    })
    if err := http.ListenAndServe(":8000", nil); err != nil {
        fmt.Println("start http server fail:", err)
    }
}
注册路由
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
    w.Write([]byte("Hello World"))
})
这段代码是在注册一个路由及这个路由的handler到DefaultServeMux中
// server.go:L2366-2388
func (mux *ServeMux) Handle(pattern string, handler Handler) {
    mux.mu.Lock()
    defer mux.mu.Unlock()
    if pattern == "" {
        panic("http: invalid pattern")
    }
    if handler == nil {
        panic("http: nil handler")
    }
    if _, exist := mux.m[pattern]; exist {
        panic("http: multiple registrations for " + pattern)
    }
    if mux.m == nil {
        mux.m = make(map[string]muxEntry)
    }
    mux.m[pattern] = muxEntry{h: handler, pattern: pattern}
    if pattern[0] != '/' {
        mux.hosts = true
    }
}
可以看到这个路由注册太过简单了, 也就给gin, iris, echo等框架留下了扩展的空间, 后面详细说这个东西
服务监听及响应
上面路由已经注册到net/http了, 下面就该如何建立socket了, 以及最后又如何取到已经注册到的路由, 将正确的响应信息从handler中取出来返回给客户端
1.创建 socket
if err := http.ListenAndServe(":8000", nil); err != nil {
    fmt.Println("start http server fail:", err)
}
// net/http/server.go:L3002-3005
func ListenAndServe(addr string, handler Handler) error {
    server := &Server{Addr: addr, Handler: handler}
    return server.ListenAndServe()
}
// net/http/server.go:L2752-2765
func (srv *Server) ListenAndServe() error {
    // ... 省略代码
    ln, err := net.Listen("tcp", addr) // <-----看这里listen
    if err != nil {
      return err
    }
    return srv.Serve(tcpKeepAliveListener{ln.(*net.TCPListener)})
}
2.Accept 等待客户端链接
// net/http/server.go:L2805-2853
func (srv *Server) Serve(l net.Listener) error {
    // ... 省略代码
    for {
      rw, e := l.Accept() // <----- 看这里accept
      if e != nil {
        select {
        case <-srv.getDoneChan():
          return ErrServerClosed
        default:
        }
        if ne, ok := e.(net.Error); ok && ne.Temporary() {
          if tempDelay == 0 {
            tempDelay = 5 * time.Millisecond
          } else {
            tempDelay *= 2
          }
          if max := 1 * time.Second; tempDelay > max {
            tempDelay = max
          }
          srv.logf("http: Accept error: %v; retrying in %v", e, tempDelay)
          time.Sleep(tempDelay)
          continue
        }
        return e
      }
      tempDelay = 0
      c := srv.newConn(rw)
      c.setState(c.rwc, StateNew) // before Serve can return
      go c.serve(ctx) // <--- 看这里
    }
}
3. 提供回调接口 ServeHTTP
// net/http/server.go:L1739-1878
func (c *conn) serve(ctx context.Context) {
    // ... 省略代码
    serverHandler{c.server}.ServeHTTP(w, w.req)
    w.cancelCtx()
    if c.hijacked() {
      return
    }
    w.finishRequest()
    // ... 省略代码
}
// net/http/server.go:L2733-2742
func (sh serverHandler) ServeHTTP(rw ResponseWriter, req *Request) {
    handler := sh.srv.Handler
    if handler == nil {
      handler = DefaultServeMux
    }
    if req.RequestURI == "*" && req.Method == "OPTIONS" {
      handler = globalOptionsHandler{}
    }
    handler.ServeHTTP(rw, req)
}
// net/http/server.go:L2352-2362
func (mux *ServeMux) ServeHTTP(w ResponseWriter, r *Request) {
    if r.RequestURI == "*" {
      if r.ProtoAtLeast(1, 1) {
        w.Header().Set("Connection", "close")
      }
      w.WriteHeader(StatusBadRequest)
      return
    }
    h, _ := mux.Handler(r) // <--- 看这里
    h.ServeHTTP(w, r)
}
4. 回调到实际要执行的 ServeHTTP
// net/http/server.go:L1963-1965
func (f HandlerFunc) ServeHTTP(w ResponseWriter, r *Request) {
	  f(w, r)
}
这基本是整个过程的代码了.
- ln, err := net.Listen("tcp", addr)做了- 初试化了socket,- bind,- listen的操作.
- rw, e := l.Accept()进行accept, 等待客户端进行连接
- go c.serve(ctx)启动新的goroutine来处理本次请求. 同时主goroutine继续等待客户端连接, 进行高并发操作
- h, _ := mux.Handler(r)获取注册的路由, 然后拿到这个路由的handler, 然后将处理结果返回给客户端
从这里也能够看出来, net/http基本上提供了全套的服务.
为什么会出现很多go框架
// net/http/server.go:L2218-2238
func (mux *ServeMux) match(path string) (h Handler, pattern string) {
    // Check for exact match first.
    v, ok := mux.m[path]
    if ok {
        return v.h, v.pattern
    }
    // Check for longest valid match.
    var n = 0
    for k, v := range mux.m {
      if !pathMatch(k, path) {
          continue
      }
      if h == nil || len(k) > n {
          n = len(k)
          h = v.h
          pattern = v.pattern
      }
    }
    return
}
从这段函数可以看出来, 匹配规则过于简单, 当能匹配到路由的时候就返回其对应的handler, 当不能匹配到时就返回/. net/http的路由匹配无法满足复杂的需求开发.
所以基本所有的go框架干的最主要的一件事情就是重写net/http的route。我们直接说 gin就是一个 httprouter 也不过分, 当然gin也提供了其他比较主要的功能, 后面会一一介绍。
综述, net/http基本已经提供http服务的70%的功能, 那些号称贼快的go框架, 基本上都是提供一些功能, 让我们能够更好的处理客户端发来的请求. 如果你有兴趣的话,也可以基于 net/http 做一个 Go 框架出来。
gin 源码阅读(1) - gin 与 net/http 的关系的更多相关文章
- gin 源码阅读(2) - http请求是如何流入gin的?
		推荐阅读: gin 源码阅读(1) - gin 与 net/http 的关系 本篇文章是 gin 源码分析系列的第二篇,这篇文章我们主要弄清一个问题:一个请求通过 net/http 的 socket ... 
- gin 源码阅读(5) - 灵活的返回值处理
		gin 源码阅读系列文章列表: gin 源码阅读(1) - gin 与 net/http 的关系 gin 源码阅读(2) - http请求是如何流入gin的? gin 源码阅读(3) - gin 路由 ... 
- gin源码解读1-net/http的大概流程
		gin框架预览 router.Run()的源码: func (engine *Engine) Run(addr ...string) (err error) { defer func() { debu ... 
- gin源码解读3-gin牛逼的context
		Gin封装的最好的地方就是context和对response的处理. github的README的介绍,基本就是对这两个东西的解释. 本篇文章主要解释context的使用方法, 以及其设计原理 为什么 ... 
- gin源码剖析
		介绍 Gin 是一个 Golang 写的 web 框架,具有高性能的优点,基于 httprouter,它提供了类似martini但更好性能(路由性能约快40倍)的API服务.官方地址:https:// ... 
- 【原】FMDB源码阅读(三)
		[原]FMDB源码阅读(三) 本文转载请注明出处 —— polobymulberry-博客园 1. 前言 FMDB比较优秀的地方就在于对多线程的处理.所以这一篇主要是研究FMDB的多线程处理的实现.而 ... 
- 【原】FMDB源码阅读(二)
		[原]FMDB源码阅读(二) 本文转载请注明出处 -- polobymulberry-博客园 1. 前言 上一篇只是简单地过了一下FMDB一个简单例子的基本流程,并没有涉及到FMDB的所有方方面面,比 ... 
- 【原】FMDB源码阅读(一)
		[原]FMDB源码阅读(一) 本文转载请注明出处 —— polobymulberry-博客园 1. 前言 说实话,之前的SDWebImage和AFNetworking这两个组件我还是使用过的,但是对于 ... 
- 【原】AFNetworking源码阅读(六)
		[原]AFNetworking源码阅读(六) 本文转载请注明出处 —— polobymulberry-博客园 1. 前言 这一篇的想讲的,一个就是分析一下AFSecurityPolicy文件,看看AF ... 
随机推荐
- 012 基于FPGA的网口通信实例设计【转载】
			一.网口通信设计分类 通过上面其他章节的介绍,网口千兆通信,可以使用TCP或者UDP协议,可以外挂PHY片或者不挂PHY片,总结下来就有下面几种方式完成通信: 图8‑17基于FPGA的网口通信实例设计 ... 
- Spring源码解析之ConfigurationClassPostProcessor(二)
			上一个章节,笔者向大家介绍了spring是如何来过滤配置类的,下面我们来看看在过滤出配置类后,spring是如何来解析配置类的.首先过滤出来的配置类会存放在configCandidates列表, 在代 ... 
- Mac 证书错误
			在 Mac 操作系统安装 Python 3.6 或以上版本时,可能会遇到证书错误:Error: [SSL: CERTIFICATE_VERIFY_FAILED] certificate verify ... 
- 基础篇:一文讲懂树莓派命令行文本编辑工具Vim的使用
			简介 众所周知,在Linux系统下的命令行调试界面,经常会遇到需要文本编辑的情况,而树莓派官方系统默认自带了Nano编辑器,Nano的操作门槛更低,但却不如Vim编辑器方便.Vim编辑器是由早期在Li ... 
- 流媒体 Ubuntu部署srs、windows部署nginx
			一.获取项目//码云克隆git clone https://gitee.com/winlinvip/srs.oschina.git srs//githubgit clone https://githu ... 
- mfc HackerTools全局钩子
			钩子英文名叫Hook,是一种截获windows系统中某应用程序或者所有进程的消息的一种技术. 如在键盘中按下一键,操作系统将收到键按下消息,把消息放入消息队列,然后消息队列对消息进行派发,发给相应的应 ... 
- Linux下修改mysql默认最大连接数
			liunx下修改mysql最大连接数(Centos下测试通过)1.查看当前系统下mysql设置的最大连接数方式一.mysqladmin -uroot -p variables |grep max_co ... 
- MySQL高可用主从复制部署
			原文转自:https://www.cnblogs.com/itzgr/p/10233932.html作者:木二 目录 一 基础环境 二 实际部署 2.1 安装MySQL 2.2 初始化MySQL 2. ... 
- 接口自动化-python+requests+pytest+csv+yaml
			本套代码和逻辑 是本人的劳动成果,如果有转载需要标注, 非常适合公司做项目的同学!!!小白也可以学哦! 1.项目目录 2.公共方法的封装 2.1如果不用配置文件 可以使用这个方法进行封装--但是有一 ... 
- JVM加载class文件的一些理解
			Java是一种动态解释型语言,类(class)只有被加载到JVM中后才能运行.每当一个Java程序运行时,都会有一个对应的JVM实例,只有当程序运行结束后,这个JVM才会退出.JVM实例通过调用类的m ... 
