Triển khai máy chủ HTTP cơ bản bằng cách sử dụng Go

Post on: 2023-05-15 22:35:54 | in: Golang
Bài viết này sẽ trình bày cách triển khai HTTP server trong ngôn ngữ lập trình Go. Gói go "net" chứa các gói tiện ích để xử lý các chức năng mạng.

Tổng quan

HTTP (Hypertext Transfer Protocol) là một giao thức ở tầng ứng dụng và hoạt động ở chế độ client-server. HTTP server là chương trình chạy trên một máy tính. Nó lắng nghe và phản hồi các yêu cầu HTTP trên địa chỉ IP của nó với một cổng cụ thể. Vì HTTP là nền tảng của World Wide Web và được sử dụng để tải bất kỳ trang web nào, mỗi nhà phát triển phần mềm đều gặp phải tình huống khi cần triển khai một HTTP Server để phản hồi một yêu cầu HTTP.

Gói "net" chứa gói "http" cung cấp cả các giao thức HTTP client (để thực hiện các yêu cầu http) và HTTP server (lắng nghe các yêu cầu http). Trong bài viết này, chúng ta sẽ tìm hiểu về HTTP server. Dưới đây là câu lệnh để nhập gói http:

import "net/http"

Vấn đề cốt lõi để hiểu cách triển khai HTTP server là hiểu các điều sau đây:

  • Request – nó xác định các tham số yêu cầu, chẳng hạn như Phương thức, Chữ ký API, tiêu đề yêu cầu, thân yêu cầu, tham số truy vấn v.v.
  • Response – xác định các tham số phản hồi, chẳng hạn như Mã trạng thái, thân phản hồi, tiêu đề.
  • Pair of API signature and its handler – Mỗi chữ ký API tương ứng với một bộ xử lý. Bạn có thể coi bộ xử lý như là một hàm được gọi khi yêu cầu được thực hiện cho chữ ký API cụ thể đó. Mux đăng ký các cặp này của chữ ký API và bộ xử lý tương ứng.
  • Mux– Nó hoạt động như một bộ định tuyến. Dựa vào chữ ký API của yêu cầu, nó định tuyến yêu cầu đó đến bộ xử lý đã đăng ký cho chữ ký API đó. Bộ xử lý sẽ xử lý yêu cầu đến và cung cấp phản hồi. Ví dụ, một cuộc gọi API với "/v2/teachers" có thể được xử lý bởi một chức năng khác và cuộc gọi API với "/v2/students" có thể được xử lý bởi một chức năng khác. Vì vậy, dựa trên chữ ký API (và đôi khi cả phương thức yêu cầu), 
  • Listener – Nó chạy trên máy tính, lắng nghe một cổng cụ thể. Mỗi khi nó nhận được yêu cầu trên cổng đó, nó chuyển tiếp yêu cầu đó đến Mux. Nó cũng xử lý các chức năng khác nhưng chúng tôi sẽ không thảo luận chúng trong bài viết này.

Còn nhiều điều hơn khi nói đến HTTP, nhưng để đơn giản, chúng tôi chỉ nói về năm điều trên. Sơ đồ dưới đây cho thấy tương tác cho một yêu cầu API xuất phát từ client.

Hãy xem một ví dụ. Dưới đây là hai cặp chữ ký

  • “/v1/abc”  and handlerfunc_1
  • “/v1/xyz” and handlerfunc_2

Khi client gọi API “/v1/abc”, listener sẽ chuyển tiếp yêu cầu đó tới mux và mux sẽ định tuyến nó tới handler phù hợp handlerfunc_1.

Khi client gọi API “/v1/xyz”, listener sẽ chuyển tiếp yêu cầu đó tới mux và mux sẽ định tuyến nó tới handler phù hợp handlerfunc_2.

Bây giờ chúng ta đã hiểu được những phần trên, hãy tiếp tục xem làm thế nào mỗi phần được thực hiện trong GO, sau đó ở cuối chúng ta sẽ xem một chương trình hoàn chỉnh với toàn bộ quy trình từ đầu đến cuối.

Request

Trong GO, một yêu cầu được biểu diễn bằng cấu trúc Request. Đây là liên kết tới cấu trúc đó: https://golang.org/pkg/net/http/#Request

Nó chứa các thông tin của yêu cầu như phương thức, Api Signature, các tiêu đề yêu cầu, body, tham số truy vấn, v.v.

Response

Trong GO, một phản hồi được biểu diễn bằng giao diện ResponseWriter. Đây là liên kết tới giao diện đó: https://golang.org/pkg/net/http/#ResponseWriter

