본문 바로가기

Go

[묘공단] Tucker의 Go 언어 프로그래밍 29장 : Go언어로 만드는 웹서버

  • HTTP 웹서버 만들기
  • HTTP 동작원리
  • HTTP 쿼리 인수 사용하기 
  • ServeMux 인스턴스 사용하기
  • 파일서버
  • 웹서버 테스트 코드 만들기
  • JSON데이터 전송
  • HTTPS웹서버 만들기

 

 

 

HTTP 웹서버 만들기

  • http.HandleFunc() 로 웹핸들러를 등록하고,
  • http.ListenAndServe() 웹서버를 시작한다. 
// ch29/ex29.1/ex29.1.go
package main

import (
	"fmt"
	"net/http"
)

func main() {
	http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
		fmt.Fprint(w, "Method : ", r.Method, "\n")
		fmt.Fprint(w, "Path: ", r.URL.Path, "\n")
		fmt.Fprint(w, "RawQAuery: ", r.URL.RawQuery, "\n")
		fmt.Fprint(w, "/") // ❶ 웹 핸들러 등록
	})

	http.HandleFunc("/sub", func(w http.ResponseWriter, r *http.Request) {
		fmt.Fprint(w, "/sub Method : ", r.Method, "\n")
		fmt.Fprint(w, "/sub Path: ", r.URL.Path, "\n")
		fmt.Fprint(w, "/sub RawQAuery: ", r.URL.RawQuery, "\n")
		fmt.Fprint(w, "/sub") // ❶ 웹 핸들러 등록
	})

	http.ListenAndServe(":3000", nil) // ❷ 웹 서버 시작
}

 

 

 

HTTP 동작원리

  • http 의 기본포트는 80번이고, https의 기본포트는 443번이다. http의 특징은 아래와 같다.
  • 무상태(Stateless) : HTTP는 상태를 유지하지 않는 무상태 프로토콜이다. 즉, 각각의 요청은 서로 독립적으로 처리되며 이전 요청과 관련이 없다. 이를 통해 서버는 각각의 요청을 독립적으로 처리할 수 있고, 확장성을 높일 수 있다.
  • 요청-응답 모델(Request-Response Model): HTTP는 클라이언트가 서버에 요청을 보내고, 서버가 그 요청에 대한 응답을 반환하는 요청-응답 모델을 사용한다. 이를 통해 클라이언트와 서버 간의 효율적인 통신이 가능하다.
  • 텍스트 기반 프로토콜: HTTP는 텍스트를 기반으로 한 프로토콜로, 요청과 응답은 텍스트 형식으로 전송된다. 이는 사람이 읽고 이해하기 쉽고, 디버깅 및 개발 과정에서 유용하다.
  • 메서드(Method): HTTP는 다양한 메서드를 제공하여 서버에 요청을 보낼 수 있다. 가장 일반적인 메서드로는 GET(리소스를 가져오기 위한 요청), POST(리소스를 생성하기 위한 요청), PUT(리소스를 갱신하기 위한 요청), DELETE(리소스를 삭제하기 위한 요청) 등이 있다.
  • 상태 코드(Status Code): HTTP 응답은 상태 코드와 함께 반환됩니다. 상태 코드는 요청이 성공했는지, 실패했는지 등을 나타내는 코드이다. 예를 들어, 200은 성공, 404는 찾을 수 없음, 500은 서버 내부 오류 등을 나타낸다.
  • HTTP는 TCP/IP 프로토콜 스택 위에서 동작하는 프로토콜로, 웹 서버와 웹 클라이언트 간의 데이터 전송을 담당합니다

 

 

 

 

 

  • IP (Internet Protocol)
    • IP는 인터넷에서 데이터를 주고받을 때 사용되는 프로토콜이다.
    • IP는 각 장치에 고유한 IP 주소를 할당하여 데이터를 전달한다. IP 주소는 IPv4에서는 32비트 주소 체계를 사용하고, IPv6에서는 128비트 주소 체계를 사용한다.
    • IP는 데이터 패킷을 라우팅하는 역할을 한다. 데이터 패킷은 출발지와 목적지 IP 주소를 포함하고, 이를 기반으로 네트워크 상에서 경로를 찾아 목적지까지 전달된다.
  • TCP (Transmission Control Protocol)
    • TCP는 IP 위에서 동작하는 프로토콜로, 신뢰성 있는 데이터 전송을 담당합니다.
    • TCP는 연결 지향형 프로토콜로, 데이터의 전송을 위해 세션을 설정하고 종료하는 과정을 포함합니다. 이를 통해 데이터의 손실이나 오류를 최소화하고 데이터의 순서를 보장합니다.
    • TCP는 데이터의 분할 및 재조립, 흐름 제어, 혼잡 제어 등의 기능을 제공하여 안정적이고 효율적인 데이터 전송을 보장합니다.

 

 

 

 

