Go语言编程笔记13:处理器

Go语言编程笔记13:处理器

image-20211108153040805

图源:wallpapercave.com

一个完整的Web应用,其功能可以主要划分为:接收请求、处理请求、生成返回数据、返回数据这几个部分。今天来讨论如何使用Go的标准库http来接收http请求。

处理器和Server

如在上一篇笔记Go语言编程笔记12:web基础中展示的那样,一个最简单的Go编写的Web应用可能是这样的:

package main

import "net/http"

func main() {
	http.ListenAndServe(":8080", nil)
}

代码很简单,但运行后就会发现这样的Web应用并没有什么实际意义。访问http://localhost:8080/或者别的子路径都会显示404 page not found

这是因为这个Web应用缺乏处理器(Handle)。这个所谓的处理器可以理解为对HTTP请求做出响应的具体代码块。

事实上ListenAndServe函数的第二个参数类型是http.Handler,这就是http包定义的处理器,它是一个接口:

type Handler interface {
	ServeHTTP(ResponseWriter, *Request)
}

也就是说实现了该接口的类型都可以看做是一个处理器,我们可以借此创建自定义的处理器:

...
type hello struct{}

func (hello) ServeHTTP(rw http.ResponseWriter, r *http.Request) {
	fmt.Fprintf(rw, "hello world!")
}

func main() {
	http.ListenAndServe(":8080", hello{})
}

事实上ListenAndServe函数是通过我们指定的地址和处理器来自行创建http.Server并进行监听:

func ListenAndServe(addr string, handler Handler) error {
	server := &Server{Addr: addr, Handler: handler}
	return server.ListenAndServe()
}

通过这种方式创建的服务器参数受限,如果需要创建设置更复杂的服务器,可以使用另一种方式创建并监听:

...
func main() {
	my_server := http.Server{
		Addr:    ":8080",
		Handler: hello{},
	}
	my_server.ListenAndServe()
}

这里的http.Server其实就是http包对服务器配置的抽象,它是一个结构体:

type Server struct {
	Addr string
	Handler Handler // handler to invoke, http.DefaultServeMux if nil
	TLSConfig *tls.Config
	ReadTimeout time.Duration
	ReadHeaderTimeout time.Duration
	WriteTimeout time.Duration
	IdleTimeout time.Duration
	MaxHeaderBytes int
	TLSNextProto map[string]func(*Server, *tls.Conn, Handler)
	ConnState func(net.Conn, ConnState)
	ErrorLog *log.Logger
	BaseContext func(net.Listener) context.Context
	inShutdown atomicBool // true when server is in shutdown
	disableKeepAlives int32     // accessed atomically.
	nextProtoOnce     sync.Once // guards setupHTTP2_* init
	nextProtoErr      error     // result of http2.ConfigureServer if used
	mu         sync.Mutex
	listeners  map[*net.Listener]struct{}
	activeConn map[*conn]struct{}
	doneChan   chan struct{}
	onShutdown []func()
}

通过创建并通过这个结构体的实例来启动Web服务,可以设置更多的服务器选项。

现在我们的web应用会响应http请求,并对任何http请求都会返回hello world!字样的页面内容。

或许对某些特殊的Web应用来说这已经足够了,但对于一般的Web应用这显然不够。一般来说,普通的Web应用需要对不同的URL返回不同的响应内容。

多路复用器

这就需要用到多路复用器(Server Mux)了。所谓的多路复用器,其实就是一个负责分析请求到服务器的URL的路径并在分析后按规则转发给对应的处理器的组件。一般也会称为“路由”。

http包的多路复用器是一个结构体:

type ServeMux struct {
	mu    sync.RWMutex
	m     map[string]muxEntry
	es    []muxEntry // slice of entries sorted from longest to shortest.
	hosts bool       // whether any patterns contain hostnames
}

具体字段含义我们可以先不管,我们只需要知道如何创建一个多路复用器并“绑定”处理器即可:

...
type bye struct{}

