Featured image of post Go Web编程学习一:接受请求

Go Web编程学习一:接受请求

Go高效的net/http模块

一、Go的net/http标准库

  1. net/http 标准库的各个组成部分:

go-http

  1. Go服务器的处理请求:

在启动了一个http服务器之后,就可以就收客户端发过来的请求,进行处理并相应。net/http标准库还提供了一个连接多路复用器的接口以及一个默认实现了的多路复用器。多路复用器根据客户端的请求路径,找到对应的处理器,处理器就会进行一系列的操作。最后把处理结果通过模板引擎在模板上显示。

二、使用net/http标准库搭建一个简单web服务器

新建一个server.go以下代码。然后运行,在浏览器上访问http://localhost:8080,浏览器上就会出现一个空白页。至此一个简单地web服务器就这样搭建成功了。对比于Java Web,在搭建的时候还要使用Servlet,然后面向对象,需要写的代码量比Go会多很多。所以说Go比较适合做网络应用。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
package main

import "net/http"

func main() {
	server := http.Server{
		Addr:    "127.0.0.1:8080",
		Handler: nil,
	}
	err := server.ListenAndServe()
	if err != nil {
		panic(err)
	}
}

关于http.Server的详细配置

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
// 定义Http服务器的运行参数
// 服务器的零值是有效配置
type Server struct {
	// Addr可以选择以“ host:port”的形式指定服务器侦听的TCP地址。如果为空,则使用“:http”(端口80)
	// 有关地址格式的详细信息,请参见net.Dial
	Addr string

    // 调用的处理程序,如果为nil,则为http.DefaultServeMux
	Handler Handler 

	// TLSConfig optionally provides a TLS configuration for use
	// by ServeTLS and ListenAndServeTLS. Note that this value is
	// cloned by ServeTLS and ListenAndServeTLS, so it's not
	// possible to modify the configuration with methods like
	// tls.Config.SetSessionTicketKeys. To use
	// SetSessionTicketKeys, use Server.Serve with a TLS Listener
	// instead.
	TLSConfig *tls.Config

	// ReadTimeout is the maximum duration for reading the entire
	// request, including the body.
	//
	// Because ReadTimeout does not let Handlers make per-request
	// decisions on each request body's acceptable deadline or
	// upload rate, most users will prefer to use
	// ReadHeaderTimeout. It is valid to use them both.
	ReadTimeout time.Duration

	// ReadHeaderTimeout is the amount of time allowed to read
	// request headers. The connection's read deadline is reset
	// after reading the headers and the Handler can decide what
	// is considered too slow for the body. If ReadHeaderTimeout
	// is zero, the value of ReadTimeout is used. If both are
	// zero, there is no timeout.
	ReadHeaderTimeout time.Duration

	// WriteTimeout is the maximum duration before timing out
	// writes of the response. It is reset whenever a new
	// request's header is read. Like ReadTimeout, it does not
	// let Handlers make decisions on a per-request basis.
	WriteTimeout time.Duration

	// IdleTimeout is the maximum amount of time to wait for the
	// next request when keep-alives are enabled. If IdleTimeout
	// is zero, the value of ReadTimeout is used. If both are
	// zero, there is no timeout.
	IdleTimeout time.Duration

	// MaxHeaderBytes controls the maximum number of bytes the
	// server will read parsing the request header's keys and
	// values, including the request line. It does not limit the
	// size of the request body.
	// If zero, DefaultMaxHeaderBytes is used.
	MaxHeaderBytes int

	// TLSNextProto optionally specifies a function to take over
	// ownership of the provided TLS connection when an ALPN
	// protocol upgrade has occurred. The map key is the protocol
	// name negotiated. The Handler argument should be used to
	// handle HTTP requests and will initialize the Request's TLS
	// and RemoteAddr if not already set. The connection is
	// automatically closed when the function returns.
	// If TLSNextProto is not nil, HTTP/2 support is not enabled
	// automatically.
	TLSNextProto map[string]func(*Server, *tls.Conn, Handler)

	// ConnState specifies an optional callback function that is
	// called when a client connection changes state. See the
	// ConnState type and associated constants for details.
	ConnState func(net.Conn, ConnState)

	// ErrorLog specifies an optional logger for errors accepting
	// connections, unexpected behavior from handlers, and
	// underlying FileSystem errors.
	// If nil, logging is done via the log package's standard logger.
	ErrorLog *log.Logger

	// BaseContext optionally specifies a function that returns
	// the base context for incoming requests on this server.
	// The provided Listener is the specific Listener that's
	// about to start accepting requests.
	// If BaseContext is nil, the default is context.Background().
	// If non-nil, it must return a non-nil context.
	BaseContext func(net.Listener) context.Context

	// ConnContext optionally specifies a function that modifies
	// the context used for a new connection c. The provided ctx
	// is derived from the base context and has a ServerContextKey
	// value.
	ConnContext func(ctx context.Context, c net.Conn) context.Context

	disableKeepAlives int32     // accessed atomically.
	inShutdown        int32     // accessed atomically (non-zero means we're in Shutdown)
	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()
}

