前言
大家都知道,Go的标准库HTTP服务器默认支持HTTP/2。那么,在这篇文章中,我们将首先展示Go的http/2服务器功能,并解释如何将它们作为客户端使用。
在这篇文章中,我们将首先展示Go的http/2服务器功能,并解释如何将它们作为客户端使用。Go的标准库HTTP服务器默认支持HTTP/2。
下面话不多说了,来一起看看详细的介绍吧
HTTP/2 服务器
首先,让我们在Go中创建一个http/2服务器!根据http/2文档,所有东西都是为我们自动配置的,我们甚至不需要导入Go的标准库http2包:
HTTP/2强制使用TLS。为了实现这一点,我们首先需要一个私钥和一个证书。在Linux上,下面的命令执行这个任务。
1
|
openssl req -newkey rsa:2048 -nodes -keyout server.key -x509 -days 365 -out server.crt
|
该命令将生成两个文件:server.key 以及 server.crt
现在,对于服务器代码,以最简单的形式,我们将使用Go的标准库HTTP服务器,并启用TLS与生成的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
|
package main
import (
"log"
"net/http"
)
func main() {
// 在 8000 端口启动服务器
// 确切地说,如何运行HTTP/1.1服务器。
srv := &http.Server{Addr: ":8000" , Handler: http.HandlerFunc(handle)}
// 用TLS启动服务器,因为我们运行的是http/2,它必须是与TLS一起运行。
// 确切地说,如何使用TLS连接运行HTTP/1.1服务器。
log .Printf( "Serving on https://0.0.0.0:8000" )
log .Fatal(srv.ListenAndServeTLS( "server.crt" , "server.key" ))
}
func handle(w http.ResponseWriter, r *http.Request) {
// 记录请求协议
log .Printf( "Got connection: %s" , r.Proto)
// 向客户发送一条消息
w.Write([]byte( "Hello" ))
}
|
HTTP/2 客户端
在go中,标准 http.Client 也用于http/2请求。惟一的区别是在客户端的Transport字段,使用 http2.Transport 代替 http.Transport。
我们生成的服务器证书是“自签名”的,这意味着它不是由一个已知的证书颁发机构(CA)签署的。这将导致我们的客户端不相信它:
1
2
3
4
5
6
7
8
9
10
11
12
13
|
package main
import (
"fmt"
"net/http"
)
const url = "https://localhost:8000"
func main() {
_, err := http.Get(url)
fmt.Println(err)
}
|
让我们试着运行它:
1
2
|
$ go run h2-client.go
Get https: //localhost:8000: x509: certificate signed by unknown authority
|
在服务器日志中,我们还将看到客户端(远程)有一个错误:
http: TLS handshake error from [::1]:58228: remote error: tls: bad certificate
为了解决这个问题,我们可以用定制的TLS配置去配置我们的客户端。我们将把服务器证书文件添加到客户端“证书池”中,因为我们信任它,即使它不是由已知CA签名的。
我们还将添加一个选项,根据命令行标志在HTTP/1.1和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
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
|
package main
import (
"crypto/tls"
"crypto/x509"
"flag"
"fmt"
"io/ioutil"
"log"
"net/http"
"golang.org/x/net/http2"
)
const url = "https://localhost:8000"
var httpVersion = flag.Int( "version" , 2, "HTTP version" )
func main() {
flag.Parse()
client := &http.Client{}
// Create a pool with the server certificate since it is not signed
// by a known CA
caCert, err := ioutil.ReadFile( "server.crt" )
if err != nil {
log .Fatalf( "Reading server certificate: %s" , err)
}
caCertPool := x509.NewCertPool()
caCertPool.AppendCertsFromPEM(caCert)
// Create TLS configuration with the certificate of the server
tlsConfig := &tls.Config{
RootCAs: caCertPool,
}
// Use the proper transport in the client
switch *httpVersion {
case 1:
client.Transport = &http.Transport{
TLSClientConfig: tlsConfig,
}
case 2:
client.Transport = &http2.Transport{
TLSClientConfig: tlsConfig,
}
}
// Perform the request
resp, err := client.Get(url)
if err != nil {
log .Fatalf( "Failed get: %s" , err)
}
defer resp.Body.Close()
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
log .Fatalf( "Failed reading response body: %s" , err)
}
fmt.Printf(
"Got response %d: %s %s\n" ,
resp.StatusCode, resp.Proto, string(body)
)
}
|
这一次我们得到了正确的回应:
1
2
|
$ go run h2-client.go
Got response 200: HTTP /2 .0 Hello
|
在服务器日志中,我们将看到正确的日志线:获得连接:Got connection: HTTP/2.0!!
但是当我们尝试使用HTTP/1.1传输时,会发生什么呢?
1
2
|
$ go run h2-client.go -version 1
Got response 200: HTTP /1 .1 Hello
|
我们的服务器对HTTP/2没有任何特定的东西,所以它支持HTTP/1.1连接。这对于向后兼容性很重要。此外,服务器日志表明连接是HTTP/1.1:Got connection: HTTP/1.1。
HTTP/2 高级特性
我们创建了一个HTTP/2客户机-服务器连接,并且我们正在享受安全有效的连接带来的好处。但是HTTP/2提供了更多的特性,让我们来研究它们!
服务器推送
HTTP/2允许服务器推送“使用给定的目标构造一个合成请求”。
这可以很容易地在服务器处理程序中实现(在github上的视图):
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
|
func handle(w http.ResponseWriter, r *http.Request) {
// Log the request protocol
log .Printf( "Got connection: %s" , r.Proto)
// Handle 2nd request, must be before push to prevent recursive calls.
// Don't worry - Go protect us from recursive push by panicking.
if r.URL.Path == "/2nd" {
log .Println( "Handling 2nd" )
w.Write([]byte( "Hello Again!" ))
return
}
// Handle 1st request
log .Println( "Handling 1st" )
// Server push must be before response body is being written.
// In order to check if the connection supports push, we should use
// a type-assertion on the response writer.
// If the connection does not support server push, or that the push
// fails we just ignore it - server pushes are only here to improve
// the performance for HTTP/2 clients.
pusher, ok := w.(http.Pusher)
if !ok {
log .Println( "Can't push to client" )
} else {
err := pusher.Push( "/2nd" , nil)
if err != nil {
log .Printf( "Failed push: %v" , err)
}
}
// Send response body
w.Write([]byte( "Hello" ))
}
|
使用服务器推送
让我们重新运行服务器,并测试客户机。
对于HTTP / 1.1客户端:
1
2
|
$ go run . /h2-client .go -version 1
Got response 200: HTTP /1 .1 Hello
|
服务器日志将显示:
Got connection: HTTP/1.1Handling 1st
Can't push to client
HTTP/1.1客户端传输连接产生一个 http.ResponseWriter 没有实现http.Pusher,这是有道理的。在我们的服务器代码中,我们可以选择在这种客户机的情况下该做什么。
对于HTTP/2客户:
1
2
|
go run . /h2-client .go -version 2
Got response 200: HTTP /2 .0 Hello
|
服务器日志将显示:
Got connection: HTTP/2.0Handling 1st
Failed push: feature not supported
这很奇怪。我们的HTTP/2传输的客户端只得到了第一个“Hello”响应。日志表明连接实现了 http.Pusher 接口——但是一旦我们实际调用 Push() 函数——它就失败了。
排查发现,HTTP/2客户端传输设置了一个HTTP/2设置标志,表明推送是禁用的。
因此,目前没有选择使用Go客户机来使用服务器推送。
作为一个附带说明,google chrome作为一个客户端可以处理服务器推送。
服务器日志将显示我们所期望的,处理程序被调用两次,路径 / 和 /2nd,即使客户实际上只对路径 /:
Got connection: HTTP/2.0Handling 1st
Got connection: HTTP/2.0Handling 2nd
全双工通信
Go HTTP/2演示页面有一个echo示例,它演示了服务器和客户机之间的全双工通信。
让我们先用CURL来测试一下:
1
2
3
4
5
6
|
$ curl -i -XPUT --http2 https: //http2 .golang.org /ECHO -d hello
HTTP /2 200
content- type : text /plain ; charset=utf-8
date : Tue, 24 Jul 2018 12:20:56 GMT
HELLO
|
我们把curl配置为使用HTTP/2,并将一个PUT/ECHO发送给“hello”作为主体。服务器以“HELLO”作为主体返回一个HTTP/2 200响应。但我们在这里没有做任何复杂的事情,它看起来像是一个老式的HTTP/1.1半双工通信,有不同的头部。让我们深入研究这个问题,并研究如何使用HTTP/2全双工功能。
服务器实现
下面是HTTP echo处理程序的简化版本(不使用响应)。它使用 http.Flusher 接口,HTTP/2添加到http.ResponseWriter。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
|
type flushWriter struct {
w io.Writer
}
func (fw flushWriter) Write(p []byte) (n int , err error) {
n, err = fw.w.Write(p)
// Flush - send the buffered written data to the client
if f, ok := fw.w.(http.Flusher); ok {
f.Flush()
}
return
}
func echoCapitalHandler(w http.ResponseWriter, r *http.Request) {
// First flash response headers
if f, ok := w.(http.Flusher); ok {
f.Flush()
}
// Copy from the request body to the response writer and flush
// (send to client)
io.Copy(flushWriter{w: w}, r.Body)
}
|
服务器将从请求正文读取器复制到写入ResponseWriter和 Flush() 的“冲洗写入器”。同样,我们看到了笨拙的类型断言样式实现,冲洗操作将缓冲的数据发送给客户机。
请注意,这是全双工,服务器读取一行,并在一个HTTP处理程序调用中重复写入一行。
GO客户端实现
我试图弄清楚一个启用了HTTP/2的go客户端如何使用这个端点,并发现了这个Github问题。提出了类似于下面的代码。
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
|
const url = "https://http2.golang.org/ECHO"
func main() {
// Create a pipe - an object that implements `io.Reader` and `io.Writer`.
// Whatever is written to the writer part will be read by the reader part.
pr, pw := io.Pipe()
// Create an `http.Request` and set its body as the reader part of the
// pipe - after sending the request, whatever will be written to the pipe,
// will be sent as the request body. // This makes the request content dynamic, so we don't need to define it
// before sending the request.
req, err := http.NewRequest(http.MethodPut, url, ioutil.NopCloser(pr))
if err != nil {
log .Fatal(err)
}
// Send the request
resp, err := http.DefaultClient.Do(req) if err != nil {
log .Fatal(err)
}
log .Printf( "Got: %d" , resp.StatusCode)
// Run a loop which writes every second to the writer part of the pipe
// the current time.
go func() { for {
time .Sleep(1 * time .Second)
fmt.Fprintf(pw, "It is now %v\n" , time .Now())
}
}()
// Copy the server's response to stdout.
_, err = io.Copy(os.Stdout, res.Body)
log .Fatal(err)
}
|
总结
Go支持与服务器推送和全双工通信的HTTP/2连接,这也支持HTTP/1.1与标准库的标准TLS服务器的连接——这太不可思议了。对于标准的库HTTP客户端,它不支持服务器推送,但是支持标准库的标准HTTP的全双工通信。以上就是本篇的内容,大家有什么疑问可以在文章下面留言沟通。
好了,以上就是这篇文章的全部内容了,希望本文的内容对大家的学习或者工作具有一定的参考学习价值,如果有疑问大家可以留言交流,谢谢大家对服务器之家的支持。
原文链接:https://mp.weixin.qq.com/s/neBQ4Etx3RLhMQdM6GksVg