go语言提供了net/http标准库,帮助开发者轻松的开发web应用,不论是了解http协议还是深入理解go语言的设计理念,阅读http标准库都极为重要。但如果新手一头扎进源码的阅读,总因为抓不住主线脉络或者不懂某个步骤的设计原因导致雾水重重,本系列将从tcp基础上通过从零实现的方式带你剖析net/http标准库的实现过程。

net/http标准库

其实net/http库已经完全符合了作为一个轻量级web框架要求:

  • Request的解析
  • Response的设置
  • 静态路由的实现
  • Form表单的解析
  • ……

相较于其他开源的框架,如gin、beego和iris,标准库缺少的只是动态路由、orm、session以及更简单的中间件这些非必须功能。这些框架只是在标准库上述功能的基础上进行了一定的封装扩展,核心的http协议的解析依旧由net/http标准库完成。

学习这些框架,可能对你的抽象思维、设计理念得以提升,但阅读net/http标准库的源码才是真正打通你web开发任督二脉的关键一招,你将深入了解到http协议的解析过程、go的语言特性以及任何一种应用层协议框架的设计过程。

我们先看看如何通过net/http标准库写一个简单的web服务器:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
package main

import "net/http"

type myHandler struct{}

func (*myHandler) ServeHTTP(w http.ResponseWriter,r *http.Request){
	w.Write([]byte("hello world!"))
}

func main(){
	svr:=&http.Server{
		Addr: "127.0.0.1:8080",
		Handler: new(myHandler),
	}
	panic(svr.ListenAndServe())
}

你会发现启动一个web服务器很简单,只需要指定一个Addr以及一个Handler

  • Addr告诉框架监听哪个地址端口;
  • Handler是一个有ServeHTTP方法的接口,就好比一个拦截所有http请求的拦截器,告诉框架如何处理来自客户端的所有http请求。

Handler的存在给框架的拓展带来了极大的灵活性,有了Handler,我们可以让任何一个HTTP请求以自己的规则映射到自己的路由。比如http标准库用ServeMux类型实现了Handler接口,从而实现了静态路由(将在本系列的末尾讨论);gin的gin.Engine也是实现了自己的Handler,有了动态路由功能。

上述代码是整个http标准库的基石,我们目前将专注于它的实现,以其为中期实现目标。

需要注意的是本系列只专注net/http标准库服务端的实现。

Nodejs这门语言的http标准库也是实现了类似于上述代码的功能,不过它对http协议的解析不如go语言标准库彻底,如POST表单的解析,Nodejs还需要借助其他开源库的实现。同时Nodejs也没有自己实现一套静态路由机制。

需求分析

从需求分析的角度出发,看看我们的web框架大体上需要哪些功能:

  • http协议的解析不应该由开发者完成,我们需要从tcp字节流中解析出http的报文。
  • 框架需要设置Request并为Request绑定易用API。
  • 框架需要设置Response并为Response绑定易用API。

乍一看,我们需要给框架完成的功能甚少,但每一步都会有很多情况需要处理:

  1. 比如对于http 1.1协议来说,因为支持长连接,一个tcp连接能发送多个http请求,如果框架未正确完成上一个请求的解析(如未将当前报文主体全部读完),那么随之到来的下一个请求就无法正确解析。
  2. 客户端有时会以chunk方式传输报文主体,我们应该保证用户read到的只有有效载荷(playload),而没有chunk协议里的控制信息。
  3. 前端提交上来的form表单有多种类型,最常见的如application/x-www-form-urlencoded以及multipart/form-data,我们框架应该予以区分并分别提供解析方法。
  4. 服务端发送的数据是放在http响应报文的响应体中,客户端怎么知道我们发送了多少数据呢?一般来说可以查看响应头中的Content-Length字段,从而知道响应体的长度。观察上述的代码的ServeHTTP方法,我们并没有显式为头部指定Content-Length,但客户端依旧可以完整的读取出数据,这就说明标准库帮助我们完成了相关的设置工作。
  5. 从可行性角度来说,框架为我们的每一次响应都自动正确设置Content-Length(以下简称CT)是不现实的,发送CT所在的响应头必须是先于发送响应报文主体的,如果框架要自动设置CT,也就意味着我们必须为用户Write的所有数据进行缓存,这对一定长度内的发送量还实用,但对于大响应主体来说绝对是不可行。所以我们的框架还需要在必要时刻转化为利用chunk方式传输数据,这一部分对用户来说必须是无感知的。

