4 * Copyright 2005-2006 The Apache Software Foundation
6 * Licensed under the Apache License, Version 2.0 (the "License");
7 * you may not use this file except in compliance with the License.
8 * You may obtain a copy of the License at
10 * http://www.apache.org/licenses/LICENSE-2.0
12 * Unless required by applicable law or agreed to in writing, software
13 * distributed under the License is distributed on an "AS IS" BASIS,
14 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15 * See the License for the specific language governing permissions and
16 * limitations under the License.
19 /* vim: set expandtab tabstop=3 shiftwidth=3: */
21 require_once 'Stomp/Frame.php';
28 * @author Hiram Chirino <hiram@hiramchirino.com>
29 * @author Dejan Bosanac <dejan@nighttale.net>
30 * @author Michael Caplan <mcaplan@labnet.net>
31 * @version $Revision: 43 $
36 * Perform request synchronously
43 * Default prefetch size
47 public $prefetchSize = 1;
50 * Client id used for durable subscriptions
54 public $clientId = null;
56 protected $_brokerUri = null;
57 protected $_socket = null;
58 protected $_hosts = array();
59 protected $_params = array();
60 protected $_subscriptions = array();
61 protected $_defaultPort = 61613;
62 protected $_currentHost = - 1;
63 protected $_attempts = 10;
64 protected $_username = '';
65 protected $_password = '';
66 protected $_sessionId;
67 protected $_read_timeout_seconds = 60;
68 protected $_read_timeout_milliseconds = 0;
69 protected $_connect_timeout_seconds = 60;
74 * @param string $brokerUri Broker URL
75 * @throws StompException
77 public function __construct ($brokerUri)
79 $this->_brokerUri = $brokerUri;
83 * Initialize connection
85 * @throws StompException
87 protected function _init ()
89 $pattern = "|^(([a-zA-Z]+)://)+\(*([a-zA-Z0-9\.:/i,-]+)\)*\??([a-zA-Z0-9=]*)$|i";
90 if (preg_match($pattern, $this->_brokerUri, $regs)) {
94 if ($scheme != "failover") {
95 $this->_processUrl($this->_brokerUri);
97 $urls = explode(",", $hosts);
98 foreach ($urls as $url) {
99 $this->_processUrl($url);
102 if ($params != null) {
103 parse_str($params, $this->_params);
106 require_once 'Stomp/Exception.php';
107 throw new StompException("Bad Broker URL {$this->_brokerUri}");
113 * @param string $url Broker URL
114 * @throws StompException
117 protected function _processUrl ($url)
119 $parsed = parse_url($url);
121 array_push($this->_hosts, array($parsed['host'] , $parsed['port'] , $parsed['scheme']));
123 require_once 'Stomp/Exception.php';
124 throw new StompException("Bad Broker URL $url");
128 * Make socket connection to the server
130 * @throws StompException
132 protected function _makeConnection ()
134 if (count($this->_hosts) == 0) {
135 require_once 'Stomp/Exception.php';
136 throw new StompException("No broker defined");
139 // force disconnect, if previous established connection exists
142 $i = $this->_currentHost;
145 $connect_errno = null;
146 $connect_errstr = null;
148 while (! $connected && $att ++ < $this->_attempts) {
149 if (isset($this->_params['randomize']) && $this->_params['randomize'] == 'true') {
150 $i = rand(0, count($this->_hosts) - 1);
152 $i = ($i + 1) % count($this->_hosts);
154 $broker = $this->_hosts[$i];
157 $scheme = $broker[2];
159 $port = $this->_defaultPort;
161 if ($this->_socket != null) {
162 fclose($this->_socket);
163 $this->_socket = null;
165 $this->_socket = @fsockopen($scheme . '://' . $host, $port, $connect_errno, $connect_errstr, $this->_connect_timeout_seconds);
166 if (!is_resource($this->_socket) && $att >= $this->_attempts && !array_key_exists($i + 1, $this->_hosts)) {
167 require_once 'Stomp/Exception.php';
168 throw new StompException("Could not connect to $host:$port ($att/{$this->_attempts})");
169 } else if (is_resource($this->_socket)) {
171 $this->_currentHost = $i;
176 require_once 'Stomp/Exception.php';
177 throw new StompException("Could not connect to a broker");
183 * @param string $username
184 * @param string $password
186 * @throws StompException
188 public function connect ($username = '', $password = '')
190 $this->_makeConnection();
191 if ($username != '') {
192 $this->_username = $username;
194 if ($password != '') {
195 $this->_password = $password;
197 $headers = array('login' => $this->_username , 'passcode' => $this->_password);
198 if ($this->clientId != null) {
199 $headers["client-id"] = $this->clientId;
201 $frame = new StompFrame("CONNECT", $headers);
202 $this->_writeFrame($frame);
203 $frame = $this->readFrame();
204 if ($frame instanceof StompFrame && $frame->command == 'CONNECTED') {
205 $this->_sessionId = $frame->headers["session"];
208 require_once 'Stomp/Exception.php';
209 if ($frame instanceof StompFrame) {
210 throw new StompException("Unexpected command: {$frame->command}", 0, $frame->body);
212 throw new StompException("Connection not acknowledged");
218 * Check if client session has ben established
222 public function isConnected ()
224 return !empty($this->_sessionId) && is_resource($this->_socket);
227 * Current stomp session ID
231 public function getSessionId()
233 return $this->_sessionId;
236 * Send a message to a destination in the messaging system
238 * @param string $destination Destination queue
239 * @param string|StompFrame $msg Message
240 * @param array $properties
241 * @param boolean $sync Perform request synchronously
244 public function send ($destination, $msg, $properties = array(), $sync = null)
246 if ($msg instanceof StompFrame) {
247 $msg->headers['destination'] = $destination;
248 if (is_array($properties)) $msg->headers = array_merge($msg->headers, $properties);
251 $headers = $properties;
252 $headers['destination'] = $destination;
253 $frame = new StompFrame('SEND', $headers, $msg);
255 $this->_prepareReceipt($frame, $sync);
256 $this->_writeFrame($frame);
257 return $this->_waitForReceipt($frame, $sync);
260 * Prepair frame receipt
262 * @param StompFrame $frame
263 * @param boolean $sync
265 protected function _prepareReceipt (StompFrame $frame, $sync)
267 $receive = $this->sync;
268 if ($sync !== null) {
271 if ($receive == true) {
272 $frame->headers['receipt'] = md5(microtime());
278 * @param StompFrame $frame
279 * @param boolean $sync
281 * @throws StompException
283 protected function _waitForReceipt (StompFrame $frame, $sync)
286 $receive = $this->sync;
287 if ($sync !== null) {
290 if ($receive == true) {
291 $id = (isset($frame->headers['receipt'])) ? $frame->headers['receipt'] : null;
295 $frame = $this->readFrame();
296 if ($frame instanceof StompFrame && $frame->command == 'RECEIPT') {
297 if ($frame->headers['receipt-id'] == $id) {
300 require_once 'Stomp/Exception.php';
301 throw new StompException("Unexpected receipt id {$frame->headers['receipt-id']}", 0, $frame->body);
304 require_once 'Stomp/Exception.php';
305 if ($frame instanceof StompFrame) {
306 throw new StompException("Unexpected command {$frame->command}", 0, $frame->body);
308 throw new StompException("Receipt not received");
315 * Register to listen to a given destination
317 * @param string $destination Destination queue
318 * @param array $properties
319 * @param boolean $sync Perform request synchronously
321 * @throws StompException
323 public function subscribe ($destination, $properties = null, $sync = null)
325 $headers = array('ack' => 'client');
326 $headers['activemq.prefetchSize'] = $this->prefetchSize;
327 $headers['prefetch-count'] = '1';
329 if ($this->clientId != null) {
330 $headers["activemq.subcriptionName"] = $this->clientId;
332 if (isset($properties)) {
333 foreach ($properties as $name => $value) {
334 $headers[$name] = $value;
337 $headers['destination'] = $destination;
338 $frame = new StompFrame('SUBSCRIBE', $headers);
339 $this->_prepareReceipt($frame, $sync);
340 $this->_writeFrame($frame);
341 if ($this->_waitForReceipt($frame, $sync) == true) {
342 $this->_subscriptions[$destination] = $properties;
349 * Remove an existing subscription
351 * @param string $destination
352 * @param array $properties
353 * @param boolean $sync Perform request synchronously
355 * @throws StompException
357 public function unsubscribe ($destination, $properties = null, $sync = null)
360 if (isset($properties)) {
361 foreach ($properties as $name => $value) {
362 $headers[$name] = $value;
365 $headers['destination'] = $destination;
366 $frame = new StompFrame('UNSUBSCRIBE', $headers);
367 $this->_prepareReceipt($frame, $sync);
368 $this->_writeFrame($frame);
369 if ($this->_waitForReceipt($frame, $sync) == true) {
370 unset($this->_subscriptions[$destination]);
377 * Start a transaction
379 * @param string $transactionId
380 * @param boolean $sync Perform request synchronously
382 * @throws StompException
384 public function begin ($transactionId = null, $sync = null)
387 if (isset($transactionId)) {
388 $headers['transaction'] = $transactionId;
390 $frame = new StompFrame('BEGIN', $headers);
391 $this->_prepareReceipt($frame, $sync);
392 $this->_writeFrame($frame);
393 return $this->_waitForReceipt($frame, $sync);
396 * Commit a transaction in progress
398 * @param string $transactionId
399 * @param boolean $sync Perform request synchronously
401 * @throws StompException
403 public function commit ($transactionId = null, $sync = null)
406 if (isset($transactionId)) {
407 $headers['transaction'] = $transactionId;
409 $frame = new StompFrame('COMMIT', $headers);
410 $this->_prepareReceipt($frame, $sync);
411 $this->_writeFrame($frame);
412 return $this->_waitForReceipt($frame, $sync);
415 * Roll back a transaction in progress
417 * @param string $transactionId
418 * @param boolean $sync Perform request synchronously
420 public function abort ($transactionId = null, $sync = null)
423 if (isset($transactionId)) {
424 $headers['transaction'] = $transactionId;
426 $frame = new StompFrame('ABORT', $headers);
427 $this->_prepareReceipt($frame, $sync);
428 $this->_writeFrame($frame);
429 return $this->_waitForReceipt($frame, $sync);
432 * Acknowledge consumption of a message from a subscription
433 * Note: This operation is always asynchronous
435 * @param string|StompFrame $messageMessage ID
436 * @param string $transactionId
438 * @throws StompException
440 public function ack ($message, $transactionId = null)
442 if ($message instanceof StompFrame) {
443 $headers = $message->headers;
444 if (isset($transactionId)) {
445 $headers['transaction'] = $transactionId;
447 $frame = new StompFrame('ACK', $headers);
448 $this->_writeFrame($frame);
452 if (isset($transactionId)) {
453 $headers['transaction'] = $transactionId;
455 $headers['message-id'] = $message;
456 $frame = new StompFrame('ACK', $headers);
457 $this->_writeFrame($frame);
462 * Graceful disconnect from the server
465 public function disconnect ()
469 if ($this->clientId != null) {
470 $headers["client-id"] = $this->clientId;
473 if (is_resource($this->_socket)) {
474 $this->_writeFrame(new StompFrame('DISCONNECT', $headers));
475 fclose($this->_socket);
477 $this->_socket = null;
478 $this->_sessionId = null;
479 $this->_currentHost = -1;
480 $this->_subscriptions = array();
481 $this->_username = '';
482 $this->_password = '';
485 * Write frame to server
487 * @param StompFrame $stompFrame
489 protected function _writeFrame (StompFrame $stompFrame)
491 if (!is_resource($this->_socket)) {
492 require_once 'Stomp/Exception.php';
493 throw new StompException('Socket connection hasn\'t been established');
496 $data = $stompFrame->__toString();
497 $r = fwrite($this->_socket, $data, strlen($data));
498 if ($r === false || $r == 0) {
500 $this->_writeFrame($stompFrame);
505 * Set timeout to wait for content to read
507 * @param int $seconds_to_wait Seconds to wait for a frame
508 * @param int $milliseconds Milliseconds to wait for a frame
510 public function setReadTimeout($seconds, $milliseconds = 0)
512 $this->_read_timeout_seconds = $seconds;
513 $this->_read_timeout_milliseconds = $milliseconds;
517 * Read response frame from server
519 * @return StompFrame False when no frame to read
521 public function readFrame ()
523 if (!$this->hasFrameToRead()) {
532 $read = fread($this->_socket, $rb);
533 if ($read === false) {
535 return $this->readFrame();
538 if (strpos($data, "\x00") !== false) {
540 $data = rtrim($data, "\n");
542 $len = strlen($data);
543 } while ($len < 2 || $end == false);
545 list ($header, $body) = explode("\n\n", $data, 2);
546 $header = explode("\n", $header);
549 foreach ($header as $v) {
550 if (isset($command)) {
551 list ($name, $value) = explode(':', $v, 2);
552 $headers[$name] = $value;
557 $frame = new StompFrame($command, $headers, trim($body));
558 if (isset($frame->headers['transformation']) && $frame->headers['transformation'] == 'jms-map-json') {
559 require_once 'Stomp/Message/Map.php';
560 return new StompMessageMap($frame);
568 * Check if there is a frame to read
572 public function hasFrameToRead()
574 $read = array($this->_socket);
578 $has_frame_to_read = @stream_select($read, $write, $except, $this->_read_timeout_seconds, $this->_read_timeout_milliseconds);
580 if ($has_frame_to_read !== false)
581 $has_frame_to_read = count($read);
584 if ($has_frame_to_read === false) {
585 throw new StompException('Check failed to determine if the socket is readable');
586 } else if ($has_frame_to_read > 0) {
594 * Reconnects and renews subscriptions (if there were any)
595 * Call this method when you detect connection problems
597 protected function _reconnect ()
599 $subscriptions = $this->_subscriptions;
601 $this->connect($this->_username, $this->_password);
602 foreach ($subscriptions as $dest => $properties) {
603 $this->subscribe($dest, $properties);
607 * Graceful object desruction
610 public function __destruct()