http 包怎么用
使用 golang 的 http 包可以很簡易的實現(xiàn)一個 web 服務悲柱,如下
main.go
package main
import (
"log"
"net/http"
"runtime"
"fmt"
)
func foo(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("hi! babe~"))
}
func echo(w http.ResponseWriter, r *http.Request) {
s := fmt.Sprintf("gorotines count: %d", runtime.NumGoroutine())
w.Write([]byte(s))
}
func main() {
mux := http.NewServeMux()
mux.HandleFunc("/foo", foo)plainplainplainplainplainplainplainplainplainplainplainplainplainplainplainplainplainplainplainplain
mux.HandleFunc("/echo/goroutines", echo)
log.Println("Listening...")
http.ListenAndServe(":3000", mux)
}
那如果我想看看整個服務是怎么實現(xiàn)的捧请,該怎么辦呢凡涩?
ListenAndServe()
接收一個地址和處理程序的參數(shù),此函數(shù)的定義如下
func ListenAndServe(addr string, handler Handler) error {
server := &Server{Addr: addr, Handler: handler}
return server.ListenAndServe()
}
然后調用了
func (srv *Server) ListenAndServe() error {
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)})
}
然后上述函數(shù)又調用了 Serve 函數(shù)
func (srv *Server) Serve(l net.Listener) error {
defer l.Close()
...
srv.trackListener(l, true)
defer srv.trackListener(l, false)
baseCtx := context.Background() // base is always background, per Issue 16220
ctx := context.WithValue(baseCtx, ServerContextKey, srv)
ctx = context.WithValue(ctx, LocalAddrContextKey, l.Addr())
for {
// Accept等待并返回listener的下一個連接
rw, e := l.Accept()
if e != nil { ... } // 省略一些代碼
tempDelay = 0
// 使用rw創(chuàng)建一個新連接
c := srv.newConn(rw)
// 將鏈接置為激活狀態(tài)疹蛉,同時可指定在客戶端連接更改狀態(tài)時調用可選的回調函數(shù)
c.setState(c.rwc, StateNew) // before Serve can return
go c.serve(ctx)
}
}
從上面的go c.serve(ctx)
可以看出活箕,http 包在 ctx 上下文組裝好之后交給了 gorotine 來處理這個請求。在繼續(xù)下一步之前可款,我們先看看這個 ctx 上下文育韩,Context 被定義為一個接口,它在 golang 中被運用的非常廣泛闺鲸。
type Context interface {
// Deadline 設置了兩個參數(shù)deadline, ok
// deadline 表示上下文被取消的截止時間
// 如果沒有設置deadline筋讨,Deadline的ok參數(shù)會返回false。
// 連續(xù)調用返回結果相同
Deadline() (deadline time.Time, ok bool)
// 如果上下文被取消摸恍,Done會返回一個被關閉的chan
// 如果上下文從沒被取消過悉罕,Done將返回nil
// 連續(xù)調用返回結果相同
Done() <-chan struct{}
// Done 的 chan被關閉后,也就是上下文被取消時立镶,Err會返回非零的錯誤值壁袄。
// 當 Done 的 chan被關閉后,連續(xù)調用返回結果相同
Err() error
// 也就是通過key去獲取該key上下文中的值谜慌,如果沒有則為nil然想,可見ctx是一個鍵值對。該值是線程安全的
Value(key interface{}) interface{}
}
好了介紹完 context 之后欣范,我們再來看看 Serve 函數(shù)中的baseCtx := context.Background()
是干什么的变泄。
// Background返回一個非零的空Context令哟。它沒有值也沒有deadline,所以也不會被取消妨蛹,
// 它通常在main函數(shù)被用來初始化屏富,測試,以及作為請求傳入的頂級Context
func Background() Context {
return background
}
嗯蛙卤,他其實就是初始化的一個作用狠半。
接下來又碰到了 WithValue 函數(shù),我們繼續(xù)看看 WithValue 的定義颤难。
// 生成一個綁定了一個鍵值對數(shù)據的Context神年,可以通過parent訪問到上一層的context,這個綁定的數(shù)據可以通過Context.Value方法訪問到
func WithValue(parent Context, key, val interface{}) Context {
if key == nil {
panic("nil key")
}
if !reflect.TypeOf(key).Comparable() {
panic("key is not comparable")
}
return &valueCtx{parent, key, val}
}
// 一個valueCtx結構帶有一個鍵值對行嗤。然后用來嵌套其他的Context已日。
type valueCtx struct {
Context
key, val interface{}
}
結合源碼,那么這個 context 定義結構就可以了解了
// 下面定義了兩個context key栅屏,一個存儲了type *Server飘千,另一個存儲了type net.Addr
ServerContextKey = &contextKey{"http-server"}
LocalAddrContextKey = &contextKey{"local-addr"}
// 頂層context
ctx := context.WithValue(baseCtx, ServerContextKey, srv)
// parent 為 頂層的context
ctx = context.WithValue(ctx, LocalAddrContextKey, l.Addr())
那么為什么這么定義呢? 思考思考栈雳,對 context 的作用和細節(jié)還沒系統(tǒng)了解過护奈,context 是一個很重要的功能 TODO
繼續(xù)往下看 go serve(ctx)
,可以看到這里用 gorotine 來處理每個鏈接來支撐并發(fā)哥纫,這也是支持并發(fā)的關鍵霉旗。
// 處理一個新鏈接
func (c *conn) serve(ctx context.Context) {
c.remoteAddr = c.rwc.RemoteAddr().String()
...
// HTTP/1.x from here on.
// 這里又碰到WithCancel函數(shù),WithCancel返回帶有父context 的Done通道副本和一個cancelCtx函數(shù)磺箕。
// 返回的上下文的Done通道在調用了返回的cancelCtx函數(shù)或父context的Done通道關閉時關閉奖慌,以先發(fā)生者為準。
// 取消此上下文會釋放與其關聯(lián)的資源松靡,因此代碼應在此上下文中運行的操作完成后立即調用cancelCtx简僧。所以可以看到使用了defer去調用cancelCtx釋放資源
ctx, cancelCtx := context.WithCancel(ctx)
c.cancelCtx = cancelCtx
defer cancelCtx()
c.r = &connReader{conn: c}
c.bufr = newBufioReader(c.r)
c.bufw = newBufioWriterSize(checkConnErrorWriter{c}, 4<<10)
for {
// 從鏈接中讀取請求
w, err := c.readRequest(ctx)
if c.r.remain != c.server.initialReadLimitSize() {
// If we read any bytes off the wire, we're active.
c.setState(c.rwc, StateActive)
}
...
// 核心點,該處就是處理請求的hanler
serverHandler{c.server}.ServeHTTP(w, w.req)
w.cancelCtx()
...
c.rwc.SetReadDeadline(time.Time{})
}
}
好了到這我們知道是用serverHandler{c.server}.ServeHTTP(w, w.req)
來處理請求的雕欺。我們回過頭去看看岛马,路由和 handler 是怎么綁定到一起的
// ServeMux是一個HTTP請求多路復用器,說白了就承擔了路由功能唄
// 在ServeMux 的注釋中屠列,我們可以了解到整個路由的一些機制啦逆。
// 模式名稱固定,帶根的路徑笛洛,如"/favicon.ico"夏志,或帶根的子樹,如"/images/"(請注意尾部斜杠)苛让。
// 較長的模式優(yōu)先于較短的模式沟蔑,因此如果存在"/images/"和"/images/thumbnails/"注冊的handler湿诊,則"/images/thumbnails/"開頭的路徑將調用后者的handler,然后前者將接收"/images/"子樹中任何其他路徑的請求瘦材,比方說"/images/xxxx"等等厅须。
mux := http.NewServeMux()
// 往mux上綁定了兩個handler
mux.HandleFunc("/foo", foo)
mux.HandleFunc("/echo/goroutines", echo)
我們看到 mux 調用了 HandleFunc,來看看他們的定義
// HandleFunc為給定pattern注冊handler
func (mux *ServeMux) HandleFunc(pattern string, handler func(ResponseWriter, *Request)) {
mux.Handle(pattern, HandlerFunc(handler))
}
// 如果pattern已經存在handler了食棕,將會panic
func (mux *ServeMux) Handle(pattern string, handler Handler) {
mux.mu.Lock()
...
if mux.m == nil {
mux.m = make(map[string]muxEntry)
}
mux.m[pattern] = muxEntry{explicit: true, h: handler, pattern: pattern}
if pattern[0] != '/' {
mux.hosts = true
}
// 如果pattern是/tree/朗和,則為/tree插入隱式永久重定向
// 通過顯式注冊可以覆蓋
n := len(pattern)
if n > 0 && pattern[n-1] == '/' && !mux.m[pattern[0:n-1]].explicit {
// 如果pattern包含host name,將其刪除并使用剩余的路徑進行重定向簿晓。
path := pattern
if pattern[0] != '/' {
// strings.Index 返回子串 sep "/" 在字符串 pattern 中第一次出現(xiàn)的位置
// 如果找不到眶拉,則返回 -1,如果 sep 為空憔儿,則返回 0镀层。
path = pattern[strings.Index(pattern, "/"):]
}
url := &url.URL{Path: path}
// 在我們的例子中pattern為"/echo/gorotine/",則會為"/echo/gorotine" 添加一個重定向
mux.m[pattern[0:n-1]] = muxEntry{h: RedirectHandler(url.String(), StatusMovedPermanently), pattern: pattern}
}
}
看完上面的定義皿曲,我們知道路由和 handler 是怎么存儲的了。
再看看 ServeMux 結構吴侦,m 是一個字典形式的屋休,當我們調用 HandlerFunc 會把 pattern 即"/echo/goroutines"作為 key,muxEntry 作為 value备韧,muxEntry 為一個包含 pattern劫樟,handler 和 explicit 的結構。
type ServeMux struct {
mu sync.RWMutex // 讀寫鎖
m map[string]muxEntry // 存儲結構
hosts bool // whether any patterns contain hostnames
}
type muxEntry struct {
explicit bool // 該pattern是否完全匹配handler
h Handler
pattern string
}
好了织堂,上述把 handler 綁定到了 server 上叠艳。那么是如何通過 url 查找 handler 的呢?先看看 ServeHTTP
// ServeHTTP將請求分派給handler易阳,該handler的pattern與請求URL最匹配附较。
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
}
// 通過Handler找到最匹配的handler來處理該請求
h, _ := mux.Handler(r)
// 調用處理請求
h.ServeHTTP(w, r)
}
從上面代碼可以看到,使用 handler 的 ServeHTTP 方法去處理請求潦俺,這里又有一個疑問了拒课,為什么 ServeHTTP 的 w 是值傳遞,而 r 是引用傳遞呢事示?
先看看 w早像,r 的定義,通過觀察 ResponseWriter肖爵,和 Request 的定義就知道為什么這么做了卢鹦。
// HTTP處理程序使用ResponseWriter接口來構造HTTP響應。
// Handler.ServeHTTP方法返回后劝堪,可能就無法使用ResponseWriter了冀自。
type ResponseWriter interface {
Header() Header
Write([]byte) (int, error)
WriteHeader(int)
}
// 請求表示由服務器接收或由客戶端發(fā)送的HTTP請求揉稚。
type Request struct {
...
}
可以看到,ResponseWriter 是一個接口凡纳,Request 是一個結構窃植。我們往回撥一下,看看這個接口是什么荐糜。
w, err := c.readRequest(ctx)
...
// 正是readRequest返回的
serverHandler{c.server}.ServeHTTP(w, w.req)
// 再看看 readRequest的函數(shù)簽名巷怜,其實也是一個指針來的。
func (c *conn) readRequest(ctx context.Context) (w *response, err error)
{
...
w = &response{
conn: c,
cancelCtx: cancelCtx,
req: req,
reqBody: req.Body,
handlerHeader: make(Header),
contentLength: -1,
closeNotifyCh: make(chan bool, 1),
// We populate these ahead of time so we're not
// reading from req.Header after their Handler starts
// and maybe mutates it (Issue 14940)
wants10KeepAlive: req.wantsHttp10KeepAlive(),
wantsClose: req.wantsClose(),
}
if isH2Upgrade {
w.closeAfterReply = true
}
// 這里有個令人窒息的操作暴氏,對于vegetable的我來說有點難以理解延塑。w.cw.res的res其實也是一個response,w.w的第一個w是response結構答渔,第二個w是一個*bufio.Writer結構关带。
w.cw.res = w
// newBufioWriterSize返回一個Writer結構的指針,而w的Writer是一個方法沼撕,注意區(qū)分
w.w = newBufioWriterSize(&w.cw, bufferBeforeChunkingSize)
return w, nil
}
// response的Write方法正是掉用了第二個w結構的Write方法宋雏,把數(shù)據寫入了緩沖區(qū)
// 在main.go中向response里寫數(shù)據的方法 w.Write([]byte("hi! babe~"))
func (w *response) Write(data []byte) (n int, err error) {
return w.write(len(data), data, "")
}
// Write的具體實現(xiàn)
func (w *response) write(lenData int, dataB []byte, dataS string) (n int, err error) {
...
if dataB != nil {
return w.w.Write(dataB)
} else {
return w.w.WriteString(dataS)
}
}
// w.w 也就是 *bufio.Writer結構 的方法∥癫颍可以看到通過copy把p寫入了write結構磨总。
func (b *Writer) Write(p []byte) (nn int, err error) {
...
n := copy(b.buf[b.n:], p)
b.n += n
nn += n
return nn, nil
}
從上面的代碼可以看到 ResponseWriter 接口,其實也是傳入了一個 response 結構的指針笼沥,又解決一個疑問蚪燕,nice
// Handler返回用于給定請求的handler,返回依據參考r.Method奔浅,r.Host和r.URL.Path等參數(shù)馆纳。它總是返回一個非空的handler(如果沒有則返回NotFound的handler)。
// 如果路徑不規(guī)范汹桦,則處理程序將會走內部生成的handler鲁驶,重定向到它的規(guī)范路徑。
func (mux *ServeMux) Handler(r *Request) (h Handler, pattern string) {
if r.Method != "CONNECT" {
if p := cleanPath(r.URL.Path); p != r.URL.Path {
_, pattern = mux.handler(r.Host, p)
url := *r.URL
url.Path = p
return RedirectHandler(url.String(), StatusMovedPermanently), pattern
}
}
return mux.handler(r.Host, r.URL.Path)
}
// handler函數(shù)是Handler的主要實現(xiàn)舞骆,host參數(shù)傳入請求的r.Host, path參數(shù)傳入r.URL.Path
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
}
上面的調用鏈handler.ServeHTTP
-> func (mux *ServeMux) Handler(r *Request)
-> func (mux *ServeMux) handler(r *Request)
當?shù)竭_func (mux *ServeMux) handler
的時候灵嫌,一切邏輯就清晰了起來。先拋一個問題葛作,"/echo/"和"/echo/goroutines/"這倆怎么區(qū)分 handler寿羞?從前面的注釋,我們知道"/echo/"會處理它所有的子樹赂蠢,而"/echo/goroutines/"就是它的子樹绪穆,匹配的時候會根據最長原則,也就是會先匹配"/echo/goroutines/"的 handler,那我們來看看這個具體實現(xiàn)玖院。
// 上層調用match函數(shù)path傳入r.URL.Path
func (mux *ServeMux) match(path string) (h Handler, pattern string) {
var n = 0
// m里的存儲規(guī)則是 m['/echo/gorotines/'] = EntryMux{}
for k, v := range mux.m {
if !pathMatch(k, path) {
continue
}
// 從pathMatch函數(shù)可以知道"/echo/"會匹配所有它的子樹菠红,也就是類似"/echo/xxx"這些,該函數(shù)都會返回true难菌。
// 所以下面這段邏輯就是上面問題的答案试溯。即最長原則,如果滿足len(k) > n 的情況郊酒,h會被替換成更長path的那個handler遇绞。
// 這段代碼的時間復雜度是O(n),其他的更高效的web框架會不會實現(xiàn)一個O(lgn)的算法呢燎窘?我們知道trie樹可以做的摹闽,下次看看其他的框架怎么實現(xiàn)的
if h == nil || len(k) > n {
n = len(k)
h = v.h
pattern = v.pattern
}
}
return
}
// 可以看到這個pathMatch是拿pattern和path做比較,
func pathMatch(pattern, path string) bool {
if len(pattern) == 0 {
// should not happen
return false
}
n := len(pattern)
// 如果pattern不以'/'結尾 直接比較
if pattern[n-1] != '/' {
return pattern == path
}
// 關鍵部位褐健,path比pattern要長
// 截取path[0:n] 和 pattern匹配付鹿,也就是如果我們的path為"/echo/goroutines/" 我們注冊的handler只有"/echo/"的話,那么"/echo/goroutines/" 會匹配到"/echo/"
return len(path) >= n && path[0:n] == pattern
}
帶著一些問題蚜迅,閱讀了整個 http 請求的一些源碼舵匾,其中確實很復雜,通過了解代碼能搞大概搞清楚怎么處理的谁不,當然作者為什么這么寫腦海里仍然有一個疑問纽匙,等姿勢水平再高一點再來探究。整篇分析到此結束拍谐,怎么把這些條理化展示的水平還有待提高。
回顧一下提出的幾個問題
- "/echo/"和"/echo/goroutines/"這怎么區(qū)分匹配 handler馏段?
- 為什么 ServeHTTP 的 w 是值傳遞轩拨,而 r 是引用傳遞呢?
- http server 怎么處理并發(fā)請求院喜?
- 了解 context 在golang中的應用亡蓉?TODO
參考:
傅小黑的這篇文章框架很清晰
http://fuxiaohei.me/2016/9/20/go-and-http-server.html