1. 程式人生 > >服務計算——開發簡單 web 服務程式 cloudgo

服務計算——開發簡單 web 服務程式 cloudgo

文章目錄

服務計算——cloudgo

1. 基本任務——搭建簡單web伺服器

1.1 框架選擇

本次使用的web開發框架是Martini,Martini 是一個非常新的 Go 語言的 Web 框架,使用 Go 的 net/http 介面開發,類似 Sinatra 或者 Flask 之類的框架,也可使用自己的 DB 層、會話管理和模板。這個框架在GitHub上都有中文的解釋以及用法,比較容易上手。

其特性如下:

  • 使用非常簡單
  • 無侵入設計
  • 可與其他 Go 的包配合工作
  • 超棒的路徑匹配和路由
  • 模組化設計,可輕鬆新增工具
  • 大量很好的處理器和中介軟體
  • 很棒的開箱即用特性
  • 完全相容 http.HandlerFunc 介面

簡單例子:

package main
 
import "github.com/codegangsta/martini"
 
func main() {
  m := martini.Classic()
  m.Get("/", func() string {
    return "Hello world!"
  }
) m.Run() }

請求處理器:

m.Get("/", func() {
  println("hello world")
})
 
m.Get("/", func(res http.ResponseWriter, req *http.Request) { // res and req are injected by Martini
  res.WriteHeader(200) // HTTP 200
})

1.2 程式碼

1.2.1 main.go

main.go檔案使用了老師部落格中給出的程式碼,完成繫結埠解析埠啟動server完成操作的任務

package main
import (
    "os"
    "web/service"
    flag "github.com/spf13/pflag"
)
const (
    //預設8080埠
    PORT string = "8080" 
)
func main() {
    //預設8080埠
    port := os.Getenv("PORT") 
    if len(port) == 0 {
        port = PORT
    }
    //埠號的解析
    pPort := flag.StringP("port", "p", PORT, "PORT for httpd listening")
    flag.Parse()
    if len(*pPort) != 0 {
        port = *pPort
    }
    //啟動server
    service.NewServer(port)
}
1.2.2 server.go

server.go檔案則是使用martini框架中的函式格式具體定義main.go檔案中啟動server後要具體進行的操作

package service
import (
    "github.com/go-martini/martini" 
)
func NewServer(port string) {   
    m := martini.Classic()

    m.Get("/", func(params martini.Params) string {
        return "hello world"
    })

    m.RunOnAddr(":"+port)   
}

1.3 伺服器測試

1.3.1 執行伺服器輸出helloworld

截圖:

在這裡插入圖片描述


1.3.2 curl 測試

截圖:

在這裡插入圖片描述

提示資訊:

$ curl -v http://localhost:9090/  
  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
  0     0    0     0    0     0      0      0 --:--:-- --:--:-- --:--:--     0*  Trying ::1...
