一、Web 工作方式
对于普通的上网过程,系统其实是这样做的:浏览器本身是一个客户端,当输入 URL 请求网页时,首先浏览器会去请求 DNS 服务器,获取与域名对应的 IP,然后通过 IP 地址找到对应的服务器后,要求建立 TCP 连接,等浏览器发送完 HTTP Request (请求)包后,服务器接收到请求包之后才开始处理请求包,服务器调用自身服务,返回 HTTP Response(响应)包;客户端收到来自服务器的响应后开始渲染这个 Response 包 里的主体(body),等收到全部的内容随后断开与该服务器之间的 TCP 连接。
以上 Web 工作方式可以简单地归纳为:
- 客户端通过 DNS 获取服务器网络 IP
- 客户端通过 TCP/IP 协议建立到服务器的 TCP 连接
- 客户端向服务器发送 HTTP 协议请求包,请求服务器里的资源文档
- 服务器向客户机发送 HTTP 协议应答包,如果请求的资源包含有动态语言的内容,那么服务器会调用动态语言的解释引擎负责处理“动态内容”,并将处理得到的数据返回给客户端
- 客户机与服务器断开。由客户端解释 HTML 文档,在客户端屏幕上渲染图形结果
服务器端的几个概念:
- Request:用户请求的信息,用来解析用户的请求信息,包括post、get、cookie、url等信息
- Response:服务器需要反馈给客户端的信息
- Conn:用户的每次请求链接
- Handler:处理请求和生成返回信息的处理逻辑
以下是 http 包执行流程:
- 服务端启动 Socket, 创建 Listen Socket, 监听指定的端口, 等待客户端请求到来。
- 客户端链接服务端
- 客户端发送请求(http)
- 服务端接收到客户端的请求,判断是否为 HTTP/HTTPS 请求,如果是,则读取 HTTP/HTTPS 请求头和 body 数据。
- 使用 HTTP/HTTPS 格式生成返回信息(HTTP/HTTPS 响应头、响应数据)
- 将这些信息通过 Socket 返回给客户端,完成了一个请求响应的过程
二、使用 Go 语言编写一个HTTP服务器
Go 语言的标准库 net/http 提供了 http 编程有关的接口,封装了内部 TCP 连接和报文解析的复杂琐碎的细节,使用者只需要和 http.request 、 http.ResponseWriter 这两个对象交互就行。也就是说,只要写一个 handler,请求会通过参数传递进来,而它要做的就是根据请求的数据做处理,把结果写到 Response 中。
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 路由主要做三件事:
- 监听端口
- 接收客户端的请求
- 为每个请求分配对应的 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
复制代码
在生成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",未认证证书只能用于开发测试。
有疑问加站长微信联系(非本文作者)