Go 框架学习之旅 ① 深入解析 net/http 启动服务的层级逻辑

选择框架

有的同学喜欢追求最全的框架,觉得功能越多越好,但是往往并不是每一个功能都能在实际工作中用到;有的同学喜欢追求性能最高的框架,但是总觉得用起来非常别扭;也常有同学会认为框架无好坏,每个框架基本都差不多,最终的选择就是两个字:都行。

这些说法其实是进入了一个思维误区:没有把框架放到特定场景上讨论,这个特定场景,就是开发团队的业务环境。

如果你一个人负责一个小的外包项目,那可以说所有框架都差不多;如果是 2~3 个服务端人员的初创团队,你用的框架要承担的工作大概率是提升开发效率,那就要选择功能更强大、易用性更好的框架;等团队再大一些,框架所承担的职责就更多了,往往得更多考虑性能、扩展性,或者定制自己的框架。

框架分类

分为两个大类:一类是追求运行性能,一类是追求开发效率。追求性能的框架往往很简洁,包含的东西也很少,一个路由一个 MVC 就完事了,比如 Gin 框架;另外一类框架追求开发效率,封装得非常好,很多功能会让你惊叹,能帮你省很多事,最典型的就是 Beego 框架。

在任何领域做到第一名的产品基本上都有一个共性:开发、迭代速度快。

Go Web 框架

Go 语言中的 Goroutine 设计,提供了“一个请求一个协程”的请求模型,对比 PHP 的“一个请求一个进程”的模型,能有效提升后端的资源占用和调度负载;另外,Go 的 Runtime 机制让运行程序不再依赖各种的环境和库,将 Web 服务的部署和搭建变得简单高效;而 Go 提供的交叉编译、数据结构、channel 等语言级别特性,都让“处理 Web 请求”这个事情变得非常简单。

为什么从零学框架

可选框架这么多,新框架也层出不穷,我们不可能也没有必要完全掌握所有框架。如果你只学怎么用框架,按照文档“按部就班”,是永远不可能真正做到掌握框架的。

但是只要你开始动手做一个框架,你就能站在框架作者的角度,遇到作者开发时遇到的问题,思考作者开发时选择的方案,从本质上理解清楚这些框架都在做些什么、为什么这么设计,之后在工作中遇到类似问题的时候,也会清楚这个问题为什么会出现,解决也就不在话下了。

Web Server

用官方提供的 net/http 标准库搭建一个 Web Server,是一件非常简单的事。不少同学,在怎么搭怎么用的问题上,回答的非常溜,但是再追问一句为什么这个 Server 这么设计,涉及的 net/http 实现原理是什么? 一概不知。

这其实是非常危险的。实际工作中,我们会因为不了解底层原理,想当然的认为它的使用方式,直接导致在代码编写、应用调优的时候出现各种问题。

Web Server 是一个通过 HTTP 协议处理 Web 请求的计算机系统。

HTTP 协议,在 OSI 网络体系结构中,是基于 TCP/IP 之上第七层应用层的协议,全称叫做超文本传输协议。啥意思?就是说 HTTP 协议传输的都是文本字符,只是这些字符是有规则排列的。这些字符的排列规则,就是一种约定,也就是协议。这个协议还有一个专门的描述文档,就是RFC 2616。

对于 HTTP 协议,无论是请求还是响应,传输的消息体都可以分为两个部分:HTTP 头部和 HTTP Body 体头部描述的一般是和业务无关但与传输相关的信息,比如请求地址、编码格式、缓存时长等;Body 里面主要描述的是与业务相关的信息。

Web Server 的本质,实际上就是接收、解析 HTTP 请求传输的文本字符,理解这些文本字符的指令,然后进行计算,再将返回值组织成 HTTP 响应的文本字符,通过 TCP 网络传输回去

对 Web Server 来说,Golang 提供了 net 库和 net/http 库,分别对应 OSI 的 TCP 层和 HTTP 层,它们两个负责的就是 HTTP 的接收和解析。

net/http 标准库怎么学

官方给出的范例

// 创建一个Foo路由和处理函数
http.Handle("/foo", fooHandler)

// 创建一个bar路由和处理函数
http.HandleFunc("/bar", func(w http.ResponseWriter, r *http.Request) {
  fmt.Fprintf(w, "Hello, %q", html.EscapeString(r.URL.Path))
})

// 监听8080端口
log.Fatal(http.ListenAndServe(":8080", nil))

