Simple publish-subscribe messaging system
example based on kanbanery.com
past days
pusher.com
-
hosted API
-
client/server libs (pusher gem)
-
based on WebSocket
- 200$/month for 5,000 max connections
Pusher is great for...
Chat
Activity Streams
Notifications
Collaboration
Multiplayer games
Realtime data
Dashboards
2nd Screen experiences
nowadays
Faye
-
provides message servers for
Node.js
and
Ruby
(Faye is just like any other Rack app)
-
clients for use on the
server
and in all major
web browsers
- storage layer: memory or redis
architecture
Faye
-
based on the
Bayeux protocol
(operations: handshake, connect, disconnect, subscribe, unsubscribe and publish )
- Server-side/Client-side extensions
Clustering
Redis
Faye
Adapter
Faye
-
Persistent connections using
WebSocket
-
Long-polling
via HTTP POST
-
Cross Origin Resource Sharing (XHR to another domain, modern browsers, supports many types of HTTP requests)
- Callback-polling via JSON-P (supports old browsers, only GET requests)
Faye
Ruby Server
# file: faye.ru - rackup config file
require File.expand_path('../config/environment', __FILE__)
bayeux = Faye::RackAdapter.new(:mount => '/faye', :timeout => 25)
bayeux.add_extension(PushOnlyServer.new)
bayeux.add_extension(ServerAuth.new)
bayeux.bind(:subscribe) do |client_id, channel|
Rails.logger.info "[#{Time.now.to_s(:db)} Faye #{client_id}] Subscribed channel #{channel}"
end
run bayeux
Faye
Start Ruby Server
$ thin --rackup faye.ru -e development -p 8090 start
http://127.0.0.1:8090/faye
http://127.0.0.1:8090/faye/client.js
tip for circleci.yml
- nohup bash -c "bundle exec thin --rackup faye.ru -e test
-p 8090 start &"
Extensions
Push Only Server
incoming message
# lib/faye/push_only_server.rb
class PushOnlyServer
def incoming(message, callback)
unless message['channel'] =~ /^\/meta\//
password = message['ext'] && message['ext']['password']
if password != AppConfig.faye.push_secret
message['error'] = '403::Password required'
Rails.logger.warn "[#{Time.now.to_s(:db)} Faye #{message['clientId']}] Failed to authenticate as message sender: invalid password"
end
end
callback.call(message)
end
# ...
end
Extensions
Push Only Server
outgoing message
# lib/faye/push_only_server.rb
class PushOnlyServer
# ...
def outgoing(message, callback)
unless message['ext'].nil?
message['ext'].delete('password')
end
callback.call(message)
end
end
publish message on server-side
class LiveUpdate
def self.send_to_faye(projects, project_parts, ...)
# ...
channel = FayeUtils.js_app_channel_name(project_part, project.id)
message = {
:channel => channel,
:data => {
:event => msg,
:faye_socket_id => socket_id
},
:ext => {
:password => AppConfig.faye.push_secret
}
}
uri = URI.parse(AppConfig.faye.internal_url)
Net::HTTP.post_form(uri, :message => message.to_json) # POST message
# ...
end
end
Extensions
Server Auth
class ServerAuth
def incoming(message, callback)
# Let non-subscribe messages through
return callback.call(message) unless message['channel'] == '/meta/subscribe'
if message['ext'].nil?
message['error'] = 'Missing ext key in the message'
else
user_id = message['ext']['user_id']
# ... check if user can subscribe the channel
end
# Call the server back now we're done
callback.call(message)
end
end
# ...
user_id = message['ext']['user_id']
faye_token = message['ext']['faye_token']
public_board_keys = message['ext']['public_board_keys']
project_id = message['ext']['project_id']
project = Project.get(project_id)
if project.present?
# ... check if user has access to project etc
user = User.get(user_id)
if user && user.valid_faye_token?(faye_token)
client_id = message['clientId'] # faye client id
project_membership = project.project_memberships.first(user_id: user_id)
$semaphore.synchronize do
$user_activities[client_id] = project_membership.id
end
end
elsif user
message['error'] = "Invalid subscription faye token"
else
message['error'] = "User doesn't exist"
end
end
else
message['error'] = "Project doesn't exist"
end
end
# ...
faye.ru - Ruby Server with Mutex
require File.expand_path('../config/environment', __FILE__)
# key: faye client id
# value: list of project memberships ids
$user_activities = {}
$semaphore = Mutex.new
$update_thread = Thread.new do
loop do
sleep 60
$semaphore.synchronize do
ids = $user_activities.values.uniq
Rails.logger.info "[#{Time.now.to_s(:db)} Faye] Updating user activity timestamps for #{ids.size} membership(s): #{ids.join(", ")}"
ProjectMembership.touch_timestamps_and_last_activity($user_activities.values.uniq)
end
end
end
bayeux = Faye::RackAdapter.new(:mount => '/faye', :timeout => 25)
# ...
Browser client
Subscribing to channels
The * wildcard matches any channel segment.
So /foo/* matches /foo/bar and /foo/thing but not /foo/bar/thing.
The ** wildcard matches any channel name recursively. So /foo/** matches /foo/bar, /foo/thing and /foo/bar/thing.
Browser client
Client-side extension
class K2.FayeConnection
# ...
connectAndBindEvents: ->
# ...
@faye_client = new Faye.Client '<%= AppConfig.faye.public_url %>',
timeout: timeout
# client-side extension
@faye_client.addExtension outgoing: (message, callback) ->
message.ext = message.ext or {}
message.ext.user_id = K2.faye.currentUserId
message.ext.faye_token = K2.faye.fayeToken
message.ext.public_board_keys = K2.faye.publicBoardKeys
message.ext.project_id = K2.faye.projectId
callback(message)
# ...
Browser client
Bind events
class K2.FayeConnection
# ...
@faye_client.bind 'transport:up', ->
# the client is online
console.log "TRANSPORT_UP"
console.log "New socket ID: " + faye_connection.faye_client._0
faye_connection.polling.stop()
faye_connection.polling.fullRefresh()
@faye_client.bind 'transport:down', ->
# the client is offline
# update data with interval (kanbanery stuff - not related to faye)
faye_connection.polling.start()
faye_connected = false
Browser client
Subscribe to channel
class K2.FayeConnection
subscribeToChannelAndBindEvents: ->
# ...
subscription = @faye_client.subscribe @fayeChannelName(), (message) ->
# handle message
console.log "GOT MESSAGE", message
# ignore message sent by client
if message.faye_socket_id != faye_connection.faye_client._0
faye_connection.store.refresh message.event
# subscription has been set up and acknowledged by the server
subscription.callback ->
# uniq faye hash for connection
console.log "SUBSCRIBED"
console.log "New socket ID: " + faye_connection.faye_client._0
faye_connection.polling.stop()
# error while creating subscription
subscription.errback (error) ->
console.log(error.message)
faye_connection.polling.start()
Check 2nd screen experiences
on
Thanks
Links
Alternatives for Faye
https://github.com/maccman/juggernaut (deprecated)
http://socket.io
Alternative hosted API for pusher.com
http://www.pubnub.com
Other:
http://railscasts.com/episodes/260-messaging-with-faye
http://net.tutsplus.com/tutorials/ruby/how-to-use-faye-as-a-real-time-push-server-in-rails/
https://blog.jcoglan.com/2012/06/09/why-you-should-never-use-hash-functions-for-message-authentication/
Artur Trzop
lunarlogic.io
XII 2013
Faye
By ArturT
Faye
- 3,590