stomp_out 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- data/.travis.yml +10 -0
- data/CHANGELOG.rdoc +3 -0
- data/LICENSE +20 -0
- data/README.rdoc +91 -0
- data/Rakefile +36 -0
- data/VERSION +1 -0
- data/examples/config.ru +13 -0
- data/examples/websocket_client.rb +153 -0
- data/examples/websocket_server.rb +120 -0
- data/lib/stomp_out/client.rb +580 -0
- data/lib/stomp_out/errors.rb +67 -0
- data/lib/stomp_out/frame.rb +71 -0
- data/lib/stomp_out/heartbeat.rb +151 -0
- data/lib/stomp_out/parser.rb +134 -0
- data/lib/stomp_out/server.rb +667 -0
- data/lib/stomp_out.rb +29 -0
- data/stomp_out.gemspec +95 -0
- metadata +293 -0
data/.travis.yml
ADDED
data/CHANGELOG.rdoc
ADDED
data/LICENSE
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright (c) 2015 RightScale, Inc.
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
4
|
+
a copy of this software and associated documentation files (the
|
5
|
+
'Software'), to deal in the Software without restriction, including
|
6
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
7
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
8
|
+
permit persons to whom the Software is furnished to do so, subject to
|
9
|
+
the following conditions:
|
10
|
+
|
11
|
+
The above copyright notice and this permission notice shall be
|
12
|
+
included in all copies or substantial portions of the Software.
|
13
|
+
|
14
|
+
THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
|
15
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
16
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
17
|
+
IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
|
18
|
+
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
|
19
|
+
TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
|
20
|
+
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.rdoc
ADDED
@@ -0,0 +1,91 @@
|
|
1
|
+
= Introduction
|
2
|
+
|
3
|
+
StompOut is an implementation of the Simple Text Orientated Messaging Protocol (STOMP[http://stomp.github.io]).
|
4
|
+
Both a client and server interface to the protocol are provided.
|
5
|
+
They are implemented independent of the underlying network protocol over which they are applied.
|
6
|
+
In that sense they run outboard of the underlying connection, hence "Out" in the name.
|
7
|
+
The Client and Server classes provide a function for receiving blocks of STOMP encoded
|
8
|
+
data from the connection and they expect the subclass to implement a function
|
9
|
+
for sending data that has been STOMP encoded over the connection.
|
10
|
+
|
11
|
+
The Client and Server classes support version 1.0, 1.1, and 1.2 of STOMP. The built-in version
|
12
|
+
negotiation in the protocol determines which is actually applied for a given connection.
|
13
|
+
|
14
|
+
= Interface
|
15
|
+
|
16
|
+
== Client
|
17
|
+
|
18
|
+
There are two parts to the interface: the functions the StompOut::Client[link:lib/stomp_out/client.rb] class exposes
|
19
|
+
and the functions the Client subclass must implement.
|
20
|
+
|
21
|
+
The Client exposes the following functions:
|
22
|
+
* <b>receive_data</b> - process data received over connection from server
|
23
|
+
* <b>connect</b> - connect to server
|
24
|
+
* <b>message</b> - send message to given destination
|
25
|
+
* <b>subscribe</b> - register to listen to given destination
|
26
|
+
* <b>unsubscribe</b> - remove an existing subscription
|
27
|
+
* <b>ack</b> - acknowledge consumption of a message from a subscription
|
28
|
+
* <b>nack</b> - tell the server that a message was not consumed
|
29
|
+
* <b>begin</b> - start a transaction
|
30
|
+
* <b>commit</b> - commit a transaction
|
31
|
+
* <b>abort</b> - roll back a transaction
|
32
|
+
* <b>disconnect</b> - disconnect from the server
|
33
|
+
* <b>subscriptions</b> - list active subscriptions
|
34
|
+
* <b>transactions</b> - list active transactions
|
35
|
+
|
36
|
+
The Client subclass must implement the following functions:
|
37
|
+
* <b>send_data</b> - send data over connection to server
|
38
|
+
* <b>on_connected</b> - handle notification that now connected
|
39
|
+
* <b>on_message</b> - handle message received from server
|
40
|
+
* <b>on_receipt</b> - handle notification that request was successfully handled by server
|
41
|
+
* <b>on_error</b> - handle notification that a client request failed and connection should be closed
|
42
|
+
|
43
|
+
== Server
|
44
|
+
|
45
|
+
There are two parts to the interface: the functions the StompOut::Server[link:lib/stomp_out/server.rb] class exposes
|
46
|
+
and the functions the Server subclass must implement.
|
47
|
+
|
48
|
+
The Server exposes the following functions:
|
49
|
+
* <b>receive_data</b> - process data received over connection from client
|
50
|
+
* <b>disconnect</b> - stop service in anticipation of connection being closed
|
51
|
+
* <b>message</b> - send message from a subscribed destination to client
|
52
|
+
|
53
|
+
The Server subsclass must implement the following functions:
|
54
|
+
* <b>send_data</b> - send data over connection to client
|
55
|
+
* <b>on_connect</b> - handle connect request from client including any authentication
|
56
|
+
* <b>on_message</b> - handle delivery of message from client to given destination
|
57
|
+
* <b>on_subscribe</b> - subscribe client to messages from given destination
|
58
|
+
* <b>on_unsubscribe</b> - unsubscribe client from given destination
|
59
|
+
* <b>on_ack</b> - handle acknowledgement from client that a message has been successfully processed
|
60
|
+
* <b>on_nack</b> - handle negative acknowledgement from client for a message
|
61
|
+
* <b>on_error</b> - handle notification that a client or server request failed and connection should be closed
|
62
|
+
* <b>on_disconnect</b> - handle request from client to close connection
|
63
|
+
|
64
|
+
The above functions should raise StompOut::ApplicationError[link:lib/stomp_out/errors.rb] for requests
|
65
|
+
that violate any application specific constraints.
|
66
|
+
|
67
|
+
= Example
|
68
|
+
|
69
|
+
The gem contains client[link:examples/websocket_client.rb] and server[link:examples/websocket_server.rb]
|
70
|
+
examples of how it can be applied in a WebSocket environment. The server example can be run in the
|
71
|
+
examples directory using thin:
|
72
|
+
thin -R config.ru start
|
73
|
+
The client example is a script that can executed against the server, e.g., to produce messages:
|
74
|
+
./websocket_client.rb -u ws://0.0.0.0:3000 -d /queue -m "hi"
|
75
|
+
./websocket_client.rb -u ws://0.0.0.0:3000 -d /queue -m "bye"
|
76
|
+
and to consume messages:
|
77
|
+
./websocket_client.rb -u ws://0.0.0.0:3000 -d /queue -a client -r
|
78
|
+
|
79
|
+
= Dependencies
|
80
|
+
|
81
|
+
The simple_uuid gem is used by the Server for generating subscription identifiers.
|
82
|
+
The eventmachine gem is used only if the STOMP heartbeat feature is used.
|
83
|
+
The the json gem is used by the Client only if a content-type is "application/json"
|
84
|
+
and the :auto_json option is enabled.
|
85
|
+
The examples make use of the faye-websocket, eventmachine, trollop, and json gems.
|
86
|
+
|
87
|
+
= License
|
88
|
+
|
89
|
+
This software is released under the {MIT License}[http://www.opensource.org/licenses/MIT]. Please see link:LICENSE for further details.
|
90
|
+
|
91
|
+
Copyright (c) 2015 RightScale
|
data/Rakefile
ADDED
@@ -0,0 +1,36 @@
|
|
1
|
+
# -*-ruby-*-
|
2
|
+
require 'rubygems'
|
3
|
+
require 'rake'
|
4
|
+
require 'right_develop'
|
5
|
+
require 'spec/rake/spectask'
|
6
|
+
require 'rubygems/package_task'
|
7
|
+
require 'rake/clean'
|
8
|
+
|
9
|
+
task :default => [:spec]
|
10
|
+
|
11
|
+
desc "Run unit tests"
|
12
|
+
Spec::Rake::SpecTask.new do |t|
|
13
|
+
t.spec_files = Dir['**/*_spec.rb']
|
14
|
+
t.spec_opts = lambda do
|
15
|
+
IO.readlines(File.join(File.dirname(__FILE__), 'spec', 'spec.opts')).map {|l| l.chomp.split " "}.flatten
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
require 'jeweler'
|
20
|
+
Jeweler::Tasks.new do |gem|
|
21
|
+
# gem is a Gem::Specification; see http://docs.rubygems.org/read/chapter/20 for more options
|
22
|
+
gem.name = "stomp_out"
|
23
|
+
gem.homepage = "https://github.com/rightscale/stomp_out"
|
24
|
+
gem.license = "MIT"
|
25
|
+
gem.summary = %Q{Client and server for STOMP protocol that operate outboard of separately supplied network connection.}
|
26
|
+
gem.description = %Q{This implementation of STOMP is aimed at environments where a network connection, such as a WebSocket or TCP socket, is created and then raw data from that connection is passed to/from the STOMP client or server messaging layer provided by this gem.}
|
27
|
+
gem.email = "support@rightscale.com"
|
28
|
+
gem.authors = ["Lee Kirchhoff"]
|
29
|
+
gem.files.exclude "Gemfile*"
|
30
|
+
gem.files.exclude "spec/**/*"
|
31
|
+
end
|
32
|
+
Jeweler::RubygemsDotOrgTasks.new
|
33
|
+
|
34
|
+
CLEAN.include("pkg")
|
35
|
+
|
36
|
+
RightDevelop::CI::RakeTask.new
|
data/VERSION
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
0.1.0
|
data/examples/config.ru
ADDED
@@ -0,0 +1,13 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
$:.unshift(File.expand_path(File.join(File.dirname(__FILE__), '..', 'lib')))
|
3
|
+
require 'bundler/setup'
|
4
|
+
require 'rack'
|
5
|
+
require 'rack/server'
|
6
|
+
require 'eventmachine'
|
7
|
+
require 'rack/websocket'
|
8
|
+
require 'stomp_out'
|
9
|
+
require 'websocket_server'
|
10
|
+
|
11
|
+
map '/' do
|
12
|
+
run WebSocketServerApp.new
|
13
|
+
end
|
@@ -0,0 +1,153 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require 'rubygems'
|
4
|
+
$:.unshift(File.expand_path(File.join(File.dirname(__FILE__), '..', 'lib')))
|
5
|
+
require 'bundler/setup'
|
6
|
+
require 'eventmachine'
|
7
|
+
require 'faye/websocket'
|
8
|
+
require 'trollop'
|
9
|
+
require 'json'
|
10
|
+
require 'stomp_out'
|
11
|
+
|
12
|
+
# Example of a StompOut::Client subclass in a WebSocket environment
|
13
|
+
class WebSocketClient < StompOut::Client
|
14
|
+
|
15
|
+
def initialize(options = {})
|
16
|
+
options = options.dup
|
17
|
+
@parent = options.delete(:parent)
|
18
|
+
@destination = options.delete(:destination)
|
19
|
+
@ack = options.delete(:ack) || "auto"
|
20
|
+
@message = options.delete(:message)
|
21
|
+
@receipts = {}
|
22
|
+
|
23
|
+
super(options)
|
24
|
+
end
|
25
|
+
|
26
|
+
def send_data(data)
|
27
|
+
@parent.send_data(data)
|
28
|
+
end
|
29
|
+
|
30
|
+
def on_connected(frame, session_id, server_name)
|
31
|
+
puts "connected to #{server_name} for session #{session_id}"
|
32
|
+
if @message
|
33
|
+
receipt_id = message(@destination, @message)
|
34
|
+
@receipts[receipt_id] = "message to #{@destination}" if receipt_id
|
35
|
+
close
|
36
|
+
else
|
37
|
+
puts "subscribing to #{@destination} with ack #{@ack}"
|
38
|
+
receipt_id = subscribe(@destination, @ack, receipt = true)
|
39
|
+
@receipts[receipt_id] = "subscribe to #{@destination} with ack #{@ack}" if receipt_id
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
def on_message(frame, destination, message, content_type, message_id, ack_id)
|
44
|
+
puts "received #{content_type} message #{message_id} from #{destination} " +
|
45
|
+
"with ack #{ack_id.inspect}: #{message.inspect}"
|
46
|
+
if @ack != "auto"
|
47
|
+
receipt_id = ack(ack_id)
|
48
|
+
@receipts[receipt_id] = "ack #{ack_id}" if receipt_id
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
def on_receipt(frame, receipt_id)
|
53
|
+
@subscribed = true if @receipts[receipt_id].to_s =~ /subscribe to/
|
54
|
+
puts "received receipt #{receipt_id} for #{@receipts.delete(receipt_id).inspect}"
|
55
|
+
end
|
56
|
+
|
57
|
+
def on_error(frame, error, details, receipt_id)
|
58
|
+
receipt = receipt_id ? " with receipt #{receipt_id.inspect}" : nil
|
59
|
+
puts "error#{receipt}: #{error}" + (details ? "\n#{details}" : "")
|
60
|
+
close
|
61
|
+
end
|
62
|
+
|
63
|
+
def close
|
64
|
+
if connected?
|
65
|
+
if @subscribed
|
66
|
+
@subscribed = false
|
67
|
+
puts "unsubscribing from #{@destination}"
|
68
|
+
receipt_id = unsubscribe(@destination)
|
69
|
+
@receipts[receipt_id] = "unsubscribe from #{@destination}" if receipt_id
|
70
|
+
end
|
71
|
+
receipt_id = disconnect
|
72
|
+
@receipts[receipt_id] = "disconnect" if receipt_id
|
73
|
+
@parent.stop
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
# Simple application using WebSocketClient
|
79
|
+
class WebSocketClientApp
|
80
|
+
|
81
|
+
def self.run
|
82
|
+
r = WebSocketClientApp.new
|
83
|
+
options = r.parse_args
|
84
|
+
r.start(options)
|
85
|
+
end
|
86
|
+
|
87
|
+
def start(options)
|
88
|
+
['INT', 'TERM'].each do |signal|
|
89
|
+
trap(signal) do stop end
|
90
|
+
end
|
91
|
+
|
92
|
+
EM.run do
|
93
|
+
@client = WebSocketClient.new(options.merge(:parent => self, :name => self.class.name, :auto_json => true))
|
94
|
+
@websocket = Faye::WebSocket::Client.new(options[:url])
|
95
|
+
@websocket.onerror = lambda { |e| puts "error #{e.message}"; stop }
|
96
|
+
@websocket.onclose = lambda { |e| puts "close #{e.code} #{e.reason}"; stop }
|
97
|
+
@websocket.onmessage = lambda { |e| puts "received #{e.data}"; @client.receive_data(JSON.load(e.data)) }
|
98
|
+
@client.connect
|
99
|
+
end
|
100
|
+
end
|
101
|
+
|
102
|
+
def send_data(data)
|
103
|
+
data = JSON.dump(data)
|
104
|
+
puts "sending: #{data}"
|
105
|
+
@websocket.send(data)
|
106
|
+
end
|
107
|
+
|
108
|
+
def stop
|
109
|
+
if EM.reactor_running?
|
110
|
+
@client.close if @client
|
111
|
+
|
112
|
+
EM.next_tick do
|
113
|
+
@websocket.close if @websocket
|
114
|
+
EM.next_tick { EM.stop }
|
115
|
+
end
|
116
|
+
end
|
117
|
+
end
|
118
|
+
|
119
|
+
def parse_args
|
120
|
+
options = {}
|
121
|
+
|
122
|
+
parser = Trollop::Parser.new do
|
123
|
+
opt :url, "server WebSocket URL", :default => nil, :type => String, :short => "-u", :required => true
|
124
|
+
opt :destination, "messaging destination", :default => nil, :type => String, :short => "-d", :required => true
|
125
|
+
opt :host, "server virtual host name", :default => nil, :type => String, :short => "-h"
|
126
|
+
opt :ack, "auto, client, or client-individual acks", :default => nil, :type => String, :short => "-a"
|
127
|
+
opt :receipt, "enable receipts", :default => nil, :short => "-r"
|
128
|
+
opt :message, "send message to destination", :default => nil, :type => String, :short => "-m"
|
129
|
+
version ""
|
130
|
+
end
|
131
|
+
|
132
|
+
parse do
|
133
|
+
options.merge!(parser.parse)
|
134
|
+
end
|
135
|
+
end
|
136
|
+
|
137
|
+
def parse
|
138
|
+
begin
|
139
|
+
yield
|
140
|
+
rescue Trollop::VersionNeeded
|
141
|
+
puts version
|
142
|
+
exit 0
|
143
|
+
rescue Trollop::HelpNeeded
|
144
|
+
puts "Usage: websocket_client --url <s> --destination <s> [--host <s> --ack <s> --receipt --message <s>]"
|
145
|
+
exit 0
|
146
|
+
rescue Trollop::CommandlineError => e
|
147
|
+
STDERR.puts e.message + "\nUse --help for additional information"
|
148
|
+
exit 1
|
149
|
+
end
|
150
|
+
end
|
151
|
+
end
|
152
|
+
|
153
|
+
WebSocketClientApp.run
|
@@ -0,0 +1,120 @@
|
|
1
|
+
|
2
|
+
# Example of a StompOut::Server subclass in a WebSocket environment
|
3
|
+
class WebSocketServer < StompOut::Server
|
4
|
+
|
5
|
+
def initialize(options = {})
|
6
|
+
options = options.dup
|
7
|
+
@parent = options.delete(:parent)
|
8
|
+
@subscriptions = {}
|
9
|
+
@message_ids = {}
|
10
|
+
super(options.merge(:name => self.class.name))
|
11
|
+
end
|
12
|
+
|
13
|
+
def send_data(data)
|
14
|
+
@parent.send_data(data)
|
15
|
+
end
|
16
|
+
|
17
|
+
def on_connect(frame, login, passcode, host, session_id)
|
18
|
+
true
|
19
|
+
end
|
20
|
+
|
21
|
+
def on_message(frame, destination, message, content_type)
|
22
|
+
@parent.deliver_message(destination, message, content_type)
|
23
|
+
end
|
24
|
+
|
25
|
+
def on_subscribe(frame, id, destination, ack_setting)
|
26
|
+
@subscriptions[id] = {:destination => destination, :ack_setting => ack_setting}
|
27
|
+
end
|
28
|
+
|
29
|
+
def on_unsubscribe(frame, id, destination)
|
30
|
+
@subscriptions.delete(id)
|
31
|
+
end
|
32
|
+
|
33
|
+
def on_ack(frame, id)
|
34
|
+
@parent.delete_message(@message_ids.delete(id))
|
35
|
+
end
|
36
|
+
|
37
|
+
def on_nack(frame, id)
|
38
|
+
@message_ids.delete(id)
|
39
|
+
end
|
40
|
+
|
41
|
+
def on_error(frame, error)
|
42
|
+
EM.next_tick { @parent.close_websocket }
|
43
|
+
end
|
44
|
+
|
45
|
+
def on_disconnect(frame, reason)
|
46
|
+
EM.next_tick { @parent.close_websocket }
|
47
|
+
end
|
48
|
+
|
49
|
+
def deliver_messages(destination, messages)
|
50
|
+
@subscriptions.each do |id, s|
|
51
|
+
if s[:destination] == destination
|
52
|
+
messages.each do |m|
|
53
|
+
headers = {
|
54
|
+
"subscription" => id,
|
55
|
+
"destination" => destination,
|
56
|
+
"message-id" => m[:id].to_s,
|
57
|
+
"content-type" => m[:content_type] }
|
58
|
+
message_id, ack_id = message(headers, m[:message])
|
59
|
+
if s[:ack_setting] == "auto"
|
60
|
+
@parent.delete_message(message_id)
|
61
|
+
else
|
62
|
+
@message_ids[ack_id] = message_id
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
# Simple WebSocket Rack application using WebSocketServer
|
71
|
+
class WebSocketServerApp < Rack::WebSocket::Application
|
72
|
+
|
73
|
+
@@servers = {}
|
74
|
+
@@connections = {}
|
75
|
+
@@connection_id = 0
|
76
|
+
@@messages = Hash.new { |h, k| h[k] = [] }
|
77
|
+
@@message_id = 0
|
78
|
+
|
79
|
+
def on_open(env)
|
80
|
+
socket = env["async.connection"]
|
81
|
+
@@connections[socket] = @@connection_id += 1
|
82
|
+
puts "opened connection #{@@connection_id}"
|
83
|
+
@@servers[socket] = WebSocketServer.new(:parent => self)
|
84
|
+
end
|
85
|
+
|
86
|
+
def on_close(env)
|
87
|
+
socket = env["async.connection"]
|
88
|
+
connection = @@connections.delete(socket)
|
89
|
+
puts "closed connection #{connection}"
|
90
|
+
@@servers.delete(socket)
|
91
|
+
end
|
92
|
+
|
93
|
+
def on_error(env, error)
|
94
|
+
socket = env["async.connection"]
|
95
|
+
connection = @@connections[socket]
|
96
|
+
STDERR.puts "error on connection #{connection} (#{error})"
|
97
|
+
end
|
98
|
+
|
99
|
+
def on_message(env, message)
|
100
|
+
socket = env["async.connection"]
|
101
|
+
connection = @@connections[socket]
|
102
|
+
puts "received #{message} on connection #{connection}"
|
103
|
+
@@servers[socket].receive_data(JSON.load(message))
|
104
|
+
end
|
105
|
+
|
106
|
+
def send_data(data)
|
107
|
+
data = JSON.dump(data)
|
108
|
+
puts "sending #{data}"
|
109
|
+
super(data)
|
110
|
+
end
|
111
|
+
|
112
|
+
def deliver_message(destination, message, content_type)
|
113
|
+
@@messages[destination] << {:id => (@@message_id += 1).to_s, :message => message, :content_type => content_type}
|
114
|
+
@@servers.each_value { |s| s.deliver_messages(destination, @@messages[destination]) }
|
115
|
+
end
|
116
|
+
|
117
|
+
def delete_message(id)
|
118
|
+
@@messages.each_value { |ms| ms.reject! { |m| m[:id] == id } }
|
119
|
+
end
|
120
|
+
end
|