net/http包的使用模式和原始碼解析

後開啟撒打發了發表於2017-10-20

目錄:

 
一、http包的3個關鍵型別:
Handler介面:所有請求的處理器、路由ServeMux都滿足該介面;
1
2
3
type Handler interface {
   ServeHTTP(ResponseWriter, *Request)
}
ServeMux結構體:HTTP請求的多路轉接器(路由),它負責將每一個接收到的請求的URL與一個註冊模式的列表進行匹配,並呼叫和URL最匹配的模式的處理器。它內部用一個map來儲存所有處理器Handler
  • http包有一個包級別變數DefaultServeMux,表示預設路由:var DefaultServeMux = NewServeMux(),使用包級別的http.Handle()、http.HandleFunc()方法註冊處理器時都是註冊到該路由中;
  • ServeMux結構體有ServeHTTP()方法(滿足Handler介面),主要用於間接呼叫它所儲存的處理器的ServeHTTP()方法
http.HandlerFunc函式型別:它滿足Handler介面
1
2
3
4
5
type HandlerFunc func(ResponseWriter, *Request)
//實現Handler介面的ServeHTTP方法
func (f HandlerFunc) ServeHTTP(w ResponseWriter, r *Request) {
    f(w, r) //呼叫自身
}

二、HTTP伺服器的使用模式:
處理函式:只要函式的簽名為 func(w http.ResponseWriter, r *http.Request) ,均可作為處理函式,即它可以被轉換為http.HandlerFunc函式型別;

模式一:使用預設的路由來註冊處理函式:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
var addr = flag.String("addr"":8080""http server address")
//1.不帶引數處理函式
func serveHome(w http.ResponseWriter, r *http.Request) {
   if r.URL.Path != "/" {
       http.NotFound(w, r)
       return
   }
   http.ServeFile(w, r, "home.html")
}
//2.帶引數處理函式,閉包函式隱式轉換為http.HandlerFunc函式型別
func myHandler(s string) http.HandlerFunc {
    return func(w http.ResponseWriter, r *http.Request) {
        if r.URL.Path != "/" {
            http.NotFound(w, r)
            return
        }
        http.ServeFile(w, r, s) //使用引數s
    }
}
func main() {
   flag.Parse()
     
   //向預設路由註冊處理器函式
   http.HandleFunc("/", serveHome) //或http.Handle("/", http.HandlerFunc(serveHome))
   http.Handle("/file",myHandler("somefile"))
     
   err := http.ListenAndServe(*addr, nil) //啟動監聽,第二個引數nil表示使用預設路由DefaultServeMux中註冊的處理器
   if err != nil {
      log.Fatalln("ListenAndServe: ", err)
   }
}
 
模式二:使用自定義的路由來註冊處理函式:
1
2
3
4
5
6
7
8
9
10
func main() {
  mux := http.NewServeMux() //新建一個自定義的路由
  mux.Handle("/file",myHandler("somefile"))
  mux.HandleFunc("/", serveHome)
     
  err := http.ListenAndServe(*addr,mux) //啟動監聽
   if err != nil {
      log.Fatalln("ListenAndServe: ", err)
   }
}
 
模式三:直接自定義一個Server例項:該模式可以很方便的管理服務端的行為
1
2
3
4
5
6
7
8
9
10
11
12
13
14
mux := http.NewServeMux()
mux.Handle("/file",myHandler("somefile"))
mux.HandleFunc("/", serveHome)
 
s := &http.Server{
   Addr: ":8080",
   Handler: mux, //指定路由或處理器,不指定時為nil,表示使用預設的路由DefaultServeMux
   ReadTimeout: 10 * time.Second,
   WriteTimeout: 10 * time.Second,
   MaxHeaderBytes: 1 << 20,
   ConnState: //指定連線conn的狀態改變時的處理函式
       //....
}
log.Fatal(s.ListenAndServe())

 

接下來,我們就跟蹤原始碼來仔細的分析下整個執行過程。


三、HTTP伺服器的執行過程:
1.使用http.ListenAndServe()方法啟動服務,它根據給定引數構造Server型別,然後呼叫server.ListenAndServe()
1
2
3
4
func ListenAndServe(addr string, handler Handler) error {
    server := &Server{Addr: addr, Handler: handler}
    return server.ListenAndServe()
}

  
2.而server.ListenAndServe()方法內部呼叫net.Listen("tcp", addr),該方法內部又呼叫net.ListenTCP()建立並返回一個監聽器net.Listener,如下的ln;

1
2
3
4
5
6
7
8
9
10
11
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)})
}
 
3.然後把監聽器 ln 斷言轉換為 TCPListener 型別,並根據它構造一個 tcpKeepAliveListener 物件並傳遞給server.Serve()方法;
  • 因為TCPListener實現了Listener介面,所以tcpKeepAliveListener也實現了Listener介面,並且它重寫了Accept()方法,目的是為了呼叫SetKeepAlive(true),讓作業系統為收到的每一個連線啟動傳送keepalive訊息(心跳,為了保持連線不斷開)。
1
2
3
4
5
6
7
8
9
10
11
12
type tcpKeepAliveListener struct {
    *net.TCPListener
}
func (ln tcpKeepAliveListener) Accept() (c net.Conn, err error) {
    tc, err := ln.AcceptTCP()
    if err != nil {
        return
    }
    tc.SetKeepAlive(true) //傳送心跳
    tc.SetKeepAlivePeriod(3 * time.Minute) //傳送週期
    return tc, nil
}
 
