Golang Web入门(1):自顶向下理解Http服务器
摘要
由于Golang优秀的并发处理,很多公司使用Golang编写微服务。对于Golang来说,只需要短短几行代码就可以实现一个简单的Http服务器。加上Golang的协程,这个服务器可以拥有极高的性能。然而,正是因为代码过于简单,我们才应该去研究他的底层实现,做到会用,也知道为什么这么用。
在本文中,会以自顶向下的方式,从如何使用,到如何实现,一点点的分析Golang中net/http这个包中关于Http服务器的实现方式。内容可能会越来越难理解,作者会尽量把这些源码讲的更清楚一些,希望对各位有所帮助。
1 创建
首先,我们以怎么用为起点。
毕竟,知道了怎么用,才能一步一步的深入挖掘为什么这么用。
先来看第一种最简单的创建方式(省略了导包):
func helloWorldHandler(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "Hello World !")
}
func main() {
http.HandleFunc("/", helloWorldHandler)
http.ListenAndServe(":8000", nil)
}
其实在这一部分中,代码应该很容易理解。就是先做一个映射,把需要访问的地址,和访问后执行的函数,写在一起。然后再加上监听的端口,就可以了。
如果你是一个Java程序员,你应该能发觉这个和Java中的Servlet很相似。也是创建一个个的Servlet,然后注册。
再来看看第二种创建方式,也一样省略了导包:
type helloWorldHandler struct {
content string
}
func (handler *helloWorldHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, handler.content)
}
func main() {
http.Handle("/", &helloWorldHandler{content: "Hello World!"})
http.ListenAndServe(":8000", nil)
}
在这里,我们能发现相较于第一种方法,有些许的改动。
我们定义了一个结构体,然后又给这个结构体编写了一个方法。根据我们之前对于接口的概念:要实现一个接口必须要实现这个接口的所有方法。
那么我们是不是可以推测:存在这么一个接口A,里面有一个名为ServeHTTP的方法,而我们所编写的这个结构体,他已经实现了这个接口A了,他现在是属于这个A类型的一个结构体了。
type A interface{
ServeHTTP()
}
并且,在main函数中关于映射URI和方法的参数部分,需要调用实现了这个接口A的一个对象。
带着这个问题,我们可以继续往下。
2 注册
在第一部分,我们提到了两种注册方式,一种是传入一个函数,一种是传入一个结构体指针。
http.HandleFunc("/", helloWorldHandler)
http.Handle("/", &helloWorldHandler{content: "Hello World!"})
我们来看看http包内的源码:
package http
func HandleFunc(pattern string, handler func(ResponseWriter, *Request)) {
DefaultServeMux.HandleFunc(pattern, handler)
}
func Handle(pattern string, handler Handler) {
DefaultServeMux.Handle(pattern, handler)
}
先看一下这里的代码,他们被称为注册函数。
首先研究一下HandleFunc
这个函数。在main
函数中,调用了这个具有func(pattern string, handler func(ResponseWriter, *Request))
签名的函数,这里的pattern
是string
类型的,指的是匹配的URI,这个很容易理解。第二个参数是一个具有func(ResponseWriter, *Request)
签名的函数。
然后我们继续看,在这个函数中,调用了这个方法:
func (mux *ServeMux) HandleFunc(pattern string, handler func(ResponseWriter, *Request)) {
if handler == nil {
panic("http: nil handler")
}
mux.Handle(pattern, HandlerFunc(handler))
}
我们可以看到,最终是调用了DefaultServeMux
对象的Handle方法。
好,先到这里,我们再看一看刚刚提到的签名为func (pattern string, handler Handler)
另外一个函数。在这个函数里面,同样是调用了DefaultServeMux
对象的Handle方法。
也就是说,无论我们使用哪种注册函数,最终调用的都是这个函数:
func (mux *ServeMux) Handle(pattern string, handler Handler)
这里涉及到了两种对象,第一是ServeMux
对象,第二是Handler
对象。
ServeMux
对象我们一会再聊,先聊聊Handler
对象。
type Handler interface {
ServeHTTP(ResponseWriter, *Request)
}
在Golang中,Handler是一种接口类型,只要一个类型的对象实现了ServeHTTP
这个方法,就可以称这个对象是Handler
类型的。
注意到,在前面有一行代码是这样的:
mux.Handle(pattern, HandlerFunc(handler))
有人可能会想,HandlerFunc func(ResponseWriter, *Request)
这个函数,是输入一个函数,返回一个Handler
类型的对象,其实这是不对的。我们来看看这个函数的源码:
type HandlerFunc func(ResponseWriter, *Request)
func (f HandlerFunc) ServeHTTP(w ResponseWriter, r *Request) {
f(w, r)
}
我们可以发现,这个函数,他是属于一种叫HandlerFunc
类型的函数。
在Golang中,这是一种很特别的特性。我们可以将函数设置为一种类型,或者你可以理解成变量,你可以用一个变量名去表示这个函数,可以把这个函数赋值给某一个变量。
fn := func(){
fmt.Println("x is",x)
}
fn()
所以,在这里这个函数类型也是实现了ServeHTTP
方法的,也就是说,这个名为HandlerFunc
的函数类型,也是属于Handler
类型的。所以,这个方法其实并不是输入一组参数,返回一个Handler
类型,而是他本身就是一个Handler
类型,可以直接调用ServeHTTP
方法。
这里比较绕,但是相信当你理解了之后,会感觉妙啊。
说完了Handler
,我们再来聊聊ServeMux
。先来看看他的结构:
type ServeMux struct {
mu sync.RWMutex
m map[string]muxEntry
es []muxEntry // slice of entries sorted from longest to shortest.
hosts bool // whether any patterns contain hostnames
}
type muxEntry struct {
h Handler
pattern string
}
我们先关注一下这个结构里面的m
字段。这个字段是一个map
类型,key是URI
,value是muxEntry
类型。而这个muxEntry
类型,里面包含了一个Handler
和URI
。也就是说,通过这个m
字段,我们可以用URI
找到对应的Handler
对象。
继续说回上面提到的func (mux *ServeMux) Handle(pattern string, handler Handler)
方法。我们已经知道了调用这个方法的对象是ServeMux
,也知道了这个方法的参数中的Handler
是什么,下面让我们来看看这个方法的详细实现:
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)
}
e := muxEntry{h: handler, pattern: pattern}
mux.m[pattern] = e
if pattern[len(pattern)-1] == '/' {
mux.es = appendSorted(mux.es, e)
}
if pattern[0] != '/' {
mux.hosts = true
}
}
在这个方法中,我们可以看到,Handle方法会先判断传入的URI
和handler
是否合法,然后判断这个URI
对应的处理器是否已经注册,然后将这个URI
和handler
对应的map
写入ServeMux
对象中。
注意,这里还有一个步骤。如果这个URI
是以/
结尾的,将会被送入es数组
中,按长度排序。至于为什么会这么做,我们在后面的内容将会提到。
说完了这些,我们应该可以猜到这个ServeMux
对象的作用了。他可以存储我们注册的URI
和Handler
,以实现当有请求进来的时候,可以委派给相对应的Handler
的功能。
考虑到这个功能,那么我们也可以推断出,这个ServeMux
也是一个Handler
,只不过他和其他的Handler
不同。其他的Handler
处理的是具体的请求,而这个ServeMux
处理的是请求的分配。
所以,ServeMux也实现了ServeHTTP方法,他也是一个Handler。而对于他是怎么实现ServeHTTP方法的,我们也在后面的内容提到。
3 监听
现在,让我们来聊聊main函数中的第二行:
http.ListenAndServe(":8000", nil)
按照惯例,我们来看一看这个方法的实现:
func ListenAndServe(addr string, handler Handler) error {
server := &Server{Addr: addr, Handler: handler}
return server.ListenAndServe()
}
这里的Server,是一个复杂的结构体,里面包含了设置服务器的很多参数,但是这里我们只聊Addr
和Handler
这两个属性。
Addr
很容易理解,就是这个服务器所监听的地址。
Handler
是处理器,负责把请求分配给各个对应的handler
。在这里留空,则使用Golang默认的处理器,也就是上文中我们提到的实现了ServeHTTP
方法的ServeMux
。
知道了这些,我们继续往下看server.ListenAndServe()
的实现:
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)
}
这里比较重要的有两行,第一是ln, err := net.Listen("tcp", addr)
,也就是说,开始监听addr
这个地址的tcp连接。
然后,调用srv.Serve(ln)
,我们来看看代码(省略部分,只保留与本文有关的逻辑):
func (srv *Server) Serve(l net.Listener) error {
...
for{
...
c := srv.newConn(rw)
c.setState(c.rwc, StateNew) // before Serve can return
go c.serve(connCtx)
}
}
简单来讲,在这个方法中,有一个死循环,他不断接收新的连接,然后启动一个协程,处理这个连接。我们来看看c.serve(connCtx)
的具体实现:
func (c *conn) serve(ctx context.Context) {
...
serverHandler{c.server}.ServeHTTP(w, w.req)
...
}
省略其他所有的细节,最关键的就是这一行代码了,然后我们再看看这个ServeHTTP
方法。注意,这里的c.server
,还是指的是最开始的那个Server结构体。坚持一下下,马上就到最关键的地方啦:
type serverHandler struct {
srv *Server
}
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)
}
这里的ServeHTTP
方法逻辑很容易看出,如果最开始没有定义一个全局处理的Handler
,则会使用Golang的默认handler
:DefaultServeMux
。
假设,我们这里使用的是DefaultServeMux
,执行ServeHTTP
方法。说到这里你是否有印象,我们在上一个章节里提到的:
所以,ServeMux也实现了ServeHTTP方法,他也是一个Handler。而对于他是怎么实现ServeHTTP方法的,我们也在后面的内容提到。
就是这里,对于ServeMux
来说,他就是一个处理请求分发的Handler
。
如果你学过Java,我跟你说他和ServletDispatcher
很相似,你应该能理解吧。
4 处理
到了这里,就是最后一步了,我们来看看这里处理请求分发的ServeHTTP
方法具体实现:
func (mux *ServeMux) ServeHTTP(w ResponseWriter, r *Request) {
...
h, _ := mux.Handler(r)
h.ServeHTTP(w, r)
}
在省去其他细节之后我们应该可以推断,这个mux.Handler(r)
方法返回的h
,应该是所请求的URI
所对应的Handler
。然后,执行这个Handler
所对应的ServeHTTP
方法。我们来看看mux.Handler(r)
这个方法:
func (mux *ServeMux) Handler(r *Request) (h Handler, pattern string) {
...
host := stripHostPort(r.Host)
path := cleanPath(r.URL.Path)
...
return mux.handler(host, r.URL.Path)
}
func (mux *ServeMux) handler(host, path string) (h Handler, pattern string) {
mux.mu.RLock()
defer mux.mu.RUnlock()
// Host-specific pattern takes precedence over generic ones
if mux.hosts {
h, pattern = mux.match(host + path)
}
if h == nil {
h, pattern = mux.match(path)
}
if h == nil {
h, pattern = NotFoundHandler(), ""
}
return
}
到了这里,代码就变得简洁明了了。重点就是这个mux.match
方法,会根据地址,来返回对应的Handler。我们来看看这个方法:
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. mux.es contains all patterns
// that end in / sorted from longest to shortest.
for _, e := range mux.es {
if strings.HasPrefix(path, e.pattern) {
return e.h, e.pattern
}
}
return nil, ""
}
这段代码也应该很容易理解。如果在ServeMux
中存储了key
为这个URI的路由规则的映射,则直接返回这个URI
对应的Handler
。
否则,就去匹配es数组
。还记得吗,这个数组是之前注册路由的时候提到的,如果URI
是以/
结尾的,就会把这个路由映射添加到es数组中
,并由长到短进行排序。
这样的作用是,可以优先匹配到最长的URI
,以达到近似匹配的时候能够匹配到最合适的路由的目的。
至此,返回对应的Handler
,然后执行,就成功的实现了处理相对应的请求了。
写在最后
首先,谢谢你能看到这里!
不知道你有没有理解我所说的内容,希望这篇文章可以给你一些帮助。
其实写这篇文章的目的是这样的,学完了Golang的基础之后作者准备开始研究Golang Web。但是查找各种资料后发现,并没有找到一条很合适的学习路线。然后本来作者打算去直接研究一个框架,如MeeGo,Gin等。但是又考虑到,框架只是用来解决问题的,学会了框架却不知道基础内容,有种知其然不知其所以然的感觉。
所以,作者打算从Golang的net/http包的源码开始,慢慢去了解怎么用原生的Go语言去建立一个HTTP服务器,然后去了解一下怎么进行缓存,做持久化等,这也是作者思考之后决定的一条学习路线。当能够把这些内容都研究明白之后,再去研究框架,去看这些框架是怎么解决问题的,可能才是比较合适的。
当然了,作者也是刚入门。所以,可能会有很多的疏漏。如果在阅读的过程中,有哪些解释不到位,或者理解出现了偏差,也请你留言指正。
再次感谢~
PS:如果有其他的问题,也可以在公众号找到作者。并且,所有文章第一时间会在公众号更新,欢迎来找作者玩~
Golang Web入门(1):自顶向下理解Http服务器的更多相关文章
- Golang Web入门(4):如何设计API
摘要 在之前的几篇文章中,我们从如何实现最简单的HTTP服务器,到如何对路由进行改进,到如何增加中间件.总的来讲,我们已经把Web服务器相关的内容大概梳理了一遍了.在这一篇文章中,我们将从最简单的一个 ...
- Golang Web入门(2):如何实现一个高性能的路由
摘要 在上一篇文章中,我们聊了聊在Golang中怎么实现一个Http服务器.但是在最后我们可以发现,固然DefaultServeMux可以做路由分发的功能,但是他的功能同样是不完善的. 由Defaul ...
- Golang Web入门(3):如何优雅的设计中间件
摘要 在上一篇文章中,我们已经可以实现一个性能较高,且支持RESTful风格的路由了.但是,在Web应用的开发中,我们还需要一些可以被扩展的功能. 因此,在设计框架的过程中,应该留出可以扩展的空间,比 ...
- golang快速入门(五)初尝web服务
提示:本系列文章适合对Go有持续冲动的读者 初探golang web服务 golang web开发是其一项重要且有竞争力的应用,本小结来看看再golang中怎么创建一个简单的web服务. 在不适用we ...
- COM编程入门第二部分——深入COM服务器
本文为刚刚接触COM的程序员提供编程指南,解释COM服务器内幕以及如何用C++编写自己的接口.继上一篇COM编程入门之后,本文将讨论有关 COM服务器的内容,解释编写自己的COM接口和COM服务器所需 ...
- golang web实战之二(iris)
之前写了一篇为:golang web实战之一(beego,mvc postgresql) 听说iris更好: 1. iris hello world package main import &quo ...
- 建站有很多技术,如 HTML、HTML5、XHTML、CSS、SQL、JavaScript、PHP、http://ASP.NET、Web Services、浏览器脚本、服务器脚本等。它们的区别是什么?新手一点不懂,想理清所有这些技术之间的关系和应用范围。
先普及用户通过 浏览器 访问网页 的过程: 网页内容是通过服务器运算得出的结果,将结果(网页代码)传输给浏览器,网页代码再通过浏览器运算(计算.渲染),最终展示在用户的眼前的. 至此,我们知道了有2个 ...
- Golang快速入门
Go语言简介: Golang 简称 Go,是一个开源的编程语言,Go是从2007年末由 Robert Griesemer, Rob Pike, Ken Thompson主持开发,后来还加入了Ian L ...
- Web入门
目录 Web入门 学习web路线 前端基础 三剑客的作用 BS架构 数据格式 HTTP协议 四大特性 数据格式 HTTP 状态码分类 状态码列表 案例:简易的BS架构 Web入门 什么是前端? 任何与 ...
随机推荐
- 初识Flask、快速启动
目录 一.初识Flask 1.1 什么是flask? 1.2 为什么要有flask? 二.Flask快速启动 一.初识Flask 1.1 什么是flask? Flask 本是作者 Armin Rona ...
- tomcat 对 vue的history默认支持 tomcat 开启步骤 1.build文件放入webapps目录 2.进入conf目录修改server.xml端口号改成8088 3.进入bin目录运行startup.bat 4.浏览器 localhost:8088/workName 访问即可
tomcat 对 vue的history默认支持 tomcat 开启步骤 1.build文件放入webapps目录 2.进入conf目录修改server.xml端口号改成8088 3.进入bin目录运 ...
- Jenkins的凭证管理
Jenkins的凭证管理 什么是凭证? 凭证(cridential)是Jenkins进行受限操作时的凭据.比如使用SSH登录远程机器时,用户名和密码或SSH key就是凭证.而这些凭证不可能以明文写在 ...
- js实践篇:例外处理Try{}catch(e){}
本文转载至:http://blog.csdn.net/ocean20/article/details/7301008 程序开发中,编程人员经常要面对的是如何编写代码来响应错误事件的发生,即例外处理(e ...
- IDEA 激活码,IDEA 注册码,IDEA 2019.3 激活码
文末分享:IDEA 激活码, IDEA 注册码,已购买的正版授权,请不要网上传播. 声明:支持知识产权,支持正版产权,以下仅限个人学习使用IDEA工具时随笔记录,禁止商业使用. 一.下载idea 下载 ...
- (转载)Why you shouldn't use Entity Framework with Transactions
Why you shouldn't use Entity Framework with Transactions EntityFramework This is a .net ORM Mapper F ...
- Vue中使用axios发送ajax请求
作为前后端交互的重要技巧--发送ajax请求,在Vue中我们使用axio来完成这一需求: 首先是下载axios的依赖, npm install --save axios vue-axios 然后在ma ...
- drf-jwt的过滤,筛选,排序,分页组件
目录 自定义drf-jwt配置 案例:实现多方式登陆签发token urls.py models.py serializers.py views.py 案例:自定义认证反爬规则的认证类 urls.py ...
- Java——Collection集合
##Collection集合 1.Collection集合是单列集合 2.Collection是所有单列集合最顶层的接口,定义了所有单列集合的共性方法 任意的单列集合都可以使用Collection接口 ...
- Spark作业提交至Yarn上执行的 一个异常
(1)控制台Yarn(Cluster模式)打印的异常日志: client token: N/A diagnostics: Application application_1584359 ...