編寫 HTTP(S) 服務器 - Go Web 開發實戰筆記

1、Web 工做方式

對於普通的上網過程,系統實際上是這樣作的:瀏覽器自己是一個客戶端,當輸入 URL 請求網頁時,首先瀏覽器會去請求 DNS 服務器,獲取與域名對應的 IP,而後經過 IP 地址找到對應的服務器後,要求創建 TCP 鏈接,等瀏覽器發送完 HTTP Request (請求)包後,服務器接收到請求包以後纔開始處理請求包,服務器調用自身服務,返回 HTTP Response(響應)包;客戶端收到來自服務器的響應後開始渲染這個 Response 包 裏的主體(body),等收到所有的內容隨後斷開與該服務器之間的 TCP 鏈接。html

用戶訪問一個web站點的過程

以上 Web 工做方式能夠簡單地概括爲:web

  1. 客戶端經過 DNS 獲取服務器網絡 IP
  2. 客戶端經過 TCP/IP 協議創建到服務器的 TCP 鏈接
  3. 客戶端向服務器發送 HTTP 協議請求包,請求服務器裏的資源文檔
  4. 服務器向客戶機發送 HTTP 協議應答包,若是請求的資源包含有動態語言的內容,那麼服務器會調用動態語言的解釋引擎負責處理「動態內容」,並將處理獲得的數據返回給客戶端
  5. 客戶機與服務器斷開。由客戶端解釋 HTML 文檔,在客戶端屏幕上渲染圖形結果

服務器端的幾個概念:編程

  • Request:用戶請求的信息,用來解析用戶的請求信息,包括post、get、cookie、url等信息
  • Response:服務器須要反饋給客戶端的信息
  • Conn:用戶的每次請求連接
  • Handler:處理請求和生成返回信息的處理邏輯

如下是 http 包執行流程:後端

http 包執行流程

  1. 服務端啓動 Socket, 建立 Listen Socket, 監聽指定的端口, 等待客戶端請求到來。
  2. 客戶端連接服務端
  3. 客戶端發送請求(http)
  4. 服務端接收到客戶端的請求,判斷是否爲 HTTP/HTTPS 請求,若是是,則讀取 HTTP/HTTPS 請求頭和 body 數據。
  5. 使用 HTTP/HTTPS 格式生成返回信息(HTTP/HTTPS 響應頭、響應數據)
  6. 將這些信息經過 Socket 返回給客戶端,完成了一個請求響應的過程

2、使用 Go 語言編寫一個HTTP服務器

Go 語言的標準庫 net/http 提供了 http 編程有關的接口,封裝了內部 TCP 鏈接和報文解析的複雜瑣碎的細節,使用者只須要和 http.request 、 http.ResponseWriter 這兩個對象交互就行。也就是說,只要寫一個 handler,請求會經過參數傳遞進來,而它要作的就是根據請求的數據作處理,把結果寫到 Response 中。
瀏覽器

如下是一個簡單的 http server 示例:
WebServer.go

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

// HelloServer 函數實現了處理器的簽名,因此這是一個處理器函數
func HelloServer(w http.ResponseWriter,r *http.Request)  {
	fmt.Println("path:", r.URL.Path)
	fmt.Fprintf(w, "Hello Go Web")
}

func main()  {
    // 註冊路由和路由函數,將url規則與處理器函數綁定作一個map映射存起來,而且會實現ServeHTTP方法,使處理器函數變成Handler函數
    http.HandleFunc("/",HelloServer)
    
    fmt.Println("服務器已經啓動,請在瀏覽器地址欄中輸入 http://localhost:8900")
    
    // 啓動 HTTP 服務,並監聽端口號,開始監聽,處理請求,返回響應
    err := http.ListenAndServe(":8900", nil)
    
    fmt.Println("監聽以後")
    if err != nil {
        log.Fatal("ListenAndServe",err)
    }
}
複製代碼

執行以上程序後,在瀏覽器輸入 http://localhost:8900,在控制檯輸出:安全

服務器已經啓動,請在瀏覽器地址欄中輸入 http://localhost:8900
path: /
複製代碼

瀏覽器訪問的網頁顯示:Hello Go Webbash

1. 爲HTTP服務器指定多個路由

在實際開發中,HTTP 接口會有許多的 URL 和對應的 Handler。這裏就要用到 net/http 的 ServeMux。Mux是 multiplexor 的縮寫,就是多路傳輸的意思(請求傳過來,根據某種判斷,分流到後端多個不一樣的地方)。ServeMux 能夠註冊多了 URL 和 handler 的對應關係,並自動把請求轉發到對應的 handler 進行處理。服務器

Go 語言實現一個 web 路由主要作三件事:cookie

  1. 監聽端口
  2. 接收客戶端的請求
  3. 爲每一個請求分配對應的 handler

如下是實現一個簡易的 http 路由示例:
WebServerRoute.go網絡

package main

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

