Go Web Programming: [03/04] Get into http Package

Get Into http Package

앞 절에서 Go 언어에서 Web 작업환경을 제공하는 것과 개발 흐름에 대해서 설명했습니다.
이 절에서는 http 패키지에 대해서 좀더 알아보도록 하겠습니다.
net/http 패키지의 핵심 프로세스는 어떻게 구현되어있는지 알아 보겠습니다.

Go의 net/http에는 2개의 핵심적인 기능이 있습니다. 바로 Conn 과 ServeMux 입니다.

Conn의 goroutine

현재 일반적으로 쓰이는 http 기반의 웹서버와는 다르게, Go 언어는 멀티스레드와 고성능을 실현하기 위해
goroutines을 사용하여 Conn 이벤트를 읽고 처리 합니다.
이 기능을 통해서 각각의 요청은 독립성을 유지 할 수 있습니다. 서로 차단하지 않고 효율적으로 네트워크 이벤트에 응답
할 수 있습니다. 이것은 Go 언어의 높은 효율을 보장 합니다.

Go가 클라이언트의 요청을 대기할 때는 다음과 같이 사용합니다.

c, err := srv.newConn(rw)
if err != nil {
    continue
}
go c.serve()

클라이언트의 각 요청은 Conn 객체 하나를 생성하고 사용하는 것을 알 수 있습니다.
이 Conn 객체에는 해당 요청 정보가 저장되어 있습니다. 이것은 주요 목적의 handler로 전달하는 것이고,
해당 handler에서 원하는 정보를 읽을 수 있습니다. 이처럼 각 요청별 독립성을 보장 합니다.

ServeMux의 정의

앞 절에서 conn.server대해 설명했을 때 내부적으로 http 패키지의 기본 루트를 호출하고 있었습니다.
이번에는 라우터를 통해서 각 요청에대한 데이터를 백엔드 처리함수에 전달 합니다.

라우터는 어떻게 구현되어 있을까요? 구조는 다음과 같습니다.

type ServeMux struct {
    mu sync.RWMutex       // 뮤텍스 요청이 멀티스레드로 처리 됩니다.     
    m map[string]muxEntry // 라우팅 규칙 하나의 string이 하나의 mux 엔티티에 대응 합니다. 
    hosts bool            // 어떤 규칙에 host 정보가 포함되어 있는지
}

다음으로 muxEntry를 살펴보겠습니다.

type muxEntry struct {
    explicit bool      // 일치 여부 검증  
    h Handler          // 라우팅 식과 연결된 handler
    pattern string     // 매칭 문자열
}

다음으로 Handler의 정의에 대해서 알아 보겠습니다.

type Handler interface {
    ServeHTTP(ResponseWriter *Request) // 라우팅 기능 구현 
}

Handler는 인터페이스지만, 앞부분 중에서 sayhelloName()함수는 ServeHTTP라는 인터페이스를
구현하지는 않았습니다. 그런데 어떻게 추가 할 수 있을까요?
원래 http 패키지에는 HandlerFunc라는 형식이 정의되어 있습니다. 우리가 정의한 sayhelloName함수는
바로이 HandlerFunc가 호출 된 결과이며,이 형태는 기본적으로 ServeHTTP 인터페이스를 구현하고있는 것입니다.
즉, HandlerFunc(f)를 호출해서 강제로 f를 HandlerFunc 타입으로 형변환하고있는 것입니다.
이런 과정을 거쳐서 f는 ServeHTTP 메소드를 갖게됩니다.

type HandlerFunc func (ResponseWriter *Request)

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

라우터는 해당 라우팅 규칙을 저장 한 후에는 실제적으로는 어떻게 요청을 분류할까요?
다음 코드를 참고 하시기 바랍니다. 기본 라우터는 ServeHTTP를 구현 합니다.

func (mux *ServeMux) ServeHTTP(w ResponseWriter, r * Request) {
    if r.RequestURI == "*" {
        w.Header()Set("Connection", "close")
        w.WriteHeader(StatusBadRequest)
        return
    }
    h , _ := mux.Handler(r)
    h.ServeHTTP(w, r)
}

