Golang標準庫探秘(二):快速搭建HTTP服務器
來自: http://www.infoq.com/cn/articles/golang-standard-library-part02
服務器闡述:
現在市面上有很多高并發服務器,Nginx就是一個領軍人物,也是我們仰望的存在;Nginx+Lua這種組合也是高并發服務器的一個代表;PHP語言作為Nginx+FastCGI上一個優秀的解釋語言也占據大半江山。而如今的Golang也作為高并發服務器語言橫空出世,因其“語法簡單、代碼結構簡明,維護成本還低,天生高并發”等特性而被廣泛應用,尤其是各種云服務,還有Docker也是用Golang來做實現語言。
接著我們介紹下服務器編程模型,只從線程的角度,不談并發模型。
從線程的角度,可以分為“單線程”,“多線程”2種。
單線程:
整個進程只有一個線程,因為只有一個線程的緣故,當請求來的時候只能一個個按照順序處理,要想實現高性能只能用“non-blocking IO + IO multiplexing”組合 (非阻塞io + io復用)。 Nginx采用的就是多進程 + 單線程( 非阻塞io+io復用)模式。
多線程:
進程有多個線程,多個線程就不好控制,還帶來一些問題:鎖競爭,數據污染、山下文切換帶來的開銷,但是可以充分利用CPU。要實現高性能也是“non-blocking IO + IO multiplexing”組合。
所以,其實不管單線程還是多線程都是要用“non-blocking IO + IO multiplexing”組合的。還有一種用戶級線程,整個線程的庫都是自己維護,“創建,撤銷,切換”,內核是不知道用戶級線程存在的,缺點是阻塞時會阻塞整個進程。
其實想實現高并發服務器最好用單線程(不處理邏輯的情況下),節省很多上下文切換開銷(CPU分配時間片給任務,CPU加載上下文),但一定要采用io上“非阻塞和異步”。因為多線程很難控制,鎖,數據依賴,不同場景會讓多線程變成串行,控制起來相當繁瑣,犧牲很多并發性能(Golang采用的搶占式調度),但正常情況下多線程還是挺不錯的。下面我們說下Golang實現的高并發。
在Golang的調度器里用的也是“csp”并發模型,有3個重要的概念P、M、G。
P是Processor,G是Goroutine,M是Machine。
簡述:M是執行G的機器線程,跟P綁定才可以執行,P存放G的隊列。看到這里大家會問到剛剛不是說多線程切換上下文開銷很大嗎?其實每個M都有一個g0棧內存,用來執行運行時管理命令。調度時候M的g0會從每個G棧取出棧(現場),調度之后再保存到G,這樣不同的M就可以接著調度了。所有上下文都是自己在切換,省去了內核帶來的開銷,而且Golang會觀察,長時間不調度的G會被其他G搶占(搶占調度其實就是一個標記)。
采用異步的方式運行G,這樣就實現了并發(M可不止一個啊,感興趣看下 Go并發實戰 。
看到上面估計大家可能稍微了解點Golang的優勢了吧。不要擔心GC問題,選擇場景問題。
實戰
現在我們進入實戰部分,手把手教你實現CGI,FastCGI,HTTP服務器,主要是用Golang的HTTP包。TCP實戰就不在這次說了,TCP其實是塊難啃的骨頭,簡單的幾乎話說不清楚,如果是簡單寫一個“hello world”的例子,讓大家似懂非懂的,不如單獨開篇講解一下,從Tcp 到 Protobuf 再到RPC,然后寫一個稍微復雜點的tcp服務器,我們也可以處理下“粘包,丟包”等問題(Protobuf解決或者做一個分包算法),如果簡單的demo可能會導致你丟失興趣的。
首先了解什么是CGI?CGI和FastCGI的區別是什么?
CGI:全拼(Common Gateway Interface)是能讓web服務器和CGI腳本共同處理客戶的請求的協議。Web服務器把請求轉成CGI腳本,CGI腳本執行回復Web服務器,Web服務回復給客戶端。
CGI fork一個新的進程來執行,讀取參數,處理數據,然后就結束生命期。
FastCGI采用tcp鏈接,不用fork新的進程,因為程序啟動的時候就已經開啟了,等待數據的到來,處理數據。
看出來差距在哪里了吧?就是CGI每次都要fork進程,這個開銷很大的。(感興趣的看下linux進程相關知識)。
現在我們來做我們的CGI服務器
CGI服務器
需要用到的包:
"net/http/cgi" "net/http"
簡單的2個包就可以實現CGI服務器了。“高秀敏:準備好了嗎?希望別看到老頭子他又錯了的場景啊”。我們按照“代碼->講解”的流程,先運行在講解。
package main; import ( "net/http/cgi" "fmt" "net/http" )funcmain() { http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request){ handler := new(cgi.Handler); handler.Path = "/usr/local/go/bin/go"; script := "/Users/liujinlong/cgi-script" + r.URL.Path; fmt.Println(handler.Path); handler.Dir = "/Users/liujinlong/cgi-script"; args := []string{"run", script}; handler.Args = append(handler.Args, args...); fmt.Println(handler.Args); handler.ServeHTTP(w, r); }); http.ListenAndServe(":8989",nil); select {}//阻塞進程 } test.go package main import( "fmt" ) funcinit() { fmt.Print("Content-Type: text/plain;charset=utf-8\n\n"); } funcmain() { fmt.Println("hello!!!!") }</pre>
看來我們成功了。來看下net/http/cgi的包。
![]()
先看host.go,這里有一個重要的結構Handler。
// Handler runs an executable in a subprocess with a CGI environment. type Handler struct{ Path string // 執行程序 Root string // 處理url的根,為空的時候“/” Dir string //目錄 Env []string // 環境變量 InheritEnv []string //集成環境變量 Logger *log.Logger// 日志 Args []string //參數 PathLocationHandlerhttp.Handler //http包的handler宿主 }func(h *Handler) ServeHTTP(rwhttp.ResponseWriter, req *http.Request)</pre>
它也實現了ServeHttp,所有請求都會調用這個,這個后面分析HTTP源碼的時候回詳細講解它是做什么的。Handler是在子程序中執行CGI腳本的。
funcRequest() (*http.Request, error) funcServe(handler http.Handler) …先是將前端CGI請求轉換成net包的HTTP請求,然后執行Handler,然后處理response。
FastCGI服務器
接下來是FastCGI服務器,
用到的包:
"net" "net/http" "net/http/fcgi"上面已經講過,它是TCP的方式實現的,需要借助其他服務器來做轉發,這里我們只提供代碼,demo的截圖講解TCP的時候在加上。
需要使用Nginx,我電腦上沒有。各位自己測試一下
server { listen 80; server_name **; ... location *... { include fastcgi.conf; fastcgi_pass 127.0.0.1:9001; } ... }//…是省略,自己去寫一個server。(具體谷歌) package mainimport (
"net" "net/http" "net/http/fcgi" )
type FastCGIstruct{} func(s *FastCGI) ServeHTTP(resphttp.ResponseWriter, req *http.Request) { resp.Write([]byte("Hello, fastcgi")) } funcmain() { listener, _ := net.Listen("tcp", "127.0.0.1:8989")
srv := new(FastCGI) fcgi.Serve(listener, srv) select {
} }</pre>
HTTP服務器
接下來就是重點了,我們的HTTP服務器,這個大家都不陌生,HTTP是最常用的方式之一,通用性很強,跨團隊協作上也比較受到推薦,排查問題也相對來說簡單。
我們接下來以3種方式來展現Golang的HTTP服務器的簡潔和強大。
- 寫一個簡單的HTTP服務器
- 寫一個稍微復雜帶路由的HTTP服務器
- 分析源碼,然后實現一個自定義Handler的服務器
然后我們對照net/http包來進行源碼分析,加強對http包的理解。
1、寫一個簡單的HTTP服務器:
package main;import ( "net/http" ) funchello(w http.ResponseWriter, req *http.Request) { w.Write([]byte("Hello")) } funcsay(w http.ResponseWriter, req *http.Request) { w.Write([]byte("Hello")) } funcmain() { http.HandleFunc("/hello", hello); http.Handle("/handle",http.HandlerFunc(say)); http.ListenAndServe(":8001", nil); select{};//阻塞進程 }</pre>
![]()
![]()
是不是很簡單,我用2種方式演示了這個例子,HandleFunc和Handle方式不同,卻都能實現一個路由的監聽,其實很簡單,但是很多人看到這都會有疑惑,別著急,咱們源碼分析的時候你會看到。
2、寫一個稍微復雜帶路由的HTTP服務器:
對著上面的例子想一個問題,我們在開發中會遇到很多問題,比如handle/res,handle/rsa…等等路由,這兩個路由接受的參數都不一樣,我們應該怎么寫。我先來個圖展示下運行結果。
![]()
是不是挺驚訝的,404了,路由沒有匹配到。可是我們寫handle這個路由了。
問題:
- 什么原因導致的路由失效
- 如何解決這種問題,做一個可以用Controller來控制的路由
問題1:
我們在源碼閱讀分析的時候會解決。
問題2:
我們可以設定一個控制器Handle,它有2個action,我們的執行handle/res對應的結果是調用Handle的控制器下的res方法。這樣是不是很酷。
來我們先上代碼:
靜態目錄:
- css
- js
- image
靜態目錄很好實現,只要一個函數http.FileServer(),這個函數從文字上看就是文件服務器,他需要傳遞一個目錄,我們常以http.Dir("Path")來傳遞。
其他目錄大家自己實現下,我們來實現問題2,一個簡單的路由。
我們來看下代碼
package main;import ( "net/http" "strings" "reflect" "fmt" ) funchello(w http.ResponseWriter, req *http.Request) { w.Write([]byte("Hello")); } type Handlers struct{ } func(h *Handlers) ResAction(w http.ResponseWriter, req *http.Request) { fmt.Println("res"); w.Write([]byte("res")); } funcsay(w http.ResponseWriter, req *http.Request) { pathInfo := strings.Trim(req.URL.Path, "/"); parts := strings.Split(pathInfo, "/"); varaction = ""; fmt.Println(strings.Join(parts,"|")); if len(parts) >1 { action = strings.Title(parts[1]) + "Action"; } fmt.Println(action); handle := &Handlers{}; controller := reflect.ValueOf(handle); method := controller.MethodByName(action); r := reflect.ValueOf(req); wr := reflect.ValueOf(w); method.Call([]reflect.Value{wr, r}); } funcmain() { http.HandleFunc("/hello", hello); http.Handle("/handle/",http.HandlerFunc(say)); http.ListenAndServe(":8081", nil); select{};//阻塞進程 }</pre>
上面代碼就可以實現handle/res,handle/rsa等路由監聽,把前綴相同的路由業務實現放在一個文件里,這樣也可以解耦合,是不是清爽多了。其實我們可以在做的更加靈活些。在文章最后我們放出來一個流程圖,按照流程圖做你們就能寫出一個簡單的mvc路由框架。接下來看運行之后的結果。
如下圖:
(點擊放大圖像)
![]()
3、分析源碼,然后實現一個自定義Handler的服務器
現在我們利用這個例子來分析下http包的源碼(只是服務器相關的,Request我們此期不講,簡單看看就行。)
其實使用Golang做web服務器的方式有很多,TCP也是一種,net包就可以實現,不過此期我們不講,因為HTTP服務器如果不懂,TCP會讓你更加不明白。
我們從入口開始,首先看main方法里的http.HandleFunc和http.Handle這個綁定路由的方法,上面一直沒解釋有啥區別。現在我們來看一下。
// HandleFunc registers the handler function for the given pattern // in the DefaultServeMux. // The documentation for ServeMux explains how patterns are matched. funcHandleFunc(pattern string, handler func(ResponseWriter, *Request)) funcHandle(pattern string, handler Handler)Handle 和HandleFunc都是注冊路由,從上面也能看出來這兩個函數都是綁定注冊路由函數的。如何綁定的呢?我們來看下。
上面2個函數通過DefaultServeMux.handle,DefaultServeMux.handleFunc把pattern和HandleFunc綁定到ServeMux的Handle上。
為什么DefaultServeMux會把路由綁定到ServeMux上呢?
// DefaultServeMux is the default ServeMux used by Serve. varDefaultServeMux = NewServeMux()因為DefaultServeMux就是ServeMux的實例對象。導致我們就把路由和執行方法綁注冊好了。不過大家請想下handle/res的問題?
從上面的分析我們要知道幾個重要的概念。
HandlerFunc// The HandlerFunc type is an adapter to allow the use of // ordinary functions as HTTP handlers. If f is a function // with the appropriate signature, HandlerFunc(f) is a // Handler object that calls f. type HandlerFuncfunc(ResponseWriter, *Request) // ServeHTTP calls f(w, r). func(f HandlerFunc) ServeHTTP(w ResponseWriter, r *Request) { f(w, r) }</pre>
上面的大概意思是,定義了一個函數適配器(可以理解成函數指針)HandleFunc,通過HandlerFunc(f)來進行適配。其實調用的實體是f本身。
package main import "fmt" type A func(int, int) func(f A)Serve() { fmt.Println("serve2") } funcserve(int,int) { fmt.Println("serve1") } funcmain() { a := A(serve) a(1,2)//這行輸出的結果是serve1 a.Serve()//這行輸出的結果是serve2 }上面結果是serve1,serve2
Golang的源碼里用了很多HandleFunc這個適配器。
接下來我們看第二個,ServeMux結構,最終我們是綁定它,也是通過它來解析。
type ServeMuxstruct{ mu sync.RWMutex//讀寫鎖 m map[string]muxEntry//路由map,pattern->HandleFunc hosts bool//是否包含hosts
}type muxEntrystruct{ explicit bool//是否精確匹配,這個在Golang實現里是ture h Handler //這個路由表達式對應哪個handler pattern string//路由 }</pre>
看到explicit的時候是不是就明白為啥handle/res不能用handle來監聽了?原來如此。大致綁定流程大家看明白了嗎?如果不理解可以回去再看一遍。
接下來我們來看實現“啟動/監聽/觸發”服務器的代碼。
http.ListenAndServe(":8081", nil);上面這句就是,”:8081”是監聽的端口,也是socket監聽的端口,第二個參數就是我們的Handler,這里我們寫nil。
funcListenAndServe(addr string, handler Handler) error { server := &Server{Addr: addr, Handler: handler} return server.ListenAndServe() }從這個代碼看出來,Server這個結構很重要。我們來看看他是什么。
type Server struct { Addr string // 監聽的地址和端口 Handler Handler // 所有請求需要調用的Handler( ReadTimeouttime.Duration // 讀的最大Timeout時間 WriteTimeouttime.Duration // 寫的最大Timeout時間 MaxHeaderBytesint // 請求頭的最大長度 TLSConfig *tls.Config // 配置TLS ... //結構太長我省略些,感興趣大家自己看下 }Server提供的方法有:
func(srv *Server) Serve(l net.Listener) error //對某個端口進行監聽,里面就是調用for進行accept的處理了 func(srv *Server) ListenAndServe() error //開啟http server服務 func(srv *Server) ListenAndServeTLS(certFile, keyFile string) error //開啟https server服務Server的ListenAndServe方法通過TCP的方式監聽端口,然后調用Serve里的實現等待client來accept,然后開啟一個協程來處理邏輯(go c.serve)。
它的格式
func(srv *Server) ListenAndServe() error看到這里我們要了解幾個重要的概念。
ResponseWriter:生成Response的接口
Handler:處理請求和生成返回的接口
ServeMux:路由,后面會說到ServeMux也是一種Handler
Conn : 網絡連接
這幾個概念看完之后我們下面要用。
type conn struct這個結構是一個網絡間接。我們暫時忽略。
這個c.serve里稍微有點復雜,它有關閉這次請求,讀取數據的,刷新緩沖區的等實現。這里我們主要關注一個c.readRequest(),通過redRequest可以得到Response,就是輸出給客戶端數據的一個回復者。
它里面包含request。如果要看懂這里的實現就要搞懂三個接口。
ResponseWriter, Flusher, Hijacker// ResponseWriter的作用是被Handler調用來組裝返回的Response的 type ResponseWriter interface { // 這個方法返回Response返回的Header供讀寫 Header() Header // 這個方法寫Response的Body Write([]byte) (int, error) // 這個方法根據HTTP State Code來寫Response的Header WriteHeader(int) } // Flusher的作用是被Handler調用來將寫緩存中的數據推給客戶端 type Flusher interface { // 刷新緩沖區 Flush() } // Hijacker的作用是被Handler調用來關閉連接的 type Hijacker interface { Hijack() (net.Conn, *bufio.ReadWriter, error) }</pre>
而我們這里的w也就是ResponseWriter了。而調用了下面這句方法,就可以利用它的Write方法輸出內容給客戶端了。
serverHandler{c.server}.ServeHTTP(w, w.req)這句就是觸發路由綁定的方法了。要看這個觸發器我們還要知道幾個接口。
具體我們先看下如何實現這三個接口的,因為后面我們要看觸發路由執行邏輯片段。實現這三個接口的結構是response
response // response包含了所有server端的HTTP返回信息 type response struct { conn *conn // 保存此次HTTP連接的信息 req *Request // 對應請求信息 chunking bool // 是否使用chunk wroteHeaderbool // header是否已經執行過寫操作 wroteContinuebool // 100 Continue response was written header Header // 返回的HTTP的Header written int64 // Body的字節數 contentLength int64 // Content長度 status int // HTTP狀態 needSniffbool //是否需要使用sniff。(當沒有設置Content-Type的時候,開啟sniff能根據HTTP body來確定Content-Type) closeAfterReplybool //是否保持長鏈接。如果客戶端發送的請求中connection有keep-alive,這個字段就設置為false。 requestBodyLimitHitbool //是否requestBody太大了(當requestBody太大的時候,response是會返回411狀態的,并把連接關閉) }在response中是可以看到
func(w *response) Header() Header func(w *response) WriteHeader(code int) func(w *response) Write(data []byte) (n int, err error) func(w *response) WriteString(data string) (n int, err error) // either dataB or dataS is non-zero. func(w *response) write(lenDataint, dataB []byte, dataS string) (n int, err error) func(w *response) finishRequest() func(w *response) Flush() func(w *response) Hijack() (rwcnet.Conn, buf *bufio.ReadWriter, err error)我簡單羅列一些,從上面可以看出,response實現了這3個接口。
接下來我們請求真正的觸發者也就是serverHandle要觸發路由(hijacked finishRequest暫且不提)。先看一個接口。
Handlertype Handler interface { ServeHTTP(ResponseWriter, *Request) // 具體的邏輯函數 }</pre>
實現了handler接口,就意味著往server端添加了處理請求的邏輯函數。
serverHandle調用ServeHttp來選擇觸發的HandleFunc。這里面會做一個判斷,如果你傳遞了Handler,就調用你自己的,如果沒傳遞就用DefaultServeMux默認的。到這整體流程就結束了。
過程是:
DefaultServeMux.ServeHttp執行的簡單流程.
- h, _ := mux.Handler(r)
- h.ServeHTTP(w, r) //執行ServeHttp函數
查找路由,mux.handler函數里又調用了另外一個函數mux.handler(r.Host, r.URL.Path)。
還記得我們的ServeMux里的hosts標記嗎?這個函數里會進行判斷。
// 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(), "" }上面就是匹配查找pattern和handler的流程了
我們來總結一下。
首先調用Http.HandleFunc
按順序做了幾件事:
- 調用了DefaultServerMux的HandleFunc
- 調用了DefaultServerMux的Handle
- 往DefaultServeMux的map[string]muxEntry中增加對應的handler和路由規則
別忘記DefaultServerMux是ServeMux的實例。其實都是圍繞ServeMux,muxEntry2個結構進行操作綁定。
其次調用http.ListenAndServe(":12345", nil)
按順序做了幾件事情:
- 實例化Server
- 調用Server的ListenAndServe()
- 調用net.Listen("tcp", addr)監聽端口,啟動for循環,等待accept請求
- 對每個請求實例化一個Conn,并且開啟一個goroutine處理請求。
- 如:go c.serve()
- 讀取請求的內容w, err := c.readRequest(),也就是response的取值過程。
- 調用serverHandler的ServeHTTP,ServeHTTP里會判斷Server的屬性里的header是否為空,如果沒有設置handler,handler就設置為DefaultServeMux,反之用自己的(我們后面會做一個利用自己的Handler寫服務器)
- 調用DefaultServeMux的ServeHttp( 因為我們沒有自己的Handler,所以走默認的)
通過request選擇匹配的handler:
A request匹配handler的方式。Hosts+pattern或pattern或notFound
B 如果有路由滿足,返回這個handler
C 如果沒有路由滿足,返回NotFoundHandler
- 根據返回的handler進入到這個handler的ServeHTTP
大概流程就是這個樣子,其實在net.Listen("tcp", addr)里也做了很多事,我們下期說道TCP服務器的時候回顧一下他做了哪些。
通過上面的解釋大致明白了我們綁定觸發的都是DefaultServeMux的Handler。現在我們來實現一個自己的Handler,這也是做框架的第一步。我們先來敲代碼。
package main;import ( "fmt" "net/http" "time" ) type customHandlerstruct{ } func(cb *customHandler) ServeHTTP( w http.ResponseWriter, r *http.Request ) { fmt.Println("customHandler!!"); w.Write([]byte("customHandler!!")); } funcmain() { varserver *http.Server = &http.Server{ Addr: ":8080", Handler: &customHandler{}, ReadTimeout: 10 * time.Second, WriteTimeout: 10 * time.Second, MaxHeaderBytes: 1 <<20, } server.ListenAndServe(); select { } }</pre>
![]()
是不是很酷,我們可以利用自己的handler做一個智能的路由出來。
不過還是建議使用國內Golang語言框架 beego ,已開源。一款非常不錯的框架,謝大維護的很用心,絕對良心框架,而且文檔支持,社區也很不錯。
最后附上一張最早設計框架時候的一個流程圖(3年前)。大家可以簡單看看,當然也可以嘗試的動動手。起碼收獲很多。
(點擊放大圖像)
![]()
[1]: http://item.jd.com/11573034.html
[2]: https://github.com/astaxie/beego
作者簡介
劉金龍,藝名:金灶沐 ,go語言愛好者,2015年8月加入創業團隊,負責各種“打雜”工作,之前在360電商購物小蜜java組擔任java高級工程師職位,負責購物小蜜服務開發。14年開始用go語言做高并發服務并且嘗試閱讀go語言的源碼來學習go語言的特性。
</div>