go语言调用http协议,相信都用过,那它底层是如何实现的呢?今天我们来扒一扒。
如上,交互框架有客户端和服务端组成,Client发送request,Server处理后回复response,那底层是什么呢?
使用方式
按照惯例,都是先来一个简单的例子,然后由浅入深进行分析,我们来看看go语言启动一个http服务,来实现上图中的例子
- server端代码
func main() {
http.HandleFunc("/ping", func(writer http.ResponseWriter, request *http.Request) {
writer.Write([]byte("pong"))
})
http.ListenAndServe(":8081", nil)
}
- Client代码
func main() {
resp, err := http.Get("http://127.0.0.1:8081/ping")
if err != nil {
log.Fatal(err)
}
defer resp.Body.Close()
body, _ := io.ReadAll(resp.Body)
fmt.Println(string(body))
}
上述例子,就是Client请求一个ping,server回复一个pone。很简单的一个请求,服务端主要完成了两件事:
- 调用HandleFun的方法,注册了请求路径/ping,然后通过ping后进入到方法,再通过write写回到客户端
- 调用ListenAndServe,启动一个8081端口的http服务。这是一个常驻的进程。
客户端也主要是完成了两件事 - 通过get方法去调用具体的路径,发起http请求
- 读取response回来的数据流信息,然后解析
golang语言其实比较适合web服务的开发,比对于处理并发逻辑有很大优势,天然支持goroutine,而且你看,搭建一个http只需要寥寥几行代码即可。
我们通过进行服务端和客户端两个源码进行分析。
服务端
数据结构
type Server struct {
// Addr optionally specifies the TCP address for the server to listen on,
// in the form "host:port". If empty, ":http" (port 80) is used.
// The service names are defined in RFC 6335 and assigned by IANA.
// See net.Dial for details of the address format.
Addr string
Handler Handler // handler to invoke, http.DefaultServeMux if nil
// TLSConfig optionally provides a TLS configuration for use
// by ServeTLS and ListenAndServeTLS. Note that this value is
// cloned by ServeTLS and ListenAndServeTLS, so it's not
// possible to modify the configuration with methods like
// tls.Config.SetSessionTicketKeys. To use
// SetSessionTicketKeys, use Server.Serve with a TLS Listener
// instead.
TLSConfig *tls.Config
// ReadTimeout is the maximum duration for reading the entire
// request, including the body. A zero or negative value means
// there will be no timeout.
//
// Because ReadTimeout does not let Handlers make per-request
// decisions on each request body's acceptable deadline or
// upload rate, most users will prefer to use
// ReadHeaderTimeout. It is valid to use them both.
ReadTimeout time.Duration
// ReadHeaderTimeout is the amount of time allowed to read
// request headers. The connection's read deadline is reset
// after reading the headers and the Handler can decide what
// is considered too slow for the body. If ReadHeaderTimeout
// is zero, the value of ReadTimeout is used. If both are
// zero, there is no timeout.
ReadHeaderTimeout time.Duration
// WriteTimeout is the maximum duration before timing out
// writes of the response. It is reset whenever a new
// request's header is read. Like ReadTimeout, it does not
// let Handlers make decisions on a per-request basis.
// A zero or negative value means there will be no timeout.
WriteTimeout time.Duration
// IdleTimeout is the maximum amount of time to wait for the
// next request when keep-alives are enabled. If IdleTimeout
// is zero, the value of ReadTimeout is used. If both are
// zero, there is no timeout.
IdleTimeout time.Duration
// MaxHeaderBytes controls the maximum number of bytes the
// server will read parsing the request header's keys and
// values, including the request line. It does not limit the
// size of the request body.
// If zero, DefaultMaxHeaderBytes is used.
MaxHeaderBytes int
// TLSNextProto optionally specifies a function to take over
// ownership of the provided TLS connection when an ALPN
// protocol upgrade has occurred. The map key is the protocol
// name negotiated. The Handler argument should be used to
// handle HTTP requests and will initialize the Request's TLS
// and RemoteAddr if not already set. The connection is
// automatically closed when the function returns.
// If TLSNextProto is not nil, HTTP/2 support is not enabled
// automatically.
TLSNextProto map[string]func(*Server, *tls.Conn, Handler)
// ConnState specifies an optional callback function that is
// called when a client connection changes state. See the
// ConnState type and associated constants for details.
ConnState func(net.Conn, ConnState)
// ErrorLog specifies an optional logger for errors accepting
// connections, unexpected behavior from handlers, and
// underlying FileSystem errors.
// If nil, logging is done via the log package's standard logger.
ErrorLog *log.Logger
// BaseContext optionally specifies a function that returns
// the base context for incoming requests on this server.
// The provided Listener is the specific Listener that's
// about to start accepting requests.
// If BaseContext is nil, the default is context.Background().
// If non-nil, it must return a non-nil context.
BaseContext func(net.Listener) context.Context
// ConnContext optionally specifies a function that modifies
// the context used for a new connection c. The provided ctx
// is derived from the base context and has a ServerContextKey
// value.
ConnContext func(ctx context.Context, c net.Conn) context.Context
inShutdown atomicBool // true when server is in shutdown
disableKeepAlives int32 // accessed atomically.
nextProtoOnce sync.Once // guards setupHTTP2_* init
nextProtoErr error // result of http2.ConfigureServer if used
mu sync.Mutex
listeners map[*net.Listener]struct{}
activeConn map[*conn]struct{}
doneChan chan struct{}
onShutdown []func()
}
当然,我们不会看那么多,不会太过于细节,支持查看我们可以自定义服务器的监听地址、处理程序、超时设置和其他等等行为,满足我们日常开发行为,如:
- Addr string: 服务端的地址,表示服务器要监听的网络地址和端口号,如 host:port
- Handler Handler: 处理HTTP请求的接口,通常是一个http.handler接口的实现,用于处理请求,决定服务器如何处理传入的http请求
- ReadTimeout:表示服务器在读取请求头和请求体数据时的超时时间,如果在指定时间内没有完成请求的话,服务器将关闭连接。防止阻塞。
- WriteTimeOut:服务器在写入响应数据时的超时时间,如果在指定时间内没有完成响应的写入,关闭连接。
- MaxHeaderBytes:字段定义了请求头的最大字节数。
- ErrorLog:字段是一个可选的日志记录器,用于记录服务器内部错误。
- TLSConfig:字段允许您指定用于启用TLS(Transport Layer Security)的配置。如果不使用TLS,可以将其设置为nil。
由上述例子可看,我们用到的方法,其实就两个,一个是HandleFunc,一个是ListenAndServe,大道至简,我们从分析这两个,应该可以把整个server的框架分析出来。话不多说,开干。
HandleFunc
老规矩,先上源码片段,该源码片段,采取了截取、组装到同一个文件,方便查看,关键地方都给了注释:
var DefaultServeMux = &defaultServeMux // 这个是单例ServeMux,如果直接通过HandleFunc注入handle的话,会直接注册到DefaultSerMux中来
var defaultServeMux ServeMux
func HandleFunc(pattern string, handler func(ResponseWriter, *Request)) {
DefaultServeMux.HandleFunc(pattern, handler)
}
type muxEntry struct {
h Handler
pattern string
}
type ServeMux struct {
mu sync.RWMutex // 并发下的读写锁,主要是针对map的
m map[string]muxEntry // 这里就定义了key:路径和value:处理当前路径的Handle,这里的Handle是一个接口,主要是ServeHTTP方法,下面介绍
es []muxEntry // 这个是用户路径匹配的,如果你路径的最后一个字符位/的话,后续在map中找不到就跑到es中来找。
hosts bool // whether any patterns contain hostnames
}
func (mux *ServeMux) HandleFunc(pattern string, handler func(ResponseWriter, *Request)) {
if handler == nil {
panic("http: nil handler")
}
mux.Handle(pattern, HandlerFunc(handler))
}
type Handler interface {
ServeHTTP(ResponseWriter, *Request) // 根据http的请求路径对应到处理函数,对请求进行处理和响应的
}
// 这里,才是重点,就是把所有的请求和请求处理的Handle,都放到map中,方便匹配
func (mux *ServeMux) Handle(pattern string, handler Handler) {
mux.mu.Lock() // 并发下,给了读写锁
defer mux.mu.Unlock()
...
if mux.m == nil {
mux.m = make(map[string]muxEntry)
}
e := muxEntry{h: handler, pattern: pattern} // 这里把handler和路径参数,都封装到了一个结构体中,但是map的key又是参数,这里有点看不懂,总感觉有点冗余
mux.m[pattern] = e // 路径、Handle在这里对应上了
if pattern[len(pattern)-1] == '/' {
mux.es = appendSorted(mux.es, e) // 这里就是处理了短路匹配逻辑
}
...
}
以上代码就是关键性的,总而言之,就是通过路由,把参数pattern和处理的函数handler绑定起来。如下图:(来源于:小徐先生的编程世界)
ListenAndServe
这个是启动监听端口,一般来说我们不会自定义handler。
上关键源码,还是从上到下把一些关键函数摘出来了
// 一般来说我们都不会自定义handler,所以都会置为nil
func ListenAndServe(addr string, handler Handler) error {
server := &Server{Addr: addr, Handler: handler} // 声明一个新的server对象
return server.ListenAndServe() // 嵌套再次执行ListenAndServe
}
func (srv *Server) ListenAndServe() error {
...
addr := srv.Addr
if addr == "" {
addr = ":http" // 处理端口、参数情况
}
ln, err := net.Listen("tcp", addr) // 申请到一个监听器,net下面的监听器,本文不涉及如何申请监听器,后续有时间整理
if err != nil {
return err
}
return srv.Serve(ln) //把监听的信息,丢到当前server下面Serve方法,核心。
}
// 这个方法就很核心了,提现了服务器端运行架构,for+listener.accept模式,监听Accept,使用了epoll多路复用机制。
func (srv *Server) Serve(l net.Listener) error {
...
ctx := context.WithValue(baseCtx, ServerContextKey, srv) // 创建了一个新的Context,是一个valueCtx,并且把当前的请求server封装进去。
for {
rw, err := l.Accept() // 使用管理epoll多路复用机制,也就是如果请求到值了,就处理,请求不到不会阻塞,等到有值了会自动返回调用对应的逻辑处理。
...
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) // 如果有一个连接到达,创建一个 goroutine 异步执行 conn.serve 方法负责处理
}
}
// 响应客户端连接的核心方法:
func (c *conn) serve(ctx context.Context) {
c.remoteAddr = c.rwc.RemoteAddr().String() // 读取到请求对应的地址
ctx = context.WithValue(ctx, LocalAddrContextKey, c.rwc.LocalAddr())
...
for {
w, err := c.readRequest(ctx)
if c.r.remain != c.server.initialReadLimitSize() {
// If we read any bytes off the wire, we're active.
c.setState(c.rwc, StateActive, runHooks)
}
...
serverHandler{c.server}.ServeHTTP(w, w.req) // 请求的 path 为其分配 handler
...
}
}
type serverHandler struct {
srv *Server
}
func (sh serverHandler) ServeHTTP(rw ResponseWriter, req *Request) {
handler := sh.srv.Handler
if handler == nil {
handler = DefaultServeMux // 会对 Handler 作判断,倘若其未声明,则取全局单例 DefaultServeMux 进行路由匹配,呼应了 http.HandleFunc 中的处理细节.
}
...
handler.ServeHTTP(rw, req) // 进行当前请求地址的一个请求,然后处理Handle
}
// 转了一圈,终于转到了serveMux中来了,这个ServeHTTP我们上面也说过,就是根据http的请求路径对应到处理函数,对请求进行处理和响应的
// 但是这个方法是一个实现,主要是做两件事情,第一,找到对应的hander,第二,处理和响应
func (mux *ServeMux) ServeHTTP(w ResponseWriter, r *Request) {
...
h, _ := mux.Handler(r) // 这里就是找到对应的h,根据传入的请求结构体中的路径,见如下:
h.ServeHTTP(w, r) // 找到了对应的hander,调用处理对应的事件,返回给客户端。
}
func (mux *ServeMux) Handler(r *Request) (h Handler, pattern string) {
...
return mux.handler(host, r.URL.Path) // 这里再封装了一层前面...是一些判断过程。
}
func (mux *ServeMux) handler(host, path string) (h Handler, pattern string) {
mux.mu.RLock() // 这里很巧妙,使用了读锁,因为一般服务启动了,就会再注册handler,所以只是用到了读锁。
defer mux.mu.RUnlock()
if mux.hosts {
h, pattern = mux.match(host + path)
}
if h == nil {
h, pattern = mux.match(path)
}
if h == nil {
h, pattern = NotFoundHandler(), ""
}
return
}
// 这里,就是根据传入的地址,找到匹配的hander,如果没有,则去判断路径前缀中的es里面有没有
func (mux *ServeMux) match(path string) (h Handler, pattern string) {
v, ok := mux.m[path]
if ok {
return v.h, v.pattern // 这里就是去map中找,也就是我前面说的,为什么不直接使用key:路径,value:hander,而是要封装一个结构体做路径和hander。
}
// ServeMux.es 本身是按照 pattern 的长度由大到小排列的
for _, e := range mux.es {
if strings.HasPrefix(path, e.pattern) {
return e.h, e.pattern
}
}
return nil, ""
}
上述就是整个服务端监听的核心代码片段过程,以下流程图更为直观的展现了整个过程: