來我們探究一下net/http 的程式碼流程

小魔童哪吒發表於2021-06-03
[TOC]

net/http 是什麼?

是GO的其中一個標準庫,用於Web應用的開發,使用這個庫,可以讓開發變得更加迅速和簡便,且易於上手。

那麼問題來了

使用庫,確實方便,無腦調介面,拼拼湊湊能跑就行,管他效率效能,出了問題,刪庫跑路就行了。。。

實際真的是這個樣子嗎?作為一個開發,一定要想辦法弄明白不清楚的事情,要弄明白用到工具的原理,更需要清晰的知道自己開發產品的運作原理,正所謂

知其然,而不知其所以然,欲摹寫其情狀,而心不能自喻,口不能自宣,筆不能自傳。

我們對於技術要有探索精神,對程式碼要有敬畏之心,那今天我們們就來看看net/http的程式碼流程吧

使用框架/庫,必要要接受其自身的一套約定和模式,我們必須要了解和熟悉這些約定和模式的用法,否則就會陷入用錯了都不知道的境地。

在GOLANG中,net/http的組成部分有客戶端服務端

庫中的結構和函式有的只支援客戶端和伺服器這兩者中的一個,有的同時支援客戶端和伺服器,用圖說話:

  • 只支援客戶端的

Client , response

  • 只支援服務端的

ServerMux,Server ,ResponseWriter,Handler 和 HandlerFunc

  • 客戶端,服務端都支援的

Header , Request , Cookie

net/http構建伺服器也很簡單,大體框架如下:

客戶端 請求 伺服器,伺服器裡面使用 net/http包,包中有多路複用器,和對應多路複用器的介面,伺服器中的多個處理器處理不同的請求,最終需要落盤的資料即入庫

萬里長城第一步,我們發車了

開始寫一個簡單的Request

package main

import (
   "fmt"
   "net/http"
)

func main() {

   http.HandleFunc("/Hi", func(w http.ResponseWriter, r *http.Request) {
       // <h1></h1> 是html 標籤
      w.Write([]byte("<h1>Hi xiaomotong</h1>"))
   })

   // ListenAndServe 不寫ip 預設伺服器地址是 127.0.0.1
   if err := http.ListenAndServe(":8888", nil); err != nil {
      fmt.Println("http server error:", err)
   }

}

執行伺服器程式碼,在瀏覽器中輸入127.0.0.1:8888/Hi或者localhost:8888/Hi,即可看到效果

建立一個Go寫的伺服器就是那麼簡單,只要呼叫ListenAndServe並傳入網路地址,埠,處理請求的處理器(handler)即可。

注意:

  • 如果網路地址引數為空字串,那麼伺服器預設使用80埠進行網路連線
  • 如果處理器引數為nil,那麼伺服器將使用預設的多路複用器DefaultServeMux

可是實際上http是如何建立起來的呢?一頓操作猛如虎,一問細節二百五

HTTP的建立過程

HTTP的建立流程都是通用的,因為他是標準協議。寫C/C++的時候,這些流程基本上自己都要去寫一遍,但是寫GO的時候,標準庫裡面已經封裝好了,因此才會有上述一個函式就可以寫一個web伺服器的情況

服務端涉及的流程

  • socket建立套接字
  • bind繫結地址和埠
  • listen設定最大監聽數
  • accept開始阻塞等待客戶端的連線
  • read讀取資料
  • write回寫資料
  • close 關閉

客戶端涉及的流程

  • socket建立套接字
  • connect 連線服務端
  • write寫資料
  • read讀取資料

那麼資料在各個層級之間是如何走的呢

還是那個熟悉的7層OSI模型,不過實際應用的話,我們用TCP/IP 5層模型

上述TCP/IP五層模型,可能會用到的協議大體列一下

  • 應用層:

    HTTP協議,SMTP,SNMP,FTP,Telnet,SIP,SSH,NFS,RTSP

  • 傳輸層

比較常見的協議是TCP,UDP,SCTP,SPX,ATP等

UDP不可靠, SCTP有自己特殊的運用場景, 所以一般情況下HTTP是由TCP協議進行傳輸