是不是代码足够简单?一共就 5 行,但往前继续推进之前,我想先问你几个问题,这五行代码做了什么,为什么就能启动一个 HTTP 服务,具体的逻辑是什么样的?

想要在 net/http 标准库纷繁复杂的代码层级和调用中,弄清楚主流程不是一件容易事。要快速熟悉一个标准库,就得找准方法。这里我教给你一个快速掌握代码库的技巧:库函数 > 结构定义 > 结构函数。

这就和写书一样,先写大纲,再写章节目录,最后补充书本内容。

库函数:这个库要提供什么功能(对外提供的方法)

结构定义:整个库分为几个核心模块 (核心模块的结构是怎么定义的,为什么这么定义)

结构函数:每个核心模块应该提供什么能力 (各个结构是如何实现功能的,结构的方法)

库函数

查询出 net/http 库所有的对外库函数。注意,在 windows 中使用的是 findstr 命令,在 linux 中使用的 grep 查询命令

go doc net/http | findstr "^func"

输出如下结果

func CanonicalHeaderKey(s string) string
func DetectContentType(data []byte) string
func Error(w ResponseWriter, error string, code int)
func Get(url string) (resp *Response, err error)
func Handle(pattern string, handler Handler)
func HandleFunc(pattern string, handler func(ResponseWriter, *Request))
func Head(url string) (resp *Response, err error)
func ListenAndServe(addr string, handler Handler) error
func ListenAndServeTLS(addr, certFile, keyFile string, handler Handler) error
func MaxBytesReader(w ResponseWriter, r io.ReadCloser, n int64) io.ReadCloser
func NewRequest(method, url string, body io.Reader) (*Request, error)
func NewRequestWithContext(ctx context.Context, method, url string, body io.Reader) (*Request, error)
func NotFound(w ResponseWriter, r *Request)
func ParseHTTPVersion(vers string) (major, minor int, ok bool)
func ParseTime(text string) (t time.Time, err error)
func Post(url, contentType string, body io.Reader) (resp *Response, err error)
func PostForm(url string, data url.Values) (resp *Response, err error)
func ProxyFromEnvironment(req *Request) (*url.URL, error)
func ProxyURL(fixedURL *url.URL) func(*Request) (*url.URL, error)
func ReadRequest(b *bufio.Reader) (*Request, error)
func ReadResponse(r *bufio.Reader, req *Request) (*Response, error)
func Redirect(w ResponseWriter, r *Request, url string, code int)
func Serve(l net.Listener, handler Handler) error
func ServeContent(w ResponseWriter, req *Request, name string, modtime time.Time, ...)
func ServeFile(w ResponseWriter, r *Request, name string)
func ServeTLS(l net.Listener, handler Handler, certFile, keyFile string) error
func SetCookie(w ResponseWriter, cookie *Cookie)
func StatusText(code int) string
  • New 和 Set 开头的函数是对某个对象或者属性的设置
  • 为服务端提供创建 HTTP 服务的函数,名字中一般包含 Serve 字样,比如 Serve、ServeFile、ListenAndServe 等。
  • 为客户端提供调用 HTTP 服务的类库,以 HTTP 的 method 同名,比如 Get、Post、Head 等。
  • 提供中转代理的一些函数,比如 ProxyURL、ProxyFromEnvironment 等。

研究如何创建一个HTTP服务,关注包含Serve字样的函数

// 通过监听的URL地址和控制器函数来创建HTTP服务
func ListenAndServe(addr string, handler Handler) error{}
// 通过监听的URL地址和控制器函数来创建HTTPS服务
func ListenAndServeTLS(addr, certFile, keyFile string, handler Handler) error{}
// 通过net.Listener结构和控制器函数来创建HTTP服务
func Serve(l net.Listener, handler Handler) error{}
// 通过net.Listener结构和控制器函数来创建HTTPS服务
func ServeTLS(l net.Listener, handler Handler, certFile, keyFile string) error{}

结构定义

输出这个库提供的所有 struct,查看库最核心的结构

go doc net/http | findstr "^type"| findstr struct
type Client struct{ ... }
type Cookie struct{ ... }
type ProtocolError struct{ ... }
type PushOptions struct{ ... }
type Request struct{ ... } 
type Response struct{ ... }
type ServeMux struct{ ... }
type Server struct{ ... }
type Transport struct{ ... }

Client 负责构建 HTTP 客户端;

Server 负责构建 HTTP 服务端;

ServerMux 负责 HTTP 服务端路由;

Transport、Request、Response、Cookie 负责客户端和服务端传输对应的不同模块

