Phát triển web server tại Golang - từ đơn giản đến phức tạp

Phát triển web server tại Golang - từ đơn giản đến phức tạp

Năm năm trước tôi bắt đầu phát triển Gophish, điều này tạo cơ hội để tìm hiểu Golang. Tôi nhận ra rằng Go là một ngôn ngữ mạnh mẽ, được bổ sung bởi nhiều thư viện. Go rất linh hoạt: đặc biệt, nó có thể được sử dụng để phát triển các ứng dụng phía máy chủ mà không gặp vấn đề gì.

Bài viết này nói về việc viết một máy chủ trong Go. Hãy bắt đầu với những điều đơn giản như "Xin chào thế giới!" và kết thúc bằng một ứng dụng có các khả năng sau:

- Sử dụng Let's Encrypt cho HTTPS.
- Hoạt động như một bộ định tuyến API.
- Làm việc với phần mềm trung gian.
- Xử lý các tập tin tĩnh.
- Tắt máy đúng cách.

Hộp kỹ năng khuyến nghị: khóa học thực hành "Nhà phát triển Python từ đầu".

Chúng tôi nhắc nhở: cho tất cả độc giả của "Habr" - giảm giá 10 rúp khi đăng ký bất kỳ khóa học Skillbox nào bằng mã khuyến mại "Habr".

Chào thế giới!

Bạn có thể tạo một máy chủ web trong Go rất nhanh chóng. Dưới đây là ví dụ về cách sử dụng trình xử lý trả về "Xin chào thế giới!" đã hứa ở trên.

package main
 
import (
"fmt"
"net/http"
)
 
func main() {
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "Hello World!")
})
http.ListenAndServe(":80", nil)
}

Sau này, nếu bạn chạy ứng dụng và mở trang localhost, sau đó bạn sẽ nhìn thấy ngay dòng chữ “Xin chào thế giới!” (tất nhiên là nếu mọi thứ hoạt động chính xác).

Chúng ta sẽ sử dụng trình xử lý nhiều lần sau, nhưng trước tiên hãy hiểu cách mọi thứ hoạt động.

net / http

Ví dụ sử dụng gói net/http, nó là công cụ chính trong Go để phát triển cả máy chủ và máy khách HTTP. Để hiểu mã, chúng ta hãy hiểu ý nghĩa của ba thành phần quan trọng: http.Handler, http.ServeMux và http.Server.

Trình xử lý HTTP

Khi chúng tôi nhận được yêu cầu, trình xử lý sẽ phân tích yêu cầu đó và tạo phản hồi. Trình xử lý trong Go được triển khai như sau:

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

Ví dụ đầu tiên sử dụng hàm trợ giúp http.HandleFunc. Nó bao bọc một hàm khác, hàm này lần lượt đưa http.ResponseWriter và http.Request vào ServeHTTP.

Nói cách khác, các trình xử lý trong Golang được trình bày trong một giao diện duy nhất, mang lại nhiều tùy chọn cho lập trình viên. Vì vậy, ví dụ, phần mềm trung gian được triển khai bằng cách sử dụng một trình xử lý, trong đó ServeHTTP trước tiên thực hiện điều gì đó rồi gọi phương thức ServeHTTP của một trình xử lý khác.

Như đã đề cập ở trên, trình xử lý chỉ cần tạo phản hồi cho các yêu cầu. Nhưng trình xử lý cụ thể nào sẽ được sử dụng tại một thời điểm cụ thể?

Yêu cầu định tuyến

Để đưa ra lựa chọn đúng đắn, hãy sử dụng bộ ghép kênh HTTP. Trong một số thư viện, nó được gọi là muxer hoặc router, nhưng chúng đều giống nhau. Chức năng của bộ ghép kênh là phân tích đường dẫn yêu cầu và chọn trình xử lý thích hợp.

Nếu bạn cần hỗ trợ cho việc định tuyến phức tạp thì tốt hơn nên sử dụng thư viện của bên thứ ba. Một số tiên tiến nhất - khỉ đột / mux и go-chi/chi, những thư viện này giúp bạn có thể triển khai quá trình xử lý trung gian mà không gặp bất kỳ sự cố nào. Với sự trợ giúp của họ, bạn có thể định cấu hình định tuyến ký tự đại diện và thực hiện một số tác vụ khác. Ưu điểm của chúng là khả năng tương thích với các trình xử lý HTTP tiêu chuẩn. Kết quả là bạn có thể viết mã đơn giản có thể sửa đổi trong tương lai.