除此之外,还有许多问题需要去解决,我们这里只是讲个大概,把我们实现过程中将遇到的几个主要问题进行了粗略讲解,主要是打个预防针。现在看见这些问题或者一些知识盲点,不需要感到焦虑,在随后的章节中我都会一一介绍并分别给出相应的解决方案。

框架流程

就以上述代码来说,svr.ListenAndServe()之后标准库底层到底做了哪些工作?它是怎么运行起来的?如果你是老手,应该可以很轻松的推测出大概流程。如果你是新手,别急,可以看看下面的讲解:

①在指定的Addr上监听tcp连接。②为每一个连接开启一个goroutine,在这个goroutine里从连接上读出Request并且生成response。③将Requestresponse作为参数传入用户指定的HandlerServeHTTP方法中。

当然这是省略掉部分细节后的流程,实际上的代码过程中,肯定会构建很多用来辅助的结构体。如net.Conn的表达能力过弱,我们会将其封装形成一个包内不考导出http.conn结构,这个结构逻辑上代表http连接,服务于我们的http协议解析过程。我们用更详细的图例方式剖析这个过程:

可以看见在框架内部中读到request以及response之后,框架会自动调用Handler的ServeHTTP方法,并将req和res作为传入参数,ServeHTTP中会执行我们框架用户事先告诉我们的处理逻辑。所以ServeHTTP无需用户手动调用,这也是很多新手很容易迷惑的一点。

httpd框架

本文的框架将命名为httpd框架,用于区分http标准库,该框架将支持http1.1协议。本系列将大概需要8篇文章才能完整的呈现标准库的设计,其中会遇到三处重难点:

  1. request中chunk协议的解析。

  2. 两种form表单的解析。

  3. 响应主体过大时,response自动转化为以chunk的方式传输数据。

我在初次阅读net/http库源码时,对于http协议的了解也仅停留在能够大概看懂http报文的水平,随后就兴致满满的扎入对源码的阅读,结果可想而知。所以我希望读者能够在阅读本教程的同时,配合着RFC文档或者http相关的权威书籍学习。本系列更注重框架的实现,对于http协议某些部分的讲解可能并不会过分细致,如有不懂的地方,还需要读者主动查阅。

框架的设计中所有模块的设计以及某种类型的引入,如果只告诉怎么实现,却不告知为什么,并不能将一个框架完整的设计思路呈现出来。所以我会对所有设计进行原因的阐述,告诉你引入这个模块的需求是什么,是为了解决怎样的难题,防止你产生见木不见林的感觉。

本系列主要面向希望go语言进阶的朋友,您最好具备以下技能:

  • 熟悉go语言的基础语法。
  • 掌握go语言socket编程。
  • 使用过net/http标准库。
  • 对http协议有基本的了解。

一、二条是必须项,本系列不会为这些地方做出讲解。三、四条非必须,但会让你随后的学习更轻松,我会在后面的章节进行相关的知识补充。

本章只是绪论,讲述本系列将干什么,以及让你对框架运行流程有个大概的了解,下一章将会为整个框架搭建核心的骨干,将各模块进行具体开发前的抽象,不会涉及http协议的解析。

本人水平有限,同时因为毕业季事务繁忙,难免会出现纰漏以及错误,如果你有很好的建议以及问题,欢迎在留言区评论。如果你觉得文章解决您的问题,对您有帮助,也欢迎您的赞赏支持,您的赞赏是我最大的写作动力。

系列目录