Pound:配置处理 WebSocket

Pound:配置处理 WebSocket

是否可以配置处理 WebSocket 请求?如果不行,反向代理的最佳替代方案是什么?我想使用 pound 或等效的轻量级反向代理。

答案1

Pound 似乎包含支持协议升级的代码,但我从未能够让它工作。论坛和 Pound 邮件列表中的许多人也没有做到这一点。

有一篇非常详细的帖子exratione.com描述了 SSL 背后的许多 websocket 负载平衡选项,包括 Pound(作者最终也放弃了)。这篇文章(发表于 2012 年初)的结论是,没有好的解决方案。

自从那篇文章发表以来,nginx 可能已添加 websocket 代理支持,所以值得一看。nginx 在配置方面更复杂一些,IIRC 在粘性会话管理方面有一些限制,但它是一个可靠、快速的支持 SSL 的反向代理。

如果您的 websocket 连接不需要 SSL,您可能需要尝试一个简单的 TCP 负载均衡器。有很多可供选择——HAProxy深受 Linux 用户的喜爱,但也存在简单、高质量的替代品,如, OpenBSD 的中继(或其FreeBSD 移植), ETC。

如果您只需要在单个后端服务器前面使用反向代理,并且不需要负载平衡,那么您可能只需使用 stunnel 来接收前端 HTTPS/WSS 连接并连接到内部后端。 以下是一些 stunnel 配置示例。或者,您也可以在,但你必须进行实验——我没有这样做过,也不能告诉你它是否会起作用。(如果你尝试过,请告诉我们你的结果!)

更新:

HAProxy 1.5.0 于 2014 年 6 月 19 日发布。此版本在连接两端均包含本机 SSL 支持,这意味着它现在是我“首选”的 WebSocket 代理解决方案。配置非常简单:

frontend http-in
    ...
    bind 192.0.2.1:80     # if you want
    bind 192.0.2.1:443 ssl crt /etc/ssl/yadda.pem
    use_backend ws if { hdr(Upgrade) -i WebSocket }

backend ws
    server node1 192.168.1.111:8000
    server node2 192.168.1.112:8000

或者,您也可以使用 ACL 通过主机名执行此操作:

frontend http-in
    ...
    acl is_ws hdr_end(host) -i ws.example.com
    use_backend ws if is_ws

答案2

@ghoti 的答案很有效,我可能会坚持按照建议使用 stunnel,但这个问题仍然困扰着我,所以我将扩展@JanDvorak 的评论,他声称已经做了一些实验,但没有进一步详细说明。

我使用了以下简单的 python websocket 服务器,它源自https://gist.github.com/jkp/3136208

import struct
import SocketServer
from base64 import b64encode
from hashlib import sha1
from mimetools import Message
from StringIO import StringIO

class WebSocketsHandler(SocketServer.StreamRequestHandler):
    magic = '258EAFA5-E914-47DA-95CA-C5AB0DC85B11'

    def setup(self):
        SocketServer.StreamRequestHandler.setup(self)
        print "connection established", self.client_address

    def handle(self):
        data = self.request.recv(1024).strip()
        headers = Message(StringIO(data.split('\r\n', 1)[1]))
        if headers.get("Upgrade", None) != "websocket":
            return
        print 'Handshaking...'
        key = headers['Sec-WebSocket-Key']
        digest = b64encode(sha1(key + self.magic).hexdigest().decode('hex'))
        response = 'HTTP/1.1 101 Switching Protocols\r\n'
        response += 'Upgrade: websocket\r\n'
        response += 'Connection: Upgrade\r\n'
        response += 'Sec-WebSocket-Accept: %s\r\n\r\n' % digest
        self.request.send(response)

        length = ord(self.rfile.read(2)[1]) & 127
        if length == 126:
            length = struct.unpack(">H", self.rfile.read(2))[0]
        elif length == 127:
            length = struct.unpack(">Q", self.rfile.read(8))[0]
        masks = [ord(byte) for byte in self.rfile.read(4)]
        decoded = ""
        for char in self.rfile.read(length):
            decoded += chr(ord(char) ^ masks[len(decoded) % 4])

        print decoded

        self.request.send(chr(129))
        length = len(decoded)
        if length <= 125:
            self.request.send(chr(length))
        elif length >= 126 and length <= 65535:
            self.request.send(126)
            self.request.send(struct.pack(">H", length))
        else:
            self.request.send(127)
            self.request.send(struct.pack(">Q", length))
        self.request.send(decoded)

        self.finish()

if __name__ == "__main__":
    server = SocketServer.TCPServer(
        ("localhost", 9000), WebSocketsHandler)
    try:
        server.serve_forever()
    except KeyboardInterrupt:
        print "Got ^C"
        server.server_close();
        print "bye!"

我将其与以下 html 结合起来,该 html 借鉴自http://www.websocket.org/echo.html

<!DOCTYPE html>
<meta charset="utf-8" />
<title>WebSocket Test</title>
<script language="javascript" type="text/javascript">
  var wsUri = "ws://localhost:9000/";
  var output;
  function init() {
    output = document.getElementById("output");
    testWebSocket();
  }
  function testWebSocket() {
    websocket = new WebSocket(wsUri);
    websocket.onopen = function(evt) { onOpen(evt) };
    websocket.onclose = function(evt) { onClose(evt) };
    websocket.onmessage = function(evt) { onMessage(evt) };
    websocket.onerror = function(evt) { onError(evt) };
  }
  function onOpen(evt) {
    writeToScreen("CONNECTED");
    doSend("WebSocket rocks");
  }
  function onClose(evt) {
    writeToScreen("DISCONNECTED");
  }
  function onMessage(evt) {
    writeToScreen('<span style="color: blue;">RESPONSE: ' + evt.data+'</span>');
    websocket.close();
  }
  function onError(evt) {
    writeToScreen('<span style="color: red;">ERROR:</span> ' + evt.data);
  }
  function doSend(message) {
    writeToScreen("SENT: " + message); 
    websocket.send(message);
  }
  function writeToScreen(message) {
    var pre = document.createElement("p");
    pre.style.wordWrap = "break-word";
    pre.innerHTML = message;
    output.appendChild(pre);
  }
  window.addEventListener("load", init, false);
</script>
<h2>WebSocket Test</h2>
<div id="output"></div>
</html>

这个效果很好,因此我将磅号放在中间,并输入以下配置条目:

ListenHTTP
    Address 127.0.0.1
    Port    9999
    Service
            BackEnd
                    Address 127.0.0.1
                    Port    9000
            End
    End
End

并将 html 中的端口从 9000 更改为 9999。更改后它停止工作。

使用 wireshark 分析流量后,我发现 HTTP 101 切换协议请求被正确转发。但随后的第一个 websocket 数据包从未被 pound 转发。pythonprint服务器脚本的输出证实了这一点,该脚本从未收到WebSocket rocks中间带有 pound 的消息。

每当 pound 收到 WebSocket 消息时,它都会丢弃该消息,而是写入e414 headers: request URI too long系统日志。查看 pound 源代码,这似乎是因为 pound 尝试解析 HTTP 标头。为此,它首先搜索在 WebSocket 消息中找不到的 EOL,因此将该消息视为无效消息丢弃。

因此看起来 OP 问题的答案确实是:pound 不能做 WebSocket。

我给英镑列表写了一封关于这个问题的电子邮件:http://www.apsis.ch/pound/pound_list/archive/2014/2014-01/1388844924000

相关内容