em-websocket 0.0.4 → 0.0.5
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +3 -0
- data/README.rdoc +30 -1
- data/Rakefile +13 -2
- data/VERSION +1 -1
- data/examples/multicast.rb +43 -0
- data/lib/em-websocket.rb +1 -1
- data/lib/em-websocket/connection.rb +8 -5
- data/spec/websocket_spec.rb +92 -12
- metadata +16 -3
data/.gitignore
ADDED
data/README.rdoc
CHANGED
@@ -1,6 +1,7 @@
|
|
1
1
|
= EM-WebSocket
|
2
2
|
|
3
|
-
EventMachine based WebSocket server
|
3
|
+
EventMachine based, async WebSocket server. Take a look at examples directory, or
|
4
|
+
check out the blog post below:
|
4
5
|
- http://www.igvita.com/2009/12/22/ruby-websockets-tcp-for-the-browser/
|
5
6
|
|
6
7
|
== Simple server example
|
@@ -23,4 +24,32 @@ EventMachine based WebSocket server -- highly experimental, at this point, at le
|
|
23
24
|
end
|
24
25
|
}
|
25
26
|
|
27
|
+
== Examples
|
28
|
+
- examples/multicast.rb - broadcast all ruby tweets to all subscribers
|
29
|
+
- examples/echo.rb - server <> client exchange via a websocket
|
30
|
+
- http://github.com/rubenfonseca/twitter-amqp-websocket-example
|
26
31
|
|
32
|
+
== License
|
33
|
+
|
34
|
+
(The MIT License)
|
35
|
+
|
36
|
+
Copyright (c) 2009 Ilya Grigorik
|
37
|
+
|
38
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
39
|
+
a copy of this software and associated documentation files (the
|
40
|
+
'Software'), to deal in the Software without restriction, including
|
41
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
42
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
43
|
+
permit persons to whom the Software is furnished to do so, subject to
|
44
|
+
the following conditions:
|
45
|
+
|
46
|
+
The above copyright notice and this permission notice shall be
|
47
|
+
included in all copies or substantial portions of the Software.
|
48
|
+
|
49
|
+
THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
|
50
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
51
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
52
|
+
IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
|
53
|
+
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
|
54
|
+
TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
|
55
|
+
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/Rakefile
CHANGED
@@ -1,4 +1,5 @@
|
|
1
1
|
require 'rake'
|
2
|
+
require 'spec/rake/spectask'
|
2
3
|
|
3
4
|
begin
|
4
5
|
require 'jeweler'
|
@@ -10,11 +11,21 @@ begin
|
|
10
11
|
gemspec.homepage = "http://github.com/igrigorik/em-websocket"
|
11
12
|
gemspec.authors = ["Ilya Grigorik"]
|
12
13
|
gemspec.add_dependency("eventmachine", ">= 0.12.9")
|
13
|
-
gemspec.add_dependency("addressable")
|
14
|
+
gemspec.add_dependency("addressable", '>= 2.1.1')
|
15
|
+
gemspec.add_development_dependency('em-http-request', '>= 0.2.6')
|
14
16
|
gemspec.rubyforge_project = "em-websocket"
|
15
17
|
end
|
16
18
|
|
17
19
|
Jeweler::GemcutterTasks.new
|
18
20
|
rescue LoadError
|
19
|
-
puts "Jeweler not available. Install it with: sudo gem install
|
21
|
+
puts "Jeweler not available. Install it with: sudo gem install jeweler -s http://gemcutter.org"
|
20
22
|
end
|
23
|
+
|
24
|
+
task :default => :spec
|
25
|
+
|
26
|
+
Spec::Rake::SpecTask.new do |t|
|
27
|
+
t.ruby_opts = ['-rtest/unit']
|
28
|
+
t.spec_files = FileList['spec/**/*_spec.rb']
|
29
|
+
end
|
30
|
+
|
31
|
+
|
data/VERSION
CHANGED
@@ -1 +1 @@
|
|
1
|
-
0.0.
|
1
|
+
0.0.5
|
@@ -0,0 +1,43 @@
|
|
1
|
+
require 'lib/em-websocket'
|
2
|
+
require 'twitter/json_stream'
|
3
|
+
require 'json'
|
4
|
+
|
5
|
+
#
|
6
|
+
# broadcast all ruby related tweets to all connected users!
|
7
|
+
#
|
8
|
+
|
9
|
+
username = ARGV.shift
|
10
|
+
password = ARGV.shift
|
11
|
+
raise "need username and password" if !username or !password
|
12
|
+
|
13
|
+
EventMachine.run {
|
14
|
+
@channel = EM::Channel.new
|
15
|
+
|
16
|
+
@twitter = Twitter::JSONStream.connect(
|
17
|
+
:path => '/1/statuses/filter.json?track=ruby',
|
18
|
+
:auth => "#{username}:#{password}"
|
19
|
+
)
|
20
|
+
|
21
|
+
@twitter.each_item do |status|
|
22
|
+
status = JSON.parse(status)
|
23
|
+
@channel.push "#{status['user']['screen_name']}: #{status['text']}"
|
24
|
+
end
|
25
|
+
|
26
|
+
|
27
|
+
EventMachine::WebSocket.start(:host => "0.0.0.0", :port => 8080, :debug => true) do |ws|
|
28
|
+
ws.onopen {
|
29
|
+
@sid = @channel.subscribe { |msg| ws.send msg }
|
30
|
+
@channel.push "#{@sid} connected!"
|
31
|
+
}
|
32
|
+
|
33
|
+
ws.onmessage { |msg|
|
34
|
+
@channel.push "<#{@sid}>: #{msg}"
|
35
|
+
}
|
36
|
+
|
37
|
+
ws.onclose {
|
38
|
+
@channel.unsubscribe(@sid)
|
39
|
+
}
|
40
|
+
end
|
41
|
+
|
42
|
+
puts "Server started"
|
43
|
+
}
|
data/lib/em-websocket.rb
CHANGED
@@ -7,7 +7,7 @@ module EventMachine
|
|
7
7
|
PATH = /^GET (\/[^\s]*) HTTP\/1\.1$/
|
8
8
|
HEADER = /^([^:]+):\s*([^$]+)/
|
9
9
|
|
10
|
-
attr_reader :state
|
10
|
+
attr_reader :state, :request
|
11
11
|
|
12
12
|
# define WebSocket callbacks
|
13
13
|
def onopen(&blk); @onopen = blk; end
|
@@ -33,7 +33,7 @@ module EventMachine
|
|
33
33
|
|
34
34
|
def unbind
|
35
35
|
debug [:unbind, :connection]
|
36
|
-
|
36
|
+
|
37
37
|
@state = :closed
|
38
38
|
@onclose.call if @onclose
|
39
39
|
end
|
@@ -59,6 +59,9 @@ module EventMachine
|
|
59
59
|
# extract request path
|
60
60
|
@request['Path'] = lines.shift.match(PATH)[1].strip
|
61
61
|
|
62
|
+
# extract query string values
|
63
|
+
@request['Query'] = Addressable::URI.parse(@request['Path']).query_values ||= {}
|
64
|
+
|
62
65
|
# extract remaining headers
|
63
66
|
lines.each do |line|
|
64
67
|
h = HEADER.match(line)
|
@@ -67,7 +70,7 @@ module EventMachine
|
|
67
70
|
|
68
71
|
# transform headers
|
69
72
|
@request['Host'] = Addressable::URI.parse("ws://"+@request['Host'])
|
70
|
-
|
73
|
+
|
71
74
|
if not websocket_connection?
|
72
75
|
send_data "HTTP/1.1 400 Bad request\r\n\r\n"
|
73
76
|
close_connection_after_writing
|
@@ -90,7 +93,7 @@ module EventMachine
|
|
90
93
|
def send_upgrade
|
91
94
|
location = "ws://#{@request['Host'].host}"
|
92
95
|
location << ":#{@request['Host'].port}" if @request['Host'].port
|
93
|
-
location << @request['Path']
|
96
|
+
location << @request['Path'].split("?").first
|
94
97
|
|
95
98
|
upgrade = "HTTP/1.1 101 Web Socket Protocol Handshake\r\n"
|
96
99
|
upgrade << "Upgrade: WebSocket\r\n"
|
@@ -120,7 +123,7 @@ module EventMachine
|
|
120
123
|
msg.gsub!(/^\x00|\xff$/, '')
|
121
124
|
@onmessage.call(msg)
|
122
125
|
end
|
123
|
-
|
126
|
+
|
124
127
|
false
|
125
128
|
end
|
126
129
|
|
data/spec/websocket_spec.rb
CHANGED
@@ -11,7 +11,7 @@ describe EventMachine::WebSocket do
|
|
11
11
|
EM.run do
|
12
12
|
MSG = "Hello World!"
|
13
13
|
EventMachine.add_timer(0.1) do
|
14
|
-
http = EventMachine::HttpRequest.new('ws://127.0.0.1:
|
14
|
+
http = EventMachine::HttpRequest.new('ws://127.0.0.1:12345/').get :timeout => 0
|
15
15
|
http.errback { failed }
|
16
16
|
http.callback { http.response_header.status.should == 101 }
|
17
17
|
|
@@ -21,7 +21,7 @@ describe EventMachine::WebSocket do
|
|
21
21
|
}
|
22
22
|
end
|
23
23
|
|
24
|
-
EventMachine::WebSocket.start(:host => "0.0.0.0", :port =>
|
24
|
+
EventMachine::WebSocket.start(:host => "0.0.0.0", :port => 12345) do |ws|
|
25
25
|
ws.onopen {
|
26
26
|
puts "WebSocket connection open"
|
27
27
|
ws.send MSG
|
@@ -33,7 +33,7 @@ describe EventMachine::WebSocket do
|
|
33
33
|
it "should fail on non WebSocket requests" do
|
34
34
|
EM.run do
|
35
35
|
EventMachine.add_timer(0.1) do
|
36
|
-
http = EventMachine::HttpRequest.new('http://127.0.0.1:
|
36
|
+
http = EventMachine::HttpRequest.new('http://127.0.0.1:12345/').get :timeout => 0
|
37
37
|
http.errback { failed }
|
38
38
|
http.callback {
|
39
39
|
http.response_header.status.should == 400
|
@@ -41,17 +41,17 @@ describe EventMachine::WebSocket do
|
|
41
41
|
}
|
42
42
|
end
|
43
43
|
|
44
|
-
EventMachine::WebSocket.start(:host => "0.0.0.0", :port =>
|
44
|
+
EventMachine::WebSocket.start(:host => "0.0.0.0", :port => 12345) {}
|
45
45
|
end
|
46
46
|
end
|
47
47
|
|
48
48
|
it "should split multiple messages into separate callbacks" do
|
49
49
|
EM.run do
|
50
50
|
messages = %w[1 2]
|
51
|
-
|
51
|
+
received = []
|
52
52
|
|
53
53
|
EventMachine.add_timer(0.1) do
|
54
|
-
http = EventMachine::HttpRequest.new('ws://127.0.0.1:
|
54
|
+
http = EventMachine::HttpRequest.new('ws://127.0.0.1:12345/').get :timeout => 0
|
55
55
|
http.errback { failed }
|
56
56
|
http.stream {|msg|}
|
57
57
|
http.callback {
|
@@ -61,14 +61,14 @@ describe EventMachine::WebSocket do
|
|
61
61
|
}
|
62
62
|
end
|
63
63
|
|
64
|
-
EventMachine::WebSocket.start(:host => "0.0.0.0", :port =>
|
64
|
+
EventMachine::WebSocket.start(:host => "0.0.0.0", :port => 12345) do |ws|
|
65
65
|
ws.onopen {}
|
66
66
|
ws.onclose {}
|
67
67
|
ws.onmessage {|msg|
|
68
|
-
msg.should == messages[
|
69
|
-
|
68
|
+
msg.should == messages[received.size]
|
69
|
+
received.push msg
|
70
70
|
|
71
|
-
EventMachine.stop if
|
71
|
+
EventMachine.stop if received.size == messages.size
|
72
72
|
}
|
73
73
|
end
|
74
74
|
end
|
@@ -77,7 +77,7 @@ describe EventMachine::WebSocket do
|
|
77
77
|
it "should call onclose callback when client closes connection" do
|
78
78
|
EM.run do
|
79
79
|
EventMachine.add_timer(0.1) do
|
80
|
-
http = EventMachine::HttpRequest.new('ws://127.0.0.1:
|
80
|
+
http = EventMachine::HttpRequest.new('ws://127.0.0.1:12345/').get :timeout => 0
|
81
81
|
http.errback { failed }
|
82
82
|
http.callback {
|
83
83
|
http.response_header.status.should == 101
|
@@ -86,7 +86,7 @@ describe EventMachine::WebSocket do
|
|
86
86
|
http.stream{|msg|}
|
87
87
|
end
|
88
88
|
|
89
|
-
EventMachine::WebSocket.start(:host => "0.0.0.0", :port =>
|
89
|
+
EventMachine::WebSocket.start(:host => "0.0.0.0", :port => 12345) do |ws|
|
90
90
|
ws.onopen {}
|
91
91
|
ws.onclose {
|
92
92
|
ws.state.should == :closed
|
@@ -96,4 +96,84 @@ describe EventMachine::WebSocket do
|
|
96
96
|
end
|
97
97
|
end
|
98
98
|
|
99
|
+
it "should populate ws.request with appropriate headers" do
|
100
|
+
EM.run do
|
101
|
+
EventMachine.add_timer(0.1) do
|
102
|
+
http = EventMachine::HttpRequest.new('ws://127.0.0.1:12345/').get :timeout => 0
|
103
|
+
http.errback { failed }
|
104
|
+
http.callback {
|
105
|
+
http.response_header.status.should == 101
|
106
|
+
http.close_connection
|
107
|
+
}
|
108
|
+
http.stream { |msg| }
|
109
|
+
end
|
110
|
+
|
111
|
+
EventMachine::WebSocket.start(:host => "0.0.0.0", :port => 12345) do |ws|
|
112
|
+
ws.onopen {
|
113
|
+
ws.request["User-Agent"].should == "EventMachine HttpClient"
|
114
|
+
ws.request["Connection"].should == "Upgrade"
|
115
|
+
ws.request["Upgrade"].should == "WebSocket"
|
116
|
+
ws.request["Path"].should == "/"
|
117
|
+
ws.request["Origin"].should == "127.0.0.1"
|
118
|
+
ws.request["Host"].to_s.should == "ws://127.0.0.1:12345"
|
119
|
+
}
|
120
|
+
ws.onclose {
|
121
|
+
ws.state.should == :closed
|
122
|
+
EventMachine.stop
|
123
|
+
}
|
124
|
+
end
|
125
|
+
end
|
126
|
+
end
|
127
|
+
|
128
|
+
it "should allow sending and retrieving query string args passed in on the connection request." do
|
129
|
+
EM.run do
|
130
|
+
EventMachine.add_timer(0.1) do
|
131
|
+
http = EventMachine::HttpRequest.new('ws://127.0.0.1:12345/').get(:query => {'foo' => 'bar', 'baz' => 'qux'}, :timeout => 0)
|
132
|
+
http.errback { failed }
|
133
|
+
http.callback {
|
134
|
+
http.response_header.status.should == 101
|
135
|
+
http.close_connection
|
136
|
+
}
|
137
|
+
http.stream { |msg| }
|
138
|
+
end
|
139
|
+
|
140
|
+
EventMachine::WebSocket.start(:host => "0.0.0.0", :port => 12345) do |ws|
|
141
|
+
ws.onopen {
|
142
|
+
ws.request["Path"].should == "/?baz=qux&foo=bar"
|
143
|
+
ws.request["Query"]["foo"].should == "bar"
|
144
|
+
ws.request["Query"]["baz"].should == "qux"
|
145
|
+
}
|
146
|
+
ws.onclose {
|
147
|
+
ws.state.should == :closed
|
148
|
+
EventMachine.stop
|
149
|
+
}
|
150
|
+
end
|
151
|
+
end
|
152
|
+
end
|
153
|
+
|
154
|
+
it "should ws.response['Query'] to empty hash when no query string params passed in connection URI" do
|
155
|
+
EM.run do
|
156
|
+
EventMachine.add_timer(0.1) do
|
157
|
+
http = EventMachine::HttpRequest.new('ws://127.0.0.1:12345/').get(:timeout => 0)
|
158
|
+
http.errback { failed }
|
159
|
+
http.callback {
|
160
|
+
http.response_header.status.should == 101
|
161
|
+
http.close_connection
|
162
|
+
}
|
163
|
+
http.stream { |msg| }
|
164
|
+
end
|
165
|
+
|
166
|
+
EventMachine::WebSocket.start(:host => "0.0.0.0", :port => 12345) do |ws|
|
167
|
+
ws.onopen {
|
168
|
+
ws.request["Path"].should == "/"
|
169
|
+
ws.request["Query"].should == {}
|
170
|
+
}
|
171
|
+
ws.onclose {
|
172
|
+
ws.state.should == :closed
|
173
|
+
EventMachine.stop
|
174
|
+
}
|
175
|
+
end
|
176
|
+
end
|
177
|
+
end
|
178
|
+
|
99
179
|
end
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: em-websocket
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.0.
|
4
|
+
version: 0.0.5
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Ilya Grigorik
|
@@ -9,7 +9,7 @@ autorequire:
|
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
11
|
|
12
|
-
date:
|
12
|
+
date: 2010-01-06 00:00:00 -05:00
|
13
13
|
default_executable:
|
14
14
|
dependencies:
|
15
15
|
- !ruby/object:Gem::Dependency
|
@@ -30,7 +30,17 @@ dependencies:
|
|
30
30
|
requirements:
|
31
31
|
- - ">="
|
32
32
|
- !ruby/object:Gem::Version
|
33
|
-
version:
|
33
|
+
version: 2.1.1
|
34
|
+
version:
|
35
|
+
- !ruby/object:Gem::Dependency
|
36
|
+
name: em-http-request
|
37
|
+
type: :development
|
38
|
+
version_requirement:
|
39
|
+
version_requirements: !ruby/object:Gem::Requirement
|
40
|
+
requirements:
|
41
|
+
- - ">="
|
42
|
+
- !ruby/object:Gem::Version
|
43
|
+
version: 0.2.6
|
34
44
|
version:
|
35
45
|
description: EventMachine based WebSocket server
|
36
46
|
email: ilya@igvita.com
|
@@ -41,10 +51,12 @@ extensions: []
|
|
41
51
|
extra_rdoc_files:
|
42
52
|
- README.rdoc
|
43
53
|
files:
|
54
|
+
- .gitignore
|
44
55
|
- README.rdoc
|
45
56
|
- Rakefile
|
46
57
|
- VERSION
|
47
58
|
- examples/echo.rb
|
59
|
+
- examples/multicast.rb
|
48
60
|
- examples/test.html
|
49
61
|
- lib/em-websocket.rb
|
50
62
|
- lib/em-websocket/connection.rb
|
@@ -83,3 +95,4 @@ test_files:
|
|
83
95
|
- spec/helper.rb
|
84
96
|
- spec/websocket_spec.rb
|
85
97
|
- examples/echo.rb
|
98
|
+
- examples/multicast.rb
|