bkdragon's log

[go] http 통신 과정 본문

golang

[go] http 통신 과정

bkdragon 2024. 9. 10. 14:55

net/http를 활용한 golang 서버에서 요청을 받아서 처리하는 순서를 간단하게(디테일 X) 살펴보려고 한다.

http 패키지의 HandleFunc로 request handler를 등록하고 ListenAndServe로 실행하면 요청에 맞는 handler가 실행된다.

http.HandleFunc("/helloworld", helloWorldHandler)
http.ListenAndServe(fmt.Sprintf(":%v", port), nil)

ListenAndServe 함수는 다음과 같다.


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

여기서 나오는 Handler 인터페이스는 잘 기억해두자.

Server 구조체를 생성해서 ListenAndServe를 호출한다. (Server는 HTTP 서버의 설정과 동작을 정의하는 구조체)

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

net.Listen을 통해 TCP 리스너를 생성하고, 요청을 받아들이기 위한 Serve 함수를 호출한다.

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 {
            if srv.shuttingDown() {
                return ErrServerClosed
            }
            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)
    }
}

Serve 함수는 HTTP 설정, Context 생성 등을 작업한 후, for 문을 통해 지속적으로 새로운 요청을 받아들이는 작업을 수행한다.

l.Accept()에서 블로킹 되어 요청을 기다리고 있다가, 요청이 들어오면 처리한다.

go c.serve(connCtx)

각 연결은 고루틴으로 처리된다. 고루틴은 가벼운 스레드로, 각 연결을 비동기적으로 처리하게 된다.

func (c *conn) serve(ctx context.Context) {
    // ... 생략
        serverHandler{c.server}.ServeHTTP(w, w.req)

    // ... 생략
}

핵심은 ServeHTTP 함수다. 이 함수에서 실제로 핸들러가 실행되며, 요청을 처리한다.

type serverHandler struct {
    srv *Server
}

func (sh serverHandler) ServeHTTP(rw ResponseWriter, req *Request) {
    handler := sh.srv.Handler
    if handler == nil {
        handler = DefaultServeMux
    }
    if !sh.srv.DisableGeneralOptionsHandler && req.RequestURI == "*" && req.Method == "OPTIONS" {
        handler = globalOptionsHandler{}
    }

    handler.ServeHTTP(rw, req)
}

serverHandler라는 구조체를 생성해서 ServeHTTP를 실행한다. 여기서 Server 구조체에 있는 Handler를 확인하고, 없다면 DefaultServeMux를 사용하게 된다. 그리고 이 handler의 ServeHTTP를 실행한다.

맨 위로 돌아가서 ListenAndServe 호출할 때 handler를 nil로 넘겼으므로, sh.srv.Handler는 nil이고, 결국 DefaultServeMux를 사용하게 된다.

DefaultServeMux는 ServeMux 타입의 전역 인스턴스다. net/http에서 제공하는 기본 라우터 역할을 한다.

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
    }
    var h Handler
    if use121 {
        h, _ = mux.mux121.findHandler(r)
    } else {
        h, r.Pattern, r.pat, r.matches = mux.findHandler(r)
    }
    h.ServeHTTP(w, r)
}

ServeMux의 ServeHTTP는 들어온 요청에 맞는 request handler를 찾아서 ServeHTTP를 실행한다. 여기서 라우팅이 일어난다.

지금까지 Handler라는 용어가 계속 등장했는데, Handler 인터페이스가 어떻게 동작하는지 살펴보자.

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

serverHandler, ServeMux 둘 다 ServeHTTP를 가지고 있으므로, 이들은 Handler 인터페이스를 만족하는 것이다. 그리고 ServeMux는 우리가 등록한 request handler의 ServeHTTP를 실행한다.

type HandlerFunc func(ResponseWriter, *Request)

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

여기서 request handler는 HandlerFunc 타입의 함수인데, 이 함수도 ServeHTTP 메서드를 가지고 있고, 자기 자신을 호출하는 역할을 한다.

h.ServeHTTP(w, r)

이제 이 코드가 우리가 작성한 request handler를 실행하는 이유가 명확해졌다.

gin에서의 동작

gin 프레임워크는 net/http 기반으로 만들어졌고, 요청을 처리하기 전 사용할 미들웨어와 라우팅을 더욱 편하게 해준다.

