Aldebaran

人生最棒的感觉,就是你做到别人说你做不到的事。

0%

Nginx作为WebSocket代理

三田寺円

什么是WebSocket

WebSocket协议相比较于HTTP协议成功握手后可以多次进行通讯,直到连接被关闭。但是WebSocket中的握手和HTTP中的握手兼容,它使用HTTP中的Upgrade协议头将连接从HTTP升级到WebSocket。这使得WebSocket程序可以更容易的使用现已存在的基础设施。

WebSocket工作在HTTP的80和443端口并使用前缀ws://或者wss://进行协议标注,在建立连接时使用HTTP/1.1的101状态码进行协议切换,当前标准不支持两个客户端之间不借助HTTP直接建立Websocket连接。

为什么是 Websocket

由于基本的 HTTP 是半双工的请求-响应的模式,客户端发起一个请求后服务端才能返回一个响应,在设计之初是没有考虑服务端主动推送数据、没有考虑数据实时更新的情况的;在 Websocket 出现之前,为了保持数据的实时更新和服务端主动推送数据给客户端,最早也是最简单粗暴的实现是让客户端不断发送请求(即轮询),后来出现了 Comet 模型和 SSE(Server Sent Event),Comet 是让服务器在没有接到浏览器显式请求的情况下,实现推送数据的一类技术的总称,是一类模式而不是一个标准,其原理是大多都是服务端延迟完成 HTTP 的响应,又可以分为长轮询和流两种实现方式。

  • Polling(轮询):客户端定期发起请求,无论请求的内容是否存在,服务器马上返回响应。对于出现时间不可预测的内容,轮询效率很低。
  • Long-Polling(长轮询):包括插入 <scrpit> 标签实现长轮询和 XMLHttpRequest(XHR)客户端发送请求,服务器保留连接和请求持续一段时间,如果需要的内容在预定时间内出现则返回,如需要的内容没有出现,服务器将关闭请求。但是当消息量较大时,长轮询相比轮询没有性能提升,反而可能因为需要维护长连接造成性能比轮询更差。
  • Streaming(流):包括隐藏 iframe 和 XHR,客户端发送请求,服务器保持连接,并持续发回响应,如 Google talk 使用了嵌入隐藏 iframe,而 Gmail 使用的是 XHR。

但是这些技术都是基于 HTTP 的,而 HTTP/1.1 的标准 RFC 7230 并不建议客户端打开过多的连接,基于 HTTP 的传输技术不仅实现繁杂、开销较大(比如频繁的 TCP 握手和 HTTP header 传递),而且总是会受到 HTTP 单向传输和连接数的限制。

一言蔽之,HTTP 不是为实时全双工的目的而设计的协议,在 HTTP 的基础上模拟全双工制约太大,因此要提高性能一个独立于 HTTP 的协议是必要的,使用独立的协议可以完全摆脱这些限制。

HTTP & Websocket

Websocket 出现的一个最大的原因就是 HTTP 是半双工的,Websocket 解决了服务端主动向客户推送数据的难题,但是 HTTP/2 出现了,联想到之前 HTTP/2 的 server push 特性,那么有了 HTTP/2 之后是否还需要 Websocket 这样基于 TCP 的独立双工协议,InfoQ 和 Stackoverflow 的结论都是认为 HTTP/2 不能代替 Websocket 这样的推送技术,主要原因是 HTTP/2 的 Server push 实际上是通过服务端在主页响应之前,通过一个 PUSH PROMISE 告诉客户端有哪些比较重要资源需要预先加载,只能被浏览器执行用于加载文件;如果客户端没有请求,服务端并不能主动推流,实际上并没有改变 HTTP 半双工协议的性质。

Server pushes are only processed by the browser and do not pop up to the application code, meaning there is no API for the application to get notifications for those events.

详细讨论参见:HTTP/2 中 Server push 的讨论

此外,SSE 和 Websocket 理论存在建立在 HTTP/2 上的可能性,IETF 在这个方向有一个草案:WebSocket over HTTP/2.0,可惜这个草案一直没有进一步具体定义,也没有具体实现,基本处于停滞状态。

关于 HTTP 和 websocket 的选择,可以参考微软的这篇博客:When to use a HTTP call instead of a WebSocket (or HTTP 2.0),总结一下是:

HTTP WEBSOCKET
双工 半双工,请求-响应模型 全双工,双向发送消息
推送支持 不能实现服务端主动推送 有服务端推送
额外开销 每次请求存在一定开销 连接建立存在开销,后续开销很小
缓存 有缓存支持 不能实现缓存