不過企業應用的話,會將UDP改造成可靠的傳輸,實際上是對標準udp上封裝自定義的頭,模擬TCP的可靠傳輸,三次握手, 四次揮手就是在這裡發生的

  • 網路層

IP協議、ICMP,IGMP,IPX,BGP,OSPF,RIP,IGRP,EIGRP,ARP,RARP協議 ,等等

  • 資料鏈路層

Ethernet , PPP,WiFi ,802.11等等

  • 物理層

SO2110,IEEE802 等等

知道HTTP的通用流程,那麼我們來具體看看net/http標準庫是如何實現這整個流程的,先從建立socket看起

net/http 建立socket

還記得最上面說到的request小案例嗎?我們可以從這裡開始入手

package main

import (
   "fmt"
   "net/http"
)

func main() {

   http.HandleFunc("/Hi", func(w http.ResponseWriter, r *http.Request) {
      w.Write([]byte("<h1>Hi xiaomotong</h1> "))
   })

   if err := http.ListenAndServe(":8888", nil); err != nil {
      fmt.Println("http server error:", err)
   }
}

http.HandleFunc(“/Hi”, func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte(“


})

HandleFunc這一段是註冊路由,這個路由的handler會預設放到到DefaultServeMux

// HandleFunc registers the handler function for the given pattern
// in the DefaultServeMux.
// The documentation for ServeMux explains how patterns are matched.
func HandleFunc(pattern string, handler func(ResponseWriter, *Request)) {
   DefaultServeMux.HandleFunc(pattern, handler)
}

HandleFunc 實際上是呼叫了 ServeMux服務的HandleFunc

// HandleFunc registers the handler function for the given pattern.
func (mux *ServeMux) HandleFunc(pattern string, handler func(ResponseWriter, *Request)) {
   if handler == nil {
      panic("http: nil handler")
   }
   mux.Handle(pattern, HandlerFunc(handler))
}

ServeMux服務的HandleFunc呼叫了自己服務的Handle 實現

// 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")
   }
   if handler == nil {
      panic("http: nil handler")
   }
   if _, exist := mux.m[pattern]; exist {
      panic("http: multiple registrations for " + pattern)
   }

   if mux.m == nil {
      mux.m = make(map[string]muxEntry)
   }
   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
   }
}

看了實際的註冊路由實現還是比較簡單,我們先不再深入的網下看,要是感興趣的可以接著這裡往下追具體的資料結構

目前的註冊路由的流程是:

  • http.HandleFunc ->
  • (mux *ServeMux) HandleFunc ->
  • (mux *ServeMux) Handle

net/http監聽埠+響應請求

那我們在來看看剛才request案例裡面的監聽地址和埠的程式碼是如何走的

if err := http.ListenAndServe(“:8888”, nil); err != nil {
fmt.Println(“http server error:”, err)
}

經過上面的三個函式流程,已經知道註冊路由是如何走的了,那麼ListenAndServe這個函式的監聽已經handler處理資料後的響應是如何實現的呢?來我們繼續

ListenAndServe偵聽TCP網路地址addr,然後呼叫handler來處理傳入連線的請求,收的連線配置為啟用TCP keep-alive,該引數通常為nil,在這種情況下使用DefaultServeMux,上面提過一次,此處再次強調

