em-websocket-server 0.1.1 → 0.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/README.markdown +1 -1
- data/em-websocket-server.gemspec +6 -9
- data/lib/web_socket.rb +11 -0
- data/lib/web_socket/client.rb +69 -0
- data/lib/web_socket/frame.rb +20 -0
- data/lib/web_socket/server.rb +146 -0
- data/lib/web_socket/util.rb +50 -0
- metadata +6 -9
- data/examples/chat.html +0 -40
- data/examples/chat.rb +0 -33
- data/examples/tictactoe/tictactoe.html +0 -51
- data/examples/tictactoe/tictactoe.js +0 -130
- data/examples/tictactoe/tictactoe.rb +0 -215
- data/examples/timesync.html +0 -29
- data/examples/timesync.rb +0 -35
- data/lib/em-websocket-server.rb +0 -158
data/README.markdown
CHANGED
data/em-websocket-server.gemspec
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
spec = Gem::Specification.new do |s|
|
2
2
|
s.name = 'em-websocket-server'
|
3
|
-
s.version = '0.1.
|
3
|
+
s.version = '0.1.2'
|
4
4
|
s.date = '2009-12-14'
|
5
5
|
s.summary = 'An evented ruby websocket server built on top of EventMachine'
|
6
6
|
s.email = "dan.simpson@gmail.com"
|
@@ -16,13 +16,10 @@ spec = Gem::Specification.new do |s|
|
|
16
16
|
s.files = [
|
17
17
|
"README.markdown",
|
18
18
|
"em-websocket-server.gemspec",
|
19
|
-
"
|
20
|
-
"
|
21
|
-
"
|
22
|
-
"
|
23
|
-
"
|
24
|
-
"examples/tictactoe/tictactoe.js",
|
25
|
-
"examples/tictactoe/tictactoe.rb",
|
26
|
-
"lib/em-websocket-server.rb"
|
19
|
+
"lib/web_socket.rb",
|
20
|
+
"lib/web_socket/util.rb",
|
21
|
+
"lib/web_socket/frame.rb",
|
22
|
+
"lib/web_socket/client.rb",
|
23
|
+
"lib/web_socket/server.rb"
|
27
24
|
]
|
28
25
|
end
|
data/lib/web_socket.rb
ADDED
@@ -0,0 +1,69 @@
|
|
1
|
+
|
2
|
+
module WebSocket
|
3
|
+
|
4
|
+
class Client < EM::Connection
|
5
|
+
|
6
|
+
def path
|
7
|
+
"/chat"
|
8
|
+
end
|
9
|
+
|
10
|
+
def host
|
11
|
+
"localhost:8000"
|
12
|
+
end
|
13
|
+
|
14
|
+
def origin
|
15
|
+
"localhost"
|
16
|
+
end
|
17
|
+
|
18
|
+
# em override
|
19
|
+
def post_init
|
20
|
+
@connected = false
|
21
|
+
end
|
22
|
+
|
23
|
+
def connection_completed
|
24
|
+
send_headers
|
25
|
+
end
|
26
|
+
|
27
|
+
# em override
|
28
|
+
def unbind
|
29
|
+
on_disconnect
|
30
|
+
end
|
31
|
+
|
32
|
+
def on_disconnect
|
33
|
+
end
|
34
|
+
|
35
|
+
def send_message msg
|
36
|
+
send_data Frame.encode(msg)
|
37
|
+
end
|
38
|
+
|
39
|
+
private
|
40
|
+
|
41
|
+
def receive_data data
|
42
|
+
unless @connected
|
43
|
+
handshake data
|
44
|
+
else
|
45
|
+
on_receive Frame.decode(data)
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
def handshake data
|
50
|
+
|
51
|
+
#convert the headers to a hash
|
52
|
+
@headers = Util.parse_headers(data)
|
53
|
+
@connected = true
|
54
|
+
|
55
|
+
on_connect
|
56
|
+
end
|
57
|
+
|
58
|
+
def send_headers
|
59
|
+
result = "GET #{path} HTTP/1.1\r\n"
|
60
|
+
result << "Upgrade: WebSocket\r\n"
|
61
|
+
result << "Connection: Upgrade\r\n"
|
62
|
+
result << "Host: #{host}\r\n"
|
63
|
+
result << "Origin: #{origin}\r\n\r\n"
|
64
|
+
|
65
|
+
send_data result
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
module WebSocket
|
2
|
+
|
3
|
+
class Frame
|
4
|
+
|
5
|
+
# Frames need to start with 0x00-0x7f byte and end with
|
6
|
+
# an 0xFF byte. Per spec, we can also set the first
|
7
|
+
# byte to a value betweent 0x80 and 0xFF, followed by
|
8
|
+
# a leading length indicator. No support yet.
|
9
|
+
def self.encode data
|
10
|
+
"\x00#{data}\xff"
|
11
|
+
end
|
12
|
+
|
13
|
+
# Strip leading and trailing bytes
|
14
|
+
def self.decode data
|
15
|
+
data.gsub(/^(\x00)|(\xff)$/, "")
|
16
|
+
end
|
17
|
+
|
18
|
+
end
|
19
|
+
|
20
|
+
end
|
@@ -0,0 +1,146 @@
|
|
1
|
+
module WebSocket
|
2
|
+
|
3
|
+
class Server < EM::Connection
|
4
|
+
|
5
|
+
@@logger = nil
|
6
|
+
@@num_connections = 0
|
7
|
+
@@callbacks = {}
|
8
|
+
@@accepted_origins = []
|
9
|
+
|
10
|
+
attr_accessor :connected,
|
11
|
+
:headers
|
12
|
+
|
13
|
+
def initialize *args
|
14
|
+
super
|
15
|
+
@connected = false
|
16
|
+
end
|
17
|
+
|
18
|
+
def valid_origin?
|
19
|
+
@@accepted_origins.empty? || @@accepted_origins.include?(origin)
|
20
|
+
end
|
21
|
+
|
22
|
+
#not doing anything with this yet
|
23
|
+
def valid_path?
|
24
|
+
true
|
25
|
+
end
|
26
|
+
|
27
|
+
def valid_upgrade?
|
28
|
+
@headers[:upgrade] =~ /websocket/i
|
29
|
+
end
|
30
|
+
|
31
|
+
def origin
|
32
|
+
@headers[:origin]
|
33
|
+
end
|
34
|
+
|
35
|
+
def host
|
36
|
+
@headers[:host]
|
37
|
+
end
|
38
|
+
|
39
|
+
def path
|
40
|
+
@headers[:path]
|
41
|
+
end
|
42
|
+
|
43
|
+
def cookies
|
44
|
+
@headers[:cookie]
|
45
|
+
end
|
46
|
+
|
47
|
+
def self.path name, &block
|
48
|
+
@@callbacks[name] = block
|
49
|
+
end
|
50
|
+
|
51
|
+
#tcp connection established
|
52
|
+
def post_init
|
53
|
+
@@num_connections += 1
|
54
|
+
end
|
55
|
+
|
56
|
+
#must be public for em
|
57
|
+
def unbind
|
58
|
+
@@num_connections -= 1
|
59
|
+
on_disconnect
|
60
|
+
end
|
61
|
+
|
62
|
+
def send_message msg
|
63
|
+
send_data Frame.encode(msg)
|
64
|
+
end
|
65
|
+
|
66
|
+
protected
|
67
|
+
|
68
|
+
#override this method
|
69
|
+
def on_receive msg
|
70
|
+
log msg
|
71
|
+
end
|
72
|
+
|
73
|
+
#override this method
|
74
|
+
def on_connect
|
75
|
+
log "connected"
|
76
|
+
end
|
77
|
+
|
78
|
+
#override this method
|
79
|
+
def on_disconnect
|
80
|
+
log "disconnected"
|
81
|
+
end
|
82
|
+
|
83
|
+
def log msg
|
84
|
+
if @@logger
|
85
|
+
@@logger.info msg
|
86
|
+
else
|
87
|
+
puts msg
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
private
|
92
|
+
|
93
|
+
# when the connection receives data from the client
|
94
|
+
# we either handshake or handle the message at
|
95
|
+
# the app layer
|
96
|
+
def receive_data data
|
97
|
+
unless @connected
|
98
|
+
handshake data
|
99
|
+
else
|
100
|
+
on_receive Frame.decode(data)
|
101
|
+
end
|
102
|
+
end
|
103
|
+
|
104
|
+
# parse the headers, validate the origin and path
|
105
|
+
# and respond with appropiate headers for a
|
106
|
+
# healthy relationship with the client
|
107
|
+
def handshake data
|
108
|
+
|
109
|
+
#convert the headers to a hash
|
110
|
+
@headers = Util.parse_headers(data)
|
111
|
+
|
112
|
+
# close the connection if the connection
|
113
|
+
# originates from an invalid source
|
114
|
+
close_connection unless valid_origin?
|
115
|
+
|
116
|
+
# close the connection if a callback
|
117
|
+
# is not registered for the path
|
118
|
+
close_connection unless valid_path?
|
119
|
+
|
120
|
+
# don't respond to non-websocket HTTP requests
|
121
|
+
close_connection unless valid_upgrade?
|
122
|
+
|
123
|
+
#complete the handshake
|
124
|
+
send_headers
|
125
|
+
|
126
|
+
@connected = true
|
127
|
+
|
128
|
+
on_connect
|
129
|
+
end
|
130
|
+
|
131
|
+
# send the handshake response headers to
|
132
|
+
# complete the initial com
|
133
|
+
def send_headers
|
134
|
+
|
135
|
+
response = "HTTP/1.1 101 Web Socket Protocol Handshake\r\n"
|
136
|
+
response << "Upgrade: WebSocket\r\n"
|
137
|
+
response << "Connection: Upgrade\r\n"
|
138
|
+
response << "WebSocket-Origin: #{origin}\r\n"
|
139
|
+
response << "WebSocket-Location: ws://#{host}#{path}\r\n\r\n"
|
140
|
+
|
141
|
+
send_data response
|
142
|
+
end
|
143
|
+
|
144
|
+
end
|
145
|
+
|
146
|
+
end
|
@@ -0,0 +1,50 @@
|
|
1
|
+
module WebSocket
|
2
|
+
|
3
|
+
class Util
|
4
|
+
|
5
|
+
@@path_regex = /^GET (\/[^\s]*) HTTP\/1\.1$/
|
6
|
+
@@hs_regex = /^HTTP\/1\.1 101 Web Socket Protocol Handshake$/
|
7
|
+
@@header_regex = /^([^:]+):\s*([^$]+)/
|
8
|
+
|
9
|
+
# Parse http style headers into a ruby hash
|
10
|
+
def self.parse_headers data
|
11
|
+
lines = data.split("\r\n")
|
12
|
+
line = lines.shift
|
13
|
+
|
14
|
+
headers = {}
|
15
|
+
|
16
|
+
case line
|
17
|
+
when @@path_regex
|
18
|
+
headers[:path] = @@path_regex.match(line)[1]
|
19
|
+
when @@hs_regex
|
20
|
+
#do nothing
|
21
|
+
else
|
22
|
+
throw "Unrecognized Header!"
|
23
|
+
end
|
24
|
+
|
25
|
+
|
26
|
+
lines.each do |line|
|
27
|
+
kvp = @@header_regex.match(line)
|
28
|
+
headers[kvp[1].strip.downcase.to_sym] = kvp[2].strip
|
29
|
+
end
|
30
|
+
|
31
|
+
headers
|
32
|
+
end
|
33
|
+
|
34
|
+
# encode ruby hash into HTTP style headers
|
35
|
+
def self.encode_headers data
|
36
|
+
result = ""
|
37
|
+
|
38
|
+
data.each_pair do |k,v|
|
39
|
+
result << k.to_s
|
40
|
+
result << ": "
|
41
|
+
result << v.to_s
|
42
|
+
result << "\r\n"
|
43
|
+
end
|
44
|
+
|
45
|
+
result << "\r\n"
|
46
|
+
result
|
47
|
+
end
|
48
|
+
|
49
|
+
end
|
50
|
+
end
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: em-websocket-server
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.1.
|
4
|
+
version: 0.1.2
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Dan Simpson
|
@@ -33,14 +33,11 @@ extra_rdoc_files: []
|
|
33
33
|
files:
|
34
34
|
- README.markdown
|
35
35
|
- em-websocket-server.gemspec
|
36
|
-
-
|
37
|
-
-
|
38
|
-
-
|
39
|
-
-
|
40
|
-
-
|
41
|
-
- examples/tictactoe/tictactoe.js
|
42
|
-
- examples/tictactoe/tictactoe.rb
|
43
|
-
- lib/em-websocket-server.rb
|
36
|
+
- lib/web_socket.rb
|
37
|
+
- lib/web_socket/util.rb
|
38
|
+
- lib/web_socket/frame.rb
|
39
|
+
- lib/web_socket/client.rb
|
40
|
+
- lib/web_socket/server.rb
|
44
41
|
has_rdoc: true
|
45
42
|
homepage: http://github.com/dansimpson/em-websocket-server
|
46
43
|
licenses: []
|
data/examples/chat.html
DELETED
@@ -1,40 +0,0 @@
|
|
1
|
-
<!doctype html>
|
2
|
-
<html>
|
3
|
-
<head>
|
4
|
-
<title>em-websocket-server test</title>
|
5
|
-
</head>
|
6
|
-
<body>
|
7
|
-
|
8
|
-
|
9
|
-
<h1>em-websocket-server chat demo</h1>
|
10
|
-
<div id="chat">connecting....</div>
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
<script>
|
15
|
-
|
16
|
-
var webSocket = new WebSocket('ws://192.168.0.2:8000/time');
|
17
|
-
|
18
|
-
webSocket.onopen = function(event){
|
19
|
-
document.getElementById('chat').innerHTML = 'connected';
|
20
|
-
};
|
21
|
-
|
22
|
-
webSocket.onmessage = function(event){
|
23
|
-
var object = JSON.parse(event.data);
|
24
|
-
document.getElementById('chat').innerHTML += "<b>" + object.user + "</b>" + object.message;
|
25
|
-
};
|
26
|
-
|
27
|
-
webSocket.onclose = function(event){
|
28
|
-
document.getElementById('chat').innerHTML = 'socket closed';
|
29
|
-
};
|
30
|
-
|
31
|
-
document.getElementById('chatty').addEventListener("onkeypress", function() {
|
32
|
-
alert("X")
|
33
|
-
});
|
34
|
-
|
35
|
-
</script>
|
36
|
-
|
37
|
-
<input type="text" onkeypress="webSocket.send('hi');" />
|
38
|
-
|
39
|
-
</body>
|
40
|
-
</html>
|
data/examples/chat.rb
DELETED
@@ -1,33 +0,0 @@
|
|
1
|
-
$:.unshift File.dirname(__FILE__) + '/../lib'
|
2
|
-
|
3
|
-
require 'rubygems'
|
4
|
-
require 'em-websocket-server'
|
5
|
-
require 'json'
|
6
|
-
|
7
|
-
$chatroom = EM::Channel.new
|
8
|
-
|
9
|
-
class ChatServer < WebSocketServer
|
10
|
-
|
11
|
-
def on_connect
|
12
|
-
@sid = $chatroom.subscribe do |msg|
|
13
|
-
send_message msg
|
14
|
-
end
|
15
|
-
end
|
16
|
-
|
17
|
-
def on_disconnect
|
18
|
-
$chatroom.unsubscribe(@sid)
|
19
|
-
end
|
20
|
-
|
21
|
-
def on_receive msg
|
22
|
-
$chatroom.push msg
|
23
|
-
end
|
24
|
-
|
25
|
-
end
|
26
|
-
|
27
|
-
|
28
|
-
EM.epoll
|
29
|
-
EM.set_descriptor_table_size(10240)
|
30
|
-
|
31
|
-
EM.run do
|
32
|
-
EM.start_server "0.0.0.0", 8000, ChatServer
|
33
|
-
end
|
@@ -1,51 +0,0 @@
|
|
1
|
-
<!doctype html>
|
2
|
-
|
3
|
-
<html>
|
4
|
-
<head>
|
5
|
-
<title>Tic Tac Toe</title>
|
6
|
-
<meta name="author" content="Jordan Fowler (TheBreeze)">
|
7
|
-
|
8
|
-
<style type="text/css" media="screen">
|
9
|
-
#navigation ul {
|
10
|
-
list-style-type: none;
|
11
|
-
padding: 0;
|
12
|
-
margin: 25px 0px;
|
13
|
-
}
|
14
|
-
|
15
|
-
#navigation ul li {
|
16
|
-
display: inline;
|
17
|
-
padding: 0;
|
18
|
-
margin: 0;
|
19
|
-
margin-right: 10px;
|
20
|
-
}
|
21
|
-
|
22
|
-
#game-board .cell {
|
23
|
-
width: 100px;
|
24
|
-
height: 100px;
|
25
|
-
padding: 5px;
|
26
|
-
background: #000;
|
27
|
-
color: #FFF;
|
28
|
-
font-size: 100px;
|
29
|
-
text-align: center;
|
30
|
-
vertical-align: middle;
|
31
|
-
}
|
32
|
-
</style>
|
33
|
-
</head>
|
34
|
-
<body>
|
35
|
-
|
36
|
-
<h1>Tic Tac Toe</h1>
|
37
|
-
|
38
|
-
<div id="user-count"></div>
|
39
|
-
<div id="game-stats"></div>
|
40
|
-
<div id="status"></div>
|
41
|
-
|
42
|
-
<table id="game-board">
|
43
|
-
<tr><td id="cell-0-0" class="cell"></td><td id="cell-1-0" class="cell"></td><td id="cell-2-0" class="cell"></td></tr>
|
44
|
-
<tr><td id="cell-0-1" class="cell"></td><td id="cell-1-1" class="cell"></td><td id="cell-2-1" class="cell"></td></tr>
|
45
|
-
<tr><td id="cell-0-2" class="cell"></td><td id="cell-1-2" class="cell"></td><td id="cell-2-2" class="cell"></td></tr>
|
46
|
-
</table>
|
47
|
-
|
48
|
-
<script type="text/javascript" charset="utf-8" src="mootools-1.2.4-core-nc.js"></script>
|
49
|
-
<script type="text/javascript" charset="utf-8" src="tictactoe.js"></script>
|
50
|
-
</body>
|
51
|
-
</html>
|
@@ -1,130 +0,0 @@
|
|
1
|
-
// Author: Jordan Fowler (TheBreeze)
|
2
|
-
|
3
|
-
var TicTacToe = new Class({
|
4
|
-
wins: 0,
|
5
|
-
loses: 0,
|
6
|
-
draws: 0,
|
7
|
-
methodMap: {
|
8
|
-
'queued': 'onQueued',
|
9
|
-
'start': 'onStart',
|
10
|
-
'turn': 'onTurn',
|
11
|
-
'move': 'onMove',
|
12
|
-
'game_over': 'onGameOver',
|
13
|
-
'win': 'onWin',
|
14
|
-
'loss': 'onLoss',
|
15
|
-
'draw': 'onDraw',
|
16
|
-
'user_count': 'onUserCount'
|
17
|
-
},
|
18
|
-
|
19
|
-
initialize: function() {
|
20
|
-
if (TicTacToe.connection == null) {
|
21
|
-
TicTacToe.connection = new WebSocket('ws://192.168.0.2:8000/tictactoe');
|
22
|
-
};
|
23
|
-
|
24
|
-
TicTacToe.connection.onopen = this.join.bind(this);
|
25
|
-
TicTacToe.connection.onmessage = this.onMessage.bind(this);
|
26
|
-
TicTacToe.connection.onclose = this.onGameOver.bind(this);
|
27
|
-
|
28
|
-
this.setGameStats();
|
29
|
-
},
|
30
|
-
|
31
|
-
onMessage: function(event) {
|
32
|
-
var command = JSON.decode(event.data);
|
33
|
-
|
34
|
-
console.log('[RCV] ' + command.msg);
|
35
|
-
|
36
|
-
this[this.methodMap[command.msg]].call(this, command);
|
37
|
-
},
|
38
|
-
|
39
|
-
message: function(msg, options) {
|
40
|
-
var command = JSON.encode({msg: msg, data: options});
|
41
|
-
|
42
|
-
console.log('[SENT] ' + msg);
|
43
|
-
|
44
|
-
TicTacToe.connection.send(command);
|
45
|
-
},
|
46
|
-
|
47
|
-
setStatus: function(status) {
|
48
|
-
$('status').set('text', status);
|
49
|
-
},
|
50
|
-
|
51
|
-
setGameStats: function() {
|
52
|
-
$('game-stats').set('text', 'Wins: '+this.wins+' / Losses: '+this.wins+' / Draws: '+this.draws);
|
53
|
-
},
|
54
|
-
|
55
|
-
setUserCount: function(userCount) {
|
56
|
-
$('user-count').set('text', 'Number of players: ' + userCount);
|
57
|
-
},
|
58
|
-
|
59
|
-
join: function(event) {
|
60
|
-
this.message('join');
|
61
|
-
|
62
|
-
this.setStatus('Connecting you to a game...');
|
63
|
-
},
|
64
|
-
|
65
|
-
reset: function() {
|
66
|
-
$$('.cell').set('text', '');
|
67
|
-
|
68
|
-
this.join();
|
69
|
-
},
|
70
|
-
|
71
|
-
onQueued: function(command) {
|
72
|
-
this.setStatus('Waiting for another player...');
|
73
|
-
},
|
74
|
-
|
75
|
-
onStart: function(command) {
|
76
|
-
this.setStatus('Game found! Their turn first...');
|
77
|
-
},
|
78
|
-
|
79
|
-
onTurn: function(command) {
|
80
|
-
this.setStatus('Your turn...');
|
81
|
-
},
|
82
|
-
|
83
|
-
onMove: function(command) {
|
84
|
-
$('cell-'+command.data.x+'-'+command.data.y).set('text', command.key);
|
85
|
-
|
86
|
-
this.setStatus('Their turn...');
|
87
|
-
},
|
88
|
-
|
89
|
-
move: function(x, y) {
|
90
|
-
this.message('move', {x: x, y: y});
|
91
|
-
},
|
92
|
-
|
93
|
-
onGameOver: function(command) {
|
94
|
-
this.setStatus('Game over.');
|
95
|
-
this.setGameStats();
|
96
|
-
this.reset();
|
97
|
-
},
|
98
|
-
|
99
|
-
onWin: function(command) {
|
100
|
-
this.wins += 1;
|
101
|
-
this.setStatus('Game over. You win!');
|
102
|
-
this.setGameStats();
|
103
|
-
},
|
104
|
-
|
105
|
-
onLoss: function(command) {
|
106
|
-
this.losses += 1;
|
107
|
-
this.setStatus('Game over. You lose!');
|
108
|
-
this.setGameStats();
|
109
|
-
},
|
110
|
-
|
111
|
-
onDraw: function(command) {
|
112
|
-
this.draws += 1;
|
113
|
-
this.setStatus('Game over. It was a draw!');
|
114
|
-
this.setGameStats();
|
115
|
-
},
|
116
|
-
|
117
|
-
onUserCount: function(command) {
|
118
|
-
this.setUserCount(command.data);
|
119
|
-
}
|
120
|
-
});
|
121
|
-
|
122
|
-
$$('.cell').addEvent('click', function(event) {
|
123
|
-
try {
|
124
|
-
currentGame.move($(this).get('id').split('-')[1], $(this).get('id').split('-')[2]);
|
125
|
-
} catch(error) {
|
126
|
-
alert('Please wait while we connect you to a game...');
|
127
|
-
}
|
128
|
-
});
|
129
|
-
|
130
|
-
currentGame = new TicTacToe();
|
@@ -1,215 +0,0 @@
|
|
1
|
-
$:.unshift File.dirname(__FILE__) + '/../../lib'
|
2
|
-
|
3
|
-
require 'rubygems'
|
4
|
-
require 'em-websocket-server'
|
5
|
-
require 'json'
|
6
|
-
require 'uuid'
|
7
|
-
require 'pp'
|
8
|
-
|
9
|
-
$games = {}
|
10
|
-
$waiting = nil
|
11
|
-
$status = nil
|
12
|
-
|
13
|
-
class StatusChannel < EM::Channel
|
14
|
-
|
15
|
-
def initialize
|
16
|
-
super
|
17
|
-
@count = 0
|
18
|
-
end
|
19
|
-
|
20
|
-
def increment
|
21
|
-
@count += 1
|
22
|
-
push @count
|
23
|
-
end
|
24
|
-
|
25
|
-
def decrement
|
26
|
-
@count -= 1
|
27
|
-
push @count
|
28
|
-
end
|
29
|
-
end
|
30
|
-
|
31
|
-
class Game < EM::Channel
|
32
|
-
|
33
|
-
attr_accessor :id, :player1, :player2, :current, :matrix
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
def initialize player1, player2
|
38
|
-
super()
|
39
|
-
@id = UUID.new
|
40
|
-
@player1 = player1
|
41
|
-
@player2 = player2
|
42
|
-
@matrix = (0..2).collect { [false, false, false] }
|
43
|
-
end
|
44
|
-
|
45
|
-
def set_turn p
|
46
|
-
@current = p
|
47
|
-
@current.turn!
|
48
|
-
end
|
49
|
-
|
50
|
-
def start!
|
51
|
-
@current = nil
|
52
|
-
@player1.start!
|
53
|
-
@player2.start!
|
54
|
-
toggle
|
55
|
-
end
|
56
|
-
|
57
|
-
def move p, data
|
58
|
-
if @current == p
|
59
|
-
unless @matrix[data["x"].to_i][data["y"].to_i]
|
60
|
-
@matrix[data["x"].to_i][data["y"].to_i] = p.key
|
61
|
-
|
62
|
-
@player1.send_move(p.key, data)
|
63
|
-
@player2.send_move(p.key, data)
|
64
|
-
|
65
|
-
|
66
|
-
winner = has_winner?
|
67
|
-
full = full?
|
68
|
-
|
69
|
-
if winner || full
|
70
|
-
@player1.send_command("game_over")
|
71
|
-
@player2.send_command("game_over")
|
72
|
-
if winner
|
73
|
-
p.send_command("win")
|
74
|
-
opponent(p).send_command("loss")
|
75
|
-
else full?
|
76
|
-
@player1.send_command("draw")
|
77
|
-
@player2.send_command("draw")
|
78
|
-
end
|
79
|
-
else
|
80
|
-
toggle
|
81
|
-
end
|
82
|
-
end
|
83
|
-
end
|
84
|
-
end
|
85
|
-
|
86
|
-
def full?
|
87
|
-
@matrix.each do |row|
|
88
|
-
row.each do |col|
|
89
|
-
return false unless col
|
90
|
-
end
|
91
|
-
end
|
92
|
-
return true
|
93
|
-
end
|
94
|
-
|
95
|
-
def has_winner?
|
96
|
-
return true if @matrix[1][1] && (@matrix[0][0] == @matrix[1][1]) && (@matrix[1][1] == @matrix[2][2])
|
97
|
-
return true if @matrix[1][1] && (@matrix[0][2] == @matrix[1][1]) && (@matrix[1][1] == @matrix[2][0])
|
98
|
-
@matrix.each do |row|
|
99
|
-
return true if row[1] && (row[0] == row[1]) && (row[1] == row[2])
|
100
|
-
end
|
101
|
-
return false
|
102
|
-
end
|
103
|
-
|
104
|
-
def opponent p
|
105
|
-
@player1 == p ? @player2 : @player1
|
106
|
-
end
|
107
|
-
|
108
|
-
def toggle
|
109
|
-
set_turn(@current == @player1 ? @player2 : @player1)
|
110
|
-
end
|
111
|
-
end
|
112
|
-
|
113
|
-
class TickTackToeServer < WebSocketServer
|
114
|
-
|
115
|
-
attr_accessor :game_id, :key, :status_key
|
116
|
-
|
117
|
-
def on_connect
|
118
|
-
@status_key = $status.subscribe do |c|
|
119
|
-
send_user_count c
|
120
|
-
end
|
121
|
-
$status.increment
|
122
|
-
end
|
123
|
-
|
124
|
-
def on_disconnect
|
125
|
-
$status.unsubscribe @status_key
|
126
|
-
$status.decrement
|
127
|
-
delete_game!
|
128
|
-
end
|
129
|
-
|
130
|
-
def on_receive data
|
131
|
-
|
132
|
-
begin
|
133
|
-
msg = JSON.parse(data)
|
134
|
-
rescue
|
135
|
-
send_command "error"
|
136
|
-
return
|
137
|
-
end
|
138
|
-
|
139
|
-
case msg["msg"]
|
140
|
-
when "join"
|
141
|
-
if $waiting.empty?
|
142
|
-
$waiting.push(self)
|
143
|
-
send_command "queued"
|
144
|
-
else
|
145
|
-
$waiting.pop do |opponent|
|
146
|
-
game = Game.new(self, opponent)
|
147
|
-
self.key = "X"
|
148
|
-
opponent.key = "O"
|
149
|
-
self.game_id = opponent.game_id = game.id
|
150
|
-
game.start!
|
151
|
-
$games[game_id] = game
|
152
|
-
end
|
153
|
-
end
|
154
|
-
when "move"
|
155
|
-
if game
|
156
|
-
game.move self, msg["data"]
|
157
|
-
|
158
|
-
else
|
159
|
-
log "Cannot move on a nil game!"
|
160
|
-
end
|
161
|
-
end
|
162
|
-
end
|
163
|
-
|
164
|
-
def delete_game!
|
165
|
-
if $games.key?(game_id)
|
166
|
-
$games.delete(game_id)
|
167
|
-
end
|
168
|
-
end
|
169
|
-
|
170
|
-
def game
|
171
|
-
$games[game_id]
|
172
|
-
end
|
173
|
-
|
174
|
-
def turn!
|
175
|
-
send_command "turn"
|
176
|
-
end
|
177
|
-
|
178
|
-
def game_over!
|
179
|
-
delete_game!
|
180
|
-
send_command "game_over"
|
181
|
-
end
|
182
|
-
|
183
|
-
def start!
|
184
|
-
send_command "start"
|
185
|
-
end
|
186
|
-
|
187
|
-
def send_move key, data
|
188
|
-
send_message({:msg => "move", :key => key, :data => data}.to_json)
|
189
|
-
end
|
190
|
-
|
191
|
-
def send_command cmd
|
192
|
-
send_message({:msg => cmd}.to_json)
|
193
|
-
end
|
194
|
-
|
195
|
-
def send_user_count count
|
196
|
-
send_message({:msg => :user_count, :data => count}.to_json)
|
197
|
-
end
|
198
|
-
|
199
|
-
|
200
|
-
def send_message msg
|
201
|
-
super msg
|
202
|
-
puts "Sent: #{msg}"
|
203
|
-
end
|
204
|
-
end
|
205
|
-
|
206
|
-
|
207
|
-
EM.epoll
|
208
|
-
EM.set_descriptor_table_size(10240)
|
209
|
-
|
210
|
-
EM.run do
|
211
|
-
$waiting = EM::Queue.new
|
212
|
-
$status = StatusChannel.new
|
213
|
-
|
214
|
-
EM.start_server "0.0.0.0", 8000, TickTackToeServer
|
215
|
-
end
|
data/examples/timesync.html
DELETED
@@ -1,29 +0,0 @@
|
|
1
|
-
<!doctype html>
|
2
|
-
<html>
|
3
|
-
<head>
|
4
|
-
<title>em-websocket-server test</title>
|
5
|
-
</head>
|
6
|
-
<body>
|
7
|
-
|
8
|
-
<h1>em-websocket-server timesync demo</h1>
|
9
|
-
<h2 id="time">opening socket</h2>
|
10
|
-
|
11
|
-
<script>
|
12
|
-
var webSocket = new WebSocket('ws://192.168.0.2:8000/time');
|
13
|
-
|
14
|
-
webSocket.onopen = function(event){
|
15
|
-
document.getElementById('time').innerHTML = 'waiting for socket';
|
16
|
-
};
|
17
|
-
|
18
|
-
webSocket.onmessage = function(event){
|
19
|
-
var object = JSON.parse(event.data);
|
20
|
-
document.getElementById('time').innerHTML = object.time;
|
21
|
-
};
|
22
|
-
|
23
|
-
webSocket.onclose = function(event){
|
24
|
-
document.getElementById('time').innerHTML = 'socket closed';
|
25
|
-
};
|
26
|
-
</script>
|
27
|
-
|
28
|
-
</body>
|
29
|
-
</html>
|
data/examples/timesync.rb
DELETED
@@ -1,35 +0,0 @@
|
|
1
|
-
$:.unshift File.dirname(__FILE__) + '/../lib'
|
2
|
-
|
3
|
-
require 'rubygems'
|
4
|
-
require 'em-websocket-server'
|
5
|
-
require 'json'
|
6
|
-
|
7
|
-
class TimeServer < WebSocketServer
|
8
|
-
|
9
|
-
def on_connect
|
10
|
-
@timer = EM.add_periodic_timer(5) do
|
11
|
-
sync_time
|
12
|
-
end
|
13
|
-
end
|
14
|
-
|
15
|
-
def on_disconnect
|
16
|
-
@timer
|
17
|
-
end
|
18
|
-
|
19
|
-
def on_receive msg
|
20
|
-
puts "msg rcv"
|
21
|
-
end
|
22
|
-
|
23
|
-
def sync_time
|
24
|
-
send_message({ :time => Time.now }.to_json)
|
25
|
-
end
|
26
|
-
|
27
|
-
end
|
28
|
-
|
29
|
-
|
30
|
-
EM.epoll
|
31
|
-
EM.set_descriptor_table_size(10240)
|
32
|
-
|
33
|
-
EM.run do
|
34
|
-
EM.start_server "0.0.0.0", 8000, TimeServer
|
35
|
-
end
|
data/lib/em-websocket-server.rb
DELETED
@@ -1,158 +0,0 @@
|
|
1
|
-
require 'rubygems'
|
2
|
-
require 'eventmachine'
|
3
|
-
|
4
|
-
class WebSocketServer < EM::Connection
|
5
|
-
|
6
|
-
@@logger = nil
|
7
|
-
@@num_connections = 0
|
8
|
-
@@path_regex = /^GET (\/[^\s]*) HTTP\/1\.1$/
|
9
|
-
@@header_regex = /^([^:]+):\s*([^$]+)/
|
10
|
-
@@callbacks = {}
|
11
|
-
@@accepted_origins = []
|
12
|
-
|
13
|
-
attr_accessor :connected,
|
14
|
-
:headers,
|
15
|
-
:path
|
16
|
-
|
17
|
-
def initialize *args
|
18
|
-
super
|
19
|
-
@connected = false
|
20
|
-
end
|
21
|
-
|
22
|
-
def valid_origin?
|
23
|
-
@@accepted_origins.empty? || @@accepted_origins.include?(origin)
|
24
|
-
end
|
25
|
-
|
26
|
-
#not doing anything with this yet
|
27
|
-
def valid_path?
|
28
|
-
true
|
29
|
-
end
|
30
|
-
|
31
|
-
def valid_upgrade?
|
32
|
-
@headers["Upgrade"] =~ /websocket/i
|
33
|
-
end
|
34
|
-
|
35
|
-
def origin
|
36
|
-
@headers["Origin"]
|
37
|
-
end
|
38
|
-
|
39
|
-
def host
|
40
|
-
@headers["Host"]
|
41
|
-
end
|
42
|
-
|
43
|
-
def self.path name, &block
|
44
|
-
@@callbacks[name] = block
|
45
|
-
end
|
46
|
-
|
47
|
-
#tcp connection established
|
48
|
-
def post_init
|
49
|
-
@@num_connections += 1
|
50
|
-
end
|
51
|
-
|
52
|
-
#must be public for em
|
53
|
-
def unbind
|
54
|
-
@@num_connections -= 1
|
55
|
-
on_disconnect
|
56
|
-
end
|
57
|
-
|
58
|
-
# Frames need to start with 0x00-0x7f byte and end with
|
59
|
-
# an 0xFF byte. Per spec, we can also set the first
|
60
|
-
# byte to a value betweent 0x80 and 0xFF, followed by
|
61
|
-
# a leading length indicator. No support yet
|
62
|
-
def send_message msg
|
63
|
-
send_data "\x00#{msg}\xff"
|
64
|
-
end
|
65
|
-
|
66
|
-
protected
|
67
|
-
|
68
|
-
#override this method
|
69
|
-
def on_receive msg
|
70
|
-
log msg
|
71
|
-
end
|
72
|
-
|
73
|
-
#override this method
|
74
|
-
def on_connect
|
75
|
-
log "connected"
|
76
|
-
end
|
77
|
-
|
78
|
-
#override this method
|
79
|
-
def on_disconnect
|
80
|
-
log "disconnected"
|
81
|
-
end
|
82
|
-
|
83
|
-
def log msg
|
84
|
-
if @@logger
|
85
|
-
@@logger.info msg
|
86
|
-
else
|
87
|
-
puts msg
|
88
|
-
end
|
89
|
-
end
|
90
|
-
|
91
|
-
private
|
92
|
-
|
93
|
-
# when the connection receives data from the client
|
94
|
-
# we either handshake, accept the start command
|
95
|
-
# or handle the message at the app layer
|
96
|
-
def receive_data data
|
97
|
-
unless @connected
|
98
|
-
handshake data
|
99
|
-
else
|
100
|
-
on_receive data.gsub(/^(\x00)|(\xff)$/, "")
|
101
|
-
end
|
102
|
-
end
|
103
|
-
|
104
|
-
# parse the headers, validate the origin and path
|
105
|
-
# and respond with appropiate headers for a
|
106
|
-
# healthy relationship with the client
|
107
|
-
def handshake data
|
108
|
-
|
109
|
-
#convert the headers to a hash
|
110
|
-
parse_headers data
|
111
|
-
|
112
|
-
# close the connection if the connection
|
113
|
-
# originates from an invalid source
|
114
|
-
close_connection unless valid_origin?
|
115
|
-
|
116
|
-
# close the connection if a callback
|
117
|
-
# is not registered for the path
|
118
|
-
close_connection unless valid_path?
|
119
|
-
|
120
|
-
# don't respond to non-websocket HTTP requests
|
121
|
-
close_connection unless valid_upgrade?
|
122
|
-
|
123
|
-
#complete the handshake
|
124
|
-
send_headers
|
125
|
-
|
126
|
-
@connected = true
|
127
|
-
|
128
|
-
on_connect
|
129
|
-
end
|
130
|
-
|
131
|
-
# send the handshake response headers to
|
132
|
-
# complete the initial com
|
133
|
-
def send_headers
|
134
|
-
|
135
|
-
response = "HTTP/1.1 101 Web Socket Protocol Handshake\r\n"
|
136
|
-
response << "Upgrade: WebSocket\r\n"
|
137
|
-
response << "Connection: Upgrade\r\n"
|
138
|
-
response << "WebSocket-Origin: #{origin}\r\n"
|
139
|
-
response << "WebSocket-Location: ws://#{host}#{path}\r\n\r\n"
|
140
|
-
|
141
|
-
send_data response
|
142
|
-
end
|
143
|
-
|
144
|
-
# turn http style headers into a ruby hash
|
145
|
-
# TODO: this is probably not done "well"
|
146
|
-
def parse_headers data
|
147
|
-
lines = data.split("\r\n")
|
148
|
-
|
149
|
-
@path = @@path_regex.match(lines.shift)[1]
|
150
|
-
@headers = {}
|
151
|
-
|
152
|
-
lines.each do |line|
|
153
|
-
kvp = @@header_regex.match(line)
|
154
|
-
@headers[kvp[1].strip] = kvp[2].strip
|
155
|
-
end
|
156
|
-
end
|
157
|
-
|
158
|
-
end
|