接下來就是重點了,我們的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{};
}


是不是很簡單,我用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{};
}
上面代碼就可以實現handle/res,handle/rsa等路由監聽,把前綴相同的路由業務實現放在一個文件里,這樣也可以解耦合,是不是清爽多了。其實我們可以在做的更加靈活些。在文章最后我們放出來一個流程圖,按照流程圖做你們就能寫出一個簡單的mvc路由框架。接下來看運行之后的結果。
如下圖:
(點擊放大圖像)

3、分析源碼,然后實現一個自定義Handler的服務器
現在我們利用這個例子來分析下http包的源碼(只是服務器相關的,Request我們此期不講,簡單看看就行。)
其實使用Golang做web服務器的方式有很多,TCP也是一種,net包就可以實現,不過此期我們不講,因為HTTP服務器如果不懂,TCP會讓你更加不明白。
我們從入口開始,首先看main方法里的http.HandleFunc和http.Handle這個綁定路由的方法,上面一直沒解釋有啥區別。現在我們來看一下。
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上呢?
varDefaultServeMux = NewServeMux()
因為DefaultServeMux就是ServeMux的實例對象。導致我們就把路由和執行方法綁注冊好了。不過大家請想下handle/res的問題?
從上面的分析我們要知道幾個重要的概念。
HandlerFunc
type HandlerFuncfunc(ResponseWriter, *Request)
func(f HandlerFunc) ServeHTTP(w ResponseWriter, r *Request) {
f(w, r)
}
上面的大概意思是,定義了一個函數適配器(可以理解成函數指針)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)
a.Serve()
}
上面結果是serve1,serve2
Golang的源碼里用了很多HandleFunc這個適配器。
接下來我們看第二個,ServeMux結構,最終我們是綁定它,也是通過它來解析。
type ServeMuxstruct{
mu sync.RWMutex
m map[string]muxEntry
hosts bool
}
type muxEntrystruct{
explicit bool
h Handler
pattern string
}
看到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
ReadTimeouttime.Duration
WriteTimeouttime.Duration
MaxHeaderBytesint
TLSConfig *tls.Config
...
}
Server提供的方法有:
func(srv *Server) Serve(l net.Listener) error
func(srv *Server) ListenAndServe() error
func(srv *Server) ListenAndServeTLS(certFile, keyFile string) error
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
type ResponseWriter interface {
Header() Header
Write([]byte) (int, error)
WriteHeader(int)
}
type Flusher interface {
Flush()
}
type Hijacker interface {
Hijack() (net.Conn, *bufio.ReadWriter, error)
}
而我們這里的w也就是ResponseWriter了。而調用了下面這句方法,就可以利用它的Write方法輸出內容給客戶端了。
serverHandler{c.server}.ServeHTTP(w, w.req)
這句就是觸發路由綁定的方法了。要看這個觸發器我們還要知道幾個接口。
具體我們先看下如何實現這三個接口的,因為后面我們要看觸發路由執行邏輯片段。實現這三個接口的結構是response
response
type response struct {
conn *conn
req *Request
chunking bool
wroteHeaderbool
wroteContinuebool
header Header
written int64
contentLength int64
status int
needSniffbool
closeAfterReplybool
requestBodyLimitHitbool
}
在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)
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暫且不提)。先看一個接口。
Handler
type Handler interface {
ServeHTTP(ResponseWriter, *Request)
}
實現了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標記嗎?這個函數里會進行判斷。
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 {
}
}

是不是很酷,我們可以利用自己的handler做一個智能的路由出來。
不過還是建議使用國內Golang語言框架beego,已開源。一款非常不錯的框架,謝大維護的很用心,絕對良心框架,而且文檔支持,社區也很不錯。
最后附上一張最早設計框架時候的一個流程圖(3年前)。大家可以簡單看看,當然也可以嘗試的動動手。起碼收獲很多。
(點擊放大圖像)

[1]: http://item.jd.com/11573034.html
[2]: https://github.com/astaxie/beego