[The Go Programming Language] 1장 튜토리얼 - 1.7 웹 서버
안녕하세요. 개발자 모도리입니다.
The Go Programming Language 라는 책으로 Go를 공부하고 있으며, 해당 책의 내용을 요약 정리해서 올리려고 합니다. 저는 번역본을 구매해서 공부하고 있습니다.
지난 게시물
- [Go] Mac에서 Atom으로 Go 개발 환경 구축하기
- [The Go Programming Language] 1장 튜토리얼 - 1.1 Hello, World
- [The Go Programming Language] 1장 튜토리얼 - 1.2 커맨드라인 인수
- [The Go Programming Language] 1장 튜토리얼 - 1.3 중복 줄 찾기
- [The Go Programming Language] 1장 튜토리얼 - 1.4 애니메이션 GIF
- [The Go Programming Language] 1장 튜토리얼 - 1.5, 1.6 URL 반입(Fetching)
1장 튜토리얼
1.7 웹 서버
웹 서버 구현 1
- 서버에 접근하는 데 사용된 URL의 경로를 반환하는 최소한의 서버를 작성합니다.
// Server1은 최소한의 "echo" 서버입니다.
package main
import (
"fmt"
"log"
"net/http"
)
func main() {
http.HandleFunc("/", handler) // 각 요청은 핸들러를 호출합니다.
log.Fatal(http.ListenAndServe("localhost:8000", nil))
}
// handler는 요청된 URL r의 Path 구성 요소를 반환합니다.
func handler(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "URL.Path = %q\n", r.URL.Path)
}
예제코드 [ch1/server1.go]
실행결과
$ go run ch1/server1.go
- 브라우저를 열어서 localhost:8000 에 접속하면 URL.Path = "/" 라는 메세지를 확인할 수 있습니다.
- localhost:8000/[path] : path 부분에 임의의 주소를 넣을 경우 해당 주소를 출력합니다.
- main 함수는 모든 URL을 의미하는
/
로 시작하는 URL에 핸들러 함수를 연결하고 8000번 포트로 들어오는 요청을 처리하는 서버를 시작합니다. - 요청은 http.Request 구조체로 표현되며, 그 안에 들어온 요청의 URL을 비롯한 다수의 관련 필드가 있습니다.
- 요청이 도착하면 핸들러 함수로 넘기고, 핸들러는 요청 URL에서 경로 구성요소를 추출하고
fmt.Fprintf
를 이용해 응답으로 돌려줍니다. - 웹 서버는 7.7절에서 자세히 다룹니다.
웹 서버 구현 2
- /count URL로 요청하면 /count로의 요청을 제외하고 지금까지 요청 된 수를 반환하는 서버를 작성합니다.
// Server2는 최소한의 "echo" 및 카운터 서버입니다.
package main
import (
"fmt"
"log"
"net/http"
"sync"
)
var mu sync.Mutex
var count int
func main() {
http.HandleFunc("/", handler)
http.HandleFunc("/count", counter)
log.Fatal(http.ListenAndServe("localhost:8000", nil))
}
// 핸들러는 요청된 URL의 Path 구성 요소를 반환합니다.
func handler(w http.ResponseWriter, r *http.Request) {
mu.Lock()
count++
mu.Unlock()
fmt.Fprintf(w, "URL.Path = %q\n", r.URL.Path)
}
// counter는 지금까지 요청된 수를 반환합니다.
func counter(w http.ResponseWriter, r *http.Request) {
mu.Lock()
fmt.Fprintf(w, "Count %d\n", count)
mu.Unlock()
}
예제코드 [ch1/server2.go]
실행결과
$ go run ch1/server2.go
- 브라우저를 열어서 localhost:8000/count 에 접속하면 현재까지 요청했던 요청 횟수가 나옵니다.
- http handler
- /count 요청은 counter 함수를 호출
- 그 외의 요청은 handler 함수를 호출
- 내부적으로 서버는 들어오는 각각의 요청 마다 별도의 고루틴을 실행해 동시에 여러 요청을 처리할 수 있게 합니다.
- mutex
- 여러 요청을 처리할 수 있지만, 두 개의 요청이 동시에 count를 갱신하려 할 경우 증가 값의 무결성을 보장하지 못할 수 있습니다.
- 그러한 프로그램에서는 경쟁 조건(Race Condition)이라 불리는 심각한 버그가 발생할 수 있는데, 이것은 9.1절에서 자세히 다룹니다.
- 이 문제를 방지하기 위해서 한 번에 하나의 고루틴만이 해당 변수에 접근하게 해야 하며, 이를 위해 mutex를 사용합니다.
웹 서버 구현 3
- 예제를 더 풍부하게 하기 위해 핸들러 함수가 받는 요청의 헤더와 폼 데이터에 대해 보고해 서버가 요청을 조사하고 디버깅하기에 유용하게 만들었습니다.
// Server3은 디버깅하기 유용하게 요청의 헤더와 폼 데이터를 반환합니다.
package main
import (
"fmt"
"log"
"net/http"
)
func main() {
http.HandleFunc("/", handler) // 각 요청은 핸들러를 호출합니다.
log.Fatal(http.ListenAndServe("localhost:8000", nil))
}
// handler는 HTTP 요청을 반환합니다.
func handler(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "%s %s %s\n", r.Method, r.URL, r.Proto)
for k, v := range r.Header {
fmt.Fprintf(w, "Header[%q] = %q\n", k, v)
}
fmt.Fprintf(w, "Host = %q\n", r.Host)
fmt.Fprintf(w, "RemoteAddr = %q\n", r.RemoteAddr)
if err := r.ParseForm(); err != nil {
log.Print(err)
}
for k, v := range r.Form {
fmt.Fprintf(w, "Form[%q] = %q\n", k, v)
}
}
예제코드 [ch1/server3.go]
실행결과
$ go run ch1/server3.go
- 브라우저를 열어서 localhost:8000 에 접속하면 http.Request 정보를 출력합니다.
- 변수 범위
- if 문의 조건 앞에 지역 변수 선언과 같은 간단한 구문을 나오게 할 수 있습니다.
- line 23의 코드를 아래와 같이도 표현할 수 있지만, 문장을 결합해서 작성하는 것이 더 짧고 err 변수의 범위를 줄일 수 있어 더 좋은 방법입니다.
err := r.ParseForm() if err != nil { log.Print(err) }
- 범위에 대해서는 2.7절에서 더 자세히 다룹니다.
웹 서버 구현 4
- 브라우저 상에서 lissajous GIF 애니메이션을 출력하게 합니다.
// Server4는 lissajous 애니메이션 GIF를 생성하여 http response로 반환합니다.
package main
import (
"image"
"image/color"
"image/gif"
"io"
"log"
"math"
"math/rand"
"net/http"
)
var palette = []color.Color{color.White, color.Black}
const (
whiteIndex = 0 // 팔레트의 첫 번째 색상
blackIndex = 1 // 팔레트의 다음 색상
)
func main() {
handler := func(w http.ResponseWriter, r *http.Request) {
lissajous(w)
}
http.HandleFunc("/", handler) // 각 요청은 핸들러를 호출합니다.
log.Fatal(http.ListenAndServe("localhost:8000", nil))
}
func lissajous(out io.Writer) {
const (
cycles = 5 // x 진동자의 회전수
res = 0.001 // 회전각
size = 100 // 이미지 캔버스 크기 [-size..+size]
nframes = 64 // 애니메이션 프레임 수
delay = 8 // 10ms 단위의 프레임 간 지연
)
freq := rand.Float64() * 3.0 // y 진동자의 상대적 진동수
anim := gif.GIF{LoopCount: nframes}
phase := 0.0 // 위상 차이
for i := 0; i < nframes; i++ {
rect := image.Rect(0, 0, 2*size+1, 2*size+1)
img := image.NewPaletted(rect, palette)
for t := 0.0; t < cycles*2*math.Pi; t += res {
x := math.Sin(t)
y := math.Sin(t*freq + phase)
img.SetColorIndex(size+int(x*size+0.5), size+int(y*size+0.5), blackIndex)
}
phase += 0.1
anim.Delay = append(anim.Delay, delay)
anim.Image = append(anim.Image, img)
}
gif.EncodeAll(out, &anim) // NOTE: 인코딩 오류 무시
}
예제코드 [ch1/server4.go]
실행결과
$ go run ch1/server4.go
- 브라우저를 열어서 localhost:8000 에 접속하면 lissajous 애니메이션이 출력됩니다.
이어보기
[The Go Programming Language] 1장 튜토리얼 - 1.8 미진한 부분