Run 메서드를 통해 서버를 실행할 수 있다.

r := gin.Default()
r.Run(":8000")

Run 메서드는 다음과 같다.

func (engine *Engine) Run(addr ...string) (err error) {
    defer func() { debugPrintError(err) }()

    if engine.isUnsafeTrustedProxies() {
        debugPrint("[WARNING] You trusted all proxies, this is NOT safe. We recommend you to set a value.\n" +
            "Please check https://pkg.go.dev/github.com/gin-gonic/gin#readme-don-t-trust-all-proxies for details.")
    }

    address := resolveAddress(addr)
    debugPrint("Listening and serving HTTP on %s\n", address)
    err = http.ListenAndServe(address, engine.Handler())
    return
}

ListenAndServe를 볼 수 있다. 근데 이번에는 라우터 역할을 하는 핸들러를 넘긴다.

func (engine *Engine) Handler() http.Handler {
    if !engine.UseH2C {
        return engine
    }

    h2s := &http2.Server{}
    return h2c.NewHandler(engine, h2s)
}

이 Handler는 engine을 반환하는데, 이 engine이 결국 ServeHTTP를 구현하는 구조체다.

func (engine *Engine) ServeHTTP(w http.ResponseWriter, req *http.Request) {
    c := engine.pool.Get().(*Context)
    c.writermem.reset(w)
    c.Request = req
    c.reset()

    engine.handleHTTPRequest(c)

    engine.pool.Put(c)
}

engine.handleHTTPRequest(c)에서 요청을 처리한다.

func (engine *Engine) handleHTTPRequest(c *Context) {
    httpMethod := c.Request.Method
    rPath := c.Request.URL.Path
    unescape := false
    if engine.UseRawPath && len(c.Request.URL.RawPath) > 0 {
        rPath = c.Request.URL.RawPath
        unescape = engine.UnescapePathValues
    }

    if engine.RemoveExtraSlash {
        rPath = cleanPath(rPath)
    }

    // Find root of the tree for the given HTTP method
    t := engine.trees
    for i, tl := 0, len(t); i < tl; i++ {
        if t[i].method != httpMethod {
            continue
        }
        root := t[i].root
        // Find route in tree
        value := root.getValue(rPath, c.params, c.skippedNodes, unescape)
        if value.params != nil {
            c.Params = *value.params
        }
        if value.handlers != nil {
            c.handlers = value.handlers
            c.fullPath = value.fullPath
            c.Next()
            c.writermem.WriteHeaderNow()
            return
        }
        if httpMethod != http.MethodConnect && rPath != "/" {
            if value.tsr && engine.RedirectTrailingSlash {
                redirectTrailingSlash(c)
                return
            }
            if engine.RedirectFixedPath && redirectFixedPath(c, root, engine.RedirectFixedPath) {
                return
            }
        }
        break
    }

    if engine.HandleMethodNotAllowed {
        allowed := make([]string, 0, len(t)-1)
        for _, tree := range engine.trees {
            if tree.method == httpMethod {
                continue
            }
            if value := tree.root.getValue(rPath, nil, c.skippedNodes, unescape); value.handlers != nil {
                allowed = append(allowed, tree.method)
            }
        }
        if len(allowed) > 0 {
            c.handlers = engine.allNoMethod
            c.writermem.Header().Set("Allow", strings.Join(allowed, ", "))
            serveError(c, http.StatusMethodNotAllowed, default405Body)
            return
        }
    }

    c.handlers = engine.allNoRoute
    serveError(c, http.StatusNotFound, default404Body)
}

여기서 중요한 부분은 Next() 함수다. 이 함수는 미들웨어와 핸들러 체인을 순차적으로 실행시킨다.

func (c *Context) Next() {
    c.index++
    for c.index < int8(len(c.handlers)) {
        c.handlers[c.index](c)
        c.index++
    }
}

gin.Context의 handlers에 미들웨어와 request handler가 함께 들어있고, Next()는 이들을 순서대로 호출하며 요청을 처리한다.

'golang' 카테고리의 다른 글

[gin] Clean Architecture  (1) 2024.09.20
[gorm] 다형성 관계  (0) 2024.09.11
[gin] JSON  (1) 2024.09.08
[gin] ShouldBindJSON  (0) 2024.09.08
[go] io 패키지  (5) 2024.09.07