// ListenAndServe listens on the TCP network address addr and then calls
// Serve with handler to handle requests on incoming connections.
// Accepted connections are configured to enable TCP keep-alives.
//
// The handler is typically nil, in which case the DefaultServeMux is used.
//
// ListenAndServe always returns a non-nil error.
func ListenAndServe(addr string, handler Handler) error {
   server := &Server{Addr: addr, Handler: handler}
   return server.ListenAndServe() // 呼叫Server服務的 ListenAndServe函式 (srv *Server) ListenAndServe
}
// ListenAndServe listens on the TCP network address srv.Addr and then
// calls Serve to handle requests on incoming connections.
// Accepted connections are configured to enable TCP keep-alives.
//
// If srv.Addr is blank, ":http" is used.
//
// ListenAndServe always returns a non-nil error. After Shutdown or Close,
// the returned error is ErrServerClosed.
func (srv *Server) ListenAndServe() error {
   if srv.shuttingDown() {
      return ErrServerClosed
   }
   addr := srv.Addr
   if addr == "" {
      addr = ":http"
   }
   ln, err := net.Listen("tcp", addr) //實際是通過 net.Listen 進行監聽地址和埠的
   if err != nil {
      return err
   }
   return srv.Serve(ln)
}
func Listen(network, address string) (Listener, error) {
   var lc ListenConfig
   return lc.Listen(context.Background(), network, address)
}
func (srv *Server) Serve(l net.Listener) error {
   if fn := testHookServerServe; fn != nil {
      fn(srv, l) // call hook with unwrapped listener // 回撥函式的呼叫的位置
   }

 // ...此處省略15行程式碼...

   var tempDelay time.Duration // how long to sleep on accept failure   // accept阻塞失敗睡眠的間隔時間

   ctx := context.WithValue(baseCtx, ServerContextKey, srv)
   for {
       // 開始Accept 阻塞監聽客戶端的連線
      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, runHooks) // before Serve can return
      go c.serve(connCtx)    // 此處開一個協程來處理具體的請求訊息
   }
}

此處通過 go c.serve(connCtx) 開啟一個協程專門處理具體的請求訊息

// Serve a new connection.
func (c *conn) serve(ctx context.Context) {
   c.remoteAddr = c.rwc.RemoteAddr().String()
   ctx = context.WithValue(ctx, LocalAddrContextKey, c.rwc.LocalAddr())
   //  ... 此處省略部分程式碼
    // HTTP cannot have multiple simultaneous active requests.[*]
    // Until the server replies to this request, it can't read another,
    // so we might as well run the handler in this goroutine.
    // [*] Not strictly true: HTTP pipelining. We could let them all process
    // in parallel even if their responses need to be serialized.
    // But we're not going to implement HTTP pipelining because it
    // was never deployed in the wild and the answer is HTTP/2.
    //HTTP不能同時有多個活動請求。[*],直到伺服器響應這個請求,它不能讀取另一個
    serverHandler{c.server}.ServeHTTP(w, w.req)  // ServeHTTP 是重點
    w.cancelCtx()
    if c.hijacked() {
        return
    }
    w.finishRequest()
    //  ... 此處省略部分程式碼
}

此處ServeHTTP相當重要

// serverHandler delegates to either the server's Handler or
// DefaultServeMux and also handles "OPTIONS *" requests.
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)
}
// ServeHTTP dispatches the request to the handler whose
// pattern most closely matches the request URL.
func (mux *ServeMux) ServeHTTP(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)
}

(sh serverHandler) ServeHTTP 呼叫 (mux *ServeMux) ServeHTTP , ServeHTTP將請求傳送給處理程式 h, _ := mux.Handler(r)

原始碼看到這裡,對於net/http標準庫 對於註冊路由,監聽服務端地址和埠的流程,大致清楚了吧

整個過程,net/http基本上是提供了 HTTP流程的整套服務,可以說是非常的香了, 整個過程基本上是這個樣子的

  • net.Listen 做了初始化 套接字 socket,bind 繫結ip 和埠,listen 設定最大監聽數量的 操作
  • Accept 進行阻塞等待客戶端的連線
  • go c.serve(ctx) 啟動新的協程來處理當前的請求. 同時主協程繼續等待其他客戶端的連線, 進行高併發操作
  • mux.Handler獲取註冊的路由, 然後拿到這個路由的handler 處理器, 處理客戶端的請求後,返回給客戶端結果

關於底層是如何封包解包,位元組是如何偏移的,ipv4,ipv6如何去處理的,有興趣的朋友們可以順著程式碼繼續追,歡迎多多溝通交流

好了,本次就到這裡,下一次是 gin的路由演算法分享

技術是開放的,我們的心態,更應是開放的。擁抱變化,向陽而生,努力向前行。

我是小魔童哪吒,歡迎點贊關注收藏,下次見~

本作品採用《CC 協議》,轉載必須註明作者和本文連結

相關文章