本文作為解決如何通過 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 分鐘
前面小節我們認識了 Web 的工作方式,也成功用 Go 搭建了一個最簡單的 Web 服務瞭解了 Golang 執行 Web 的原理。現在我們詳細地去解剖以下 http 包,看看它如何實現整個過程的
Go 的 http 包中有兩個核心功能:Conn 、ServeMux
與我們使用其他語言編寫 http 伺服器不同, Go為了實現高並行和高效能,使用了 goroutines 來處理 Conn 的讀寫事件。這樣讓每個請求都能保持獨立,相互不會阻塞,可以高效地響應網路事件,這是 Go 高效的保證。
根據上一節,我們知道 Go 在等待使用者端請求裡面是這樣寫的:
c, err := srv.newConn(rw)
if ree != nil {
continue
}
go c.serve()
這段程式碼中,使用者端的每一次請求都會建立一個 Conn,這個 Conn 裡面儲存了這次請求的資訊,然後再傳遞到對應的 handler,該handler中便可以讀取到相應的 header 資訊,這樣保證了每個請求的獨立性。
在之前我們 使用 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!") // 輸出到使用者端
}
我們會發現,我們自己寫的 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)
}
實現效果:
最後我們來梳理一下整個程式碼的執行過程
http.ListenAndServe(":9090",nil)
範例化 Server
呼叫 Server 的 ListenAndServer()
呼叫 net.Listen("tcp", addr)監聽埠
啟動一個 for 迴圈,在迴圈題中 Accept 請求
對每一個請求範例化一個 Conn,並且開啟一個 goroutine 為這個請求開一個 go.c.serve()
讀取每個請求的內容 w, err := c.readRequest()
判斷 handler 是否為空,如果沒有就設定 handler(預設設定)
呼叫 handler 的ServeHTTP
進入到 DefaultServerMux.ServeHTTP
根據 request 選擇 handler, 並且進去到這個 handler 的 ServerHTTP
選擇 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