GoLang HTTP 使用简介

2018-04-27 language golang network

除去细节,理解 HTTP 构建的网络应用只要关注客户端和服务端的处理,服务器主要用来接收客户端的请求,然后返回响应,在接收请求并处理的过程中,最重要的莫过于路由 (Router)。

这里简单介绍如何使用 net/http 构建 HTTP 请求。

示例

如下是一个最简单的示例。

package main

import (
    "fmt"
    "net/http"
)

func IndexHandler(w http.ResponseWriter, r *http.Request) {
    fmt.Fprintln(w, "hello world")
}

func FoobarHandler(w http.ResponseWriter, r *http.Request) {
    w.Write([]byte("Hi, foobar\n"))
}

func main() {
    http.HandleFunc("/", IndexHandler)
    http.Handle("/foobar", http.HandlerFunc(FoobarHandler))
    http.ListenAndServe("127.0.0.1:8000", nil)
}

然后可以通过 curl http://127.0.0.1:8000 访问。

Handler

对于服务器来说,在接收请求的过程中,最重要的就是路由,也就是实现一个 Multiplexer 器,其中有一个内置的 DefautServeMux,当然也可以自定义,其目的就是为了找到真正的处理函数 Handler,并构建 Response

Handler 负责输出 HTTP 响应的头和正文,任何满足了 http.Handler 接口的对象都可作为一个处理器。

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

Go 语言的 HTTP 包自带了几个函数用作常用处理器,比如 FileServer NotFoundHandlerRedirectHandler,如下是一个简单的示例。

package main

import (
    "fmt"
    "net/http"
    "time"
)

type customHandler struct {}

func (cb *customHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
    fmt.Fprintln(w, "Custom Handler!!! URI %s", r.URL.Path)
}

func main() {
    var server *http.Server = &http.Server{
        Addr:           ":8080",
        Handler:        &customHandler{},
        ReadTimeout:    10 * time.Second,
        WriteTimeout:   10 * time.Second,
        MaxHeaderBytes: 1 << 20,
    }
    server.ListenAndServe()
}

错误处理

通常有几种方式。

http.Error()

在标准的 net/http 库中提供了一个 Error() 函数,可以通过如下方式使用。

func ServeHTTP(w http.ResponseWriter, r *http.Request) {
    err := riskyFunc(r)
    if err != nil {
        http.Error(w, err.Error(), http.StatusInternalServerError)
        return
    }
}

此时的错误信息将直接返回给用户,如果有些系统相关的信息返回那么可能会导致部分安全问题,其中 Error() 函数在 net/http/server.go 中定义,代码如下。

func Error(w ResponseWriter, error string, code int) {
    w.Header().Set("Content-Type", "text/plain; charset=utf-8")
    w.Header().Set("X-Content-Type-Options", "nosniff")
    w.WriteHeader(code)
    fmt.Fprintln(w, error)
}

自定义

实现也很简单,模仿上述的代码即可。

Middleware

现在多数 Web 组件允许通过组件式中间件 “过滤” 请求,也就是中间件的作用。

package main

import (
    "fmt"
    "net/http"
)

func LogMidWare(h http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        fmt.Printf("logging %#v\n", *r.URL)
        h.ServeHTTP(w, r)
    })
}

func main() {
    http.Handle("/", LogMidWare(http.HandlerFunc(FoobarHandler)))
    http.ListenAndServe("127.0.0.1:8000", nil)
}

源码解析

对应的代码在 $GOPATH/src/net/http 目录下,在 server.go 中,有默认的 Mux 实现,也就是 DefaultServeMux

var DefaultServeMux = &defaultServeMux
var defaultServeMux ServeMux

如果用户没有创建自己的 MUX 那么实际使用的就是该变量。

Handler注册

默认调用的 Handler() 以及 HandleFunc() 实现如下。

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

func HandleFunc(pattern string, handler func(ResponseWriter, *Request)) {
    DefaultServeMux.HandleFunc(pattern, handler)
}

而根据如上的定义可知,真正调用的是 ServeMux 中的实现,上述两个接口真正调用的实际上是 ServeMux.Handle() 函数,而真正有效的是 mux.m[pattern] = muxEntry{h: handler, pattern: pattern} 这行代码。

监听服务端

在注册完用户需要的 Handler 之后,就开始调用 http.ListenAndServe() 监听端口并处理请求。除此之外,还提供了 TLS 版本的 http.ListenAndServeTLS() 接口。

ListenAndServe()
 |-net.Listen()                  开始监听
 |-Server.Serve()
   |-Accetp()
   |-Server.newConn()            新建链接
   |-conn.serve()                启动单独的协程开始处理
     |-serverHandler.ServeHTTP() 真正的处理
       |-ServeMux.ServeHTTP()    如果没有定义,那么使用默认的MUX
       | |-ServeMux.Handler()    这里会从Request.URL.Path中取出请求的路径
       | | |-ServeMux.handler()
       | |   |-Servemux.match()  匹配map表中的路由规则,也就是通过Handle()/HandleFunc()注册回调函数
       | |-Handler.ServeHTTP()   这里实际上已经实现了MAP的功能,也就是路由
       |-Handler.ServeHTTP()     如果在初始化时有自定义的Handler,这里会调用用户的代码