现在通过库方法(function)和结构体(struct),我们对整个库的结构和功能有大致印象了。整个库承担了两部分功能,一部分是构建 HTTP 客户端,一部分是构建 HTTP 服务端。

构建的 HTTP 服务端除了提供真实服务之外,也能提供代理中转服务,它们分别由 Client 和 Server 两个数据结构负责。除了这两个最重要的数据结构之外,HTTP 协议的每个部分,比如请求、返回、传输设置等都有具体的数据结构负责。

结构函数

// 创建一个Foo路由和处理函数
http.Handle("/foo", fooHandler)

// 创建一个bar路由和处理函数
http.HandleFunc("/bar", func(w http.ResponseWriter, r *http.Request) {
  fmt.Fprintf(w, "Hello, %q", html.EscapeString(r.URL.Path))
})

// 监听8080端口
log.Fatal(http.ListenAndServe(":8080", nil))

我们跟着 http.ListenAndServe 这个函数来理一下 net/http 创建服务的主流程逻辑。

阅读具体的代码逻辑用 go doc 命令明显就不够了,你需要两个东西:

一个是可以灵活进行代码跳转的 IDE,VS Code 和 GoLand 都是非常好的工具。以我们现在要查看的 http.ListenAndServe 这个函数为例,我们可以从上面的例子代码中,直接通过 IDE 跳转到这个函数的源码中阅读,有一个能灵活跳转的 IDE 工具是非常必要的。

在VS Code中按下F12定位源码

另一个是可以方便记录代码流程的笔记,这里我的个人方法是使用思维导图。具体方法是将要分析的代码从入口处一层层记录下来,每个函数,我们只记录其核心代码,然后对每个核心代码一层层解析。记得把思维导图的结构设置为右侧分布,这样更直观。

思维导图解析HTTP服务端

总框架

image-20220329234710610

层级逻辑

http.ListenAndServe 本质是通过创建一个 Server 数据结构,调用 server.ListenAndServe 对外提供服务,这一层完全是比较简单的封装,目的是,将 Server 结构创建服务的方法 ListenAndServe ,直接作为库函数对外提供,增加库的易用性。(本来开发要写多行代码,现在合并为一个方法写一行就好)

img

进入到第二层,创建服务的方法 ListenAndServe 先定义了监听信息 net.Listen,然后调用 Serve 函数

而在第三层 Serve 函数中,用了一个 for 循环,通过 l.Accept不断接收从客户端传进来的请求连接。当接收到了一个新的请求连接的时候,通过 srv.NewConn 创建了一个连接结构(http.conn),并创建一个 Goroutine 为这个请求连接对应服务(c.serve)。

img

在第四层,c.serve函数先判断本次 HTTP 请求是否需要升级为 HTTPs,接着创建读文本的 reader 和写文本的 buffer,再进一步读取本次请求数据,然后第五层调用最关键的方法 serverHandler{c.server}.ServeHTTP(w, w.req) ,来处理这次请求。

serverHandler 结构体,是标准库封装的,代表“请求对应的处理逻辑”,它只包含了一个指向总入口服务 server 的指针。

这个结构将总入口的服务结构 Server 和每个连接的处理逻辑巧妙联系在一起了,你可以看接着的第六层逻辑:

// serverHandler 结构代表请求对应的处理逻辑
type serverHandler struct {
  srv *Server
}

// 具体处理逻辑的处理函数
func (sh serverHandler) ServeHTTP(rw ResponseWriter, req *Request) {
  handler := sh.srv.Handler  // 入口服务结构的 Handler
  if handler == nil {
    handler = DefaultServeMux
  }
  ...
  handler.ServeHTTP(rw, req)
}

如果入口服务 server 结构已经设置了 Handler,就调用这个 Handler 来处理此次请求,反之则使用库自带的 DefaultServerMux。

这里的 serverHandler 设计,能同时保证这个库的扩展性和易用性:你可以很方便使用默认方法处理请求,但是一旦有需求,也能自己扩展出方法处理请求。

那么 DefaultServeMux 是怎么寻找 Handler 的呢,这就是思维导图的最后一部分第七层。

img

DefaultServeMux.Handle 是一个非常简单的 map 实现,key 是路径(pattern),value 是这个 pattern 对应的处理函数(handler)。它是通过 mux.match(path) 寻找对应 Handler,也就是从 DefaultServeMux 内部的 map 中直接根据 key 寻找到 value 的。

HTTP 库 Server 的代码流程梳理完成了,整个逻辑线大致是:

