浅谈 Websocket 和反向代理实践

目录

  1. 什么是 Websocket
  2. 为什么使用 Websocket
  3. 性能理论分析
  4. Websocket 服务端和反代实践
  5. 对反代性能进行测试
  6. Websocket 和 HTTP

什么是 Websocket

Websocket 是起初由 HTML5 定义的一个建立在单 TCP 连接上的全双工通信协议,后从 HTML5 规范独立并由 RFC 6455 标准化,但仍被习惯性地称为 HTML5 Websocket。

Websocket 工作在 HTTP 的80和443端口并使用前缀ws://或者wss://(with ssl) 进行协议标注,但是实际上这个协议和 HTTP 并没有什么关联性,参见 RFC 6455 Section 1.7 的说明,在建立时,使用 HTTP/1.1 101状态码进行协议切换,当前标准不支持两个客户端之间不借助 HTTP 直接建立 Websocket 连接,参见 StackOverflow


GET /chat HTTP/1.1
Host: server.example.com
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: x3JJHMbDL1EzLkh9GBhXDw==
Sec-WebSocket-Protocol: chat, superchat
Sec-WebSocket-Version: 13
Origin: http://example.com

服务端返回响应:


HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: HSmrc0sMlYUkAGmm5OPpG2HaGWk=
Sec-WebSocket-Protocol: chat

为什么是 Websocket

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

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

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

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

性能分析

先前提到,在 HTTP 下实现双向持续通信一大问题就是 HTTP 有 header,有时 header 比需要传输的消息本身还大,Websocket 刚被提出时,号称只有2字节的额外开销,能将 HTTP 的延迟降低到三分之一,一个很大的原因就是 Websocket 有很小的 overhead,payload 大小不同的时候的开销如下表:

Payload Client-to-server Server-to-client
\< 126 6 2
\< 64k 8 4
\< 2**63 12 8

此外,Websocket 允许传输二进制内容,减少了转码的开销。

关于性能开销的分析参考了 http://tavendo.com/blog/post/dissecting-websocket-overhead/

可以看到,大体的结论是,TCP 上的开销比 Websocket 本身的开销要大的多。

在 Websocket 和 Comet 的对比测试中,Websocket 针对大量用户请求很小的消息这样一个场景,比轮询极大的减少了非必需的网络传输,提高了带宽利用率:

nginx 官方也做过 nginx 对 Websocket 反代的性能测试:NGINX WebSocket Performance

搭建 Websocket 服务端和代理服务端

关于 Websocket 的服务端实现,W3C 给出了 Websocket API,但是到现在实现 Websocket 的通用服务端较少,考虑到 Websocket 这个东西更多的是工具而不是目的,一个纯粹的 Socket Server 实际上并没有什么实际作用,更多的是实现了 Websocket 封装的后端语言的库,JS 有 socket.io、Python 有 tornado 都实现了 Websocket 库。

Websocket 的实现方式是通过发送 Upgrade 头,但是 Upgrade 和 Connection 都是逐跳(hop-by-hop)的 header,即只在一跳内有效而不会被传递到源站,这个 header 在正常情况下经过代理的时候会被去掉,因此需要在反代服务器上做一点设置。

Nginx 在博客上给出了一个实践样例:Using 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;
        }
    
        server {
            ...
    
            location /socket/ {
                proxy_pass http://backend;
                proxy_http_version 1.1;
                proxy_set_header Upgrade $http_upgrade;
                    proxy_set_header Connection $connection_upgrade;
            }
        }
    }

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

使用 nginx 对 Websocket 进行反代

之前提到,Websocket 是工具性协议,而其开源实现大多是基于后端语言如 Python 和 nodejs,关于选用什么样的服务端和客户端,nginx 官网有过示例,在这里我们大体遵从这篇文章的示范进行测试。需要注意,由于 nodejs 在社区发展中遇到过分支间的斗争,版本号存在历史遗留的一个超大跨度问题,加上 nodejs 本身更新迭代很快,Debian8 默认源里的 nodejs 版本号尚在 0.x 不足以支持本次测试的需要,而主流 LTS 支持版本已经在4.x,我们使用 nodejs 官方目前建议的方法进行安装 4.x 版本的 nodejs:

curl -sL https://deb.nodesource.com/setup_4.x | sudo -E bash -
apt-get update && apt-get install -y nodejs