func (bye) ServeHTTP(rw http.ResponseWriter, r *http.Request) {
	fmt.Fprintf(rw, "bye~~")
}

func main() {
	serverMux := http.NewServeMux()
	serverMux.Handle("/hello", hello{})
	serverMux.Handle("/bye", bye{})
	http.ListenAndServe(":8080", serverMux)
}

这里通过http.NewServeMux创建了一个多路复用器,其实该函数内容跟简单:

func NewServeMux() *ServeMux { return new(ServeMux) }

然后我们就可以调用多路复用器的Handle方法绑定处理器,并且在绑定的同时可以指定一个处理器作用的路径。

最后http.ListenAndServe函数会使用传入的多路复用器创建http.Server并启动Web服务。

可能有人还记得,ListenAndServe接收的类型是http.Handler而非http.ServeMux,但实际上多路复用器是一个特殊的处理器,它也实现了ServeHTTP方法,所以这样做是没有问题的。

现在运行我们的Web应用就能发现,对于根目录的请求会返回404 page not found,对/hello的请求会返回hello world!,对/bye的请求会返回bye~~

当然,也可以使用我们之前所说的自行创建http.Server实例的方式使用多路复用器:

...
func main() {
	serverMux := http.NewServeMux()
	serverMux.Handle("/hello", hello{})
	serverMux.Handle("/bye", bye{})
	server := http.Server{
		Addr:    ":8080",
		Handler: serverMux,
	}
	server.ListenAndServe()
}

两者只是绑定多路复用器和启动服务器的方式略有区别,其它地方区别不大。

DefaultServerMux

此外,对于构建不需要自行创建http.Server实例的简单Web应用,我们可以使用一种更简单的方式给多路复用器绑定处理器:

...
func main() {
	http.Handle("/hello", hello{})
	http.Handle("/bye", bye{})
	http.ListenAndServe(":8080", nil)
}

这里指定给服务器的处理器参数是nil,在这种情况下http包会使用一个默认的多路复用器DefaultServerMux作为服务器的处理器,包变量DefaultServerMux实际上是http.ServerMux的一个实例:

var DefaultServeMux = &defaultServeMux
var defaultServeMux ServeMux

http.Handle函数的作用其实是调用DefaultServerMuxHandle方法来绑定处理器到默认的多路复用器:

func Handle(pattern string, handler Handler) { DefaultServeMux.Handle(pattern, handler) }

URL解析规则

http包的多路复用器对URL的解析规则很微妙,我们看这个例子:

...
type indexHandler struct{}

func (indexHandler) ServeHTTP(rw http.ResponseWriter, r *http.Request) {
	fmt.Fprintf(rw, "This is home page.")
}

func main() {
	http.Handle("/", indexHandler{})
	http.Handle("/hello", hello{})
	http.Handle("/bye", bye{})
	http.ListenAndServe(":8080", nil)
}

这里给根路径添加一个处理器indexHandler,现在访问/显示的是This is home page.,访问/hello显示的是hello world!,访问/bye显示的是bye~~,这都很正常。但如果访问/bye/123就会显示This is home page.。也就是说多路复用器会将/bye/123这样的路径解析到/对应的indexHandler处理器而非/bye对应的bye这个处理器。

这是因为http包的多路复用器在解析路径时,对于绑定的/hello这样路径的处理器,仅会转发xxx/hello这样的http请求,如果想要能够转发xxx/hello/xxx这样的请求,则需要定义为/hello/这样的形式:

...
func main() {
	http.Handle("/", indexHandler{})
	http.Handle("/hello/", hello{})
	http.Handle("/bye/", bye{})
	http.ListenAndServe(":8080", nil)
}

现在访问http://localhost:8080/bye/123就会显示bye~~

第三方多路复用器

标准库http的多路复用器也并非尽善尽美,比如虽然一般情况我们都是通过URL中的查询字符串来传递参数,但是某些情况下也可以用URL中的路径本身来传递查询信息。

