We are no longer offering accounts on this server. Consider https://gitlab.freedesktop.org/ as a place to host projects.

Stomp.php 18.4 KB
Newer Older
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68
<?php
/**
 *
 * Copyright 2005-2006 The Apache Software Foundation
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

/* vim: set expandtab tabstop=3 shiftwidth=3: */

require_once 'Stomp/Frame.php';

/**
 * A Stomp Connection
 *
 *
 * @package Stomp
 * @author Hiram Chirino <hiram@hiramchirino.com>
 * @author Dejan Bosanac <dejan@nighttale.net> 
 * @author Michael Caplan <mcaplan@labnet.net>
 * @version $Revision: 43 $
 */
class Stomp
{
    /**
     * Perform request synchronously
     *
     * @var boolean
     */
    public $sync = false;

    /**
     * Default prefetch size
     *
     * @var int
     */
	public $prefetchSize = 1;
    
	/**
     * Client id used for durable subscriptions
     *
     * @var string
     */
	public $clientId = null;
    
    protected $_brokerUri = null;
    protected $_socket = null;
    protected $_hosts = array();
    protected $_params = array();
    protected $_subscriptions = array();
    protected $_defaultPort = 61613;
    protected $_currentHost = - 1;
    protected $_attempts = 10;
    protected $_username = '';
    protected $_password = '';
    protected $_sessionId;
    protected $_read_timeout_seconds = 60;
    protected $_read_timeout_milliseconds = 0;
Craig Andrews's avatar
Craig Andrews committed
69
    protected $_connect_timeout_seconds = 60;
70 71 72 73 74
    
