go语言从0实现net/http标准库(序):概述
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服务器:
|
|
你会发现启动一个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。
乍一看,我们需要给框架完成的功能甚少,但每一步都会有很多情况需要处理:
- 比如对于http 1.1协议来说,因为支持长连接,一个tcp连接能发送多个http请求,如果框架未正确完成上一个请求的解析(如未将当前报文主体全部读完),那么随之到来的下一个请求就无法正确解析。
- 客户端有时会以
chunk
方式传输报文主体,我们应该保证用户read到的只有有效载荷(playload),而没有chunk协议里的控制信息。 - 前端提交上来的form表单有多种类型,最常见的如
application/x-www-form-urlencoded
以及multipart/form-data
,我们框架应该予以区分并分别提供解析方法。 - 服务端发送的数据是放在http响应报文的响应体中,客户端怎么知道我们发送了多少数据呢?一般来说可以查看响应头中的
Content-Length
字段,从而知道响应体的长度。观察上述的代码的ServeHTTP
方法,我们并没有显式为头部指定Content-Length
,但客户端依旧可以完整的读取出数据,这就说明标准库帮助我们完成了相关的设置工作。 - 从可行性角度来说,框架为我们的每一次响应都自动正确设置
Content-Length
(以下简称CT)是不现实的,发送CT
所在的响应头必须是先于发送响应报文主体的,如果框架要自动设置CT
,也就意味着我们必须为用户Write的所有数据进行缓存,这对一定长度内的发送量还实用,但对于大响应主体来说绝对是不可行。所以我们的框架还需要在必要时刻转化为利用chunk
方式传输数据,这一部分对用户来说必须是无感知的。
除此之外,还有许多问题需要去解决,我们这里只是讲个大概,把我们实现过程中将遇到的几个主要问题进行了粗略讲解,主要是打个预防针。现在看见这些问题或者一些知识盲点,不需要感到焦虑,在随后的章节中我都会一一介绍并分别给出相应的解决方案。
框架流程
就以上述代码来说,svr.ListenAndServe()
之后标准库底层到底做了哪些工作?它是怎么运行起来的?如果你是老手,应该可以很轻松的推测出大概流程。如果你是新手,别急,可以看看下面的讲解:
①在指定的Addr上监听tcp连接。②为每一个连接开启一个goroutine,在这个goroutine里从连接上读出
Request
并且生成response
。③将Request
和response
作为参数传入用户指定的Handler
的ServeHTTP
方法中。
当然这是省略掉部分细节后的流程,实际上的代码过程中,肯定会构建很多用来辅助的结构体。如net.Conn
的表达能力过弱,我们会将其封装形成一个包内不考导出http.conn
结构,这个结构逻辑上代表http连接,服务于我们的http协议解析过程。我们用更详细的图例方式剖析这个过程:
可以看见在框架内部中读到request
以及response
之后,框架会自动调用Handler的ServeHTTP
方法,并将req和res作为传入参数,ServeHTTP
中会执行我们框架用户事先告诉我们的处理逻辑。所以ServeHTTP
无需用户手动调用,这也是很多新手很容易迷惑的一点。
httpd框架
本文的框架将命名为httpd框架,用于区分http标准库,该框架将支持http1.1协议。本系列将大概需要8篇文章才能完整的呈现标准库的设计,其中会遇到三处重难点:
-
request中chunk协议的解析。
-
两种form表单的解析。
-
响应主体过大时,response自动转化为以chunk的方式传输数据。
我在初次阅读net/http库源码时,对于http协议的了解也仅停留在能够大概看懂http报文的水平,随后就兴致满满的扎入对源码的阅读,结果可想而知。所以我希望读者能够在阅读本教程的同时,配合着RFC文档或者http相关的权威书籍学习。本系列更注重框架的实现,对于http协议某些部分的讲解可能并不会过分细致,如有不懂的地方,还需要读者主动查阅。
框架的设计中所有模块的设计以及某种类型的引入,如果只告诉怎么实现,却不告知为什么,并不能将一个框架完整的设计思路呈现出来。所以我会对所有设计进行原因的阐述,告诉你引入这个模块的需求是什么,是为了解决怎样的难题,防止你产生见木不见林的感觉。
本系列主要面向希望go语言进阶的朋友,您最好具备以下技能:
- 熟悉go语言的基础语法。
- 掌握go语言socket编程。
- 使用过net/http标准库。
- 对http协议有基本的了解。
一、二条是必须项,本系列不会为这些地方做出讲解。三、四条非必须,但会让你随后的学习更轻松,我会在后面的章节进行相关的知识补充。
本章只是绪论,讲述本系列将干什么,以及让你对框架运行流程有个大概的了解,下一章将会为整个框架搭建核心的骨干,将各模块进行具体开发前的抽象,不会涉及http协议的解析。
本人水平有限,同时因为毕业季事务繁忙,难免会出现纰漏以及错误,如果你有很好的建议以及问题,欢迎在留言区评论。如果你觉得文章解决您的问题,对您有帮助,也欢迎您的赞赏支持,您的赞赏是我最大的写作动力。
系列目录
- 01、go语言从0实现net/http标准库(序):概述
- 02、go语言从0实现net/http标准库(一):框架骨干
- 03、go语言从0实现net/http标准库(二):Request首部字段
- 04、go语言从0实现net/http标准库(三):Request设置Body
- 05、go语言从0实现net/http标准库(四):multipart表单
- 06、go语言从0实现net/http标准库(五):表单API封装
- 07、go语言从0实现net/http标准库(六):response
- 08、go语言从0实现net/http标准库(终):静态路由
- 原文作者:辜飞俊
- 原文链接:https://gufeijun.com/post/httpframe/1/
- 版权声明:本作品采用知识共享署名-非商业性使用-禁止演绎 4.0 国际许可协议进行许可,非商业转载请注明出处(作者,原文链接),商业转载请联系作者获得授权。