stomp_out 0.1.0
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/.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
|