Golang đại pháp – 1.7 Xây dựng một web server đơn giản
Go
50
golang
49
White

nhaancs viết ngày 06/01/2018

Bài viết được đăng tải đầu tiên tại: http://nhaancs.com/golang-dai-phap-1-7-xay-dung-mot-web-server-don-gian/
golang dai phap

Các thư viên của Go giúp chúng ta dễ dàng hơn trong việc viết một web server để xử lý các request từ client (giống như request trong hàm fetch).

Trong phần này chúng ta sẽ viết một web server mini in ra URL mà người dùng truy cập vào server. Ví dụ nếu có 1 request truy cập vào http://localhost:8000/hello thì chương trình sẽ in ra thên trình duyệt là URL.Path = "/hello". File: server1.go

// Server 1 is minimal "echo" server 
package main 

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

func main() {
    http.HandleFunc("/", handler) // mỗi request tới sẽ gọi tới hàm handler 
    log.Fatal(http.ListenAndServe("localhost:8000", nil))
}

// in ra url cua request
func handler(w http.ResponseWriter, r *http.Request) {
    fmt.Fprintf(w, "URL.Path = %q\n", r.URL.Path)
}

Chương trình chỉ có 1 vài dòng vì các function của thư viện đã làm gần hết cho chúng ta.

Hàm main liên kết hàm handler với URL của các request bắt đầu với "/" (có nghĩa là tất cả các request), và bật server để lắng nghe các request đến port 8000. Request được thể hiện bằng 1 struct kiểu http.Request chứa các field liên quan, trong đó có URL của request đến.

Khi 1 request được gửi đến server, nó sẽ được ném vào hàm handler của chúng ta để tách ra URL dạng /hello và ném vào lại response bằng hàm fmt.Fprintf. Web server sẽ được trình bày chi tiết hơn trong các chương sau.

Bây giờ chúng ta sẽ bật server lên:

$ go build server1.go 

$ ./server1 

Sau đó chạy chương trình fetch)

$ ./fetch http://localhost:8000/hello

URL.Path = "/hello"

Ngoài ra bạn có thể bật trình duyệt và địa chỉ localhost:8000/hello.

Việc thêm tính năng vào server cũng khá dễ dàng. Trong ví dụ sau chúng ta sẽ đếm số lượng các request gọi đế server. Request tới URL /count trả về số lượng đã đến, không tính request tới /count. File server2.go:

// server2 is a minimal "echo" server and counter server 
package main

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

var mu sync.Mutex
var count int

func main() {
    // "/" quản lý các request bắt đầu với "/"
    http.HandleFunc("/", handler)
    http.HandleFunc("/count", counter)
    log.Fatal(http.ListenAndServe("localhost:8000", nil))
}

// in ra url cua request
func handler(w http.ResponseWriter, r *http.Request) {
    mu.Lock()
    count++
    mu.Unlock()
    fmt.Fprintf(w, "URL.Path = %q\n", r.URL.Path)
}

// in ra số lần request
func counter(w http.ResponseWriter, r *http.Request) {
    mu.Lock()
    fmt.Fprintf(w, "Count %d\n", count)
    mu.Unlock()
}

Bây giờ server của chúng ta có hai hàm để xử lý request đến. Request đến url /count sẽ được xử lý bởi hàm counter, tất cả các request khác sẽ do hàm handler quản lý.

Về bản chất, server quản lý các request đế trong các goroutine khác nhau, vì vậy nó có thể xử lý các request một cách bất đồng bộ. Tuy nhiên nếu 2 hay nhiều request đồng thời cùng update biến count cùng 1 lúc thì giá trị của biến count sẽ không còn chính xác nữa (gọi là lỗi race condition). Để tránh gây ra lỗi trên, bạn phải đảm bảo rằng tại một thời điểm chỉ có 1 goroutine được phép truy cập tới count.

Khi 1 goroutine truy cập tới count, hàm mu.Lock() khoá lại không cho các goroutine khác truy cập vào nữa. Sau khi biến count được update, mu.Unlock() mở khoá cho goroutine khác truy cập count.

Ví dụ tiếp theo, hàm handler cung cấp nhiều thông tin của request hơn như thông tin header và form. File: server3.go:

package main

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

func main() {
    http.HandleFunc("/", handler)
    log.Fatal(http.ListenAndServe("localhost:8000", nil))
}

// in ra url cua request
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", 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)
    }
}

Ví dụ trên dùng thông tin từ các fields của struct http.Request để tạo ra output. Thử bật server lên và vào localhost:8000/hello bằng trình duyệt, kết quả:

GET /hello?p=query HTTP/1.1
Header["Connection"] = ["keep-alive"]
Header["User-Agent"] = ["Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/63.0.3239.84 Safari/537.36"]
Header["Upgrade-Insecure-Requests"] = ["1"]
Header["Accept"] = ["text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8"]
Header["Accept-Encoding"] = ["gzip, deflate, br"]
Header["Accept-Language"] = ["vi-VN,vi;q=0.9,fr-FR;q=0.8,fr;q=0.7,en-US;q=0.6,en;q=0.5"]
Host = "localhost:8000"RemoteAddr = "127.0.0.1:51445"
Form["p"] = ["query"]

