Go 實現獨立的 Web 服務器(一)
Go 實現獨立的 Web 服務器
一. Web 服務器
說起 web 服務器,相信大家都比較熟悉,比如 Nginx、Apache、Tomcat 等,通過代理或者反向代理方式爲用戶提供服務。如果使用這些組件,則需要部署 Web 服務器、項目代碼等,而且相關配置等一堆,還是比較麻煩的,而且很多功能需要基於網關開發或者在項目代碼中支持開發等。那麼如果脫離這些 Web 服務器,我們是否可以實現一個 Web 服務器,完全自我可控?
二. Go 實現 Web 服務
Golang 本身提供了一個比較完善的 Http 服務的內置包,在業務開發中,只需要在此包基礎上就可以實現一個功能豐富、強大的 web 服務器。
2.1 Golang 標準庫:net/http
net/http 庫實現了整套的 http 服務中的客戶端、服務端接口,可以基於此輕鬆的發起 HTTP 請求或者對外提供 HTTP 服務。本期主要介紹基於此包實現對外提供 HTTP 服務。
server 基本介紹
server 服務的基本信息
type Server struct {
Addr string // 定義服務監聽的地址端口,如果爲空,則默認監聽80端口
Handler Handler // 請求被處理的業務方,默認 http.DefaultServeMux
TLSConfig *tls.Config // 可選的TLS配置,對外提供https服務
ReadTimeout time.Duration // 讀取客戶端請求的超時時間,包含讀取請求體
ReadHeaderTimeout time.Duration // 讀取請求頭的超時時間,如果爲空,則使用 ReadTimeout, 如果兩者都沒有,則沒有超時時間
WriteTimeout time.Duration // 服務響應的超時時間
IdleTimeout time.Duration // 長鏈接空閒的超時時間
MaxHeaderBytes int // 客戶端請求頭的最大大小,默認爲1MB
ConnState func(net.Conn, ConnState) // 指定可選的回調方法,當客戶端連接狀態發生改變時
ErrorLog *log.Logger // 連接錯誤、handlers異常或者文件系統異常時使用,默認使用標準庫的logger接口
onShutdown []func() // 服務停止時觸發的方法調用
}
基於以上 server 結構,Golang 標準庫提供瞭如下幾個服務接口
func (srv *Server) Close() error // 立即關閉所有的活躍監聽以及所有的連接,包括新建的連接、活躍的或者空閒的連接
func (srv *Server) ListenAndServe() error // 啓動服務監聽tcp連接以及將請求轉發到handler中
func (srv *Server) ListenAndServeTLS(certFile, keyFile string) error // 支持https服務
func (srv *Server) Shutdown(ctx context.Context) error // 實現優雅關閉連接
2.2 啓動首個 web 服務示例
2.2.1 Web 服務示例
最簡單的 Server:
package main
import (
"fmt"
"net/http"
)
func helloWorldHandler(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "Hello world\n")
}
func main() {
http.HandleFunc("/", helloWorldHandler)
srv := http.Server{
Addr: ":9090",
}
er := srv.ListenAndServe()
if er != nil {
panic("Start Server Failed With " + er.Error())
}
}
啓動服務:
go run http_be.go
請求
curl -i -X 'GET' 'http://127.0.0.1:9090/'
HTTP/1.1 200 OK
Date: Sun, 25 Jul 2021 05:43:52 GMT
Content-Length: 12
Content-Type: text/plain; charset=utf-8
Hello world
僅僅 20 行代碼就實現了一個 web 服務器,那麼這中間都發生了什麼事情呢?
2.2.2 起源
世界萬物的起源都來自於一點,如同單細胞生物進化到現如今五彩斑斕的世界。Web 服務的起源也來自於一點,即
srv := http.Server{
Addr: ":9090",
}
在此處初始化 server 服務,除顯示指定監聽端口外,還有一個重要的默認 handler 參數,即 http.DefaultServeMux ,提供 web 服務的路由解析功能。即
http.HandleFunc("/", helloWorldHandler)
以上代碼其實等同於如下:
hdler := http.DefaultServeMux
hdler.HandleFunc("/", helloWorldHandler)
srv := http.Server{
Addr: ":9090",
Handler: hdler,
}
http 的 mux 維護了一個路由解析的 map 表,服務在啓動時,所有的請求路由會被解析到這個 map 表中,其結構體爲:
type ServeMux struct {
mu sync.RWMutex
m map[string]muxEntry
es []muxEntry // slice of entries sorted from longest to shortest.
hosts bool // whether any patterns contain hostnames
}
type muxEntry struct {
h Handler
pattern string
}
其中 muxEntry 中存儲着路由的路徑以及對應的處理方法 handler。基於此 Mux 接口,還可以實現更加複雜的路由協議。
2.2.3 請求的處理
er := srv.ListenAndServe()
通過以上簡單的一句代碼,就實現了服務的監聽以及服務,那麼他是如何做到的呢?帶着疑問,又一次進入如海般的代碼中尋找代碼。
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)
}
func (srv *Server) Serve(l net.Listener) error {
if fn := testHookServerServe; fn != nil {
fn(srv, l) // call hook with unwrapped listener
}
origListener := l
l = &onceCloseListener{Listener: l}
defer l.Close()
if err := srv.setupHTTP2_Serve(); err != nil {
return err
}
if !srv.trackListener(&l, true) {
return ErrServerClosed
}
defer srv.trackListener(&l, false)
baseCtx := context.Background()
if srv.BaseContext != nil {
baseCtx = srv.BaseContext(origListener)
if baseCtx == nil {
panic("BaseContext returned a nil context")
}
}
var tempDelay time.Duration // how long to sleep on accept failure
ctx := context.WithValue(baseCtx, ServerContextKey, srv)
for {
rw, err := l.Accept()
if err != nil {
select {
case <-srv.getDoneChan():
return ErrServerClosed
default:
}
if ne, ok := err.(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", err, tempDelay)
time.Sleep(tempDelay)
continue
}
return err
}
connCtx := ctx
if cc := srv.ConnContext; cc != nil {
connCtx = cc(connCtx, rw)
if connCtx == nil {
panic("ConnContext returned nil")
}
}
tempDelay = 0
c := srv.newConn(rw)
c.setState(c.rwc, StateNew) // before Serve can return
go c.serve(connCtx)
}
}
可以看出,Golang 接收 web 是基於 TCP 協議之上,然後就是調用 Serve 方法,處理連接請求,Serve 方法會啓動 goroutine 進行異步處理,這也是高併發的基本所在。
宏觀層面查看大致流程如圖:
serve 方法中以無限循環方式 (for) 接收客戶端請求並進行處理,主要邏輯如下:
if tlsConn, ok := c.rwc.(*tls.Conn); ok {
if d := c.server.ReadTimeout; d != 0 {
c.rwc.SetReadDeadline(time.Now().Add(d))
}
if d := c.server.WriteTimeout; d != 0 {
c.rwc.SetWriteDeadline(time.Now().Add(d))
}
if err := tlsConn.Handshake(); err != nil {
// If the handshake failed due to the client not speaking
// TLS, assume they're speaking plaintext HTTP and write a
// 400 response on the TLS conn's underlying net.Conn.
if re, ok := err.(tls.RecordHeaderError); ok && re.Conn != nil && tlsRecordHeaderLooksLikeHTTP(re.RecordHeader) {
io.WriteString(re.Conn, "HTTP/1.0 400 Bad Request\r\n\r\nClient sent an HTTP request to an HTTPS server.\n")
re.Conn.Close()
return
}
c.server.logf("http: TLS handshake error from %s: %v", c.rwc.RemoteAddr(), err)
return
}
c.tlsState = new(tls.ConnectionState)
*c.tlsState = tlsConn.ConnectionState()
if proto := c.tlsState.NegotiatedProtocol; validNextProto(proto) {
if fn := c.server.TLSNextProto[proto]; fn != nil {
h := initALPNRequest{ctx, tlsConn, serverHandler{c.server}}
fn(c.server, tlsConn, h)
}
return
}
}
...
serverHandler{c.server}.ServeHTTP(w, w.req)
其中 ServeHTTP 的實現:
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)
}
在這裏終於發現我們定義的路由處理的 handler 方法。
整體流程如圖:
通過以上源碼的觀看,基本瞭解了 golang 處理一次 web 請求的大體過程。對此,你是否已經有所瞭解?
本文首次初步探索 Golang 的 web 服務的大體過程,提供一個功能強大的 web 服務這纔是初步探索,還要有路由解析、中間件等更多的組件進行封裝,敬請期待後續的探索研究。
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/DiJYnoZ4mO4iATDGgxEApg