Làm việc với các khung phức tạp trong tình huống thông thường sẽ yêu cầu các giải pháp không chuẩn và điều này làm phức tạp đáng kể việc sử dụng trình xử lý mặc định. Để tạo ra phần lớn các ứng dụng, chỉ cần kết hợp thư viện mặc định và một bộ định tuyến đơn giản là đủ.

Xử lý truy vấn

Ngoài ra, chúng ta cần một thành phần sẽ “lắng nghe” các kết nối đến và chuyển hướng tất cả các yêu cầu đến trình xử lý chính xác. http.Server có thể dễ dàng xử lý tác vụ này.

Phần sau đây cho thấy máy chủ chịu trách nhiệm về tất cả các tác vụ liên quan đến xử lý kết nối. Ví dụ: điều này hoạt động bằng giao thức TLS. Để thực hiện lệnh gọi http.ListenAndServer, máy chủ HTTP tiêu chuẩn sẽ được sử dụng.

Bây giờ hãy xem xét các ví dụ phức tạp hơn.

Thêm Hãy mã hóa

Theo mặc định, ứng dụng của chúng tôi chạy trên giao thức HTTP nhưng bạn nên sử dụng giao thức HTTPS. Điều này có thể được thực hiện mà không gặp vấn đề gì trong Go. Nếu bạn đã nhận được chứng chỉ và khóa riêng thì chỉ cần đăng ký ListenAndServeTLS với chứng chỉ và tệp khóa chính xác là đủ.

http.ListenAndServeTLS(":443", "cert.pem", "key.pem", nil)

Bạn luôn có thể làm tốt hơn.

Hãy mã hóa cung cấp chứng chỉ miễn phí với khả năng gia hạn tự động. Để sử dụng dịch vụ, bạn cần có gói autocert.

Cách dễ nhất để định cấu hình nó là sử dụng phương thức autocert.NewListener kết hợp với http.Serve. Phương pháp này cho phép bạn lấy và cập nhật chứng chỉ TLS trong khi máy chủ HTTP xử lý các yêu cầu:

http.Serve(autocert.NewListener("example.com"), nil)

Nếu chúng ta mở trong trình duyệt example.com, chúng ta sẽ nhận được phản hồi HTTPS “Xin chào thế giới!”

Nếu bạn cần cấu hình chi tiết hơn thì bạn nên sử dụng trình quản lý autocert.Manager. Sau đó, chúng tôi tạo phiên bản http.Server của riêng mình (cho đến bây giờ chúng tôi vẫn sử dụng nó theo mặc định) và thêm trình quản lý vào máy chủ TLSConfig:

m := &autocert.Manager{
Cache:      autocert.DirCache("golang-autocert"),
Prompt:     autocert.AcceptTOS,
HostPolicy: autocert.HostWhitelist("example.org", "www.example.org"),
}
server := &http.Server{
    Addr:      ":443",
    TLSConfig: m.TLSConfig(),
}
server.ListenAndServeTLS("", "")

Đây là một cách dễ dàng để triển khai hỗ trợ HTTPS đầy đủ với việc gia hạn chứng chỉ tự động.

Thêm tuyến đường tùy chỉnh

Bộ định tuyến mặc định có trong thư viện chuẩn là tốt nhưng nó rất cơ bản. Hầu hết các ứng dụng đều yêu cầu định tuyến phức tạp hơn, bao gồm các tuyến đường lồng nhau và ký tự đại diện hoặc một quy trình để thiết lập các tham số và mẫu đường dẫn.

Trong trường hợp này nên sử dụng các gói khỉ đột / mux и go-chi/chi. Chúng ta sẽ học cách làm việc với cái sau - một ví dụ được hiển thị bên dưới.

Đưa ra là tệp api/v1/api.go chứa các tuyến cho API của chúng tôi:

/ HelloResponse is the JSON representation for a customized message
type HelloResponse struct {
Message string `json:"message"`
}
 
