Go For Web:Golang http 包詳解(原始碼剖析)

2023-04-15 06:01:13

前言:

本文作為解決如何通過 Golang 來編寫 Web 應用這個問題的前瞻,對 Golang 中的 Web 基礎部分進行一個簡單的介紹。目前 Go 擁有成熟的 Http 處理包,所以我們去編寫一個做任何事情的動態 Web 程式應該是很輕鬆的,接下來我們就去學習瞭解一些關於 Web 的相關基礎,瞭解一些概念,以及 Golang 是如何執行一個 Web 程式的。
文章預計分為四個部分逐步更新
2023-04-13 星期四 一更 全文共計約 3800 字 閱讀大約花費 5 分鐘
2023-04-14 星期五 二更(兩篇) 全文共計約 2000 字 閱讀大概花費 4 分鐘
2023-04-14 星期五 三更 全文共計約 2000 字 閱讀大概花費 5 分鐘


文章目錄:

  1. Web 的工作方式
  2. 用 Go 搭建一個最簡單的 Web 服務
  3. 瞭解 Golang 執行 web 的原理
  4. Golang http 包詳解(原始碼剖析)

正文:

Golang http 包詳解(原始碼剖析)

前面小節我們認識了 Web 的工作方式,也成功用 Go 搭建了一個最簡單的 Web 服務瞭解了 Golang 執行 Web 的原理。現在我們詳細地去解剖以下 http 包,看看它如何實現整個過程的

Go 的 http 包中有兩個核心功能:Conn 、ServeMux

Conn 的 goroutine

與我們使用其他語言編寫 http 伺服器不同, Go為了實現高並行和高效能,使用了 goroutines 來處理 Conn 的讀寫事件。這樣讓每個請求都能保持獨立,相互不會阻塞,可以高效地響應網路事件,這是 Go 高效的保證。

根據上一節,我們知道 Go 在等待使用者端請求裡面是這樣寫的:

點選檢視程式碼
c, err := srv.newConn(rw)
if ree != nil {
	continue
}
go c.serve()

這段程式碼中,使用者端的每一次請求都會建立一個 Conn,這個 Conn 裡面儲存了這次請求的資訊,然後再傳遞到對應的 handler,該handler中便可以讀取到相應的 header 資訊,這樣保證了每個請求的獨立性。

ServeMux 的自定義

在之前我們 使用 conn.server 的時候,其實內部是呼叫了 http 包預設的路由器也就是DefaultServeMux,通過這個路由器把本次請求的資訊傳遞到了後端的處理常式。那麼這個路由器是怎麼實現的呢?

結構如下:

  • 首先是一個 自定義型別結構體 ServeMux 其中包含一個
    和一個 路由規則

  • 路由規則中一個 string 對應一個 mux 實體,我們來看看 muxEntry 它也是一個自定義型別結構體,包含一個 布林值,一個Handler 處理常式

  • 最後再來看看 Handler 的定義,它其實是一個介面,實現了 ServeHTTP 這個函數

這個時候我們可以回過頭來看我們之前自己寫的 Web 伺服器

點選檢視程式碼
// Handler處理常式
func sayhelloName(w http.ResponseWriter, r *http.Request) {
	r.ParseForm() // 解析引數,預設不會解析
	fmt.Println(r.Form)// 以下這些資訊是輸出到伺服器端的列印資訊:請求表單form、路徑path、格式scheme
	fmt.Println("path", r.URL.Path)
	fmt.Println("scheme", r.URL.Scheme)
	fmt.Println(r.Form["url_long"])
	for k, v := range r.Form {
		fmt.Println("key:", k)
		fmt.Println("val:", strings.Join(v, ""))
	}
	fmt.Fprintln(w, "Hello astaxie!") // 輸出到使用者端
}
呼叫: `http.HandleFunc("/", sayhelloName) // 設定存取的路由`

我們會發現,我們自己寫的 sayhelloName 函數並沒有實現 ServeHTTP 這個函數,也就是說按照常理我們並沒有實現 Handler 這個介面,那我們是怎麼新增的?