创建服务 -> 监听请求 -> 创建连接 -> 处理请求

  • 第一层,标准库创建 HTTP 服务是通过创建一个 Server 数据结构完成的;

  • 第二层,Server 数据结构在 for 循环中不断监听每一个连接

  • 第三层,每个连接默认开启一个 Goroutine 为其服务;

  • 第四、五层,serverHandler 结构代表请求对应的处理逻辑,并且通过这个结构进行具体业务逻辑处理;

  • 第六层,Server 数据结构如果没有设置处理函数 Handler,默认使用 DefaultServerMux 处理请求

  • 第七层,DefaultServerMux 是使用 map 结构来存储和查找路由规则。

阅读核心逻辑代码是会有点枯燥,但是这条逻辑线是 HTTP 服务启动最核心的主流程逻辑,后面我们会基于这个流程继续开发,你要掌握到能背下来的程度。千万不要觉得要背诵了,压力太大,其实对照着思维导图,顺几遍逻辑,理解了再记忆就很容易。

创建框架的Server结构

第一层的关键结论就是:net/http 标准库创建服务,实质上就是通过创建 Server 数据结构来完成的。所以接下来,我们就来创建一个 Server 数据结构。

type Server struct {
    // 请求监听地址
  Addr string
    // 请求核心处理函数
  Handler Handler 
  ...
}

其中最核心的是 Handler 这个字段,从主流程中我们知道(第六层关键结论),当 Handler 这个字段设置为空的时候,它会默认使用 DefaultServerMux 这个路由器来填充这个值,但是我们一般都会使用自己定义的路由来替换这个默认路由。

创建一个 framework 文件夹,新建 core.go,在里面写入。

package framework

import "net/http"

// 框架核心结构
type Core struct {
}

// 初始化框架核心结构
func NewCore() *Core {
  return &Core{}
}

// 框架核心结构实现Handler接口(重点)
func (c *Core) ServeHTTP(response http.ResponseWriter, request *http.Request) {
  // TODO
}

而在业务文件夹中创建 main.go,其中的 main 函数就变成这样

func main() {
  server := &http.Server{
        // 自定义的请求核心处理函数
    Handler: framework.NewCore(),
        // 请求监听地址
    Addr:    ":8080",
  }
  server.ListenAndServe()
}

我们通过自己创建了 Server 数据结构,并且在数据结构中创建了自定义的 Handler(Core 数据结构)和监听地址,实现了一个 HTTP 服务。这个服务的具体业务逻辑都集中在我们自定义的 Core 结构中,后续我们要做的事情就是不断丰富这个 Core 数据结构的功能逻辑。

学习补充

OSI参考模型

https://www.cnblogs.com/qishui/p/5428938.html

1、OSI的来源

​ OSI(Open System Interconnect),即开放式系统互联。 一般都叫OSI参考模型,是ISO(国际标准化组织)组织在1985年研究的网络互连模型。

​ ISO为了更好的使网络应用更为普及,推出了OSI参考模型。其含义就是推荐所有公司使用这个规范来控制网络。这样所有公司都有相同的规范,就能互联了。

2、OSI七层模型的划分

​ OSI定义了网络互连的七层框架(物理层、数据链路层、网络层、传输层、会话层、表示层、应用层),即ISO开放互连系统参考模型。如下图。

​ 每一层实现各自的功能和协议,并完成与相邻层的接口通信。OSI的服务定义详细说明了各层所提供的服务。某一层的服务就是该层及其下各层的一种能力,它通过接口提供给更高一层。各层所提供的服务与这些服务是怎么实现的无关。

应用层:应用层确定进程之间通信的性质以满足用户的需要。
运输层:解决进程间的通信。
网络层:解决跨网络的主机通信问题。
数据链路层:解决相邻主机通信问题。
物理层:物理层的任务就是透明地传输比特流。

img

3、各层功能定义

​ 这里我们只对OSI各层进行功能上的大概阐述,不详细深究,因为每一层实际都是一个复杂的层。我们从最顶层——应用层 开始介绍。整个过程以公司A和公司B的一次商业报价单发送为例子进行讲解。

应用层

​ OSI参考模型中最靠近用户的一层,是为计算机用户提供应用接口,也为用户直接提供各种网络服务。我们常见应用层的网络服务协议有:HTTP,HTTPS,FTP,POP3、SMTP等。

​ 实际公司A的老板就是我们所述的用户,而他要发送的商业报价单,就是应用层提供的一种网络服务,当然,老板也可以选择其他服务,比如说,发一份商业合同,发一份询价单,等等。

