22

编写 HTTP(S) 服务器 - Go Web 开发实战笔记

 4 years ago
source link: https://www.tuicool.com/articles/vyqiUzI
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

对于普通的上网过程,系统其实是这样做的:浏览器本身是一个客户端,当输入 URL 请求网页时,首先浏览器会去请求 DNS 服务器,获取与域名对应的 IP,然后通过 IP 地址找到对应的服务器后,要求建立 TCP 连接,等浏览器发送完 HTTP Request (请求)包后,服务器接收到请求包之后才开始处理请求包,服务器调用自身服务,返回 HTTP Response(响应)包;客户端收到来自服务器的响应后开始渲染这个 Response 包 里的主体(body),等收到全部的内容随后断开与该服务器之间的 TCP 连接。

r2EJ3uQ.png!web

以上 Web 工作方式可以简单地归纳为:

  1. 客户端通过 DNS 获取服务器网络 IP
  2. 客户端通过 TCP/IP 协议建立到服务器的 TCP 连接
  3. 客户端向服务器发送 HTTP 协议请求包,请求服务器里的资源文档
  4. 服务器向客户机发送 HTTP 协议应答包,如果请求的资源包含有动态语言的内容,那么服务器会调用动态语言的解释引擎负责处理“动态内容”,并将处理得到的数据返回给客户端
  5. 客户机与服务器断开。由客户端解释 HTML 文档,在客户端屏幕上渲染图形结果

服务器端的几个概念:

  • Request:用户请求的信息,用来解析用户的请求信息,包括post、get、cookie、url等信息
  • Response:服务器需要反馈给客户端的信息
  • Conn:用户的每次请求链接
  • Handler:处理请求和生成返回信息的处理逻辑

以下是 http 包执行流程:

yIBjeau.png!web
  1. 服务端启动 Socket, 创建 Listen Socket, 监听指定的端口, 等待客户端请求到来。
  2. 客户端链接服务端
  3. 客户端发送请求(http)
  4. 服务端接收到客户端的请求,判断是否为 HTTP/HTTPS 请求,如果是,则读取 HTTP/HTTPS 请求头和 body 数据。
  5. 使用 HTTP/HTTPS 格式生成返回信息(HTTP/HTTPS 响应头、响应数据)
  6. 将这些信息通过 Socket 返回给客户端,完成了一个请求响应的过程

二、使用 Go 语言编写一个HTTP服务器

Go 语言的标准库 net/http 提供了 http 编程有关的接口,封装了内部 TCP 连接和报文解析的复杂琐碎的细节,使用者只需要和 http.request 、 http.ResponseWriter 这两个对象交互就行。也就是说,只要写一个 handler,请求会通过参数传递进来,而它要做的就是根据请求的数据做处理,把结果写到 Response 中。

maqiyaA.png!web

以下是一个简单的 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 Web

1. 为HTTP服务器指定多个路由

在实际开发中,HTTP 接口会有许多的 URL 和对应的 Handler。这里就要用到 net/http 的 ServeMux。Mux是 multiplexor 的缩写,就是多路传输的意思(请求传过来,根据某种判断,分流到后端多个不同的地方)。ServeMux 可以注册多了 URL 和 handler 的对应关系,并自动把请求转发到对应的 handler 进行处理。

Go 语言实现一个 web 路由主要做三件事:

  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
复制代码
RVzeA3y.png!web

在生成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",未认证证书只能用于开发测试。


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK