网络上的客户端-服务器通信在过去曾是一种请求-响应模型,要求客户端(比如 Web 浏览器)向服务器请求资源。服务器通过发送所请求的资源来响应客户端请求。如果资源不可用,或者客户端没有权限访问它,那么服务器会发送一条错误消息。在请求-响应架构中,服务器绝不能向客户端发送未经请求的消息。
随着 Web 应用程序变得更强大和更具交互性,请求-响应模型的局限性也开始显现出来。需要更频繁更新的客户端应用程序被要求更频繁地发送 GET 请求。这种技术称为轮询,在高峰期间,这可能会使服务器不堪重负,并导致性能问题。该技术效率低下,因为客户端发送的许多请求都没有返回更新。此外,客户端只能按指定间隔进行轮询,这可能减缓客户端的响应速度。
HTTP 服务器推送技术的出现,就是为了解决与频繁轮询相关的性能问题和其他局限。尤其是对于交互式 Web 应用程序,比如游戏和屏幕共享服务,Web 服务器能更高效地在新数据可用时向客户端发送更新。
var source = new EventSource(“/user-log-stream”);
source.onmessage = function(event) {
var message = event.data;
// do stuff based on received message
};
Websockify 代理由 python-websockify 库实现。它使用 CherryPy Web 服务器引擎来实现初始化,如清单 3 所示。
清单 3. Websockify 代理:
params = {'listen_host': '127.0.0.1',
'listen_port': config.get('server', 'websockets_port'),
'ssl_only': False}
# old websockify: do not use TokenFile
if not tokenFile:
params['target_cfg'] = WS_TOKENS_DIR
# websockify 0.7 and higher: use TokenFile
else:
params['token_plugin'] = TokenFile(src=WS_TOKENS_DIR)
def start_proxy():
try:
server = WebSocketProxy(RequestHandlerClass=CustomHandler,
**params)
except TypeError:
server = CustomHandler(**params)
server.start_server()
proc = Process(target=start_proxy)
proc.start()
return proc
Websockify 代理将所有已注册的令牌都绑定到它的 WebSocket URI,如下所示。
清单 4. 绑定已注册的令牌:
def add_proxy_token(name, port, is_unix_socket=False):
with open(os.path.join(WS_TOKENS_DIR, name), 'w') as f:
"""
From python documentation base64.urlsafe_b64encode(s)
substitutes - instead of + and _ instead of / in the
standard Base64 alphabet, BUT the result can still
contain = which is not safe in a URL query component.
So remove it when needed as base64 can work well without it.
"""
name = base64.urlsafe_b64encode(name).rstrip('=')
if is_unix_socket:
f.write('%s: unix_socket:%s' % (name.encode('utf-8'), port))
else:
f.write('%s: localhost:%s' % (name.encode('utf-8'), port))
在推送服务器中,我们监听从 UI 传入的所有消息,查看是否收到了 CLOSE 消息。如果收到该消息,则立即关闭套接字,如下所示。
清单 9. 推送服务器对 CLOSE 的响应:
def listen(self):
try:
while self.server_running:
read_ready, _, _ = select.select(self.connections,
[], [], 1)
for sock in read_ready:
if not self.server_running:
break
if sock == self.server_socket:
new_socket, addr = self.server_socket.accept()
self.connections.append(new_socket)
else:
try:
data = sock.recv(4096)
except:
try:
self.connections.remove(sock)
except ValueError:
pass
continue
if data and data == 'CLOSE':
try:
self.connections.remove(sock)
except ValueError:
pass
sock.close()
在 WebSocket 关闭和发送 CLOSE 消息之前调用 onclose,这样就能预防 send_notification 例程中发生管道损坏错误。不幸的是,尽管 onclose 在 Firefox 中运行良好,但测试表明,我们在 Chrome 中打开该应用程序时,仍在 send_notification 方法中获得了管道损坏错误。这是由于 Google Chrome 中的一个未解决的问题。
def send_notification(self, message):
for sock in self.connections:
if sock != self.server_socket:
try:
sock.send(message)
except IOError as e:
if 'Broken pipe' in str(e):
sock.close()
try:
self.connections.remove(sock)
except ValueError:
pass