HTTP 쿼리 인수 사용하기 

 

//ch29/ex29.2/ex29.2.go
package main

import (
	"fmt"
	"net/http"
	"strconv"
)

func barHandler(w http.ResponseWriter, r *http.Request) {
	values := r.URL.Query()    // ❶ 쿼리 인수 가져오기
	name := values.Get("name") // ❷ 특정 키값이 있는지 확인
	if name == "" {
		name = "World"
	}
	id, _ := strconv.Atoi(values.Get("id")) // ❸ id값을 가져와서 int타입 변환
	fmt.Fprintf(w, "Hello %s! id:%d", name, id)
}

func main() {
	http.HandleFunc("/bar", barHandler) // ❹ "/bar" 핸들러 등록
	http.ListenAndServe(":3000", nil)
}

 

 

 

 

ServeMux 인스턴스 사용하기

  • 위의 예제에서는 ListenAndServe()의 두번째 인자로 nil을 넣어서, DefaultServeMux를 사용하였는데, 이번에는 직접  ServeMux를 만들어서 사용해보자. 
  • 아래 예제에서는 http.NewServeMux() 를 사용하여 새로운 인스턴스를 생성하고, 각각의 handleFunc를 등록하였다.
//ch29/ex29.3/ex29.3.go
package main

import (
	"fmt"
	"net/http"
)

func main() {
	mux := http.NewServeMux() // ❶ ServeMux 인스턴스 생성
	mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
		fmt.Fprint(w, "Hello World") // ❷ 인스턴스에 핸들러 등록
	})
	mux.HandleFunc("/bar", func(w http.ResponseWriter, r *http.Request) {
		fmt.Fprint(w, "Hello Bar")
	})

	http.ListenAndServe(":3000", mux) // ❸ mux 인스턴스 사용
}

 

 

파일서버

  • http.Handle()에서 http.FileServer()를 사용하여 파일서버를 만들수있다.
  • 아래의 소스를 실행하고, test.html을 웹브라우저에서 실행해보자, 아래와 같인 결과를 볼수있다.
  • 실제 웹서비스에서는 CDN(content delivery network)서비스를 이용하는 방식으로 제공한다. 

 

 

CDN의 기본원리

 

 

AWS에서 설명하는 CDN

 

 

 

//ch29/ex29.4/ex29.4.go
package main

import "net/http"

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

 

 

<!--ch29/ex29.4/test.html-->
<html>
<body>
<img src="http://localhost:3000/gopher.jpg"/>
<h1>이것은 Gopher 이미지입니다.</h1>
</body>
</html>

 

 

 

 

웹서버 테스트 코드 만들기

  • 아래소스는 29.3과 거의 같다. MakeWebHandler()함수를 따로 만들어 NewServeMux를 리턴하도록 하였다.
//ch29/ex29.5/ex29.5.go
package main

import (
	"fmt"
	"net/http"
)

func MakeWebHandler() http.Handler { // ❶ 핸들러 인스턴스를 생성하는 함수
	mux := http.NewServeMux()
	mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
		fmt.Fprint(w, "Hello World")
	})
	mux.HandleFunc("/bar", func(w http.ResponseWriter, r *http.Request) {
		fmt.Fprint(w, "Hello Bar")
	})
	return mux
}

func main() {
	http.ListenAndServe(":3000", MakeWebHandler())
}

 

  • 아래의 코드를 웹서버를 테스트하는 코드이다. 매번 웹브라우저로 테스트하는 방법은 번거롭기때문에 아래와 같이 테스트코드를 만들어서, 해당 웹서버가 정상적인지 확인할수있다. 
  • TestIndexHandler()는 / 경로를 테스트하여 Hello World 가 정상적으로 리턴되었는지 확인한다.
  • TestBarHandler()는 /bar 경로를 테스트하여 Hello World 가 정상적으로 리턴되었는지 확인한다.