// 首頁處理器
func HomeHandler (w http.ResponseWriter, r *http.Request)  {
	fmt.Println("path:", r.URL.Path)
	fmt.Fprintf(w, "Welcome to home")
}

// 註冊頁處理器
func registerHandler (w http.ResponseWriter, r *http.Request)  {
	fmt.Println("path:", r.URL.Path)
	fmt.Fprintf(w, "Welcome to register")
}

// 登陸頁處理器
func loginHandler (w http.ResponseWriter, r *http.Request)  {
	fmt.Println("path:", r.URL.Path)
	fmt.Fprintf(w, "Welcome to login")
}

func main()  {
	// 路由:/home -- 首頁
	http.HandleFunc("/home",loginHandler)

	// 路由:/register -- 註冊頁
	http.HandleFunc("/register",registerHandler)

	// 路由:/login -- 登陸頁
	http.HandleFunc("/login",loginHandler)


	fmt.Println("服務器已經啓動,訪問:\n首頁地址:http://localhost:8900/home\n註冊頁地址:http://localhost:8900/register\n登陸頁地址:http://localhost:8900/login")
	err := http.ListenAndServe(":8900", nil)
	fmt.Println("監聽以後")
	if err != nil {
		log.Fatal("ListenAndServe",err)
	}
}
複製代碼

執行以上程序後,在瀏覽器先後輸入
http://localhost:8900/home
http://localhost:8900/register
http://localhost:8900/login
在控制檯輸出:

服務器已經啓動,訪問:
首頁地址:http://localhost:8900/home
註冊頁地址:http://localhost:8900/register
登陸頁地址:http://localhost:8900/login
path: /home
path: /register
path: /login
複製代碼

瀏覽器訪問的網頁先後分別顯示:
Welcome to home
Welcome to register
Welcome to login

2. 獲取 HTTP 請求頭信息

如下示例獲取 HTTP 請求頭: Path、Host、Method(Get、post)、Proto、UserAgent 等信息
RequestInfo.go

package main

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

// HelloServer2 函數實現了處理器的簽名,因此這是一個處理器函數
func HelloServer2(w http.ResponseWriter,r *http.Request)  {
	fmt.Println("path:", r.URL.Path)
	fmt.Println("Url:",r.URL)
	fmt.Println("Host:",r.Host)
	fmt.Println("Header:",r.Header)
	fmt.Println("Method:",r.Method)
	fmt.Println("Proto:",r.Proto)
	fmt.Println("UserAgent:",r.UserAgent())

	fmt.Fprintf(w, "Hello Go Web")
}

func main()  {
	// 註冊路由和路由函數,將url規則與處理器函數綁定作一個map映射存起來,而且會實現ServeHTTP方法,使處理器函數變成Handler函數
	http.HandleFunc("/",HelloServer2)

	fmt.Println("服務器已經啓動,請在瀏覽器地址欄中輸入 http://localhost:8900/a/b")

	// 啓動 HTTP 服務,並監聽端口號,開始監聽,處理請求,返回響應
	err := http.ListenAndServe(":8900", nil)

	fmt.Println("監聽以後")
	if err != nil {
		log.Fatal("ListenAndServe",err)
	}
}
複製代碼

執行以上程序後,在瀏覽器輸入 http://localhost:8900/a/b , 在控制檯輸出:

服務器已經啓動,請在瀏覽器地址欄中輸入 http://localhost:8900/a/b
path: /a/b
Url: /a/b
Host: localhost:8900
Header: map[Accept:[text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3] Accept-Encoding:[gzip, deflate, br] Cookie:[UM_distinctid=1682d397d27566-07ba4cbda8d037-2d604637-4a640-1682d397d2a745; _ga=GA1.1.301532435.1546946971; Hm_lvt_ac60c3773958d997a64b55feababb4a1=1547204629] Upgrade-Insecure-Requests:[1] User-Agent:[Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/75.0.3770.100 Safari/537.36] Accept-Language:[en,zh-CN;q=0.9,zh;q=0.8] Connection:[keep-alive] Cache-Control:[max-age=0]]
Method: GET
Proto: HTTP/1.1
UserAgent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/75.0.3770.100 Safari/537.36
複製代碼

瀏覽器訪問的網頁顯示:Hello Go Web

3. 獲取完整的請求路徑

完整的請求路徑由 scheme(http/https) + 域名或IP(localhost) + 端口號 + Path 組成,示例以下:
FullRequestPath.go

package main
import (
	"fmt"
	"log"
	"net/http"
	"strings"
)

/*
獲取完整的請求路徑
http://localhost:8900/a/b/x.html

1. scheme: http/https
2. 域名或IP: localhost
3. 端口號: 8900
4. Path: /a/b/x.html
*/

// HelloServer 函數實現了處理器的簽名,因此這是一個處理器函數
func HelloServer3(w http.ResponseWriter,r *http.Request)  {
	scheme := "http://"
	if r.TLS != nil {
		scheme = "https://"
	}
	fmt.Println("scheme:", scheme)
	fmt.Println("域名(IP)和端口號:", r.Host)
	fmt.Println("Path:", r.RequestURI)
	fmt.Println("完整的請求路徑:", strings.Join([]string{scheme,r.Host,r.RequestURI},""))
	fmt.Fprintf(w, "Hello Go Web")
}

