Web server luôn là project thú vị và đơn giản khi chúng ta bắt đầu học 1 ngôn ngữ lập trình mới. Trong Go, nó cũng không khác biệt, để build 1 web server chúng ta sử dụng package 'net/http', đây là cách tuyệt vời để chúng ta nắm được những kiến thức cơ bản.

Trong bài viết này, chúng ta sẽ tập trung để tạo 1 web server rất đơn giản, sử dụng package 'net/http'. Nếu bạn đã từng sử dụng ExpressJS của Node hay Tornado của Python, thì hy vọng bạn sẽ thấy một số điểm tương đồng về cách xử lý giữa chúng.

Trước tiên, chúng ta hãy tập trung build 1 server đơn giản, có thể phục vụ trả về nội dung cho các client gửi request tới. Sau khi làm được điều này, tiếp tục chúng ta sẽ tìm cách để trả về cho client các file tĩnh. Cuối cùng là sử dụng HTTP qua TLS (HTTPS).

Yêu cầu

Máy của bạn phải cài đặt Go version 1.11++

Tạo 1 Web Server cơ bản

Chúng ta sẽ tạo 1 web server rất đơn giản. Nó sẽ chỉ trả về đường dẫn URL trong truy vấn của chúng ta. Nó sẽ là nền tảng để chúng ta tiếp tục tìm hiểu.

package main

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

func main() {
    http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
        fmt.Fprintf(w, "Hello, %q", html.EscapeString(r.URL.Path))
    })

    http.HandleFunc("/hi", func(w http.ResponseWriter, r *http.Request) {
        fmt.Fprintf(w, "Hi")
    })

    log.Fatal(http.ListenAndServe(":8081", nil))
}

Trong code bên trên, chúng ta định nghĩa 2 Handlers khác nhau. Các handlers này sẽ trả về response tương ứng cho các 'HTTP' request có định dạng trùng khớp với tham số đầu tiên của hàm. Vì vậy, về cơ bản bất cứ khi nào một yêu cầu được thực hiện cho trang chủ hoặc http://localhost:8081/, chúng ta sẽ thấy handler đầu tiên của chúng ta phản hồi khi truy vấn khớp với định dạng đó.

Chạy Server của chúng ta

Bây giờ hãy tạo server của chúng ta bằng lệnh quen thuộc go run server.go trong console của mình. Sau khi hoàn tất, hãy truy cập trình duyệt của bạn và truy cập http://localhost:8081/world. Trên trang này, chúng ta thấy chuỗi truy vấn của mình được trả lại là Hello, word

Phức tạp hơn

Bây giờ chúng ta đã thiết lập một máy chủ web cơ bản, hãy thử tăng bộ đếm mỗi khi một URL cụ thể được truy cập. Do máy chủ web không đồng bộ, chúng ta sẽ phải "bảo vệ" bộ đếm của mình bằng mutex để ngăn chúng ta gặp phải các lỗi do race-condition.

Lưu ý - Nếu bạn không chắc mutex là gì, đừng lo lắng, điều này chỉ được sử dụng để làm nổi bật rằng các máy chủ này không được "bảo vệ" trước các race-condition. Nếu bạn muốn tìm hiểu thêm về mutexes, bạn có thể xem hướng dẫn khác tại đây https://tutorialedge.net/golang/go-mutex-tutorial/

package main

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

var counter int
var mutex = &sync.Mutex{}

func main() {
    http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
        fmt.Fprintf(w, "Hello, %q", html.EscapeString(r.URL.Path))
    })

    http.HandleFunc("/hi", func(w http.ResponseWriter, r *http.Request) {
        fmt.Fprintf(w, "Hi")
    })

    http.HandleFunc("/increment", incrementCounter)

    log.Fatal(http.ListenAndServe(":8081", nil))
}

func incrementCounter(w http.ResponseWriter, r *http.Request) {
    mutex.Lock()
    counter++
    fmt.Fprintf(w, strconv.Itoa(counter))
    mutex.Unlock()
}

Gọi đường dẫn trên trình duyệt http://localhost:8081/increment và bạn sẽ thấy số lượng tăng dần theo số lần gửi request tới.

Trả về file tĩnh

Bây giờ chúng ta đã nhận thấy bản chất bất đồng bộ của web server trên Go, chúng ta có thể chuyển sang trả về một số tệp tĩnh.

