Go BIO/NIO探讨:Gin框架中如何处理HTTP请求

开发 前端
熟悉Java的朋友看到 NIO 这个词,应该会有莫名的亲切感。值得庆幸的是,现在终于有人用Go翻译一波netty了。

最近看到字节跳动开源了Go语言的Hertz,声称使用了 Non-blocking IO 网络库 Netpoll,所以性能非常强大,并给出了Echo的性能测试数据。

性能测试

相对于目前业界流行的 gin 框架,QPS提升超过100%,而且单次请求的数据包越大,性能提升越明显。

熟悉Java的朋友看到 NIO 这个词,应该会有莫名的亲切感。值得庆幸的是,现在终于有人用Go翻译一波netty了。

在这之前,为了更好地理解Blocking IO,我们从 Web 框架入手,看 Go 内置的 net/http 是如何工作的。

下面这段代码展示了Gin框架中如何定义一个路由,并启动Server。代码来自于 Github:gin-gonic/gin,我们从这段代码入手,了解 http 的运行机制。

package main

import (
  "net/http"
  "github.com/gin-gonic/gin"
)

func main() {
  r := gin.Default()
  r.GET("/ping", func(c *gin.Context) {
    c.JSON(http.StatusOK, gin.H{
      "message": "pong",
    })
  })
  r.Run() // 监听8080端口
}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.

在这段代码中,r 是一个 *gin.Engine 对象,它实现了 net/http下的Handler 接口:

type Handler interface {
  ServeHTTP(ResponseWriter, *Request)
}
  • 1.
  • 2.
  • 3.

第二步是设置路由和对应的处理函数,*gin.Engine 对象嵌入了 struct RouterGroup,实现路由功能。Web开发中,这是所有业务逻辑的入口。

第三步是运行服务,基本流程是三个tcp系统调用 socket, bind, listen,然后在 for 循环里调用 accept 接收新的tcp连接(更多细节参考Unix网络编程卷一第四章)。

// 来源于Go源码 net/http/server.go struct Server
func (srv *Server) ListenAndServe() error {
  if srv.shuttingDown() {
    return ErrServerClosed
  }
  addr := srv.Addr
  if addr == "" {
    addr = ":http"
  }
  ln, err := net.Listen("tcp", addr)
  if err != nil {
    return err
  }
  return srv.Serve(ln)
}

func (srv *Server) Serve(l net.Listener) error {
  // ... 省略一部分代码

  origListener := l
  l = &onceCloseListener{Listener: l}
  defer l.Close()

  if err := srv.setupHTTP2_Serve(); err != nil {
    return err
  }

  // ... 省略一部分代码

  ctx := context.WithValue(baseCtx, ServerContextKey, srv)
  for {
    rw, err := l.Accept()
    // ... 省略一部分代码
    c := srv.newConn(rw)
    c.setState(c.rwc, StateNew, runHooks) // before Serve can return
    go c.serve(connCtx)
  }
}
  • 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.

到这里,HTTP还没有登场,因为HTTP是应用层协议,而TCP是传输层协议。我们拿到 tcp connection 以后,HTTP协议体现在对数据包的处理上。

HTTP/1 仍然是一个超文本协议,HTTP/2 是一个基于帧的二进制协议,并且支持了server端主动推送。由于 HTTP/1 仍然是主流,我们这里先说 HTTP/1,它的报文分三部分:

  1. 起始行
  2. Headers 消息头,以一个空行结束
  3. Body 消息体

应用程序可以方便地解析HTTP 请求和响应。HTTP/1.1 已经支持复用TCP连接,也就是说多个HTTP请求可以在同一个tcp conn上传输,那么client端如何把request和response 对应起来呢?会有什么问题?HTTP/2中又是怎么做的?这里先挖个坑。

继续看 net/http 的代码,了解HTTP 如何在tcp conn之上工作的。主要逻辑如下:

