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

play · · 1095 次点击 · · 开始浏览    
这是一个创建于 的文章,其中的信息可能已经有所发展或是发生改变。

一、Web 工作方式

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

用户访问一个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 返回给客户端,完成了一个请求响应的过程

二、使用 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 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
复制代码

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


有疑问加站长微信联系(非本文作者)

本文来自:掘金

感谢作者:play

查看原文:编写 HTTP(S) 服务器 - Go Web 开发实战笔记

入群交流(和以上内容无关):加入Go大咖交流群,或添加微信:liuxiaoyan-s 备注:入群;或加QQ群:692541889

1095 次点击  
加入收藏 微博
暂无回复
添加一条新回复 (您需要 登录 后才能回复 没有账号 ?)
  • 请尽量让自己的回复能够对别人有帮助
  • 支持 Markdown 格式, **粗体**、~~删除线~~、`单行代码`
  • 支持 @ 本站用户;支持表情(输入 : 提示),见 Emoji cheat sheet
  • 图片支持拖拽、截图粘贴等方式上传