stomp 1.0.6 → 1.1
Sign up to get free protection for your applications and to get access to all the features.
- 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
|
+
|