* TCP_NODELAY set
* Connected to localhost (::1) port 9090 (#0)
> GET / HTTP/1.1
> Host: localhost:9090
> User-Agent: curl/7.61.1
> Accept: */*
>
< HTTP/1.1 200 OK
< Date: Thu, 15 Nov 2018 12:07:26 GMT
< Content-Length: 11
< Content-Type: text/plain; charset=utf-8
<
{ [11 bytes data]
100    11  100    11    0     0    354      0 --:--:-- --:--:-- --:--:--   354hello world
* Connection #0 to host localhost left intact


1.3.3 ab測試

截圖:

在這裡插入圖片描述

提示資訊:

$ ./ab -n 1000 -c 100 http://localhost:9090/
This is ApacheBench, Version 2.3 <$Revision: 1843412 $>
Copyright 1996 Adam Twiss, Zeus Technology Ltd, http://www.zeustech.net/
Licensed to The Apache Software Foundation, http://www.apache.org/

Benchmarking localhost (be patient)
Completed 100 requests
Completed 200 requests
Completed 300 requests
Completed 400 requests
Completed 500 requests
Completed 600 requests
Completed 700 requests
Completed 800 requests
Completed 900 requests
Completed 1000 requests
Finished 1000 requests


Server Software:
Server Hostname:        localhost
Server Port:            9090

Document Path:          /
Document Length:        11 bytes

Concurrency Level:      100
Time taken for tests:   0.271 seconds
Complete requests:      1000
Failed requests:        0
Total transferred:      128000 bytes
HTML transferred:       11000 bytes
Requests per second:    3692.76 [#/sec] (mean)
Time per request:       27.080 [ms] (mean)
Time per request:       0.271 [ms] (mean, across all concurrent requests)
Transfer rate:          461.60 [Kbytes/sec] received

Connection Times (ms)
              min  mean[+/-sd] median   max
Connect:        0    0   0.4      0       1
Processing:     4   25   3.9     26      31
Waiting:        2   15   6.7     14      27
Total:          4   25   3.9     26      31

Percentage of the requests served within a certain time (ms)
  50%     26
  66%     26
  75%     26
  80%     27
  90%     27
  95%     27
  98%     28
  99%     28
 100%     31 (longest request)

2. 拓展任務 net/http 原始碼閱讀與關鍵功能解釋

總的來說,Golang的HTTP框架可以由下圖簡略表示:

在這裡插入圖片描述

2.1 HTTP的處理流程

理解 HTTP 構建的網路應用只要關注兩個端—客戶端(clinet)服務端(server),兩個端的互動來自 client 的 request,以及server端的response。所謂的http伺服器,主要在於如何接受 client 的 request,並向client返回response

接收request的過程中,最重要的莫過於路由(router),即實現一個Multiplexer器。Go中既可以使用內建的mutilplexer — DefautServeMux,也可以自定義。Multiplexer路由的目的就是為了找到處理器函式(handler),後者將對request進行處理,同時構建response

簡單總結為如下流程:

Client -> Requests ->  [Multiplexer(router) -> Handler  -> Response -> Client

2.2 Handler

理解go中的http服務,最重要就是要理解MultiplexerHandler,Golang中的Multiplexer基於ServeMux結構,同時也實現了Handler介面。

Handler 可以有以下幾種型別:

如圖:

在這裡插入圖片描述

  • handler函式: 具有func(w http.ResponseWriter, r *http.Requests)簽名的函式
  • handler處理器(函式): 經過HandlerFunc結構包裝的handler函式,它實現了ServeHTTP介面方法的函式。呼叫handler處理器的ServeHTTP方法時,即呼叫handler函式本身。
  • handler物件:實現了Handler介面ServeHTTP方法的結構。

handler處理器handler物件的差別在於,一個是函式,另外一個是結構,它們都有實現了ServeHTTP方法。

Golang沒有繼承,類多型的方式可以通過介面實現。所謂介面則是定義聲明瞭函式簽名,任何結構只要實現了與介面函式簽名相同的方法,就等同於實現了介面。go的HTTP服務都是基於handler進行處理。

type Handler interface {
    ServeHTTP(ResponseWriter, *Request)
}

任何結構體,只要實現了ServeHTTP方法,這個結構就可以稱之為handler物件。ServeMux會使用handler並呼叫其ServeHTTP方法處理請求並返回響應。


2.3 ServerMux

ServeMux的原始碼:

type ServeMux struct {
    mu    sync.RWMutex
    m     map[string]muxEntry
    hosts bool 
}

type muxEntry struct {
    explicit bool
    h        Handler
    pattern  string
}

ServeMux結構中最重要的欄位為m,這是一個map,key是一些url模式,value是一個muxEntry結構,後者裡定義儲存了具體的url模式和handler。


2.4 Server

http.ListenAndServe的原始碼可以看出,Server建立了一個server物件,並呼叫server物件的ListenAndServe方法:

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

Server 的結構如下:

type Server struct {
    Addr         string        
    Handler      Handler       
    ReadTimeout  time.Duration 
    WriteTimeout time.Duration 
    TLSConfig    *tls.Config   

    MaxHeaderBytes int

    TLSNextProto map[string]func(*Server, *tls.Conn, Handler)

    ConnState func(net.Conn, ConnState)
    ErrorLog *log.Logger
    disableKeepAlives int32     nextProtoOnce     sync.Once 
    nextProtoErr      error     
}

server結構儲存了伺服器處理請求常見的欄位。其中Handler欄位也保留Handler介面。如果Server介面沒有提供Handler結構物件,那麼會使用DefautServeMuxmultiplexer.

2.5 建立HTTP服務

建立一個http服務,大致需要經歷兩個過程,

  • 註冊路由,即提供url模式和handler函式的對映
  • 例項化一個server物件,並開啟對客戶端的監聽
2.5.1 註冊路由

net/http包暴露的註冊路由的api很簡單,http.HandleFunc選取了DefaultServeMux作為multiplexer

func HandleFunc(pattern string, handler func(ResponseWriter, *Request)) {
    DefaultServeMux.HandleFunc(pattern, handler)
}

DefaultServeMuxServeMux的一個例項。http包也提供了NewServeMux方法建立一個ServeMux例項,預設則建立一個DefaultServeMux

// NewServeMux allocates and returns a new ServeMux.
func NewServeMux() *ServeMux { return new(ServeMux) }

// DefaultServeMux is the default ServeMux used by Serve.
var DefaultServeMux = &defaultServeMux

var defaultServeMux ServeMux

DefaultServeMuxHandleFunc(pattern, handler)方法實際是定義在ServeMux下的。


func (mux *ServeMux) HandleFunc(pattern string, handler func(ResponseWriter, *Request)) {
	mux.Handle(pattern, HandlerFunc(handler))
}

上述程式碼中,HandlerFunc是一個函式型別。同時實現了Handler介面的ServeHTTP方法。使用HandlerFunc型別包裝一下路由定義的indexHandler函式,其目的就是為了讓這個函式也實現ServeHTTP方法,即轉變成一個handler處理器(函式)。一旦這樣做了,就意味著我們的 indexHandler函式也有了ServeHTTP方法。此外,ServeMuxHandle方法,將會對patternhandler函式做一個map對映。


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}

    if pattern[0] != '/' {
        mux.hosts = true
    }

    n := len(pattern)
    if n > 0 && pattern[n-1] == '/' && !mux.m[pattern[0:n-1]].explicit {

        path := pattern
        if pattern[0] != '/' {
            path = pattern[strings.Index(pattern, "/"):]
        }
        url := &url.URL{Path: path}
        mux.m[pattern[0:n-1]] = muxEntry{
            h: RedirectHandler(url.String(),StatusMovedPermanently), pattern: pattern
        }
	}
}    

由此可見,Handle函式的主要目的在於把handlerpattern模式繫結到map[string]muxEntrymap上,其中muxEntry儲存了更多patternhandler的資訊,前面討論的Server結構的m欄位就是map[string]muxEntry這樣一個map。 此時,pattern和handler的路由註冊完成。

2.5.2 開始監聽

開始server的監聽,以接收客戶端的請求。

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

Server的ListenAndServe方法中,會初始化監聽地址Addr,同時呼叫Listen方法設定監聽。最後將監聽的TCP物件傳入Serve方法。


2.5.3 處理請求

監聽開啟之後,一旦客戶端請求到底,go就開啟一個協程處理請求,主要邏輯都在serve方法之中。 serve方法比較長,其主要職能就是,建立一個上下文物件,然後呼叫Listener的Accept方法用來獲取連線資料並使用newConn方法建立連線物件。最後使用goroutein協程的方式處理連線請求。因為每一個連線都開起了一個協程,請求的上下文都不同,同時又保證了go的高併發。

goserver方法如下:

func (c *conn) serve(ctx context.Context) {
	c.remoteAddr = c.rwc.RemoteAddr().String()
	defer func() {
		if err := recover(); err != nil {
			const size = 64 << 10
            buf := make([]byte, size)
            buf = buf[:runtime.Stack(buf, false)]
            c.server.logf("http: panic serving %v: %v\n%s", c.remoteAddr, err, buf)
		}
		if !c.hijacked() {
			c.close()
			c.setState(c.rwc, StateClosed)
		}
	}()

...

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)
    }
    ...
    
    }
    
    ...
  
    serverHandler{c.server}.ServeHTTP(w, w.req)
    w.cancelCtx()
    if c.hijacked() {
        return
    }
    w.finishRequest()
    if !w.shouldReuseConnection() {
        if w.requestBodyLimitHit || w.closedRequestBodyEarly() {
            c.closeWriteAndWait()
        }
        return
    }
    c.setState(c.rwc, StateIdle)
}

defer定義了函式退出時,連線關閉相關的處理。然後就是讀取連線的網路資料,並處理讀取完畢時候的狀態。接下來就是呼叫serverHandler{c.server}.ServeHTTP(w, w.req)方法處理請求了。最後就是請求處理完畢的邏輯。serverHandler是一個重要的結構,它具有一個欄位,即Server結構,同時它也實現了Handler介面方法ServeHTTP,並在該介面方法中做了一個重要的事情,初始化multiplexer路由多路複用器。如果server物件沒有指定Handler,則使用預設的DefaultServeMux作為路由Multiplexer,並呼叫初始化Handler的ServeHTTP方法。


func (mux *ServeMux) (w ResponseWriter, r Request) {
    if r.RequestURI == "" {
    	if r.ProtoAtLeast(1, 1) {
    		w.Header().Set("Connection", "close")
    	}
    	w.WriteHeader(StatusBadRequest)
    	return
    }
    h, _ := mux.Handler(r)
    h.ServeHTTP(w, r)
}

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

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
    }

    func (mux *ServeMux) match(path string) (h Handler, pattern string) {
    	var n = 0
    	for k, v := range mux.m {
    		if !pathMatch(k, path) {
    			continue
    		}
    	if h == nil || len(k) > n {
    		n = len(k)
    		h = v.h
    		pattern = v.pattern
    	}
    }
    return
}

mux的ServeHTTP方法通過呼叫其Handler方法尋找註冊到路由上的handler函式,並呼叫該函式的ServeHTTP方法,本例則是IndexHandler函式。 mux的Handler方法對URL簡單的處理,然後呼叫handler方法,後者會建立一個鎖,同時呼叫match方法返回一個handler和pattern。 在match方法中,mux的m欄位是map[string]muxEntry圖,後者儲存了pattern和handler處理器函式,因此通過迭代m尋找出註冊路由的patten模式與實際url匹配的handler函式並返回。 返回的結構一直傳遞到mux的ServeHTTP方法,接下來呼叫handler函式的ServeHTTP方法,即IndexHandler函式,然後把response寫到http.RequestWirter物件返回給客戶端。 上述函式執行結束即serverHandler{c.server}.ServeHTTP(w, w.req)執行結束。接下來就是對請求處理完畢之後上希望和連線斷開的相關邏輯。


3. 原始碼與Readme

Github


4. 參考文獻