[TOC]
探究一下net/http 的代码流程
net/http 是什么?
是GO的其中一个标准库,用于Web应用的开发,使用这个库,可以让开发变得更加迅速和简便,且易于上手。
那么问题来了
使用库,确实方便,无脑调接口,拼拼凑凑能跑就行,管他效率性能,出了问题,删库跑路就行了。。。
实际真的是这个样子吗?作为一个开发,一定要想办法弄明白不清楚的事情,要弄明白用到工具的原理,更需要清晰的知道自己开发产品的运作原理,正所谓
知其然,而不知其所以然,欲摹写其情状,而心不能自喻,口不能自宣,笔不能自传。
我们对于技术要有探索精神,对代码要有敬畏之心,那今天咱们就来看看net/http
的代码流程吧
使用框架/库,必要要接受其自身的一套约定和模式,我们必须要了解和熟悉这些约定和模式的用法,否则就会陷入用错了都不知道的境地。
在GOLANG中,net/http
的组成部分有客户端 和 服务端
库中的结构和函数有的只支持客户端和服务器这两者中的一个,有的同时支持客户端和服务器,用图说话:
- 只支持客户端的
Client , response
- 只支持服务端的
ServerMux,Server ,ResponseWriter,Handler 和 HandlerFunc
- 客户端,服务端都支持的
Header , Request , Cookie
net/http构建服务器也很简单,大体框架如下:
客户端 请求 服务器,服务器里面使用 net/http
包,包中有多路复用器,和对应多路复用器的接口,服务器中的多个处理器处理不同的请求,最终需要落盘的数据即入库
万里长城第一步,我们发车了
开始写一个简单的Request
package main | |
import ( | |
"fmt" | |
"net/http" | |
) | |
func main() { | |
http.HandleFunc("/Hi", func(w http.ResponseWriter, r *http.Request) { | |
// <h1></h1> 是html 标签 | |
w.Write([]byte("<h1>Hi xiaomotong</h1>")) | |
}) | |
// ListenAndServe 不写ip 默认服务器地址是 127.0.0.1 | |
if err := http.ListenAndServe(":8888", nil); err != nil { | |
fmt.Println("http server error:", err) | |
} | |
} |
运行服务器代码,在浏览器中输入127.0.0.1:8888/Hi
或者localhost:8888/Hi
,即可看到效果
创建一个Go写的服务器就是那么简单,只要调用ListenAndServe
并传入网络地址,端口,处理请求的处理器(handler)即可。
注意:
- 如果网络地址参数为空字符串,那么服务器默认使用80端口进行网络连接
- 如果处理器参数为
nil
,那么服务器将使用默认的多路复用器DefaultServeMux
。
可是实际上http是如何建立起来的呢?一顿操作猛如虎,一问细节二百五
HTTP的建立过程
HTTP的建立流程都是通用的,因为他是标准协议。写C/C++
的时候,这些流程基本上自己都要去写一遍,但是写GO
的时候,标准库里面已经封装好了,因此才会有上述一个函数就可以写一个web服务器的情况
服务端涉及的流程
- socket建立套接字
- bind绑定地址和端口
- listen设置最大监听数
- accept开始阻塞等待客户端的连接
- read读取数据
- write回写数据
- close 关闭
客户端涉及的流程
- socket建立套接字
- connect 连接服务端
- write写数据
- read读取数据
那么数据在各个层级之间是如何走的呢?
还是那个熟悉的7层OSI
模型,不过实际应用的话,我们用TCP/IP
5层模型
上述TCP/IP五层模型,可能会用到的协议大体列一下
- 应用层:
- HTTP协议,SMTP,SNMP,FTP,Telnet,SIP,SSH,NFS,RTSP
- 传输层
比较常见的协议是TCP,UDP,SCTP,SPX,ATP等
UDP不可靠, SCTP有自己特殊的运用场景, 所以一般情况下HTTP是由TCP协议进行传输
不过企业应用的话,会将UDP改造成可靠的传输,实际上是对标准udp上封装自定义的头,模拟TCP的可靠传输,三次握手, 四次挥手就是在这里发生的
- 网络层
IP协议、ICMP,IGMP,IPX,BGP,OSPF,RIP,IGRP,EIGRP,ARP,RARP协议 ,等等
- 数据链路层
Ethernet , PPP,WiFi ,802.11等等
- 物理层
SO2110,IEEE802 等等
知道HTTP的通用流程,那么我们来具体看看net/http
标准库是如何实现这整个流程的,先从建立socket看起
net/http 建立socket
还记得最上面说到的request小案例吗?我们可以从这里开始入手
package main | |
import ( | |
"fmt" | |
"net/http" | |
) | |
func main() { | |
http.HandleFunc("/Hi", func(w http.ResponseWriter, r *http.Request) { | |
w.Write([]byte("<h1>Hi xiaomotong</h1> ")) | |
}) | |
if err := http.ListenAndServe(":8888", nil); err != nil { | |
fmt.Println("http server error:", err) | |
} | |
} |
http.HandleFunc(“/Hi”, func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte(“
Hi xiaomotong
“))
})
HandleFunc这一段是注册路由,这个路由的handler会默认放到到DefaultServeMux
中
// HandleFunc registers the handler function for the given pattern | |
// in the DefaultServeMux. | |
// The documentation for ServeMux explains how patterns are matched. | |
func HandleFunc(pattern string, handler func(ResponseWriter, *Request)) { | |
DefaultServeMux.HandleFunc(pattern, handler) | |
} |
HandleFunc 实际上是调用了 ServeMux
服务的HandleFunc
// HandleFunc registers the handler function for the given pattern. | |
func (mux *ServeMux) HandleFunc(pattern string, handler func(ResponseWriter, *Request)) { | |
if handler == nil { | |
panic("http: nil handler") | |
} | |
mux.Handle(pattern, HandlerFunc(handler)) | |
} |
ServeMux
服务的HandleFunc
调用了自己服务的Handle
实现
// Handle registers the handler for the given pattern. | |
// If a handler already exists for pattern, Handle panics. | |
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 | |
} | |
} |
看了实际的注册路由实现还是比较简单,我们先不再深入的网下看,要是感兴趣的可以接着这里往下追具体的数据结构
目前的注册路由的流程是:
- http.HandleFunc ->
- (mux *ServeMux) HandleFunc ->
- (mux *ServeMux) Handle
net/http监听端口+响应请求
那我们在来看看刚才request案例里面的监听地址和端口的代码是如何走的
if err := http.ListenAndServe(“:8888”, nil); err != nil {
fmt.Println(“http server error:”, err)
}
经过上面的三个函数流程,已经知道注册路由是如何走的了,那么ListenAndServe
这个函数的监听已经handler处理数据后的响应是如何实现的呢?来我们继续
ListenAndServe
侦听TCP网络地址addr,然后调用handler来处理传入连接的请求,收的连接配置为启用TCP keep-alive,该参数通常为nil,在这种情况下使用DefaultServeMux
,上面提过一次,此处再次强调
// ListenAndServe listens on the TCP network address addr and then calls | |
// Serve with handler to handle requests on incoming connections. | |
// Accepted connections are configured to enable TCP keep-alives. | |
// | |
// The handler is typically nil, in which case the DefaultServeMux is used. | |
// | |
// ListenAndServe always returns a non-nil error. | |
func ListenAndServe(addr string, handler Handler) error { | |
server := &Server{Addr: addr, Handler: handler} | |
return server.ListenAndServe() // 调用Server服务的 ListenAndServe函数 (srv *Server) ListenAndServe | |
} | |
// ListenAndServe listens on the TCP network address srv.Addr and then | |
// calls Serve to handle requests on incoming connections. | |
// Accepted connections are configured to enable TCP keep-alives. | |
// | |
// If srv.Addr is blank, ":http" is used. | |
// | |
// ListenAndServe always returns a non-nil error. After Shutdown or Close, | |
// the returned error is ErrServerClosed. | |
func (srv *Server) ListenAndServe() error { | |
if srv.shuttingDown() { | |
return ErrServerClosed | |
} | |
addr := srv.Addr | |
if addr == "" { | |
addr = ":http" | |
} | |
ln, err := net.Listen("tcp", addr) //实际是通过 net.Listen 进行监听地址和端口的 | |
if err != nil { | |
return err | |
} | |
return srv.Serve(ln) | |
} | |
func Listen(network, address string) (Listener, error) { | |
var lc ListenConfig | |
return lc.Listen(context.Background(), network, address) | |
} | |
func (srv *Server) Serve(l net.Listener) error { | |
if fn := testHookServerServe; fn != nil { | |
fn(srv, l) // call hook with unwrapped listener // 回调函数的调用的位置 | |
} | |
// ...此处省略15行代码... | |
var tempDelay time.Duration // how long to sleep on accept failure // accept阻塞失败睡眠的间隔时间 | |
ctx := context.WithValue(baseCtx, ServerContextKey, srv) | |
for { | |
// 开始Accept 阻塞监听客户端的连接 | |
rw, err := l.Accept() | |
if err != nil { | |
select { | |
case <-srv.getDoneChan(): | |
return ErrServerClosed | |
default: | |
} | |
if ne, ok := err.(net.Error); ok && ne.Temporary() { | |
if tempDelay == 0 { | |
tempDelay = 5 * time.Millisecond | |
} else { | |
tempDelay *= 2 | |
} | |
if max := 1 * time.Second; tempDelay > max { | |
tempDelay = max | |
} | |
srv.logf("http: Accept error: %v; retrying in %v", err, tempDelay) | |
time.Sleep(tempDelay) | |
continue | |
} | |
return err | |
} | |
connCtx := ctx | |
if cc := srv.ConnContext; cc != nil { | |
connCtx = cc(connCtx, rw) | |
if connCtx == nil { | |
panic("ConnContext returned nil") | |
} | |
} | |
tempDelay = 0 | |
c := srv.newConn(rw) | |
c.setState(c.rwc, StateNew, runHooks) // before Serve can return | |
go c.serve(connCtx) // 此处开一个协程来处理具体的请求消息 | |
} | |
} |
此处通过 go c.serve(connCtx)
开启一个协程专门处理具体的请求消息
// Serve a new connection. | |
func (c *conn) serve(ctx context.Context) { | |
c.remoteAddr = c.rwc.RemoteAddr().String() | |
ctx = context.WithValue(ctx, LocalAddrContextKey, c.rwc.LocalAddr()) | |
// ... 此处省略部分代码 | |
// HTTP cannot have multiple simultaneous active requests.[*] | |
// Until the server replies to this request, it can't read another, | |
// so we might as well run the handler in this goroutine. | |
// [*] Not strictly true: HTTP pipelining. We could let them all process | |
// in parallel even if their responses need to be serialized. | |
// But we're not going to implement HTTP pipelining because it | |
// was never deployed in the wild and the answer is HTTP/2. | |
//HTTP不能同时有多个活动请求。[*],直到服务器响应这个请求,它不能读取另一个 | |
serverHandler{c.server}.ServeHTTP(w, w.req) // ServeHTTP 是重点 | |
w.cancelCtx() | |
if c.hijacked() { | |
return | |
} | |
w.finishRequest() | |
// ... 此处省略部分代码 | |
} |
此处ServeHTTP
相当重要
// serverHandler delegates to either the server's Handler or | |
// DefaultServeMux and also handles "OPTIONS *" requests. | |
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 dispatches the request to the handler whose | |
// pattern most closely matches the request URL. | |
func (mux *ServeMux) ServeHTTP(w ResponseWriter, r *Request) { | |
if r.RequestURI == "*" { | |
if r.ProtoAtLeast(1, 1) { | |
w.Header().Set("Connection", "close") | |
} | |
w.WriteHeader(StatusBadRequest) | |
return | |
} | |
h, _ := mux.Handler(r) | |
h.ServeHTTP(w, r) | |
} |
(sh serverHandler) ServeHTTP
调用 (mux *ServeMux) ServeHTTP
, ServeHTTP将请求发送给处理程序 h, _ := mux.Handler(r)
源码看到这里,对于net/http标准库 对于注册路由,监听服务端地址和端口的流程,大致清楚了吧
整个过程,net/http
基本上是提供了 HTTP流程的整套服务,可以说是非常的香了, 整个过程基本上是这个样子的
- net.Listen 做了初始化 套接字 socket,bind 绑定ip 和端口,listen 设置最大监听数量的 操作
- Accept 进行阻塞等待客户端的连接
- go c.serve(ctx) 启动新的协程来处理当前的请求. 同时主协程继续等待其他客户端的连接, 进行高并发操作
- mux.Handler获取注册的路由, 然后拿到这个路由的handler 处理器, 处理客户端的请求后,返回给客户端结果
关于底层是如何封包解包,字节是如何偏移的,ipv4,ipv6如何去处理的,有兴趣的朋友们可以顺着代码继续追,欢迎多多沟通交流
好了,本次就到这里,下一次是 gin的路由算法分享,
技术是开放的,我们的心态,更应是开放的。拥抱变化,向阳而生,努力向前行。
我是小魔童哪吒,欢迎点赞关注收藏,下次见~