    /**
     * Constructor
     *
     * @param string $brokerUri Broker URL
Craig Andrews's avatar
Craig Andrews committed
75
     * @throws StompException
76 77 78 79 80 81 82 83 84
     */
    public function __construct ($brokerUri)
    {
        $this->_brokerUri = $brokerUri;
        $this->_init();
    }
    /**
     * Initialize connection
     *
Craig Andrews's avatar
Craig Andrews committed
85
     * @throws StompException
86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106
     */
    protected function _init ()
    {
        $pattern = "|^(([a-zA-Z]+)://)+\(*([a-zA-Z0-9\.:/i,-]+)\)*\??([a-zA-Z0-9=]*)$|i";
        if (preg_match($pattern, $this->_brokerUri, $regs)) {
            $scheme = $regs[2];
            $hosts = $regs[3];
            $params = $regs[4];
            if ($scheme != "failover") {
                $this->_processUrl($this->_brokerUri);
            } else {
                $urls = explode(",", $hosts);
                foreach ($urls as $url) {
                    $this->_processUrl($url);
                }
            }
            if ($params != null) {
                parse_str($params, $this->_params);
            }
        } else {
            require_once 'Stomp/Exception.php';
Craig Andrews's avatar
Craig Andrews committed
107
            throw new StompException("Bad Broker URL {$this->_brokerUri}");
108 109 110 111 112 113
        }
    }
    /**
     * Process broker URL
     *
     * @param string $url Broker URL
Craig Andrews's avatar
Craig Andrews committed
114
     * @throws StompException
115 116 117 118 119 120 121 122 123
     * @return boolean
     */
    protected function _processUrl ($url)
    {
        $parsed = parse_url($url);
        if ($parsed) {
            array_push($this->_hosts, array($parsed['host'] , $parsed['port'] , $parsed['scheme']));
        } else {
            require_once 'Stomp/Exception.php';
Craig Andrews's avatar
Craig Andrews committed
124
            throw new StompException("Bad Broker URL $url");
125 126 127 128 129
        }
    }
    /**
     * Make socket connection to the server
     *
Craig Andrews's avatar
Craig Andrews committed
130
     * @throws StompException
131 132 133 134 135
     */
    protected function _makeConnection ()
    {
        if (count($this->_hosts) == 0) {
            require_once 'Stomp/Exception.php';
Craig Andrews's avatar
Craig Andrews committed
136
            throw new StompException("No broker defined");
137 138 139 140 141 142 143 144
        }
        
        // force disconnect, if previous established connection exists
        $this->disconnect();
        
        $i = $this->_currentHost;
        $att = 0;
        $connected = false;
Craig Andrews's avatar
Craig Andrews committed
145 146 147
        $connect_errno = null;
        $connect_errstr = null;
        
148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164
        while (! $connected && $att ++ < $this->_attempts) {
            if (isset($this->_params['randomize']) && $this->_params['randomize'] == 'true') {
                $i = rand(0, count($this->_hosts) - 1);
            } else {
                $i = ($i + 1) % count($this->_hosts);
            }
            $broker = $this->_hosts[$i];
            $host = $broker[0];
            $port = $broker[1];
            $scheme = $broker[2];
            if ($port == null) {
                $port = $this->_defaultPort;
            }
            if ($this->_socket != null) {
                fclose($this->_socket);
                $this->_socket = null;
            }
Craig Andrews's avatar
Craig Andrews committed
165
            $this->_socket = @fsockopen($scheme . '://' . $host, $port, $connect_errno, $connect_errstr, $this->_connect_timeout_seconds);
166 167
            if (!is_resource($this->_socket) && $att >= $this->_attempts && !array_key_exists($i + 1, $this->_hosts)) {
                require_once 'Stomp/Exception.php';
Craig Andrews's avatar
Craig Andrews committed
168
                throw new StompException("Could not connect to $host:$port ($att/{$this->_attempts})");
169 170 171 172 173 174 175 176
            } else if (is_resource($this->_socket)) {
                $connected = true;
                $this->_currentHost = $i;
                break;
            }
        }
        if (! $connected) {
            require_once 'Stomp/Exception.php';
Craig Andrews's avatar
Craig Andrews committed
177
            throw new StompException("Could not connect to a broker");
178 179 180 181 182 183 184 185
        }
    }
    /**
     * Connect to server
     *
     * @param string $username
     * @param string $password
     * @return boolean
Craig Andrews's avatar
Craig Andrews committed
186
     * @throws StompException
187 188 189 190 191 192 193 194 195 196 197 198 199 200
     */
    public function connect ($username = '', $password = '')
    {
        $this->_makeConnection();
        if ($username != '') {
            $this->_username = $username;
        }
        if ($password != '') {
            $this->_password = $password;
        }
		$headers = array('login' => $this->_username , 'passcode' => $this->_password);
		if ($this->clientId != null) {
			$headers["client-id"] = $this->clientId;
		}
Craig Andrews's avatar
Craig Andrews committed
201
		$frame = new StompFrame("CONNECT", $headers);
202 203
        $this->_writeFrame($frame);
        $frame = $this->readFrame();
Craig Andrews's avatar
Craig Andrews committed
204
        if ($frame instanceof StompFrame && $frame->command == 'CONNECTED') {
205 206 207 208
            $this->_sessionId = $frame->headers["session"];
            return true;
        } else {
            require_once 'Stomp/Exception.php';
Craig Andrews's avatar
Craig Andrews committed
209 210
            if ($frame instanceof StompFrame) {
                throw new StompException("Unexpected command: {$frame->command}", 0, $frame->body);
211
            } else {
Craig Andrews's avatar
Craig Andrews committed
212
                throw new StompException("Connection not acknowledged");
213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238
            }
        }
    }
    
