1. 程式人生 > 程式設計 >net/http - 學習

net/http - 學習

Introduction

  1. 什麼是函式登記中心 跳轉
  2. 登記中心裡的處理函式是什麼 跳轉
  3. 登記中心裡的處理工具是什麼 跳轉
  4. 登記中心裡的內部結構是什麼樣的 跳轉
  5. 向登記中心登記處理函式 跳轉
  6. 服務中心,生成服務物件 跳轉
  7. 服務中心,生成監聽器 跳轉
  8. 服務中心,迴圈收集請求 跳轉
  9. 取出服務中心的處理工具→登記中心 跳轉
  10. 登記中心找出匹配的處理函式,處理請求 跳轉

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,""
}

複製程式碼