Saturday, October 15, 2016

Bài 26: Lập trình web với package net/http

Mặc dù Go được xây dựng để phát triển các ứng dụng đa mục đích nhưng với môi trường web Go có những ưu thế giúp cho lập trình viên tạo nên những ứng dụng web nhanh chóng nhưng mạnh mẽ, hiệu năng cao. Go không có thế mạnh tạo ra các ứng dụng web truyền thống đòi hỏi xử lý và tạo giao diện web nhưng với các ứng dụng back-end cho mobile và các hệ thống ứng dụng web API thì Go có thế mạnh nhất định. Trong thời kỳ bùng nổ ứng dụng điện thoại kết nối internet như hiện nay thì việc xây dựng các hệ thống web API, đặc biệt RESTful API cung cấp dịch vụ cho các ứng dụng điện thoại đang rất cần thiết. Go là môi trường lý tưởng để làm việc đó. 

Thông thường ở các môi trường phát triển web khác, chúng ta sử dụng các framework có sẵn để phát triển ứng dụng web và chỉ thêm phần chức năng bên trên để tạo thành một ứng dụng web hoàn chỉnh phục vụ mục đích nhất định. Với Go cũng vậy. Chúng ta có thể sử dụng các framework có sẵn như Beego, Revel hay Martini. Tuy vậy, trong loạt bài này, tôi sẽ sử dụng net/http để phát triển ứng dụng web kết hợp với các module hữu ích từ các package bên ngoài. 

Trong bài 19 tôi đã có nêu về package net/http và cả ví dụ xây dựng một web server đơn giản. Package net/http từ thư viện chuẩn của Go cung cấp tất cả các hàm thiết yếu để phát triển một ứng dụng/dịch vụ web hoàn chỉnh.

Xử lý các yêu cầu HTTP


Web hoạt động theo nguyên tắc yêu cầu (request) - phản hồi (response). Các client sẽ gửi yêu cầu cho server, server xử lý yêu cầu rồi phản hồi kết quả về cho client. Trong trường hợp là trang web thì nội dung phản hồi dưới định dạng HTML để trình duyệt có thể vẽ nội dung ra màn hình. Trong trường hợp là web API thì nội dung phản hồi là định dạng JSON hoặc XML, v.v... 

Để tạo một web server, Go cung cấp đúng 1 hàm thuộc gói net/http để xử lý đó là ListenAndServe(addr string, handler Handler). Addr là địa chỉ server và cổng nhưng thường chỉ có cổng vì chúng ta mở server tại cùng ứng dụng web nên địa chỉ là localhost có thể bỏ. Tham số thứ 2 là kiểu interface http.Handler, nơi đảm nhận việc nhận và xử lý yêu cầu từ client. Interface này có duy nhất phương thức ServeHTTP(ResponseWriter, *Request). Để có thể tạo một đối tượng xử lý yêu cầu từ client, chúng ta cần khai báo một kiểu dữ liệu thỏa mãn interface http.Handler, tức có phương thức ServeHTTP(ResponseWriter, *Request). Phương thức ServeHTTP có 2 tham số: interface http.ResponseWriter đảm nhận việc ghi phần đầu và thân vào gói phản hồi và con trỏ thực thể cấu trúc http.Request chứa các thông tin phần đầu và thân của gói yêu cầu. 

Chúng ta cùng thử tạo một web server đơn giản mà mọi yêu cầu đến nó đều được trả về là "Chào mừng đến lập trình Go cho web!".
01    package main
02    
03    import (
04        "fmt"
05        "net/http"
06    )
07    
08    type liteHandler struct {
09        message string
10    }
11    
12    func (m *liteHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
13        fmt.Fprintf(w, m.message)
14    }
15    func main() {
16        hdl := &liteHandler{"Chào mừng đến lập trình Go cho web!"}
17        http.ListenAndServe(":8080", hdl)
18    }
- Cấu trúc liteHandler được khai báo và cài đặt phương thức ServeHTTP ở dòng 08-14. Như vậy liteHandler đã thỏa mãn interface http.Handler.
- Hàm main chỉ gồm 2 bước: tạo biến con trỏ hdl của cấu trúc liteHandler (dòng 16) và khai báo web server lắng nghe cổng 8080 và chuyển mọi yêu cầu cho hdl xử lý (dòng 17).

Vậy là chúng ta đã có một web server! Thử trên trình duyệt truy cập localhost:8080 sau khi đã chạy file thực thi server này, chúng ta thấy chuỗi "Chào mừng đến lập trình Go cho web!" xuất hiện ở trình duyệt. Dù có yêu cầu gì khác, localhost:8080/test chẳng hạn, nội dung vẫn không thay đổi.

Một website hay hệ thống web API thì phức tạp hơn ví dụ trên nhiều và do đó đối tượng thỏa mãn http.Handler (như liteHandler ở trên) cũng phải được cài đặt phức tạp do yêu cầu đa dạng. Trong tình huống này cần phải tách thành 2 thành phần, một đóng vai trò nhận yêu cầu và thành phần thứ hai là các đối tượng chuyên xử lý yêu cầu và phản hồi về client. Package net/http cung cấp công cụ xây dựng 2 thành phần này:
- ServeMux: là bộ định tuyến các yêu cầu. Nó so sánh các yêu cầu gửi đến với danh sách tài nguyên đã khai báo trước đó để gọi đúng handler tương ứng để xử lý yêu cầu và phản hồi về cho client. ServeMux bản chất cũng thỏa mãn interface http.Handler vì nó sẽ được truyền làm tham số thứ 2 của http.ListenAndServe.
- Handler là các thực thể thỏa mãn http.Handler chuyên xử lý yêu cầu cụ thể từ client và tạo ra phản hồi gửi về client. Các handler này sẽ được đăng ký với ServeMux để xác định nó chuyên xử lý yêu cầu nào.

Để tạo một ServeMux, package http cung cấp hàm http.NewServeMux(). Đối tượng ServeMux này có phương thức Handle để đăng ký handler. Ngoài ra package http cung cấp một số hàm trả về http.Handler giúp chúng ta có thể sử dụng trong một số tình huống phổ biến mà không cần phát triển lại handler:
- FileServer: trả về handler phản hồi cây thư mục con của thư mục được nêu trong tham số.
- NotFoundHandler: trả về handler phản hồi lỗi không tìm thấy.
- RedirectHandler: trả về handler phản hồi thông báo chuyển hướng các yêu cầu nó nhận.
- StripPrefix: trả về handler loại bỏ phần nêu ở tham số ra khỏi phần đầu đường dẫn tài nguyên và tiến hành xử lý phản hồi với đường dẫn mới.
- TimeoutHandler: trả về handler xử lý phản hồi trong khoản thời gian nêu trong tham số. Nếu xử lý quá thời gian đó, handler phản hồi với mã lỗi 503.

Chúng ta cùng khảo sát ví dụ sau: tạo ra một web server hiển thị danh sách file, thư mục con của đối tượng được nêu và hiển thị lời chào trong trường hợp yêu cầu /welcome. Ở đây sử dụng 1 handler có sẵn và tạo ra handler mới (lấy lại từ ví dụ trên). Hàm main được viết lại như sau:
func main() {
    mux := http.NewServeMux()
    fs := http.FileServer(http.Dir("html"))
    mux.Handle("/", fs)
    hdl := &liteHandler{"Chào mừng đến lập trình Go cho web!"}
    mux.Handle("/welcome", hdl)
    http.ListenAndServe(":8080", mux)
}
- Hàm http.NewServeMux() được gọi để tạo ra đối tượng ServeMux mux. 
- Hàm http.FileServer được gọi để tạo ra handler phục vụ cung cấp cây thư mục con thư mục html. 
- Phương thức Handle được gọi để đăng ký URL "/" với handler fs. 
- Tương tự biến hdl được tạo và đăng ký với mux.
- Cuối cùng hàm http.ListenAndServe được gọi để tạo web server. 
- Chạy thử ứng dụng này, sau đó mở trình duyệt truy cập localhost:8080/ sẽ cho kết quả là danh sách các thư mục và file có chứa trong thư mục html. Chọn một file cụ thể trong trang, nếu hiển thị được trình duyệt sẽ hiển thị, còn không nó sẽ thực hiện việc tải file về. Tuy nhiên nếu vào localhost:8080/welcome, chúng ta sẽ nhận được chuỗi như bên trên.

Hàm đóng vai trò handler


Như vậy là chúng ta có thể sử dụng handler do Go cung cấp hoặc tự tạo riêng để xử lý các yêu cầu của client. Tuy nhiên mỗi lần tạo handler lại phải tạo struct rồi khai báo phương thức thỏa mãn http.Handler rất rối rắm, nhất là khi có cả trăm handler như vậy. Làm sao để có thể sử dụng hàm làm handler là tiện nhất. Và Go cung cấp một số cách giúp đáp ứng nhu cầu này:
- Dùng http.HandleFunc
- Dùng ServeMux.HandleFunc

 Tạo handler với http.HandleFunc


http.HandleFunc là một kiểu dữ liệu hàm được khai báo như sau:
type HandlerFunc func(ResponseWriter, *Request)
Do đó bất kỳ hàm nào có dạng func(ResponseWriter, *Request) đều thuộc kiểu dữ liệu này. Ngoài ra nó có khai báo phương thức ServeHTTP (ResponseWriter, *Request) nên nó thỏa mãn một http.Handler. Nói cách khác, chỉ cần tạo hàm có dạng func(ResponseWriter, *Request) là có thể đăng ký nó như một Handler sau khi ép kiểu về http.HandleFunc. Để hiểu rõ hơn chúng ta xem ví dụ như sau:
func liteHandler(w http.ResponseWriter, r *http.Request) {
    fmt.Fprintf(w, "Chào mừng đến lập trình Go cho web!")
}
func main() {
    mux := http.NewServeMux()
    // Ép kiểu liteHandler về http.HandleFunc
    hdl := http.HandlerFunc(liteHandler)
    mux.Handle("/welcome", hdl)
    http.ListenAndServe(":8080", mux)
}

  Tạo hander với ServeMux.HandleFunc


