em-websocket 0.0.4 → 0.0.5
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/.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
|