// HelloName returns a personalized JSON message
func HelloName(w http.ResponseWriter, r *http.Request) {
name := chi.URLParam(r, "name")
response := HelloResponse{
Message: fmt.Sprintf("Hello %s!", name),
}
jsonResponse(w, response, http.StatusOK)
}
 
// NewRouter returns an HTTP handler that implements the routes for the API
func NewRouter() http.Handler {
r := chi.NewRouter()
r.Get("/{name}", HelloName)
return r
}

Chúng tôi đặt tiền tố api/vq cho các tuyến đường trong tệp chính.

Sau đó, chúng ta có thể gắn cái này vào bộ định tuyến chính của mình dưới tiền tố api/v1/ trong ứng dụng chính của mình:

// NewRouter returns a new HTTP handler that implements the main server routes
func NewRouter() http.Handler {
router := chi.NewRouter()
    router.Mount("/api/v1/", v1.NewRouter())
    return router
}
http.Serve(autocert.NewListener("example.com"), NewRouter())

Tính dễ dàng làm việc với các tuyến đường phức tạp của Go giúp đơn giản hóa cấu trúc và bảo trì các ứng dụng lớn, phức tạp.

Làm việc với phần mềm trung gian

Giai đoạn bao gồm việc gói một trình xử lý HTTP bằng một trình xử lý khác, giúp có thể nhanh chóng thực hiện xác thực, nén, ghi nhật ký và một số chức năng khác.

Ví dụ: hãy xem giao diện http.Handler, chúng ta sẽ sử dụng nó để viết một trình xử lý xác thực người dùng dịch vụ.

func RequireAuthentication(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        if !isAuthenticated(r) {
            http.Redirect(w, r, "/login", http.StatusTemporaryRedirect)
            return
        }
        // Assuming authentication passed, run the original handler
        next.ServeHTTP(w, r)
    })
}

Có các bộ định tuyến của bên thứ ba, chẳng hạn như chi, cho phép bạn mở rộng chức năng của phần mềm trung gian.

Làm việc với các tập tin tĩnh

Thư viện chuẩn của Go bao gồm các khả năng làm việc với nội dung tĩnh, bao gồm hình ảnh, tệp JavaScript và CSS. Chúng có thể được truy cập thông qua chức năng http.FileServer. Nó trả về một trình xử lý phục vụ các tệp từ một thư mục cụ thể.

func NewRouter() http.Handler {
    router := chi.NewRouter()
    r.Get("/{name}", HelloName)
 
// Настройка раздачи статических файлов
staticPath, _ := filepath.Abs("../../static/")
fs := http.FileServer(http.Dir(staticPath))
    router.Handle("/*", fs)
    
    return r

Điều đáng ghi nhớ là http.Dir hiển thị nội dung của thư mục nếu nó không chứa tệp index.html chính. Trong trường hợp này, để tránh bị xâm phạm thư mục, bạn nên sử dụng gói unindexed.

Tắt máy đúng cách

Go cũng có một tính năng gọi là tắt máy chủ HTTP một cách duyên dáng. Điều này có thể được thực hiện bằng phương thức Shutdown(). Máy chủ được khởi động trong một goroutine và sau đó kênh được lắng nghe để nhận tín hiệu ngắt. Ngay sau khi nhận được tín hiệu, máy chủ sẽ tắt, nhưng không phải ngay lập tức mà sau vài giây.

handler := server.NewRouter()
srv := &http.Server{
    Handler: handler,
}
 
go func() {
srv.Serve(autocert.NewListener(domains...))
}()
 
// Wait for an interrupt
c := make(chan os.Signal, 1)
signal.Notify(c, os.Interrupt)
<-c
 
// Attempt a graceful shutdown
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
srv.Shutdown(ctx)

Như một kết luận

Go là một ngôn ngữ mạnh mẽ với thư viện tiêu chuẩn gần như phổ biến. Các khả năng mặc định của nó rất rộng và chúng có thể được nâng cao bằng các giao diện - điều này cho phép bạn phát triển các máy chủ HTTP thực sự đáng tin cậy.

Hộp kỹ năng khuyến nghị:

Nguồn: www.habr.com

Thêm một lời nhận xét