WebSockets¶
WebSocket 回显示例
概览¶
什么是 WebSocket? WebSocket API
lighttpd 可以通过多种方式支持 WebSocket 连接,包括通过 mod_proxy、mod_cgi、mod_scgi、mod_fastcgi 和 mod_wstunnel。
- lighttpd 支持 HTTP/1.1 连接中的
Upgrade: websocket
。 - lighttpd 支持 HTTP/2 扩展 CONNECT,通过单个 HTTP/2 连接在 HTTP/2 流上使用
:protocol: websocket
。
出于安全原因并遵循最小惊喜原则,lighttpd 对 WebSocket 的支持默认是特意禁用的。可以配置 lighttpd 动态模块以启用连接升级到 WebSocket 协议。
自 lighttpd 1.4.46 版本起,lighttpd 的 mod_cgi、mod_proxy 和 mod_wstunnel 模块支持 WebSocket。在 lighttpd 1.4.74 版本中,又增加了对 mod_scgi 和 mod_fastcgi 这两个动态模块的支持。
本文档演示了配置 lighttpd 模块以运行 WebSocket “回显” 示例应用程序的多种方法。
基本配置¶
通过 mod_wstunnel 使用 WebSockets¶
配置 mod_wstunnel
mod_wstunnel 是一个 WebSocket 隧道端点,用于终止来自客户端的 WebSocket 隧道。mod_wstunnel 会解码 WebSocket 帧,然后将数据(不带 WebSocket 帧)传递给后端,而在相反方向上,它会在将响应发送给客户端之前,将后端响应编码成 WebSocket 帧。
通过 mod_proxy 使用 WebSockets¶
配置 mod_proxy 并启用 proxy.header += ("upgrade" => "enable")
从 lighttpd 1.4.74 及更高版本开始,"upgrade" => "enable"
也是 proxy.server
中的一个主机选项。
当启用升级时,mod_proxy 可以作为 WebSocket 连接的反向代理。
通过 mod_cgi 使用 WebSockets¶
配置 mod_cgi 并启用 cgi.upgrade = "enable"
mod_cgi 允许目标程序升级使用 WebSocket 协议。
通过 mod_scgi 使用 WebSockets¶
配置 mod_scgi 并在 scgi.server
中为每个主机添加 "upgrade" => "enable"
选项(lighttpd 1.4.74 及更高版本)
mod_scgi 允许目标程序升级使用 WebSocket 协议。
通过 mod_fastcgi 使用 WebSockets¶
配置 mod_fastcgi 并在 fastcgi.server
中为每个主机添加 "upgrade" => "enable"
选项(lighttpd 1.4.74 及更高版本)
mod_fastcgi 允许目标程序升级使用 WebSocket 协议。
虽然 lighttpd 目前不支持在单个 FastCGI 连接上多路复用多个请求,但 FastCGI 协议本身是允许的。即使请求升级到使用 WebSocket 协议后,lighttpd 的 mod_fastcgi 仍然会从后端发送和接收 FastCGI 数据包。WebSocket 帧再次被封装到 FastCGI 数据包中。因此,这可能不如使用其他支持升级到 WebSocket 协议的 lighttpd 模块效率高。
示例:ws_echo.py¶
ws_echo.py - 简单的“回显”WebSocket 服务器 CGI 示例程序
将文件安装到文档根目录和 ws/ 子目录中<docroot>/ws_echo.html
<docroot>/ws/ws_echo.py
如果使用 HTTPS,请修改 ws_echo.html 中的 URL,将 ws:// 改为 wss://。var host = "ws://" + window.location.hostname + "/ws/ws_echo.py";
<docroot>/ws_echo.html
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8" /> <!-- favicon: none --> <link rel="icon" href="data:,"> <!-- <meta http-equiv="Content-Security-Policy" content="default-src 'self' 'unsafe-inline'" /> --> <title>websocket echo client</title> <style> #output { border: solid 1px #000; } </style> </head> <body> <form id="form" accept-charset=utf-8> <input type="text" id="message"> <button type="submit">Send</button> </form> <hr> <div id="output"></div> <script> var inputBox = document.getElementById("message"); var output = document.getElementById("output"); var form = document.getElementById("form"); try { var host = "ws://" + window.location.hostname + "/ws/ws_echo.py"; //var host = "wss://" + window.location.hostname + "/ws/ws_echo.py"; //var host = "ws://" + window.location.hostname + ":8080/ws/ws_echo.py"; //var host = "wss://" + window.location.hostname + ":8443/ws/ws_echo.py"; console.log("Host:", host); var s = new WebSocket(host); s.onopen = function (e) { console.log("Socket opened."); }; s.onclose = function (e) { console.log("Socket closed."); }; s.onmessage = function (e) { console.log("Socket message:", e.data); var p = document.createElement("p"); p.innerHTML = e.data; output.appendChild(p); }; s.onerror = function (e) { console.log("Socket error:", e); }; } catch (ex) { console.log("Socket exception:", ex); } form.addEventListener("submit", function (e) { e.preventDefault(); s.send(inputBox.value); inputBox.value = ""; }, false) </script> </body> </html>
<docroot>/ws/ws_echo.py
#!/usr/bin/env python3 # # ws_echo.py - websocket echo; reflect back to peer the frames sent by peer # # Copyright(c) 2023 Glenn Strauss gstrauss()gluelogic.com All rights reserved # License: BSD 3-clause (same as lighttpd) import struct import hashlib import base64 def ws_upgrade(rd, wr, env): if env.get('HTTP_UPGRADE', '') != 'websocket': wr.write("Status: 204\n\n".encode()) return False # (throws KeyError exception if any of these env vars are not set) origin = env['HTTP_ORIGIN'] scheme = "wss" if env['REQUEST_SCHEME'] == "https" else "ws" host = env['HTTP_HOST'] urlpath= env['SCRIPT_NAME'] key = env['HTTP_SEC_WEBSOCKET_KEY'].encode('utf-8') GUID = b"258EAFA5-E914-47DA-95CA-C5AB0DC85B11" digest = str(base64.b64encode(hashlib.sha1(key + GUID).digest()), 'utf-8') wr.write(("Status: 101\n" + "Upgrade: WebSocket\n" + "Connection: Upgrade\n" + f"Sec-WebSocket-Origin: {origin}\n" + f"Sec-WebSocket-Location: {scheme}://{host}{urlpath}\n" + f"Sec-WebSocket-Accept: {digest}\n\n" ).encode()) return True ## +-+-+-+-+-------++-+-------------+-------------------------------+ ## |F|R|R|R| opcode||M| Payload len | Extended payload length | ## |I|S|S|S| (4) ||A| (7) | (16/63) | ## |N|V|V|V| ||S| | (if payload len==126/127) | ## | |1|2|3| ||K| | | ## +-+-+-+-+-------++-+-------------+ - - - - - - - - - - - - - - - + ## +-+-+-+-+--------------------------------------------------------+ ## | Extended payload length continued, if payload len == 127 | ## + - - - - - - - - - - - - - - - +--------------------------------+ ## + - - - - - - - - - - - - - - - +-------------------------------+ ## | |Masking-key, if MASK set to 1 | ## +-------------------------------+-------------------------------+ ## | Masking-key (continued) | Payload Data | ## +-------------------------------- - - - - - - - - - - - - - - - + ## : Payload Data continued ... : ## + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + ## | Payload Data continued ... | ## +---------------------------------------------------------------+ def ws_recv_and_echo(rd, wr): # (note: assumes well-formed websocket frame; not validating or paranoid) # (note: inefficient small reads rather than read/buffering larger blocks) # (note: no buffering; program exits if partial read of frame header) # (note: no buffering; program loses state if partial read of frame data # and subsequently will likely thrown an exception) # (tl;dr: this toy program is sufficient for a demo of frames with small # data payloads, but probably nothing more) data = rd.read(2) if len(data) == 0: # EOF ws_send(wr, b'1000', opcode=0x8) # CLOSE "1000 = Normal Closure" return False head1, head2 = struct.unpack('!BB', data) fin = bool(head1 & 0b10000000) opcode = head1 & 0b00001111 if opcode == 0x8: # CLOSE (0x8) # (note: not reading and parsing close reason sent by peer) ws_send(wr, b'1000', opcode=opcode) # "1000 = Normal Closure" return False length = head2 & 0b01111111 if length == 126: data = rd.read(2) length, = struct.unpack('!H', data) elif length == 127: data = rd.read(8) length, = struct.unpack('!Q', data) mask_bit = bool(head2 & 0b10000000) masking_key = rd.read(4) if mask_bit else False data = bytearray(rd.read(length)) if length else b'' if opcode == 0xA: # PONG # receive and ignore response to PING return True if opcode == 0x9: # PING # reflect PING back to peer as PONG opcode = 0xA # PONG if mask_bit: # unmask data for i in range(0, len(data)): data[i] ^= masking_key[i % 4] # echo data back to peer using same masking_key # (note: not validating continuation frame follows frame without fin set) #if opcode == 0x0: # CONTINUATION #if opcode == 0x1: # TEXT #if opcode == 0x2: # BINARY #if opcode == 0xA: # PONG (from PING above) ws_send(wr, ''.join(map(chr, data)).encode(), opcode=opcode, fin=fin, masking_key=masking_key) return True def ws_send(wr, data, opcode=1, fin=True, masking_key=False): if 0x3 <= opcode <= 0x7 or 0xB <= opcode: raise ValueError('Invalid opcode') header = struct.pack('!B', ((bool(fin) << 7) | opcode)) mask_bit = (1 << 7) if masking_key else 0 length = len(data) if length < 126: header += struct.pack('!B', (mask_bit|length)) elif length < (1 << 16): header += struct.pack('!B', (mask_bit|126)) + struct.pack('!H', length) elif length < (1 << 63): header += struct.pack('!B', (mask_bit|127)) + struct.pack('!Q', length) else: raise ValueError('Data too large') if mask_bit: # mask data header += masking_key data = bytearray(data) for i in range(0, length): data[i] ^= masking_key[i % 4] wr.write(header + data) def ws_echo_app(rd, wr, env): if not ws_upgrade(rd, wr, env): return while True: if not ws_recv_and_echo(rd, wr): break import os import sys #import errno #import io #from traceback import format_exc as format #import logging #logger = logging.getLogger('mylogger') #logger.setLevel(logging.WARNING) if __name__ == '__main__': try: sys.stdin = os.fdopen(sys.stdin.fileno(), 'r+b', buffering=0) sys.stdout = os.fdopen(sys.stdout.fileno(), 'r+b', buffering=0) ws_echo_app(sys.stdin, sys.stdout, os.environ) #except IOError as e: # if e.errno != errno.EPIPE: # raise e except KeyboardInterrupt: # Ctrl-C or SIGINT try: sys.exit(130) except SystemExit: os._exit(130) except Exception as e: err = e.args[0] #logger.warning("err:{}\n".format(str(err)))
示例:配置 lighttpd 以在 /ws/ 下运行 CGI 程序¶
server.modules += ("mod_cgi") $HTTP["url"] =^ "/ws/" { cgi.assign = ("" => "") cgi.upgrade = 1 #cgi.limits = ( "read-timeout" => 60, "write-timeout" => 60 ) } #server.max-read-idle := 60 #server.max-write-idle := 60
示例:配置 lighttpd 以在 /ws/ 下运行 SCGI 程序¶
使用 scgi-cgi 运行 ws_echo.py
server.modules += ("mod_scgi") scgi.server = ("/ws/" => (( "socket" => "/tmp/scgi-ws.sock", # should use more secure location "bin-path" => "/usr/local/bin/scgi-cgi" # modify path to scgi-cgi "check-local" => "disable", "min-procs" => 1, "max-procs" => 1, "upgrade" => 1 #"read-timeout" => 60, #"write-timeout" => 60, )) ) #server.max-read-idle := 60 #server.max-write-idle := 60
示例:配置 lighttpd 以在 /ws/ 下运行 FastCGI 程序¶
使用 fcgi-cgi 运行 ws_echo.py
server.modules += ("mod_fastcgi") fastcgi.server = ("/ws/" => (( "socket" => "/tmp/fcgi-ws.sock", # should use more secure location "bin-path" => "/usr/local/bin/fcgi-cgi" # modify path to fcgi-cgi "check-local" => "disable", "min-procs" => 1, "max-procs" => 1, "upgrade" => 1 #"read-timeout" => 60, #"write-timeout" => 60, )) ) #server.max-read-idle := 60 #server.max-write-idle := 60
示例:echo.pl¶
echo.pl - 读取并逐行回显的简单“回显”脚本
这个脚本对 WebSocket 一无所知,它可以读/写 JSON 或其他任何数据。本示例是逐行读取和回显数据。
将文件安装到脚本位置和文档根目录/tmp/echo.pl
<docroot>/count.html
echo.pl
#!/usr/bin/perl -Tw $SIG{PIPE} = 'IGNORE'; for (my $FH; accept($FH, STDIN); close $FH) { select($FH); $|=1; # $FH->autoflush; print $FH $_ while (<$FH>); }
<docroot>/count.html
<!DOCTYPE html> <!-- modified from example in https://github.com/joewalnes/websocketd README.md --> <pre id="log"></pre> <script> // helper function: log message to screen var logelt = document.getElementById('log'); function log(msg) { logelt.textContent += msg + '\n'; } // helper function: send websocket msg with count (1 .. 5) var ll = 0; function send_msg() { if (++ll <= 5) { log('SEND: '+ll); ws.send(ll+'\n'); } } // setup websocket with callbacks var ws = new WebSocket('ws://'+location.host+'/ws/'); ws.onopen = function() { log('CONNECT\n'); send_msg(); }; ws.onclose = function() { log('DISCONNECT'); }; ws.onmessage = function(event) { log('RECV: ' + event.data); send_msg(); }; </script>
echo-incremental.pl
这是一个替代的“回显”示例脚本,它是无缓冲的——而不是上面更简单的行缓冲示例——并且在每次读取之间会休眠 1 秒,以便您可以在浏览器中直观地看到多个 WebSocket 消息在服务器之间来回传递。此示例脚本将运行 5 秒,然后关闭 WebSocket 连接。(要运行此示例,请使用此脚本替换 echo.pl
并重新启动运行 lighttpd-wstunnel.conf 的 lighttpd。)
#!/usr/bin/perl -Tw $SIG{PIPE} = 'IGNORE'; for (my $FH; accept($FH, STDIN); close $FH) { foreach (1..5) { sleep 1; my $foo = ""; sysread($FH, $foo, 1024); if (length($foo)) { syswrite($FH, $foo); } } close $FH; }
示例:配置 lighttpd 作为 /ws/ 下 URL 的 WebSocket 端点¶
这是一个简单的示例,展示了 lighttpd 的 mod_wstunnel 如何终止 WebSocket 隧道并将负载发送/接收到后端“回显”脚本。对于此示例,请在备用端口上使用 mod_wstunnel 运行 lighttpd,此示例将在下面使用 mod_proxy 的示例中重复使用。
lighttpd-wstunnel.conf
(在端口 8081 监听;使用 lighttpd -D -f /dev/shm/lighttpd-wstunnel.conf
启动;按 Ctrl-C 退出)
server.document-root = "/tmp" # place count.html here (better: use a more secure location) server.bind = "127.0.0.1" # comment out if accessing from remote machine server.port = 8081 server.modules += ("mod_wstunnel") wstunnel.server = ( "/ws/" => ( ( "socket" => "/tmp/echo.sock", # should use more secure location "bin-path" => "/tmp/echo.pl", # should use more secure location "max-procs" => 1 ) ) )
在本地浏览器中加载为 http://localhost:8081/count.html,并观察它从 1 数到 5。如果您是从远程系统连接,则修改 lighttpd-wstunnel.conf,注释掉
server.bind = "127.0.0.1"
并替换 count.html URL 中的 localhost
。如果从远程机器访问,请替换 URL 中的 localhost
。
示例:配置 lighttpd 反向代理 /ws/ 下的 URL¶
这是一个简单的示例,展示了 lighttpd 的 mod_proxy 如何支持 WebSocket 连接到另一个 lighttpd 实例,该实例使用 mod_wstunnel 终止 WebSocket 隧道并将负载发送/接收到后端“回显”脚本。它使用了上面 mod_wstunnel 的示例,因此请确保该示例也正在运行。
lighttpd-proxy.conf
(在端口 8080 监听;在另一个 shell 中使用 lighttpd -D -f /dev/shm/lighttpd-proxy.conf
启动;按 Ctrl-C 退出)
server.document-root = "/tmp" # place count.html here (better: use a more secure location) server.bind = "127.0.0.1" # comment out if accessing from remote machine server.port = 8080 server.modules += ("mod_proxy") proxy.server = ( "/" => (( "host" => "127.0.0.1", "port" => "8081" ))) proxy.header = ( "upgrade" => "enable" )
在本地浏览器中加载为 http://localhost:8080/count.html,并观察它从 1 数到 5。如果您是从远程系统连接,则修改 lighttpd-proxy.conf,注释掉
server.bind = "127.0.0.1"
并替换 count.html URL 中的 localhost
。如果从远程机器访问,请替换 URL 中的 localhost
。
请注意,在此示例中,count.html 的目标现在是 http://localhost:8080/count.html,使用端口 8080(在上面的示例中从 8081 更改)。