Глубокое понимание http-сервера Golang

Go
Глубокое понимание http-сервера Golang

предисловие

Для Golang реализация простогоhttp serverОчень просто, всего несколько строк кода. В то же время, с благословения сопрограмм, Go реализуетhttp serverМожет добиться очень хорошей производительности. В этой статье мы рассмотрим стандартную библиотеку go.net/httpУглубленное изучение принципа реализации http-сервисов, чтобы научиться понимать общие парадигмы и идеи дизайна сетевого программирования.

HTTP-сервис

Сетевое приложение, построенное на основе HTTP, включает в себя два конца, а именно клиент (Client) и сервер (Server). Взаимодействие между двумя концами включает отправку от клиентаrequest, сервер принимаетrequestобработать и вернутьresponseи обработка на стороне клиентаresponse. Таким образом, работа http-сервера заключается в том, чтобы принятьrequest, и вернуться к клиентуresponse.

Поток обработки типичного http-сервера можно представить на следующем рисунке:

Когда сервер получает запрос, он сначала входит в маршрут (router),ЭтоMultiplexer, работа маршрутизации для этогоrequestНайдите соответствующий процессор (handler), пара процессоровrequestобрабатывать и строитьresponse. Реализовано Голангомhttp serverСледуйте тому же процессу обработки.

Давайте сначала посмотрим, как Golang реализует простойhttp server:

package main

import (
    "fmt"
    "net/http"
)

func indexHandler(w http.ResponseWriter, r *http.Request) {
    fmt.Fprintf(w, "hello world")
}

func main() {
    http.HandleFunc("/", indexHandler)
    http.ListenAndServe(":8000", nil)
}

После запуска кода откройте его в браузереlocalhost:8000ты можешь видетьhello world. Сначала используйте этот кодhttp.HandleFuncмаршрут в корне/зарегистрировалindexHandler, а затем используйтеhttp.ListenAndServeВключите мониторинг. Когда приходит запрос, по маршруту выполняется соответствующий маршрут.handlerфункция.

Рассмотрим еще один распространенныйhttp serverМетод реализации:

package main

import (
    "fmt"
    "net/http"
)

type indexHandler struct {
    content string
}

func (ih *indexHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
    fmt.Fprintf(w, ih.content)
}

func main() {
    http.Handle("/", &indexHandler{content: "hello world!"})
    http.ListenAndServe(":8001", nil)
}

Реализовано GohttpШаги службы очень просты, сначала зарегистрируйте маршрут, затем создайте службу и начните слушать. Далее мы узнаем, как Golang реализует этапы регистрации маршрутов, запуска сервисов и обработки запросов.httpСлужить.

зарегистрировать маршрут

http.HandleFuncиhttp.HandleОба используются для регистрации маршрутов, можно обнаружить, что разница между ними заключается во втором параметре.func(w http.ResponseWriter, r *http.Requests)подписанная функция, представляющая собой структуру, реализующуюfunc(w http.ResponseWriter, r *http.Requests)Подписанный метод.
http.HandleFuncиhttp.HandleИсходный код выглядит следующим образом:

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

// 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))
}
func Handle(pattern string, handler Handler) { 
    DefaultServeMux.Handle(pattern, handler)
}

Можно видеть, что эти две функции в конечном счете состоят изDefaultServeMuxперечислитьHandleспособ завершения регистрации маршрута.
Здесь мы сталкиваемся с двумя типами объектов:ServeMuxиHandler, скажемHandler.

Handler

Handlerэто интерфейс:

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

HandlerИнтерфейс объявляет имяServeHTTPСигнатура функции , то есть любой структуры, которая реализует этоServeHTTPметод, то эта структура являетсяHandlerобъект. На самом деле идиhttpУслуги основаны наHandlerобрабатывается, покаHandlerобъектServeHTTPметод также используется для борьбы сrequestи построитьresponseгде основная логика.

вернуться к вершинеHandleFuncфункции, обратите внимание на эту строку кода:

mux.Handle(pattern, HandlerFunc(handler))

некоторые могут подуматьHandlerFuncЭто функция, которая обернутся входящееhandlerфункция, которая возвращаетHandlerобъект. Однако здесьHandlerFuncна самом деле будетhandlerфункция делаетпреобразование типов,посмотриHandlerFuncОпределение:

type HandlerFunc func(ResponseWriter, *Request)

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

HandlerFuncэто тип, который просто представляетfunc(ResponseWriter, *Request)сигнатура типа функции, и этот тип реализуетServeHTTPметод (вServeHTTPметод вызывает сам себя), что означает, что этот тип функции на самом деле являетсяHandlerтип объекта. Используя это преобразование типов, мы можем преобразоватьhandlerфункция преобразуется в
Handlerобъекта без необходимости определять структуру, а затем позволить этой структуре реализоватьServeHTTPметод. Читатели могут испытать эту технику.

ServeMux

Маршрутизация в Golang (т.е.Multiplexer) на основеServeMuxСтруктура, первый взгляд наServeMuxОпределение:

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
}

