您是否考虑过使用 Pub/Sub 服务器/数据库,例如 Redis?
这将以允许 Websocket 连接完全独立于 HTTP 连接的方式增强架构,因此可以将一台服务器上的事件推送到另一台服务器上的 websocket 连接。
这是一种非常常见的水平扩展网络设计,使用 Redis 或 MongoDB 应该很容易实现。
另一种方法(我发现效率较低但可以为特定数据库和设计提供扩展优势)是让每个服务器“轮询”数据库(“订阅”数据库更改),允许服务器模拟发布/订阅订阅并将数据推送到连接的客户端。
迄今为止最复杂的第三种方法是实现“gossip”协议和内部发布/订阅服务。
如您所见,所有三种方法都有一个共同点 - 它们从不假设 HTTP 连接和 Websocket 连接被路由到同一个服务器。
EDIT(使用 redis 的简单示例):
使用 Ruby 和 iodine HTTP/Websocket 服务器,这是一个使用第一种方法将事件推送到客户端(用于 Pub/Sub 的通用 Redis 数据库)的应用程序的快速示例。
请注意,无论哪个服务器发起事件,事件都会被推送到等待的客户端。
该应用程序非常简单,使用单个事件“系列”(发布/订阅频道称为“聊天”),但使用多个频道过滤事件很容易,例如每个用户的频道或每个资源的频道(即博客文章等')。
客户端也可以监听多种事件类型(订阅多个频道)或使用全局匹配来订阅所有(现有和未来)匹配的频道。
将以下内容保存到config.ru:
require 'uri'
require 'iodine'
# initialize the Redis engine for each Iodine process.
if ENV["REDIS_URL"]
uri = URI(ENV["REDIS_URL"])
Iodine.default_pubsub = Iodine::PubSub::RedisEngine.new(uri.host, uri.port, 0, uri.password)
else
puts "* No Redis, it's okay, pub/sub will support the process cluster."
end
# A simple router - Checks for Websocket Upgrade and answers HTTP.
module MyHTTPRouter
# This is the HTTP response object according to the Rack specification.
HTTP_RESPONSE = [200, { 'Content-Type' => 'text/html',
'Content-Length' => '32' },
['Please connect using websockets.']]
WS_RESPONSE = [0, {}, []]
# this is function will be called by the Rack server (iodine) for every request.
def self.call env
# check if this is an upgrade request.
if(env['upgrade.websocket?'.freeze])
env['upgrade.websocket'.freeze] = WS_RedisPubSub.new(env['PATH_INFO'] && env['PATH_INFO'].length > 1 ? env['PATH_INFO'][1..-1] : "guest")
return WS_RESPONSE
end
# simply return the RESPONSE object, no matter what request was received.
HTTP_RESPONSE
end
end
# A simple Websocket Callback Object.
class WS_RedisPubSub
def initialize name
@name = name
end
# seng a message to new clients.
def on_open
subscribe channel: "chat"
# let everyone know we arrived
# publish channel: "chat", message: "#{@name} entered the chat."
end
# send a message, letting the client know the server is suggunt down.
def on_shutdown
write "Server shutting down. Goodbye."
end
# perform the echo
def on_message data
publish channel: "chat", message: "#{@name}: #{data}"
end
def on_close
# let everyone know we left
publish channel: "chat", message: "#{@name} left the chat."
# we don't need to unsubscribe, subscriptions are cleared automatically once the connection is closed.
end
end
# this function call links our HelloWorld application with Rack
run MyHTTPRoute
确保您已安装 iodine gem (gem install ruby)。
确保你有一个 Redis 数据库服务器正在运行(在这个例子中我的运行在 localhost 上)。
从终端,在两个不同的端口上运行碘服务器的两个实例(使用两个终端窗口或添加& 来恶魔化进程):
$ REDIS_URL=redis://localhost:6379/ iodine -t 1 -p 3000 redis.ru
$ REDIS_URL=redis://localhost:6379/ iodine -t 1 -p 3030 redis.ru
在此示例中,我运行两个独立的服务器进程,使用端口 3000 和 3030。
从两个浏览器窗口连接到两个端口。例如(一个快速的 javascript 客户端):
// run 1st client app on port 3000.
ws = new WebSocket("ws://localhost:3000/Mitchel");
ws.onmessage = function(e) { console.log(e.data); };
ws.onclose = function(e) { console.log("closed"); };
ws.onopen = function(e) { e.target.send("Yo!"); };
// run 2nd client app on port 3030 and a different browser tab.
ws = new WebSocket("ws://localhost:3000/Jane");
ws.onmessage = function(e) { console.log(e.data); };
ws.onclose = function(e) { console.log("closed"); };
ws.onopen = function(e) { e.target.send("Yo!"); };
请注意,事件被推送到两个 websocket,而无需担心事件的来源。
如果我们不定义 REDIS_URL 环境变量,应用程序将不会使用 Redis 数据库(它将使用 iodine 的内部引擎)并且任何事件的范围都将被限制在单个服务器(单个端口)。
您还可以关闭 Redis 数据库并注意事件是如何暂停/延迟的,直到 Redis 服务器重新启动(在这些实例中,当不同的服务器重新连接时,某些事件可能会丢失,但我想网络故障处理是我们必须要做的事情决定一种或另一种方式)...
请注意,我是 iodine 的作者,但这种架构方法不是 Ruby 或 iodine 特定的 - 它是解决水平缩放问题的一种非常常见的方法。