4.server.Serve()方法呼叫tcpKeepAliveListener 物件的 Accept() 方法返回一個連線conn(該連線啟動了心跳),併為每一個conn建立一個新的go程執行conn.server()方法:具體見程式碼中我加的註釋說明
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
func (srv *Server) Serve(l net.Listener) error {
    defer l.Close()
    if fn := testHookServerServe; fn != nil {
        fn(srv, l)
    }
    var tempDelay time.Duration //重試間隔
 
    if err := srv.setupHTTP2_Serve(); err != nil {
        return err
    }
 
    srv.trackListener(l, true) //快取該監聽器
    defer srv.trackListener(l, false) //從快取中刪除當前監聽器
 
    baseCtx := context.Background()
    ctx := context.WithValue(baseCtx, ServerContextKey, srv) //新建一個context用來管理每個連線conn的Go程
    for {
        rw, e := l.Accept() //呼叫tcpKeepAliveListener物件的 Accept() 方法
        if e != nil {
            select {
            case <-srv.getDoneChan():
                return ErrServerClosed //退出Serve方法,並執行延遲呼叫(從快取中刪除當前監聽器)
            default:
            }
            //如果發生了net.Error錯誤,則隔一段時間就重試一次,間隔時間每次翻倍,最大為1秒
            if ne, ok := e.(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", e, tempDelay)
                time.Sleep(tempDelay)
                continue
            }
            return e
        }
        tempDelay = 0
        c := srv.newConn(rw) //該方法根據net.Conn、srv構造了一個新的http.conn型別
        c.setState(c.rwc, StateNew) //快取該連線的狀態,如果方法:Server.ConnState(net.Conn, ConnState)不為nil,就根據當前連線的狀態執行它
        go c.serve(ctx)
    }
}

  

5.而conn.server()方法會讀取請求,然後根據conn內儲存的server來構造一個serverHandler型別,並呼叫它的ServeHTTP()方法:serverHandler{c.server}.ServeHTTP(w, w.req),該方法的原始碼如下:
 
1
2
3
4
5
6
7
8
9
10
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)
}


6.如上原始碼可以看到,當 handler == nil 時使用預設的DefaultServeMux路由,否則使用在第1步中為Serve指定了的Handler;然後呼叫該Handler的ServeHTTP方法(該Handler一般被設定為路由ServeMux型別);

 
7.而路由ServeMux的ServeHTTP方法則會根據當前請求提供的資訊來查詢最匹配的Handler(這裡為):
1
2
3
4
5
6
7
8
9
10
11
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)
}

  

8.以上查詢到的Handler介面值h就是我們事先註冊到路由中與請求匹配的Handler;而h的動態型別是HandlerFunc型別(它也滿足Handler介面);
所以,以上 h.ServeHTTP(w, r) 實際上呼叫的是介面值h中持有的動態值(也就是我們定義的處理函式)
1
2
3
4
5
type HandlerFunc func(ResponseWriter, *Request)
//實現Handler介面的ServeHTTP方法
func (f HandlerFunc) ServeHTTP(w ResponseWriter, r *Request) {
    f(w, r) //呼叫自身
}
 
至此,整個呼叫過程講解完畢,至於業務層的處理邏輯,則由各個處理函式實現

四、重定向:
http包自帶了幾個建立常用處理器的函式:FileServer,NotFoundHandler、RedirectHandler、StripPrefix、TimeoutHandler。
而RedirectHandler函式就是用來重定向的:它返回一個請求處理器,該處理器會對每個請求都使用狀態碼code重定向到網址url
1
2
3
4
5
6
7
8
func main() {
  mux := http.NewServeMux()
  mux.Handle("/to",http.RedirectHandler("http://example.org", 307))
  err := http.ListenAndServe(*addr,mux) //啟動監聽
   if err != nil {
      log.Fatalln("ListenAndServe: ", err)
   }
}
 
好了,本文就暫時講關於http包關於HTTP服務端方面的東西,至於客戶端方面的就簡單引用一下官方文件說明吧,畢竟客戶端很少用Go實現。
 
五、客戶端的實現:

Get、Head、Post和PostForm函式發出HTTP/ HTTPS請求。

1
2
3
4
5
6
resp, err := http.Get("http://example.com/")
...
resp, err := http.Post("http://example.com/upload""image/jpeg", &buf)
...
resp, err := http.PostForm("http://example.com/form",
    url.Values{"key": {"Value"}, "id": {"123"}})

  

程式在使用完回覆後必須關閉回覆的主體。

1
2
3
4
5
6
7
resp, err := http.Get("http://example.com/")
if err != nil {
    // handle error
}
defer resp.Body.Close()
body, err := ioutil.ReadAll(resp.Body)
// ...

  

要管理HTTP客戶端的頭域、重定向策略和其他設定,建立一個Client:


1
2
3
4
5
6
7
8
9
10
client := &http.Client{
    CheckRedirect: redirectPolicyFunc,
}
resp, err := client.Get("http://example.com")
// ...
req, err := http.NewRequest("GET""http://example.com", nil)
// ...
req.Header.Add("If-None-Match", `W/"wyzzy"`)
resp, err := client.Do(req)
// ...

要管理代理、TLS配置、keep-alive、壓縮和其他設定,建立一個Transport:


1
2
3
4
5
6
tr := &http.Transport{
    TLSClientConfig:    &tls.Config{RootCAs: pool},
    DisableCompression: true,
}
client := &http.Client{Transport: tr}
resp, err := client.Get("https://example.com")

Client和Transport型別都可以安全的被多個go程同時使用。出於效率考慮,應該一次建立、儘量重用。

相關文章