Go语言的http/2服务器功能及客户端使用
前言
大家都知道,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上,下面的命令执行这个任务。
openssl req -newkey rsa:2048 -nodes -keyout server.key -x509 -days 365 -out server.crt
该命令将生成两个文件:server.key 以及 server.crt
现在,对于服务器代码,以最简单的形式,我们将使用go的标准库http服务器,并启用tls与生成的ssl文件。
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)签署的。这将导致我们的客户端不相信它:
package main import ( "fmt" "net/http" ) const url = "https://localhost:8000" func main() { _, err := http.get(url) fmt.println(err) }
让我们试着运行它:
$ 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传输之间进行选择。
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) ) }
这一次我们得到了正确的回应:
$ go run h2-client.go got response 200: http/2.0 hello
在服务器日志中,我们将看到正确的日志线:获得连接:got connection: http/2.0!!
但是当我们尝试使用http/1.1传输时,会发生什么呢?
$ 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上的视图):
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客户端:
$ 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客户:
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来测试一下:
$ 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。
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问题。提出了类似于下面的代码。
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的全双工通信。以上就是本篇的内容,大家有什么疑问可以在文章下面留言沟通。
好了,以上就是这篇文章的全部内容了,希望本文的内容对大家的学习或者工作具有一定的参考学习价值,如果有疑问大家可以留言交流,谢谢大家对的支持。
上一篇: 详解Linux解压缩文件
下一篇: 吐槽社会生活的幽默笑段