1. 程式人生 > >服務計算學習之路-開發 web 服務程式

服務計算學習之路-開發 web 服務程式

開發 web 服務程式

文章目錄

簡介

開發簡單 web 服務程式 cloudgo,瞭解 web 伺服器工作原理。

開發環境

  • CentOS7
  • go 1.9.4 linux/amd64

Go的http包

使用http包編寫的簡單web伺服器

下面是一個簡單的web伺服器,實現在客戶端訪問http://127.0.0.1:9090/的時候響應內容為Hello World!

package main

import (
    "fmt"
    "net/http"
    "strings"
    "log"
)

func main() {
    http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
        w.Write([]byte("Hello World!"))
    })                                       //設定訪問的路由
err := http.ListenAndServe(":9090", nil) //設定監聽的埠 if err != nil { log.Fatal("ListenAndServe: ", err) } }

從上面的程式碼可以看到,要編寫一個Web伺服器很簡單,首先呼叫http.HandleFunc()設定路由和響應處理函式,呼叫http.ListenAndServe()去監聽埠,等待客戶端訪問即可。那http包又為我們做了什麼呢,接下來我將分析一下http包的程式碼執行流程。

http包有關路由部分

根據上面程式碼,首先是呼叫了http.HandleFunc()

,它的定義如下,實現了將傳入的處理響應函式與對應的path進行匹配。

// HandleFunc registers the handler function for the given pattern.
func (mux *ServeMux) HandleFunc(pattern string, handler func(ResponseWriter, *Request)) {
	mux.Handle(pattern, HandlerFunc(handler))
}

所以在Handle()函式中預設的路由是怎樣匹配的呢,先看下面的兩個struct,它們存放了預設的路由規則

type ServeMux struct {
	mu sync.RWMutex   //鎖機制,因為請求會涉及到併發處理
	m  map[string]muxEntry  //路由規則,使用map將string對應mux實體,這裡的string是註冊的路由表示式
	hosts bool        //是否在任意的規則中帶有host資訊
}
type muxEntry struct {
	explicit bool     //是否精確匹配
	h        Handler  //這個路由表示式對應的處理響應函式
	pattern  string   //匹配字串
}

根據http.HandleFunc()中的程式碼,執行了mux.Handle(),這個函式對傳入的path進行解析,然後向ServeMux中新增路由規則

// Handle registers the handler for the given pattern.
// If a handler already exists for pattern, Handle panics.
func (mux *ServeMux) Handle(pattern string, handler Handler) {
	mux.mu.Lock()
	defer mux.mu.Unlock()

	if pattern == "" {
		panic("http: invalid pattern " + pattern)
	}
	if handler == nil {
		panic("http: nil handler")
	}
	if mux.m[pattern].explicit {
		panic("http: multiple registrations for " + pattern)
	}

	if mux.m == nil {
		mux.m = make(map[string]muxEntry)
	}
    //增加一個新的匹配規則
    mux.m[pattern] = muxEntry{explicit: true, h: handler, pattern: pattern}
     
    //根據path的第一個字母判斷是否有host
    if pattern[0] != '/' {
        mux.hosts = true
    }
 
	// Helpful behavior:
	// If pattern is /tree/, insert an implicit permanent redirect for /tree.
	// It can be overridden by an explicit registration.
	n := len(pattern)
	if n > 0 && pattern[n-1] == '/' && !mux.m[pattern[0:n-1]].explicit {
		// If pattern contains a host name, strip it and use remaining
		// path for redirect.
		path := pattern
		if pattern[0] != '/' {
			// In pattern, at least the last character is a '/', so
			// strings.Index can't be -1.
			path = pattern[strings.Index(pattern, "/"):]
		}
		url := &url.URL{Path: path}
		mux.m[pattern[0:n-1]] = muxEntry{h: RedirectHandler(url.String(), StatusMovedPermanently), pattern: pattern}
	}
}

