快速搭建 Web 服務(wù)器
在Golang僅需要幾行代碼,便可以建立一個(gè)簡(jiǎn)單的 Web 服務(wù):
package main
import (
"fmt"
"log"
"net/http"
)
func main() {
helloHandler := func(w http.ResponseWriter, req *http.Request) {
fmt.Fprintf(w, "Hello, world!\n")
}
http.HandleFunc("/hello", helloHandler)
log.Fatal(http.ListenAndServe(":9999", nil))
}
編譯運(yùn)行,如果不出意外的話,訪問(wèn) http://localhost:9999/hello,你將會(huì)在瀏覽器上看到Hello, world!
當(dāng)時(shí)剛接觸到Go的時(shí)候,我是很震驚的?。。∫郧皩?xiě) PHP,要配 web 服務(wù),還得搭個(gè) Nginx或Apache 服務(wù)器呢,配置下監(jiān)聽(tīng)的接口轉(zhuǎn)發(fā)到fastcgi進(jìn)行處理。Go這么流氓嗎,直接幾行代碼就完事了?是的,Go 就真的很流氓,它直接監(jiān)聽(tīng)了 TCP 端口。
Web 服務(wù)器運(yùn)轉(zhuǎn)機(jī)制
以使用TCP協(xié)議通訊的socket為例,socket通信交互流程大概是這樣子的

我們來(lái)看一段代碼 net 包開(kāi)頭的一段示例代碼
ln, err := net.Listen("tcp", ":8080")
if err != nil {
// handle error
}
for {
conn, err := ln.Accept()
if err != nil {
// handle error
}
go handleConnection(conn)
}
創(chuàng)建 Listen Socket,監(jiān)聽(tīng)指定端口8080,等待客戶(hù)端的請(qǐng)求,創(chuàng)建 goroutine 處理請(qǐng)求。
實(shí)際上 Go 的 web server 也就是這么個(gè)處理邏輯,只是在這多了一些處理邏輯:請(qǐng)求的協(xié)議解析,讀取客戶(hù)端提交的數(shù)據(jù),路由分配(根據(jù)請(qǐng)求分配對(duì)應(yīng)的 handler 處理請(qǐng)求),封裝響應(yīng)數(shù)據(jù)。
http 包源碼分析
還記得快速搭建 web 服務(wù)器的時(shí)候有行代碼:http.ListenAndServe(":9999", nil),Go 就是通過(guò)這個(gè)方法來(lái)處理上面 端口監(jiān)聽(tīng)、請(qǐng)求解析、路由分配、響應(yīng)處理。
1. 端口監(jiān)聽(tīng)
讓我們追進(jìn)去看看,實(shí)際上,初始化一個(gè)server對(duì)象,調(diào)用了 server 的 ListenAndServe 方法。
func ListenAndServe(addr string, handler Handler) error {
server := &Server{Addr: addr, Handler: handler}
return server.ListenAndServe()
}
再追進(jìn)去看看,瞧,這不是嘛 ln, err := net.Listen("tcp", addr) ,用TCP協(xié)議搭建了一個(gè)服務(wù),監(jiān)聽(tīng)著我們?cè)O(shè)置的端口呢。
func (srv *Server) ListenAndServe() error {
if srv.shuttingDown() {
return ErrServerClosed
}
addr := srv.Addr
if addr == "" {
addr = ":http"
}
ln, err := net.Listen("tcp", addr)
if err != nil {
return err
}
return srv.Serve(ln)
}
2. 接受請(qǐng)求
是不是有點(diǎn)小興奮了,讓我們?cè)倮^續(xù)追進(jìn)去看看 srv.Serve(ln),核心代碼片段:
for {
// 接受監(jiān)聽(tīng)器listener的請(qǐng)求
rw, e := l.Accept()
if e != nil {
// 監(jiān)聽(tīng)是否關(guān)閉信號(hào)
select {
case <-srv.getDoneChan():
return ErrServerClosed
default:
}
}
……
// 創(chuàng)建新連接
c := srv.newConn(rw)
// 再返回之前,設(shè)置連接狀態(tài)
c.setState(c.rwc, StateNew) // before Serve can return
// 創(chuàng)建goroutine,真正處理連接
go c.serve(ctx)
}
可以看到,這里用了一個(gè)死循環(huán) for{} 等待。
- 首先通過(guò)
listener.Accept接受請(qǐng)求,是不是跟上面提到的socket的運(yùn)行機(jī)制很像了。 - 接著用接受到的請(qǐng)求創(chuàng)建一個(gè)新的
Conn,并設(shè)置為New狀態(tài)。 - 最后創(chuàng)建
goroutine,真正的處理連接。
知識(shí)點(diǎn):每個(gè)請(qǐng)求都會(huì)創(chuàng)建一個(gè)對(duì)應(yīng)的goroutine去處理,所以各個(gè)請(qǐng)求之間是相互不影響的,同時(shí)提高并發(fā)性能。
3. 讀取請(qǐng)求并解析
來(lái),我們?cè)龠M(jìn)去 go c.serve(ctx) 看看,忽略一些細(xì)節(jié),重點(diǎn)關(guān)注關(guān)鍵的流程
for {
……
// 讀request請(qǐng)求
w, err := c.readRequest(ctx)
……
// 調(diào)用業(yè)務(wù)層定義的路由
serverHandler{c.server}.ServeHTTP(w, w.req)
……
// flush刷io buffer的數(shù)據(jù)
w.finishRequest()
readRequest 便是讀取數(shù)據(jù),解析請(qǐng)求的地方,包括解析請(qǐng)求的header、body,和一些基本的校驗(yàn),比如header頭信息,請(qǐng)求method等。
最后將請(qǐng)求的數(shù)據(jù)賦值到Request,并初始化Response對(duì)象,供業(yè)務(wù)層調(diào)用。
4. 路由分配handler
上面關(guān)鍵流程已經(jīng)看到了serverHandler{c.server}.ServeHTTP(w, w.req),這個(gè)實(shí)際上就是調(diào)用最開(kāi)始在main函數(shù)定義的handler,并將處理好的Request、Response對(duì)象作為參數(shù)傳入。
type serverHandler struct {
srv *Server
}
func (sh serverHandler) ServeHTTP(rw ResponseWriter, req *Request) {
handler := sh.srv.Handler
if handler == nil {
handler = DefaultServeMux
}
if req.RequestURI == "*" && req.Method == "OPTIONS" {
handler = globalOptionsHandler{}
}
handler.ServeHTTP(rw, req)
}
還記得嗎,我們最上面是這樣調(diào)用的 http.ListenAndServe(":9999", nil),第二個(gè)參數(shù)是nil。
所以,你看在ServeHTTP中,handler = DefaultServeMux,我們使用了默認(rèn)的路由器,如果 ListenAndServe 不是傳nil的話,那就會(huì)使用你自己定義的路由器。
我們來(lái)看看httprouter的例子,你就會(huì)明白了:
package main
import (
"fmt"
"net/http"
"log"
"github.com/julienschmidt/httprouter"
)
func Index(w http.ResponseWriter, r *http.Request, _ httprouter.Params) {
fmt.Fprint(w, "Welcome!\n")
}
func main() {
router := httprouter.New()
router.GET("/", Index)
log.Fatal(http.ListenAndServe(":8080", router))
}
看到了吧,上面router便是自定義的路由器,在走到上面ServerHTTP的時(shí)候,便是使用指定的路由器進(jìn)行路由。
5. 路由分配handler
好了,我們知道了使用默認(rèn)的路由器(DefaultServeMux),再看看它是怎么根據(jù)路徑找對(duì)應(yīng)handler的吧~~~
我想你看到這已經(jīng)累了,請(qǐng)不要輕易離開(kāi),學(xué)海無(wú)涯苦作舟啊~~
路由的過(guò)程里面我們只要弄懂下面的三個(gè)問(wèn)題,就知道 Go 自帶的路由是怎么運(yùn)行的了:
- 什么時(shí)候注冊(cè)的路由?
- 如何根據(jù)注冊(cè)的路由找對(duì)應(yīng)的handler?
- 如果沒(méi)注冊(cè)路由訪問(wèn)會(huì)返回什么?
好,我們首先來(lái)看看默認(rèn)路由器(DefaultServeMux)的結(jié)構(gòu)定義:
type ServeMux struct {
mu sync.RWMutex
m map[string]muxEntry
es []muxEntry
hosts bool
}
其中 m 是一個(gè)map,用來(lái)存儲(chǔ)路由pattern與handler的關(guān)系;es 是一個(gè)slice,將路由按長(zhǎng)度從大到小排序存儲(chǔ)起來(lái)。
匹配規(guī)則:首先精確匹配 m 中的pattern;如果在 m 不能精確匹配路徑時(shí),會(huì)在 es 中找到最接近的路由規(guī)則:比如注冊(cè)了兩個(gè)路徑 /a/b/ /a/ ,當(dāng)請(qǐng)求URL是 /a/b/c時(shí),會(huì)匹配到 /a/b/ 而不是 /a/。
1. 什么時(shí)候注冊(cè)的路由?
還記得吧,在 ListenAndServe 之前,有這么一行代碼,http.HandleFunc("/hello", helloHandler),這個(gè)便是在注冊(cè)路由。來(lái)我們把一些檢查代碼去掉,看看關(guān)鍵代碼:
func HandleFunc(pattern string, handler func(ResponseWriter, *Request)) {
DefaultServeMux.HandleFunc(pattern, handler)
}
func (mux *ServeMux) HandleFunc(pattern string, handler func(ResponseWriter, *Request)) {
……
mux.Handle(pattern, HandlerFunc(handler))
}
func (mux *ServeMux) Handle(pattern string, handler Handler) {
……
e := muxEntry{h: handler, pattern: pattern}
mux.m[pattern] = e
if pattern[len(pattern)-1] == '/' {
mux.es = appendSorted(mux.es, e)
}
if pattern[0] != '/' {
mux.hosts = true
}
}
看到了吧,這就是把路由表往 ServeMux.m 和 ServeMux.es 寫(xiě)的全過(guò)程
2. 如何根據(jù)注冊(cè)的路由找對(duì)應(yīng)的handler?
其實(shí)知道怎么寫(xiě)路由表,大概也能猜到是怎么進(jìn)行找了,無(wú)非就是從上面的 m,es 進(jìn)行匹配。
我們上面從端口監(jiān)聽(tīng),接受請(qǐng)求,讀取請(qǐng)求并解析,再到路由分配handler,一路追到 handler.ServeHTTP(rw, req),現(xiàn)在再看看這段代碼的實(shí)現(xiàn)
// 根據(jù)預(yù)設(shè)的pattern,將request分配最匹配的handler處理
func (mux *ServeMux) ServeHTTP(w ResponseWriter, r *Request) {
……
h, _ := mux.Handler(r)
h.ServeHTTP(w, r)
}
……
func (mux *ServeMux) handler(host, path string) (h Handler, pattern string) {
if mux.hosts {
h, pattern = mux.match(host + path)
}
if h == nil {
h, pattern = mux.match(path)
}
if h == nil {
h, pattern = NotFoundHandler(), ""
}
return
}
func (mux *ServeMux) match(path string) (h Handler, pattern string) {
// 優(yōu)先查找m表
v, ok := mux.m[path]
if ok {
return v.h, v.pattern
}
// 未精確匹配成功,查詢(xún)es(已排序),路徑長(zhǎng)的優(yōu)先匹配
for _, e := range mux.es {
if strings.HasPrefix(path, e.pattern) {
return e.h, e.pattern
}
}
return nil, ""
}
3. 如果沒(méi)注冊(cè)路由訪問(wèn)會(huì)返回什么?
這個(gè)問(wèn)題其實(shí)上面代碼已經(jīng)看到了,Go 內(nèi)置了一個(gè) NotFoundHandler(),返回 "404 page not found"
好了,我想你看完這個(gè),應(yīng)該對(duì) Go 如何用幾行代碼搭建 Web 服務(wù)器有了基本了解了吧。
結(jié)尾
個(gè)人建議,看完這個(gè)文章后,自己點(diǎn)進(jìn)去 Go 的源碼包里,我想你會(huì)有新收獲的。

如果有任何疑問(wèn)或錯(cuò)誤可直接留言,我看到會(huì)回復(fù)的~~
公眾號(hào)『知否專(zhuān)欄』,歡迎關(guān)注。