前言:
本文作為解決如何透過 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 分鐘
文章目錄:
正文:
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!") // 輸出到客戶端
}
我們會發現,我們自己寫的 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
按照順序做了這幾件事:
- 呼叫了 DefaultServeMux 的 HandleFunc
- 呼叫了 DefaultServeMux 的 Handler
- 往 DefaultServeMux 的 map[string]muxEntry 中增加對應的handler 和 路由規則
- 其次呼叫
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?
Goalng:基礎複習一遍過
漫談計算機網路:網路層 ------ 重點:IP協議與網際網路路由選擇協議
以上
看完記得留下一個?