检测来自客户端的WebSocket连接中的负载均衡器/服务器更改

时间:2018-01-31 00:03:30

标签: sockets websocket amazon-elb

当前配置

我有一个客户端应用程序通过WebSocket(node.js)连接连接到云服务器。我需要WebSocket来获取传入消息的实时通知。

在此示例中,请使用 abc.example.com 云服务器的域名。

云服务器配置

Cloud由Amazon Elastic Load Balancer提供支持。 此云服务器具有以下基础架构:

Cloud architecture

在云更新时,负载均衡器会切换到另一个,因此发布到云的所有新数据都由新的负载均衡器和服务器处理。 因此,即使负载均衡器/服务器发生更改,也始终可以访问 abc.example.com 。 (例如,进行HTTP呼叫)

WebSocket配置

WebSocket配置连接到 abc.example.com ,它连接到某个服务器并保持连接到这个服务器,直到关闭它为止。

问题

连接后,WebSocket连接对云上的服务器保持打开状态,并且无法检测负载均衡器何时切换到另一个(例如,在云更新时) 因此,如果我为客户端向服务器发送新数据(例如新消息),则连接的客户端不会通过WebSocket连接接收它。

虽然HTTP GET查询可以正常工作,因为它可以解析正确的服务器。

根据我的理解,这是一种正常的行为,因为客户端与WebSocket连接的服务器仍然处于打开状态,并且没有关闭连接;也没有错。 实际上,我试图切换负载均衡器和服务器(客户端连接的初始服务器)在定期ping通时仍向客户端发送pong响应。

那么有没有办法检测负载均衡器何时从客户端切换?我不允许修改云配置,但如果有一个相当简单的解决方案,我可以建议。

底线:云更新时我不想错过任何通知。

其他观察:

  • 在t0:
    • 由于ELB1
    • ,客户端应用程序通过WebSocket连接到server1
    • 在向云发送新消息时,接收通过WebSocket成功
  • 在t1:
    • 云更新:从ELB1切换到ELB2
    • 未能通过WebSocket接收新邮件
  • 在t2:
    • 云更新:从ELB2切换到ELB1
    • 在向云发送新消息时,接收通过WebSocket成功

感谢任何建议/帮助,

* This answer帮助我理解了网络结构,但我仍然没有想法。 *如果术语不完全合适,请道歉。

1 个答案:

答案 0 :(得分:0)

您是否考虑过使用Pubis / Sub服务器/数据库,例如Redis?

这将以允许Websocket连接完全独立于HTTP连接的方式扩充体系结构,因此可以将一台服务器上的事件推送到不同服务器上的websocket连接。

这是一种非常常见的水平扩展网络设计,应该很容易使用Redis或MongoDB实现。

另一种方法(我发现效率较低,但可以为特定的数据库和设计提供扩展优势)将是每个服务器进行"轮询"数据库("订阅"到数据库更改),允许服务器模拟发布/订阅订阅并将数据推送到连接的客户端。

第三种方法是迄今为止最复杂的方法,即实施一个“八卦”#34;协议和内部发布/订阅服务。

正如您所看到的,这三种方法都有一个共同点 - 它们永远不会假设 HTTP连接和Websocket连接被路由到同一服务器。

编辑(使用redis的简单示例):

使用Ruby和碘HTTP / Websocket服务器,这是一个使用第一种方法将事件推送到客户端的应用程序的快速示例(一个常见的Redis数据库用于Pub / Sub)。

请注意,哪个服务器发起事件并不重要,事件会被推送到等待的客户端。

该应用程序非常简单,使用单个事件"系列" (发布/订阅频道称为"聊天"),但使用多个频道过滤事件很容易,例如每个用户的频道或每个资源的频道(即博客帖子等等)

客户端也可以监听多种事件类型(订阅多个频道)或使用全局匹配来订阅所有(现有的和未来的)匹配频道。

将以下内容保存到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

确保安装了碘宝石(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

在此示例中,我使用端口30003030运行两个单独的服务器进程。

从两个浏览器窗口连接到两个端口。例如(一个快速的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!"); };

请注意,事件被推送到两个websockets,而不关心事件的来源。

如果我们不定义REDIS_URL环境变量,应用程序将不会使用Redis数据库(它将使用碘的内部引擎)以及任何事件的范围将限于单个服务器(单个端口)。

您还可以关闭Redis数据库并注意事件如何暂停/延迟,直到Redis服务器重新启动(在不同服务器重新连接时,这些实例中的某些事件可能会丢失,但我想我们必须处理网络故障处理决定某种方式)...

请注意,我是碘的作者,但这种架构方法不具备Ruby或碘特性 - 这是解决水平缩放问题的常用方法。< / p>