多讀go的源碼,可以加深對go語言的理解和認知,今天分享一下http相關的源碼部分
在不使用第三方庫的情況下,我們可以很容易的的用go實現一個http服務,
package main
import (
"fmt"
"net/http"
)
func IndexHandler(w http.ResponseWriter, r *http.Request) {
fmt.Fprintln(w, "hello world ! ")
}
func main() {
http.HandleFunc("/", IndexHandler)
if err := http.ListenAndServe(":9100", nil); err != nil {
panic(err)
}
}
直接在瀏覽器裏訪問9100端口就可以返回 hello world !
go已經把所有的細節封裝好了,我們只需要自己去寫Handler實現就夠了。源碼簡單來說做了以下幾件事:
- 把我們自定義的Handler方法添加到默認路由
DefaultServeMux
的Map裏比如:http.HandleFunc("/", IndexHandler)
(btw: go語言的map是非線程安全的,可以在http源碼裏看到官方的處理方式); - 啓動一個tcp服務監聽9100端口,等待http調用;
- 當監聽到有http調用時,啓動一個協程來處理這個請求,這個是go的http服務快的一個重要原因,把請求內容轉換成http.Request, 把當前連接封裝http.RespnseWriter;
- 默認路由
DefaultServeMux
根據request的path找到相應的Handler,把 request和 responseWriter傳給Handler 進行業務邏輯處理,response響應信息write給客戶端;
ServeMux & Handler
http 包的默認路由 DefaultServeMux
是 ServeMux
結構休的實例http.HandleFunc("/", IndexHandler)
的調用,會把path信息和自定義的方法信息保存到 DefaultServeMux
的 m map[string]muxEntry
變量裏
我們看一下ServeMux
的定義:
type ServeMux struct {
mu sync.RWMutex
m map[string]muxEntry
es []muxEntry // slice of entries sorted from longest to shortest.
hosts bool // whether any patterns contain hostnames
}
type muxEntry struct {
h Handler
pattern string
}
ServeMux
中保存了path
和Handler
的對應關係,也是路由關係。
Handler
muxEntry
中的 h Handler
對就的就是我們自定義的Handler方法比如,我們自己例子中的方法 func IndexHandler(w http.ResponseWriter, r *http.Request)
細心的同學可能會問 Handler是一個接口,但是我們只是定義了一個方法,這是怎麼轉換的呢?
接口Halder設置了簽名規則,也就是我們自定義的處理方法
type Handler interface {
ServeHTTP(ResponseWriter, *Request)
}
go語言中所有的自定義類型都可以實現自己的方法,http包是用一個自定義的func來去實現了Handler接口,
type HandlerFunc func(ResponseWriter, *Request)
// ServeHTTP calls f(w, r).
func (f HandlerFunc) ServeHTTP(w ResponseWriter, r *Request) {
f(w, r)
}
然後在ServerMux
的方法HandleFunc
處理的時候會把 handler func(ResponseWriter, *Request)
轉換成 HandlerFunc
, 如下所示:
// 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))
}
ServerMux
結構中還有一個讀寫鎖 mu sync.RWMutex
mu就是用來處理多線程下map的安全訪問的。
查找&調用 Handler
得到自定義的handler方法,就是去map中根據path匹配得到Handler
func (mux *ServeMux) handler(host, path string) (h Handler, pattern string) {
mux.mu.RLock()
defer mux.mu.RUnlock()
// Host-specific pattern takes precedence over generic ones
if mux.hosts {
h, pattern = mux.match(host + path)
}
if h == nil {
h, pattern = mux.match(path)
}
if h == nil {
h, pattern = NotFoundHandler(), ""
}
return
}
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
}
// Check for longest valid match. mux.es contains all patterns
// that end in / sorted from longest to shortest.
for _, e := range mux.es {
if strings.HasPrefix(path, e.pattern) {
return e.h, e.pattern
}
}
return nil, ""
}
ServeMux
實現了 Handler
接口,也是默認的路由調用的具體規則實現的地方,他的 ServeHTTP
方法處理方式就是得到自定義的handler方法,並調用我們自定義的方法:
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)
}
接口Halder設置了簽名規則,也就是我們自定義的處理方法
比如下面的代碼,函數IndexHandler就是我們自定義的方法,返回給客戶端請求一個 hello world !
字符串。中間請求是如何調用到我們自定義的方法的具體邏輯都是http包提供的,但是一點也不神祕,
http.HandleFunc("/", IndexHandler)
// IndexHandler 我們自己定義的Handler方法
func IndexHandler(w http.ResponseWriter, r *http.Request) {
fmt.Fprintln(w, "hello world ! ")
}
type Handler interface {
ServeHTTP(ResponseWriter, *Request)
}
//
type HandlerFunc func(ResponseWriter, *Request)
// ServeHTTP calls f(w, r).
func (f HandlerFunc) ServeHTTP(w ResponseWriter, r *Request) {
f(w, r)
}
http ListenAndServe
說完 ServeMux
是如何結合 Handler
接口,來實現路由和調用後,就要說一下,http服務是如何得到客戶端傳入的信息,封裝requet和rresponse的。
在啓動程序的時候http.ListenAndServe
, 有兩個參數,第一個是指寫端口號,第二個是處理邏輯,如果我們沒有給定處理邏輯,會使用默認的處理DefaultServeMux
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)
}
ListenAndServe
方法打開tcp端口進行監聽,然後把Listener
傳給srv.Serve
方法
func (srv *Server) ListenAndServe() error {
// 省略部分代碼 ...
ln, err := net.Listen("tcp", addr)
if err != nil {
return err
}
return srv.Serve(tcpKeepAliveListener{ln.(*net.TCPListener)})
}
具體要說一下 Service
方法,這個方法中,對監聽tcp請求,然後把請求的客戶端連接進行封裝,
func (srv *Server) Serve(l net.Listener) error {
// 省略部分代碼 ...
ctx := context.WithValue(baseCtx, ServerContextKey, srv)
for {
rw, e := l.Accept()
// 省略部分代碼 ...
tempDelay = 0
c := srv.newConn(rw)
c.setState(c.rwc, StateNew) // before Serve can return
go c.serve(ctx)
}
}
把客戶端的請求封裝成一個Conn,然後啓動一個協程go c.serve(ctx)來處理這個連接請求,這就是http包快的一個重要原因
,每一個連接就是一個協程。客戶端可以先和服務器進行連接,然後利用這個conn來多次發送http請求,這樣,就可以減少每次的進行連接而提高一些速度。像一些rpc裏就是利用這點去實現的雙向的stream流,比如我之前的帖子go微服務框架go-micro深度學習(五) stream 調用過程詳解,他就是建立一個tcp連接,然後基於這個conn,發送多個request,返回多次respose數據。
// 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())
// 省略部分代碼 ...
// 循環讀取請求 ...
for {
// 讀取請求數據,封裝response
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)
}
// 省略部分代碼 ...
// 路由調用自定義的方法,把封裝好的responseWrite和 request傳到方法內
serverHandler{c.server}.ServeHTTP(w, w.req)
w.cancelCtx()
if c.hijacked() {
return
}
w.finishRequest()
// 省略部分代碼 ...
}
}