对于用户自定义的实现,有两种方式,一种是在新建 http.Server 时设置 Handler 成员,此时需要同时处理 URI 的路由规则。

另外一种,也是最常用的,自定义 MUX 以及 Handler 规则。

常用示例

TLS

服务端

服务端加载域证书对 domain-cert.pem domain-key.pem,分别为证书以及私钥,如下是最简单的实现。

package main

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

func indexHandler(w http.ResponseWriter, r *http.Request) {
    fmt.Fprintln(w, "hello world")
}

func main() {
    http.HandleFunc("/", indexHandler)
    http.ListenAndServeTLS(
        "127.0.0.1:8000",
        "out/domain-cert.pem",
        "out/domain-key.pem",
        nil,
    )
}

如果私钥进行了加密,那么上述方式会报 failed to parse private key 的错误,此时就需要通过如下方式。

package main

import (
	"crypto/tls"
	"crypto/x509"
	"encoding/pem"
	"fmt"
	"io/ioutil"
	"log"
	"net/http"
)

func indexHandler(w http.ResponseWriter, r *http.Request) {
	fmt.Fprintln(w, "hello world")
}

func main() {
	mux := http.NewServeMux()
	mux.HandleFunc("/", indexHandler)

	keyBytes, err := ioutil.ReadFile("out/domain-key.pem")
	if err != nil {
		log.Fatalf("Read private key file failed, %v\n", err)
	}
	val, _ := pem.Decode(keyBytes)
	if x509.IsEncryptedPEMBlock(val) {
		key, err := x509.DecryptPEMBlock(val, []byte("YourPassHere"))
		if err != nil {
			log.Fatalf("Decrypt private key failed, %v\n", err)
		}
		keyBytes = pem.EncodeToMemory(&pem.Block{
			Type: val.Type, Bytes: key,
		})
	}

	certBytes, err := ioutil.ReadFile("out/domain-cert.pem")
	if err != nil {
		log.Fatalf("Read certificate file failed, %v\n", err)
	}

	pair, err := tls.X509KeyPair(certBytes, keyBytes)
	if err != nil {
		log.Fatalf("Load key pair failed, %v\n", err)
	}

	cfg := &tls.Config{
		MinVersion: tls.VersionTLS12,
		CurvePreferences: []tls.CurveID{
			tls.CurveP521, tls.CurveP384, tls.CurveP256,
		},
		PreferServerCipherSuites: true,
		CipherSuites: []uint16{
			tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384,
			tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256,
			tls.TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA,
			tls.TLS_RSA_WITH_AES_256_GCM_SHA384,
			tls.TLS_RSA_WITH_AES_256_CBC_SHA,
		},
		Certificates: []tls.Certificate{pair},
	}
	svr := http.Server{
		Addr:         "127.0.0.1:8000",
		Handler:      mux,
		TLSConfig:    cfg,
		TLSNextProto: make(map[string]func(*http.Server, *tls.Conn, http.Handler), 0),
	}

	// 这里设置为空,会默认使用上述的证书
	log.Fatal(svr.ListenAndServeTLS("", ""))
}

客户端

一个证书文件中是可以保存多个证书的,所以,如下文件中会循环读取。

package main

import (
	"crypto/tls"
	"crypto/x509"
	"encoding/pem"
	"io/ioutil"
	"log"
	"net/http"
)

func main() {
	rest, err := ioutil.ReadFile("out/certs.pem")
	if err != nil {
		log.Fatalf("Read certificate file failed, %v\n", err)
	}

	var cert *pem.Block
	certs := x509.NewCertPool()
	for {
		cert, rest = pem.Decode(rest)
		if cert == nil {
			break
		}
		if ok := certs.AppendCertsFromPEM(pem.EncodeToMemory(cert)); !ok {
			log.Fatalf("Add cert failed.")
		}
	}

	cfg := &tls.Config{
		RootCAs:            certs,
		InsecureSkipVerify: false,
	}
	//cfg.BuildNameToCertificate()
	client := &http.Client{
		Transport: &http.Transport{
			TLSClientConfig: cfg,
		},
	}
	req, err := http.NewRequest("GET", "https://files.cargo.com:8000", nil)
	if err != nil {
		log.Fatalf("Create request failed, %v\n", err)
	}

	resp, err := client.Do(req)
	if err != nil {
		log.Fatalf("Get request failed, %v\n", err)
	}
	body, err := ioutil.ReadAll(resp.Body)
	if err != nil {
		log.Fatalf("Read body failed, %v\n", err)
	}
	log.Printf("response: %v", string(body))
}

常见错误

  • certificate relies on legacy Common Name field, use SANs... 建议不要使用 CommonName 需要使用 Subject Alternative Name 替换。

参考

为了防止由于压力过大导致雪崩,可以限制客户端的数量,详细可以参考 golang.org/x/net/netutil/listen.go 中关于 LimitListener 的实现。