这是Golang源码分析之net/http主题的第二篇,我们已经在上一章节了解了TCP通信的基础框架,现在进入正真的源码分析
以下源码基于go1.14.13 windows/amd64进行分析
一、举个栗子
我们写一个C/S例子
service监听1234端口,设置路由为/hello,处理函数为HelloServer
1 | //service.go |
client设置每1s发送一个请求,并打印服务端的返回值
1 | // client.go |
现在启动2个进程(先启服务器,再其客户端)
1 | // service.go的日志打印 |
查看网络状态,可以看到客户端和服务端已经建立TCP连接,在双端都是ESTABLISHED状态下进行读写。LISTEN是客户端在监听客户端的接入情况。
二、源码分析
1、服务端源码
1.1、http.HandleFunc 将处理请求的函数注册到路由器map中
1 | // 将处理函数handler 注册到默认的路由器DefaultServeMux中,服务器监听到对应的路由,会使用该handler处理 |
DefaultServeMux也即ServeMux,可以看下该路由存储的变量
1 | type ServeMux struct { |
将pattern路由及对应的处理器以hash表形式存储,在HTTP处理请求的时候会查找对应路由的处理器
1 | func (mux *ServeMux) Handle(pattern string, handler Handler) { |
1.2、http.ListenAndServe 用来监听 TCP 连接并处理请求
1 | func ListenAndServe(addr string, handler Handler) error { |
ListenAndServe监听对应地址的TCP并处理对应的客户端请求
1 | func (srv *Server) ListenAndServe() error { |
这里我们看一下net.Listen接口,包含了创建socket、bind绑定socket与地址、listen端口操作。
1 | func socket(ctx context.Context, net string, family, sotype, proto int, ipv6only bool, laddr, raddr sockaddr, ctrlFn func(string, string, syscall.RawConn) error) (fd *netFD, err error) { |
监听客户端的请求,使用for循环一直等待客户端的请求。
1 | // 监听客户端的请求 |
处理客户端的请求
1 | // Serve a new connection. |
交给DefaultServeMux路由来处理
1 | func (sh serverHandler) ServeHTTP(rw ResponseWriter, req *Request) { |
2、客户端源码
http.NewRequest NewRequest根据方法名、URL 和请求体构建请求。该方法封装成HTTP协议请求的格式,不详细展开
client.Do(req) 处理请求
对于Do方法,我们详细展开,根据层层调用最核心接口即是roundTrip。该接口发送HTTP请求并处理Resp
1 | func (t *Transport) roundTrip(req *Request) (*Response, error) { |
连接是一种相对比较昂贵的资源,如果在每次发出 HTTP 请求之前都建立新的连接,可能会消耗比较多的时间,带来较大的额外开销,通过连接池对资源进行分配和复用可以有效地提高 HTTP 请求的整体性能,多数的网络库客户端都会采取类似的策略来复用资源。
所以在获取连接(t.getConn的时候,有两种方法。
第一种:选取在队列中闲置的连接
长链接由一个map[[connectMethodKey]][][]*persistConn组成,connectMethodKey即为一个连接的key包含地址等信息,对应value为persistConn为连接的句柄。选取队列中的连接也就是获取一个句柄。若能获取成功,把该连接从队列中以及idleLRU中删除该连接。
第二种:如果空闲的连接池中没有可用的连接,则会调用queueForDial方法新建连接
使用Dial创建TCP连接,也就是发起socket的listen()。连接创建成功后,会开启两个协程,一个用于处理输入流writeLoop,一个用于处理输出流readLoop。分别从 TCP 连接中读取数据或者向 TCP 连接写入数据,从建立连接的过程我们可以发现,如果我们为每一个 HTTP 请求都创建新的连接并启动 Goroutine 处理读写数据,会占用很多的资源。
发送请求之后,roundTrip接口中pconn.roundTrip处理响应
1 | func (pc *persistConn) roundTrip(req *transportRequest) (resp *Response, err error) { |