Trước tiên, hãy tạo một thư mục tĩnh trong thư mục project của bạn và sau đó tạo một số file HTML đơn giản. Đối với ví dụ này, hãy xem file html sau:

<html>
  <head>
    <title>Hello World</title>
  </head>
  <body>
    <h2>Hello World!</h2>
  </body>
</html>

Tiếp theo chúng ta có thể sửa đổi code trên server của mình sử dụng phương thức http.ServeFile. Về cơ bản, chúng ta sẽ đưa vào url của request được gửi đến server và nếu nó chứa chỉ mục index.html thì nó sẽ trả về tệp index.html, được hiển thị dưới dạng HTML trong trình duyệt.

Nếu chúng ta tạo một trang edit.html và gửi một request đến http://localhost:8081/edit.html thì nó sẽ trả về bất kỳ nội dung HTML nào bạn chọn để đưa vào trang edit.html đó.

package main

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

func main() {

    http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
        http.ServeFile(w, r, r.URL.Path[1:])
    })

    http.HandleFunc("/hi", func(w http.ResponseWriter, r *http.Request) {
        fmt.Fprintf(w, "Hi")
    })

    log.Fatal(http.ListenAndServe(":8081", nil))

}

Chạy thử nhé. Hãy gọi đường dẫn http://localhost:8081/index.html và hy vọng bạn sẽ thấy tệp index.html rất đơn giản của mình được hiển thị.

Trả về nội dung từ 1 thư mục

Phương pháp trên trả về tệp dựa trên đường dẫn của nó, làm theo cách này có nghĩa là chúng ta có cấu trúc thư mục giống như sau:

- main.go ## our webserver
- index.html
- styles/
- - style.css
- images/
- - image1.png
- ...

Điều này đúng, nhưng nếu web server của chúng ta ngày càng phức tạp, chúng ta có thể muốn chuyển nội dung trang web của mình vào một thư mục của riêng nó. Hãy tạo một thư mục trong project của chúng ta có tên là static/ sẽ chứa tất cả các tệp tĩnh trên trang web của chúng ta.

## Our Updated project structure
- main.go
- static/
- - index.html
- - styles/
- - - style.css
- - images/
- - - image1.png
- ...

Nếu chúng ta muốn làm điều này, chúng ta sẽ cần sửa đổi code hiện có của mình như sau:

ackage main

import (
    "log"
    "net/http"
)

func main() {

    http.Handle("/", http.FileServer(http.Dir("./static")))

    log.Fatal(http.ListenAndServe(":8081", nil))
}

Như bạn có thể thấy, chúng ta đã không sử dụng phương thức HandleFunc và bắt đầu sử dụng http.Handle() truyền vào đường dẫn của chúng ta và http.Dir() trỏ đến thư mục static/ mới được tạo.

Trả về nội dung qua HTTPS

Tuyệt vời, như vậy chúng ta đã có thể tạo một máy chủ web thực sự đơn giản có thể trả về bất kỳ tệp tĩnh nào mà chúng ta muốn, nhưng chúng ta chưa nghĩ đến bảo mật. Chúng ta làm cách nào để bảo mật máy chủ web và trả về nội dung của mình bằng HTTPS?

Với Go, chúng ta có thể sửa đổi máy chủ web hiện có của mình để sử dụng http.ListenAndServeTLS như sau:

package main

import (
    "log"
    "net/http"
)

func main() {

    http.Handle("/", http.FileServer(http.Dir("./static")))

    log.Fatal(http.ListenAndServeTLS(":443", "server.crt", "server.key", nil))
}

Lưu ý - Trong ví dụ này, sử dụng tệp chứng chỉ server.crtserver.key đã được tạo.

Tạo Keys

Nếu bạn chưa tạo keys, bạn có thể tạo self-signed certs bằng cách sử dụng openssl:

$ openssl genrsa -out server.key 2048
$ openssl ecparam -genkey -name secp384r1 -out server.key
$ openssl req -new -x509 -sha256 -key server.key -out server.crt -days 3650

Điều này sẽ tạo self-signed certs và sau đó bạn có thể thử khởi động máy chủ web https của mình bằng lệnh go run main.go. Gọi đến https://localhost:8081, bạn sẽ thấy rằng kết nối hiện đã được bảo mật dựa trên self-signed certs.

Dựa theo bài viết: https://tutorialedge.net/golang/creating-simple-web-server-with-golang/