比如要是查询一本书,正常情况下可能是这样的URL:http://sample.com/book?id=123。但也可以使用http://sample.com/book/123这样的URL,这样做的好处是可以让URL更简洁。

但是标准库http提供的多路复用器并不能从URL中提取出参数,如果想要实现上面的效果就需要我们自己实现一个多路复用器,不过有更好的选择——使用第三方多路复用器。

package main

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

	"github.com/julienschmidt/httprouter"
)

type book struct {
	Name string
	Id   int
	Desc string
}

var books = map[int]book{
	1: {Name: "哈利波特", Id: 1, Desc: "小说"},
	2: {Name: "时间简史", Id: 2, Desc: "科普读物"},
	3: {Name: "Go程序设计语言", Id: 3, Desc: "程序设计"},
}

func bookHandleFunc(rw http.ResponseWriter, r *http.Request, p httprouter.Params) {
	id := p.ByName("id")
	bookId, err := strconv.Atoi(id)
	if err != nil {
		log.Fatal(err)
	}
	book, ok := books[bookId]
	if !ok {
		fmt.Fprintf(rw, "not find the book.")
	} else {
		fmt.Fprintf(rw, "Book details:\n")
		fmt.Fprintf(rw, "name: %s\n", book.Name)
		fmt.Fprintf(rw, "id: %d\n", book.Id)
		fmt.Fprintf(rw, "description:%s\n", book.Desc)
	}
}

func main() {
	router := httprouter.New()
	router.Handle("GET", "/book/:id", bookHandleFunc)
	http.ListenAndServe(":8080", router)
}

这里使用的是第三方库httprouter的多路复用器,要使用必须先安装:

go get github.com/julienschmidt/httprouter

然后就可以在代码中使用httprouter的多路复用器:

	router := httprouter.New()
	router.Handle("GET", "/book/:id", bookHandleFunc)
	http.ListenAndServe(":8080", router)

要注意的是,该多路复用器绑定处理器、处理器函数的时候与标准库稍有不同,需要传递三个参数:

  • HTTP方法
  • 路径
  • httprouter.Handle

其中HTTP方法在前一篇笔记中解释过,在这里可以通过指定HTTP方法让对应的处理器仅处理某种方法的HTTP请求。特别的是,在路径中可以通过xxx/:param_name这样的方式来指定一个从路径中传递和解析的查询参数。比如这里的/book/:id其实就类似于/book?id=xxxhttprouter.Handle可以看做是httprouter包定义的处理器函数,其具体定义为:

type Handle func(http.ResponseWriter, *http.Request, Params)

所以我们可以按照这个函数类型来创建具体的处理器函数:

func bookHandleFunc(rw http.ResponseWriter, r *http.Request, p httprouter.Params) {
	...
}

处理器函数中可以通过第三个参数p来获取路径中的查询参数:

	id := p.ByName("id")

现在只要运行Web应用,并使用http://localhost:8080/book/1就可以看到以下内容:

Book details:
name: 哈利波特
id: 1
description:小说

此外,除了使用router.Handle绑定处理器,还可以使用另一种形式:

	router.GET("/book/:id", bookHandleFunc)

GET方法本身就说明了HTTP方法,所以无需在参数中额外指定,这种方式比Handle方法简洁一些。

处理器函数

使用处理器可能会显得繁琐,毕竟要为每个处理器创建一个结构体。所以http包提供一种更简单的方式——处理器函数

处理器函数本质上就是一个和处理器接口中定义的ServerHTTP方法签名相同的函数:

package main

import (
	"fmt"
	"net/http"
)

func helloHandleFunc(rw http.ResponseWriter, r *http.Request) {
	fmt.Fprintf(rw, "hello world!")
}

func byeHandleFunc(rw http.ResponseWriter, r *http.Request) {
	fmt.Fprintf(rw, "bye~~")
}

func main() {
	serverMux := http.NewServeMux()
	serverMux.HandleFunc("/hello", helloHandleFunc)
	serverMux.HandleFunc("/bye", byeHandleFunc)
	server := http.Server{
		Addr:    ":8080",
		Handler: serverMux,
	}
	server.ListenAndServe()
}

