2

http/2 协议刚刚发布不久,http1.1的服务器和客户端依然大量存在,新老协议必定长期共存一段时间。这样,浏览器和服务器就需要协商使用何种协议进行通讯。

主流的方法是使用ALPN或者NPN来做协商。

Next Protocol Negotiation (NPN)是一个使SPDY在TLS服务器上对使用何种应用层协议进行协商的协议。IETF(h2的标准化组织)拿到这个,肯定要改改,然后盖个章,把它变成标准。名字也改了叫ALPN(Application Layer Protocol Negotiation)。

区别是有的。就在于谁持有会话协议的决定权。ALPN是由客户端给服务器发送一个协议清单,由服务器来最终选择一个。而NPN则正好相反。

Node 已经在tls模块内实现了NPN支持。只要创建tls服务器(createServer),在options参数内传递服务器支持的协议清单NPNProtocols,在客户端连接(connect)传递NPNProtocols,这样建立连接后,就可以在socket.npnProtocol内得到协商的结果。

请看mocha测试用例:

describe('tls', function() {
    it('npn', function() {      
     var fs = require('fs');
        var path = require('path');
        var tls = require('tls');
        tls.createServer({
          key: fs.readFileSync("example/localhost.key"),// 私有键
          cert: fs.readFileSync("example/localhost.crt"),// 证书
          NPNProtocols: ['h2', 'http 1.1','http 1.0'] // 服务器支持协议清单
        }, function(socket) {
          console.log("s1:"+socket.npnProtocol);// 协商结果
        }).listen(1111);
        //client
      process.env.NODE_TLS_REJECT_UNAUTHORIZED = "0";
      tls.connect({ port: 1111 }, function() {
      });
      tls.connect({ port: 1111 ,NPNProtocols: ['h2'] }, function() {     
      });
      tls.connect({ port: 1111, NPNProtocols: ['http 1.1'] }, function() {
      });      
      tls.connect({ port: 1111, NPNProtocols: ['http 1.0'] }, function() {
      });
    })
  })

输出结果

  my.js
    scenario
      tls
        √ npn (279ms)
s1:http/1.1
s1:h2
s1:http 1.1
s1:http 1.0

程序员的代码常常气死写文字的人——因为一堆艰涩文字,变成代码常常简单无比。

理论上来说,http/2 可以架构在tls(加密通道)上,也可以架构在tcp(平文本)上。协议文本也确实没有限定或者强制使用tls信道。但是,h2的前身是spdy,而spdy是在tls之上的;spdy的主人家google的浏览器,chrome也只支持tls;另外一家主流浏览器firefox也跟进。这就让架构于tls之上就成为http/2的事实上的标准。所有协商协议也都支持NPN(tls的一个扩展)。

两家浏览器厂商的做法,其实并非强梁,而是基于现实考量:
1. 大量现存的代理、中介软件,都假设80端口上跑的是http1.1,并且基于这个假设,对流经它们的流量做出修改。如果h2继续使用80,很可能和这些修改发生冲突。使用tls(默认为443端口)就避开了这个冲突的可能性
2. 加密化信道(相比http1.x)对用户隐私是更好的保护

通过平文本升级协议到h2,依然可能。采用的是现存的http1.1的升级机制。

GET /page HTTP/1.1
Host: server.example.com
Connection: Upgrade, HTTP2-Settings
Upgrade: h2c 
HTTP2-Settings: (SETTINGS payload) 

HTTP/1.1 200 OK 
Content-length: 243
Content-type: text/html

(... HTTP/1.1 response ...)

          (or)

HTTP/1.1 101 Switching Protocols 
Connection: Upgrade
Upgrade: h2c

(... HTTP/2 response ...)

由 Upgrade指定升级目标协议, HTTP2-Settings传递base64后的settings。如果服务器为1.1的,那么返回1.1的响应。否则,就发101 Switching Protocols ,随后升级为h2。

因为这个做法比NPN要做一个网络往返,这个做法(101 Switching Protocols )被很多实现忽略。比如,node-http2就没有实现(请脑补标准机构的脸色:)。在程序员友好,客户友好的柔情面纱下面,遇到核心的性能问题,依然是一片丛林景象。

尽管node-http2代码内,把ALPN的协议协商也有,并且堂而皇之的也和NPN一样:

    options.ALPNProtocols = supportedProtocols;
    options.NPNProtocols = supportedProtocols;
    ...
    this._server = https.createServer(options);

但是经过测试,node还没有支持ALPN,有测试用例为证:

describe('tls', function() {
    it('alpn', function() {      
     var fs = require('fs');
        var path = require('path');
        var tls = require('tls');
        tls.createServer({
          key: fs.readFileSync("example/localhost.key"),
          cert: fs.readFileSync("example/localhost.crt"),
          ALPNProtocols: ['h2', 'http 1.1','http 1.0']
        }, function(socket) {
          console.log("s1:"+socket.npnProtocol);
        }).listen(1111);
        //client
      process.env.NODE_TLS_REJECT_UNAUTHORIZED = "0";
      tls.connect({ port: 1111 }, function() {          
      });
      tls.connect({ port: 1111 ,ALPNProtocols: ['h2'] }, function() {

      });
      tls.connect({ port: 1111, ALPNProtocols: ['http 1.1'] }, function() {

      });      
      tls.connect({ port: 1111, ALPNProtocols: ['http 1.0'] }, function() {

      });
    })
  })

根本就没有协商,怎么样都是http/1.1 !

  my.js
    scenario
      tls
        √ alpn (307ms)
s1:http/1.1
s1:http/1.1
s1:http/1.1
s1:http/1.1

关于Node对alpn的支持,文档也没有提及,但是在issue内有提到,它们正在等待openssl实现并且稳定。稳!定!openssl这几年出的糗还少吗,什么时候能弄稳定呢。


Reco
4.6k 声望541 粉丝

敢作敢为