    /**
     * Check if client session has ben established
     *
     * @return boolean
     */
    public function isConnected ()
    {
        return !empty($this->_sessionId) && is_resource($this->_socket);
    }
    /**
     * Current stomp session ID
     *
     * @return string
     */
    public function getSessionId()
    {
        return $this->_sessionId;
    }
    /**
     * Send a message to a destination in the messaging system 
     *
     * @param string $destination Destination queue
Craig Andrews's avatar
Craig Andrews committed
239
     * @param string|StompFrame $msg Message
240 241 242 243
     * @param array $properties
     * @param boolean $sync Perform request synchronously
     * @return boolean
     */
Craig Andrews's avatar
Craig Andrews committed
244
    public function send ($destination, $msg, $properties = array(), $sync = null)
245
    {
Craig Andrews's avatar
Craig Andrews committed
246
        if ($msg instanceof StompFrame) {
247
            $msg->headers['destination'] = $destination;
Craig Andrews's avatar
Craig Andrews committed
248
            if (is_array($properties)) $msg->headers = array_merge($msg->headers, $properties);
249 250 251 252
            $frame = $msg;
        } else {
            $headers = $properties;
            $headers['destination'] = $destination;
Craig Andrews's avatar
Craig Andrews committed
253
            $frame = new StompFrame('SEND', $headers, $msg);
254 255 256 257 258 259 260 261
        }
        $this->_prepareReceipt($frame, $sync);
        $this->_writeFrame($frame);
        return $this->_waitForReceipt($frame, $sync);
    }
    /**
     * Prepair frame receipt
     *
Craig Andrews's avatar
Craig Andrews committed
262
     * @param StompFrame $frame
263 264
     * @param boolean $sync
     */
Craig Andrews's avatar
Craig Andrews committed
265
    protected function _prepareReceipt (StompFrame $frame, $sync)
266 267 268 269 270 271 272 273 274 275 276 277
    {
        $receive = $this->sync;
        if ($sync !== null) {
            $receive = $sync;
        }
        if ($receive == true) {
            $frame->headers['receipt'] = md5(microtime());
        }
    }
    /**
     * Wait for receipt
     *
Craig Andrews's avatar
Craig Andrews committed
278
     * @param StompFrame $frame
279 280
     * @param boolean $sync
     * @return boolean
Craig Andrews's avatar
Craig Andrews committed
281
     * @throws StompException
282
     */
Craig Andrews's avatar
Craig Andrews committed
283
    protected function _waitForReceipt (StompFrame $frame, $sync)
284 285 286 287 288 289 290 291 292 293 294 295
    {

        $receive = $this->sync;
        if ($sync !== null) {
            $receive = $sync;
        }
        if ($receive == true) {
            $id = (isset($frame->headers['receipt'])) ? $frame->headers['receipt'] : null;
            if ($id == null) {
                return true;
            }
            $frame = $this->readFrame();
Craig Andrews's avatar
Craig Andrews committed
296
            if ($frame instanceof StompFrame && $frame->command == 'RECEIPT') {
297 298 299 300
                if ($frame->headers['receipt-id'] == $id) {
                    return true;
                } else {
                    require_once 'Stomp/Exception.php';
Craig Andrews's avatar
Craig Andrews committed
301
                    throw new StompException("Unexpected receipt id {$frame->headers['receipt-id']}", 0, $frame->body);
302 303 304
                }
            } else {
                require_once 'Stomp/Exception.php';
Craig Andrews's avatar
Craig Andrews committed
305 306
                if ($frame instanceof StompFrame) {
                    throw new StompException("Unexpected command {$frame->command}", 0, $frame->body);
307
                } else {
Craig Andrews's avatar
Craig Andrews committed
308
                    throw new StompException("Receipt not received");
309 310 311 312 313 314 315 316 317 318 319 320
                }
            }
        }
        return true;
    }
    /**
     * Register to listen to a given destination
     *
     * @param string $destination Destination queue
     * @param array $properties
     * @param boolean $sync Perform request synchronously
     * @return boolean
Craig Andrews's avatar
Craig Andrews committed
321
     * @throws StompException
322 323 324 325 326 327 328 329 330 331 332 333 334 335
     */
    public function subscribe ($destination, $properties = null, $sync = null)
    {
        $headers = array('ack' => 'client');
		$headers['activemq.prefetchSize'] = $this->prefetchSize;
		if ($this->clientId != null) {
			$headers["activemq.subcriptionName"] = $this->clientId;
		}
        if (isset($properties)) {
            foreach ($properties as $name => $value) {
                $headers[$name] = $value;
            }
        }
        $headers['destination'] = $destination;
Craig Andrews's avatar
Craig Andrews committed
336
        $frame = new StompFrame('SUBSCRIBE', $headers);
337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352
        $this->_prepareReceipt($frame, $sync);
        $this->_writeFrame($frame);
        if ($this->_waitForReceipt($frame, $sync) == true) {
            $this->_subscriptions[$destination] = $properties;
            return true;
        } else {
            return false;
        }
    }
    /**
     * Remove an existing subscription
     *
     * @param string $destination
     * @param array $properties
     * @param boolean $sync Perform request synchronously
     * @return boolean
Craig Andrews's avatar
Craig Andrews committed
353
     * @throws StompException
354 355 356 357 358 359 360 361 362 363
     */
    public function unsubscribe ($destination, $properties = null, $sync = null)
    {
        $headers = array();
        if (isset($properties)) {
            foreach ($properties as $name => $value) {
                $headers[$name] = $value;
            }
        }
        $headers['destination'] = $destination;
Craig Andrews's avatar
Craig Andrews committed
364
        $frame = new StompFrame('UNSUBSCRIBE', $headers);
365 366 367 368 369 370 371 372 373 374 375 376 377 378 379
        $this->_prepareReceipt($frame, $sync);
        $this->_writeFrame($frame);
        if ($this->_waitForReceipt($frame, $sync) == true) {
            unset($this->_subscriptions[$destination]);
            return true;
        } else {
            return false;
        }
    }
    /**
     * Start a transaction
     *
     * @param string $transactionId
     * @param boolean $sync Perform request synchronously
     * @return boolean
Craig Andrews's avatar
Craig Andrews committed
380
     * @throws StompException
381 382 383 384 385 386 387
     */
    public function begin ($transactionId = null, $sync = null)
    {
        $headers = array();
        if (isset($transactionId)) {
            $headers['transaction'] = $transactionId;
        }
Craig Andrews's avatar
Craig Andrews committed
388
        $frame = new StompFrame('BEGIN', $headers);
389 390 391 392 393 394 395 396 397 398
        $this->_prepareReceipt($frame, $sync);
        $this->_writeFrame($frame);
        return $this->_waitForReceipt($frame, $sync);
    }
    /**
     * Commit a transaction in progress
     *
     * @param string $transactionId
     * @param boolean $sync Perform request synchronously
     * @return boolean
Craig Andrews's avatar
Craig Andrews committed
399
     * @throws StompException
400 401 402 403 404 405 406
     */
    public function commit ($transactionId = null, $sync = null)
    {
        $headers = array();
        if (isset($transactionId)) {
            $headers['transaction'] = $transactionId;
        }
Craig Andrews's avatar
Craig Andrews committed
407
        $frame = new StompFrame('COMMIT', $headers);
408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423
        $this->_prepareReceipt($frame, $sync);
        $this->_writeFrame($frame);
        return $this->_waitForReceipt($frame, $sync);
    }
    /**
     * Roll back a transaction in progress
     *
     * @param string $transactionId
     * @param boolean $sync Perform request synchronously
     */
    public function abort ($transactionId = null, $sync = null)
    {
        $headers = array();
        if (isset($transactionId)) {
            $headers['transaction'] = $transactionId;
        }
Craig Andrews's avatar
Craig Andrews committed
424
        $frame = new StompFrame('ABORT', $headers);
425 426 427 428 429 430 431 432
        $this->_prepareReceipt($frame, $sync);
        $this->_writeFrame($frame);
        return $this->_waitForReceipt($frame, $sync);
    }
    /**
     * Acknowledge consumption of a message from a subscription
	 * Note: This operation is always asynchronous
     *
Craig Andrews's avatar
Craig Andrews committed
433
     * @param string|StompFrame $messageMessage ID
434 435
     * @param string $transactionId
     * @return boolean
Craig Andrews's avatar
Craig Andrews committed
436
     * @throws StompException
437 438 439
     */
    public function ack ($message, $transactionId = null)
    {
Craig Andrews's avatar
Craig Andrews committed
440 441 442 443 444 445
        if ($message instanceof StompFrame) {
            $headers = $message->headers;
            if (isset($transactionId)) {
                $headers['transaction'] = $transactionId;
            }			
            $frame = new StompFrame('ACK', $headers);
446 447 448 449 450 451 452 453
            $this->_writeFrame($frame);
            return true;
        } else {
            $headers = array();
            if (isset($transactionId)) {
                $headers['transaction'] = $transactionId;
            }
            $headers['message-id'] = $message;
Craig Andrews's avatar
Craig Andrews committed
454
            $frame = new StompFrame('ACK', $headers);
455 456 457 458 459 460 461 462 463 464
            $this->_writeFrame($frame);
            return true;
        }
    }
    /**
     * Graceful disconnect from the server
     *
     */
    public function disconnect ()
    {
Craig Andrews's avatar
Craig Andrews committed
465
		$headers = array();
466 467 468 469 470 471

		if ($this->clientId != null) {
			$headers["client-id"] = $this->clientId;
		}

        if (is_resource($this->_socket)) {
Craig Andrews's avatar
Craig Andrews committed
472
            $this->_writeFrame(new StompFrame('DISCONNECT', $headers));
473 474 475 476 477 478 479 480 481 482 483 484
            fclose($this->_socket);
        }
        $this->_socket = null;
        $this->_sessionId = null;
        $this->_currentHost = -1;
        $this->_subscriptions = array();
        $this->_username = '';
        $this->_password = '';
    }
    /**
     * Write frame to server
     *
Craig Andrews's avatar
Craig Andrews committed
485
     * @param StompFrame $stompFrame
486
     */
Craig Andrews's avatar
Craig Andrews committed
487
    protected function _writeFrame (StompFrame $stompFrame)
488 489 490
    {
        if (!is_resource($this->_socket)) {
            require_once 'Stomp/Exception.php';
Craig Andrews's avatar
Craig Andrews committed
491
            throw new StompException('Socket connection hasn\'t been established');
492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514
        }

        $data = $stompFrame->__toString();
        $r = fwrite($this->_socket, $data, strlen($data));
        if ($r === false || $r == 0) {
            $this->_reconnect();
            $this->_writeFrame($stompFrame);
        }
    }
    