使用起来也很简单,只要通过多路复用器的HandleFunc方法绑定到多路复用器即可。

实际上HandleFunc方法是将一个具有func(ResponseWriter, *Request)签名的处理器函数转化为处理器后进行的绑定:

func (mux *ServeMux) HandleFunc(pattern string, handler func(ResponseWriter, *Request)) {
	if handler == nil {
		panic("http: nil handler")
	}
	mux.Handle(pattern, HandlerFunc(handler))
}

其中HandlerFunc实际上是http包定义的一个具名函数类型:

type HandlerFunc func(ResponseWriter, *Request)

// ServeHTTP calls f(w, r).
func (f HandlerFunc) ServeHTTP(w ResponseWriter, r *Request) {
	f(w, r)
}

这个函数类型是func(ResponseWriter, *Request)并不让人奇怪,巧妙的是这个函数类型具有一个方法ServeHTTP,而该方法的内容是调用自身f(w,r),也就是说这个函数类型本身就是一个处理器。

所以我们可以利用这个函数类型将一个处理器函数转化为处理器后进行绑定:

mux.Handle(pattern, HandlerFunc(handler))

这是我没有想到的,我本来的想法是通过匿名结构体之类的去构建一个处理器,不得不说上面这种实现更为简单巧妙。这里利用了Go语言可以定义函数类型、可以给任意类型添加方法、以及类型转换这几个语法特点,相当的有Go风格。

虽然使用处理器函数的方式比处理器更简洁,但是处理器并非没有用处的,比如要将已经存在的结构体或其它类型重构为处理器并绑定到多路复用器,可以很容易地通过添加一个ServerHTTP方法的方式实现。

串联处理器

可以利用Go语言对函数式编程的支持实现“对处理器的串联”:

func welcome(hf func(http.ResponseWriter, *http.Request)) func(http.ResponseWriter, *http.Request) {
	return func(rw http.ResponseWriter, r *http.Request) {
		fmt.Fprintf(rw, "welcome everyone!\n")
		hf(rw, r)
	}
}

func main() {
	serverMux := http.NewServeMux()
	serverMux.HandleFunc("/hello", welcome(helloHandleFunc))
	serverMux.HandleFunc("/bye", welcome(byeHandleFunc))
	server := http.Server{
		Addr:    ":8080",
		Handler: serverMux,
	}
	server.ListenAndServe()
}

假设我们需要在用户访问任意URL时,先输出一个欢迎语句,可以这样做:

...
func welcome(hf func(http.ResponseWriter, *http.Request)) func(http.ResponseWriter, *http.Request) {
	return func(rw http.ResponseWriter, r *http.Request) {
		fmt.Fprintf(rw, "welcome everyone!\n")
		hf(rw, r)
	}
}

func main() {
	serverMux := http.NewServeMux()
	serverMux.HandleFunc("/hello", welcome(helloHandleFunc))
	serverMux.HandleFunc("/bye", welcome(byeHandleFunc))
	server := http.Server{
		Addr:    ":8080",
		Handler: serverMux,
	}
	server.ListenAndServe()
}

这里welcome函数接收一个处理器函数并返回一个处理器函数,唯一做出的改变是通过一个匿名函数对处理器函数进行了“包装”,先输出欢迎语句,然后再调用处理器函数本身。

使用起来很简单:welcome(helloHandleFunc)。除了对处理器执行的内容稍微改变之外,经过welcome函数包装后的返回值依然是一个处理器函数,所以不需要对调用代码做出任何额外改变。

如果觉得welcome函数签名太过复杂,可以利用http包定义的HandlerFunc函数类型进行简化:

func welcome(hf http.HandlerFunc) http.HandlerFunc {
	return func(rw http.ResponseWriter, r *http.Request) {
		fmt.Fprintf(rw, "welcome everyone!\n")
		hf(rw, r)
	}
}

实际效果是相同的。

