生命不止,继续 go go go !!!
继续echo web框架,今天搞一下http2。
HTTP2
What is HTTP/2?
HTTP/2 is a replacement for how HTTP is expressed “on the wire.” It is not a ground-up rewrite of the protocol; HTTP methods,status codes and semantics are the same,and it should be possible to use the same APIs as HTTP/1.x (possibly with some small additions) to represent the protocol.
The focus of the protocol is on performance; specifically,end-user perceived latency,network and server resource usage. One major goal is to allow the use of a single connection from browsers to a Web site.
新的二进制格式(Binary Format)
HTTP1.x的解析是基于文本。基于文本协议的格式解析存在天然缺陷,文本的表现形式有多样性,要做到健壮性考虑的场景必然很多,二进制则不同,只认0和1的组合。基于这种考虑HTTP2.0的协议解析决定采用二进制格式,实现方便且健壮。
多路复用(MultiPlexing)
即连接共享,即每一个request都是是用作连接共享机制的。一个request对应一个id,这样一个连接上可以有多个request,每个连接的request可以随机的混杂在一起,接收方可以根据request的 id将request再归属到各自不同的服务端请求里面。多路复用原理图:
header压缩
HTTP2.0使用encoder来减少需要传输的header大小,通讯双方各自cache一份header fields表,既避免了重复header的传输,又减小了需要传输的大小。
服务端推送(server push)
同SPDY一样,HTTP2.0也具有server push功能。
生成证书
go run C:\go\src\crypto\tls\generate_cert.go --host localhost 2017/11/22 10:06:58 written cert.pem 2017/11/22 10 :06:58 written key.pem
echo中的HTTP/2
代码main.go:
package main
import (
"fmt"
"net/http"
"github.com/labstack/echo"
)
func main() {
e := echo.New()
e.GET("/request",func(c echo.Context) error {
req := c.Request()
format := ` <code> Protocol: %s<br> Host: %s<br> Remote Address: %s<br> Method: %s<br> Path: %s<br> </code> `
return c.HTML(http.StatusOK,fmt.Sprintf(format,req.Proto,req.Host,req.RemoteAddr,req.Method,req.URL.Path))
})
e.Logger.Fatal(e.StartTLS(":1323","cert.pem","key.pem"))
}
浏览器输入:
https://localhost:1323/request
结果:
Protocol: HTTP/2.0
Host: localhost:1323
Remote Address: [::1]:1905
Method: GET
Path: /request
如果出现错误:
http: TLS handshake error from [::1]:1735: tls: first record does not look like a TLS handshake.
请检查是否输入的是https
golang.org/x/net/http2
文档地址:
https://godoc.org/golang.org/x/net/http2
获取:
get golang.org/x/net/http2
代码main.go:
package main
import (
"fmt"
"html"
"log"
"net/http"
"golang.org/x/net/http2"
)
func main() {
var srv http.Server
http2.VerboseLogs = true
srv.Addr = ":8080"
// This enables http2 support
http2.ConfigureServer(&srv,nil)
http.HandleFunc("/",func(w http.ResponseWriter,r *http.Request) {
fmt.Fprintf(w,"Hi tester %q\n",html.EscapeString(r.URL.Path))
ShowRequestInfoHandler(w,r)
})
// Listen as https ssl server
// NOTE: WITHOUT SSL IT WONT WORK!!
log.Fatal(srv.ListenAndServeTLS("cert.pem","key.pem"))
}
func ShowRequestInfoHandler(w http.ResponseWriter,r *http.Request) {
w.Header().Set("Content-Type","text/plain")
fmt.Fprintf(w,"Method: %s\n",r.Method)
fmt.Fprintf(w,"Protocol: %s\n",r.Proto)
fmt.Fprintf(w,"Host: %s\n",r.Host)
fmt.Fprintf(w,"RemoteAddr: %s\n",r.RemoteAddr)
fmt.Fprintf(w,"RequestURI: %q\n",r.RequestURI)
fmt.Fprintf(w,"URL: %#v\n",r.URL)
fmt.Fprintf(w,"Body.ContentLength: %d (-1 means unknown)\n",r.ContentLength)
fmt.Fprintf(w,"Close: %v (relevant for HTTP/1 only)\n",r.Close)
fmt.Fprintf(w,"TLS: %#v\n",r.TLS)
fmt.Fprintf(w,"\nHeaders:\n")
r.Header.Write(w)
}
浏览器输入:
https://localhost:8080/
结果:
Hi tester "/"
Method: GET
Protocol: HTTP/2.0
Host: localhost:8080
RemoteAddr: [::1]:2750
RequestURI: "/"
URL: &url.URL{Scheme:"",Opaque:"",User:(*url.Userinfo)(nil),Host:"",Path:"/",RawPath:"",ForceQuery:false,RawQuery:"",Fragment:""}
Body.ContentLength: 0 (-1 means unknown)
Close: false (relevant for HTTP/1 only)
TLS: &tls.ConnectionState{Version:0x303,HandshakeComplete:true,DidResume:false,CipherSuite:0xc02f,NegotiatedProtocol:"h2",NegotiatedProtocolIsMutual:true,ServerName:"localhost",PeerCertificates:[]*x509.Certificate(nil),VerifiedChains:[][]*x509.Certificate(nil),SignedCertificateTimestamps:[][]uint8(nil),OCSPResponse:[]uint8(nil),TLSUnique:[]uint8{0xa6, 0x3c, 0xfe, 0x93, 0x15, 0x4f, 0x74, 0xfc, 0x97, 0xca, 0x73}}
Headers:
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8 Accept-Encoding: gzip,deflate,br Accept-Language: zh-CN,zh;q=0.9,en;q=0.8 Alexatoolbar-Alx_ns_ph: AlexaToolbar/alx-4.0 Cookie: _ga=GA1.1.981224509.1509938615 Upgrade-Insecure-Requests: 1 User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML,like Gecko) Chrome/62.0.3202.94 Safari/537.36
Server Push
Server Push是什么
简单来讲就是当用户的浏览器和服务器在建立链接后,服务器主动将一些资源推送给浏览器并缓存起来,这样当浏览器接下来请求这些资源时就直接从缓存中读取,不会在从服务器上拉了,提升了速率。举一个例子就是:
假如一个页面有3个资源文件index.html,index.css,index.js,当浏览器请求index.html的时候,服务器不仅返回index.html的内容,同时将index.css和index.js的内容push给浏览器,当浏览器下次请求这2两个文件时就可以直接从缓存中读取了。
Server Push原理是什么
要想了解server push原理,首先要理解一些概念。我们知道HTTP2传输的格式并不像HTTP1使用文本来传输,而是启用了二进制帧(Frames)格式来传输,和server push相关的帧主要分成这几种类型:
HEADERS frame(请求返回头帧):这种帧主要携带的http请求头信息,和HTTP1的header类似。
DATA frames(数据帧) :这种帧存放真正的数据content,用来传输。
PUSH_PROMISE frame(推送帧):这种帧是由server端发送给client的帧,用来表示server push的帧,这种帧是实现server push的主要帧类型。
RST_STREAM(取消推送帧):这种帧表示请求关闭帧,简单讲就是当client不想接受某些资源或者接受timeout时会向发送方发送此帧,和PUSH_PROMISE frame一起使用时表示拒绝或者关闭server push。
了解了相关的帧类型,下面就是具体server push的实现过程了:
由多路复用我们可以知道HTTP2中对于同一个域名的请求会使用一条tcp链接而用不同的stream ID来区分各自的请求。
当client使用stream 1请求index.html时,server正常处理index.html的请求,并可以得知index.html页面还将要会请求index.css和index.js。
server使用stream 1发送PUSH_PROMISE frame给client告诉client我这边可以使用stream 2来推送index.js和stream 3来推送index.css资源。
server使用stream 1正常的发送HEADERS frame和DATA frames将index.html的内容返回给client。
client接收到PUSH_PROMISE frame得知stream 2和stream 3来接收推送资源。
server拿到index.css和index.js便会发送HEADERS frame和DATA frames将资源发送给client。
client拿到push的资源后会缓存起来当请求这个资源时会从直接从从缓存中读取。
Golang1.8中的Server Push
代码main.go:
package main
import (
"fmt"
"io/IoUtil"
"net/http"
)
var image []byte
// preparing image
func init() {
var err error
image,err = IoUtil.ReadFile("./image.png")
if err != nil {
panic(err)
}
}
// Send HTML and push image
func handlerHtml(w http.ResponseWriter,r *http.Request) {
pusher,ok := w.(http.Pusher)
if ok {
fmt.Println("Push /image")
pusher.Push("/image",nil)
}
w.Header().Add("Content-Type","text/html")
fmt.Fprintf(w,`<html><body><img src="/image"></body></html>`)
}
// Send image as usual HTTP request
func handlerImage(w http.ResponseWriter,"image/png")
w.Write(image)
}
func main() {
http.HandleFunc("/",handlerHtml)
http.HandleFunc("/image",handlerImage)
fmt.Println("start http listening :18443")
err := http.ListenAndServeTLS(":18443","server.crt","server.key",nil)
fmt.Println(err)
}
浏览器输入:
https://localhost:18443/
可以使用插件HTTP/2 and SPDY indicator
chrome://net-internals/#http2
echo框架中的Server Push
index.html
<!DOCTYPE html>
<html lang="en">
<head>
<Meta charset="UTF-8">
<Meta name="viewport" content="width=device-width,initial-scale=1.0">
<Meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>HTTP/2 Server Push</title>
<link rel="stylesheet" href="/app.css">
<script src="/app.js"></script>
</head>
<body>
<img class="echo" src="/echo.png">
<h2>The following static files are served via HTTP/2 server push</h2>
<ul>
<li><code>/app.css</code></li>
<li><code>/app.js</code></li>
<li><code>/echo.png</code></li>
</ul>
</body>
</html>
main.go
package main
import (
"net/http"
"github.com/labstack/echo"
)
func main() {
e := echo.New()
e.Static("/","static")
e.GET("/",func(c echo.Context) (err error) {
pusher,ok := c.Response().Writer.(http.Pusher)
if ok {
if err = pusher.Push("/app.css",nil); err != nil {
return
}
if err = pusher.Push("/app.js",nil); err != nil {
return
}
if err = pusher.Push("/echo.png",nil); err != nil {
return
}
}
return c.File("index.html")
})
e.Logger.Fatal(e.StartTLS(":1323","key.pem"))
}
浏览器输入:
https://localhost:1323/
参考:
http://www.alloyteam.com/2017/01/http2-server-push-research/