stomp 1.0.6 → 1.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/CHANGELOG +25 -0
- data/LICENSE +202 -0
- data/README.rdoc +36 -0
- data/Rakefile +52 -0
- data/bin/catstomp +55 -0
- data/bin/stompcat +56 -0
- data/lib/stomp.rb +4 -398
- data/lib/stomp/client.rb +207 -0
- data/lib/stomp/connection.rb +256 -0
- data/lib/stomp/message.rb +17 -0
- data/test/test_client.rb +182 -0
- data/test/test_connection.rb +95 -0
- data/test/test_helper.rb +5 -0
- metadata +41 -15
data/lib/stomp/client.rb
ADDED
@@ -0,0 +1,207 @@
|
|
1
|
+
module Stomp
|
2
|
+
|
3
|
+
# Typical Stomp client class. Uses a listener thread to receive frames
|
4
|
+
# from the server, any thread can send.
|
5
|
+
#
|
6
|
+
# Receives all happen in one thread, so consider not doing much processing
|
7
|
+
# in that thread if you have much message volume.
|
8
|
+
class Client
|
9
|
+
|
10
|
+
attr_reader :login, :passcode, :host, :port, :reliable, :running
|
11
|
+
|
12
|
+
# A new Client object can be initialized using two forms:
|
13
|
+
#
|
14
|
+
# Standard positional parameters:
|
15
|
+
# login (String, default : '')
|
16
|
+
# passcode (String, default : '')
|
17
|
+
# host (String, default : 'localhost')
|
18
|
+
# port (Integer, default : 61613)
|
19
|
+
# reliable (Boolean, default : false)
|
20
|
+
#
|
21
|
+
# e.g. c = Client.new('login', 'passcode', 'localhost', 61613, true)
|
22
|
+
#
|
23
|
+
# Stomp URL :
|
24
|
+
# A Stomp URL must begin with 'stomp://' and can be in one of the following forms:
|
25
|
+
#
|
26
|
+
# stomp://host:port
|
27
|
+
# stomp://host.domain.tld:port
|
28
|
+
# stomp://login:passcode@host:port
|
29
|
+
# stomp://login:passcode@host.domain.tld:port
|
30
|
+
#
|
31
|
+
def initialize(login = '', passcode = '', host = 'localhost', port = 61613, reliable = false)
|
32
|
+
|
33
|
+
# Parse stomp:// URL's or set positional params
|
34
|
+
case login
|
35
|
+
when /stomp:\/\/([\w\.]+):(\d+)/ # e.g. stomp://host:port
|
36
|
+
# grabs the matching positions out of the regex which are stored as
|
37
|
+
# $1 (host), $2 (port), etc
|
38
|
+
@login = ''
|
39
|
+
@passcode = ''
|
40
|
+
@host = $1
|
41
|
+
@port = $2.to_i
|
42
|
+
@reliable = false
|
43
|
+
when /stomp:\/\/([\w\.]+):(\w+)@([\w\.]+):(\d+)/ # e.g. stomp://login:passcode@host:port
|
44
|
+
@login = $1
|
45
|
+
@passcode = $2
|
46
|
+
@host = $3
|
47
|
+
@port = $4.to_i
|
48
|
+
@reliable = false
|
49
|
+
else
|
50
|
+
@login = login
|
51
|
+
@passcode = passcode
|
52
|
+
@host = host
|
53
|
+
@port = port.to_i
|
54
|
+
@reliable = reliable
|
55
|
+
end
|
56
|
+
|
57
|
+
raise ArgumentError if @host.nil? || @host.empty?
|
58
|
+
raise ArgumentError if @port.nil? || @port == '' || @port < 1 || @port > 65535
|
59
|
+
raise ArgumentError unless @reliable.is_a?(TrueClass) || @reliable.is_a?(FalseClass)
|
60
|
+
|
61
|
+
@id_mutex = Mutex.new
|
62
|
+
@ids = 1
|
63
|
+
@connection = Connection.new(@login, @passcode, @host, @port, @reliable)
|
64
|
+
@listeners = {}
|
65
|
+
@receipt_listeners = {}
|
66
|
+
@running = true
|
67
|
+
@replay_messages_by_txn = {}
|
68
|
+
|
69
|
+
@listener_thread = Thread.start do
|
70
|
+
while @running
|
71
|
+
message = @connection.receive
|
72
|
+
case
|
73
|
+
when message.nil?
|
74
|
+
break
|
75
|
+
when message.command == 'MESSAGE'
|
76
|
+
if listener = @listeners[message.headers['destination']]
|
77
|
+
listener.call(message)
|
78
|
+
end
|
79
|
+
when message.command == 'RECEIPT'
|
80
|
+
if listener = @receipt_listeners[message.headers['receipt-id']]
|
81
|
+
listener.call(message)
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
end
|
88
|
+
|
89
|
+
# Syntactic sugar for 'Client.new' See 'initialize' for usage.
|
90
|
+
def self.open(login = '', passcode = '', host = 'localhost', port = 61613, reliable = false)
|
91
|
+
Client.new(login, passcode, host, port, reliable)
|
92
|
+
end
|
93
|
+
|
94
|
+
# Join the listener thread for this client,
|
95
|
+
# generally used to wait for a quit signal
|
96
|
+
def join
|
97
|
+
@listener_thread.join
|
98
|
+
end
|
99
|
+
|
100
|
+
# Begin a transaction by name
|
101
|
+
def begin(name, headers = {})
|
102
|
+
@connection.begin(name, headers)
|
103
|
+
end
|
104
|
+
|
105
|
+
# Abort a transaction by name
|
106
|
+
def abort(name, headers = {})
|
107
|
+
@connection.abort(name, headers)
|
108
|
+
|
109
|
+
# lets replay any ack'd messages in this transaction
|
110
|
+
replay_list = @replay_messages_by_txn[name]
|
111
|
+
if replay_list
|
112
|
+
replay_list.each do |message|
|
113
|
+
if listener = @listeners[message.headers['destination']]
|
114
|
+
listener.call(message)
|
115
|
+
end
|
116
|
+
end
|
117
|
+
end
|
118
|
+
end
|
119
|
+
|
120
|
+
# Commit a transaction by name
|
121
|
+
def commit(name, headers = {})
|
122
|
+
txn_id = headers[:transaction]
|
123
|
+
@replay_messages_by_txn.delete(txn_id)
|
124
|
+
@connection.commit(name, headers)
|
125
|
+
end
|
126
|
+
|
127
|
+
# Subscribe to a destination, must be passed a block
|
128
|
+
# which will be used as a callback listener
|
129
|
+
#
|
130
|
+
# Accepts a transaction header ( :transaction => 'some_transaction_id' )
|
131
|
+
def subscribe(destination, headers = {})
|
132
|
+
raise "No listener given" unless block_given?
|
133
|
+
@listeners[destination] = lambda {|msg| yield msg}
|
134
|
+
@connection.subscribe(destination, headers)
|
135
|
+
end
|
136
|
+
|
137
|
+
# Unsubecribe from a channel
|
138
|
+
def unsubscribe(name, headers = {})
|
139
|
+
@connection.unsubscribe(name, headers)
|
140
|
+
@listeners[name] = nil
|
141
|
+
end
|
142
|
+
|
143
|
+
# Acknowledge a message, used when a subscription has specified
|
144
|
+
# client acknowledgement ( connection.subscribe "/queue/a", :ack => 'client'g
|
145
|
+
#
|
146
|
+
# Accepts a transaction header ( :transaction => 'some_transaction_id' )
|
147
|
+
def acknowledge(message, headers = {})
|
148
|
+
txn_id = headers[:transaction]
|
149
|
+
if txn_id
|
150
|
+
# lets keep around messages ack'd in this transaction in case we rollback
|
151
|
+
replay_list = @replay_messages_by_txn[txn_id]
|
152
|
+
if replay_list.nil?
|
153
|
+
replay_list = []
|
154
|
+
@replay_messages_by_txn[txn_id] = replay_list
|
155
|
+
end
|
156
|
+
replay_list << message
|
157
|
+
end
|
158
|
+
if block_given?
|
159
|
+
headers['receipt'] = register_receipt_listener lambda {|r| yield r}
|
160
|
+
end
|
161
|
+
@connection.ack message.headers['message-id'], headers
|
162
|
+
end
|
163
|
+
|
164
|
+
# Send message to destination
|
165
|
+
#
|
166
|
+
# If a block is given a receipt will be requested and passed to the
|
167
|
+
# block on receipt
|
168
|
+
#
|
169
|
+
# Accepts a transaction header ( :transaction => 'some_transaction_id' )
|
170
|
+
def send(destination, message, headers = {})
|
171
|
+
if block_given?
|
172
|
+
headers['receipt'] = register_receipt_listener lambda {|r| yield r}
|
173
|
+
end
|
174
|
+
@connection.send(destination, message, headers)
|
175
|
+
end
|
176
|
+
|
177
|
+
# Is this client open?
|
178
|
+
def open?
|
179
|
+
@connection.open?
|
180
|
+
end
|
181
|
+
|
182
|
+
# Is this client closed?
|
183
|
+
def closed?
|
184
|
+
@connection.closed?
|
185
|
+
end
|
186
|
+
|
187
|
+
# Close out resources in use by this client
|
188
|
+
def close
|
189
|
+
@connection.disconnect
|
190
|
+
@running = false
|
191
|
+
end
|
192
|
+
|
193
|
+
private
|
194
|
+
|
195
|
+
def register_receipt_listener(listener)
|
196
|
+
id = -1
|
197
|
+
@id_mutex.synchronize do
|
198
|
+
id = @ids.to_s
|
199
|
+
@ids = @ids.succ
|
200
|
+
end
|
201
|
+
@receipt_listeners[id] = listener
|
202
|
+
id
|
203
|
+
end
|
204
|
+
|
205
|
+
end
|
206
|
+
end
|
207
|
+
|
@@ -0,0 +1,256 @@
|
|
1
|
+
module Stomp
|
2
|
+
|
3
|
+
# Low level connection which maps commands and supports
|
4
|
+
# synchronous receives
|
5
|
+
class Connection
|
6
|
+
|
7
|
+
|
8
|
+
# A new Connection object accepts the following parameters:
|
9
|
+
#
|
10
|
+
# login (String, default : '')
|
11
|
+
# passcode (String, default : '')
|
12
|
+
# host (String, default : 'localhost')
|
13
|
+
# port (Integer, default : 61613)
|
14
|
+
# reliable (Boolean, default : false)
|
15
|
+
# reconnect_delay (Integer, default : 5)
|
16
|
+
#
|
17
|
+
# e.g. c = Client.new("username", "password", "localhost", 61613, true)
|
18
|
+
#
|
19
|
+
# TODO
|
20
|
+
# Stomp URL :
|
21
|
+
# A Stomp URL must begin with 'stomp://' and can be in one of the following forms:
|
22
|
+
#
|
23
|
+
# stomp://host:port
|
24
|
+
# stomp://host.domain.tld:port
|
25
|
+
# stomp://user:pass@host:port
|
26
|
+
# stomp://user:pass@host.domain.tld:port
|
27
|
+
#
|
28
|
+
def initialize(login = '', passcode = '', host = 'localhost', port = 61613, reliable = false, reconnect_delay = 5, connect_headers = {})
|
29
|
+
@host = host
|
30
|
+
@port = port
|
31
|
+
@login = login
|
32
|
+
@passcode = passcode
|
33
|
+
@transmit_semaphore = Mutex.new
|
34
|
+
@read_semaphore = Mutex.new
|
35
|
+
@socket_semaphore = Mutex.new
|
36
|
+
@reliable = reliable
|
37
|
+
@reconnect_delay = reconnect_delay
|
38
|
+
@connect_headers = connect_headers
|
39
|
+
@closed = false
|
40
|
+
@subscriptions = {}
|
41
|
+
@failure = nil
|
42
|
+
socket
|
43
|
+
end
|
44
|
+
|
45
|
+
# Syntactic sugar for 'Connection.new' See 'initialize' for usage.
|
46
|
+
def Connection.open(login = '', passcode = '', host = 'localhost', port = 61613, reliable = false, reconnect_delay = 5, connect_headers = {})
|
47
|
+
Connection.new(login, passcode, host, port, reliable, reconnect_delay, connect_headers)
|
48
|
+
end
|
49
|
+
|
50
|
+
def socket
|
51
|
+
# Need to look into why the following synchronize does not work.
|
52
|
+
#@read_semaphore.synchronize do
|
53
|
+
s = @socket;
|
54
|
+
while s.nil? || !@failure.nil?
|
55
|
+
@failure = nil
|
56
|
+
begin
|
57
|
+
s = TCPSocket.open @host, @port
|
58
|
+
headers = @connect_headers.clone
|
59
|
+
headers[:login] = @login
|
60
|
+
headers[:passcode] = @passcode
|
61
|
+
_transmit(s, "CONNECT", headers)
|
62
|
+
@connect = _receive(s)
|
63
|
+
# replay any subscriptions.
|
64
|
+
@subscriptions.each { |k,v| _transmit(s, "SUBSCRIBE", v) }
|
65
|
+
rescue
|
66
|
+
@failure = $!;
|
67
|
+
s=nil;
|
68
|
+
raise unless @reliable
|
69
|
+
$stderr.print "connect failed: " + $! +" will retry in #{@reconnect_delay}\n";
|
70
|
+
sleep(@reconnect_delay);
|
71
|
+
end
|
72
|
+
end
|
73
|
+
@socket = s
|
74
|
+
return s;
|
75
|
+
#end
|
76
|
+
end
|
77
|
+
|
78
|
+
# Is this connection open?
|
79
|
+
def open?
|
80
|
+
!@closed
|
81
|
+
end
|
82
|
+
|
83
|
+
# Is this connection closed?
|
84
|
+
def closed?
|
85
|
+
@closed
|
86
|
+
end
|
87
|
+
|
88
|
+
# Begin a transaction, requires a name for the transaction
|
89
|
+
def begin(name, headers = {})
|
90
|
+
headers[:transaction] = name
|
91
|
+
transmit("BEGIN", headers)
|
92
|
+
end
|
93
|
+
|
94
|
+
# Acknowledge a message, used when a subscription has specified
|
95
|
+
# client acknowledgement ( connection.subscribe "/queue/a", :ack => 'client'g
|
96
|
+
#
|
97
|
+
# Accepts a transaction header ( :transaction => 'some_transaction_id' )
|
98
|
+
def ack(message_id, headers = {})
|
99
|
+
headers['message-id'] = message_id
|
100
|
+
transmit("ACK", headers)
|
101
|
+
end
|
102
|
+
|
103
|
+
# Commit a transaction by name
|
104
|
+
def commit(name, headers = {})
|
105
|
+
headers[:transaction] = name
|
106
|
+
transmit("COMMIT", headers)
|
107
|
+
end
|
108
|
+
|
109
|
+
# Abort a transaction by name
|
110
|
+
def abort(name, headers = {})
|
111
|
+
headers[:transaction] = name
|
112
|
+
transmit("ABORT", headers)
|
113
|
+
end
|
114
|
+
|
115
|
+
# Subscribe to a destination, must specify a name
|
116
|
+
def subscribe(name, headers = {}, subId = nil)
|
117
|
+
headers[:destination] = name
|
118
|
+
transmit("SUBSCRIBE", headers)
|
119
|
+
|
120
|
+
# Store the sub so that we can replay if we reconnect.
|
121
|
+
if @reliable
|
122
|
+
subId = name if subId.nil?
|
123
|
+
@subscriptions[subId] = headers
|
124
|
+
end
|
125
|
+
end
|
126
|
+
|
127
|
+
# Unsubscribe from a destination, must specify a name
|
128
|
+
def unsubscribe(name, headers = {}, subId = nil)
|
129
|
+
headers[:destination] = name
|
130
|
+
transmit("UNSUBSCRIBE", headers)
|
131
|
+
if @reliable
|
132
|
+
subId = name if subId.nil?
|
133
|
+
@subscriptions.delete(subId)
|
134
|
+
end
|
135
|
+
end
|
136
|
+
|
137
|
+
# Send message to destination
|
138
|
+
#
|
139
|
+
# Accepts a transaction header ( :transaction => 'some_transaction_id' )
|
140
|
+
def send(destination, message, headers = {})
|
141
|
+
headers[:destination] = destination
|
142
|
+
transmit("SEND", headers, message)
|
143
|
+
end
|
144
|
+
|
145
|
+
# Close this connection
|
146
|
+
def disconnect(headers = {})
|
147
|
+
transmit("DISCONNECT", headers)
|
148
|
+
@closed = true
|
149
|
+
end
|
150
|
+
|
151
|
+
# Return a pending message if one is available, otherwise
|
152
|
+
# return nil
|
153
|
+
def poll
|
154
|
+
@read_semaphore.synchronize do
|
155
|
+
return nil if @socket.nil? || !@socket.ready?
|
156
|
+
return receive
|
157
|
+
end
|
158
|
+
end
|
159
|
+
|
160
|
+
# Receive a frame, block until the frame is received
|
161
|
+
def __old_receive
|
162
|
+
# The recive my fail so we may need to retry.
|
163
|
+
while TRUE
|
164
|
+
begin
|
165
|
+
s = socket
|
166
|
+
return _receive(s)
|
167
|
+
rescue
|
168
|
+
@failure = $!;
|
169
|
+
raise unless @reliable
|
170
|
+
$stderr.print "receive failed: " + $!;
|
171
|
+
end
|
172
|
+
end
|
173
|
+
end
|
174
|
+
|
175
|
+
def receive
|
176
|
+
super_result = __old_receive()
|
177
|
+
if super_result.nil? && @reliable
|
178
|
+
$stderr.print "connection.receive returning EOF as nil - resetting connection.\n"
|
179
|
+
@socket = nil
|
180
|
+
super_result = __old_receive()
|
181
|
+
end
|
182
|
+
return super_result
|
183
|
+
end
|
184
|
+
|
185
|
+
private
|
186
|
+
|
187
|
+
def _receive( s )
|
188
|
+
line = ' '
|
189
|
+
@read_semaphore.synchronize do
|
190
|
+
line = s.gets while line =~ /^\s*$/
|
191
|
+
return nil if line.nil?
|
192
|
+
|
193
|
+
message = Message.new do |m|
|
194
|
+
m.command = line.chomp
|
195
|
+
m.headers = {}
|
196
|
+
until (line = s.gets.chomp) == ''
|
197
|
+
k = (line.strip[0, line.strip.index(':')]).strip
|
198
|
+
v = (line.strip[line.strip.index(':') + 1, line.strip.length]).strip
|
199
|
+
m.headers[k] = v
|
200
|
+
end
|
201
|
+
|
202
|
+
if (m.headers['content-length'])
|
203
|
+
m.body = s.read m.headers['content-length'].to_i
|
204
|
+
c = RUBY_VERSION > '1.9' ? s.getc.ord : s.getc
|
205
|
+
raise "Invalid content length received" unless c == 0
|
206
|
+
else
|
207
|
+
m.body = ''
|
208
|
+
if RUBY_VERSION > '1.9'
|
209
|
+
until (c = s.getc.ord) == 0
|
210
|
+
m.body << c.chr
|
211
|
+
end
|
212
|
+
else
|
213
|
+
until (c = s.getc) == 0
|
214
|
+
m.body << c.chr
|
215
|
+
end
|
216
|
+
end
|
217
|
+
end
|
218
|
+
#c = s.getc
|
219
|
+
#raise "Invalid frame termination received" unless c == 10
|
220
|
+
end # message
|
221
|
+
return message
|
222
|
+
|
223
|
+
end
|
224
|
+
end
|
225
|
+
|
226
|
+
def transmit(command, headers = {}, body = '')
|
227
|
+
# The transmit may fail so we may need to retry.
|
228
|
+
while TRUE
|
229
|
+
begin
|
230
|
+
s = socket
|
231
|
+
_transmit(s, command, headers, body)
|
232
|
+
return
|
233
|
+
rescue
|
234
|
+
@failure = $!;
|
235
|
+
raise unless @reliable
|
236
|
+
$stderr.print "transmit failed: " + $!+"\n";
|
237
|
+
end
|
238
|
+
end
|
239
|
+
end
|
240
|
+
|
241
|
+
def _transmit(s, command, headers = {}, body = '')
|
242
|
+
@transmit_semaphore.synchronize do
|
243
|
+
s.puts command
|
244
|
+
headers.each {|k,v| s.puts "#{k}:#{v}" }
|
245
|
+
s.puts "content-length: #{body.length}"
|
246
|
+
s.puts "content-type: text/plain; charset=UTF-8"
|
247
|
+
s.puts
|
248
|
+
s.write body
|
249
|
+
s.write "\0"
|
250
|
+
end
|
251
|
+
end
|
252
|
+
|
253
|
+
end
|
254
|
+
|
255
|
+
end
|
256
|
+
|