您的位置:新葡亰496net > 新葡亰官网 > 新葡亰496net:特色和抓包剖析,golang中使用echo框

新葡亰496net:特色和抓包剖析,golang中使用echo框

发布时间:2019-06-17 08:31编辑:新葡亰官网浏览(126)

    HTTP2 Server Push的研究

    2017/01/05 · 基础技术 · HTTP/2

    原文出处: AlloyTeam   

    本文首发地址为-iOS HTTP/2 Server Push 探索 | 李剑飞的博客

    生命不止,继续 go go go !!!

    一分钟预览 HTTP2 特性和抓包分析

    2016/09/26 · JavaScript · HTTP/2

    原文出处: 段隆贤   

    参考cnodejs.org上面的静态服务器例子,写了下面的一个nodejs静态服务器例子,里面包含cache,压缩,贴代码如下:

    1,HTTP2的新特性。

    关于HTTP2的新特性,读着可以参看我之前的文章,这里就不在多说了,本篇文章主要讲一下server push这个特性。

    HTTP,HTTP2.0,SPDY,HTTPS你应该知道的一些事

     


    继续echo web框架,今天搞一下http2。

    背景

    近年来,http网络请求量日益添加,以下是httparchive统计,从2012-11-01到2016-09-01的请求数量和传输大小的趋势图:

    新葡亰496net 1

     

    当前大部份客户端&服务端架构的应用程序,都是用http/1.1连接的,现代浏览器与单个域最大连接数,都在4-6个左右,由上图Total Requests数据,如果不用CDN分流,平均有20个左右的串行请求。
    HTTP2 是1999年发布http1.1后的一次重大的改进,在协议层面改善了以上问题,减少资源占用,来,直接感受一下差异:

    HTTP/2 is the future of the Web, and it is here!
    这是 Akamai 公司建立的一个官方的演示,用以说明 HTTP/2 相比于之前的 HTTP/1.1 在性能上的大幅度提升。 同时请求 379 张图片,从Load time 的对比可以看出 HTTP/2 在速度上的优势。

    新葡亰496net 2

     

    本文所有源码和抓包文件在github

    复制代码 代码如下:

    2,Server Push是什么。

    简单来讲就是当用户的浏览器和服务器在建立链接后,服务器主动将一些资源推送给浏览器并缓存起来,这样当浏览器接下来请求这些资源时就直接从缓存中读取,不会在从服务器上拉了,提升了速率。举一个例子就是:

    假如一个页面有3个资源文件index.html,index.css,index.js,当浏览器请求index.html的时候,服务器不仅返回index.html的内容,同时将index.css和index.js的内容push给浏览器,当浏览器下次请求这2两个文件时就可以直接从缓存中读取了。

    新葡亰496net 3

    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功能。

    HTTP/2 源自 SPDY/2

    SPDY 系列协议由谷歌开发,于 2009 年公开。它的设计目标是降低 50% 的页面加载时间。当下很多著名的互联网公司都在自己的网站或 APP 中采用了 SPDY 系列协议(当前最新版本是 SPDY/3.1),因为它对性能的提升是显而易见的。主流的浏览器(谷歌、火狐、Opera)也都早已经支持 SPDY,它已经成为了工业标准,HTTP Working-Group 最终决定以 SPDY/2 为基础,开发 HTTP/2。HTTP/2标准于2015年5月以RFC 7540正式发表。

    但是,HTTP/2 跟 SPDY 仍有不同的地方,主要是以下两点:

    HTTP/2 支持明文 HTTP 传输,而 SPDY 强制使用 HTTPS
    HTTP/2 消息头的压缩算法采用 HPACK ,而非 SPDY 采用的 DEFLATE(感谢网友 逸风之狐指正)

    协议文档请见:rfc7540:HTTP2

    /**
     * 静态文件服务器测试例子
     * User: xuwm
     * Date: 13-5-17
     * Time: 上午8:38
     * To change this template use File | Settings | File Templates.
     */
    var port=3333;
    var http = require("http");
    var url = require("url");
    var fs = require("fs");
    var path = require("path");
    var mime = require("./mime").types;
    var config = require("./config");
    var zlib = require("zlib");
    //创建http服务端
    var server=http.createServer(function(request,response){
        var obj= url.parse(request.url);
        response.setHeader("Server","Node/V8");
        console.log(obj);
        var pathname=obj.pathname;
        if(pathname.slice(-1)==="/"){
            pathname=pathname config.Welcome.file;   //默认取当前默认下的index.html
        }
        var realPath = path.join("assets", path.normalize(pathname.replace(/../g, "")));
        console.log(realPath) ;
        var pathHandle=function(realPath){
        //用fs.stat方法获取文件
            fs.stat(realPath,function(err,stats){
                if(err){
                    response.writeHead(404,"not found",{'Content-Type':'text/plain'});
                    response.write("the request " realPath " is not found");
                    response.end();
                }else{
                    if(stats.isDirectory()){
                    }else{
                        var ext = path.extname(realPath);
                        ext = ext ? ext.slice(1) : 'unknown';
                        var contentType = mime[ext] || "text/plain";
                        response.setHeader("Content-Type", contentType);

    3,Server Push原理是什么。

    要想了解server push原理,首先要理解一些概念。我们知道HTTP2传输的格式并不像HTTP1使用文本来传输,而是启用了二进制帧(Frames)格式来传输,和server push相关的帧主要分成这几种类型:

    1. HEADERS frame(请求返回头帧):这种帧主要携带的http请求头信息,和HTTP1的header类似。
    2. DATA frames(数据帧) :这种帧存放真正的数据content,用来传输。
    3. PUSH_PROMISE frame(推送帧):这种帧是由server端发送给client的帧,用来表示server push的帧,这种帧是实现server push的主要帧类型。
    4. RST_STREAM(取消推送帧):这种帧表示请求关闭帧,简单讲就是当client不想接受某些资源或者接受timeout时会向发送方发送此帧,和PUSH_PROMISE frame一起使用时表示拒绝或者关闭server push。

    Note:HTTP2.0相关的帧其实包括10种帧,正是因为底层数据格式的改变,才为HTTP2.0带来许多的特性,帧的引入不仅有利于压缩数据,也有利于数据的安全性和可靠传输性。

    了解了相关的帧类型,下面就是具体server push的实现过程了:

    1. 由多路复用我们可以知道HTTP2中对于同一个域名的请求会使用一条tcp链接而用不同的stream ID来区分各自的请求。
    2. 当client使用stream 1请求index.html时,server正常处理index.html的请求,并可以得知index.html页面还将要会请求index.css和index.js。
    3. server使用stream 1发送PUSH_PROMISE frame给client告诉client我这边可以使用stream 2来推送index.js和stream 3来推送index.css资源。
    4. server使用stream 1正常的发送HEADERS frame和DATA frames将index.html的内容返回给client。
    5. client接收到PUSH_PROMISE frame得知stream 2和stream 3来接收推送资源。
    6. server拿到index.css和index.js便会发送HEADERS frame和DATA frames将资源发送给client。
    7. client拿到push的资源后会缓存起来当请求这个资源时会从直接从从缓存中读取。

    下图表示了整个流程:

    新葡亰496net 4

    HTTP/2

    生成证书

    go run C:gosrccryptotlsgenerate_cert.go --host localhost
    2017/11/22 10:06:58 written cert.pem
    2017/11/22 10 :06:58 written key.pem
    

    HTTP2特性概览

                        var lastModified = stats.mtime.toUTCString();
                        var ifModifiedSince = "If-Modified-Since".toLowerCase();
                        response.setHeader("Last-Modified", lastModified);

    4,Server Push怎么用。

    既然server push这么神奇,那么我们如何使用呢?怎么设置服务器push哪些文件呢?

    首先并不是所有的服务器都支持server push,nginx目前还不支持这个特性,可以在nginx的官方博客上得到证实,但是Apache和nodejs都已经支持了server push这一个特性,需要说明一点的是server push这个特性是基于浏览器和服务器的,所以浏览器并没有提供相应的js api来让用户直接操作和控制push的内容,所以只能是通过header信息和server的配置来实现具体的push内容,本文主要以nodejs来说明具体如何使用server push这一特性。

    准备工作:下载nodejs http2支持,本地启动nodejs服务。

    1. 首先我们使用nodejs搭建基本的server:

    JavaScript

    var http2 = require('http2');   var url=require('url'); var fs=require('fs'); var mine=require('./mine').types; var path=require('path');   var server = http2.createServer({   key: fs.readFileSync('./zs/localhost.key'),   cert: fs.readFileSync('./zs/localhost.crt') }, function(request, response) {     var pathname = url.parse(request.url).pathname;     var realPath = path.join("my", pathname);    //这里设置自己的文件名称;       var pushArray = [];     var ext = path.extname(realPath);     ext = ext ? ext.slice(1) : 'unknown';     var contentType = mine[ext] || "text/plain";       if (fs.existsSync(realPath)) {           response.writeHead(200, {             'Content-Type': contentType         });           response.write(fs.readFileSync(realPath,'binary'));       } else {       response.writeHead(404, {           'Content-Type': 'text/plain'       });         response.write("This request URL " pathname " was not found on this server.");       response.end();     }   });   server.listen(443, function() {   console.log('listen on 443'); });

    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
    var http2 = require('http2');
     
    var url=require('url');
    var fs=require('fs');
    var mine=require('./mine').types;
    var path=require('path');
     
    var server = http2.createServer({
      key: fs.readFileSync('./zs/localhost.key'),
      cert: fs.readFileSync('./zs/localhost.crt')
    }, function(request, response) {
        var pathname = url.parse(request.url).pathname;
        var realPath = path.join("my", pathname);    //这里设置自己的文件名称;
     
        var pushArray = [];
        var ext = path.extname(realPath);
        ext = ext ? ext.slice(1) : 'unknown';
        var contentType = mine[ext] || "text/plain";
     
        if (fs.existsSync(realPath)) {
     
            response.writeHead(200, {
                'Content-Type': contentType
            });
     
            response.write(fs.readFileSync(realPath,'binary'));
     
        } else {
          response.writeHead(404, {
              'Content-Type': 'text/plain'
          });
     
          response.write("This request URL " pathname " was not found on this server.");
          response.end();
        }
     
    });
     
    server.listen(443, function() {
      console.log('listen on 443');
    });

    这几行代码就是简单搭建一个nodejs http2服务,打开chrome,我们可以看到所有请求都走了http2,同时也可以验证多路复用的特性。

    新葡亰496net 5

    这里需要注意几点:

    1. 创建http2的nodejs服务必须时基于https的,因为现在主流的浏览器都要支持SSL/TLS的http2,证书和私钥可以自己通过OPENSSL生成。
    2. node http2的相关api和正常的node httpserver相同,可以直接使用。

    3. 设置我们的server push:

    JavaScript

    var pushItem = response.push('/css/bootstrap.min.css', {        request: {             accept: '*/*'        },       response: {             'content-type': 'text/css'      } }); pushItem.end(fs.readFileSync('/css/bootstrap.min.css','binary'));

    1
    2
    3
    4
    5
    6
    7
    8
    9
    var pushItem = response.push('/css/bootstrap.min.css', {
           request: {
                accept: '*/*'
           },
          response: {
                'content-type': 'text/css'
         }
    });
    pushItem.end(fs.readFileSync('/css/bootstrap.min.css','binary'));

    我们设置了bootstrap.min.css来通过server push到我们的浏览器,我们可以在浏览器中查看:

    新葡亰496net 6

    可以看到,启动server push的资源timelime非常快,大大加速了css的获取时间。

    这里需要注意下面几点:

    1. 我们调用response.push(),就是相当于server发起了PUSH_PROMISE frame来告知浏览器bootstrap.min.css将会由server push来获取。
    2. response.push()返回的对象时一个正常的ServerResponse,end(),writeHeader()等方法都可以正常调用。
    3. 这里一旦针对某个资源调用response.push()即发起PUSH_PROMISE frame后,要做好容错机制,因为浏览器在下次请求这个资源时会且只会等待这个server push回来的资源,这里要做好超时和容错即下面的代码:
    4. JavaScript

      try {     pushItem.end(fs.readFileSync('my/css/bootstrap.min.css','binary'));     } catch(e) {        response.writeHead(404, {            'Content-Type': 'text/plain'        });        response.end('request error'); }   pushItem.stream.on('error', function(err){     response.end(err.message); });   pushItem.stream.on('finish', function(err){    console.log('finish'); });

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      try {
          pushItem.end(fs.readFileSync('my/css/bootstrap.min.css','binary'));
          } catch(e) {
             response.writeHead(404, {
                 'Content-Type': 'text/plain'
             });
             response.end('request error');
      }
       
      pushItem.stream.on('error', function(err){
          response.end(err.message);
      });
       
      pushItem.stream.on('finish', function(err){
         console.log('finish');
      });

      上面的代码你可能会发现许多和正常nodejs的httpserver不一样的东西,那就是stream,其实整个http2都是以stream为单位,这里的stream其实可以理解成一个请求,更多的api可以参考:node-http2。

    5. 最后给大家推荐一个老外写的专门服务http2的node server有兴趣的可以尝试一下。

    HTTP/2 Server Push 是什么

    当用户的浏览器和服务器在建立链接后,服务器主动将一些资源推送给浏览器并缓存起来,这样当浏览器接下来请求这些资源时就直接从缓存中读取,不会在从服务器上拉了,提升了速率。举一个例子就是:

    假如一个页面有3个资源文件index.html,index.css,index.js,当浏览器请求index.html的时候,服务器不仅返回index.html的内容,同时将index.css和index.js的内容push给浏览器,当浏览器下次请求这2两个文件时就可以直接从缓存中读取了。

    如下图所示:

    新葡亰496net 7

    Apple-http2ServerPush

    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"))
    }
    

    浏览器输入:

    结果:

    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

    1. 二进制协议

    HTTP/2 采用二进制格式传输数据,而非 HTTP/1.x 的文本格式

    新葡亰496net 8

     

    由上图可以看到HTTP2在原来的应用层和HTTP层添加了一层二进制传输。

    二进制协议的一个好处是,可以定义额外的帧。

    HTTP/2 定义了近十种帧(详情可分析抓包文件),为将来的高级应用打好了基础。如果使用文本实现这种功能,解析数据将会变得非常麻烦,二进制解析则方便得多。
    RFC7540:Frame Definitions

    新葡亰496net 9

    协议中定义的帧

                        if (ext.match(config.Expires.fileMatch)) {
                            var expires = new Date();
                            expires.setTime(expires.getTime() config.Expires.maxAge * 1000);
                            response.setHeader("Expires", expires.toUTCString());
                            response.setHeader("Cache-Control", "max-age=" config.Expires.maxAge);
                        }

    5,Server Push相关问题。

    1. 我们知道现在我们web的资源一般都是放在CDN上的,那么CDN的优势和server push的优势有何区别呢,到底是哪个比较快呢?这个问题笔者也一直在研究,本文的相关demo都只能算做一个演示,具体的线上实践还在进行中。
    2. 由于HTTP2的一些新特性例如多路复用,server push等等都是基于同一个域名的,所以这可能会对我们之前对于HTTP1的一些优化措施例如(资源拆分域名,合并等等)不一定适用。
    3. server push不仅可以用作拉取静态资源,我们的cgi请求即ajax请求同样可以使用server push来发送数据。
    4. 最完美的结果是CDN域名支持HTTP2,web server域名也同时支持HTTP2。

     

    新葡亰496net:特色和抓包剖析,golang中使用echo框架中的HTTP。参考资料:

    1. HTTP2官方标准:
    2. 维基百科:
    3. 1 赞 1 收藏 评论

    新葡亰496net 10

    HTTP/2 Server Push 原理是什么

    要想了解server push原理,首先要理解一些概念。我们知道HTTP/2传输的格式并不像HTTP1使用文本来传输,而是启用了二进制帧(Frames)格式来传输,和server push相关的帧主要分成这几种类型:

    1. HEADERS frame(请求返回头帧):这种帧主要携带的http请求头信息,和HTTP1的header类似。
    2. DATA frames(数据帧) :这种帧存放真正的数据content,用来传输。
    3. PUSH_PROMISE frame(推送帧):这种帧是由server端发送给client的帧,用来表示server push的帧,这种帧是实现server push的主要帧类型。
    4. RST_STREAM(取消推送帧):这种帧表示请求关闭帧,简单讲就是当client不想接受某些资源或者接受timeout时会向发送方发送此帧,和PUSH_PROMISE frame一起使用时表示拒绝或者关闭server push。

    (PS:HTTP/2相关的帧其实包括10种帧,正是因为底层数据格式的改变,才为HTTP/2带来许多的特性,帧的引入不仅有利于压缩数据,也有利于数据的安全性和可靠传输性。)

    了解了相关的帧类型,下面就是具体server push的实现过程了:

    1. 由多路复用我们可以知道HTTP/2中对于同一个域名的请求会使用一条tcp链接而用不同的stream ID来区分各自的请求。
    2. 当client使用stream 1请求index.html时,server正常处理index.html的请求,并可以得知index.html页面还将要会请求index.css和index.js。
    3. server使用stream 1发送PUSH_PROMISE frame给client告诉client我这边可以使用stream 2来推送index.js和stream 3来推送index.css资源。
    4. server使用stream 1正常的发送HEADERS frame和DATA frames将index.html的内容返回给client。
    5. client接收到PUSH_PROMISE frame得知stream 2和stream 3来接收推送资源。
    6. server拿到index.css和index.js便会发送HEADERS frame和DATA frames将资源发送给client。
    7. client拿到push的资源后会缓存起来当请求这个资源时会从直接从从缓存中读取。

    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 %qn", 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: %sn", r.Method)
        fmt.Fprintf(w, "Protocol: %sn", r.Proto)
        fmt.Fprintf(w, "Host: %sn", r.Host)
        fmt.Fprintf(w, "RemoteAddr: %sn", r.RemoteAddr)
        fmt.Fprintf(w, "RequestURI: %qn", r.RequestURI)
        fmt.Fprintf(w, "URL: %#vn", 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: %#vn", r.TLS)
        fmt.Fprintf(w, "nHeaders:n")
        r.Header.Write(w)
    }
    

    浏览器输入:

    结果:

    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, 0x3c, 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
    

    2. 多路复用

    HTTP/2 复用TCP连接,在一个连接里,客户端和浏览器都可以同时发送多个请求或回应,而且不用按照顺序一一对应,这样就避免了”队头堵塞”(见TCP/IP详解卷一)。
    每个 Frame Header 都有一个 Stream ID 就是被用于实现该特性。每次请求/响应使用不同的 Stream ID。就像同一个 TCP 链接上的数据包通过 IP: PORT 来区分出数据包去往哪里一样。

    新葡亰496net 11

    rfc7540: HTTP2 Multiplexing中对Multiplexing的说明

    Streams and Multiplexing A "stream" is an independent, bidirectional sequence of frames exchanged between the client and server within an HTTP/2 connection. Streams have several important characteristics: o A single HTTP/2 connection can contain multiple concurrently open streams, with either endpoint interleaving frames from multiple streams. o Streams can be established and used unilaterally or shared by either the client or server. o Streams can be closed by either endpoint. o The order in which frames are sent on a stream is significant. Recipients process frames in the order they are received. In particular, the order of HEADERS and DATA frames is semantically significant. o Streams are identified by an integer. Stream identifiers are assigned to streams by the endpoint initiating the stream.

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    Streams and Multiplexing
     
       A "stream" is an independent, bidirectional sequence of frames
       exchanged between the client and server within an HTTP/2 connection.
       Streams have several important characteristics:
     
       o  A single HTTP/2 connection can contain multiple concurrently open
          streams, with either endpoint interleaving frames from multiple
          streams.
     
       o  Streams can be established and used unilaterally or shared by
          either the client or server.
     
       o  Streams can be closed by either endpoint.
     
       o  The order in which frames are sent on a stream is significant.
          Recipients process frames in the order they are received.  In
          particular, the order of HEADERS and DATA frames is semantically
          significant.
     
       o  Streams are identified by an integer.  Stream identifiers are
          assigned to streams by the endpoint initiating the stream.

                        if (request.headers[ifModifiedSince] && lastModified == request.headers[ifModifiedSince]) {
                            console.log("从浏览器cache里取")
                            response.writeHead(304, "Not Modified");
                            response.end();
                        } else {
                            var raw = fs.createReadStream(realPath);
                            var acceptEncoding = request.headers['accept-encoding'] || "";
                            var matched = ext.match(config.Compress.match);

    Server Push 怎么用

    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的资源后会缓存起来当请求这个资源时会从直接从从缓存中读取。

    3. 数据流

    数据流发送到一半的时候,客户端和服务器都可以发送信号(RST_STREAM帧),取消这个数据流。1.1版取消数据流的唯一方法,就是关闭TCP连接。这就是说,HTTP/2 可以取消某一次请求,同时保证TCP连接还打开着,可以被其他请求使用。

                            if (matched && acceptEncoding.match(/bgzipb/)) {
                                response.writeHead(200, "Ok", {'Content-Encoding': 'gzip'});
                                raw.pipe(zlib.createGzip()).pipe(response);
                            } else if (matched && acceptEncoding.match(/bdeflateb/)) {
                                response.writeHead(200, "Ok", {'Content-Encoding': 'deflate'});
                                raw.pipe(zlib.createDeflate()).pipe(response);
                            } else {
                                response.writeHead(200, "Ok");
                                raw.pipe(response);
                            }
                        }
                    }
                }
            });

    使用 nghttp2 调试 HTTP/2 流量

    查看 HTTP/2 流量的几种方式

    • 在 Chrome 地址栏输入 chrome://net-internals/#http2,使用 Chrome 自带的 HTTP/2 调试工具;
      使用方便,但受限于 Chrome 浏览器,对于 Chrome 不支持的 h2c(HTTP/2 Cleartext,没有部署 TLS 的 HTTP/2)协议无能为力。同时,这个工具显示的信息经过了解析和筛选,不够全面。
    • 使用 Wireshark 调试 HTTP/2 流量;
      Wireshark 位于服务端和浏览器之间,充当的是中间人角色,用它查看 HTTP/2 over HTTPS 流量时,必须拥有网站私钥或者借助浏览器共享对称密钥,才能解密 TLS 流量,配置起来比较麻烦。

    nghttp2,是一个用 C 实现的 HTTP/2 库,支持 h2c。它可以做为其它软件的一部分,为其提供 HTTP/2 相关功能(例如 curl 的 HTTP/2 功能就是用的 nghttp2)。除此之外,它还提供了四个有用的 HTTP/2 工具:

    • nghttp:HTTP/2 客户端;
    • nghttpd:HTTP/2 服务端;
    • nghttpx:HTTP/2 代理,提供 HTTP/1、HTTP/2 等协议之间的转换;
    • h2load:HTTP/2 性能测试工具;

    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, r *http.Request) {
        w.Header().Set("Content-Type", "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)
    }
    

    浏览器输入:

    可以使用插件HTTP/2 and SPDY indicator
    chrome://net-internals/#http2

    4. 头信息压缩:

    HTTP/2 对消息头采用 HPACK 进行压缩传输,能够节省消息头占用的网络的流量。而 HTTP/1.x 每次请求,都会携带大量冗余头信息,浪费了很多带宽资源。
    HTTP2对http头建立索引表,相同的头只发送hash table 的index, 同时还用了霍夫曼编码和传统的gzip压缩。

        }
        pathHandle(realPath);
    });
    server.listen(port);
    console.log("http server run in port:" port);

    nghttp2 安装

    先来用 brew 看一下有没有 nghttp 相关的库:

    ~ brew search nghttp
    nghttp2
    

    看来是有 nghttp2 的,再用 brew 看下需要安装哪些环境:

    ~ brew info nghttp2
    nghttp2: stable 1.21.0 (bottled), HEAD
    HTTP/2 C Library
    https://nghttp2.org/
    Not installed
    From: https://github.com/Homebrew/homebrew-core/blob/master/Formula/nghttp2.rb
    ==> Dependencies
    Build: sphinx-doc ✘, pkg-config ✔, cunit ✘
    Required: c-ares ✘, libev ✘, openssl ✔, libevent ✘, jansson ✘, boost ✘, spdylay ✘
    Recommended: jemalloc ✘
    ==> Requirements
    Optional: python3 ✔
    ==> Options
    --with-examples
        Compile and install example programs
    --with-python3
        Build python3 bindings
    --without-docs
        Don't build man pages
    --without-jemalloc
        Build without jemalloc support
    --HEAD
        Install HEAD version
    

    看来需要的依赖还挺多。

    使用 brew 安装 nghttp2 :

    brew install nghttp2
    

    一切妥当后,nghttp2 提供的几个工具就可以直接用了。

    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", "cert.pem", "key.pem"))
    }
    

    浏览器输入:

    参考:

    新葡亰496net 12

    5. 服务器推送

    服务端能够更快的把资源推送给客户端。例如服务端可以主动把 JS 和 CSS 文件推送给客户端,而不需要客户端解析 HTML 再发送这些请求。当客户端需要的时候,它已经在客户端了。

    那么存在一个问题,如果客户端设置了缓存怎么办。有三种方式(来自社区)

    • 客户端可以通过设置SETTINGS_ENABLE_PUSH为0值通知服务器端禁用推送
    • 发现缓存后,客户端和服务器都可以发送信号(RST_STREAM帧),取消这个数据流。
    • cache-digest(提案)

      rfc7540: HTTP2 Server Push

      #### 6. 流优先级

      HTTP2允许浏览器指定资源的优先级。

      rfc7540: Stream Priority

    首先需要在JS文件里创建一个assets的文件夹,里面放入你要浏览的静态文件,比如,index.html,demo.js等。

    nghttp

    nghttp 做为一个功能完整的 HTTP/2 客户端,非常适合用来查看和调试 HTTP/2 流量。它支持的参数很多,通过官方文档或者 nghttp -h 都能查看。最常用几个参数如下:

    • -v, --verbose,输出完整的 debug 信息;
    • -n, --null-out,丢弃下载的数据;
    • -a, --get-assets,下载 html 中的 css、js、image 等外链资源;
    • -H, --header = < HEADER >,添加请求头部字段,如 -H':method: PUT';
    • -u, --upgrade,使用 HTTP 的 Upgrade 机制来协商 HTTP/2 协议,用于 h2c,详见下面的例子;

    以下是使用 nghttp 访问 https://h2o.examp1e.net 的结果。从调试信息中可以清晰看到 h2c 协商以及 Server Push 的整个过程:

    nghttp -nv 'https://h2o.examp1e.net'
    [  0.201] Connected
    The negotiated protocol: h2
    [  1.180] send SETTINGS frame <length=12, flags=0x00, stream_id=0>
              (niv=2)
              [SETTINGS_MAX_CONCURRENT_STREAMS(0x03):100]
              [SETTINGS_INITIAL_WINDOW_SIZE(0x04):65535]
    [  1.180] send PRIORITY frame <length=5, flags=0x00, stream_id=3>
              (dep_stream_id=0, weight=201, exclusive=0)
    [  1.180] send PRIORITY frame <length=5, flags=0x00, stream_id=5>
              (dep_stream_id=0, weight=101, exclusive=0)
    [  1.180] send PRIORITY frame <length=5, flags=0x00, stream_id=7>
              (dep_stream_id=0, weight=1, exclusive=0)
    [  1.180] send PRIORITY frame <length=5, flags=0x00, stream_id=9>
              (dep_stream_id=7, weight=1, exclusive=0)
    [  1.180] send PRIORITY frame <length=5, flags=0x00, stream_id=11>
              (dep_stream_id=3, weight=1, exclusive=0)
    [  1.180] send HEADERS frame <length=39, flags=0x25, stream_id=13>
              ; END_STREAM | END_HEADERS | PRIORITY
              (padlen=0, dep_stream_id=11, weight=16, exclusive=0)
              ; Open new stream
              :method: GET
              :path: /
              :scheme: https
              :authority: h2o.examp1e.net
              accept: */*
              accept-encoding: gzip, deflate
              user-agent: nghttp2/1.21.1
    [  1.373] recv SETTINGS frame <length=12, flags=0x00, stream_id=0>
              (niv=2)
              [SETTINGS_MAX_CONCURRENT_STREAMS(0x03):100]
              [SETTINGS_INITIAL_WINDOW_SIZE(0x04):16777216]
    [  1.373] recv SETTINGS frame <length=0, flags=0x01, stream_id=0>
              ; ACK
              (niv=0)
    [  1.373] recv (stream_id=13) :method: GET
    [  1.373] recv (stream_id=13) :scheme: https
    [  1.373] recv (stream_id=13) :authority: h2o.examp1e.net
    [  1.373] recv (stream_id=13) :path: /search/jquery-1.9.1.min.js
    [  1.373] recv (stream_id=13) accept: */*
    [  1.373] recv (stream_id=13) accept-encoding: gzip, deflate
    [  1.373] recv (stream_id=13) user-agent: nghttp2/1.21.1
    [  1.373] recv PUSH_PROMISE frame <length=59, flags=0x04, stream_id=13>
              ; END_HEADERS
              (padlen=0, promised_stream_id=2)
    [  1.373] recv (stream_id=2) :status: 200
    [  1.373] recv (stream_id=2) server: h2o/2.2.0-beta2
    [  1.373] recv (stream_id=2) date: Mon, 10 Apr 2017 06:30:29 GMT
    [  1.373] recv (stream_id=2) content-type: application/javascript
    [  1.373] recv (stream_id=2) last-modified: Thu, 14 May 2015 04:10:14 GMT
    [  1.373] recv (stream_id=2) etag: "55542026-169d5"
    [  1.373] recv (stream_id=2) accept-ranges: bytes
    [  1.373] recv (stream_id=2) x-http2-push: pushed
    [  1.373] recv (stream_id=2) content-length: 92629
    [  1.373] recv HEADERS frame <length=126, flags=0x04, stream_id=2>
              ; END_HEADERS
              (padlen=0)
              ; First push response header
    [  1.373] recv (stream_id=13) :method: GET
    [  1.373] recv (stream_id=13) :scheme: https
    [  1.373] recv (stream_id=13) :authority: h2o.examp1e.net
    [  1.373] recv (stream_id=13) :path: /search/oktavia-jquery-ui.js
    [  1.373] recv (stream_id=13) accept: */*
    [  1.373] recv (stream_id=13) accept-encoding: gzip, deflate
    [  1.373] recv (stream_id=13) user-agent: nghttp2/1.21.1
    [  1.373] recv PUSH_PROMISE frame <length=33, flags=0x04, stream_id=13>
              ; END_HEADERS
              (padlen=0, promised_stream_id=4)
    [  1.373] recv (stream_id=4) :status: 200
    [  1.373] recv (stream_id=4) server: h2o/2.2.0-beta2
    [  1.373] recv (stream_id=4) date: Mon, 10 Apr 2017 06:30:29 GMT
    [  1.373] recv (stream_id=4) content-type: application/javascript
    [  1.373] recv (stream_id=4) last-modified: Thu, 14 May 2015 04:10:14 GMT
    [  1.373] recv (stream_id=4) etag: "55542026-1388"
    [  1.373] recv (stream_id=4) accept-ranges: bytes
    [  1.374] recv (stream_id=4) x-http2-push: pushed
    [  1.374] recv (stream_id=4) content-length: 5000
    [  1.374] recv HEADERS frame <length=28, flags=0x04, stream_id=4>
              ; END_HEADERS
              (padlen=0)
              ; First push response header
    [  1.374] recv (stream_id=13) :method: GET
    [  1.374] recv (stream_id=13) :scheme: https
    [  1.374] recv (stream_id=13) :authority: h2o.examp1e.net
    [  1.374] recv (stream_id=13) :path: /search/oktavia-english-search.js
    [  1.374] recv (stream_id=13) accept: */*
    [  1.374] recv (stream_id=13) accept-encoding: gzip, deflate
    [  1.374] recv (stream_id=13) user-agent: nghttp2/1.21.1
    [  1.374] recv PUSH_PROMISE frame <length=35, flags=0x04, stream_id=13>
              ; END_HEADERS
              (padlen=0, promised_stream_id=6)
    [  1.374] recv (stream_id=6) :status: 200
    [  1.374] recv (stream_id=6) server: h2o/2.2.0-beta2
    [  1.374] recv (stream_id=6) date: Mon, 10 Apr 2017 06:30:29 GMT
    [  1.374] recv (stream_id=6) content-type: application/javascript
    [  1.374] recv (stream_id=6) last-modified: Thu, 14 May 2015 04:10:14 GMT
    [  1.374] recv (stream_id=6) etag: "55542026-34dd6"
    [  1.374] recv (stream_id=6) accept-ranges: bytes
    [  1.374] recv (stream_id=6) x-http2-push: pushed
    [  1.374] recv (stream_id=6) content-length: 216534
    [  1.374] recv HEADERS frame <length=31, flags=0x04, stream_id=6>
              ; END_HEADERS
              (padlen=0)
              ; First push response header
    [  1.374] recv (stream_id=13) :method: GET
    [  1.374] recv (stream_id=13) :scheme: https
    [  1.374] recv (stream_id=13) :authority: h2o.examp1e.net
    [  1.374] recv (stream_id=13) :path: /assets/style.css
    [  1.374] recv (stream_id=13) accept: */*
    [  1.374] recv (stream_id=13) accept-encoding: gzip, deflate
    [  1.374] recv (stream_id=13) user-agent: nghttp2/1.21.1
    [  1.374] recv PUSH_PROMISE frame <length=24, flags=0x04, stream_id=13>
              ; END_HEADERS
              (padlen=0, promised_stream_id=8)
    [  1.374] recv (stream_id=8) :status: 200
    [  1.374] recv (stream_id=8) server: h2o/2.2.0-beta2
    [  1.374] recv (stream_id=8) date: Mon, 10 Apr 2017 06:30:29 GMT
    [  1.374] recv (stream_id=8) content-type: text/css
    [  1.374] recv (stream_id=8) last-modified: Tue, 20 Sep 2016 05:27:06 GMT
    [  1.374] recv (stream_id=8) etag: "57e0c8aa-1586"
    [  1.374] recv (stream_id=8) accept-ranges: bytes
    [  1.374] recv (stream_id=8) x-http2-push: pushed
    [  1.374] recv (stream_id=8) content-length: 5510
    [  1.374] recv HEADERS frame <length=58, flags=0x04, stream_id=8>
              ; END_HEADERS
              (padlen=0)
              ; First push response header
    [  1.374] recv (stream_id=13) :method: GET
    [  1.374] recv (stream_id=13) :scheme: https
    [  1.374] recv (stream_id=13) :authority: h2o.examp1e.net
    [  1.374] recv (stream_id=13) :path: /assets/searchstyle.css
    [  1.374] recv (stream_id=13) accept: */*
    [  1.374] recv (stream_id=13) accept-encoding: gzip, deflate
    [  1.374] recv (stream_id=13) user-agent: nghttp2/1.21.1
    [  1.374] recv PUSH_PROMISE frame <length=28, flags=0x04, stream_id=13>
              ; END_HEADERS
              (padlen=0, promised_stream_id=10)
    [  1.374] recv (stream_id=10) :status: 200
    [  1.374] recv (stream_id=10) server: h2o/2.2.0-beta2
    [  1.374] recv (stream_id=10) date: Mon, 10 Apr 2017 06:30:29 GMT
    [  1.374] recv (stream_id=10) content-type: text/css
    [  1.374] recv (stream_id=10) last-modified: Tue, 20 Sep 2016 05:27:06 GMT
    [  1.374] recv (stream_id=10) etag: "57e0c8aa-8dd"
    [  1.374] recv (stream_id=10) accept-ranges: bytes
    [  1.374] recv (stream_id=10) x-http2-push: pushed
    [  1.374] recv (stream_id=10) content-length: 2269
    [  1.374] recv HEADERS frame <length=27, flags=0x04, stream_id=10>
              ; END_HEADERS
              (padlen=0)
              ; First push response header
    [  1.374] recv (stream_id=13) :status: 200
    [  1.374] recv (stream_id=13) server: h2o/2.2.0-beta2
    [  1.374] recv (stream_id=13) date: Mon, 10 Apr 2017 06:30:29 GMT
    [  1.374] recv (stream_id=13) link: </search/jquery-1.9.1.min.js>; rel=preload
    [  1.374] recv (stream_id=13) link: </search/oktavia-jquery-ui.js>; rel=preload
    [  1.374] recv (stream_id=13) link: </search/oktavia-english-search.js>; rel=preload
    [  1.374] recv (stream_id=13) link: </assets/style.css>; rel=preload
    [  1.374] recv (stream_id=13) link: </assets/searchstyle.css>; rel=preload
    [  1.374] recv (stream_id=13) cache-control: no-cache
    [  1.374] recv (stream_id=13) content-type: text/html
    [  1.374] recv (stream_id=13) last-modified: Wed, 05 Apr 2017 06:55:14 GMT
    [  1.374] recv (stream_id=13) etag: "58e494d2-1665"
    [  1.374] recv (stream_id=13) accept-ranges: bytes
    [  1.374] recv (stream_id=13) set-cookie: h2o_casper=AmgAAAAAAAAAAAAYxfEYAAABSA; Path=/; Expires=Tue, 01 Jan 2030 00:00:00 GMT; Secure
    [  1.374] recv (stream_id=13) content-length: 5733
    [  1.374] recv HEADERS frame <length=304, flags=0x04, stream_id=13>
              ; END_HEADERS
              (padlen=0)
              ; First response header
    [  1.375] send SETTINGS frame <length=0, flags=0x01, stream_id=0>
              ; ACK
              (niv=0)
    [  1.566] recv DATA frame <length=16137, flags=0x00, stream_id=2>
    [  1.567] recv DATA frame <length=5000, flags=0x01, stream_id=4>
              ; END_STREAM
    [  1.567] recv DATA frame <length=4915, flags=0x00, stream_id=6>
    [  1.766] recv DATA frame <length=2829, flags=0x00, stream_id=8>
    [  1.766] recv DATA frame <length=2269, flags=0x01, stream_id=10>
              ; END_STREAM
    [  1.766] send WINDOW_UPDATE frame <length=4, flags=0x00, stream_id=0>
              (window_size_increment=33120)
    [  1.767] recv DATA frame <length=9065, flags=0x00, stream_id=2>
    [  1.970] recv DATA frame <length=2829, flags=0x00, stream_id=6>
    [  1.970] recv DATA frame <length=2681, flags=0x01, stream_id=8>
              ; END_STREAM
    [  1.971] send WINDOW_UPDATE frame <length=4, flags=0x00, stream_id=2>
              (window_size_increment=33855)
    [  1.971] recv DATA frame <length=10072, flags=0x00, stream_id=2>
    [  2.172] recv DATA frame <length=2829, flags=0x00, stream_id=6>
    [  2.172] recv DATA frame <length=4248, flags=0x00, stream_id=2>
    [  2.173] recv DATA frame <length=4248, flags=0x00, stream_id=6>
    [  2.173] send WINDOW_UPDATE frame <length=4, flags=0x00, stream_id=0>
              (window_size_increment=34002)
    [  2.173] recv DATA frame <length=4248, flags=0x00, stream_id=2>
    [  2.577] recv DATA frame <length=4248, flags=0x00, stream_id=6>
    [  2.578] recv DATA frame <length=2829, flags=0x00, stream_id=2>
    [  2.579] recv DATA frame <length=12762, flags=0x00, stream_id=6>
    [  2.777] recv DATA frame <length=2829, flags=0x00, stream_id=2>
    [  2.777] send WINDOW_UPDATE frame <length=4, flags=0x00, stream_id=6>
              (window_size_increment=33241)
    [  2.778] recv DATA frame <length=2829, flags=0x00, stream_id=6>
    [  3.177] recv DATA frame <length=8505, flags=0x00, stream_id=2>
    [  3.177] recv DATA frame <length=5667, flags=0x00, stream_id=6>
    [  3.177] send WINDOW_UPDATE frame <length=4, flags=0x00, stream_id=0>
              (window_size_increment=33993)
    [  3.177] recv DATA frame <length=2829, flags=0x00, stream_id=2>
    [  3.177] recv DATA frame <length=2829, flags=0x00, stream_id=6>
    [  3.378] recv DATA frame <length=2829, flags=0x00, stream_id=2>
    [  3.579] recv DATA frame <length=11343, flags=0x00, stream_id=6>
    [  3.580] send WINDOW_UPDATE frame <length=4, flags=0x00, stream_id=0>
              (window_size_increment=34002)
    [  3.580] send WINDOW_UPDATE frame <length=4, flags=0x00, stream_id=2>
              (window_size_increment=33984)
    [  3.583] recv DATA frame <length=7086, flags=0x00, stream_id=2>
    [  3.779] recv DATA frame <length=2829, flags=0x00, stream_id=6>
    [  4.186] recv DATA frame <length=7086, flags=0x00, stream_id=2>
    [  4.186] recv DATA frame <length=2829, flags=0x00, stream_id=6>
    [  4.186] recv DATA frame <length=2829, flags=0x00, stream_id=2>
    [  4.395] recv DATA frame <length=2829, flags=0x00, stream_id=6>
    [  4.396] recv DATA frame <length=2829, flags=0x00, stream_id=2>
    [  4.602] recv DATA frame <length=5667, flags=0x00, stream_id=6>
    [  4.602] send WINDOW_UPDATE frame <length=4, flags=0x00, stream_id=6>
              (window_size_increment=33993)
    [  4.602] recv DATA frame <length=2829, flags=0x00, stream_id=2>
    [  4.602] send WINDOW_UPDATE frame <length=4, flags=0x00, stream_id=0>
              (window_size_increment=33975)
    [  4.808] recv DATA frame <length=4248, flags=0x00, stream_id=6>
    [  4.809] recv DATA frame <length=6379, flags=0x01, stream_id=2>
              ; END_STREAM
    [  5.010] recv DATA frame <length=3536, flags=0x00, stream_id=6>
    [  5.420] recv DATA frame <length=8505, flags=0x00, stream_id=6>
    [  5.420] recv DATA frame <length=5667, flags=0x00, stream_id=6>
    [  5.628] recv DATA frame <length=4248, flags=0x00, stream_id=6>
    [  5.842] recv DATA frame <length=4248, flags=0x00, stream_id=6>
    [  5.842] recv DATA frame <length=2829, flags=0x00, stream_id=6>
    [  5.842] send WINDOW_UPDATE frame <length=4, flags=0x00, stream_id=0>
              (window_size_increment=34002)
    [  5.842] send WINDOW_UPDATE frame <length=4, flags=0x00, stream_id=6>
              (window_size_increment=33281)
    [  6.057] recv DATA frame <length=4248, flags=0x00, stream_id=6>
    [  6.273] recv DATA frame <length=8505, flags=0x00, stream_id=6>
    [  6.490] recv DATA frame <length=9924, flags=0x00, stream_id=6>
    [  6.490] recv DATA frame <length=4248, flags=0x00, stream_id=6>
    [  6.706] recv DATA frame <length=4248, flags=0x00, stream_id=6>
    [  6.706] send WINDOW_UPDATE frame <length=4, flags=0x00, stream_id=0>
              (window_size_increment=34002)
    [  6.706] send WINDOW_UPDATE frame <length=4, flags=0x00, stream_id=6>
              (window_size_increment=34002)
    [  6.924] recv DATA frame <length=8505, flags=0x00, stream_id=6>
    [  7.141] recv DATA frame <length=8505, flags=0x00, stream_id=6>
    [  7.361] recv DATA frame <length=8505, flags=0x00, stream_id=6>
    [  7.361] send WINDOW_UPDATE frame <length=4, flags=0x00, stream_id=0>
              (window_size_increment=34020)
    [  7.574] recv DATA frame <length=9924, flags=0x00, stream_id=6>
    [  7.574] send WINDOW_UPDATE frame <length=4, flags=0x00, stream_id=6>
              (window_size_increment=34029)
    [  7.787] recv DATA frame <length=9924, flags=0x00, stream_id=6>
    [  7.787] recv DATA frame <length=2829, flags=0x00, stream_id=6>
    [  7.998] recv DATA frame <length=7086, flags=0x00, stream_id=6>
    [  8.210] recv DATA frame <length=9924, flags=0x00, stream_id=6>
    [  8.210] send WINDOW_UPDATE frame <length=4, flags=0x00, stream_id=0>
              (window_size_increment=34011)
    [  8.210] send WINDOW_UPDATE frame <length=4, flags=0x00, stream_id=6>
              (window_size_increment=34011)
    [  8.425] recv DATA frame <length=11343, flags=0x00, stream_id=6>
    [  8.426] recv DATA frame <length=2829, flags=0x00, stream_id=6>
    [  8.426] recv DATA frame <length=4053, flags=0x01, stream_id=6>
              ; END_STREAM
    [  8.631] recv DATA frame <length=4443, flags=0x00, stream_id=13>
    [  8.633] recv DATA frame <length=1290, flags=0x01, stream_id=13>
              ; END_STREAM
    [  8.633] send GOAWAY frame <length=8, flags=0x00, stream_id=0>
              (last_stream_id=10, error_code=NO_ERROR(0x00), opaque_data(0)=[])
    

    当然,我们也可以使用 grep 搜索出来 server push 的相关 stream:

    nghttp -nv 'https://h2o.examp1e.net' | grep 'PUSH_PROMISE'
    [  1.582] recv PUSH_PROMISE frame <length=59, flags=0x04, stream_id=13>
    [  1.582] recv PUSH_PROMISE frame <length=33, flags=0x04, stream_id=13>
    [  1.582] recv PUSH_PROMISE frame <length=35, flags=0x04, stream_id=13>
    [  1.582] recv PUSH_PROMISE frame <length=24, flags=0x04, stream_id=13>
    [  1.582] recv PUSH_PROMISE frame <length=28, flags=0x04, stream_id=13>
    

    浏览器支持

    主流浏览器都只支持 HTTP/2 Over TLS

    运行方式为:在命令行里切换到上面的JS的文件目录,然后输入 node JS文件名

    使用 NodeJS 搭建 HTTP/2 服务器

    在大前端的时代背景下,客户端开发不会点 JavaScript 都快混不下去了,笔者前段时间在我司前端轮岗了两周,再加上之前也写过 ReactNative,但还是感觉前端变化之快领人咋舌,革命尚未结束,同志仍需努力啊。

    咱们直接上代码:

    var http2 = require('http2');// http2
    var url=require('url'); // https://www.npmjs.com/package/url
    var fs=require('fs'); // https://www.npmjs.com/package/fs
    var mine=require('mine');
    var path=require('path'); // 路径
    
    var server = http2.createServer({
      key: fs.readFileSync('./localhost.key'),
      cert: fs.readFileSync('./localhost.crt')
    }, function(request, response) {
    
        // var pathname = url.parse(request.url).pathname;
        var realPath = './push.json' ;//path.join(pathname,"push.json");    //这里设置自己的文件路径,这是该次response返回的内容;
    
        var pushArray = [];
        var ext = path.extname(realPath);
        ext = ext ? ext.slice(1) : 'unknown';
        var contentType = mine[ext] || "text/plain";
    
        if (fs.existsSync(realPath)) {
    
            console.log('success')
            response.writeHead(200, {
                'Content-Type': contentType
            });
    
            response.write(fs.readFileSync(realPath,'binary'));
    
            // 注意 push 路径必须是绝对路径,这是该次 server push 返回的内容
            var pushItem = response.push('/Users/f.li/Desktop/http2-nodeServer/newpush.json', {
                    response: {
                      'content-type': contentType
                    }    
            });
            pushItem.end(fs.readFileSync('/Users/f.li/Desktop/http2-nodeServer/newpush.json','binary'),()=>{
              console.log('newpush end')
            });
    
            response.end();
    
        } else {
          response.writeHead(404, {
              'Content-Type': 'text/plain'
          });
    
          response.write("This request URL "   realPath   " was not found on this server.");
          response.end();
        }
    
    });
    
    server.listen(3000, function() {
      console.log('listen on 3000');
    });
    

    这里需要注意几点:

    • 创建http2的nodejs服务必须时基于https的,因为现在主流的浏览器都要支持SSL/TLS的http2,证书和私钥可以自己通过OPENSSL生成。
    • node http2的相关api和正常的node httpserver相同,可以直接使用。

    使用 nghttp 测试一下我们的代码有没有进行 server push:

    ~ nghttp -nv 'https://localhost:3000/'
    [  0.007] Connected
    The negotiated protocol: h2
    [  0.029] recv SETTINGS frame <length=0, flags=0x00, stream_id=0>
              (niv=0)
    [  0.029] send SETTINGS frame <length=12, flags=0x00, stream_id=0>
              (niv=2)
              [SETTINGS_MAX_CONCURRENT_STREAMS(0x03):100]
              [SETTINGS_INITIAL_WINDOW_SIZE(0x04):65535]
    [  0.029] send SETTINGS frame <length=0, flags=0x01, stream_id=0>
              ; ACK
              (niv=0)
    [  0.029] send PRIORITY frame <length=5, flags=0x00, stream_id=3>
              (dep_stream_id=0, weight=201, exclusive=0)
    [  0.029] send PRIORITY frame <length=5, flags=0x00, stream_id=5>
              (dep_stream_id=0, weight=101, exclusive=0)
    [  0.029] send PRIORITY frame <length=5, flags=0x00, stream_id=7>
              (dep_stream_id=0, weight=1, exclusive=0)
    [  0.029] send PRIORITY frame <length=5, flags=0x00, stream_id=9>
              (dep_stream_id=7, weight=1, exclusive=0)
    [  0.029] send PRIORITY frame <length=5, flags=0x00, stream_id=11>
              (dep_stream_id=3, weight=1, exclusive=0)
    [  0.029] send HEADERS frame <length=38, flags=0x25, stream_id=13>
              ; END_STREAM | END_HEADERS | PRIORITY
              (padlen=0, dep_stream_id=11, weight=16, exclusive=0)
              ; Open new stream
              :method: GET
              :path: /
              :scheme: https
              :authority: localhost:3000
              accept: */*
              accept-encoding: gzip, deflate
              user-agent: nghttp2/1.21.1
    [  0.043] recv SETTINGS frame <length=0, flags=0x01, stream_id=0>
              ; ACK
              (niv=0)
    [  0.049] recv (stream_id=13) :status: 200
    [  0.049] recv (stream_id=13) content-type: text/plain
    [  0.049] recv (stream_id=13) date: Tue, 11 Apr 2017 08:34:46 GMT
    [  0.049] recv HEADERS frame <length=34, flags=0x04, stream_id=13>
              ; END_HEADERS
              (padlen=0)
              ; First response header
    [  0.049] recv DATA frame <length=35, flags=0x00, stream_id=13>
    [  0.049] recv (stream_id=13) :method: GET
    [  0.049] recv (stream_id=13) :scheme: https
    [  0.050] recv (stream_id=13) :authority: localhost:3000
    [  0.050] recv (stream_id=13) :path: /Users/f.li/Desktop/http2-nodeServer/newpush.json
    [  0.050] recv PUSH_PROMISE frame <length=56, flags=0x04, stream_id=13>
              ; END_HEADERS
              (padlen=0, promised_stream_id=2)
    [  0.050] recv DATA frame <length=0, flags=0x01, stream_id=13>
              ; END_STREAM
    [  0.050] recv (stream_id=2) :status: 200
    [  0.050] recv (stream_id=2) date: Tue, 11 Apr 2017 08:34:46 GMT
    [  0.050] recv HEADERS frame <length=2, flags=0x04, stream_id=2>
              ; END_HEADERS
              (padlen=0)
              ; First push response header
    [  0.050] recv DATA frame <length=21, flags=0x00, stream_id=2>
    [  0.050] recv DATA frame <length=0, flags=0x01, stream_id=2>
              ; END_STREAM
    [  0.050] send GOAWAY frame <length=8, flags=0x00, stream_id=0>
              (last_stream_id=2, error_code=NO_ERROR(0x00), opaque_data(0)=[])
    

    看到了 PUSH_PROMISE 的帧,说明进行了 server push。

    同样也可以使用chrome查看 server push,如下图所示:

    新葡亰496net 13

    chrome 查看 http2 server push

    服务端介绍基本完毕。下面我们来介绍一些 iOS 客户端对 Server Push 的使用。

    node中启用http2

    node中可以用spdy模块来启动应用,spdy的api,与https是一致的且主流浏览器只支持HTTP/2 Over TLS,需要配置 私钥和证书,本地自签名服务器配置可参考引用6,7

    JavaScript

    const express = require('express'); const fs = require('fs'); const http2 = require('spdy'); const path = require('path'); const options = { key: fs.readFileSync('./keys/privatekey.pem'), cert: fs.readFileSync('./keys/certificate.pem') }; const app = new express(); http2 .createServer(options, app) .listen(8080, ()=>{ console.log(`Server is listening on . You can open the URL in the browser.`) } ) app.use("/",(req,res)=>{ res.send("hello http2!"); })

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    const express = require('express');
    const fs =  require('fs');
    const http2 = require('spdy');
    const path = require('path');
    const options = {
        key: fs.readFileSync('./keys/privatekey.pem'),
        cert: fs.readFileSync('./keys/certificate.pem')
    };
    const app = new express();
    http2
      .createServer(options, app)
      .listen(8080, ()=>{
        console.log(`Server is listening on https://localhost:8080.
         You can open the URL in the browser.`)
      }
    )
    app.use("/",(req,res)=>{
        
      res.send("hello http2!");
    })

    如上,对于已存在的项目只要修改几行代码就可以使用http2.0了。

    请求头和响应头:

    说明:新版的Chrome,对不安全的证书(如本地的自签名服务)会降级到http1.1,firefox不会出现此问题。

    启动server push

    JavaScript

    app.get("/",(req,res)=>{ var stream = res.push('/app.js', { //服务器推送 status: 200, // optional method: 'GET', // optional request: { accept: '*/*' }, response: { 'content-type': 'application/javascript' } }) stream.on('error', function() { }) stream.end('console.log("http2 push stream, by Lucien ");') res.send(`hello http2! <script src="/app.js"></script>`);//express 并没有host static ,这个app.js 来自push })

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    app.get("/",(req,res)=>{
        var stream = res.push('/app.js', {   //服务器推送
        status: 200, // optional
        method: 'GET', // optional
        request: {
          accept: '*/*'
        },
        response: {
          'content-type': 'application/javascript'
        }
      })
      stream.on('error', function() {
      })
      stream.end('console.log("http2 push stream, by Lucien ");')
     
      res.send(`hello http2!
        <script src="/app.js"></script>`);//express 并没有host static ,这个app.js 来自push
    })

    源码在github

    响应

    浏览器内输入

    iOS 使用 HTTP/2 Server Push

    Apple 在这方面做的很好,基本实现了客户端无感调用http/2 server push。但是笔者查看了些许资料,现在只有iOS 10 支持 http/2。

    直接上代码吧:

    #import "ViewController.h"
    
    @interface ViewController ()<NSURLSessionDelegate>
    
    @property(nonatomic,strong)NSURLSession *session;
    
    @end
    
    @implementation ViewController
    
    - (void)viewDidLoad
    {
        [super viewDidLoad];
        // Do any additional setup after loading the view, typically from a nib.
    
    }
    #pragma mark - Touch
    - (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
    {
        [self urlSession];
    }
    #pragma mark - 发送请求
    - (void)urlSession
    {
        NSURL *url = [NSURL URLWithString:@"https://localhost:3000"];
    
        //发送HTTPS请求是需要对网络会话设置代理的
        _session = [NSURLSession sessionWithConfiguration:[NSURLSessionConfiguration defaultSessionConfiguration] delegate:self delegateQueue:[NSOperationQueue mainQueue]];
    
        NSURLSessionDataTask *dataTask = [_session dataTaskWithURL:url completionHandler:^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error) {
            NSLog(@"%@",[[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding]);
            // 收到该次请求后,立即请求下次的内容
            [self urlSessionPush];
    
        }];
    
        [dataTask resume];
    }
    
    - (void)urlSessionPush
    {
        NSURL *url = [NSURL URLWithString:@"https://localhost:3000/Users/f.li/Desktop/http2-nodeServer/newpush.json"];
        NSURLSessionDataTask *dataTask = [_session dataTaskWithURL:url completionHandler:^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error) {
            NSLog(@"%@",[[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding]);
        }];
        [dataTask resume];
    }
    
    #pragma mark - URLSession Delegate
    - (void)URLSession:(NSURLSession *)session didReceiveChallenge:(NSURLAuthenticationChallenge *)challenge completionHandler:(void (^)(NSURLSessionAuthChallengeDisposition, NSURLCredential * _Nullable))completionHandler
    {
        // 这里还要设置下 plist 中设置 ATS
        if (![challenge.protectionSpace.authenticationMethod isEqualToString:@"NSURLAuthenticationMethodServerTrust"])
        {
            return;
        }
        NSURLCredential *credential = [[NSURLCredential alloc] initWithTrust:challenge.protectionSpace.serverTrust];
        completionHandler(NSURLSessionAuthChallengeUseCredential,credential);
    }
    
    
    - (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didFinishCollectingMetrics:(NSURLSessionTaskMetrics *)metrics
    {
        NSArray *fetchTypes = @[ @"Unknown", @"Network Load", @"Server Push", @"Local Cache"];
    
        for(NSURLSessionTaskTransactionMetrics *transactionMetrics in [metrics transactionMetrics])
        {
    
            NSLog(@"protocol[%@] reuse[%d] fetch:%@ - %@", [transactionMetrics networkProtocolName], [transactionMetrics isReusedConnection], fetchTypes[[transactionMetrics resourceFetchType]], [[transactionMetrics request] URL]);
    
            if([transactionMetrics resourceFetchType] == NSURLSessionTaskMetricsResourceFetchTypeServerPush)
            {
                NSLog(@"Asset was server pushed");
            }
        }
    }
    
    - (void)didReceiveMemoryWarning
    {
        [super didReceiveMemoryWarning];
        // Dispose of any resources that can be recreated.
    }
    
    @end
    

    分别看下服务端和客户端的Log:
    客户端:

    Http2ServerPush[2525:274943] protocol[h2] reuse[0] fetch:Network Load - https://localhost:3000/
    Http2ServerPush[2525:274943] {"message":" http2.0 server is ok"}
    Http2ServerPush[2525:274943] protocol[h2] reuse[1] fetch:Server Push - https://localhost:3000/Users/f.li/Desktop/http2-nodeServer/newpush.json
    Http2ServerPush[2525:274943] Asset was server pushed
    Http2ServerPush[2525:274943] {"message":"newPush"}
    

    服务端:

    http2-nodeServer npm start
    
    > http2-nodeServer@1.0.0 start /Users/f.li/Desktop/http2-nodeServer
    > node index.js
    
    listen on 3000
    success
    newpush end
    

    看来确实是客户端发出了两次请求,但是服务端只响应了一次(该次响应 server push)

    抓包分析

    可以用chrome 内部自带的工具(chrome://net-internals/)查看http2流量,但这个包信息量比较少,结构不如我们熟悉的Fiddler查看http2流量,但这个包信息量比较少,结构不如我们熟悉的Fiddler) or Wireshark清晰。

    Fiddler是直接作为中间代理,可以作为客户端直接与服务端通讯,可以像浏览器那样直接解密https,直接看到https报文,
    但是由于受限于.NET Framework暂不支持Http2.

    用wireshark直接抓包 https:443端口的流量是这样的:

    数据被加密了,协议细节完全看不到。
    这里介绍了一种方法获取私钥解包。
    抓包https包时要把代理关了,不然私钥不是同一个,wireshark不能解包(被这个坑了两小时T T)。

    一个包内有多个不同的Steam ID

    追踪解密后TCP流可以看到,由于多路复用,各个不同的请求交替传输不同的帧,所以流数据是乱的。但在同一帧内数据还是正常的。

    --补上上面代码里缺少的两个模块

    本文相关Demo

    • Github:lijianfeigeek

    最后

    最后,HTTP2有更高的传输速度,更少的资源占用,可以去除各种性能优化tricks(如css sprite,inline-image.)
    转向WEB开发的美好未来T.T

    mime.js

    参考文献

    • HTTP2 Server Push的研究 | AlloyTeam
    • 使用 nghttp2 调试 HTTP/2 流量 | JerryQu 的小站
    • objective c - HTTP/2 Server Push in iOS 10 - Stack Overflow
    • 使用NSURLSession或者AFN发送HTTPS请求 - 简书

    参考资料

    1. Turn-on HTTP/2 today!
    2. Hypertext Transfer Protocol Version 2 (HTTP/2)
    3. npm spdy
    4. npm spdy push
    5. How to create a self-signed SSL Certificate
    6. HPACK: Header Compression for HTTP/2
    7. 用Node.js创建自签名的HTTPS服务器

      1 赞 收藏 评论

    新葡亰496net 14

    复制代码 代码如下:

    exports.types = {

      "css": "text/css",

      "gif": "image/gif",

      "html": "text/html",

      "ico": "image/x-icon",

      "jpeg": "image/jpeg",

      "jpg": "image/jpeg",

      "js": "text/javascript",

      "json": "application/json",

      "pdf": "application/pdf",

      "png": "image/png",

      "svg": "image/svg xml",

      "swf": "application/x-shockwave-flash",

      "tiff": "image/tiff",

      "txt": "text/plain",

      "wav": "audio/x-wav",

      "wma": "audio/x-ms-wma",

      "wmv": "video/x-ms-wmv",

      "xml": "text/xml"
    };

    config.js

    复制代码 代码如下:

    exports.Expires = {
        fileMatch: /^(gif|png|jpg|js|css)$/ig,
        maxAge: 60 * 60 * 24 * 365
    };

    exports.Compress = {
        match: /css|js|html/ig
    };

    exports.Welcome = {
        file: "index.html"
    };

    您可能感兴趣的文章:

    • 实战node静态文件服务器的示例代码
    • Node.js静态服务器的实现方法
    • nodejs构建本地web测试服务器 如何解决访问静态资源问题
    • 用Nodejs搭建服务器访问html、css、JS等静态资源文件
    • 在windows上用nodejs搭建静态文件服务器的简单方法
    • Node.js静态文件服务器改进版
    • 使用nodejs、Python写的一个简易HTTP静态文件服务器
    • 手写Node静态资源服务器的实现方法

    本文由新葡亰496net发布于新葡亰官网,转载请注明出处:新葡亰496net:特色和抓包剖析,golang中使用echo框

    关键词:

上一篇:2首部裁减,完全剖判

下一篇:没有了