// Serve a new connection.
func (c *conn) serve(ctx context.Context) {
  c.remoteAddr = c.rwc.RemoteAddr().String()
  ctx = context.WithValue(ctx, LocalAddrContextKey, c.rwc.LocalAddr())
  var inFlightResponse *response
  defer func() {...  // cleanup

  if tlsConn, ok := c.rwc.(*tls.Conn); ok {
    // ... 省略部分代码
  }

  // HTTP/1.x from here on.
  ctx, cancelCtx := context.WithCancel(ctx)
  c.cancelCtx = cancelCtx
  defer cancelCtx()

  c.r = &connReader{conn: c}
  c.bufr = newBufioReader(c.r)
  c.bufw = newBufioWriterSize(checkConnErrorWriter{c}, 4<<10)

  for {
    w, err := c.readRequest(ctx)
    // ... 省略部分代码
    serverHandler{c.server}.ServeHTTP(w, w.req)
    // ... 省略一部分代码
    w.finishRequest()
    // ... 省略一部分代码
  }
  • 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.

这段代码的前半部分是设置cleanup逻辑、初始化conn相关的变量,后半部分是在 for 循环里处理 HTTP 请求,体现为:

  1. 读取 tcp conn上的数据,并解析&组装成一个请求,存储到一个http.response对象里。
  2. 通过 ServeHTTP 调用业务逻辑(路由转发到特定处理函数),获取响应。
  3. 把响应报文写入 tcp conn。

关于Gin框架和net/http 的逻辑到这里就介绍完了,大的流程参考下面这个思维导图:

为什么 Blocking IO

当我们聊到 Blocking IO 和 Non-blocking IO,通常是指一个线程调用 read 或 write 时,是否被阻塞:

  • BIO: 线程被阻塞直到读到数据或写入完成。
  • NIO: 线程不被阻塞,可以去做其他事情,但是有数据到来或者写入完成时,线程会接收到通知。

这里我们限定到网络IO的情况,Go net/http 里BIO体现在两个地方:

  • case 1: listener 在for循环里等待接收新的tcp conn。
  • case 2: conn 等待读取新的 request。
// case 1
for {
  rw, err := l.Accept()
  // ... 省略一部分代码
  c := srv.newConn(rw)
  c.setState(c.rwc, StateNew, runHooks) // before Serve can return
  go c.serve(connCtx)
}

// case 2
for {
  w, err := c.readRequest(ctx)
  // ... 省略部分代码
  serverHandler{c.server}.ServeHTTP(w, w.req)
  // ... 省略一部分代码
  w.finishRequest()
  // ... 省略一部分代码
}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.

如果把 goroutine 当成操作系统线程,我们可以把这种模式当作BIO。由于在网络IO上 Go语言对 goroutine 的特殊实现,在细节上可能会有点争议,有兴趣的同学可以自己研究下。

要做到 NIO,典型的方式是通过 reactor 模式,替换上面的两个for循环。在后面的文章中,我们介绍 hertz 框架的时候,会详细聊一下这个方案。

责任编辑:姜华 来源: 今日头条
相关推荐

2023-03-07 08:00:12

netpollGo

2023-03-31 07:49:51

syscall库Echo Serve

2023-03-09 08:22:57

Go net库Socket

2023-09-19 22:41:30

控制器HTTP

2011-12-15 12:32:19

JavaNIO

2022-12-26 00:00:01

Go框架前端

2021-01-18 05:13:04

TomcatHttp

2024-11-04 08:16:08

Go语言Web 框架

2025-01-09 10:20:53

2014-11-10 10:52:33

Go语言

2020-04-16 15:20:43

PHP前端BIO

2011-03-31 10:41:49

BIONIOIO

2022-04-16 16:52:24

Netty网络服务器客户端程序

2022-10-17 09:02:49

Go自动验证数据绑定

2023-10-04 07:35:03

2024-03-05 07:55:41

框架GINGo

2024-02-19 07:40:10

2024-01-30 12:08:31

Go框架停止服务

2021-01-25 06:53:59

前端AJAX技术热点

2018-11-21 12:27:21

JavaScript 货币值区域
点赞
收藏

51CTO技术栈公众号