搭建一个提供https服务的服务器

  1. 生成SSL证书和私钥

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    
    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{"com.msr"},
    		OrganizationalUnit: []string{"better"},
    		CommonName:         "go web",
    	}
    
    	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()
    }
    
  2. 使用https,启动运行。访问https://localhost:8080

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    
    package main
    
    import (
    	"fmt"
    	"net/http"
    )
    
    func main() {
    	server := http.Server{
    		Addr:    127.0.0.1:8080",
    		Handler: nil,
    	}
    	err := server.ListenAndServeTLS("cert.pem", "key.pom")
    	if err != nil {
    		fmt.Println(err.Error())
    	}
    }
    

三、处理器和处理函数

处理器:

Web应用中的处理器除了要接收和处理客户端发来的请求,还需要调用模板引擎,然后由模板引擎生成HTML并把数据填充到要回传到客户端的响应报文中。在net/http库中,一个处理器就是拥有一个ServeHTTP方法,这个方法一个参数是ResponseWriter 接口,而第二个参数则是一个指向Request 结构的指针。换句话说,任何接口只要拥有一个ServeHTTP 方法,并且该方法带有以下签名(signature),那么它就是一个处理器。

1
ServeHTTP(w ResponseWriter, r *Request) 

2.1 自定义Handler处理请求

创建了一个处理器并将它与服务器进行了绑定,以此来代替原本正在使用的默认多路复用器。这意味着服务器不会再通过URL匹配来将请求路由至不同的处理器,而是直接使用同一个处理器来处理所有请求,因此无论浏览器访问什么地址,服务器返回的都是同样的Hello World!响应。很显然这不是常用的方法。

这也是我们在Web应用中使用多路复用器的原因:对某些特殊用途的服务器来说,只使用一个处理器也许就可以很好地完成工作了,但是在大部分情况下,我们还是希望服务器可以根据不同的URL请求返回不同的响应,而不是一成不变地只返回一种响应。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
package main

import (
	"fmt"
	"net/http"
)

type MyHandler struct{}

func (h *MyHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
	fmt.Fprintf(w, "Hello World!")
}

func main() {
	handler := MyHandler{}
	server := http.Server{
		Addr:    "127.0.0.1:8080",
		Handler: &handler,
	}
	server.ListenAndServe()
}

2.2 使用多个Handler来处理不同请求

不在Server结构的Handler字段中指定处理器,而是让服务器使用默认的DefaultServeMux作为处理器,然后通http.Handle 函数将处理器绑定至DefaultServeMux。需要注意的是,虽然Handle 函数来源于http 包,但它实际上是ServeMux 结构的方法:这些函数是为了操作便利而创建的函数,调用它们等同于调用DefaultServeMux 的某个方法。比如说,调用http.Handle实际上就是在调用DefaultServeMux的Handle 方法。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
package main

import "net/http"

type HelloHandler struct {
}

type WorldHandler struct {
}

func (h *HelloHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
	w.Write([]byte("hello!!!"))
}

func (h *WorldHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
	w.Write([]byte("world!!!"))
}

func main() {
	hello := HelloHandler{}
	world := WorldHandler{}

	server := http.Server{
		Addr: "127.0.0.1:8080",
	}

	http.Handle("/hello", &hello)
	http.Handle("/world", &world)

	server.ListenAndServe()
}

2.3 处理器函数

Go语言拥有一种HandlerFunc函数类型,它可以把一个带有正确签名的函数f 转换成一个带有方法f的Handler

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
package main

import "net/http"

func hello(w http.ResponseWriter, r *http.Request){
	w.Write([]byte("hello!!!"))
}

func world(w http.ResponseWriter, r *http.Request){
	w.Write([]byte("hello!!!"))
}

func main() {
	server := http.Server{
		Addr: "127.0.0.1:8080",
	}
	http.HandleFunc("/hello", hello)
	http.HandleFunc("/world", world)

	server.ListenAndServe()
}

Handle 函数将一个处理器绑定至URL的具体方法。HandleFun函数会将函数转换成一个Handler ,并将它与DefaultServeMux进行绑定,以此来简化创建并绑定Handler 的工作。换句话说,处理器函数只不过是创建处理器的一种便利的方法而已。

2.4 串联多个处理器和处理函数

Go语言并不是一门函数式编程语言,但它也拥有一些函数式编程语言的特性,如函数类型、匿名函数和闭包。正如前面的代码所示,在Go语言里面,程序可以将一个函数传递给另一个函数,又或者通过标识符去引用一个具名函数。这意味着,可以将函数f1传递给另一个函数f2,然后在函数f2执行完某些操作之后调用f1 。例如:在每个处理器被调用时,都都记录一下调用日志

代码示例:

log函数返回的是一个匿名的HandlerFunc函数。因为hello 函数就是一个HandlerFunc 类型的函数,所以代码log(hello) 实际上就是将hello 函数发送至log 函数之内,换句话说,这段代码串联起了log函数和hello 函数。protect函数也是如此。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
package main

import (
	"fmt"
	"net/http"
)

type HelloHandler struct{}

func (h HelloHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
	fmt.Fprintf(w, "Hello!")
}

func log(h http.Handler) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		fmt.Fprintf(w,"Handler called - %T\n", h)
		h.ServeHTTP(w, r)
	})
}

func protect(h http.Handler) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		// 假设有一系列操作....
		fmt.Fprintf(w,"Handler called - %T\n", h)
		h.ServeHTTP(w, r)
	})
}

func main() {
	server := http.Server{
		Addr: "127.0.0.1:8080",
	}
	hello := HelloHandler{}
	http.Handle("/hello", protect(log(hello)))
	server.ListenAndServe()
}

2.5 ServeMux和DefaultServeMux

ServeMux是一个HTTP请求多路复用器,它负责接收HTTP请求并根据请求中的URL将请求重定向到正确的处理器

ServeMux结构包含一个映射,这个映射会将URL映射到相应的处理器。ServeMux也实现了ServeHTTP方法,所以也是一个处理器。当ServeHTTP方法接收到一个请求的时候,它会在结构的映射里面找出与请求URL最匹配的URL,然后调用对应的处理器的ServeHTTP方法。

ServeMux是一个结构体并不是一个接口,所以DefaultServeMux并不是ServeMux的实现。DefaultServeMux是ServeMux的实例。并且所有引入了net/http 标准库的程序都可以使用这个实例。当用户没有为Server结构指定处理器时,服务器就会使用DefaultServeMux作为ServeMux的默认实例。

因为ServeMux也是一个处理器 ,所以用户也可以在有需要的情况下对其实例实施处理器串联。

如果绑定根URL(/ ),那么匹配不成功的URL将会根据URL的层级进行下降,并最终降落在根URL之上。当浏览器访问/random的时候,因为服务器无法找到负责处理这个URL的处理器,所以它会把这个URL交给根URL的处理器处理。例如/hello绑定了处理器,当访问/hello/mike的时候,由于/hello/mike没有绑定处理器,就会降级给/hello的处理器去处理。

最小惊讶原则:

也称最小意外原则,是设计包括软件在内的一切事物的一条通用规则,它指的是我们在 进行设计的时候,应该做那些合乎常理的事情,使事物的行为总是显而易见、始终如一并且合乎情理。

2.6 使用其他多路复用器(HttpRouter)

创建一个处理器和多路复用器唯一需要做的就是实现ServeHTTP方法,所以通过创建多路复用器来代替net/http包下的ServeMux是完全可行。

ServeMux 的一个缺陷是无法使用变量实现URL模式匹配。例如:/user/123查询id为123的用户,对于Go原生的ServeMux对URL进行语法分析提取123会比较麻烦,就必须在程序里面进行大量复杂的语法分析,并因此给程序带来额外的复杂度。

HttpRouter文档:https://github.com/julienschmidt/httprouter。

简单示例:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
package main

import (
	"fmt"
	"github.com/julienschmidt/httprouter"
	"net/http"
)

func hello(w http.ResponseWriter, r *http.Request, p httprouter.Params) {
	fmt.Fprintf(w, "hello, %s!\n", p.ByName("name"))
}

func main() {
	mux := httprouter.New()
	mux.GET("/hello/:name", hello)

	server := http.Server{
		Addr:    "127.0.0.1:8080",
		Handler: mux,
	}
	server.ListenAndServe()
}

对于这些路由比配和变量获取很多的Web框架都有实现。通过httprouter.New()来创建一个多路复用器。程序不需要在使用HandleFunc绑定处理函数,可以直接给定HTTP方法的uri与对应的处理函数,如下:

mux.GET("/hello/:name", hello)

最后不再使用DefaultServerMux,而是通过将HttpRouter传递给Server结构来使用通过HttpRouter创建出来的多路复用器。

2.7 使用HTTP/2

在Go1.6以及上的版本,如果使用了HTTPS模式启动服务器,那么服务器默认使用HTTP/2。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
package main

import (
	"fmt"
	"golang.org/x/net/http2"
	"net/http"
)

type MyHandler struct {
}

func (h *MyHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
	fmt.Fprintf(w, "Hello HTTP/2!")
}
func main() {
	handler := MyHandler{}

	server := http.Server{
		Addr:    "127.0.0.1:8080",
		Handler: &handler,
	}

	http2.ConfigureServer(&server, &http2.Server{})

	server.ListenAndServeTLS("cert.pem", "key.pem")
}

通过命令go run server.go运行即可。windows系统下可以通过git bash使用curl命令来测试

curl -I –http2 –insecure https://127.0.0.1:8080

Licensed under CC BY-NC-SA 4.0