net/http - 學習
Introduction
- 什麼是函式登記中心 跳轉
- 登記中心裡的處理函式是什麼 跳轉
- 登記中心裡的處理工具是什麼 跳轉
- 登記中心裡的內部結構是什麼樣的 跳轉
- 向登記中心登記處理函式 跳轉
- 服務中心,生成服務物件 跳轉
- 服務中心,生成監聽器 跳轉
- 服務中心,迴圈收集請求 跳轉
- 取出服務中心的處理工具→登記中心 跳轉
- 登記中心找出匹配的處理函式,處理請求 跳轉
go語言大部分時候,作為後端出現. 那麼它的最基本行為就是關於"http請求"的傳送與接收. 用久了難免會想知道這裡面到底是怎麼工作的,怎麼它就能接收請求了,怎麼就能傳送請求了.
下面這個片段是一個簡單的小伺服器,我們從下面這個簡單的小函式開始,描述一下在這其中裡面到底都發生了什麼.
func handleRequest(w http.ResponseWriter,r *http.Request) {
// ..
}
func main() {
http.HandleFunc("/",handleRequest)
_ = http.ListenAndServe(":8099",nil)
}
複製程式碼
函式登記中心是什麼 - ServerMux
- Server → 伺服器
- Mux → multi-plexer,多路選擇器
二者組合在一起,表示一個伺服器,這個伺服器具有多路選擇的功能,能根據不同的情況做出不同的處理 → http請求中連結的路徑不同做出不同的反應.
我是這樣理解的,這個選擇器是一個登記中心,執行http.HandleFunc("<path>",func(ResponseWriter,*Request))
的時候其實就是往這個登記中心裡註冊一下這個函式,稍後服務中心開始工作的時候會拿著登記冊裡的註冊資訊,根據路徑找到處理函式,來做出反應.
登記中心裡的處理函式是什麼 - HandlerFunc
type HandlerFunc func(ResponseWriter,*Request)
複製程式碼
我們都知道,通過呼叫第一行的函式,我們可以將一個函式註冊進去,我們要求這個函式可以接收請求並寫一個返回值,做一個能做響應的函式. 只有這樣的函式才能往登記中心裡註冊
登記中心裡的處理工具是什麼 - Handler
type Handler interface {
ServeHTTP(ResponseWriter,*Request)
}
func (mux *ServeMux) ServeHTTP(w,r) {}
func (f HandlerFunc) ServeHTTP(w,r) {
f(w,r)
}
複製程式碼
處理工具的本質是一個用於處理HTTP請求的工具,在後面,我們會給出我們在何時呼叫處理工具來處理HTTP請求,我們對於這個工具唯一的期望,就是希望這個"處理工具"能夠實現ServeHTTP函式,做到
- 解析
r *Request
請求,找到處理函式 - 寫入
w Response
,做出反饋
ok,回到上面
- ServerMux,登記中心實現了這個函式,所以登記中心是處理工具
- HandlerFunc,實現了這個函式,所以HandlerFunc是處理工具
- 並且,處理函式實現它的方法是直接執行這個函式
事實上,在服務中心接收到請求的時候,我們直接找的"處理工具",而不是"登記中心",只是恰巧,在這個例子中. 我們用的是"登記中心"
登記中心裡的內部結構
type ServeMux struct {
mu sync.RWMutex
m map[string]muxEntry
es []muxEntry
hosts bool
}
type muxEntry struct {
h Handler
pattern string
}
複製程式碼
- muxEntry 登記冊中的一個基本單元,h是處理函式,pattern是模式,也就是對應路徑 → 一個單元裡記錄下一條路徑對應的處理函式
- map[string]muxEntry 這個就是登記冊,其中的string就是路徑,我們會根據路徑找出單元,從而
向登記中心裡註冊函式過程
// Part-1
func HandleFunc(pattern string,handler func(ResponseWriter,*Request)) {
DefaultServeMux.HandleFunc(pattern,handler)
}
// Part-2
func (mux *ServeMux) HandleFunc(pattern string,*Request)) {
if handler == nil {
panic("http: nil handler")
}
mux.Handle(pattern,HandlerFunc(handler))
}
// Part-3
func (mux *ServeMux) Handle(pattern string,handler Handler) {
mux.mu.Lock()
defer mux.mu.Unlock()
// 做一些驗證工作
e := muxEntry{h: handler,pattern: pattern}
mux.m[pattern] = e
}
複製程式碼
-
Part-1 : 註冊過程本來應該是針對一個登記中心,但是這裡沒有描述具體針對哪個登記中心,因此這時候我們是向預設登記中心:
DefaultServeMux
登記,在後面我們能看到,在啟動服務中心的時候我們會使用這個預設的登記中心 - Part-2 : 登記中心ServerMux裡這樣要求,任何試圖成為muxEntry的函式,必須得是"處理工具"型別,我們的函式是"處理函式"型別,也就是"處理工具"型別,因此能成功生成muxEntry單元而註冊
- Part-3 : 這裡我們的讀寫鎖就發揮作用了,為了防止併發寫入而造成不一樣的結果,我們會加鎖,做一些驗證工作後,針對這個路徑以及處理函式生成一個登記單元
服務中心開始工作
生成服務中心物件
在已經有了登記中心以後,我們會說說服務中心是怎麼開始工作的
_ = http.ListenAndServe(":9090",nil)
func ListenAndServe(addr string,handler Handler) error {
server := &Server{Addr: addr,Handler: handler}
return server.ListenAndServe()
}
複製程式碼
我們拿著主機+埠資訊,Handler=nil
不指定處理工具的方式,生成一個服務中心,讓這個服務中心開始工作
生成監聽器
func (srv *Server) ListenAndServe() error {
if srv.shuttingDown() {
return ErrServerClosed
}
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)})
}
複製程式碼
負責監聽的是監聽器'ln',服務中心只有有了監聽器才能收集到請求,我們設定好監聽地址,請求型別=tcp → 開始監聽
迴圈收集請求
func (srv *Server) Serve(l net.Listener) error {
if fn := testHookServerServe; fn != nil {
fn(srv,l) // call hook with unwrapped listener
}
l = &onceCloseListener{Listener: l}
defer l.Close()
if err := srv.setupHTTP2_Serve(); err != nil {
return err
}
if !srv.trackListener(&l,true) {
return ErrServerClosed
}
defer srv.trackListener(&l,false)
var tempDelay time.Duration // how long to sleep on accept failure
baseCtx := context.Background() // base is always background,per Issue 16220
ctx := context.WithValue(baseCtx,ServerContextKey,srv)
for {
rw,e := l.Accept()
if e != nil {
select {
case <-srv.getDoneChan():
return ErrServerClosed
default:
}
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)
c.setState(c.rwc,StateNew) // before Serve can return
go c.serve(ctx)
}
}
複製程式碼
收到新的請求時,開啟一個協程,生成一個Context上下文用於儲存資料,這個協程負責去讀取請求以及做出反饋,這個函式可以理解為,只負責接收請求,每次接收到請求,就負責找人(協程)處理,而它自己則迴歸原位繼續等下一個請求
讀取請求內容,Server物件開始呼叫處理工具
// 簡化後,這個函式在做什麼
func (c *conn) serve(ctx context.Context) {
for {
// PART-1:讀取請求正文,請求裡包含了什麼資訊
w,err := c.readRequest(ctx)
// PART-2:找人手去處理這個請求
serverHandler{c.server}.ServeHTTP(w,w.req)
//PART-3: 處理完了,關閉請求,善後
w.finishRequest()
}
}
複製程式碼
詳細的介紹一下,這裡是什麼一個場景,首先需要明白,現在我們還站在服務中心的維度上,我們面對的還是一個連線物件,這個函式的主體發起人還是 c *conn
,是一個連線物件
回顧HTTP協議,在HTTP協議中一個非常重要的概念叫做"連線",有了連線再延伸一下就有了諸如長連線,連線等待一系列HTTP屬性,幫大家回憶一下,長連線是這麼辦的:
- 從HTTP1.1開始預設全都走Keep-Alive:
- 客戶端 - Connection:Keep-Alive → 伺服器
- 客戶端 ← Connection:Keep-Alive - 伺服器
- 伺服器設定頭 Keep-Alive: 10 設定超時時間
- 伺服器返回 Connection: close 表達這個長連線已經結束
外圍這個大的for迴圈代表一個長連線,迴圈的讀取發來的請求,每次請求可以分成三步:
- 嘗試看看能不能讀取請求裡的內容
- 可能會遇到請求過大無法讀取的錯誤/請求讀取錯誤的問題
- Expect100: 資料很大時專用的請求頭
- 讀取出了請求正文,服務中心將請求轉移至處理工具處理,前往下一步
- 處理工具return,處理完成,開始善後工作,步驟包含
- finishRequest(),包含:w.reqBody.Close()關閉請求
- 判斷是否要複用這個TCP連結,如果不復用,處理完成後退出
- 判斷如果這個請求並不是長連線,處理完成後退出
- 設定當前連線物件狀態為"Idle",可繼續接受下一個請求
- 在超時時間內,嘗試讀取請求,如果讀不到,則判定超時,退出
找到服務中心的處理工具,登記中心
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{}
}
// 這個就是服務中心的處理工具,它要處理HTTP請求
handler.ServeHTTP(rw,req)
}
複製程式碼
從這裡開始,我們已經有了請求裡的正文,我們接下來開始找Server物件裡的處理工具,用於處理請求.
在一開始,在生成Server物件的時候,我們只給了監聽地址,但是把處理工具設定為nil,因此在下面的程式碼中,我們要開始使用預設的登記中心,作為我們的處理工具
拿到了處理工具,我們開始對著服務中心的處理工具,處理請求. 我們呼叫ServeHTTP方法,按照ServeHTTP方法的定義,它必須能接收一個請求,並且能寫一個反饋,能做響應.
回到登記中心,找到對應的處理函式
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)
}
複製程式碼
現在我們已經回到登記中心了,我們要做的第一件事,是根據請求內容,找到對應的處理函式.
在上面我們說過,登記中心的任何函式都必須實現ServeHTTP方法,因此我們的第二件事就是執行函式ServeHTTP,也就是執行這個執行函式本身.
登記中心: 解析請求尋找對應處理函式的過程
// 第一步,解析請求內容
func (mux *ServeMux) Handler(r *Request) (h Handler,pattern string) {
...
host := stripHostPort(r.Host)
...
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()
// 開始匹配
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) {
// 看看這個路徑能不能直接匹配上
v,ok := mux.m[path]
if ok {
return v.h,v.pattern
}
// 如果找不到直接匹配上,找出最為接近的
for _,e := range mux.es {
if strings.HasPrefix(path,e.pattern) {
return e.h,e.pattern
}
}
return nil,""
}
複製程式碼