faye-online 0.1
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +1 -0
- data/Gemfile +5 -0
- data/README.markdown +123 -0
- data/Rakefile +28 -0
- data/app/assets/javascripts/faye-online.js +52 -0
- data/app/models/faye_channel.rb +6 -0
- data/app/models/faye_channel_online_list.rb +27 -0
- data/app/models/faye_user_login_log.rb +6 -0
- data/app/models/faye_user_online_time.rb +60 -0
- data/db/migrate/20130326113504_faye_create_user_list.rb +26 -0
- data/db/migrate/20130531033325_add_faye_user_login_logs.rb +15 -0
- data/faye-online.gemspec +29 -0
- data/lib/faye-online.rb +85 -0
- data/lib/faye-online/message.rb +183 -0
- data/lib/faye-online/rails_engine.rb +20 -0
- data/lib/faye-online/server.rb +60 -0
- data/lib/faye-online/status.rb +79 -0
- data/rainbows.conf +34 -0
- data/test/database.yml +6 -0
- data/test/test_faye-online.rb +110 -0
- metadata +288 -0
data/.gitignore
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
Gemfile.lock
|
data/Gemfile
ADDED
data/README.markdown
ADDED
@@ -0,0 +1,123 @@
|
|
1
|
+
Faye Online user list and time count
|
2
|
+
===========================================
|
3
|
+
|
4
|
+
Usage
|
5
|
+
-------------------------------------------
|
6
|
+
1. Add it to Gemfile
|
7
|
+
|
8
|
+
```ruby
|
9
|
+
gem 'faye-online'
|
10
|
+
```
|
11
|
+
|
12
|
+
2. add faye related table
|
13
|
+
|
14
|
+
```zsh
|
15
|
+
bundle exec rake db:migrate
|
16
|
+
```
|
17
|
+
|
18
|
+
3. faye server
|
19
|
+
Create a faye.ru at your rails app root, configure it,
|
20
|
+
|
21
|
+
```ruby
|
22
|
+
$faye_server = FayeOnline.get_server(:host=>"localhost",
|
23
|
+
:port=>6379,
|
24
|
+
:database=>1,
|
25
|
+
:namespace=>"faye",
|
26
|
+
:gc=>5)
|
27
|
+
run $faye_server
|
28
|
+
```
|
29
|
+
|
30
|
+
and start faye server
|
31
|
+
|
32
|
+
```sh
|
33
|
+
bundle exec rake faye:start
|
34
|
+
DEBUG_FAYE=true DEBUG=true bundle exec rackup faye.ru -s thin -E production -p 9292
|
35
|
+
```
|
36
|
+
|
37
|
+
4. faye client
|
38
|
+
|
39
|
+
```javascript
|
40
|
+
eoe.faye = Faye.init_online_client({
|
41
|
+
faye_url: faye_url,
|
42
|
+
client_opts: {},
|
43
|
+
auth_opts: {
|
44
|
+
room_channel: eoe.class_channel,
|
45
|
+
time_channel: eoe.lesson_channel,
|
46
|
+
current_user: eoe.current_user
|
47
|
+
}
|
48
|
+
});
|
49
|
+
```
|
50
|
+
|
51
|
+
Date Storage Stucture
|
52
|
+
-------------------------------------------
|
53
|
+
* mysql.table.faye_channels channel name to id, use for key value cache.
|
54
|
+
* mysql.table.faye_channel_online_lists online user list, store as Set data structure, add and delete.
|
55
|
+
* mysql.table.faye_user_online_times user online time, store every start time and spend time.
|
56
|
+
* mysql.table.faye_user_login_logs verbose user login log, used for caculate online times alternately.
|
57
|
+
* .
|
58
|
+
* redis.hashes.ns/uid_to_clientids/room_channel all clientids of one uid in a room_channel
|
59
|
+
* redis.hashes.ns/uid_to_clientids/time_channel all clientids of one uid in a time_channel
|
60
|
+
* redis.keys.ns/clientid_auth/clientid map clientid to user info
|
61
|
+
* redis.keys.ns/clientid_status/clientid current clientid is login or logout
|
62
|
+
|
63
|
+
|
64
|
+
Glossary of Terms
|
65
|
+
-------------------------------------------
|
66
|
+
### clientId
|
67
|
+
客户端(一般是浏览器里的javascript)连到服务器后自动分配一个clientId。
|
68
|
+
示例数据为:
|
69
|
+
|
70
|
+
```json
|
71
|
+
{
|
72
|
+
"channel"=>"/meta/connect",
|
73
|
+
"clientId"=>"sqq4oxlwhj84zw92n0e592j8iq989yy",
|
74
|
+
"id"=>"7",
|
75
|
+
"auth"=>{
|
76
|
+
"room_channel"=>"/classes/4",
|
77
|
+
"time_channel"=>"/courses/5/lessons/1",
|
78
|
+
"current_user"=>{"uid"=>470700, "uname"=>"mvj3"}
|
79
|
+
}
|
80
|
+
}
|
81
|
+
```
|
82
|
+
|
83
|
+
### User Login
|
84
|
+
有很多个clientId连到server。其中可能多个clientId关联的是一个user,这个在 Faye.init_online_client 可以配置。
|
85
|
+
|
86
|
+
### User Logout
|
87
|
+
一个user关联的所有clientId失去连接后才算离线。
|
88
|
+
server判断一个user离开房间的两种机制:
|
89
|
+
|
90
|
+
1. client端主动触发disconnect发消息,对于的实际操作是关闭所有相关页面。
|
91
|
+
|
92
|
+
2. 网络掉线。在 `FayeOnline.get_server` 设置gc参数让server端定时ping所有的clientId,具体方法在faye-redis-ruby.gem里的 `EventMachine.add_periodic_timer(gc, &method(:gc))` 。如果检测是失去连接,那么server就给自己发个伪装的disconnect消息,从而清理失去网络连接的clientId。
|
93
|
+
|
94
|
+
|
95
|
+
改进的客户端autodisconnect
|
96
|
+
-------------------------------------------
|
97
|
+
### 原来的情况
|
98
|
+
在用户关闭浏览器前触发一个"/meta/disconnect"请求。这样如果用户否定关闭后,client对象却还是被销毁了。
|
99
|
+
|
100
|
+
### 改进的方案
|
101
|
+
在浏览器关闭前,发送给server一个过几秒后检测当前clientId是否失去连接的事件。
|
102
|
+
|
103
|
+
1, 这样如果浏览器真的关掉了,那就和原来的autodisconnect发送的"/meta/disconnect"消息一样。
|
104
|
+
|
105
|
+
2, 如果浏览器被用户选择否定关掉,那浏览器里的client对象还是没被销毁,而继续存活。
|
106
|
+
|
107
|
+
|
108
|
+
TODO
|
109
|
+
-------------------------------------------
|
110
|
+
1. FayeOnline.faye_online_status 通用化
|
111
|
+
2. Cluster front end https://github.com/alexkazeko/faye_shards
|
112
|
+
3. use rainbows server, see faye-websocket README。一些尝试见rainbows.conf, https://groups.google.com/forum/#!msg/faye-users/cMPhvIpk-OU/MgRcFhmz8koJ
|
113
|
+
4. 英文化
|
114
|
+
|
115
|
+
|
116
|
+
|
117
|
+
Related Resources
|
118
|
+
-------------------------------------------
|
119
|
+
1. https://github.com/ryanb/private_pub Private Pub is a Ruby gem for use with Rails to publish and subscribe to messages through Faye. It allows you to easily provide real-time updates through an open socket without tying up a Rails process. All channels are private so users can only listen to events you subscribe them to.
|
120
|
+
|
121
|
+
2. http://blog.edweng.com/2012/06/02/faye-extensions-tracking-users-in-a-chat-room/ only track clientId
|
122
|
+
|
123
|
+
3. http://faye.jcoglan.com/ruby/monitoring.html tech detail about track connect and disconnect in faye
|
data/Rakefile
ADDED
@@ -0,0 +1,28 @@
|
|
1
|
+
# encoding: UTF-8
|
2
|
+
|
3
|
+
require 'rake'
|
4
|
+
require 'rake/testtask'
|
5
|
+
|
6
|
+
namespace :faye do
|
7
|
+
desc "Start faye server"
|
8
|
+
task :start => :environment do
|
9
|
+
# Rack::Builder.new.run @server # TODO 脱离父进程
|
10
|
+
# Rack::Server.start
|
11
|
+
#
|
12
|
+
# 自动启动faye服务器
|
13
|
+
# from http://stackoverflow.com/questions/6430437/autorun-the-faye-server-when-i-start-the-rails-server
|
14
|
+
@faye_ru = File.join(`bundle show faye-online`.strip, 'faye.ru')
|
15
|
+
Thread.new do
|
16
|
+
# system("bundle exec rackup faye.ru -s thin -E production")
|
17
|
+
system("bundle exec rackup #{@faye_ru} -s thin -E production")
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
end
|
22
|
+
|
23
|
+
Rake::TestTask.new do |t|
|
24
|
+
t.libs << 'test'
|
25
|
+
end
|
26
|
+
|
27
|
+
desc "Run tests"
|
28
|
+
task :default => :test
|
@@ -0,0 +1,52 @@
|
|
1
|
+
if (!window.jQuery) { throw("please require jquery.js first!"); }
|
2
|
+
if (!window.Faye) { throw("please require faye-browser.js first!"); }
|
3
|
+
|
4
|
+
// Faye 连接
|
5
|
+
Faye.init_online_client = function(opts) {
|
6
|
+
opts = opts || {};
|
7
|
+
|
8
|
+
// validate params
|
9
|
+
if (!opts.faye_url) { throw("faye_url is not defined!"); }
|
10
|
+
opts.client_opts = $.extend({
|
11
|
+
timeout: 10, // 只用于client连server时的主动检测timeout。如果client自行disconnect,那就没办法了。
|
12
|
+
interval: 5, // handshake every 5 seconds
|
13
|
+
retry: 5
|
14
|
+
}, opts.client_opts);
|
15
|
+
var faye = new Faye.Client(opts.faye_url, opts.client_opts);
|
16
|
+
// faye.getState() // "DISCONNECTED"
|
17
|
+
// faye.disable('websocket'); // cause loop connect error
|
18
|
+
faye.disable('autodisconnect'); // 就会关了页面还是保持连接
|
19
|
+
|
20
|
+
// copied from https://github.com/cloudfuji/kandan/
|
21
|
+
// Note that these events do not reflect the status of the client’s session, http://faye.jcoglan.com/browser/transport.html
|
22
|
+
// The transport is only considered down if a request or WebSocket connection explicitly fails, or times out, indicating the server is unreachable.
|
23
|
+
faye.bind("transport:down", function() { console.log("Comm link to Cybertron is down!"); });
|
24
|
+
faye.bind("transport:up", function() { console.log("Comm link is up!"); });
|
25
|
+
|
26
|
+
// 参考 https://github.com/cloudfuji/kandan/ 在线聊天系统
|
27
|
+
// http://faye.jcoglan.com/ruby/extensions.html
|
28
|
+
opts.auth_opts = opts.auth_opts || {};
|
29
|
+
opts.auth_opts.current_user = opts.auth_opts.current_user || {};
|
30
|
+
if (!opts.auth_opts.room_channel || !opts.auth_opts.time_channel || !opts.auth_opts.current_user.uid || !opts.auth_opts.current_user.uname) {
|
31
|
+
console.log("auth_opts is ", opts.auth_opts);
|
32
|
+
throw("auth_opts is not valid. see valided format in README");
|
33
|
+
}
|
34
|
+
var AuthExtension = {
|
35
|
+
outgoing: function (message, callback) {
|
36
|
+
message.auth = (message.auth || {});
|
37
|
+
$.extend(message.auth, opts.auth_opts);
|
38
|
+
|
39
|
+
callback(message);
|
40
|
+
}
|
41
|
+
};
|
42
|
+
faye.addExtension(AuthExtension);
|
43
|
+
|
44
|
+
// notice server to check my clientId after a few seconds, if there's
|
45
|
+
// no connection, and disconnect my clientId.
|
46
|
+
$(window).bind('beforeunload', function(event) {
|
47
|
+
faye.publish("/faye_online/before_leave");
|
48
|
+
});
|
49
|
+
|
50
|
+
|
51
|
+
return faye;
|
52
|
+
};
|
@@ -0,0 +1,27 @@
|
|
1
|
+
# encoding: UTF-8
|
2
|
+
|
3
|
+
class FayeChannelOnlineList < ActiveRecord::Base
|
4
|
+
attr_accessible :channel_id, :user_info_list
|
5
|
+
|
6
|
+
def add uid
|
7
|
+
uid = uid.to_i; return if uid.zero?
|
8
|
+
online_list.update_attributes! :user_info_list => online_list.data.add(uid).to_json
|
9
|
+
end
|
10
|
+
|
11
|
+
def delete uid
|
12
|
+
uid = uid.to_i; return if uid.zero?
|
13
|
+
online_list.update_attributes! :user_info_list => online_list.data.delete(uid).to_json
|
14
|
+
end
|
15
|
+
|
16
|
+
def data
|
17
|
+
@data ||= begin
|
18
|
+
_a = JSON.parse(online_list.user_info_list) rescue []
|
19
|
+
Set.new(_a)
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
def user_list; online_list.data end
|
24
|
+
def user_count; online_list.data.count end
|
25
|
+
|
26
|
+
def online_list; self end
|
27
|
+
end
|
@@ -0,0 +1,60 @@
|
|
1
|
+
# encoding: UTF-8
|
2
|
+
|
3
|
+
class FayeUserOnlineTime < ActiveRecord::Base
|
4
|
+
attr_accessible :user_id, :online_times
|
5
|
+
TIME2013 = Time.parse("20130101")
|
6
|
+
TimeFormat = "%Y%m%d%H%M%S"
|
7
|
+
|
8
|
+
def spend_time channel_name; @channel_name = channel_name; data[spend_key] end
|
9
|
+
def start_time channel_name; @channel_name = channel_name; data[start_key] end
|
10
|
+
|
11
|
+
# 只有两种状态,不管一个用户有多少个连接
|
12
|
+
# 1, 没开始: 设置started_at为当前值
|
13
|
+
# 2, 关闭所有连接: 去除started_at,更新spended
|
14
|
+
# 再添加一个连接: 在调用接口方实现
|
15
|
+
def start? channel_name
|
16
|
+
@channel_name = channel_name
|
17
|
+
!!data[start_key]
|
18
|
+
end
|
19
|
+
def start channel_name
|
20
|
+
@channel_name = channel_name
|
21
|
+
data[start_key] = Time.now.strftime(TimeFormat)
|
22
|
+
resave!
|
23
|
+
end
|
24
|
+
def start_key; "#{@channel_name}_started_at"; end
|
25
|
+
def spend_key; "#{@channel_name}_spended"; end
|
26
|
+
|
27
|
+
def spend_time_in_realtime channel_name
|
28
|
+
_old_spend_time = self.spend_time(channel_name).to_i
|
29
|
+
|
30
|
+
# 计时为0。 用户打开多个浏览器,在完成课时时其他页面没有关掉,导致faye_online_time不能关闭计时,所以无法计算spend_time
|
31
|
+
_new_spend_time = 0
|
32
|
+
if _start_time = self.start_time(channel_name)
|
33
|
+
# 兼容 没访问页面前 计时的start_time还没有开始
|
34
|
+
_new_spend_time = _start_time.blank? ? 0 : (Time.now - Time.parse(_start_time))
|
35
|
+
end
|
36
|
+
|
37
|
+
(_old_spend_time + _new_spend_time).round(0)
|
38
|
+
end
|
39
|
+
|
40
|
+
def stop channel_name
|
41
|
+
@channel_name = channel_name
|
42
|
+
if data[start_key]
|
43
|
+
data[spend_key] ||= 0
|
44
|
+
data[spend_key] += (Time.now - Time.parse(data[start_key])).to_i
|
45
|
+
data.delete start_key
|
46
|
+
resave!
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
def data
|
51
|
+
@data ||= (JSON.parse(online_time.online_times) rescue {})
|
52
|
+
end
|
53
|
+
def resave!
|
54
|
+
online_time.update_attributes! :online_times => data.to_json
|
55
|
+
end
|
56
|
+
|
57
|
+
def online_time; self end
|
58
|
+
def uid; self.user_id; end
|
59
|
+
|
60
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
class FayeCreateUserList < ActiveRecord::Migration
|
2
|
+
def change
|
3
|
+
create_table :faye_channels, :options => 'ENGINE=MyISAM DEFAULT CHARSET=utf8' do |t|
|
4
|
+
t.string :name
|
5
|
+
t.timestamps
|
6
|
+
end
|
7
|
+
add_index :faye_channels, [:name], :unique => true
|
8
|
+
|
9
|
+
create_table :faye_channel_online_lists, :options => 'ENGINE=Innodb DEFAULT CHARSET=utf8' do |t|
|
10
|
+
t.integer :channel_id, :default => 0
|
11
|
+
t.text :user_info_list
|
12
|
+
t.integer :lock_version, :default => 0
|
13
|
+
t.timestamps
|
14
|
+
end
|
15
|
+
add_index :faye_channel_online_lists, [:channel_id]
|
16
|
+
|
17
|
+
create_table :faye_user_online_times, :options => 'ENGINE=Innodb DEFAULT CHARSET=utf8' do |t|
|
18
|
+
t.integer :user_id, :default => 0
|
19
|
+
t.text :online_times
|
20
|
+
t.integer :lock_version, :default => 0
|
21
|
+
t.timestamps
|
22
|
+
end
|
23
|
+
add_index :faye_user_online_times, [:user_id], :unique => true
|
24
|
+
|
25
|
+
end
|
26
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
class AddFayeUserLoginLogs < ActiveRecord::Migration
|
2
|
+
def up
|
3
|
+
create_table :faye_user_login_logs, :options => 'ENGINE=Innodb DEFAULT CHARSET=utf8', :id => false do |t|
|
4
|
+
t.integer :status, :limit => 2
|
5
|
+
t.integer :uid
|
6
|
+
t.datetime :t
|
7
|
+
t.integer :channel_id
|
8
|
+
t.string :clientId
|
9
|
+
end
|
10
|
+
add_index :faye_user_login_logs, [:t, :channel_id, :uid], :name => 'idx_all'
|
11
|
+
end
|
12
|
+
|
13
|
+
def down
|
14
|
+
end
|
15
|
+
end
|
data/faye-online.gemspec
ADDED
@@ -0,0 +1,29 @@
|
|
1
|
+
Gem::Specification.new do |s|
|
2
|
+
s.name = 'faye-online'
|
3
|
+
s.version = '0.1'
|
4
|
+
s.date = '2013-03-26'
|
5
|
+
s.summary = File.read("README.markdown").split(/===+/)[0].strip
|
6
|
+
s.description = s.summary
|
7
|
+
s.authors = ["David Chen"]
|
8
|
+
s.email = 'mvjome@gmail.com'
|
9
|
+
s.homepage = 'https://github.com/eoecn/faye-online'
|
10
|
+
|
11
|
+
s.add_dependency "json"
|
12
|
+
s.add_dependency "rails"
|
13
|
+
s.add_dependency "activesupport"
|
14
|
+
s.add_dependency "activerecord_idnamecache"
|
15
|
+
s.add_dependency "thin"
|
16
|
+
s.add_dependency "rainbows"
|
17
|
+
s.add_dependency "redis"
|
18
|
+
s.add_dependency "faye"
|
19
|
+
s.add_dependency "faye-redis"
|
20
|
+
s.add_dependency "cross_time_calculation"
|
21
|
+
|
22
|
+
s.add_development_dependency 'sqlite3'
|
23
|
+
s.add_development_dependency 'combustion'
|
24
|
+
s.add_development_dependency 'pry-debugger'
|
25
|
+
s.add_development_dependency 'fakeredis'
|
26
|
+
|
27
|
+
s.files = `git ls-files`.split("\n")
|
28
|
+
|
29
|
+
end
|
data/lib/faye-online.rb
ADDED
@@ -0,0 +1,85 @@
|
|
1
|
+
# encoding: UTF-8
|
2
|
+
|
3
|
+
require 'yaml'
|
4
|
+
require 'thin'
|
5
|
+
require 'json'
|
6
|
+
require 'rails'
|
7
|
+
require 'active_record'
|
8
|
+
require 'activerecord_idnamecache'
|
9
|
+
require 'redis'
|
10
|
+
require "faye"
|
11
|
+
require 'faye/redis'
|
12
|
+
|
13
|
+
(proc do
|
14
|
+
Faye::WebSocket.load_adapter('thin')
|
15
|
+
# Faye::WebSocket.load_adapter('rainbows')
|
16
|
+
if ENV['DEBUG_FAYE']
|
17
|
+
Faye::Logging.log_level = :debug
|
18
|
+
require 'logger'
|
19
|
+
_logger = Logger.new("log/faye.log")
|
20
|
+
Faye.logger = lambda {|m| _logger.info m }
|
21
|
+
end
|
22
|
+
|
23
|
+
# connect to database
|
24
|
+
database_yml = ENV['database_yml'] || File.join(ENV['RAILS_PATH'] || `pwd`.strip, 'config/database.yml')
|
25
|
+
ActiveRecord::Base.establish_connection YAML.load_file(database_yml).inject({}) {|h, kv| h[kv[0].to_sym] = kv[1]; h }[:production]
|
26
|
+
end).call
|
27
|
+
|
28
|
+
class FayeOnline
|
29
|
+
cattr_accessor :engine_proxy, :redis
|
30
|
+
|
31
|
+
ValidChannel = proc {|channel| !!channel.to_s.match(/\A[0-9a-z\/]+\Z/i) } # 只支持数字字母和斜杠
|
32
|
+
MONITORED_CHANNELS = ['/meta/connect', '/meta/disconnect'] # '/meta/subscribe', '/connect', '/close' are ignored
|
33
|
+
LOCAL_IP = Socket.ip_address_list.detect{|intf| intf.ipv4_private?}.ip_address # http://stackoverflow.com/questions/5029427/ruby-get-local-ip-nix
|
34
|
+
LOCAL_FAYE_URI = URI.parse("http://#{LOCAL_IP}:#{ENV['FAYE_PORT'] || 9292}/faye")
|
35
|
+
cattr_accessor :redis_opts, :faye_client, :valid_message_proc
|
36
|
+
|
37
|
+
def initialize redis_opts, valid_message_proc = nil
|
38
|
+
|
39
|
+
raise "Please run `$faye_server = FayeOnline.get_server` first, cause we have to bind disconnect event." if not $faye_server.is_a?(Faye::RackAdapter)
|
40
|
+
FayeOnline.redis_opts = redis_opts
|
41
|
+
FayeOnline.valid_message_proc = valid_message_proc || (proc {|message| true })
|
42
|
+
FayeOnline.redis = Redis.new(FayeOnline.redis_opts)
|
43
|
+
FayeOnline.redis.select FayeOnline.redis_opts[:database]
|
44
|
+
|
45
|
+
FayeOnline.faye_client ||= Faye::Client.new(LOCAL_FAYE_URI.to_s)
|
46
|
+
|
47
|
+
# 配置ActiveRecord
|
48
|
+
if Rails.root.nil?
|
49
|
+
Dir[File.expand_path('../../app/models/*.rb', __FILE__)].each {|f| require f }
|
50
|
+
end
|
51
|
+
|
52
|
+
return self
|
53
|
+
end
|
54
|
+
|
55
|
+
def incoming(message, callback)
|
56
|
+
Message.new(message).process
|
57
|
+
callback.call(message)
|
58
|
+
end
|
59
|
+
|
60
|
+
def self.channel_clientIds_array
|
61
|
+
array = []
|
62
|
+
FayeOnline.redis.keys("/#{FayeOnline.redis_opts[:namespace]}/uid_to_clientIds*").sort.each do |k|
|
63
|
+
_data = FayeOnline.redis.hgetall(k).values.map {|i| JSON.parse(i) rescue i }.flatten
|
64
|
+
array << [k, _data]
|
65
|
+
end
|
66
|
+
array
|
67
|
+
end
|
68
|
+
def self.uniq_clientIds
|
69
|
+
self.channel_clientIds_array.map(&:last).flatten.uniq
|
70
|
+
end
|
71
|
+
|
72
|
+
def self.disconnect clientId
|
73
|
+
message = {'channel' => '/meta/disconnect', 'clientId' => clientId}
|
74
|
+
|
75
|
+
# fake a client to disconnect, 仅仅接受message.auth为空,即网络失去连接的情况
|
76
|
+
FayeOnline::Message.new(message.merge('fake' => 'true')).process if not message['auth']
|
77
|
+
end
|
78
|
+
|
79
|
+
end
|
80
|
+
|
81
|
+
|
82
|
+
require File.expand_path("../faye-online/message.rb", __FILE__)
|
83
|
+
require File.expand_path("../faye-online/rails_engine.rb", __FILE__)
|
84
|
+
require File.expand_path("../faye-online/status.rb", __FILE__)
|
85
|
+
require File.expand_path("../faye-online/server.rb", __FILE__)
|
@@ -0,0 +1,183 @@
|
|
1
|
+
# encoding: UTF-8
|
2
|
+
|
3
|
+
# 管理用户在线文档
|
4
|
+
# 一个用户在单个房间单个多个页面,就有了多个clientId。
|
5
|
+
# 1. 用户在线状态规则
|
6
|
+
# 在线: 超过一个clientId
|
7
|
+
# 离线: 零个clientId
|
8
|
+
# 2. 清理过期clientId,以防止存储在redis Hashes里的数组无法删除个别元素。因为怀疑为过期clientId之后就没有连接,所以也不会进行任何操作。
|
9
|
+
# 采用策略是放到一个clientId和开始时间对应的redis Hashes里,在用户'/meta/disconnect'时检测各个clientId有效性
|
10
|
+
|
11
|
+
class FayeOnline
|
12
|
+
# To allow multiple messages process in their own Message instance.
|
13
|
+
class Message
|
14
|
+
|
15
|
+
attr_accessor :message
|
16
|
+
def initialize _msg
|
17
|
+
self.message = _msg
|
18
|
+
self.current_channel # load message.auth
|
19
|
+
self
|
20
|
+
end
|
21
|
+
|
22
|
+
def process
|
23
|
+
@time_begin = Time.now
|
24
|
+
|
25
|
+
# 主动检测是否离开,以减少GC的消耗
|
26
|
+
# TODO re-implement autodisonnect, pull request to faye.gem
|
27
|
+
if message['channel'] == "/faye_online/before_leave"
|
28
|
+
EM.run do
|
29
|
+
EM.add_timer(3) do
|
30
|
+
FayeOnline.disconnect(current_clientId) if not FayeOnline.engine_proxy.has_connection?(current_clientId)
|
31
|
+
end
|
32
|
+
end
|
33
|
+
return false;
|
34
|
+
end
|
35
|
+
|
36
|
+
# 验证是否用FayeOnline处理
|
37
|
+
step_idx = MONITORED_CHANNELS.index(message['channel'])
|
38
|
+
return false if step_idx.nil?
|
39
|
+
|
40
|
+
# 验证配置参数是否正确
|
41
|
+
return false if !(message['auth'] && (room_channel && time_channel && current_user))
|
42
|
+
raise "#{current_user.inspect} is invalid, the correct data struct is, .e.g. {uid: 470700, uname: 'mvj3'}" if !(current_user["uid"] && current_user["uname"])
|
43
|
+
|
44
|
+
# 验证渠道名字是否合法
|
45
|
+
(puts "invalid channel => #{message.inspect}" if ENV['DEBUG']; return false) if !(ValidChannel.call(message['auth']['room_channel']) && ValidChannel.call(message['auth']['time_channel']))
|
46
|
+
# 验证message是否合法
|
47
|
+
(puts "invalid message => #{message.inspect}" if ENV['DEBUG']; return false) if not FayeOnline.valid_message_proc.call(message)
|
48
|
+
|
49
|
+
begin
|
50
|
+
case step_idx
|
51
|
+
|
52
|
+
# A. 处理*开启*一个客户端
|
53
|
+
when 0
|
54
|
+
### 处理 room_channel 在线人数
|
55
|
+
# *开始1* add 当前用户的clientIds数组
|
56
|
+
current_user_in_current_room__clientIds.add(current_clientId)
|
57
|
+
online_list.add current_user['uid']
|
58
|
+
|
59
|
+
### 处理 time_chanel 在线时长
|
60
|
+
current_user_in_current_time__clientIds.add(current_clientId)
|
61
|
+
online_time.start time_channel if not online_time.start? time_channel
|
62
|
+
logger_online_info "连上"
|
63
|
+
|
64
|
+
# 绑定用户数据到clientId,以供服务器在主动disconnect时使用
|
65
|
+
|
66
|
+
# B. 处理*关闭*一个客户端(,但是这个用户可能还有其他客户端在连着)
|
67
|
+
when 1
|
68
|
+
# 解除 因意外原因导致的 clientId 没有过期
|
69
|
+
current_user_current_clientIds.each do |_clientId|
|
70
|
+
str = if FayeOnline.engine_proxy.has_connection?(_clientId)
|
71
|
+
"clientId[#{_clientId}] 还有连接"
|
72
|
+
else
|
73
|
+
[current_user_current_clientIds_arrays, current_user_in_current_time__clientIds].each {|a| a.delete _clientId }
|
74
|
+
"clientId[#{_clientId}] 没有连接的无效 已被删除"
|
75
|
+
end
|
76
|
+
puts str if ENV['DEBUG']
|
77
|
+
end
|
78
|
+
puts if ENV['DEBUG']
|
79
|
+
|
80
|
+
### 处理 room_channel 在线人数
|
81
|
+
# *开始2* delete 当前用户的clientIds数组
|
82
|
+
current_user_in_current_room__clientIds.delete(current_clientId)
|
83
|
+
online_list.delete current_user['uid'] if current_user_in_current_room__clientIds.blank? # 一个uid的全部clientId都退出了
|
84
|
+
|
85
|
+
### 处理 time_chanel 在线时长
|
86
|
+
# 关闭定时在线时间
|
87
|
+
current_user_in_current_time__clientIds.delete(current_clientId)
|
88
|
+
online_time.stop time_channel if current_user_in_current_time__clientIds.size.zero?
|
89
|
+
logger_online_info "离开"
|
90
|
+
end
|
91
|
+
|
92
|
+
rescue => e # 确保每次都正常存储current_user_in_current_room__clientIds
|
93
|
+
puts [e, e.backtrace].flatten.join("\n")
|
94
|
+
end
|
95
|
+
|
96
|
+
# *结束* save 当前用户的clientIds数组
|
97
|
+
FayeOnline.redis.hset redis_key__room, current_user['uid'], current_user_in_current_room__clientIds.to_json
|
98
|
+
FayeOnline.redis.hset redis_key__time, current_user['uid'], current_user_in_current_time__clientIds.to_json
|
99
|
+
|
100
|
+
# 发布在线用户列表
|
101
|
+
FayeOnline.faye_client.publish("#{room_channel}/user_list", {'count' => online_list.user_count, 'user_list' => online_list.user_list}) if FayeOnline.faye_client
|
102
|
+
|
103
|
+
puts "本次处理处理时间 #{((Time.now - @time_begin) * 1000).round(2)}ms" if ENV['DEBUG']
|
104
|
+
puts message.inspect
|
105
|
+
puts
|
106
|
+
return true
|
107
|
+
end
|
108
|
+
|
109
|
+
def logger_online_info status
|
110
|
+
_t = online_time.start_time(time_channel)
|
111
|
+
_start_time_str_ = _t ? Time.parse(_t).strftime("%Y-%m-%d %H:%M:%S") : nil
|
112
|
+
puts "*#{status}* 用户#{current_user['uname']}[#{current_user['uid']}] 的clientId #{current_clientId}。"
|
113
|
+
puts "当前用户在 #{redis_key__room} 的clientIds列表为 #{current_user_in_current_room__clientIds.inspect}。在线用户有#{online_list.user_list.count}个,他们是 #{online_list.user_list.inspect}"
|
114
|
+
puts "当前用户在 #{redis_key__time} 的clientIds列表为 #{current_user_in_current_time__clientIds.inspect}。开始时间为#{_start_time_str_}, 渡过时间为 #{online_time.spend_time(time_channel) || '空'}。"
|
115
|
+
|
116
|
+
# 记录用户登陆登出情况,方便之后追踪
|
117
|
+
_s = {"连上" => 1, "离开" => 0}[status]
|
118
|
+
# 用可以过期的Redis键值对来检测单个clientId上次是否为 "连上" 或 "离开"
|
119
|
+
_k = "/#{FayeOnline.redis_opts[:namespace]}/clientId_status/#{current_clientId}"
|
120
|
+
_s_old = FayeOnline.redis.get(_k).to_s
|
121
|
+
# *连上*和*离开* 只能操作一次
|
122
|
+
if _s_old.blank? || # 没登陆的
|
123
|
+
(_s.to_s != _s_old) # 已登陆的
|
124
|
+
|
125
|
+
# 把不*连上*和*离开*把放一张表,写时不阻塞
|
126
|
+
FayeUserLoginLog.create! :channel_id => FayeChannel[time_channel], :uid => current_user['uid'], :t => DateTime.now, :status => _s, :clientId => current_clientId
|
127
|
+
|
128
|
+
FayeOnline.redis.multi do
|
129
|
+
FayeOnline.redis.set(_k, _s)
|
130
|
+
FayeOnline.redis.expire(_k, 2.weeks)
|
131
|
+
end
|
132
|
+
end
|
133
|
+
end
|
134
|
+
def online_list; FayeChannelOnlineList.find_or_create_by_channel_id(FayeChannel[room_channel]) end
|
135
|
+
def online_time; FayeUserOnlineTime.find_or_create_by_user_id(current_user['uid']) end
|
136
|
+
|
137
|
+
# 渠道信息
|
138
|
+
def current_channel
|
139
|
+
# 从clientId反设置auth信息,并只设置一次
|
140
|
+
if message['auth'] && !message['_is_auth_load']
|
141
|
+
FayeOnline.redis.multi do
|
142
|
+
FayeOnline.redis.set(redis_key__auth, message['auth'].to_json)
|
143
|
+
FayeOnline.redis.expire(redis_key__auth, 2.weeks)
|
144
|
+
end
|
145
|
+
message['_is_auth_load'] = true
|
146
|
+
else
|
147
|
+
message['auth'] ||= JSON.parse(FayeOnline.redis.get(redis_key__auth)) rescue {}
|
148
|
+
end
|
149
|
+
message['channel']
|
150
|
+
end
|
151
|
+
def time_channel; message['auth']['time_channel'] end
|
152
|
+
def room_channel; message['auth']['room_channel'] end
|
153
|
+
|
154
|
+
# 用户信息
|
155
|
+
def current_user; message['auth']['current_user'] end
|
156
|
+
def current_clientId; message['clientId'] end
|
157
|
+
|
158
|
+
# room和time 分别对应 clientId 的关系
|
159
|
+
def current_user_in_current_room__clientIds
|
160
|
+
@_current_user_in_current_room__clientIds ||= begin
|
161
|
+
_a = JSON.parse(FayeOnline.redis.hget(redis_key__room, current_user['uid'])) rescue []
|
162
|
+
Set.new(_a)
|
163
|
+
end
|
164
|
+
end
|
165
|
+
def current_user_in_current_time__clientIds
|
166
|
+
@_current_user_in_current_time__clientIds ||= begin
|
167
|
+
_a = JSON.parse(FayeOnline.redis.hget(redis_key__time, current_user['uid'])) rescue []
|
168
|
+
Set.new(_a)
|
169
|
+
end
|
170
|
+
end
|
171
|
+
def redis_key__room; "/#{FayeOnline.redis_opts[:namespace]}/uid_to_clientIds#{room_channel}" end
|
172
|
+
def redis_key__time; "/#{FayeOnline.redis_opts[:namespace]}/uid_to_clientIds#{time_channel}" end
|
173
|
+
def redis_key__auth; "/#{FayeOnline.redis_opts[:namespace]}/clientId_auth/#{current_clientId}" end
|
174
|
+
def current_user_current_clientIds_arrays
|
175
|
+
[current_user_in_current_room__clientIds, current_user_in_current_time__clientIds]
|
176
|
+
end
|
177
|
+
def current_user_current_clientIds
|
178
|
+
current_user_current_clientIds_arrays.map(&:to_a).flatten.uniq
|
179
|
+
end
|
180
|
+
|
181
|
+
end
|
182
|
+
|
183
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
# encoding: UTF-8
|
2
|
+
|
3
|
+
module FayeOnline_Rails
|
4
|
+
class Engine < Rails::Engine
|
5
|
+
initializer "faye_online_rails.load_app_instance_data" do |app|
|
6
|
+
app.class.configure do
|
7
|
+
['db/migrate', 'app/assets', 'app/models', 'app/controllers', 'app/views'].each do |path|
|
8
|
+
config.paths[path] ||= []
|
9
|
+
config.paths[path] += QA_Rails::Engine.paths[path].existent
|
10
|
+
end
|
11
|
+
end
|
12
|
+
end
|
13
|
+
initializer "faye_online_rails.load_static_assets" do |app|
|
14
|
+
app.middleware.use ::ActionDispatch::Static, "#{root}/public"
|
15
|
+
end
|
16
|
+
rake_tasks do
|
17
|
+
load File.expand_path('../../../Rakefile', __FILE__)
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,60 @@
|
|
1
|
+
# encoding: UTF-8
|
2
|
+
|
3
|
+
def FayeOnline.get_server redis_opts, valid_message_proc = nil
|
4
|
+
$faye_server = Faye::RackAdapter.new(
|
5
|
+
:mount => '/faye',
|
6
|
+
|
7
|
+
# the maximum time to hold a connection open before returning the response. This is given in seconds and must be smaller than the timeout on your frontend webserver(thin). Faye uses Thin as its webserver, whose default timeout is 30 seconds.
|
8
|
+
# https://groups.google.com/forum/?fromgroups#!topic/faye-users/DvFrPGOinKw
|
9
|
+
:timeout => 10,
|
10
|
+
|
11
|
+
# [https://groups.google.com/forum/#!searchin/faye-users/disconnect/faye-users/2bn8xUHF5-E/A4a3Sk7RgW4J] It's expected. The browser will not always be able to deliver an explicit disconnect message, which is why there is server-side idle client detection.
|
12
|
+
# garbag collect disconnected clientIds in EventMachine.add_periodic_timer
|
13
|
+
:engine => {:gc => 60}.merge(redis_opts.merge(:type => Faye::Redis)),
|
14
|
+
|
15
|
+
:ping => 30 # (optional) how often, in seconds, to send keep-alive ping messages over WebSocket and EventSource connections. Use this if your Faye server will be accessed through a proxy that kills idle connections.
|
16
|
+
)
|
17
|
+
|
18
|
+
$faye_server.bind(:handshake) do |clientId|
|
19
|
+
end
|
20
|
+
$faye_server.bind(:subscribe) do |clientId, channel|
|
21
|
+
end
|
22
|
+
$faye_server.bind(:unsubscribe) do |clientId, channel|
|
23
|
+
end
|
24
|
+
$faye_server.bind(:publish) do |clientId, channel, data|
|
25
|
+
end
|
26
|
+
$faye_server.bind(:disconnect) do |clientId|
|
27
|
+
FayeOnline.disconnect clientId
|
28
|
+
|
29
|
+
# dynamic compute interval seconds
|
30
|
+
tmp = FayeOnline.channel_clientIds_array.reject {|i| i[1].blank? }
|
31
|
+
# delete below, cause map data is valid
|
32
|
+
if tmp.any?
|
33
|
+
puts "开始有 #{FayeOnline.uniq_clientIds.count}个"
|
34
|
+
tmp.map(&:last).flatten.uniq.shuffle[0..19].each do |_clientId|
|
35
|
+
if not FayeOnline.engine_proxy.has_connection? _clientId
|
36
|
+
puts "开始处理无效 #{_clientId}"
|
37
|
+
# 1. 先伪装去disconnect clientId
|
38
|
+
# 2. 如果失败,就直接操作redis修改
|
39
|
+
if not FayeOnline.disconnect(_clientId)
|
40
|
+
# 没删除成功,因为之前没有设置auth
|
41
|
+
k = (tmp.detect {|a, b| b.index(_clientId) } || [])[0]
|
42
|
+
# 直接从redis清除无效_clientId
|
43
|
+
FayeOnline.redis.hgetall(k).each do |k2, v2|
|
44
|
+
v3 = JSON.parse(v2) rescue []
|
45
|
+
v3.delete _clientId
|
46
|
+
FayeOnline.redis.hset(k, k2, v3.to_json)
|
47
|
+
end if k
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
puts "结束有 #{FayeOnline.uniq_clientIds.count}个"
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
$faye_server.add_extension FayeOnline.new(redis_opts, valid_message_proc)
|
56
|
+
|
57
|
+
FayeOnline.engine_proxy = $faye_server.instance_variable_get("@server").engine
|
58
|
+
|
59
|
+
return $faye_server
|
60
|
+
end
|
@@ -0,0 +1,79 @@
|
|
1
|
+
# encoding: UTF-8
|
2
|
+
|
3
|
+
def FayeOnline.status
|
4
|
+
array = []
|
5
|
+
clientIds = Set.new
|
6
|
+
channel_clientIds_array = FayeOnline.channel_clientIds_array
|
7
|
+
|
8
|
+
clientId_to_users = channel_clientIds_array.map(&:last).flatten.uniq.inject({}) do |h, _clientId|
|
9
|
+
_data = JSON.parse(Redis.current.get("/#{FayeOnline.redis_opts[:namespace]}/clientId_auth/#{_clientId}")) rescue {}
|
10
|
+
h[_clientId] = (_data['current_user'] || {}).except('uhash').values
|
11
|
+
h
|
12
|
+
end
|
13
|
+
|
14
|
+
channel_clientIds_array.each do |_channel, _clientIds|
|
15
|
+
_a = _clientIds.map {|i| [i, clientId_to_users[i]] }
|
16
|
+
_c = _a.map {|i| i[1][0] }.uniq.count
|
17
|
+
array << "#{_channel}: #{_c}个用户: #{_a}"
|
18
|
+
_clientIds.each {|i| clientIds.add i }
|
19
|
+
end
|
20
|
+
|
21
|
+
array.unshift ""
|
22
|
+
users = clientIds.map {|i| clientId_to_users[i] }.uniq {|i| i[0] }
|
23
|
+
array.unshift "/classes/[0-9]+ 用于班级讨论的消息通讯, /courses/[0-9]+/lessons/[0-9]+ 用于课时的计时"
|
24
|
+
array.unshift ""
|
25
|
+
array.unshift "#{users.count}个用户分别是: #{users}"
|
26
|
+
array.unshift ""
|
27
|
+
array.unshift "实时在线clientIds总共有#{clientIds.count}个: #{clientIds.to_a}"
|
28
|
+
array.unshift ""
|
29
|
+
array.unshift "一个clientId表示用户打开了一个页面。一个用户在同一课时可能打开多个页面,那就是一个user,多个clientId"
|
30
|
+
|
31
|
+
# 删除意外没有退出的在线用户列表
|
32
|
+
uids = users.map(&:first)
|
33
|
+
FayeChannelOnlineList.all.reject {|i| i.data.blank? }.each do |online_list|
|
34
|
+
online_list.user_list.each do |uid|
|
35
|
+
online_list.delete uid if not uids.include? uid
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
array
|
40
|
+
end
|
41
|
+
|
42
|
+
|
43
|
+
def FayeOnline.log params, user_name_proc = proc {|uid| uid }
|
44
|
+
scope = FayeUserLoginLog.order("t ASC")
|
45
|
+
|
46
|
+
# 个人整理后列出
|
47
|
+
if params[:user]
|
48
|
+
scope = scope.where(:uid => user_name_proc.call(params[:user]))
|
49
|
+
|
50
|
+
channel_to_logs = scope.inject({}) {|h, log| i = FayeChannel[log.channel_id]; h[i] ||= []; h[i] << log; h }
|
51
|
+
|
52
|
+
array = ["用户 #{params[:user]}[#{user_name_proc.call(params[:user])}] 的登陆日志详情"]
|
53
|
+
channel_to_logs.each do |channel, logs|
|
54
|
+
array << ''
|
55
|
+
array << channel
|
56
|
+
logs2 = logs.inject({}) {|h, log| h[log.clientId] ||= []; h[log.clientId] << log; h }
|
57
|
+
|
58
|
+
# 合并交叉的时间
|
59
|
+
ctc = CrossTimeCalculation.new
|
60
|
+
logs2.each do |clientId, _logs|
|
61
|
+
# logs = logs.sort {|a, b| (a && a.t) <=> (b && b.t) }
|
62
|
+
if _logs.size > 0
|
63
|
+
# binding.pry if _logs[1].nil?
|
64
|
+
te = _logs[1] ? _logs[1].t : nil
|
65
|
+
ctc.add(_logs[0].t, te)
|
66
|
+
_time_passed = CrossTimeCalculation.new.add(_logs[0].t, te).total_seconds.to_i
|
67
|
+
end
|
68
|
+
array << [clientId, _logs.map {|_log| "#{_log.status}: #{_log.t.strftime("%Y-%m-%d %H:%M:%S")}" }, "#{_time_passed || '未知'}秒"].flatten.compact.inspect
|
69
|
+
end
|
70
|
+
array << "共用时 #{ctc.total_seconds.to_i}秒"
|
71
|
+
end
|
72
|
+
array
|
73
|
+
# 群体直接列出日志
|
74
|
+
else
|
75
|
+
scope.limit(500).map do |log|
|
76
|
+
[%w[离开 连上][log.status], log.uid, user_name_proc.call(log.uid), log.t.strftime("%Y-%m-%d %H:%M:%S"), FayeChannel[log.channel_id], log.clientId].inspect
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
data/rainbows.conf
ADDED
@@ -0,0 +1,34 @@
|
|
1
|
+
# encoding: UTF-8
|
2
|
+
|
3
|
+
# DEBUG_FAYE=true DEBUG=true bundle exec rainbows faye.ru -c /Users/mvj3/github/eoecn/faye-online/rainbows.conf -E production -p 9292
|
4
|
+
|
5
|
+
Rainbows! do
|
6
|
+
####################
|
7
|
+
### concurrency model to use
|
8
|
+
#
|
9
|
+
# WebSocket connection to 'ws://10.0.0.65:9292/faye' failed: No response code found: HTTP/1.1 -1 http://10.0.0.65:8003/courses/5/lessons/1
|
10
|
+
# WebSocket connection to 'ws://10.0.0.65:9292/faye' failed: WebSocket is closed before the connection is established.
|
11
|
+
# disconnect directly
|
12
|
+
# use :ThreadSpawn
|
13
|
+
#
|
14
|
+
# 错误: XMLHttpRequest cannot load http://10.0.0.65:9292/faye. Origin http://10.0.0.65:8003 is not allowed by Access-Control-Allow-Origin.
|
15
|
+
# use :FiberSpawn
|
16
|
+
#
|
17
|
+
# the client connect to server looply by Faye.Transport.CORS in faye.client.js
|
18
|
+
# {"channel"=>"/meta/connect", "clientId"=>"ekf8100ih4z04jiy2oo7l46qcud6wai", "connectionType"=>"cross-origin-long-polling", "id"=>"bi", "auth"=>{"room_channel"=>"/classes/7", "time_channel"=>"/courses/5/lessons/1", "current_user"=>{"uid"=>557558, "uname"=>"yiyou99storm", "uhash"=>""}}}
|
19
|
+
use :EventMachine
|
20
|
+
####################
|
21
|
+
|
22
|
+
|
23
|
+
worker_connections 500
|
24
|
+
keepalive_timeout 5 # zero disables keepalives entirely
|
25
|
+
client_max_body_size 5*1024*1024 # 5 megabytes
|
26
|
+
keepalive_requests 666 # default:100
|
27
|
+
client_header_buffer_size 2 * 1024 # 2 kilobytes
|
28
|
+
|
29
|
+
end
|
30
|
+
|
31
|
+
# the rest of the Unicorn configuration...
|
32
|
+
worker_processes 2 # CPU cores
|
33
|
+
# stderr_path "/path/to/error.log"
|
34
|
+
# stdout_path "/path/to/output.log"
|
data/test/database.yml
ADDED
@@ -0,0 +1,110 @@
|
|
1
|
+
# encoding: UTF-8
|
2
|
+
|
3
|
+
ENV['database_yml'] = File.expand_path('../database.yml', __FILE__)
|
4
|
+
|
5
|
+
require 'test/unit'
|
6
|
+
require 'pry-debugger'
|
7
|
+
require 'active_support/core_ext'
|
8
|
+
|
9
|
+
require 'faye-online'
|
10
|
+
require 'fake_redis'
|
11
|
+
|
12
|
+
|
13
|
+
class TestFayeOnline < Test::Unit::TestCase
|
14
|
+
# setup database
|
15
|
+
Dir['db/migrate/*'].map {|i| eval File.read(i).gsub(/ENGINE=(MyISAM|Innodb) DEFAULT CHARSET=utf8/i, "") } # support sqlite
|
16
|
+
# migrate
|
17
|
+
FayeCreateUserList.new.change
|
18
|
+
AddFayeUserLoginLogs.new.up
|
19
|
+
|
20
|
+
# setup redis & faye server
|
21
|
+
redis_opts = {:host=>"localhost", :port=>50000, :database=>1, :namespace=>"faye"}
|
22
|
+
$faye_server = FayeOnline.get_server redis_opts
|
23
|
+
FayeOnline.faye_client = nil # dont publish from server to client
|
24
|
+
|
25
|
+
def setup
|
26
|
+
@message_connect = {"channel"=>"/meta/connect", "clientId"=>"sqq4oxlwhj84zw92n0e592j8iq989yy", "id"=>"7", "auth"=>{"room_channel"=>"/classes/4", "time_channel"=>"/courses/5/lessons/1", "current_user"=>{"uid"=>470700, "uname"=>"mvj3"}}}
|
27
|
+
@message_connect_1 = {"channel"=>"/meta/connect", "clientId"=>"qqq4oxlwhj84zw92n0e592j8iq989yy", "id"=>"8", "auth"=>{"room_channel"=>"/classes/4", "time_channel"=>"/courses/5/lessons/1", "current_user"=>{"uid"=>1, "uname"=>"admin"}}}
|
28
|
+
@message_connect_2 = {"channel"=>"/meta/connect", "clientId"=>"pqq4oxlwhj84zw92n0e592j8iq989yy", "id"=>"9", "auth"=>{"room_channel"=>"/classes/4", "time_channel"=>"/courses/5/lessons/1", "current_user"=>{"uid"=>2, "uname"=>"iceskysl"}}}
|
29
|
+
|
30
|
+
@room_channel_id = FayeChannel[@message_connect['auth']['room_channel']]
|
31
|
+
end
|
32
|
+
|
33
|
+
def test_validate_message
|
34
|
+
msg = @message_connect.dup
|
35
|
+
msg['auth']['current_user'].delete 'uid'
|
36
|
+
assert_raise RuntimeError do
|
37
|
+
FayeOnline::Message.new(msg).process
|
38
|
+
end
|
39
|
+
|
40
|
+
msg = @message_connect.dup
|
41
|
+
msg['auth'].delete('room_channel')
|
42
|
+
assert_not_equal FayeOnline::Message.new(msg).process, true
|
43
|
+
end
|
44
|
+
|
45
|
+
def test_one_user
|
46
|
+
# login
|
47
|
+
FayeOnline::Message.new(@message_connect).process
|
48
|
+
assert_equal FayeOnline.channel_clientIds_array.flatten.count("sqq4oxlwhj84zw92n0e592j8iq989yy"), 2, "Login clientId should have two `sqq4oxlwhj84zw92n0e592j8iq989yy`"
|
49
|
+
|
50
|
+
# relogin the same clientId
|
51
|
+
FayeOnline::Message.new(@message_connect).process
|
52
|
+
assert_equal FayeOnline.channel_clientIds_array.flatten.count("sqq4oxlwhj84zw92n0e592j8iq989yy"), 2, "Login clientId should have two `sqq4oxlwhj84zw92n0e592j8iq989yy`"
|
53
|
+
assert_equal FayeUserLoginLog.where(:clientId => "sqq4oxlwhj84zw92n0e592j8iq989yy").count, 1, "Login at second time, but there should only one log"
|
54
|
+
|
55
|
+
assert_equal FayeChannelOnlineList.where(:channel_id => @room_channel_id).first.user_list, Set.new([470700]), "There should be only 470700 user"
|
56
|
+
|
57
|
+
# logout
|
58
|
+
msg = @message_connect.dup
|
59
|
+
msg["channel"] = "/meta/disconnect"
|
60
|
+
FayeOnline::Message.new(msg).process
|
61
|
+
assert_equal FayeOnline.channel_clientIds_array.flatten.count("sqq4oxlwhj84zw92n0e592j8iq989yy"), 0, "Login clientId should have none `sqq4oxlwhj84zw92n0e592j8iq989yy`"
|
62
|
+
assert_equal FayeUserLoginLog.where(:clientId => "sqq4oxlwhj84zw92n0e592j8iq989yy").count, 2, "Login twice, but there should only two log"
|
63
|
+
|
64
|
+
FayeUserLoginLog.delete_all
|
65
|
+
end
|
66
|
+
|
67
|
+
def test_more_user
|
68
|
+
# login id#1, id#470700
|
69
|
+
[@message_connect, @message_connect_1].each {|msg| FayeOnline::Message.new(msg).process }
|
70
|
+
assert_equal FayeUserLoginLog.where(:clientId => [@message_connect['clientId'], @message_connect_1['clientId']]).count, 2, "Two user login, and there should have two logs"
|
71
|
+
assert_equal FayeChannelOnlineList.where(:channel_id => @room_channel_id).first.user_list, Set.new([1, 470700]), "There should be two user"
|
72
|
+
|
73
|
+
# logout id#1
|
74
|
+
FayeOnline::Message.new(@message_connect_1.merge("channel" => "/meta/disconnect")).process
|
75
|
+
assert_equal FayeUserLoginLog.where(:clientId => [@message_connect['clientId'], @message_connect_1['clientId']]).count, 3, "two user logins, one of them logout, and there should have three logs"
|
76
|
+
assert_equal FayeChannelOnlineList.where(:channel_id => @room_channel_id).first.user_list, Set.new([470700]), "There should be one user"
|
77
|
+
|
78
|
+
# login id#1, id#2
|
79
|
+
[@message_connect_1, @message_connect_2].each {|msg| FayeOnline::Message.new(msg).process }
|
80
|
+
assert_equal FayeUserLoginLog.where(:clientId => [@message_connect['clientId'], @message_connect_1['clientId'], @message_connect_2['clientId']]).count, 5, "five logs"
|
81
|
+
assert_equal FayeChannelOnlineList.where(:channel_id => @room_channel_id).first.user_list, Set.new([1, 2, 470700]), "There should be three user"
|
82
|
+
|
83
|
+
# logout id#470700
|
84
|
+
FayeOnline::Message.new(@message_connect.merge("channel" => "/meta/disconnect")).process
|
85
|
+
assert_equal FayeUserLoginLog.where(:clientId => [@message_connect['clientId'], @message_connect_1['clientId'], @message_connect_2['clientId']]).count, 6, "six logs"
|
86
|
+
assert_equal FayeChannelOnlineList.where(:channel_id => @room_channel_id).first.user_list, Set.new([1, 2]), "There should be two user"
|
87
|
+
|
88
|
+
# logout id#1, #id2
|
89
|
+
FayeOnline::Message.new(@message_connect_1.merge("channel" => "/meta/disconnect")).process
|
90
|
+
FayeOnline::Message.new(@message_connect_2.merge("channel" => "/meta/disconnect")).process
|
91
|
+
assert_equal FayeUserLoginLog.where(:clientId => [@message_connect['clientId'], @message_connect_1['clientId'], @message_connect_2['clientId']]).count, 8, "eight logs"
|
92
|
+
assert_equal FayeChannelOnlineList.where(:channel_id => @room_channel_id).first.user_list, Set.new, "There should be none user"
|
93
|
+
|
94
|
+
FayeUserLoginLog.delete_all
|
95
|
+
end
|
96
|
+
|
97
|
+
def test_online_time
|
98
|
+
channel_name = 'test'
|
99
|
+
online_time = FayeUserOnlineTime.find_or_create_by_user_id(@message_connect['auth']['current_user']['uid'])
|
100
|
+
assert_equal online_time.start?(channel_name), false
|
101
|
+
|
102
|
+
online_time.start channel_name
|
103
|
+
assert_equal online_time.start?(channel_name), true
|
104
|
+
sleep 1
|
105
|
+
online_time.stop channel_name
|
106
|
+
assert_equal online_time.spend_time_in_realtime(channel_name).zero?, false
|
107
|
+
assert_equal online_time.spend_time(channel_name).zero?, false
|
108
|
+
end
|
109
|
+
|
110
|
+
end
|
metadata
ADDED
@@ -0,0 +1,288 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: faye-online
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: '0.1'
|
5
|
+
prerelease:
|
6
|
+
platform: ruby
|
7
|
+
authors:
|
8
|
+
- David Chen
|
9
|
+
autorequire:
|
10
|
+
bindir: bin
|
11
|
+
cert_chain: []
|
12
|
+
date: 2013-03-26 00:00:00.000000000 Z
|
13
|
+
dependencies:
|
14
|
+
- !ruby/object:Gem::Dependency
|
15
|
+
name: json
|
16
|
+
requirement: !ruby/object:Gem::Requirement
|
17
|
+
none: false
|
18
|
+
requirements:
|
19
|
+
- - ! '>='
|
20
|
+
- !ruby/object:Gem::Version
|
21
|
+
version: '0'
|
22
|
+
type: :runtime
|
23
|
+
prerelease: false
|
24
|
+
version_requirements: !ruby/object:Gem::Requirement
|
25
|
+
none: false
|
26
|
+
requirements:
|
27
|
+
- - ! '>='
|
28
|
+
- !ruby/object:Gem::Version
|
29
|
+
version: '0'
|
30
|
+
- !ruby/object:Gem::Dependency
|
31
|
+
name: rails
|
32
|
+
requirement: !ruby/object:Gem::Requirement
|
33
|
+
none: false
|
34
|
+
requirements:
|
35
|
+
- - ! '>='
|
36
|
+
- !ruby/object:Gem::Version
|
37
|
+
version: '0'
|
38
|
+
type: :runtime
|
39
|
+
prerelease: false
|
40
|
+
version_requirements: !ruby/object:Gem::Requirement
|
41
|
+
none: false
|
42
|
+
requirements:
|
43
|
+
- - ! '>='
|
44
|
+
- !ruby/object:Gem::Version
|
45
|
+
version: '0'
|
46
|
+
- !ruby/object:Gem::Dependency
|
47
|
+
name: activesupport
|
48
|
+
requirement: !ruby/object:Gem::Requirement
|
49
|
+
none: false
|
50
|
+
requirements:
|
51
|
+
- - ! '>='
|
52
|
+
- !ruby/object:Gem::Version
|
53
|
+
version: '0'
|
54
|
+
type: :runtime
|
55
|
+
prerelease: false
|
56
|
+
version_requirements: !ruby/object:Gem::Requirement
|
57
|
+
none: false
|
58
|
+
requirements:
|
59
|
+
- - ! '>='
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '0'
|
62
|
+
- !ruby/object:Gem::Dependency
|
63
|
+
name: activerecord_idnamecache
|
64
|
+
requirement: !ruby/object:Gem::Requirement
|
65
|
+
none: false
|
66
|
+
requirements:
|
67
|
+
- - ! '>='
|
68
|
+
- !ruby/object:Gem::Version
|
69
|
+
version: '0'
|
70
|
+
type: :runtime
|
71
|
+
prerelease: false
|
72
|
+
version_requirements: !ruby/object:Gem::Requirement
|
73
|
+
none: false
|
74
|
+
requirements:
|
75
|
+
- - ! '>='
|
76
|
+
- !ruby/object:Gem::Version
|
77
|
+
version: '0'
|
78
|
+
- !ruby/object:Gem::Dependency
|
79
|
+
name: thin
|
80
|
+
requirement: !ruby/object:Gem::Requirement
|
81
|
+
none: false
|
82
|
+
requirements:
|
83
|
+
- - ! '>='
|
84
|
+
- !ruby/object:Gem::Version
|
85
|
+
version: '0'
|
86
|
+
type: :runtime
|
87
|
+
prerelease: false
|
88
|
+
version_requirements: !ruby/object:Gem::Requirement
|
89
|
+
none: false
|
90
|
+
requirements:
|
91
|
+
- - ! '>='
|
92
|
+
- !ruby/object:Gem::Version
|
93
|
+
version: '0'
|
94
|
+
- !ruby/object:Gem::Dependency
|
95
|
+
name: rainbows
|
96
|
+
requirement: !ruby/object:Gem::Requirement
|
97
|
+
none: false
|
98
|
+
requirements:
|
99
|
+
- - ! '>='
|
100
|
+
- !ruby/object:Gem::Version
|
101
|
+
version: '0'
|
102
|
+
type: :runtime
|
103
|
+
prerelease: false
|
104
|
+
version_requirements: !ruby/object:Gem::Requirement
|
105
|
+
none: false
|
106
|
+
requirements:
|
107
|
+
- - ! '>='
|
108
|
+
- !ruby/object:Gem::Version
|
109
|
+
version: '0'
|
110
|
+
- !ruby/object:Gem::Dependency
|
111
|
+
name: redis
|
112
|
+
requirement: !ruby/object:Gem::Requirement
|
113
|
+
none: false
|
114
|
+
requirements:
|
115
|
+
- - ! '>='
|
116
|
+
- !ruby/object:Gem::Version
|
117
|
+
version: '0'
|
118
|
+
type: :runtime
|
119
|
+
prerelease: false
|
120
|
+
version_requirements: !ruby/object:Gem::Requirement
|
121
|
+
none: false
|
122
|
+
requirements:
|
123
|
+
- - ! '>='
|
124
|
+
- !ruby/object:Gem::Version
|
125
|
+
version: '0'
|
126
|
+
- !ruby/object:Gem::Dependency
|
127
|
+
name: faye
|
128
|
+
requirement: !ruby/object:Gem::Requirement
|
129
|
+
none: false
|
130
|
+
requirements:
|
131
|
+
- - ! '>='
|
132
|
+
- !ruby/object:Gem::Version
|
133
|
+
version: '0'
|
134
|
+
type: :runtime
|
135
|
+
prerelease: false
|
136
|
+
version_requirements: !ruby/object:Gem::Requirement
|
137
|
+
none: false
|
138
|
+
requirements:
|
139
|
+
- - ! '>='
|
140
|
+
- !ruby/object:Gem::Version
|
141
|
+
version: '0'
|
142
|
+
- !ruby/object:Gem::Dependency
|
143
|
+
name: faye-redis
|
144
|
+
requirement: !ruby/object:Gem::Requirement
|
145
|
+
none: false
|
146
|
+
requirements:
|
147
|
+
- - ! '>='
|
148
|
+
- !ruby/object:Gem::Version
|
149
|
+
version: '0'
|
150
|
+
type: :runtime
|
151
|
+
prerelease: false
|
152
|
+
version_requirements: !ruby/object:Gem::Requirement
|
153
|
+
none: false
|
154
|
+
requirements:
|
155
|
+
- - ! '>='
|
156
|
+
- !ruby/object:Gem::Version
|
157
|
+
version: '0'
|
158
|
+
- !ruby/object:Gem::Dependency
|
159
|
+
name: cross_time_calculation
|
160
|
+
requirement: !ruby/object:Gem::Requirement
|
161
|
+
none: false
|
162
|
+
requirements:
|
163
|
+
- - ! '>='
|
164
|
+
- !ruby/object:Gem::Version
|
165
|
+
version: '0'
|
166
|
+
type: :runtime
|
167
|
+
prerelease: false
|
168
|
+
version_requirements: !ruby/object:Gem::Requirement
|
169
|
+
none: false
|
170
|
+
requirements:
|
171
|
+
- - ! '>='
|
172
|
+
- !ruby/object:Gem::Version
|
173
|
+
version: '0'
|
174
|
+
- !ruby/object:Gem::Dependency
|
175
|
+
name: sqlite3
|
176
|
+
requirement: !ruby/object:Gem::Requirement
|
177
|
+
none: false
|
178
|
+
requirements:
|
179
|
+
- - ! '>='
|
180
|
+
- !ruby/object:Gem::Version
|
181
|
+
version: '0'
|
182
|
+
type: :development
|
183
|
+
prerelease: false
|
184
|
+
version_requirements: !ruby/object:Gem::Requirement
|
185
|
+
none: false
|
186
|
+
requirements:
|
187
|
+
- - ! '>='
|
188
|
+
- !ruby/object:Gem::Version
|
189
|
+
version: '0'
|
190
|
+
- !ruby/object:Gem::Dependency
|
191
|
+
name: combustion
|
192
|
+
requirement: !ruby/object:Gem::Requirement
|
193
|
+
none: false
|
194
|
+
requirements:
|
195
|
+
- - ! '>='
|
196
|
+
- !ruby/object:Gem::Version
|
197
|
+
version: '0'
|
198
|
+
type: :development
|
199
|
+
prerelease: false
|
200
|
+
version_requirements: !ruby/object:Gem::Requirement
|
201
|
+
none: false
|
202
|
+
requirements:
|
203
|
+
- - ! '>='
|
204
|
+
- !ruby/object:Gem::Version
|
205
|
+
version: '0'
|
206
|
+
- !ruby/object:Gem::Dependency
|
207
|
+
name: pry-debugger
|
208
|
+
requirement: !ruby/object:Gem::Requirement
|
209
|
+
none: false
|
210
|
+
requirements:
|
211
|
+
- - ! '>='
|
212
|
+
- !ruby/object:Gem::Version
|
213
|
+
version: '0'
|
214
|
+
type: :development
|
215
|
+
prerelease: false
|
216
|
+
version_requirements: !ruby/object:Gem::Requirement
|
217
|
+
none: false
|
218
|
+
requirements:
|
219
|
+
- - ! '>='
|
220
|
+
- !ruby/object:Gem::Version
|
221
|
+
version: '0'
|
222
|
+
- !ruby/object:Gem::Dependency
|
223
|
+
name: fakeredis
|
224
|
+
requirement: !ruby/object:Gem::Requirement
|
225
|
+
none: false
|
226
|
+
requirements:
|
227
|
+
- - ! '>='
|
228
|
+
- !ruby/object:Gem::Version
|
229
|
+
version: '0'
|
230
|
+
type: :development
|
231
|
+
prerelease: false
|
232
|
+
version_requirements: !ruby/object:Gem::Requirement
|
233
|
+
none: false
|
234
|
+
requirements:
|
235
|
+
- - ! '>='
|
236
|
+
- !ruby/object:Gem::Version
|
237
|
+
version: '0'
|
238
|
+
description: Faye Online user list and time count
|
239
|
+
email: mvjome@gmail.com
|
240
|
+
executables: []
|
241
|
+
extensions: []
|
242
|
+
extra_rdoc_files: []
|
243
|
+
files:
|
244
|
+
- .gitignore
|
245
|
+
- Gemfile
|
246
|
+
- README.markdown
|
247
|
+
- Rakefile
|
248
|
+
- app/assets/javascripts/faye-online.js
|
249
|
+
- app/models/faye_channel.rb
|
250
|
+
- app/models/faye_channel_online_list.rb
|
251
|
+
- app/models/faye_user_login_log.rb
|
252
|
+
- app/models/faye_user_online_time.rb
|
253
|
+
- db/migrate/20130326113504_faye_create_user_list.rb
|
254
|
+
- db/migrate/20130531033325_add_faye_user_login_logs.rb
|
255
|
+
- faye-online.gemspec
|
256
|
+
- lib/faye-online.rb
|
257
|
+
- lib/faye-online/message.rb
|
258
|
+
- lib/faye-online/rails_engine.rb
|
259
|
+
- lib/faye-online/server.rb
|
260
|
+
- lib/faye-online/status.rb
|
261
|
+
- rainbows.conf
|
262
|
+
- test/database.yml
|
263
|
+
- test/test_faye-online.rb
|
264
|
+
homepage: https://github.com/eoecn/faye-online
|
265
|
+
licenses: []
|
266
|
+
post_install_message:
|
267
|
+
rdoc_options: []
|
268
|
+
require_paths:
|
269
|
+
- lib
|
270
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
271
|
+
none: false
|
272
|
+
requirements:
|
273
|
+
- - ! '>='
|
274
|
+
- !ruby/object:Gem::Version
|
275
|
+
version: '0'
|
276
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
277
|
+
none: false
|
278
|
+
requirements:
|
279
|
+
- - ! '>='
|
280
|
+
- !ruby/object:Gem::Version
|
281
|
+
version: '0'
|
282
|
+
requirements: []
|
283
|
+
rubyforge_project:
|
284
|
+
rubygems_version: 1.8.25
|
285
|
+
signing_key:
|
286
|
+
specification_version: 3
|
287
|
+
summary: Faye Online user list and time count
|
288
|
+
test_files: []
|