Сосредоточьтесь здесьServeMuxполя вm,Этоmap,keyэто выражение маршрутизации,valueЯвляетсяmuxEntryструктура,muxEntryВ структуре хранятся соответствующие выражения маршрутизации иhandler.

В частности,ServeMuxтакже достигнутоServeHTTPметод:

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

то естьServeMuxструктура такжеHandlerобъект, ноServeMuxизServeHTTPметоды не предназначены для работы с бетономrequestи построитьresponse, но используется для определения регистрации маршрутаhandler.

зарегистрировать маршрут

пониматьHandlerиServeMuxПосле этого возвращаемся к предыдущему коду:

DefaultServeMux.Handle(pattern, handler)

здесьDefaultServeMuxозначает значение по умолчаниюMultiplexer, когда мы не создаем пользовательскиеMultiplexer, он автоматически использует значение по умолчаниюMultiplexer.

тогда посмотри еще разServeMuxизHandleЧто именно делает метод:

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)
    }
    // 利用当前的路由和handler创建muxEntry对象
    e := muxEntry{h: handler, pattern: pattern}
    // 向ServeMux的map[string]muxEntry增加新的路由匹配规则
    mux.m[pattern] = e
    // 如果路由表达式以'/'结尾,则将对应的muxEntry对象加入到[]muxEntry中,按照路由表达式长度排序
    if pattern[len(pattern)-1] == '/' {
        mux.es = appendSorted(mux.es, e)
    }

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

HandleЭтот метод в основном делает две вещи: первая заключается в том, чтобыServeMuxизmap[string]muxEntryДобавляет заданное правило сопоставления маршрутов; затем, если выражение маршрута заканчивается на'/'конец, соответствующийmuxEntryобъект добавлен в[]muxEntry, отсортированные по длине выражения маршрутизации. Первое легко понять, но последнее может быть нелегко увидеть, какой эффект он оказывает, что будет проанализировано позже.

Пользовательский ServeMux

Мы также можем создать индивидуальныйServeMuxзаменить значение по умолчаниюDefaultServeMux:

package main

import (
    "fmt"
    "net/http"
)

func indexHandler(w http.ResponseWriter, r *http.Request) {
    fmt.Fprintf(w, "hello world")
}

func htmlHandler(w http.ResponseWriter, r *http.Request) {
    w.Header().Set("Content-Type", "text/html")
    html := `<!doctype html>
    <META http-equiv="Content-Type" content="text/html" charset="utf-8">
    <html lang="zh-CN">
            <head>
                    <title>Golang</title>
                    <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=0;" />
            </head>
            <body>
                <div id="app">Welcome!</div>
            </body>
    </html>`
    fmt.Fprintf(w, html)
}

func main() {
    mux := http.NewServeMux()
    mux.Handle("/", http.HandlerFunc(indexHandler))
    mux.HandleFunc("/welcome", htmlHandler)
    http.ListenAndServe(":8001", mux)
}

NewServeMux()может создатьServeMuxпример, упомянутый ранееServeMuxтакже достигнутоServeHTTPметод, поэтомуmuxтакжеHandlerобъект. заListenAndServe()метод, если передан вhandlerпараметр является пользовательскимServeMuxпримерmux,ТакServerОбъект маршрута, полученный экземпляром, больше не будетDefaultServeMuxноmux.

Запустить службу

Первый изhttp.ListenAndServeЭтот метод запускается:

func ListenAndServe(addr string, handler Handler) error {
    server := &Server{Addr: addr, Handler: handler}
    return server.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(tcpKeepAliveListener{ln.(*net.TCPListener)})
}

Здесь впервые создалиServerобъекта, передавая адрес иhandlerпараметры, затем вызовитеServerобъектListenAndServe()метод.

посмотриServerэта структура,ServerВ структуре много полей, сначала можно получить общее представление:

type Server struct {
    Addr    string  // TCP address to listen on, ":http" if empty
    Handler Handler // handler to invoke, http.DefaultServeMux if nil
    TLSConfig *tls.Config
    ReadTimeout time.Duration
    ReadHeaderTimeout time.Duration
    WriteTimeout time.Duration
    IdleTimeout time.Duration
    MaxHeaderBytes int
    TLSNextProto map[string]func(*Server, *tls.Conn, Handler)
    ConnState func(net.Conn, ConnState)
    ErrorLog *log.Logger

    disableKeepAlives int32     // accessed atomically.
    inShutdown        int32     // accessed atomically (non-zero means we're in Shutdown)
    nextProtoOnce     sync.Once // guards setupHTTP2_* init
    nextProtoErr      error     // result of http2.ConfigureServer if used

    mu         sync.Mutex
    listeners  map[*net.Listener]struct{}
    activeConn map[*conn]struct{}
    doneChan   chan struct{}
    onShutdown []func()
}