Giao diện ResponseWriter được sử dụng bởi một trình xử lý HTTP để xây dựng một phản hồi HTTP. Nó cung cấp ba hàm để đặt các thông số phản hồi:

  • Header – để viết tiêu đề phản hồi
  • Write([]byte) – để viết nội dung phản hồi
  • WriteHeader(statusCode int) – để viết mã trạng thái HTTP.
Pair of API signature and its handler

Một chữ ký API và trình xử lý của nó được ghép đôi với nhau. Sau đó, trình xử lý được gọi bởi mux khi nó nhận được một cuộc gọi API khớp với chữ ký API. Một trình xử lý golang có thể là một hàm hoặc một kiểu.

  • Function –hàm này nên có chữ ký như sau:
func(ResponseWriter, *Request)
  • Type – kiểu này nên thực hiện giao diện Handler.
type Handler interface {
   ServeHTTP(ResponseWriter, *Request)
}

Hãy xem từng cái một.

  • Function – Một xử lý viên (handler) có thể chỉ đơn giản là một hàm có chữ ký dưới đây.
func(ResponseWriter, *Request)

Đây là một hàm handler đơn giản trong Go. Nó có hai đối số đầu vào. Đối số đầu tiên là ResponseWriter và đối số thứ hai là một con trỏ tới cấu trúc Request. Chúng ta đã thảo luận về cả hai đối số này trước đó.

Nếu một cặp chữ ký API và một hàm có chữ ký như trên được đăng ký trong mux, thì hàm này sẽ được gọi khi có một cuộc gọi API khớp với chữ ký API đó.

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

Giao diện Handler định nghĩa một hàm ServeHTTP. Nếu một chữ ký API và một loại (type) triển khai giao diện Handler được đăng ký cùng nhau trong đối tượng mux (đối tượng định tuyến), thì phương thức ServeHTTP cho loại này sẽ được gọi khi có một cuộc gọi API khớp với chữ ký API.

Nếu bạn để ý, chữ ký API của hàm được sử dụng làm trình xử lý (handler) và hàm ListenAndServe là giống nhau, đó là

func(ResponseWriter, *Request)

Các hàm này sẽ được gọi bởi mux (đối tượng định tuyến) tùy thuộc vào loại trình xử lý (handler). Cần lưu ý rằng hai chữ ký API khác nhau có thể có cùng một trình xử lý.

Mux

Nhiệm vụ của mux (multiplexer) là định tuyến yêu cầu tới trình xử lý đã đăng ký dựa trên chữ ký API (và đôi khi cũng dựa trên phương thức yêu cầu). Nếu chữ ký và trình xử lý không được đăng ký với mux, nó sẽ gửi một phản hồi lỗi 404.