这些情况应该使用 HTTP:

  • 客户端主动拉取消息的场景
  • 重缓存场景
  • 幂等操作、需要安全保证

这些情景应该使用 Websocket:

  • 实时性要求高
  • 无明显 C/S 结构的消息交换,如:无中心多人聊天(嗯 受 pandada 指正这个需求应该用 WebRTC)
  • 高频次、小体积消息传输

NGINX Websocket Example

这里有一个实例来展示NGINX作为WebSocket代理工作。这个例子使用ws,一个构建在Node.js上的WebSocket实现。

NGINX充当使用ws和Node.js的简单WebSocket应用程序的反向代理。

这些说明已经过Ubuntu 14.04测试。

  1. 如果您尚未安装Node.js和npm,请运行以下命令:

     $ cd /usr/local/src
     $ wget https://nodejs.org/dist/v8.11.1/node-v8.11.1-linux-x64.tar.xz
     $ tar xf node-v8.11.1-linux-x64.tar.xz
     $ mv /usr/local/srcnode-v8.11.1-linux-x64 /usr/local/node-v8.11.1-linux-x64
    
     $ ln -s /usr/local/node-v8.11.1-linux-x64/bin/npm /usr/local/bin/npm
     $ ln -s /usr/local/node-v8.11.1-linux-x64/bin/node /usr/local/bin/node
    
  2. 初始化项目工程

     $ mkdir /data/wwwroot/websocket && cd /data/wwwroot/websocket
     $ npm init
    
  3. ws是nodejs的WebSocket实现,我们借助它来搭建简单的WebSocket Echo Server。要安装ws,请运行以下命令:

     $ sudo npm install ws
    

    注意:如果您收到错误消息:“Error: failed to fetch from registry: ws”,请运行以下命令修复此问题:

     $ sudo npm config set registry http://registry.npmjs.org/
    

    然后再次运行 sudo npm install ws。

  4. 使用wscat做为客户端测试,安装wscat

     $ npm install wscat
    
  5. 我们需要创建一个程序来充当服务器。用这些内容创建一个名为server.js的文件.

     console.log("Server started");
     var Msg = '';
     var WebSocketServer = require('ws').Server
     , wss = new WebSocketServer({port: 8010});
     wss.on('connection', function(ws) {
             ws.on('message', function(message) {
             console.log('Received from client: %s', message);
             ws.send('Server received from client: ' + message);
         });
     });
    
  6. 运行server.js,如果出现Server started则正常:

     $ node server.js
    
  7. 配置nginx

    Nginx 在博客上给出了一个实践样例:NGINX as a WebSocket Proxy

    • Forward Proxy: 前向代理模式下,客户端将主动识别代理并使用 CONNECT 方法让服务器 向源站打开隧道避免此问题;

      CONNECT example.com:80 HTTP/1.1
      Host: example.com
      
    • Reverse Proxy: 反向代理模式下,由于客户端不能察觉代理的存在,nginx 从1.3.13版本之后使用了一个特殊的模式,当从源站接到 HTTP 101 时,nginx 将维持客户端和服务端之间的连接,因为 Upgrade 头不能传递到后端,需要在手动添加 header:

      location /chat/ {
          proxy_pass http://backend;
          proxy_http_version 1.1;
          proxy_set_header Upgrade $http_upgrade;
          proxy_set_header Connection "upgrade";
      }
      

      一个更加优雅的解决:使用 map 指令,map 是 ngx_http_map_module中的指令,可以将变量组合成为新的变量,下面的配置根据客户端传来的连接中是否带有 Upgrade 头来决定是否给源站传递 Connection 头:

      http {
          map $http_upgrade $connection_upgrade {
              default upgrade;
              '' close;
          }
      
          upstream websocket {
              server 192.168.100.10:8010;
          }
      
          server {
              listen 8020;
              location / {
                  proxy_pass http://websocket;
                  proxy_http_version 1.1;
                  proxy_set_header Upgrade $http_upgrade;
                  proxy_set_header Connection $connection_upgrade;
              }
          }
      }
      

      默认情况下,连接将会在无数据传输60秒后关闭,proxy_read_timeout 参数可以延长这个时间;或者源站通过定期发送 ping 帧以保持连接并确认连接是否还在使用。

  8. 测试 (安装好的命令在工程目录下面的node_modules)

     $ ./node_modules/wscat/bin/wscat wscat --connect ws://192.168.99.200:8020
    

参考文档:

NGINX as a WebSocket Proxy

配置Nginx反向代理WebSocket

浅谈 Websocket 和反向代理实践