существуетServerизListenAndServeметод, адрес прослушивания будет инициализированAddr, во время вызоваListenметод установки слушателя. Наконец, передайте прослушивающий объект TCPServeметод:

func (srv *Server) Serve(l net.Listener) error {
    ...

    baseCtx := context.Background() // base is always background, per Issue 16220
    ctx := context.WithValue(baseCtx, ServerContextKey, srv)
    for {
        rw, e := l.Accept() // 等待新的连接建立

        ...

        c := srv.newConn(rw)
        c.setState(c.rwc, StateNew) // before Serve can return
        go c.serve(ctx) // 创建新的协程处理请求
    }
}

Некоторые детали скрыты здесь для того, чтобы понятьServeОсновная логика метода. Сначала создайте объект контекста, затем вызовитеListenerизAccept()Подождите, пока будет установлено новое соединение; как только новое соединение будет установлено, вызовитеServerизnewConn()Создайте новый объект соединения и отметьте состояние соединения какStateNew, затем откройте новыйgoroutineОбработка запросов на подключение.

обработать соединение

мы продолжаем исследоватьconnизserve()метод, этот метод тоже очень длинный, мы тоже смотрим только на ключевую логику. Держись, ты скоро увидишь море.

func (c *conn) serve(ctx context.Context) {

    ...

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

        ...

        // 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.
        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)
        c.curReq.Store((*response)(nil))

        ...
    }
}

Когда соединение установлено, все запросы в соединении будут обрабатываться в этой сопрограмме, пока соединение не будет закрыто. существуетserve()метод будет вызываться циклическиreadRequest()Метод считывает следующий запрос для обработки, и самая важная логика — это строка кода:

serverHandler{c.server}.ServeHTTP(w, w.req)

Дальнейшее объяснениеserverHandler:

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

существуетserverHandlerизServeHTTP()в методеsh.srv.HandlerНа самом деле мы изначальноhttp.ListenAndServe()входящийHandlerОбъект, который является нашим заказомServeMuxобъект. еслиHandlerобъектnil, будет использоваться значение по умолчаниюDefaultServeMux. последний звонокServeMuxизServeHTTP()Метод соответствует текущему маршруту, соответствующемуhandlerметод.

Логика относительно проста и ясна, в основном в вызовеServeMuxизmatchметод соответствует соответствующему зарегистрированному выражению маршрута иhandler.

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

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
}

// Find a handler on a handler map given a path string.
// Most-specific (longest) pattern wins.
func (mux *ServeMux) match(path string) (h Handler, pattern string) {
    // Check for exact match first.
    v, ok := mux.m[path]
    if ok {
        return v.h, v.pattern
    }

    // Check for longest valid match.  mux.es contains all patterns
    // that end in / sorted from longest to shortest.
    for _, e := range mux.es {
        if strings.HasPrefix(path, e.pattern) {
            return e.h, e.pattern
        }
    }
    return nil, ""
}

существуетmatchВ методе мы видим упомянутый ранее мультиплексорmполе (типmap[string]muxEntryes(тип[]muxEntry). Этот метод сначала будет использовать точное совпадение вmap[string]muxEntryНайдите, есть ли в запросе соответствующее правило маршрутизации; если нет соответствующего правила маршрутизации, он будет использоватьesСделайте примерное совпадение.

Как упоминалось ранее, при регистрации маршрута он будет начинаться с'/'трейлинг-маршрут (который можно назватьМаршрутизация узла)принять участие вesполе[]muxEntryсередина. для подобных/path1/path2/path3Для такого маршрута, если не удается найти точное соответствие правил маршрутизации, оно будет соответствовать зарегистрированному маршруту родительского узла, ближайшему к текущему маршруту, поэтому, если маршрут/path1/path2/Зарегистрирован, тогда маршрут будет сопоставлен, в противном случае продолжите сопоставление маршрута следующего родительского узла, пока не будет найден корневой маршрут./.

так как[]muxEntryсерединаmuxEntryВыражения маршрутизации сортируются от самого длинного к самому короткому, поэтому при выполнении приблизительного сопоставления маршрут сопоставленного узла должен быть ближайшим среди зарегистрированных маршрутов родительского узла.

До сих пор Go реализовывалhttp serverОбщий принцип введения выполнен!

Суммировать

Голанг черезServeMuxМультиплексор определен для управления маршрутизацией и пропускомHandlerИнтерфейс определяет унифицированную спецификацию функции обработки маршрута, а именноHandlerдолжны быть реализованыServeHTTPметод; в то же времяHandlerИнтерфейс обеспечивает мощную расширяемость, что удобно для разработчиковHandlerВ интерфейсе реализовано различное промежуточное ПО. Я верю, что каждый может почувствовать это после прочтенияHandlerобъект вserverРеализация услуг действительно везде. пониматьserverОсновной принцип реализации вы можете прочитать в некоторых стороннихhttp serverфреймворки и промежуточное ПО, которое пишет определенные функции.

выше.

использованная литература

[Документация по стандартной библиотеке Golang --net/http]