Golang之所以非常适合用于网络编程的原因之一就是其自带网络库可以非常简单快速的建立一个基于http或者tcp的服务应用,以http服务为例,只需几行代码:
1 | package main |
上面这几行代码就启动了一个http服务,运行在8888端口,虽然说非常简陋,也不区分GET或者POST,但是其性能缺十分高效,主要是得益于其底层使用了协程,对每一个请求都会分配一个协程去处理,并发能力强。
1.ISO网络模型
说到网络,不得不说一下这个模型,http本质上是一个基于tcp协议的应用层协议,而且http是超文本传输协议,注意这里的文本是指其通信协议是文本形式(具体表现就是请求头和响应头),其传输的内容并不一定是文本,可以是任何内容,图片等二进制内容都可以。
说到tcp就不得不说下socket网络编程,上面这张图基本上描述了tcp网络通信的一个流程,这和http有什么关系呢?
实际上,这种图里面处理请求
这部分则是http服务应该做的东西,tcp只负责传输控制,至于内容,其协议可能是http,也可能是ftp,甚至有可能是自定义的协议。
网上借张http协议报文的图看一下:
实际上,协议内容非常复杂,对于每一个字段代表的意思和应该出现的位置都有规定,有人可能说,我拿chrome F12打开控制台看到和这个不一样啊,那是因为浏览器把请求解析了一行行显示出来方便咱调试而已。
如果你理解了这2张图,你应该理解了一个http请求是怎么发起,怎么响应的,但是实际应用中,Go又是怎么做到解析协议并且响应结果的呢?
2.DefaultServeMux
让我们点开源码,看看这几行代码到底干了啥,首先看一下个 HandlFunc()
,Go里面关于http server的代码都在server.go这个文件里面,总共3000多行,有点多,这里摘取部分。
1 | type ServeMux struct { |
这里的核心是ServeMux
这个结构体,按照注释的介绍,这个是一个http server的多路复用分发器,顾名思义,它是用来处理分发请求的,下面是它实现的一些方法:
这个结构体有4个成员属性,其中mu是一个读写互斥锁;m是一个map,其key是一个string(实际上也是路由),value是一个muxEntry;这个muxEntry则是代表了handler和pattern,其中pattern就是咱说的路由,又叫请求path。
HandleFunc最终调用了Handle方法,其主要目的是把路由和handler函数做一个映射关系,简单说就是注册路由:
1 | // Handle registers the handler for the given pattern. |
这里需要注意一点的是ServeMux
里面包含一个es,它保存了一个有序的entry,是根据pattern从长到短排序,不知道有啥用。。。
总结,这里面的DefaultServeMux
就是库里面自己已经初始化好的一个结构体,我们可以直接用,使用它的handle方法就可以注册路由,这时有人可能会问,那咱自己动手行不行呢?当然可以
1 | package main |
上面这种写法就是自己new一个ServeMux,如果我们点开ListenAndServe
这个方法,可以看到注释非常明确的写到如果第二个参数为nil则会默认使用DefaultServeMux
,这也就解释了为什么第一种写法这个参数是nil。
1 | // ListenAndServe listens on the TCP network address addr and then calls |
2.ListenAndServe
前面只是一些准备工作,真正的逻辑是在这个方法里面,首先,这个方法接受2个参数,一个是监听的地址,一个Handler
,handler是一个interface,它只有一个方法需要实现:
1 | // A Handler responds to an HTTP request. |
如果你留意了你会发现,之前那个ServeMux
也是实现了这个方法,所以我们可以这么用。
哎,这时候你有一个大胆的想法,那如果我自己定义一个结构体去实现这个方法,是不是连ServeMux
都不用new了?你别说还真是这样
1 | package main |
但是你这么写,就连基本的路由功能都没了。。。因为ServeMux
本质上就是带了一个路由功能而已,相当于官方库实现的一个路由,虽然不够强大,但是基本够用,很多第三方框架甚至会自己实现更强大的路由功能。
继续看这个ListenAndServe
源码,可以看到它初始化了一个Server
,把地址和Handler传进去了,这个Server是最重要的一个结构体了,它里面的成员和方法特别多,这里就不列出了,直接看调用的方法。
1 | func (srv *Server) ListenAndServe() error { |
这里可以看到它调用了net.Listen
监听了端口的tcp请求,这里我就不继续往下追了,因为我认为这里面往下都是属于tcp传输层的东西,不是本文研究的重点,有兴趣的童鞋可以继续追进去看一下。
最终把TCPListener
转换成一个tcpKeepAliveListener
调用了方法Serve:
1 | // Serve accepts incoming connections on the Listener l, creating a |
其中比较核心的是for循环里面那一段,不断的Accept
新的请求,然后通过srv.newConn
创建一个新的连接,然后开启一个go协程处理这个请求。这个srv.newConn
返回的是一个conn
结构体,其成员和函数也非常之多,它代表的是服务的一个http连接。
下面这段代码是处理http请求协议内容的核心代码:
1 | // Serve a new connection. |
后面的代码非常之多,不太好解析,这里捡个重点说说,其中解析http协议调用的是readRequest
方法,里面做了很多操作,比如说解析请求头的一些属性,比如请求类型、协议版本、请求URI、缓存控制等等,把他们放入到Request
对象里面,这个结构体也是我们日常开发中最常用到的,我们会从这里获取所需要的请求信息。
1 | // parseRequestLine parses "GET /foo HTTP/1.1" into its three parts. |
当然其中还有一个非常重要的操作,那就是调用我们之前注册的handler,这个操作是在解析完http请求之后的地方,然后后面就是一些收尾操作。
3.总结
相信很多人看完之后还是一脸懵逼,我也差不多,虽然说看上去都是合情合理,但是很多细节并没有深入去研究,虽然说http协议是一个文本协议,但是其解析处理也绝非易事。对于很多使用Go做Web开发的人来说,有必要简单了解一下,有助于了解网络请求的处理过程,加深对协议的理解。