利用这种特性你可以串联任意多个处理器来实现某些功能,比如对需要进行登录验证的URL添加上登录验证和日志模块:

...
func userValidate(hf http.HandlerFunc) http.HandlerFunc {
	return func(rw http.ResponseWriter, r *http.Request) {
		//登录验证模块
		hf(rw, r)
	}
}

func log(hf http.HandlerFunc) http.HandlerFunc {
	return func(rw http.ResponseWriter, r *http.Request) {
		//日志记录模块
		hf(rw, r)
	}
}

func main() {
	serverMux := http.NewServeMux()
	serverMux.HandleFunc("/hello", log(userValidate(helloHandleFunc)))
	serverMux.HandleFunc("/bye", log(userValidate(byeHandleFunc)))
    ...
}

这里没有具体编写相应的逻辑,仅以伪代码代替。

需要注意的是,这样使用的时候要注意串联的顺序,如果是log(userValidate(helloHandleFunc)),就是先记录日志,再进行用户登录验证。如果是userValidate(log(helloHandleFunc))则是先验证登录再记录日志,不同的顺序在某些业务流下会有不同的效果。

如果了解设计模式的童鞋,肯定很容易联想到装饰器模式,事实上这里的做法就是用函数式编程的风格来实现装饰器模式。

想了解更多的装饰器模式,可以阅读设计模式 with Python3:装饰器模式

因为多路复用器是特殊的处理器,所以也可以串联多路复用器。

HTTPS

使用HTTPS而非HTTP已经是互联网的趋势,因为不管你使用何种方式来手动加密你的内容,事实上你的Web应用都在互联网上“裸奔”,是否会被人利用和破解只取决于对方想不想罢了。

所以使用基于RSA加密原理的SSL之上的HTTPS协议就相当有必要了,况且现在的主流浏览器(如Chrome或Firefox)已经将HTTPS作为连接网站的默认协议,也就是说你在地址栏输入baidu.com时,默认访问的是https://baidu.com而非http://baidu.com。并且在目标服务不支持https的情况下阻止用户访问,并显示“该网站不安全”的字样。

所以但凡是正常点的网站,都会提供HTTPS协议支持。

如果你想了解如何给自建站点添加SSL证书并提供HTTPS协议支持,可以阅读腾讯云博客搭建 III

同样的,我们也可以给Go创建的Web应用添加HTTPS支持。

SSL证书

要支持HTTPS,需要先获取一个SSL证书,对于一个正常网站,这需要一个有效CA来颁发。但对于一个实验性质的应用来说,我们使用工具自己签发生成就可以。

package main

import (
	"crypto/rand"
	"crypto/rsa"
	"crypto/x509"
	"crypto/x509/pkix"
	"encoding/pem"
	"math/big"
	"net"
	"os"
	"time"
)

func main() {
	max := new(big.Int).Lsh(big.NewInt(1), 128)
	serialNumber, _ := rand.Int(rand.Reader, max)
	subject := pkix.Name{
		Organization:       []string{"Manning Publications Co."},
		OrganizationalUnit: []string{"Books"},
		CommonName:         "Go Web Programming",
	}

	template := x509.Certificate{
		SerialNumber: serialNumber,
		Subject:      subject,
		NotBefore:    time.Now(),
		NotAfter:     time.Now().Add(365 * 24 * time.Hour),
		KeyUsage:     x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature,
		ExtKeyUsage:  []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},
		IPAddresses:  []net.IP{net.ParseIP("127.0.0.1")},
	}

	pk, _ := rsa.GenerateKey(rand.Reader, 2048)

	derBytes, _ := x509.CreateCertificate(rand.Reader, &template, &template, &pk.PublicKey, pk)
	certOut, _ := os.Create("cert.pem")
	pem.Encode(certOut, &pem.Block{Type: "CERTIFICATE", Bytes: derBytes})
	certOut.Close()

	keyOut, _ := os.Create("key.pem")
	pem.Encode(keyOut, &pem.Block{Type: "RSA PRIVATE KEY", Bytes: x509.MarshalPKCS1PrivateKey(pk)})
	keyOut.Close()
}