//ch29/ex29.5/ex29_5_test.go
package main

import (
	"io"
	"net/http"
	"net/http/httptest"
	"testing"

	"github.com/stretchr/testify/assert"
)

func TestIndexHandler(t *testing.T) {
	assert := assert.New(t)

	res := httptest.NewRecorder()
	req := httptest.NewRequest("GET", "/", nil) // ❶ / 경로 테스트

	mux := MakeWebHandler() // ❷
	mux.ServeHTTP(res, req)

	assert.Equal(http.StatusOK, res.Code) // ❸ Code 확인
	data, _ := io.ReadAll(res.Body)       // ❹ 데이터를 읽어서 확인
	assert.Equal("Hello World", string(data))
}

func TestBarHandler(t *testing.T) {
	assert := assert.New(t)

	res := httptest.NewRecorder()
	req := httptest.NewRequest("GET", "/bar", nil) // ➎ /bar 경로 테스트

	mux := MakeWebHandler()
	mux.ServeHTTP(res, req)

	assert.Equal(http.StatusOK, res.Code)
	data, _ := io.ReadAll(res.Body)
	assert.Equal("Hello Bar", string(data))
}

 

 

 

 

JSON데이터 전송

  • http는 HTML문서를 전송하는 프로토콜이지만, HTML뿐만아니라 이미지나 사운드, 기타 다양한 데이타도 전송할수있다. 이번 예제에서는 JSON데이타를 전송하는 방법을 알아보자 
  • 아래 예제에서는 student라는 구조체를 json.Marshal(student) 를 사용하여 json 데이타로 변경하여, 결과를 전송하는 예제이다. 

 

//ch29/ex29.6/ex29.6.go
package main

import (
	"encoding/json"
	"fmt"
	"net/http"
)

type Student struct {
	Name  string
	Age   int
	Score int
}

func MakeWebHandler() http.Handler { // ❶ 핸들러 인스턴스를 생성하는 함수
	mux := http.NewServeMux()
	mux.HandleFunc("/student", StudentHandler)
	return mux
}

func StudentHandler(w http.ResponseWriter, r *http.Request) {
	var student = Student{"aaa", 16, 87}
	data, _ := json.Marshal(student)                   // ❷ Student 객체를 []byte로 변환
	w.Header().Add("content-type", "application/json") // ❸ json 포맷임을 표시
	w.WriteHeader(http.StatusOK)
	fmt.Fprint(w, string(data)) // ❹ 결과 전송
}

func main() {
	http.ListenAndServe(":3000", MakeWebHandler())
}

 

 

 

//ch29/ex29.6/ex26_6_test.go
package main

import (
	"encoding/json"
	"net/http"
	"net/http/httptest"
	"testing"

	"github.com/stretchr/testify/assert"
)

func TestJsonHandler(t *testing.T) {
	assert := assert.New(t)

	res := httptest.NewRecorder()
	req := httptest.NewRequest("GET", "/student", nil) // ❶ /student 경로 테스트

	mux := MakeWebHandler()
	mux.ServeHTTP(res, req)

	assert.Equal(http.StatusOK, res.Code)
	student := new(Student)
	err := json.NewDecoder(res.Body).Decode(student) // ❷ 결과 변환
	assert.Nil(err)                                  // ❸ 결과 확인
	assert.Equal("aaa", student.Name)
	assert.Equal(16, student.Age)
	assert.Equal(87, student.Score)
}

 

 

 

HTTPS웹서버 만들기

  • 이번에는 HTTPS를 지원하는 웹서버를 만들어보자. Https는 http에 보안기능을 강화한 프로토콜이다. 
  • 아래의 openssl을 사용하여 인증서를 생성하자.
openssl req -new -newkey rsa:2048 -nodes -keyout localhost.key -out localhost.csr

openssl x509 -req -days 365 -in  localhost.csr -signkey localhost.key -out localhost.crt

 

  • 생성한 인증서를 넣고, 웹서버를 실행시킨다.
//ch29/ex29.7/ex29.7.go
package main

import (
	"fmt"
	"log"
	"net/http"
)

func main() {
	http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
		fmt.Fprint(w, "Hello World") // ❶ 웹 핸들러 등록
	})

	err := http.ListenAndServeTLS(":3000", "localhost.crt", "localhost.key", nil) // ❷ 웹 서버 시작
	if err != nil {
		log.Fatal(err)
	}
}