위에서 보았듯이 라우터는 요청을받은 후 이값이 *면 연결을 해제하고, 그렇지 않으면
mux.handler(r).ServeHTTP(w, r)를 호출하여 해​​당 설정처리 Handler를 반환해서,
h.ServeHTTP(w, r)를 실행 합니다.

즉, 원하는 라우팅 handler의 ServerHTTP 인터페이스에 대한 호출입니다.
mux.Handler(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)
            return RedirectHandler(p, 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()

    if mux.hosts {
        h, pattern = mux.match(host + path)
    }
    if h == nil {
        h, pattern = mux.match(path)
    }
    if h == nil {
        h, pattern = NotFoundHandler(), ""
    }
    return
}

원래 이것은 사용자의 요청 된 URL과 라우터에 저장되는 map 따라 일치하고 있습니다. 매칭에 의해 저장되는
handler가 반환 될 즈음이 handler의 ServeHTTP 인터페이스가 호출되어 원하는 기능을 수행 할 수 있습니다.

위의 소개를 통해 우리는 라우팅의 전체 프로세스를 이해했습니다. Go는 사실 외부에서 구현 된 라우터를 지원합니다.
ListenAndServe의 두번째 인자가 외부의 라우터를 설정하기 위해 사용됩니다.
이것은 Handler 인터페이스의 하나로 외부 라우터 Handler 인터페이스를 구현하고 ServeHTTP에
사용자 정의 라우팅 기능을 구현할 수 있습니다.

아래 코드를 통해 자신 간단한 라우터를 구현하려고합니다.

package main

import (
    "fmt"
    "net / http"
)

type MyMux struct {
}

func (p *MyMux) ServeHTTP(w http.ResponseWriter, r * http.Request) {
    if r.URL.Path == "/" {
        sayhelloName(w, r)
        return
    }
    http.NotFound(w, r)
    return
}

func sayhelloName(w http.ResponseWriter, r * http.Request) {
    fmt.Fprintf(w, "Hello myroute!")
}

func main () {
    mux := &MyMux{}
    http.ListenAndServe(": 9090", mux)
}

Go 코드의 실행 과정

http 패키지에 대한 분석을 통해서 전체 코드의 실행 과정을 정리해 보겠습니다.

  • 먼저 Http.HandleFunc()함수를 호출 합니다.
    순서에 따라서 몇 가지 작업을 실행 합니다.

    1. DefaultServeMux의 HandlerFunc를 호출 합니다.
    2. DefaultServeMux의 Handle을 호출 합니다.
    3. DefaultServeMux의 map[string]muxEntry에서 원하는 handler 및 라우팅 규칙을 추가 합니다.
  • 다음으로 http.ListenAndServe(": 9090", nil) 함수를 호출 합니다.
    이 과정도 몇가지 순서에 따라 처리합니다.

    1. Server의 엔티티화
    2. Server의 ListenAndServe() 함수를 호출
    3. net.Listen(“tcp”, addr)함수를 호출한 후 포트를 감시
    4. for 루프를 시작하고 요청을 Accept
    5. 각 요청에 대해서 Conn을 하나 생성해 엔티티화하고, 각 요청에 대해서 goroutine 하나를 실행한 후 go c.serve() 서비스를 실행
    6. 각 요청의 내용을 로드한 후 w, err := c.readRequest()
    7. 처리 handler가 존재하는지 판단. 만약 handler가 설정되어 있지 않으면
      (예제에서는 handler는 설정하지 않습니다) handler는 DefaultServeMux로 설정.
    8. handler의 ServeHttp를 호출
    9. 이후 DefaultServeMux.ServeHttp 모드에 돌입
    10. request에따라 해당 handler를 선택하고, handler의 ServeHTTP에 돌입 mux.handler®.ServeHTTP(w, r)
    11. handler를 선택
      A. 라우터가 request를 처리했는지 판단(루프에 의해 ServerMux의 muxEntry를 검사)
      B. 만약 라우팅되면 해당 라우팅 handler의 ServeHttp를 호출
      C. 라우팅되지 않으면 NotFoundHandler의 ServeHttp를 호출