    /**
     * Set timeout to wait for content to read
     *
     * @param int $seconds_to_wait  Seconds to wait for a frame
     * @param int $milliseconds Milliseconds to wait for a frame
     */
    public function setReadTimeout($seconds, $milliseconds = 0) 
    {
        $this->_read_timeout_seconds = $seconds;
        $this->_read_timeout_milliseconds = $milliseconds;
    }
    
    /**
Craig Andrews's avatar
Craig Andrews committed
515
     * Read response frame from server
516
     *
Craig Andrews's avatar
Craig Andrews committed
517
     * @return StompFrame False when no frame to read
518 519 520 521 522 523 524 525 526
     */
    public function readFrame ()
    {
        if (!$this->hasFrameToRead()) {
            return false;
        }
        
        $rb = 1024;
        $data = '';
Craig Andrews's avatar
Craig Andrews committed
527 528
        $end = false;
        
529
        do {
Craig Andrews's avatar
Craig Andrews committed
530
            $read = fread($this->_socket, $rb);
531 532 533 534 535
            if ($read === false) {
                $this->_reconnect();
                return $this->readFrame();
            }
            $data .= $read;
Craig Andrews's avatar
Craig Andrews committed
536 537 538 539
            if (strpos($data, "\x00") !== false) {
                $end = true;
                $data = rtrim($data, "\n");
            }
540
            $len = strlen($data);
Craig Andrews's avatar
Craig Andrews committed
541
        } while ($len < 2 || $end == false);
542 543 544 545 546 547 548 549 550 551 552 553 554
        
        list ($header, $body) = explode("\n\n", $data, 2);
        $header = explode("\n", $header);
        $headers = array();
        $command = null;
        foreach ($header as $v) {
            if (isset($command)) {
                list ($name, $value) = explode(':', $v, 2);
                $headers[$name] = $value;
            } else {
                $command = $v;
            }
        }
Craig Andrews's avatar
Craig Andrews committed
555 556
        $frame = new StompFrame($command, $headers, trim($body));
        if (isset($frame->headers['transformation']) && $frame->headers['transformation'] == 'jms-map-json') {
557
            require_once 'Stomp/Message/Map.php';
Craig Andrews's avatar
Craig Andrews committed
558
            return new StompMessageMap($frame);
559 560 561
        } else {
            return $frame;
        }
Craig Andrews's avatar
Craig Andrews committed
562
        return $frame;
563 564 565 566 567 568 569 570 571 572 573 574 575
    }
    
    /**
     * Check if there is a frame to read
     *
     * @return boolean
     */
    public function hasFrameToRead()
    {
        $read = array($this->_socket);
        $write = null;
        $except = null;
        
Craig Andrews's avatar
Craig Andrews committed
576 577 578 579 580
        $has_frame_to_read = @stream_select($read, $write, $except, $this->_read_timeout_seconds, $this->_read_timeout_milliseconds);
        
        if ($has_frame_to_read !== false)
            $has_frame_to_read = count($read);

581 582

        if ($has_frame_to_read === false) {
Craig Andrews's avatar
Craig Andrews committed
583
            throw new StompException('Check failed to determine if the socket is readable');
584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613
        } else if ($has_frame_to_read > 0) {
            return true;
        } else {
            return false; 
        }
    }
    
    /**
     * Reconnects and renews subscriptions (if there were any)
     * Call this method when you detect connection problems     
     */
    protected function _reconnect ()
    {
        $subscriptions = $this->_subscriptions;
        
        $this->connect($this->_username, $this->_password);
        foreach ($subscriptions as $dest => $properties) {
            $this->subscribe($dest, $properties);
        }
    }
    /**
     * Graceful object desruction
     *
     */
    public function __destruct()
    {
        $this->disconnect();
    }
}
?>