原來, http 包裡面還定義了一個自定義函數型別 HandlerFunc,而我們定義的函數 sayhelloName 就是這個 HandlerFunc 呼叫之後的結果,這個自定義函數型別預設會實現 ServeHTTP 這個方法,即我們呼叫了 HandlerFunc(f)強制型別轉換 f 成為了 HandlerFunc 型別,這樣 f 就擁有了ServeHTTP 方法

路由器裡儲存好了相應的路由規則(Response / Request)之後,那麼具體的請求又是怎麼分發的呢?
路由器接收到請求之後呼叫 mux.handler(r).ServeHTTP(w,r)
也就是呼叫對應路由的 handler 的 ServerHTTP 介面,讓我們來看看
mux.handler(r)是怎麼處理的↓

我們可以看到它是根據使用者請求的 URL 和路由器裡面儲存的 map 去匹配的,當匹配到之後返回儲存的 handler,呼叫這個 handler 的 ServeHTTP 介面就可以執行相應的函數了

通過上面的介紹,我們大致瞭解了整個構建路由的過程,Go其實支援外部實現的路由器 而 ListenAndServe 的第二個引數就是用來設定外部路由器的,它是一個 Handler 介面,所以我們的外部路由只要實現了 Handler 介面就可以發揮作用,因此我們可以在自己實現的路由器的 ServeHTTP 裡面實現自定義的路由功能

貼個程式碼↓

點選檢視程式碼
package main

import (
	"fmt"
	"net/http"
)

type MyMux struct {
}

func (p *MyMux) ServeHTTP(w http.ResponseWriter, r *http.Request) {
	if r.URL.Path == "/" {
		sayhelloName2(w, r)
		return
	} else {
		http.NotFound(w, r)
		return
	}
}

func sayhelloName2(w http.ResponseWriter, r *http.Request) {
	fmt.Fprintf(w, "Hello myroute!")
}

func main() {
	mux := &MyMux{}
	http.ListenAndServe(":9090", mux)
}

實現效果:

Go 程式碼的執行流程

最後我們來梳理一下整個程式碼的執行過程

  • 首先呼叫 Http.HandleFunc
    按照順序做了這幾件事:
  1. 呼叫了 DefaultServeMux 的 HandleFunc
  2. 呼叫了 DefaultServeMux 的 Handler
  3. 往 DefaultServeMux 的 map[string]muxEntry 中增加對應的handler 和 路由規則
  • 其次呼叫 http.ListenAndServe(":9090",nil)
    按順序做了這幾件事:
  1. 範例化 Server

  2. 呼叫 Server 的 ListenAndServer()

  3. 呼叫 net.Listen("tcp", addr)監聽埠

  4. 啟動一個 for 迴圈,在迴圈題中 Accept 請求

  5. 對每一個請求範例化一個 Conn,並且開啟一個 goroutine 為這個請求開一個 go.c.serve()

  6. 讀取每個請求的內容 w, err := c.readRequest()

  7. 判斷 handler 是否為空,如果沒有就設定 handler(預設設定)

  8. 呼叫 handler 的ServeHTTP

  9. 進入到 DefaultServerMux.ServeHTTP

  10. 根據 request 選擇 handler, 並且進去到這個 handler 的 ServerHTTP

  11. 選擇 handler
    A 判斷是否有路由能滿足這個 request (迴圈遍歷 ServerMux 的 muxEntry)
    B 如果滿足,則呼叫這個路由 handler 的 ServeHTTP
    C 如果不滿足,則呼叫 NotFoundHandler 的 ServeHTTP

總結

到這裡為止我們從第一章介紹了 HTTP 協定,DNS 解析過程,瞭解了 Web 的工作方式,第二章分別用 Go 搭建一個最簡單的 Web 服務,並且瞭解 Golang 執行 web 的原理,在最後一章,我們還深入到 net/http 包中的原始碼裡為大家揭開了更底層的原理

既然對 Go 開發 Web 有了初步的瞭解,接下來我們就可以有十足的信心去學習更多 Go For Web 的後續內容了!

關於 Golang 基礎部分 以及 計算機網路部分讀者可以參閱我的往期 blog