em-websocket-request 0.0.1
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 +2 -0
- data/.gitmodules +3 -0
- data/Gemfile +9 -0
- data/Rakefile +6 -0
- data/em-websocket-request.gemspec +19 -0
- data/lib/em-websocket-request.rb +16 -0
- data/lib/em-ws-request/client.rb +87 -0
- data/lib/em-ws-request/version.rb +3 -0
- data/lib/em-ws-request/wrapper.rb +46 -0
- data/spec/websocket_spec.rb +86 -0
- metadata +67 -0
data/.gitignore
ADDED
data/.gitmodules
ADDED
data/Gemfile
ADDED
data/Rakefile
ADDED
@@ -0,0 +1,19 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
require File.expand_path('../lib/em-ws-request/version', __FILE__)
|
3
|
+
|
4
|
+
Gem::Specification.new do |gem|
|
5
|
+
gem.authors = ["Michael Rykov"]
|
6
|
+
gem.email = ["mrykov@gmail.com"]
|
7
|
+
gem.description = %q{EventMachine WebSocket client}
|
8
|
+
gem.summary = %q{EventMachine WebSocket client}
|
9
|
+
gem.homepage = ""
|
10
|
+
|
11
|
+
gem.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
|
12
|
+
gem.files = `git ls-files`.split("\n")
|
13
|
+
gem.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
|
14
|
+
gem.name = "em-websocket-request"
|
15
|
+
gem.require_paths = ["lib"]
|
16
|
+
gem.version = EventMachine::WS_REQUEST_VERSION
|
17
|
+
|
18
|
+
gem.add_runtime_dependency 'em-http-request', '~> 1.0.0'
|
19
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
require 'em-http-request'
|
2
|
+
require "em-ws-request/version"
|
3
|
+
require "em-ws-request/client"
|
4
|
+
require "em-ws-request/wrapper"
|
5
|
+
|
6
|
+
module EventMachine
|
7
|
+
class WebsocketRequest < HttpRequest
|
8
|
+
def self.new(uri, options={})
|
9
|
+
connopt = HttpConnectionOptions.new(uri, options)
|
10
|
+
c = WebsocketConnection.new
|
11
|
+
c.connopts = connopt
|
12
|
+
c.uri = uri
|
13
|
+
c
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
@@ -0,0 +1,87 @@
|
|
1
|
+
module EventMachine
|
2
|
+
class WebsocketConnection < HttpConnection
|
3
|
+
def setup_request(method, options = {}, c = nil)
|
4
|
+
c ||= WebsocketClient.new(self, HttpClientOptions.new(@uri, options, method))
|
5
|
+
@deferred ? activate_connection(c) : finalize_request(c)
|
6
|
+
c
|
7
|
+
end
|
8
|
+
|
9
|
+
def post_init
|
10
|
+
super
|
11
|
+
@p.on_message_complete = proc do
|
12
|
+
if not client.continue?
|
13
|
+
if client.state == :websocket
|
14
|
+
client.state = :stream
|
15
|
+
else
|
16
|
+
client.state = :finished
|
17
|
+
client.on_request_complete
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
def receive_data(data)
|
24
|
+
if client.state == :stream
|
25
|
+
client.receive(data)
|
26
|
+
else
|
27
|
+
super(data)
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
class WebsocketClient < HttpClient
|
33
|
+
PROTOCOL_VERSION = '8'
|
34
|
+
|
35
|
+
def initialize(conn, options)
|
36
|
+
super(conn, options)
|
37
|
+
@wswrapper = WebSocketWrapper.new(self, PROTOCOL_VERSION)
|
38
|
+
end
|
39
|
+
|
40
|
+
def send(data, frame_type = :text)
|
41
|
+
@connection = @conn
|
42
|
+
if state == :stream || state == :websocket # FIXME
|
43
|
+
@wswrapper.send_frame(frame_type, data)
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
def send_data(data)
|
48
|
+
@conn.send_data(data)
|
49
|
+
end
|
50
|
+
|
51
|
+
def receive(data)
|
52
|
+
out = @wswrapper.receive(data)
|
53
|
+
@stream.call(out) if @stream
|
54
|
+
end
|
55
|
+
|
56
|
+
def websocket?
|
57
|
+
@req.uri.scheme == 'ws' || @req.uri.scheme == 'wss'
|
58
|
+
end
|
59
|
+
|
60
|
+
def build_request
|
61
|
+
head = super
|
62
|
+
if websocket?
|
63
|
+
head.delete_if { |k, v| !%w(host).include?(k) }
|
64
|
+
head['upgrade'] = 'websocket'
|
65
|
+
head['connection'] = 'Upgrade'
|
66
|
+
head['origin'] = @req.uri.host
|
67
|
+
head['Sec-WebSocket-Key'] = 'dGhlIHNhbXBsZSBub25jZQ=='
|
68
|
+
head['Sec-WebSocket-Version'] = PROTOCOL_VERSION
|
69
|
+
end
|
70
|
+
head
|
71
|
+
end
|
72
|
+
|
73
|
+
def parse_response_header(header, version, status)
|
74
|
+
super
|
75
|
+
if websocket?
|
76
|
+
p [:parse_response_header, :WEBSOCKET]
|
77
|
+
if @response_header.status == 101
|
78
|
+
@state = :websocket
|
79
|
+
succeed
|
80
|
+
else
|
81
|
+
fail "websocket handshake failed"
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
@@ -0,0 +1,46 @@
|
|
1
|
+
require "em-ws-request/vendor/web-socket-ruby/lib/web_socket"
|
2
|
+
|
3
|
+
module EventMachine
|
4
|
+
class WebSocketWrapper < ::WebSocket
|
5
|
+
def initialize(client, version)
|
6
|
+
@handshaked = true
|
7
|
+
@client = client
|
8
|
+
@web_socket_version = version
|
9
|
+
end
|
10
|
+
|
11
|
+
def send_frame(type, data)
|
12
|
+
opcode = type_to_opcode(type)
|
13
|
+
super(opcode, data, true)
|
14
|
+
end
|
15
|
+
|
16
|
+
def write(data)
|
17
|
+
@client.send_data(data)
|
18
|
+
end
|
19
|
+
|
20
|
+
def receive(data)
|
21
|
+
@incoming_data = data
|
22
|
+
super()
|
23
|
+
end
|
24
|
+
|
25
|
+
def read(num_bytes)
|
26
|
+
str = @incoming_data.slice(0, num_bytes)
|
27
|
+
@incoming_data = @incoming_data[num_bytes..-1]
|
28
|
+
return str
|
29
|
+
end
|
30
|
+
|
31
|
+
private
|
32
|
+
FRAME_TYPES = {
|
33
|
+
:continuation => 0,
|
34
|
+
:text => 1,
|
35
|
+
:binary => 2,
|
36
|
+
:close => 8,
|
37
|
+
:ping => 9,
|
38
|
+
:pong => 10,
|
39
|
+
}
|
40
|
+
|
41
|
+
def type_to_opcode(frame_type)
|
42
|
+
FRAME_TYPES[frame_type] || raise("Unknown frame type")
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
@@ -0,0 +1,86 @@
|
|
1
|
+
require 'em-websocket'
|
2
|
+
require 'em-websocket-request'
|
3
|
+
|
4
|
+
describe EventMachine::WebsocketRequest do
|
5
|
+
|
6
|
+
context "websocket connection" do
|
7
|
+
# Spec: http://tools.ietf.org/html/draft-hixie-thewebsocketprotocol-55
|
8
|
+
#
|
9
|
+
# ws.onopen = http.callback
|
10
|
+
# ws.onmessage = http.stream { |msg| }
|
11
|
+
# ws.errback = no connection
|
12
|
+
#
|
13
|
+
|
14
|
+
it "should invoke errback on failed upgrade" do
|
15
|
+
EventMachine.run {
|
16
|
+
http = EventMachine::WebsocketRequest.new('ws://127.0.0.1:8090/').get :timeout => 0
|
17
|
+
|
18
|
+
http.callback { failed(http) }
|
19
|
+
http.errback {
|
20
|
+
http.response_header.status.should == 0
|
21
|
+
EventMachine.stop
|
22
|
+
}
|
23
|
+
}
|
24
|
+
end
|
25
|
+
|
26
|
+
it "should complete websocket handshake and transfer data from client to server and back" do
|
27
|
+
EventMachine.run {
|
28
|
+
MSG = "hello bi-directional data exchange"
|
29
|
+
|
30
|
+
EventMachine::WebSocket.start(:host => "0.0.0.0", :port => 8085, :debug => true) do |ws|
|
31
|
+
ws.onopen { p [:OPENED_WS, ws]}
|
32
|
+
ws.onmessage {|msg| ws.send msg}
|
33
|
+
ws.onerror {|e| p [:WS_ERROR, e]}
|
34
|
+
ws.onclose { p [:WS_CLOSE, ws]}
|
35
|
+
end
|
36
|
+
|
37
|
+
http = EventMachine::WebsocketRequest.new('ws://127.0.0.1:8085/').get :keepalive => true
|
38
|
+
http.errback { failed(http) }
|
39
|
+
http.callback {
|
40
|
+
http.response_header.status.should == 101
|
41
|
+
http.response_header['CONNECTION'].should match(/Upgrade/)
|
42
|
+
http.response_header['UPGRADE'].should match(/websocket/)
|
43
|
+
|
44
|
+
# push should only be invoked after handshake is complete
|
45
|
+
http.send(MSG)
|
46
|
+
}
|
47
|
+
|
48
|
+
http.stream { |chunk|
|
49
|
+
chunk.should == MSG
|
50
|
+
EventMachine.stop
|
51
|
+
}
|
52
|
+
}
|
53
|
+
end
|
54
|
+
|
55
|
+
# parser eats up the trailign data
|
56
|
+
# [:receive, "HTTP/1.1 101 Web Socket Protocol Handshake\r\nUpgrade: WebSocket\r\nConnection: Upgrade\r\nWebSocket-Origin: 127.0.0.1\r\nWebSocket-Location: ws://127.0.0.1:8085/\r\n\r\n\x001\xFF\x002\xFF", :keep_alive?, "#<HTTP::Parser:0x0000010132d000>"]
|
57
|
+
|
58
|
+
xit "should split multiple messages from websocket server into separate stream callbacks" do
|
59
|
+
EM.run do
|
60
|
+
messages = %w[1 2]
|
61
|
+
recieved = []
|
62
|
+
|
63
|
+
EventMachine::WebSocket.start(:host => "0.0.0.0", :port => 8085) do |ws|
|
64
|
+
ws.onopen {
|
65
|
+
ws.send messages[0]
|
66
|
+
ws.send messages[1]
|
67
|
+
}
|
68
|
+
end
|
69
|
+
|
70
|
+
EventMachine.add_timer(0.1) do
|
71
|
+
http = EventMachine::WebsocketRequest.new('ws://127.0.0.1:8085/').get :keepalive => true
|
72
|
+
http.errback { failed(http) }
|
73
|
+
http.callback { http.response_header.status.should == 101; p 'WS CONNECTED' }
|
74
|
+
http.stream {|msg|
|
75
|
+
p ['GOT MSG ', msg]
|
76
|
+
msg.should == messages[recieved.size]
|
77
|
+
recieved.push msg
|
78
|
+
p [:MULTI_MESAGE, recieved]
|
79
|
+
|
80
|
+
EventMachine.stop if recieved.size == messages.size
|
81
|
+
}
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|
86
|
+
end
|
metadata
ADDED
@@ -0,0 +1,67 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: em-websocket-request
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.1
|
5
|
+
prerelease:
|
6
|
+
platform: ruby
|
7
|
+
authors:
|
8
|
+
- Michael Rykov
|
9
|
+
autorequire:
|
10
|
+
bindir: bin
|
11
|
+
cert_chain: []
|
12
|
+
date: 2011-10-12 00:00:00.000000000Z
|
13
|
+
dependencies:
|
14
|
+
- !ruby/object:Gem::Dependency
|
15
|
+
name: em-http-request
|
16
|
+
requirement: &70329779717660 !ruby/object:Gem::Requirement
|
17
|
+
none: false
|
18
|
+
requirements:
|
19
|
+
- - ~>
|
20
|
+
- !ruby/object:Gem::Version
|
21
|
+
version: 1.0.0
|
22
|
+
type: :runtime
|
23
|
+
prerelease: false
|
24
|
+
version_requirements: *70329779717660
|
25
|
+
description: EventMachine WebSocket client
|
26
|
+
email:
|
27
|
+
- mrykov@gmail.com
|
28
|
+
executables: []
|
29
|
+
extensions: []
|
30
|
+
extra_rdoc_files: []
|
31
|
+
files:
|
32
|
+
- .gitignore
|
33
|
+
- .gitmodules
|
34
|
+
- Gemfile
|
35
|
+
- Rakefile
|
36
|
+
- em-websocket-request.gemspec
|
37
|
+
- lib/em-websocket-request.rb
|
38
|
+
- lib/em-ws-request/client.rb
|
39
|
+
- lib/em-ws-request/version.rb
|
40
|
+
- lib/em-ws-request/wrapper.rb
|
41
|
+
- spec/websocket_spec.rb
|
42
|
+
homepage: ''
|
43
|
+
licenses: []
|
44
|
+
post_install_message:
|
45
|
+
rdoc_options: []
|
46
|
+
require_paths:
|
47
|
+
- lib
|
48
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
49
|
+
none: false
|
50
|
+
requirements:
|
51
|
+
- - ! '>='
|
52
|
+
- !ruby/object:Gem::Version
|
53
|
+
version: '0'
|
54
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
55
|
+
none: false
|
56
|
+
requirements:
|
57
|
+
- - ! '>='
|
58
|
+
- !ruby/object:Gem::Version
|
59
|
+
version: '0'
|
60
|
+
requirements: []
|
61
|
+
rubyforge_project:
|
62
|
+
rubygems_version: 1.8.11
|
63
|
+
signing_key:
|
64
|
+
specification_version: 3
|
65
|
+
summary: EventMachine WebSocket client
|
66
|
+
test_files:
|
67
|
+
- spec/websocket_spec.rb
|