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 ADDED
@@ -0,0 +1,10 @@
1
+ language: ruby
2
+ cache: bundler
3
+ bundler_args: --without debugger development
4
+ rvm:
5
+ - 1.8
6
+ - 1.9
7
+ - 2.0
8
+ - 2.1
9
+ script:
10
+ - bundle exec rake ci:spec
data/CHANGELOG.rdoc ADDED
@@ -0,0 +1,3 @@
1
+ === 0.1.0 (2015-01-21)
2
+
3
+ Initial commit
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
@@ -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