执行npm install -g ws wscat使用 npm 安装wswscat,其中,ws是 nodejs 的 Websocket 实现,我们借助他搭建简单的 Websocket echo server;wscat是一个可执行的 Websocket 客户端,我们用来调试 Websocket 服务是否正常。

完成安装后实现一个简单的服务端:

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

这个简单的服务端实现的是向客户端返回客户端发送是消息,执行node server.js运行这个简单的 echo 服务。

使用wscat --connect ws://127.0.0.1:8010,输入任意内容进行测试,得到相同返回则说明运行正常。再使用上面的 nginx 配置对 simple server 进行反代,端口开在8020。

常见的压力测试工具 Apache JMeter 使用插件后可以对 Websocket 进行测试,但是这个插件较老,而且 JMeter 本身使用比较麻烦。而类似 ab 的轻量级测试工具目前发现的有 thor 和 Artillery,这两工具都是 nodejs 写的,nginx.com 在测试中使用的是 thor。

Thor:

  • 可以定义并发度
  • 输出结果简洁明了
  • 针对 Websocket,没有 HTTP 功能

Artillery:

  • 可以自定义 payload、多后端带权重轮询
  • 不能定义并发度
  • 也可以测试 HTTP、支持 cookie

使用 Artillery 进行测试:artillery quick --duration 60 --rate [number] ws://127.0.0.1:8020thor --amount 10000 --concurrent [number] ws://127.0.0.1:8020,二者 在简单模式下发送的 payload 不相同,结果不能直接进比较。

由于 Websocket 原理,总请求数增加的时候会吃满一个网卡上的所有端口导致测试结束,而且测试中发现,传输速率较大时,前端 nginx 的 CPU 占有率较低,反而是后端 node 把一个 CPU 吃满。

主要原因还是源站过于简单,而由于 Websocket 的工具性质,也很难有代表性的性能评价标准,只能知道 nginx 在作为 Websocket 反代的时候能轻松抗住上万并发。

使用 haproxy 对 Websocket 进行反代

除了 nginx 之外,实际上还有 haproxy 也可以进行 Websocket 代理,haproxy 上这个功能出现的比 nginx 还要稍微早一点(haproxy 在2012年就有了 Websocket 反代支持,nginx 是在2013年初),且 haproxy 在两种模式下都支持 Websocket:TCP 模式下转发协议包,HTTP 模式下代替客户端和源站进行 Websocket 握手,本部分参考:Websocket load-balancing with haproxy-Blog.haproxy.com

haproxy 的 TCP mode 基本没有什么讨论的,HTTP mode 则有点像 nginx 的模式,通过 判断Sec-WebSocket-Protocol header,haproxy 可以转发到不同的后端。

defaults
  mode http
  log global
  option httplog
  option  http-server-close
  option  dontlognull
  option  redispatch
  option  contstats
  backlog 10000
  timeout client          25s
  timeout connect          5s
  timeout server          25s
  timeout tunnel        3600s
  timeout tarpit          60s
  option forwardfor

frontend ft_web
  bind *:8020 name http
  maxconn 10000
  default_backend bk_web

backend bk_web
  balance roundrobin
  server websrv1 127.0.0.1:8010 maxconn 10000

同样使用 Thor 和 Artillery 进行测试,nginx、haproxy 和直接连接性能并无明显区别,也可能是服务端写的很简单的缘故。

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 结构的消息交换,如:本地多人聊天
  • 高频次、小体积消息传输
  • *


本文链接:https://www.starduster.me/2016/08/23/brief-talk-of-websocket/
本站基于 Creactive Commons BY-NC-SA 4.0 License 允许并欢迎您在注明来源和非商业使用前提下自由地对本文进行复制、分享或基于本文进行创作。
请注意:受限于笔者水平,本站内容可能存在主观臆断或事实错误,文中信息也可能因时间推移而不再准确,在此提醒读者结合自身判断谨慎地采纳。

6 条评论


  1. 想请教楼主一个问题,http tunnel类型的代理,能不能支持websocket协议,一直没找到这样的资料

    回复

    1. http tunnel 不是一个标准的名词,不太清楚你指的是什么类型的业务,正向代理?

      回复

  2. 我写废话的能力真是越来越强了

    回复

      1. 因为是直接从MD编辑器里贴的,我昨天看到那个了

        回复

回复 stardust 取消回复

您的电子邮箱地址不会被公开。 必填项已用 * 标注

此站点使用Akismet来减少垃圾评论。了解我们如何处理您的评论数据