Go cung cấp một mux mặc định được tích hợp trong ngôn ngữ - ServeMux (https://golang.org/pkg/net/http/#ServeMux). Cũng có các mux khác có sẵn trên thị trường cho Golang. Các framework web khác như Gin cung cấp mux riêng của chúng.

Dưới đây là cách chúng ta tạo một mux:

mux := http.NewServeMux()

Hãy xem cách chúng ta đăng ký một cặp chữ ký API và trình xử lý của nó với mux. Có hai trường hợp:

  • Khi trình xử lý là một hàm: Chúng ta đăng ký mẫu (pattern) là chữ ký API và hàm đó là một trình xử lý.
mux.HandleFunc(pattern, handlerFunc)
  • Khi trình xử lý là một loại (type) triển khai giao diện Handler:
mux.Handle(pattern, handler)

Listener

Bộ lắng nghe (listener) lắng nghe trên một cổng và chuyển tiếp yêu cầu tới mux, sau đó chờ đợi phản hồi. Khi nhận được phản hồi, nó gửi trả lại cho khách hàng. Một bộ lắng nghe trong Golang có thể được triển khai bằng cách sử dụng cấu trúc Server - https://golang.org/pkg/net/http/#Server.

Dưới đây là cách chúng ta tạo một máy chủ. Cũng có một số tham số khác mà chúng ta có thể chỉ định khi tạo máy chủ như ReadTimeout, WriteTimeout, v.v., nhưng điều đó không được đề cập trong phạm vi của bài hướng dẫn này. Tất cả các tham số không được cung cấp sẽ có giá trị mặc định là zero.

s := &http.Server{
  Addr:    ":8080",
  Handler: mux,
}

Thuộc tính Addr có kiểu dữ liệu là chuỗi (string) đại diện cho địa chỉ của máy chủ HTTP sẽ được khởi chạy trên đó.

Địa chỉ này được biểu diễn dưới dạng

{ip_address}:{port}

Nếu chỉ sử dụng :{port} là đối số addr, điều đó có nghĩa là máy chủ HTTP có thể được truy cập từ tất cả địa chỉ IP (vòng lặp, IP công cộng, IP nội bộ) của máy.

Người dùng cũng có thể sử dụng ":http" là giá trị đối số addr cho cổng địa chỉ ":80" và ":https" cho cổng địa chỉ ":443".

Một điều rất quan trọng cần lưu ý ở đây là ServerMux, một mux mặc định được tích hợp trong ngôn ngữ cũng có một phương thức ServeHTTP (https://golang.org/pkg/net/http/#ServeMux.ServeHTTP). Do đó, ServerMux cũng triển khai giao diện Handler vì nó định nghĩa phương thức ServeHTTP. Khi tạo máy chủ, như bạn có thể đã nhận thấy, chúng ta phải cung cấp một trình xử lý (handler) có kiểu giao diện Handler. Đây là lúc ServerMux triển khai giao diện Handler trở nên hữu ích, vì chúng ta có thể truyền một phiên bản của ServerMux khi tạo máy chủ. Điều quan trọng là hiểu rằng ServerMux có kiểu giao diện Handler ngoài việc đăng ký các cặp chữ ký API và trình xử lý khác nhau.

Sau khi máy chủ được tạo, chúng ta gọi phương thức ListenAndServe của server. Máy chủ sau đó bắt đầu lắng nghe cổng được cung cấp và khi nhận được bất kỳ cuộc gọi API nào trên cổng đó, nó gọi phương thức ServeHTTP của mux, từ đó định tuyến yêu cầu tới trình xử lý đã đăng ký. Hy vọng rằng năm điều trên đã trở nên rõ ràng. Hãy xem một chương trình hoạt động minh họa các điểm trên.

Sử dụng hàm ListenAndServe của máy chủ (server):

main.go

package main

import (
"net/http"
)

func main() {

//Create the default mux
mux := http.NewServeMux()

//Handling the /v1/teachers. The handler is a function here
mux.HandleFunc("/v1/teachers", teacherHandler)

//Handling the /v1/students. The handler is a type implementing the Handler interface here
sHandler := studentHandler{}
mux.Handle("/v1/students", sHandler)

//Create the server.
s := &http.Server{
  Addr:    ":8080",
  Handler: mux,
}
s.ListenAndServe()

}

func teacherHandler(res http.ResponseWriter, req *http.Request) {
data := []byte("V1 of teacher's called")
res.WriteHeader(200)
res.Write(data)
}

type studentHandler struct{}

func (h studentHandler) ServeHTTP(res http.ResponseWriter, req *http.Request) {
data := []byte("V1 of student's called")
res.WriteHeader(200)
res.Write(data)
}

Hãy hiểu chương trình trước khi chạy nó.

  • Chúng ta đã định nghĩa một hàm có tên là teacherHandler có chữ ký chấp nhận http.ResponseWriter và con trỏ tới http.Request.
func teacherHandler(res http.ResponseWriter, req *http.Request) {
data := []byte("V1 of teacher's called")
res.Header().Set("Content-Type", "application/text")
res.WriteHeader(200)
res.Write(data)
}
  • Chúng ta định nghĩa một cấu trúc có tên là studentHandler mà định nghĩa phương thức ServeHTTP. Vì vậy, studentHandler là một loại (type) triển khai giao diện Handler.
type studentHandler struct{}

func (h studentHandler) ServeHTTP(res http.ResponseWriter, req *http.Request) {
data := []byte("V1 of student's called")
res.Header().Set("Content-Type", "application/text")
res.WriteHeader(200)
res.Write(data)
}
  • Chúng ta tạo một phiên bản của ServerMux.
mux := http.NewServeMux()
  • Chúng ta đã đăng ký cặp chữ ký API "/v1/teachers" và trình xử lý tương ứng của nó teacherHandler.
mux.HandleFunc("/v1/teachers", teacherHandler)
  • Chúng ta đã đăng ký cặp chữ ký API "/v1/students" và trình xử lý tương ứng của nó studentHandler, một loại (type) triển khai giao diện Handler.
sHandler := studentHandler{}
mux.Handle("/v1/students", sHandler)
  • Chúng ta đã tạo máy chủ và cung cấp cho nó một phiên bản của ServerMux và cổng lắng nghe là 8080. Sau đó, gọi phương thức ListenAndServe trên phiên bản của máy chủ.
s := &http.Server{
  Addr:    ":8080",
  Handler: mux,
}
s.ListenAndServe()

Bây giờ, hãy chạy máy chủ.

go run main.go

Nó sẽ bắt đầu lắng nghe cổng 8080. Chương trình này không bao giờ thoát và tiến trình vẫn bị khóa cho đến khi bị chấm dứt một cách mạnh mẽ, điều này được khuyến khích vì bất kỳ máy chủ HTTP nào cũng nên hoạt động và chạy suốt thời gian. Bây giờ hãy thực hiện các cuộc gọi API.

Gọi API "v1/teachers" - Nó trả về phản hồi chính xác "V1 of teacher's called" kèm mã trạng thái 200 chính xác.

curl -v -X GET http://localhost:8080/v1/teachers
Note: Unnecessary use of -X or --request, GET is already inferred.
*   Trying ::1...
* TCP_NODELAY set
* Connected to localhost (::1) port 8080 (#0)
> GET /v1/teachers HTTP/1.1
> Host: localhost:8080
> User-Agent: curl/7.54.0
> Accept: */*
>
< HTTP/1.1 200 OK
< Content-Type: application/text
< Date: Sat, 11 Jul 2020 16:03:33 GMT
< Content-Length: 22
<
* Connection #0 to host localhost left intact
V1 of teacher's called

Gọi API "v1/students" - Nó trả về phản hồi chính xác "V1 of student's called" kèm mã trạng thái 200 chính xác.

curl -v -X GET http://localhost:8080/v1/students
Note: Unnecessary use of -X or --request, GET is already inferred.
*   Trying ::1...
* TCP_NODELAY set
* Connected to localhost (::1) port 8080 (#0)
> GET /v1/students HTTP/1.1
> Host: localhost:8080
> User-Agent: curl/7.54.0
> Accept: */*
>
< HTTP/1.1 200 OK
< Content-Type: application/text
< Date: Sat, 11 Jul 2020 16:04:27 GMT
< Content-Length: 22
<
* Connection #0 to host localhost left intact
V1 of student's called

Bạn cũng có thể thử các API này trên trình duyệt.

Đối với API "/v1/teachers"

For api "/v1/students"

Sử dụng hàm ListenAndServe của gói http

Vậy chúng ta đã xem qua một chương trình trong đó chúng ta đã xây dựng một mux, sau đó thêm các cặp chữ ký API và các trình xử lý tương ứng của chúng. Cuối cùng, chúng ta đã tạo một máy chủ và khởi động nó. Gói net/http cũng cung cấp một hàm ListenAndServe tạo ra một máy chủ mặc định và sử dụng mux mặc định để thực hiện cùng điều chúng ta đã thảo luận ở trên. Đó là một cách ngắn gọn để khởi động một máy chủ HTTP.

Hàm ListenAndServe có hai đối số đầu vào là addrhandler, và nó khởi động một máy chủ HTTP. Nó lắng nghe các yêu cầu HTTP đến và phục vụ yêu cầu khi nhận được. Dưới đây là chữ ký của hàm ListenAndServe:

func ListenAndServe(addr string, handler Handler) error

Dưới đây là cách gọi hàm này:

http.ListenAndServe(:8080, nil)

Nếu bạn chú ý, ở trên chúng ta đã gọi hàm ListenAndServe với giá trị nil cho handler.

http.ListenAndServe(:8080, nil)

Trong tình huống đó, một phiên bản mặc định của ServeMux (https://golang.org/pkg/net/http/#ServeMux) sẽ được tạo ra.

package main

import (
"net/http"
)

func main() {

//Handling the /v1/teachers
http.HandleFunc("/v1/teachers", teacherHandler)

//Handling the /v1/students
sHandler := studentHandler{}
http.Handle("/v1/students", sHandler)

http.ListenAndServe(":8080", nil)
}

func teacherHandler(res http.ResponseWriter, req *http.Request) {
data := []byte("V1 of teacher's called")
res.WriteHeader(200)
res.Write(data)
}

type studentHandler struct{}

func (h studentHandler) ServeHTTP(res http.ResponseWriter, req *http.Request) {
data := []byte("V1 of student's called")
res.WriteHeader(200)
res.Write(data)
}

Gói net/http cung cấp các hàm HandleFunc và Handle. Các hàm này hoạt động theo cách tương tự như các phương thức của mux.

Chạy máy chủ:

go run main.go

Kết quả sẽ giống như chúng ta đã thảo luận ở trên. Đây là tất cả về việc triển khai máy chủ HTTP cơ bản trong golang.

Tổng kết

Chúng ta đã học được rằng chúng ta có thể tạo một máy chủ HTTP theo hai cách:

Sử dụng server.ListenAndServe - https://golang.org/pkg/net/http/#Server.ListenAndServe Sử dụng http.ListenAndServe - https://golang.org/pkg/net/http/#ListenAndServe Cả hai cách này đều thực hiện cùng một công việc. Cách thứ hai sử dụng giá trị mặc định cho mọi thứ, trong khi cách thứ nhất cho phép bạn tạo ra một mux và một thể hiện của máy chủ một cách rõ ràng để bạn có thể chỉ định nhiều tùy chọn hơn. Do đó, tùy chọn đầu tiên linh hoạt hơn.

 
Tag: go Golang nâng cao