간단한 웹 서버 (HTTP 서버)

1. 간단한 HTTP 서버

Go의 표준 패키지인 net/http 패키지는 웹 관련 서버 (및 클라이언트) 기능을 제공한다. Go에서 HTTP 서버를 만들기 위해 중요한 http 패키지 메서드로 ListenAndServe(), Handle(), HandleFunc() 등을 들 수 있다. ListenAndServe() 메서드는 지정된 포트에 웹 서버를 열고 클라이언트 Request를 받아들여 새 Go 루틴에 작업을 할당하는 일을 한다. Handle()과 HandleFunc() 메서드는 요청된 Request Path에 어떤 Request 핸들러를 사용할 지를 지정하는 라우팅 역활을 한다.

아래 예제는 간단한 HTTP 서버를 구현한 예로써, 브라우져에서 http://localhost:5000/hello 라고 치면, HandleFunc()의 /hello Path에 대한 익명함수를 실행하여 Hello World를 출력하게 된다. 익명함수 안의 http.ResponseWriter 파라미터는 HTTP Response에 무언가를 쓸 수 있게 하며, http.Request 파라미터는 입력된 Request 요청을 검토할 수 있게 한다. 마지막의 ListenAndServe() 메서드는 여기서 2개의 파라미터를 갖고 있는데, 첫번째는 포트 5000 에서 Request를 Listen 할 것을 지정하고, 두번째는 어떤 ServeMux를 사용할 지를 지정하는데 nil인 경우 DefaultServeMux를 사용한다. (ServeMux는 기본적으로 HTTP Request Router (혹은 Multiplexor) 인데, 일반적으로 내장된 DefaultServeMux을 사용하지만, 개발자가 별도로 ServeMux를 만들어 Routing 부분을 세밀하게 제어할 수 있다).
DefaultServeMux를 사용하는 경우, Handle() 혹은 HandleFunc()을 사용하여 라우팅 패턴을 추가하게 된다.

package main

import (
	"net/http"
)

func main() {
	http.HandleFunc("/hello", func(w http.ResponseWriter, req *http.Request) {
		w.Write([]byte("Hello World"))
	})

	http.ListenAndServe(":5000", nil)
}

2. http.Handle() 사용

위의 HandleFunc() 메서드와 비슷하게, HTTP Handler를 정의하는 또 다른 방식으로 http.Handle() 메서드를 사용할 수 있다. http.Handle() 메서드는 첫번째 파라미터로 URL (혹은 URL 패턴)을 받아들이고, 두번째 파라미터로 http.Handler 인터페이스를 갖는 객체를 받아들인다. http.Handler 인터페이스 다음과 같은 메서드 하나를 갖는 인터페이스이다. ServeHTTP() 메서드는 HTTP Response에 데이타를 쓰기 위한 Writer와 HTTP Request 입력데이타를 파라미터로 갖는다.

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

아래 예제는 http.Handler 인터페이스를 갖는 testHandler 라는 struct를 정의하고 이 struct의 메서드 ServeHTTP()을 구현한 예이다. Handle()의 두번째 파라미터는 testHandler 객체를 new() 함수로 생성하여 전달한다.

package main

import (
	"net/http"
)

func main() {
	http.Handle("/", new(testHandler))

	http.ListenAndServe(":5000", nil)
}

type testHandler struct {
	http.Handler
}

func (h *testHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) {
	str := "Your Request Path is " + req.URL.Path
	w.Write([]byte(str))
}

3. 간단한 Static 파일 핸들러

HTML, 이미지, JavaScript 파일 등의 정적(Static) 파일이 요청되었을 때, 웹 서버에서 해당 Static 파일들을 적절한 헤더와 함께 전달하는 간단한 핸들러를 아래와 같이 구현할 수 있다. 즉, 아래 코드는 staticHandler라는 새 타입을 만들고 이 타입의 ServeHTTP() 메서드를 구현한 예제인데, 이 메서드에서는 Request URL 패스에 표시된 정적 파일을 서버 상의 특정 폴더(wwwroot) 에서 읽어 들여 그 파일 내용을 그대로 전달하고 있다. 그런데 파일 내용이 전달될 때 디폴트로 text/plain 으로 전송되므로, 브라우져가 정적 파일을 텍스트 그대로 화면에 표시하게 되는데, 이를 막기 위해 파일 확장자에 따른 Content-Type를 구해 이를 Response 헤더에 추가한 후 리턴하고 있다.

Static 파일 핸들러

package main

import (
	"io/ioutil"
	"net/http"
	"path/filepath"
)

func main() {
	http.Handle("/", new(staticHandler))

	http.ListenAndServe(":5000", nil)
}

type staticHandler struct {
	http.Handler
}

func (h *staticHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) {
	localPath := "wwwroot" + req.URL.Path
	content, err := ioutil.ReadFile(localPath)
	if err != nil {
		w.WriteHeader(404)
		w.Write([]byte(http.StatusText(404)))
		return
	}

	contentType := getContentType(localPath)
	w.Header().Add("Content-Type", contentType)
	w.Write(content)
}

func getContentType(localPath string) string {
	var contentType string
	ext := filepath.Ext(localPath)

	switch ext {
	case ".html":
		contentType = "text/html"
	case ".css":
		contentType = "text/css"
	case ".js":
		contentType = "application/javascript"
	case ".png":
		contentType = "image/png"
	case ".jpg":
		contentType = "image/jpeg"
	default:
		contentType = "text/plain"
	}

	return contentType
}

4. http.FileServer를 사용한 간단한 핸들러

특정 폴더 (예를들어 wwwroot) 안에 있는 정적 파일들을 웹서버에서 클라이언트로 그대로 전달하기 위해 내장 기능인 http.FileServer()를 사용할 수 있다. http.FileServer()는 해당 디렉토리 내의 모든 정적 리소스를 1 대 1로 매핑하여 그대로 전달하는 일을 하는데, 위의 ServeHTTP() 핸들러처럼 세밀한 제어를 할 수는 없다.

아래 예제 코드는 위의 [3. 간단한 Static 파일 핸들러] 예제처럼 wwwroot 폴더 안의 정적 파일들을 클라이언트에 전달한다. 만약 시작 위치를 /static 으로 변경하려면, 라인 9 처럼 Handle()의 첫번째 파라미터를 /static 으로 지정하면 된다.

package main

import (
	"net/http"
)

func main() {	
	http.Handle("/", http.FileServer(http.Dir("wwwroot")))
	//http.Handle("/static", http.FileServer(http.Dir("wwwroot")))
	http.ListenAndServe(":5000", nil)
}