上面这个生成服务器私钥和SSL证书的代码来自《Go Web 编程》的代码库gwp

运行后会在目录下生成两个文件:

  • cert.pem,SSL证书。
  • key.pem,证书配套的服务器私钥。

HTTPS

要让服务器支持HTTPS很简单:

package main

import "net/http"

func main() {
	http.ListenAndServeTLS(":8080", "../ssl/cert.pem", "../ssl/key.pem", nil)
}

在服务器相关配置中传入证书和私钥即可。

但此时访问服务端依然会显示类似以下的内容:

image-20211222151110248

这是因为我们使用的证书是自己签发的,并非有效的CA签发,所以浏览器自然是不认可的。

要让自己签发的证书生效,需要进行以下操作:

  1. F12开启开发者工具,并在Security标签页中点击View certificate查看证书。

    image-20211222151353622

  2. 在弹出的证书信息中,选择详细信息,并点击复制到文件

    image-20211222151508353

  3. 一路按下一步进行证书导出,要注意的是导出格式是.cer

  4. 在浏览器设置中,进入安全相关设置。

    image-20211222151753022

  5. 进入管理证书

    image-20211222151828980

  6. 点击导入将刚才生成的.cer证书导入,需要注意的是存储位置需要选择为受信任的根证书颁发机构

    image-20211222153247417

  7. 关闭浏览器后重新访问https://127.0.0.1:8080/

    image-20211222153401485

  8. ok!

需要说明的是,证书中是包含Web服务的主机名的,也就是说给127.0.0.1签发的证书只能是浏览器访问https://127.0.0.1时使用,虽然理论上https://localhost也应当指向同一个服务,但在服务器看来127.0.0.1localhost并不是同一个主机名,所以后者的请求并不能正常使用该证书建立TLS连接,依然会显示网站不安全等字样。

HTTP/2

默认情况下我们之前创建的Web应用都使用的是HTTP 1.1协议,这点可以通过浏览器开发者工具确认:

image-20211222154514248

在go 1.6以下版本中,要添加HTTP/2协议支持,需要这么做:

import (
	...
    "golang.org/x/net/http2"
)
...
func main() {
	router := httprouter.New()
	router.GET("/book/:id", bookHandleFunc)
	server := http.Server{
		Addr:    ":8080",
		Handler: router,
	}
	http2.ConfigureServer(&server, &http2.Server{})
	server.ListenAndServeTLS("../ssl/cert.pem", "../ssl/key.pem")
}

http2.ConfigureServer函数会在服务器中写入HTTP/2的相关配置信息。

需要说明的是HTTP/2协议是和HTTPS搭配使用的,并不能在普通的HTTP连接中使用HTTP/2协议。

遗憾的是,使用HTTPS后,没法使用浏览器的开发者工具查看HTTP协议版本信息,所以这里我选择在处理器中打印HTTP请求中的相关协议信息:

...
func bookHandleFunc(rw http.ResponseWriter, r *http.Request, p httprouter.Params) {
	fmt.Println(r.Proto)
	...
}

实际测试后可以看到服务器的控制台输出是HTTP/2.0,的确采用了HTTP/2协议。

其实对于Go 1.6以上的版本,使用以上方式配置完全是不需要的,使用HTTPS创建的服务会自动使用HTTP/2协议:

...
func main() {
	router := httprouter.New()
	router.GET("/book/:id", bookHandleFunc)
	server := http.Server{
		Addr:    ":8080",
		Handler: router,
	}
	server.ListenAndServeTLS("../ssl/cert.pem", "../ssl/key.pem")
}

控制台输出同样会证明这一点。

好了,以上就是处理器相关内容,内容本身并不复杂,但要写文章阐述清楚还是挺累的,本来还打算补几张图来说明,但已经没力气了,有空再补吧,就先这样了。

谢谢阅读。

参考资料

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值