参考文档
基于 net/http web 服务端 示例
package main
import(
"fmt"
"log"
"net/http"
)
func main() {
http.HandleFunc("/", func1)
http.HandleFunc("/hello", func2)
log.Fatal(http.ListenAndServe(":6665", nil))
}
func func1(w http.ResponseWriter, req *http.Request){
fmt.Fprintf(w, "URL.Path = %q\n", req.URL.Path)
}
func func2(w http.ResponseWriter, req *http.Request) {
for k, v := range req.Header {
fmt.Fprintf(w, "Header[%q] = %q\n", k, v)
}
}
这段代码调用 http.HandleFunc 进行了路由注册,再调用ListenAndServe进行监听,运行结果如下
[root@localhost ~]# curl http://127.0.0.1:6665/
URL.Path = "/"
[root@localhost ~]#
关键过程分析
路由注册
几个关键的数据结构,和变量
//保存服务端路由映射
type ServeMux struct {
mu sync.RWMutex
m map[string]muxEntry
hosts bool // whether any patterns contain hostnames
}
//记录一条路由映射
type muxEntry struct {
h Handler
pattern string
}
// http 内置默认路由.
var DefaultServeMux = &defaultServeMux
var defaultServeMux ServeMux
//函数类型,我们示例代码中, 传入的函数变量,就是该数据类型
type HandlerFunc func(ResponseWriter, *Request)
//注意HandlerFunc实现了ServeHTTP方法
func (f HandlerFunc) ServeHTTP(w ResponseWriter, r *Request) {
f(w, r)
}
//接口类型,实现ServeHTTP函数, 这是实际处理客户端请求的函数,非常重要
type Handler interface {
ServeHTTP(ResponseWriter, *Request)
}
api调用跟踪
http.HandleFunc("/", func1)
func HandleFunc(pattern string, handler func(ResponseWriter, *Request)) {
DefaultServeMux.HandleFunc(pattern, handler)
}
func (mux *ServeMux) HandleFunc(pattern string, handler func(ResponseWriter, *Request)) {
mux.Handle(pattern, HandlerFunc(handler)) //将变量,转化成 type HandlerFunc
}
func (mux *ServeMux) Handle(pattern string, handler Handler) {
mux.mu.Lock()
defer mux.mu.Unlock()
//此处省略很多代码
mux.m[pattern] = muxEntry{h: handler, pattern: pattern} //建立url 与 处理函数的映射
//此处省略很多代码
}
自此,路由注册完成, 路由信息,应该会全部注册到 DefaultServeMux 变量。
下面我们调试一下,确认我们的分析。
[root@localhost httpserver]# dlv debug main.go --check-go-version=false
Type 'help' for list of commands.
(dlv) b main.go:12
Breakpoint 1 set at 0x6e6e59 for main.main() ./main.go:12
(dlv) c
> main.main() ./main.go:12 (hits goroutine(1):1 total:1) (PC: 0x6e6e59)
7: )
8:
9: func main() {
10: http.HandleFunc("/", func1)
11: http.HandleFunc("/hello", func2)
=> 12: log.Fatal(http.ListenAndServe(":6665", nil))
13: }
14:
15: func func1(w http.ResponseWriter, req *http.Request){
16: fmt.Fprintf(w, "URL.Path = %q\n", req.URL.Path)
17: }
(dlv) p http.DefaultServeMux
*net/http.ServeMux {
mu: sync.RWMutex {
w: (*sync.Mutex)(0x8eba60),
writerSem: 0,
readerSem: 0,
readerCount: 0,
readerWait: 0,},
m: map[string]net/http.muxEntry [
"/": (*"net/http.muxEntry")(0xc420012a48),
"/hello": (*"net/http.muxEntry")(0xc420012a68),
],
hosts: false,}
(dlv) q
[root@localhost httpserver]#
疑问点
传入的函数变量,为什么不是直接和url映射, 而是经过 2次 转换,第一次转换为HandlerFunc, 第二次转换为 handler,即 转换为一个接口类型。
因为我们的例子中,是传入一个函数变量,单单传一个函数过去, 能做的事情太少了。实际应用会复杂一些。如果有看过相关web框架源码就知道,一般不会使用默认的ServeMux。
如果传一个结构体过去,可以实现更复杂的功能,当结构体作为一个合法参数。这种方式适用于逻辑较为复杂的场景,如果对数据库的操作需要很多信息,地址、用户名、密码,还有很多中间状态需要保持,比如超时、重连、加锁等等。这种情况下,更适合封装为一个结构体作为参数。
上述也称为接口函数,既能够将普通的函数类型(需类型转换)作为参数,也可以将结构体作为参数,使用更为灵活,可读性也更好,这就是接口型函数的价值。
也可以参考下这篇博文 https://geektutu.com/post/7days-golang-q1.html
服务端响应分析
使用很广泛的apache使用的是多进程模型, apache会维护一个进程池, 新的请求到来,就取一个进程去处理,或者根据策略创建一个新的子进程。
golang 使用的当然就是 go 协程了, 分析一下它是怎么响应请求到。
几个关键数据结构
// A Server defines parameters for running an HTTP server.
// The zero value for Server is a valid configuration.
type Server struct {
Addr string // TCP address to listen on, ":http" if empty
Handler Handler // handler to invoke, http.DefaultServeMux if nil
TLSConfig *tls.Config
ReadTimeout time.Duration
ReadHeaderTimeout time.Duration
WriteTimeout time.Duration
IdleTimeout time.Duration
MaxHeaderBytes int
TLSNextProto map[string]func(*Server, *tls.Conn, Handler)
ConnState func(net.Conn, ConnState)
ErrorLog *log.Logger
disableKeepAlives int32 // accessed atomically.
inShutdown int32 // accessed atomically (non-zero means we're in Shutdown)
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()
}
// A conn represents the server side of an HTTP connection.
type conn struct {
server *Server
cancelCtx context.CancelFunc
rwc net.Conn
remoteAddr string
tlsState *tls.ConnectionState
werr error
r *connReader
bufr *bufio.Reader
bufw *bufio.Writer
lastMethod string
curReq atomic.Value
curState atomic.Value
mu sync.Mutex
hijackedv bool
}
//就是封装的一个server 指针
type serverHandler struct {
srv *Server
}
先从示例代码的 http.ListenAndServe(":6665", nil) 开始分析“
记住我们传入的Handler 是nil
func ListenAndServe(addr string, handler Handler) error {
server := &Server{Addr: addr, Handler: handler}
return server.ListenAndServe()
}
func (srv *Server) ListenAndServe() error {
addr := srv.Addr
if addr == "" {
addr = ":http"
}
ln, err := net.Listen("tcp", addr)
if err != nil {
return err
}
return srv.Serve(tcpKeepAliveListener{ln.(*net.TCPListener)})
}
//监听到请求, 通过go 协程处理: go c.serve(ctx)
func (srv *Server) Serve(l net.Listener) error {
//此处省略很多代码
for {
rw, e := l.Accept()
//此处省略很多代码
c := srv.newConn(rw)
c.setState(c.rwc, StateNew) // before Serve can return
go c.serve(ctx)
}
}
这就是章节开头说的, apache是过进程模型, 而net/http 就是靠go 协程去并发处理请求。
这里又有一个疑问,我们用apache时, 内部代码异常了,即使没有catch, web服务也不会退出,因为多进程模型,子进程异常退出,并不会导致父进程退出,
那用net/http时, 假如在我们的示例代码中出现panic, web服务也是不会退出的(这里就不贴测试代码了) 那它时怎么是实现的, 我猜肯定时用到了 defer + recover
我们继续分析web服务处理请求的过程。
处理过程最终
go c.serve(ctx)
调用newConn返回的是什么呢
func (srv *Server) newConn(rwc net.Conn) *conn {
c := &conn{
server: srv,
rwc: rwc,
}
if debugServerConnections {
c.rwc = newLoggingConn("server", c.rwc)
}
return c
}
c.server的具体实现中就使用了 defer + revocer, 如下:
// Serve a new connection.
func (c *conn) serve(ctx context.Context) {
..........................................
defer func() {
if err := recover(); err != nil && err != ErrAbortHandler {
const size = 64 << 10
buf := make([]byte, size)
buf = buf[:runtime.Stack(buf, false)]
c.server.logf("http: panic serving %v: %v\n%s", c.remoteAddr, err, buf)
}
if !c.hijacked() {
c.close()
c.setState(c.rwc, StateClosed)
}
}()
//此处省略了很多代码
//留意注意这里的for循环, 长连接时, 这个go协程就不会退出
//当时最终调用的处理函数是: serverHandler{c.server}.ServeHTTP(w, w.req)
for {
w, err := c.readRequest(ctx)
//此处省略了很多代码
// 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.
serverHandler{c.server}.ServeHTTP(w, w.req)
//此处省略了很多代码
if !w.conn.server.doKeepAlives() {
// We're in shutdown mode. We might've replied
// to the user without "Connection: close" and
// they might think they can send another
// request, but such is life with HTTP/1.1.
return
}
//此处省略了很多代码
}
}
继续跟踪 serverHandler{c.server}.ServeHTTP(w, w.req)
func (sh serverHandler) ServeHTTP(rw ResponseWriter, req *Request) {
handler := sh.srv.Handler
if handler == nil { //示例代码。我们传入的就是nil
handler = DefaultServeMux
}
if req.RequestURI == "*" && req.Method == "OPTIONS" {
handler = globalOptionsHandler{}
}
handler.ServeHTTP(rw, req)
}
DefaultServeMux的类型是 ServeMux, 继续跟踪
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) // 找到最终 Handler
h.ServeHTTP(w, r) // 调用ServeHTTP方法
}
怎么找到Handler的呢
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()
defer mux.mu.RUnlock()
//此处省略了很多代码
h, pattern = mux.match(path)
//此处省略了很多代码
}
// Find a handler on a handler map given a path string.
// Most-specific (longest) pattern wins.
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
}
//此处省略了很多代码
}
还记得最开始的那个数据结构吗
type Handler interface {
ServeHTTP(ResponseWriter, *Request)
}
我们注册是,传入的函数变量经过2次转换,转换成Handler 类型,其ServeHTTP方法就是其函数变量 自身。
其实, 一些web框架, 就是传入的 handler 是我们自己定义的而,不是传nil(最终会使用DefaultServeMux)
http.ListenAndServe(":6665", nil)
|
|
——————
func ListenAndServe(addr string, handler Handler) error {
server := &Server{Addr: addr, Handler: handler}
return server.ListenAndServe()
}
|
|
——————
handler := sh.srv.Handler
if handler == nil {
handler = DefaultServeMux
}
至此,路由注册, 根据url找到对应api, 并执行, 这一些列过程, 已经分析完成。
整个分析过程,我们使用的是net/http的默认路由,api追踪过程,也是做了很多理想情况的假设
我们分析了 net/http 默认路由中注册过程, 服务端如何实现并发处理请求,以及如何进行异常恢复,以及长连接保持
后续还会对httpclient, 长连接, context的使用做分析