Web Application Workflow

  1. 使用者使用瀏覽器輸入 URL
  2. 瀏覽器向 DNS 發起請求,尋找該 URL 相對應的 IP
  3. 瀏覽器透過 IP 與伺服器建立 TCP 連線
  4. 建立連線後,發送 HTTP/HTTPS Reqeust
  5. 伺服器處理 Request,並送回 Response
  6. 瀏覽器與伺服器斷開連結
  7. 瀏覽器解析 Response 顯示給使用者
  8. 完成一次服務

這是一次簡單的 web application workflow
我們現在使用的各項便利的服務都是以這樣的基礎發展下去
可能隨著服務的複雜度不同,會有其他的技術支援

Golang 是如何處理 Request ?

讓我們專注於前面 workflow 提到的第 5 點,伺服器處理 Request 的部分。

func main() {
    http.HandleFunc("/", helloWorld)
	err := http.ListenAndServe(":9090", nil)
	if err != nil {
		log.Fatal("ListenAndServe: ", err)
	}
}

func helloWorld(w http.ResponseWriter, r *http.Request) {
	fmt.Print("hello world")
}

讓我們來看這個例子,這個是一個非常簡單的例子,甚至沒有回覆任何東西給 Web。
但這讓我們更能聚焦在 Golang 是如何來處理 Request 的。

所以我們先看一下 http.ListenAndServe 裡面做了些什麼

func ListenAndServe(addr string, handler Handler) error {
	server := &Server{Addr: addr, Handler: handler}
	return server.ListenAndServe()
}

從這裡可以看到 golang 為我們建立了一個 server 來 handle request 。
那接下來讓我們看看 server 是怎麼 Serve 的吧。

func (srv *Server) Serve(l net.Listener) error {
	
    // lot of code...

	for {
		rw, err := l.Accept()
		
        // lot of code...

		c := srv.newConn(rw)
		c.setState(c.rwc, StateNew, runHooks) // before Serve can return
		go c.serve(connCtx)
	}
}

來到了 Serve func 中我們可以看到關鍵的地方 go c.serve(connCtx)
這個 func 會對 net.Listener 不斷地去接收新進的 request
確認這個 request 是可用的便會使用 goroutine 去 serve 一個新的連結
這就是為什麼 Golang 是相對容易處理高併發環境的原因。

那 Golang 又是怎麼找到對應的 handler 的呢?

你可能會好奇,我們寫 RESTful API 的時候有這麼多 Method, Endpoint 他是怎麼幫我們 Match 到的呢?
在討論這個問題之前,我們要先知道一件事就是:
在我們一開始使用 http.HandleFunc("/", helloWorld) 這個 method 時,Golang 已經幫我們把這個 HandlerFunction 給加入一個 Map 裡了(key 為 path, value 為 Handler)

// Step 1.
func (c *conn) serve(ctx context.Context) {

        // lot of code...

		// 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)
		
        // lot of code

	}
}

// Step 2.
type serverHandler struct {
	srv *Server
}

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)
}

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)
}

// Step 3
func (mux *ServeMux) Handler(r *Request) (h Handler, pattern string) {

	// CONNECT requests are not canonicalized.
	if r.Method == "CONNECT" {
		// If r.URL.Path is /tree and its handler is not registered,
		// the /tree -> /tree/ redirect applies to CONNECT requests
		// but the path canonicalization does not.
		if u, ok := mux.redirectToPathSlash(r.URL.Host, r.URL.Path, r.URL); ok {
			return RedirectHandler(u.String(), StatusMovedPermanently), u.Path
		}

		return mux.handler(r.Host, r.URL.Path)
	}

	// All other requests have any port stripped and path cleaned
	// before passing to mux.handler.
	host := stripHostPort(r.Host)
	path := cleanPath(r.URL.Path)

	// If the given path is /tree and its handler is not registered,
	// redirect for /tree/.
	if u, ok := mux.redirectToPathSlash(host, path, r.URL); ok {
		return RedirectHandler(u.String(), StatusMovedPermanently), u.Path
	}

	if path != r.URL.Path {
		_, pattern = mux.handler(host, path)
		url := *r.URL
		url.Path = path
		return RedirectHandler(url.String(), StatusMovedPermanently), pattern
	}

	return mux.handler(host, r.URL.Path)
}

// Step 4.
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
}

上面很複雜的 Code 其實是做了很多層的抽象,Connect 只知道我需要 ServeHTTP ,但實際上他要 ServeHTTP 哪個它其實並不清楚

讓我們來看看整個流程是什麼吧

  1. Conn 裡面用 ServeHanlder.ServeHTTP
  2. ServeHandler 裡面就去 Server 這個物件裡去找 handler 以我們的例子是 DefaultServeMux
  3. DefaultServeMux 裡去 check request
  4. 透過 handle 這個 function 我們透過 ServeMux 去 match 到真正要使用的 handler 再一路回傳給 Conn 去 Serve 啦

結語

通過一系列的流程可以看出 Golang 如何處理 Web Application 。 如果有錯誤的地方也煩請提出與建議,謝謝囉。