你是否遇到過兩個(多個)系統間需要通過定時任務來同步某些資料?你是否在為異構系統的不同進程間相互調用、通訊的問題而苦惱、掙紮?如果是,那麼恭喜你,Message Service讓你可以很輕鬆地解決這些問題。Message Service擅長於解決多系統、異構系統間的資料交換(訊息通知/通訊)問題,你也可以把它用於系統間服務的相互調用(RPC)。本文將要介紹的RabbitMQ就是當前最主流的訊息中介軟體之一。
RabbitMQ簡介
AMQP ,即Advanced Message Queuing Protocol,進階訊息佇列協議,是應用程式層協議的一個開放標準,為面向訊息的中介軟體設計。訊息中介軟體主要用於組件之間的解耦,訊息的寄件者無需知道訊息使用者的存在,反之亦然。
AMQP的主要特徵是面向訊息、隊列、路由(包括點對點和發布/訂閱)、可靠性、安全。
RabbitMQ 是一個開源的AMQP實現,伺服器端用Erlang語言編寫,支援多種用戶端,如:Python、Ruby、.NET、Java、JMS、C、PHP、ActionScript、XMPP、STOMP等,支援AJAX。用於在分布式系統中儲存轉寄訊息,在易用性、擴充性、高可用性等方面表現不俗。網站在: http://www.rabbitmq.com/ 上面有各種語言教程和執行個體代碼
AMPQ協議為了能夠滿足各種訊息佇列需求,在概念上比較複雜,瞭解了這些概念,是使用好RabbitMQ的基礎。
vhosts : 虛擬機器主機
虛擬機器主機( virtual host ):一個虛擬機器主機持有一組交換器、隊列和綁定。為什麼需要多個虛擬機器主機呢? RabbitMQ 當中,使用者只能在虛擬機器主機的粒度進行許可權控制。因此,如果需要禁止 A 組訪問 B 組的交換器 / 隊列 / 綁定,必須為 A 和 B 分別建立一個虛擬機器主機。每一個 RabbitMQ 伺服器都有一個預設的虛擬機器主機 “/” 。
一個RabbitMQ的Server上可以有多個vhosts,使用者與使用權限設定就是依附於vhosts。對一般PHP應用,不需要使用者權限設定,直接使用預設就存在的”/”就可以了,使用者可以使用預設就存在的”guest”。一個簡單的配置樣本:
$conn_args = array( 'host' => '127.0.0.1', 'port' => '5672', 'login' => 'guest', 'password' => 'guest', 'vhost'=>'/');
connection 與 channel : 串連與通道
connection是指物理的串連,一個client與一個server之間有一個串連;一個串連上可以建立多個channel,可以理解為邏輯上的串連。一般應用的情況下,有一個channel就夠用了,不需要建立更多的channel。範例程式碼:
//建立串連和channel$conn = new AMQPConnection($conn_args);if (!$conn->connect()) { die("Cannot connect to the broker!\n");}$channel = new AMQPChannel($conn);
Exchange 與 routingkey : 交換器 與 路由鍵
為了將不同類型的message進行區分,設定了Exchange交換器與Route路由兩個概念。比如,將A類型的message發送到名為‘C1’的交換器,將類型為B的發送到’C2′的交換器。當用戶端串連C1處理隊列訊息時,取到的就只是A類型message。進一步的,如果A類型message也非常多,需要進一步細化區分,比如某個用戶端只處理A類型message中針對K使用者的message,routingkey就是來做這個用途的。
$e_name = 'e_linvo'; //交換器名$k_route = array(0=> 'key_1', 1=> 'key_2'); //路由key//建立交換器$ex = new AMQPExchange($channel);$ex->setName($e_name);$ex->setType(AMQP_EX_TYPE_DIRECT); //direct類型$ex->setFlags(AMQP_DURABLE); //持久化echo "Exchange Status:".$ex->declare()."\n";for($i=0; $ipublish($message . date('H:i:s'), $k_route[i%2])."\n";}
由以上代碼可以看到,發送訊息時,只要有“交換器”就夠了。至於交換器後面有沒有對應的處理隊列,發送方是不用管的。routingkey可以是空的字串。在樣本中,我使用了兩個key交替發送訊息,是為了下面更便於理解routingkey的作用。
對於交換器,有兩個重要的概念:
交換器( Exchange ):可以理解成具有路由表的路由程式。每個訊息都有一個路由鍵( routing key ),就是一個簡單的字串。交換器中有一系列的綁定( binding ),即路由規則( routes )。交換器可以有多個。多個隊列可以和同一個交換器綁定,同時多個交換器也可以和同一個隊資料行繫結。(多對多的關係)
A,類型。有三種類型:
1. Fanout Exchange (不處理路由鍵):一個發送到交換器上的訊息都會被轉寄到與該交換器綁定的所有隊列上。 Fanout 交換器發訊息是最快的。
2. Direct Exchange (處理路由鍵):如果一個隊資料行繫結到該交換器上,並且當前要求路由鍵為 X ,只有路由鍵是 X 的訊息才會被這個隊列轉寄。
3. Topic Exchange (將路由鍵和某模式進行匹配,可以理解成模糊處理):路由鍵的詞由 “.” 隔開,符號 “#” 表示匹配 0 個或多個詞,符號 “*” 表示匹配不多不少一個詞。
類型總結:Fanout類型最簡單,這種模型忽略routingkey;Direct類型是使用最多的,使用確定的routingkey。這種模型下,接收訊息時綁定’key_1′則只接收key_1的訊息;最後一種是Topic,這種模式與Direct類似,但是支援萬用字元進行匹配,比如: ‘key_*’,就會接受key_1和key_2。Topic貌似美好,但是有可能導致不嚴謹,所以還是推薦使用Direct。
B,持久化。指定了持久化的交換器,在重新啟動時才能重建,否則需要用戶端重新聲明產生才行。
需要特別明確的概念:交換器的持久化,並不等於訊息的持久化。只有在持久化隊列中的訊息,才能持久化;如果沒有隊列,訊息是沒有地方儲存的;訊息本身在投遞時也有一個持久化標誌的,PHP中預設投遞到持久化交換器就是持久的訊息,不用特別指定。
4,queue: 隊列
講了這麼多,才講到隊列呀。事實上,隊列僅是針對接收方(consumer)的,由接收方根據需求建立的。只有隊列建立了,交換器才會將新接受到的訊息送到隊列中,交換器是不會在隊列建立之前的訊息放進來的。換句話說,在建立隊列之前,發出的所有訊息都被丟棄了。下面這個圖比RabbitMQ官方的圖更清楚——Queue是屬於ReceiveMessage的一部分。
接下來看一下建立隊列及接收訊息的樣本:
$e_name = 'e_linvo'; //交換器名$q_name = 'q_linvo'; //隊列名$k_route = ''; //路由key //建立串連和channel$conn = new AMQPConnection($conn_args);if (!$conn->connect()) { die("Cannot connect to the broker!\n"); } $channel = new AMQPChannel($conn); //建立交換器 $ex = new AMQPExchange($channel);$ex->setName($e_name);$ex->setType(AMQP_EX_TYPE_DIRECT); //direct類型$ex->setFlags(AMQP_DURABLE); //持久化echo "Exchange Status:".$ex->declare()."\n"; //建立隊列$q = new AMQPQueue($channel);$q->setName($q_name);$q->setFlags(AMQP_DURABLE); //持久化 //綁定交換器與隊列,並指定路由鍵echo 'Queue Bind: '.$q->bind($e_name, $k_route)."\n"; //阻塞模式接收訊息echo "Message:\n";$q->consume('processMessage', AMQP_AUTOACK); //自動ACK應答 $conn->disconnect();/** * 消費回呼函數 * 處理訊息 */function processMessage($envelope, $queue) { var_dump($envelope->getRoutingKey); $msg = $envelope->getBody(); echo $msg."\n"; //處理訊息}
從上述樣本中可以看到,交換器既可以由訊息發送端建立,也可以由訊息消費者建立。
建立一個隊列(line:20)後,需要將隊資料行繫結到交換器上(line:25)隊列才能工作,routingkey也是在這裡指定的。有的資料上寫成bindingkey,其實一回事兒,弄兩個名詞反倒容易混淆。
訊息的處理,是有兩種方式:
A,一次性。用 $q->get([...]),不管取到取不到訊息都會立即返回,一般情況下使用輪詢處理訊息佇列就要用這種方式;
B,阻塞。用 $q->consum( callback, [...] ) 程式會進入持續偵聽狀態,每收到一個訊息就會調用callback指定的函數一次,直到某個callback函數返回FALSE才結束。
關於callback,這裡多說幾句: PHP的call_back是支援使用數組的,比如: $c = new MyClass(); $c->counter = 100; $q->consume( array($c,’myfunc’) ) 這樣就可以調用自己寫的處理類。MyClass中myfunc的參數定義,與上例中processMessage一樣就行。
在上述樣本中,使用的$routingkey = ”, 意味著接收全部的訊息。我們可以將其改為 $routingkey = ‘key_1′,可以看到結果中僅有設定routingkey為key_1的內容了。
注意: routingkey = ‘key_1′ 與 routingkey = ‘key_2′ 是兩個不同的隊列。假設: client1 與 client2 都串連到 key_1 的隊列上,一個訊息被client1處理之後,就不會被client2處理。而 routingkey = ” 是另類,client_all綁定到 ” 上,將訊息全都處理後,client1和client2上也就沒有訊息了。
在程式設計上,需要規劃好exchange的名稱,以及如何使用key區分開不同類型的標記,在訊息產生的地方插入發送訊息代碼。後端處理,可以針對每一個key啟動一個或多個client,以提高訊息處理的即時性。如何使用PHP進行多線程的訊息處理,將在下一節中講述。
安裝erlang依賴的基本環境
#作業系統:CentOS release 6.2yum -y install make gcc gcc-c++ kernel-devel m4 ncurses-devel openssl-devel java-devel unixODBC-devel;
Erlang安裝方式一:源碼編譯
訪問 官網下載頁
wget http://www.erlang.org/download/otp_src_R16B03.tar.gz;tar -zxvf otp_src_R16B03.tar.gz;cd otp_src_R16B03;./configure --prefix=/usr/local/erlang --with-ssl -enable-threads -enable-smmp-support -enable-kernel-poll --enable-hipe --without-javac;#不用java編譯,故去掉java避免錯誤make && make install;
配置erlang環境
#vi /etc/profile在檔案最後加入:PATH=$PATH:/usr/local/erlang/binexport PATH#source /etc/profile
Erlang安裝方式二:YUM安裝
安裝erlang的YUM源
訪問 官網YUM安裝教程
#自動安裝erlang的YUM源wget http://packages.erlang-solutions.com/erlang-solutions-1.0-1.noarch.rpmrpm -Uvh erlang-solutions-1.0-1.noarch.rpm#或手動安裝YUM源rpm --import http://packages.erlang-solutions.com/rpm/erlang_solutions.ascAdd the following lines to some file in /etc/yum.repos.d/:[erlang-solutions]name=Centos $releasever - $basearch - Erlang Solutionsbaseurl=http://packages.erlang-solutions.com/rpm/centos/$releasever/$basearchgpgcheck=1gpgkey=http://packages.erlang-solutions.com/rpm/erlang_solutions.ascenabled=1
yum erlang
yum -y install erlang;
安裝成功檢測
安裝完後輸入“erl”以下提示即為安裝成功:
[root@localhost ~]# erlErlang/OTP 18 [erts-7.2] [source-e6dd627] [64-bit] [async-threads:10] [hipe] [kernel-poll:false]Eshell V7.2 (abort with ^G)