Chú ý là chúng ta dùng hàm ParseForm trong câu lệnh if. Go cho phép 1 câu lệnh đơn giản như khai báo biến (ngắn gọn) ở phía trước phần điều kiện của lệnh if, trong ví dụ này nó giúp quản lý lỗi dễ dàng hơn. Thay vì viết:

err := r.ParseForm()
if err != nil {
    log.Print(err)
}

chỉ cần viết:

// code ngắn gọn hơn và giảm cope của err
if err := r.ParseForm(); err != nil {
    log.Print(err)
}

Trong các ví dụ từ trước tới giờgiờ chúng ta đã thấy 3 dạng khác nhau của output stream:

fetch copy http response ra os.Stdout, một file, giống như lissajous

fetchall xoá đi các response bằng cách copy vào ioutil.Discard

server3 dùng fmt.Fprintf để write vào http.ResponseWriter và hiển thị trên trình duyệt.

Mặc dù là 3 kiểu khác nhau nhưng chúng đều thoả mãn cùng 1 interfaceio.Writer, vì vậy khi nào cần 1 output stream, chúng ta có thể dùng cái nào cũng được. Chúng ta sẽ tìm hiểu sâu hơn về interface trong các phần sau.

Bây giờ chúng ta sẽ kết hợp web server trong ví dụ này với hàm lissajous. File server4.go:

package main

import (
    "log"
    "net/http"

    "image"
    "image/color"
    "image/gif"
    "io"
    "math"
    "math/rand"
)

// bảng màu
var palatte = []color.Color{color.White, color.Black}

const (
    whiteIndex = 0 // first color in palatte
    blackIndex = 1 // next color in palatte
)

func main() {
    /*
    handler := func (w http.ResponseWriter, r *http.Request) {
        lissajous(w)
    }
    */


    http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
        /*
        w http.ResponseWriter
        lissajous(out io.Writer) {...
        => http.ResponseWriter thoả mãn interface io.Writer nên ta có thể truyền w vào lissajous như sau: lissajous(w)
        */
        lissajous(w)
    })
    log.Fatal(http.ListenAndServe("localhost:8000", nil))
}

func lissajous(out io.Writer) {
    const (
        cycles = 5 // number of complete x oscillator revolutions
        res = 0.001 // angular resolution 
        size = 100 // image canvas cover [-size .. +size]
        nframes = 64 // number of animation frames
        delay = 8 // delay between frames in 10ms units
    )

    freq := rand.Float64()*3.0 // relative frequency of y oscillator 
    anim := gif.GIF{LoopCount: nframes}
    phase := 0.0 // phase difference

    for i := 0; i < nframes; i++ {
        rect := image.Rect(0, 0, 2*size + 1, 2*size + 1)
        img := image.NewPaletted(rect, palatte)

        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 += 1
        anim.Delay = append(anim.Delay, delay)
        anim.Image = append(anim.Image, img)
    } 

    gif.EncodeAll(out, &anim) // NOTE: ignoring encode errors 
}

Bạn tự chạy thử server4 và bật trình duyệt lên thưởng thức nhé.

Hàm vô danh (anonymous function):

func (w http.ResponseWriter, r *http.Request) {
    lissajous(w)
}

Chúng ta có thể gán anonymous function cho một biến hay truyền vào function như một tham số. Anonymous function sẽ được giới thiệu kỹ hơn trong các phần sau.

Bài tập cho bạn:

Exercise 1.12: Chỉnh sửa server4.go để đọc tham số từ URL, như khi truy cập localhost:8000/?cycles=20 thì thiết lập giá trị của cycles là 20 thay vì 5. Dùng hàm strconv.Atoi để chuyển từ tham số string sang integer.

Reference: The Go Programming Language

Bình luận


White
{{ comment.user.name }}
Bỏ hay Hay
{{comment.like_count}}
Male avatar
{{ comment_error }}
Hủy
   

Hiển thị thử

Chỉnh sửa

White

nhaancs

11 bài viết.
4 người follow
Kipalog
{{userFollowed ? 'Following' : 'Follow'}}
Cùng một tác giả
White
2 0
Bài viết được đăng tải đầu tiên tại: http://nhaancs.com/golangdaiphap23khaibaobien/ Khai báo với từ khoá var tạo ra 1 biến có kiểu xác định, gán...
nhaancs viết 6 tháng trước
2 0
Bài viết liên quan
White
9 2
Makefile thực hiện một số thao tác thường dùng trong Go Khi làm project Go mình thường tạo một file Makefile dạng này: Lưu ý nhớ thay thành tên m...
Huy Trần viết 2 năm trước
9 2
White
16 0
Crawl dữ liệu Crawl là một vấn đề hay gặp trong quá trình làm software. Ví dụ lấy tin tức, tin giảm giá, vé xem phim... là những dạng của crawl. Mộ...
Thach Le viết hơn 2 năm trước
16 0
{{like_count}}

kipalog

{{ comment_count }}

bình luận

{{liked ? "Đã kipalog" : "Kipalog"}}


White
{{userFollowed ? 'Following' : 'Follow'}}
11 bài viết.
4 người follow

 Đầu mục bài viết

Vẫn còn nữa! x

Kipalog vẫn còn rất nhiều bài viết hay và chủ đề thú vị chờ bạn khám phá!