服務計算——開發簡單 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服務,最重要就是要理解Multiplexer
和Handler
,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
結構物件,那麼會使用DefautServeMux
做multiplexer
.
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)
}
DefaultServeMux
是ServeMux
的一個例項。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
DefaultServeMux
的HandleFunc(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
方法。此外,ServeMux
的Handle
方法,將會對pattern
和handler
函式做一個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函式
的主要目的在於把handler
和pattern
模式繫結到map[string]muxEntry
的map
上,其中muxEntry
儲存了更多pattern
和handler
的資訊,前面討論的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)
執行結束。接下來就是對請求處理完畢之後上希望和連線斷開的相關邏輯。