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;
73 * @param string $brokerUri Broker URL
74 * @throws Stomp_Exception
76 public function __construct ($brokerUri)
78 $this->_brokerUri = $brokerUri;
82 * Initialize connection
84 * @throws Stomp_Exception
86 protected function _init ()
88 $pattern = "|^(([a-zA-Z]+)://)+\(*([a-zA-Z0-9\.:/i,-]+)\)*\??([a-zA-Z0-9=]*)$|i";
89 if (preg_match($pattern, $this->_brokerUri, $regs)) {
93 if ($scheme != "failover") {
94 $this->_processUrl($this->_brokerUri);
96 $urls = explode(",", $hosts);
97 foreach ($urls as $url) {
98 $this->_processUrl($url);
101 if ($params != null) {
102 parse_str($params, $this->_params);
105 require_once 'Stomp/Exception.php';
106 throw new Stomp_Exception("Bad Broker URL {$this->_brokerUri}");
112 * @param string $url Broker URL
113 * @throws Stomp_Exception
116 protected function _processUrl ($url)
118 $parsed = parse_url($url);
120 array_push($this->_hosts, array($parsed['host'] , $parsed['port'] , $parsed['scheme']));
122 require_once 'Stomp/Exception.php';
123 throw new Stomp_Exception("Bad Broker URL $url");
127 * Make socket connection to the server
129 * @throws Stomp_Exception
131 protected function _makeConnection ()
133 if (count($this->_hosts) == 0) {
134 require_once 'Stomp/Exception.php';
135 throw new Stomp_Exception("No broker defined");
138 // force disconnect, if previous established connection exists
141 $i = $this->_currentHost;
144 while (! $connected && $att ++ < $this->_attempts) {
145 if (isset($this->_params['randomize']) && $this->_params['randomize'] == 'true') {
146 $i = rand(0, count($this->_hosts) - 1);
148 $i = ($i + 1) % count($this->_hosts);
150 $broker = $this->_hosts[$i];
153 $scheme = $broker[2];
155 $port = $this->_defaultPort;
157 if ($this->_socket != null) {
158 fclose($this->_socket);
159 $this->_socket = null;
161 $this->_socket = @fsockopen($scheme . '://' . $host, $port);
162 if (!is_resource($this->_socket) && $att >= $this->_attempts && !array_key_exists($i + 1, $this->_hosts)) {
163 require_once 'Stomp/Exception.php';
164 throw new Stomp_Exception("Could not connect to $host:$port ($att/{$this->_attempts})");
165 } else if (is_resource($this->_socket)) {
167 $this->_currentHost = $i;
172 require_once 'Stomp/Exception.php';
173 throw new Stomp_Exception("Could not connect to a broker");
179 * @param string $username
180 * @param string $password
182 * @throws Stomp_Exception
184 public function connect ($username = '', $password = '')
186 $this->_makeConnection();
187 if ($username != '') {
188 $this->_username = $username;
190 if ($password != '') {
191 $this->_password = $password;
193 $headers = array('login' => $this->_username , 'passcode' => $this->_password);
194 if ($this->clientId != null) {
195 $headers["client-id"] = $this->clientId;
197 $frame = new Stomp_Frame("CONNECT", $headers);
198 $this->_writeFrame($frame);
199 $frame = $this->readFrame();
200 if ($frame instanceof Stomp_Frame && $frame->command == 'CONNECTED') {
201 $this->_sessionId = $frame->headers["session"];
204 require_once 'Stomp/Exception.php';
205 if ($frame instanceof Stomp_Frame) {
206 throw new Stomp_Exception("Unexpected command: {$frame->command}", 0, $frame->body);
208 throw new Stomp_Exception("Connection not acknowledged");
214 * Check if client session has ben established
218 public function isConnected ()
220 return !empty($this->_sessionId) && is_resource($this->_socket);
223 * Current stomp session ID
227 public function getSessionId()
229 return $this->_sessionId;
232 * Send a message to a destination in the messaging system
234 * @param string $destination Destination queue
235 * @param string|Stomp_Frame $msg Message
236 * @param array $properties
237 * @param boolean $sync Perform request synchronously
240 public function send ($destination, $msg, $properties = array(), $sync = null)
242 if ($msg instanceof Stomp_Frame) {
243 $msg->headers['destination'] = $destination;
244 $msg->headers = array_merge($msg->headers, $properties);
247 $headers = $properties;
248 $headers['destination'] = $destination;
249 $frame = new Stomp_Frame('SEND', $headers, $msg);
251 $this->_prepareReceipt($frame, $sync);
252 $this->_writeFrame($frame);
253 return $this->_waitForReceipt($frame, $sync);
256 * Prepair frame receipt
258 * @param Stomp_Frame $frame
259 * @param boolean $sync
261 protected function _prepareReceipt (Stomp_Frame $frame, $sync)
263 $receive = $this->sync;
264 if ($sync !== null) {
267 if ($receive == true) {
268 $frame->headers['receipt'] = md5(microtime());
274 * @param Stomp_Frame $frame
275 * @param boolean $sync
277 * @throws Stomp_Exception
279 protected function _waitForReceipt (Stomp_Frame $frame, $sync)
282 $receive = $this->sync;
283 if ($sync !== null) {
286 if ($receive == true) {
287 $id = (isset($frame->headers['receipt'])) ? $frame->headers['receipt'] : null;
291 $frame = $this->readFrame();
292 if ($frame instanceof Stomp_Frame && $frame->command == 'RECEIPT') {
293 if ($frame->headers['receipt-id'] == $id) {
296 require_once 'Stomp/Exception.php';
297 throw new Stomp_Exception("Unexpected receipt id {$frame->headers['receipt-id']}", 0, $frame->body);
300 require_once 'Stomp/Exception.php';
301 if ($frame instanceof Stomp_Frame) {
302 throw new Stomp_Exception("Unexpected command {$frame->command}", 0, $frame->body);
304 throw new Stomp_Exception("Receipt not received");
311 * Register to listen to a given destination
313 * @param string $destination Destination queue
314 * @param array $properties
315 * @param boolean $sync Perform request synchronously
317 * @throws Stomp_Exception
319 public function subscribe ($destination, $properties = null, $sync = null)
321 $headers = array('ack' => 'client');
322 // FIXME: this seems to be activemq specific, but not hurting rabbitmq?
323 $headers['activemq.prefetchSize'] = $this->prefetchSize;
324 if ($this->clientId != null) {
325 // FIXME: this seems to be activemq specific, but not hurting rabbitmq?
326 $headers["activemq.subcriptionName"] = $this->clientId;
328 if (isset($properties)) {
329 foreach ($properties as $name => $value) {
330 $headers[$name] = $value;
333 $headers['destination'] = $destination;
334 $frame = new Stomp_Frame('SUBSCRIBE', $headers);
335 $this->_prepareReceipt($frame, $sync);
336 $this->_writeFrame($frame);
337 if ($this->_waitForReceipt($frame, $sync) == true) {
338 $this->_subscriptions[$destination] = $properties;
345 * Remove an existing subscription
347 * @param string $destination
348 * @param array $properties
349 * @param boolean $sync Perform request synchronously
351 * @throws Stomp_Exception
353 public function unsubscribe ($destination, $properties = null, $sync = null)
356 if (isset($properties)) {
357 foreach ($properties as $name => $value) {
358 $headers[$name] = $value;
361 $headers['destination'] = $destination;
362 $frame = new Stomp_Frame('UNSUBSCRIBE', $headers);
363 $this->_prepareReceipt($frame, $sync);
364 $this->_writeFrame($frame);
365 if ($this->_waitForReceipt($frame, $sync) == true) {
366 unset($this->_subscriptions[$destination]);
373 * Start a transaction
375 * @param string $transactionId
376 * @param boolean $sync Perform request synchronously
378 * @throws Stomp_Exception
380 public function begin ($transactionId = null, $sync = null)
383 if (isset($transactionId)) {
384 $headers['transaction'] = $transactionId;
386 $frame = new Stomp_Frame('BEGIN', $headers);
387 $this->_prepareReceipt($frame, $sync);
388 $this->_writeFrame($frame);
389 return $this->_waitForReceipt($frame, $sync);
392 * Commit a transaction in progress
394 * @param string $transactionId
395 * @param boolean $sync Perform request synchronously
397 * @throws Stomp_Exception
399 public function commit ($transactionId = null, $sync = null)
402 if (isset($transactionId)) {
403 $headers['transaction'] = $transactionId;
405 $frame = new Stomp_Frame('COMMIT', $headers);
406 $this->_prepareReceipt($frame, $sync);
407 $this->_writeFrame($frame);
408 return $this->_waitForReceipt($frame, $sync);
411 * Roll back a transaction in progress
413 * @param string $transactionId
414 * @param boolean $sync Perform request synchronously
416 public function abort ($transactionId = null, $sync = null)
419 if (isset($transactionId)) {
420 $headers['transaction'] = $transactionId;
422 $frame = new Stomp_Frame('ABORT', $headers);
423 $this->_prepareReceipt($frame, $sync);
424 $this->_writeFrame($frame);
425 return $this->_waitForReceipt($frame, $sync);
428 * Acknowledge consumption of a message from a subscription
429 * Note: This operation is always asynchronous
431 * @param string|Stomp_Frame $messageMessage ID
432 * @param string $transactionId
434 * @throws Stomp_Exception
436 public function ack ($message, $transactionId = null)
438 // Handle the headers,
441 if ($message instanceof Stomp_Frame) {
442 // Copy headers from the object
443 // FIXME: at least content-length can be wrong here (set to 3 sometimes).
444 $headers = $message->headers;
446 if (isset($transactionId)) {
447 $headers['transaction'] = $transactionId;
449 $headers['message-id'] = $message;
451 // An ACK has no content
452 $headers['content-length'] = 0;
454 // Create it and write it out
455 $frame = new Stomp_Frame('ACK', $headers);
456 $this->_writeFrame($frame);
460 * Graceful disconnect from the server
463 public function disconnect ()
467 if ($this->clientId != null) {
468 $headers["client-id"] = $this->clientId;
471 if (is_resource($this->_socket)) {
472 $this->_writeFrame(new Stomp_Frame('DISCONNECT', $headers));
473 fclose($this->_socket);
475 $this->_socket = null;
476 $this->_sessionId = null;
477 $this->_currentHost = -1;
478 $this->_subscriptions = array();
479 $this->_username = '';
480 $this->_password = '';
483 * Write frame to server
485 * @param Stomp_Frame $stompFrame
487 protected function _writeFrame (Stomp_Frame $stompFrame)
489 if (!is_resource($this->_socket)) {
490 require_once 'Stomp/Exception.php';
491 throw new Stomp_Exception('Socket connection hasn\'t been established');
494 $data = $stompFrame->__toString();
495 $r = fwrite($this->_socket, $data, strlen($data));
496 if ($r === false || $r == 0) {
498 $this->_writeFrame($stompFrame);
503 * Set timeout to wait for content to read
505 * @param int $seconds_to_wait Seconds to wait for a frame
506 * @param int $milliseconds Milliseconds to wait for a frame
508 public function setReadTimeout($seconds, $milliseconds = 0)
510 $this->_read_timeout_seconds = $seconds;
511 $this->_read_timeout_milliseconds = $milliseconds;
515 * Read responce frame from server
517 * @return Stomp_Frame|Stomp_Message_Map|boolean False when no frame to read
519 public function readFrame ()
521 if (!$this->hasFrameToRead()) {
528 $read = fread($this->_socket, $rb);
529 if ($read === false) {
531 return $this->readFrame();
534 $len = strlen($data);
537 // ActiveMq apparently add \n after 0 char
538 if($data[$len - 2] == "\x00" && $data[$len - 1] == "\n") {
543 if($data[$len - 1] == "\x00") {
546 } while ( $continue );
547 list ($header, $body) = explode("\n\n", $data, 2);
548 $header = explode("\n", $header);
551 foreach ($header as $v) {
552 if (isset($command)) {
553 list ($name, $value) = explode(':', $v, 2);
554 $headers[$name] = $value;
559 $frame = new Stomp_Frame($command, $headers, trim($body));
560 if (isset($frame->headers['amq-msg-type']) && $frame->headers['amq-msg-type'] == 'MapMessage') {
561 require_once 'Stomp/Message/Map.php';
562 return new Stomp_Message_Map($frame);
569 * Check if there is a frame to read
573 public function hasFrameToRead()
575 $read = array($this->_socket);
579 $has_frame_to_read = stream_select($read, $write, $except, $this->_read_timeout_seconds, $this->_read_timeout_milliseconds);
581 if ($has_frame_to_read === false) {
582 throw new Stomp_Exception('Check failed to determin if the socket is readable');
583 } else if ($has_frame_to_read > 0) {
591 * Reconnects and renews subscriptions (if there were any)
592 * Call this method when you detect connection problems
594 protected function _reconnect ()
596 $subscriptions = $this->_subscriptions;
598 $this->connect($this->_username, $this->_password);
599 foreach ($subscriptions as $dest => $properties) {
600 $this->subscribe($dest, $properties);
604 * Graceful object desruction
607 public function __destruct()