表示层

​ 表示层提供各种用于应用层数据的编码和转换功能,确保一个系统的应用层发送的数据能被另一个系统的应用层识别。如果必要,该层可提供一种标准表示形式,用于将计算机内部的多种数据格式转换成通信中采用的标准表示形式。数据压缩和加密也是表示层可提供的转换功能之一。

​ 由于公司A和公司B是不同国家的公司,他们之间的商定统一用英语作为交流的语言,所以此时表示层(公司的文秘),就是将应用层的传递信息转翻译成英语。同时为了防止别的公司看到,公司A的人也会对这份报价单做一些加密的处理。这就是表示的作用,将应用层的数据转换翻译等。

会话层

​ 会话层就是负责建立、管理和终止表示层实体之间的通信会话。该层的通信由不同设备中的应用程序之间的服务请求和响应组成。

​ 会话层的同事拿到表示层的同事转换后资料,(会话层的同事类似公司的外联部),会话层的同事那里可能会掌握本公司与其他好多公司的联系方式,这里公司就是实际传递过程中的实体。他们要管理本公司与外界好多公司的联系会话。当接收到表示层的数据后,会话层将会建立并记录本次会话,他首先要找到公司B的地址信息,然后将整份资料放进信封,并写上地址和联系方式。准备将资料寄出。等到确定公司B接收到此份报价单后,此次会话就算结束了,外联部的同事就会终止此次会话。

传输层

​ 传输层建立了主机端到端的链接,传输层的作用是为上层协议提供端到端的可靠和透明的数据传输服务,包括处理差错控制和流量控制等问题。该层向高层屏蔽了下层数据通信的细节,使高层用户看到的只是在两个传输实体间的一条主机到主机的、可由用户控制和设定的、可靠的数据通路。我们通常说的,TCP UDP就是在这一层。端口号既是这里的“端”。

​ 传输层就相当于公司中的负责快递邮件收发的人,公司自己的投递员,他们负责将上一层的要寄出的资料投递到快递公司或邮局。

网络层

​ 本层通过IP寻址来建立两个节点之间的连接,为源端的运输层送来的分组,选择合适的路由和交换节点,正确无误地按照地址传送给目的端的运输层。就是通常说的IP层。这一层就是我们经常说的IP协议层。IP协议是Internet的基础。

​ 网络层就相当于快递公司庞大的快递网络,全国不同的集散中心,比如说,从深圳发往北京的顺丰快递(陆运为例啊,空运好像直接就飞到北京了),首先要到顺丰的深圳集散中心,从深圳集散中心再送到武汉集散中心,从武汉集散中心再寄到北京顺义集散中心。这个每个集散中心,就相当于网络中的一个IP节点。

数据链路层

​ 将比特组合成字节,再将字节组合成帧,使用链路层地址 (以太网使用MAC地址)来访问介质,并进行差错检测。

数据链路层又分为2个子层:逻辑链路控制子层(LLC)和媒体访问控制子层(MAC)。

​ MAC子层处理CSMA/CD算法、数据出错校验、成帧等;LLC子层定义了一些字段使上次协议能共享数据链路层。 在实际使用中,LLC子层并非必需的。

​ 数据链路层就是准备发送前,需要装箱分类检查,去往广东的放在一块,检测是否有违禁品。

物理层

​ 实际最终信号的传输是通过物理层实现的。通过物理介质传输比特流。规定了电平、速度和电缆针脚。常用设备有(各种物理设备)集线器、中继器、调制解调器、网线、双绞线、同轴电缆。这些都是物理层的传输介质。为上一层 ( 数据链路层 ) 提供一个传输原始比特流的'物理连接' 。

​ 快递寄送过程中的交通工具,就相当于我们的物理层,例如汽车,火车,飞机,船。

4、通信特点:对等通信

对等通信,为了使数据分组从源传送到目的地,源端OSI模型的每一层都必须与目的端的对等层进行通信,这种通信方式称为对等层通信。在每一层通信过程中,使用本层自己协议进行通信。

TCP/IP五层模型

TCP/IP五层协议和OSI的七层协议对应关系如下。

img

在每一层都工作着不同的设备,比如我们常用的交换机就工作在数据链路层的,一般的路由器是工作在网络层的。

img

在每一层实现的协议也各不同,即每一层的服务也不同.下图列出了每层主要的协议。

img

七层结构记忆方法:应、表、会、传、网、数、物

img

posted @ 2022-03-31 11:52  小能日记  阅读(14)  评论(0编辑  收藏  举报