func main()  {
	// 註冊路由和路由函數,將url規則與處理器函數綁定作一個map映射存起來,而且會實現ServeHTTP方法,使處理器函數變成Handler函數
	http.HandleFunc("/",HelloServer3)

	fmt.Println("服務器已經啓動,請在瀏覽器地址欄中輸入 http://localhost:8900/a/b/x.html")

	// 啓動 HTTP 服務,並監聽端口號,開始監聽,處理請求,返回響應
	err := http.ListenAndServe(":8900", nil)

	fmt.Println("監聽以後")
	if err != nil {
		log.Fatal("ListenAndServe",err)
	}
}
複製代碼

執行以上程序後,在瀏覽器輸入 http://localhost:8900/a/b/x.html ,在控制檯輸出:

服務器已經啓動,請在瀏覽器地址欄中輸入 http://localhost:8900/a/b/x.html
scheme: http://
域名(IP)和端口號: localhost:8900
Path: /a/b/x.html
完整的請求路徑: http://localhost:8900/a/b/x.html
複製代碼

瀏覽器訪問的網頁顯示:Hello Go Web

4. 編寫 HTTPS 服務器

HTTP 服務器不一樣於 HTTPS 服務器,HTTP 協議是明文的,HTTPS 協議(HTTP over SSL 或 HTTP over TLS )是密文的。

使用如下 openssl 方式手動生成 SSL 證書:

生成密鑰文件命令:

openssl genrsa -out server.key 2048
複製代碼

結合已經生成的密鑰文件生成證書csr文件命令:

openssl req -new -x509 -sha256 -key server.key -out server.crt -days 3650
複製代碼

SSL證書申請

在生成csr文件的過程當中,會提示你輸入證書所要求的字段信息,包括國家(中國添CN)、省份、所在城市、單位名稱、單位部門名稱(能夠不填直接回車)。請注意: 除國家縮寫必須填CN外,其他均可以是英文或中文。

HttpsServer.go 文件代碼以下:

package main

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

/*
編寫 HTTPS 服務器
HTTPS = HTTP + Secure(安全)

RSA 進行加密
SHA 進行驗證
密鑰和證書

生成密鑰文件
openssl genrsa -out server.key 2048

生成證書文件
openssl req -new -x509 -sha256 -key server.key -out server.crt -days 3650

*/

func httpsServer(w http.ResponseWriter, r *http.Request)  {
	fmt.Println("path:", r.URL.Path)
	fmt.Println("Url:",r.URL)
	fmt.Println("Host:",r.Host)
	fmt.Println("Header:",r.Header)
	fmt.Println("Method:",r.Method)
	fmt.Println("Proto:",r.Proto)
	fmt.Println("UserAgent:",r.UserAgent())

	scheme := "http://"
	if r.TLS != nil {
		scheme = "https://"
	}
	fmt.Println("完整的請求路徑:", strings.Join([]string{scheme,r.Host,r.RequestURI},""))
	fmt.Fprintf(w, "Hello Go Web")
}

func main() {
	http.HandleFunc("/",httpsServer)
	fmt.Println("HTTPS 服務器已經啓動,請在瀏覽器地址欄中輸入 https://localhost:4321/")
	err := http.ListenAndServeTLS(":4321","/Users/play/goweb/src/basic/server.crt","/Users/play/goweb/src/basic/server.key",nil)
	if err != nil {
		log.Fatal("ListenAndServe",err)
	}
}
複製代碼

執行以上程序後,在瀏覽器輸入 https://localhost:4321/ ,在控制檯輸出:

HTTPS 服務器已經啓動,請在瀏覽器地址欄中輸入 https://localhost:4321/
2019/07/06 19:35:55 http: TLS handshake error from [::1]:58945: remote error: tls: unknown certificate
path: /
Url: /
Host: localhost:4321
Header: map[Cache-Control:[max-age=0] Upgrade-Insecure-Requests:[1] User-Agent:[Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/75.0.3770.100 Safari/537.36] Accept:[text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3] Accept-Encoding:[gzip, deflate, br] Accept-Language:[en,zh-CN;q=0.9,zh;q=0.8] Cookie:[UM_distinctid=1682d397d27566-07ba4cbda8d037-2d604637-4a640-1682d397d2a745; _ga=GA1.1.301532435.1546946971; Hm_lvt_ac60c3773958d997a64b55feababb4a1=1547204629]]
Method: GET
Proto: HTTP/2.0
UserAgent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/75.0.3770.100 Safari/537.36
完整的請求路徑: https://localhost:4321/
複製代碼

瀏覽器訪問的網頁顯示:Hello Go Web。 由於以上申請的 SSL 證書沒有進行認證,因此會提示"unknown certificate",未認證證書只能用於開發測試。

相關文章
相關標籤/搜索