Написание сервера HTTP(S) — Практические советы по веб-разработке Go

Go

1. Как работает сеть

Для обычного процесса доступа в Интернет система фактически делает следующее: браузер сам является клиентом.При вводе URL-адреса для запроса веб-страницы браузер сначала запрашивает у DNS-сервера получение IP-адреса, соответствующего доменному имени, а затем найти соответствующий IP-адрес через IP-адрес.После отправки сервера на сервер требуется установить TCP-соединение.После отправки браузером пакета HTTP Request (запроса) сервер начинает обрабатывать пакет-запрос после получения пакет запроса.Сервер вызывает собственный сервис и возвращает пакет HTTP Response (ответ);После получения ответа от сервера начать рендеринг тела(тела) в пакете Response, а затем разорвать соединение TCP с сервером после получение всего контента.

用户访问一个web站点的过程

Вышеупомянутые методы работы в Интернете можно просто резюмировать следующим образом:

  1. Клиент получает сетевой IP-адрес сервера через DNS
  2. Клиент устанавливает TCP-соединение с сервером по протоколу TCP/IP.
  3. Клиент отправляет пакет запроса протокола HTTP на сервер, чтобы запросить ресурсный документ на сервере.
  4. Сервер отправляет клиенту ответный пакет HTTP-протокола. Если запрошенный ресурс содержит содержимое на динамическом языке, сервер вызовет механизм интерпретации динамического языка для обработки «динамического содержимого» и возврата обработанных данных клиенту.
  5. Клиент отключен от сервера. HTML-документ интерпретируется клиентом, и графический результат отображается на экране клиента.

Несколько концепций на стороне сервера:

  • Запрос: информация, запрошенная пользователем, используемая для анализа информации о запросе пользователя, включая сообщение, получение, файл cookie, URL-адрес и другую информацию.
  • Ответ: информация, которую сервер должен передать клиенту.
  • Conn: ссылка пользователя на запрос
  • Обработчик: логика обработки для обработки запросов и генерации возвращаемой информации.

Ниже приведен поток выполнения пакета http:

http 包执行流程

  1. Сервер запускает сокет, создает сокет прослушивания, прослушивает указанный порт и ожидает поступления запроса клиента.
  2. клиентский сервер ссылок
  3. Клиент отправляет запрос (http)
  4. Сервер получает запрос клиента и определяет, является ли он HTTP/HTTPS-запросом, и если да, то считывает заголовок HTTP/HTTPS-запроса и данные тела.
  5. Создание возвращаемой информации в формате HTTP/HTTPS (заголовки ответа HTTP/HTTPS, данные ответа)
  6. Эта информация возвращается клиенту через сокет, завершая процесс ответа на запрос.

Во-вторых, используйте язык Go для написания HTTP-сервера.

Стандартная библиотека net/http языка Go предоставляет интерфейсы, связанные с http-программированием, которые инкапсулируют сложные и тривиальные детали внутреннего соединения TCP и анализа сообщений.Пользователям нужно только взаимодействовать с двумя объектами http.request и http.ResponseWriter. То есть, пока вы пишете обработчик, запрос будет передаваться через параметры, и все, что ему нужно сделать, это обработать запрошенные данные и записать результат в Response.

Вот простой пример http-сервера:
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 Web

1. Укажите несколько маршрутов для HTTP-сервера

В реальной разработке интерфейс HTTP будет иметь много URL-адресов и соответствующих обработчиков. Здесь используется ServeMux сети/http. Mux — это аббревиатура от мультиплексора, что означает мультиплексирование (запросы отправляются поверх, и по некоторым суждениям они распределяются по разным местам в бэкенде). ServeMux может регистрировать больше соответствий между URL-адресами и обработчиками и автоматически перенаправлять запросы соответствующим обработчикам для обработки.

Язык Go реализует веб-маршрутизацию в основном для выполнения трех задач:

  1. порт прослушивания
  2. Получать запросы клиентов
  3. Назначьте соответствующий обработчик для каждого запроса

Вот пример реализации простого 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. Получите полный путь запроса

Полный путь запроса состоит из схемы (http/https) + доменного имени или IP (localhost) + номера порта + пути, например:
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 через SSL или HTTP через TLS) — это зашифрованный текст.

Создайте сертификат SSL вручную, используя метод openssl:

Команда создания ключевого файла:

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-сертификат не является сертифицированным, он предложит «неизвестный сертификат», а несертифицированный сертификат можно использовать только для разработки и тестирования.