Để tránh phải rối rắm khi ép kiểu về http.HandlerFunc thì ServeMux cung cấp phương thức HandleFunc như sau: HandleFunc(pattern string, handler func(ResponseWriter, *Request)). Tham số đầu là đường dẫn tài nguyên URI, còn tham số sau là hàm có dạng func(ResponseWriter, *Request) đóng vai trò handler. Thật sự bên dưới thì ServeMux cũng ép kiểu về http.HandlerFunc và dùng nó để đăng ký handler. Ví dụ trên được viết lại theo cách này như sau:
func main() {
    mux := http.NewServeMux()
    mux.HandleFunc("/welcome", liteHandler)
    http.ListenAndServe(":8080", mux)
}

Dùng DefaultServeMux


Ở các ví dụ trên chúng ta thấy mỗi lần khai báo web server đều phải tạo 1 đối tượng ServeMux. Để thuận tiện hơn thì package net/http đã tạo ra một ServeMux mặc định, gọi là DefaultServeMux. Để sử dụng nó chúng ta chỉ việc khai báo nil ở tham số thứ 2 của http.ListenAndServe. 

Câu hỏi đặt ra lúc này là vậy muốn khai báo handler với DefaultServeMux thì làm thế nào? Package net/http cung cấp 2 hàm http.Handle(pattern string, handler Handler) và http.HandleFunc(pattern string, handler func(ResponseWriter, *Request)) giúp chúng ta thực hiện việc này. Sử dụng 2 hàm này tương tự như sử dụng hai hàm này của ServeMux ở trên.

Ví dụ trên viết lại như sau:
func main() {
    http.HandleFunc("/welcome", liteHandler)
    http.ListenAndServe(":8080", nil)
}
Mọi thứ trở nên gọn nhẹ đơn giản hơn nhiều đúng không?

Cấu trúc http.Server


Các ví dụ trên cho chúng ta thấy dễ dàng như thế nào khi tạo một web server: chỉ hàm http.ListenAndServe là đã có 1 web server rồi. Vấn đề đặt ra là nếu muốn đòi hỏi web server một số cấu hình nâng cao như thời gian timeout khi đọc yêu cầu hay gửi phản hồi thì làm thế nào? Thực ra bản thân hàm http.ListenAndServe khi chạy sẽ tạo ra một đối tượng của cấu trúc http.Server với các thông số mặc định bên cạnh 2 thông số địa chỉ và hander đưa vào rồi gọi phương thức ListenAndServe() của cấu trúc Server này để tạo web server. Dựa vào đó chúng ta có thể tạo riêng cho mình đối tượng Server với các thông số cấu hình chúng ta muốn rồi tạo web server với đối tượng này.

Trước tiên chúng ta tìm hiểu cấu trúc http.Server
type Server struct {
    Addr string                             // <Địa chỉ IP/tên miền>:<cổng>
    Handler Handler                    // Đối tượng xử yêu cầu
    ReadTimeout time.Duration  // Thời gian tối đa đọc yêu cầu
    WriteTimeout time.Duration // Thời gian tối đa ghi phản hồi
    MaxHeaderBytes int             // Chiều dàu phần đầu tính theo byte, kể cả dòng yêu cầu
    TLSConfig *tls.Config         // Cấu hình TLS, tùy chọn
    TLSNextProto map[string]func(*Server, *tls.Conn, Handler)
    ConnState func(net.Conn, ConnState)
    ErrorLog *log.Logger
}
Chúng ta cùng tìm hiểu qua cách sử dụng cấu trúc http.Server này bằng cách sửa lại ví dụ trên như sau:
func main() {
    http.HandleFunc("/welcome", liteHandler)
    server := &http.Server{
        Addr: ":8080",
        ReadTimeout: 10 * time.Second,
        WriteTimeout: 10 * time.Second,
        MaxHeaderBytes: 1 << 20,
    }
    server.ListenAndServe()
}

 Như vậy chúng ta đã tạo được một web server và còn có thể cấu hình cho nó nữa. Nói cách khác ta đã làm chủ một web server viết bằng Go với chỉ vài dòng code.

Trong bài tới chúng ta sẽ cùng tìm hiểu về cách xử lý biểu mẫu trong các ứng dụng web.


Tóm tắt:
- Package net/http của Go cung cấp hàm http.ListenAndServe(addr string, handler Handler) để tạo web server.
- Bộ phận tiếp nhập yêu cầu và định hướng xử lý yêu cầu: ServeMux.
- Bộ phận trực tiếp xử lý yêu cầu và gửi phản hồi: Handler.
- Sử dụng DefaultServeMux nếu không có nhu cầu đặc biệt.
- Để sử dụng hàm làm handler, hàm cần có dạng func(ResponseWriter, *Request) và sử dụng http.HandleFunc để đăng ký.

No comments:

Post a Comment