既然添加了路由規則,那麼如果客戶端進行訪問,是怎樣查詢到對應的路由規則呢,對過程mux.ServerHTTP->mux.Handler->mux.handler->mux.match進行追蹤,找到了路由匹配函式match()。這個函式的實現解釋了為什麼會匹配最長的最佳匹配,比如傳入/user/hh,不是先匹配/user/,而是匹配了/user/hh。

func (mux *ServeMux) match(path string) (h Handler, pattern string) {
    var n = 0
    for k, v := range mux.m {
        if !pathMatch(k, path) {
            continue
        }
        //如果匹配到了一個規則,還會繼續匹配並且判斷path的長度是否最長
        if h == nil || len(k) > n {
            n = len(k)
            h = v.h
            pattern = v.pattern
        }
    }
    return
}

http包有關監聽與服務部分

接下來就是執行http.ListenAndServe(),可以發現這個函式首先例項化了Server,接著呼叫了Server.ListenAndServe()

func ListenAndServe(addr string, handler Handler) error {
	server := &Server{Addr: addr, Handler: handler}
	return server.ListenAndServe()
}

ListenAndServe()函式呼叫了net.Listen("tcp",addr)監聽埠,接著呼叫了srv.Serve()

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)})
}

Server()函式啟動一個for迴圈,然後在迴圈體中Accept請求,對每個請求例項化一個Conn,並且開啟一個goroutine為這個請求進行服務go c.serve()。使用goroutines來處理Conn的讀寫事件,這樣每個請求都能保持獨立,相互不會阻塞,可以高效的響應網路事件,這樣使Go實現了高併發和高效能。

	for {
		rw, e := l.Accept()
		if e != nil {
			select {
			case <-srv.getDoneChan():
				return ErrServerClosed
			default:
			}
			if ne, ok := e.(net.Error); ok && ne.Temporary() {
				if tempDelay == 0 {
					tempDelay = 5 * time.Millisecond
				} else {
					tempDelay *= 2
				}
				if max := 1 * time.Second; tempDelay > max {
					tempDelay = max
				}
				srv.logf("http: Accept error: %v; retrying in %v", e, tempDelay)
				time.Sleep(tempDelay)
				continue
			}
			return e
		}
		tempDelay = 0
		c := srv.newConn(rw)
		c.setState(c.rwc, StateNew) // before Serve can return
		go c.serve(ctx)
	}

在for迴圈裡面,我們可以看到客戶端的每次請求都會建立一個Conn,這個Conn裡面儲存了該次請求的資訊,然後再傳遞到對應的handler,該handler中便可以讀取到相應的header資訊,這樣保證了每個請求的獨立性。

在為客戶端請求進行服務的c.serve()中,會讀取每個請求的內容w, err := c.readRequest(),並且判斷handler是否為空,如果沒有設定handler則為DefaultServeMux,然後呼叫handler的ServeHttp()

func (sh serverHandler) ServeHTTP(rw ResponseWriter, req *Request) {
    //這裡handler為簡單web伺服器程式碼中http.ListenAndServe中的第二個引數
    handler := sh.srv.Handler
    if handler == nil {
        //如果handler為空則使用DefaultServeMux進行處理
        handler = DefaultServeMux
    }
    if req.RequestURI == "*" && req.Method == "OPTIONS" {
        handler = globalOptionsHandler{}
    }
    //如果需要使用自定義的mux,就需要實現ServeHTTP方法也就是Handler介面
    handler.ServeHTTP(rw, req)
}

這裡需要注意的是Handler是一個介面,如一開始的web伺服器程式碼中,雖然我們並沒有實現ServeHTTP(),但是在http包裡面還定義了一個型別HandlerFunc,這個型別預設就實現了ServeHTTP(),在呼叫http.HandleFunc()的時候已經將自定義的handler處理函式強制轉為HandlerFunc型別

type HandlerFunc func(ResponseWriter, *Request)

// ServeHTTP calls f(w, r).
func (f HandlerFunc) ServeHTTP(w ResponseWriter, r *Request) {
	f(w, r)
}

所以以上就是http包的整個的程式碼執行過程,未完待續…