http://www.ftponline.com/china/XmlFile.aspx?ID=258
將異步消息傳遞(如Java Message Service)與SOAP結(jié)合起來就意味著Web services可以有更多的用途。by David Chappell and Tony Hong
直到最近,Web services的用途一直集中于通過HTTP實(shí)現(xiàn)的同步請(qǐng)求/回應(yīng)模式(request/reply model)。這是很自然的,因?yàn)閃eb services的第一個(gè)用例就是集中在Remote Procedure Call(RPC)和分布式組件交互的。然而,Web services的這種應(yīng)用情況忽視了當(dāng)今IT前景的一個(gè)重大的方面:異步消息傳遞(asynchronous messaging),它有很大的價(jià)值,因?yàn)樗o數(shù)據(jù)傳輸和處理帶來了一定的可靠性和靈活性,這些性能是同步消息傳遞很難實(shí)現(xiàn)的。通過異步消息傳輸機(jī)制(如JMS),把異步消息傳遞同SOAP結(jié)合起來就可以將如今的Web services標(biāo)準(zhǔn)應(yīng)用到更廣的領(lǐng)域。
將這種技術(shù)結(jié)合用于重要的企業(yè)集成環(huán)境是最受歡迎的了。該領(lǐng)域一直是由Electronic Data Interchange (EDI)、Enterprise Application Integration(EAI)和B2B統(tǒng)治的——然而IT社團(tuán)一直在尋找成本更低、更高效、更基于標(biāo)準(zhǔn)的方法。在這些領(lǐng)域中,用即時(shí)的同步RPC來換取一些功能會(huì)讓我們更滿意,這些功能包括可靠的傳遞能力、更適合面向企業(yè)交易的松散耦合的、粗粒度的(coarse-grained)接口;面向文檔的異步交互和對(duì)通訊協(xié)議和傳輸機(jī)制的支持。
讓我們看看JMS是如何成為一種可靠的異步SOAP消息傳遞的方法的。我們也將探討如何將JMS用于可靠的Web services調(diào)用。(你可以在這里下載樣例代碼;注意,你需要接受一個(gè)許可協(xié)議并在www.xmethods.net網(wǎng)站上注冊(cè)。)
許多Web services都運(yùn)用一個(gè)同步的RPC模式,在這個(gè)模式中,服務(wù)呈現(xiàn)細(xì)粒度的(fine-grained)接口,而且一個(gè)Web services客戶端調(diào)用RPC型(RPC-style)的交互。在一個(gè)RPC型的環(huán)境中,參與一個(gè)單獨(dú)的通訊流程的所有的應(yīng)用程序和服務(wù)都必須是可用的,以使多方通訊成功。在有些情況下,同步模式是必需的,比如當(dāng)服務(wù)傳遞的信息在本質(zhì)上是實(shí)時(shí)的(如詳細(xì)目錄查找)時(shí)候。然而,這種同步處理是一種要么全有要么全無的(all-or-nothing)命題。如果由于某種原因不能得到一個(gè)下行的(downstream)服務(wù),那么提出請(qǐng)求的應(yīng)用程序及其上行的(upstream)參與者就需要處理應(yīng)用程序代碼中的錯(cuò)誤(見
圖1)。
可靠的異步通訊是指,為了保持整個(gè)環(huán)境的健全,系統(tǒng)的所有方面不需要在任何特定的時(shí)候都是可用的。即使對(duì)于一個(gè)簡單的兩方客戶端/服務(wù)(client/service)交互來說,運(yùn)用可靠的異步通訊就意味著,當(dāng)客戶端對(duì)服務(wù)提出請(qǐng)求時(shí),被調(diào)用的服務(wù)不需要是可用的。客戶端將請(qǐng)求傳遞到一個(gè)可靠的異步傳輸機(jī)制中,該請(qǐng)求暫時(shí)不能與服務(wù)進(jìn)行對(duì)話。然后客戶端本身變成不可用的,當(dāng)服務(wù)成為可用的時(shí),就可以進(jìn)行服務(wù)調(diào)用了。
處于等待狀態(tài)的(pending)交互
異步交互也不需要客戶端等待響應(yīng)。一個(gè)Web service端點(diǎn)(end point)可以代表一個(gè)應(yīng)用程序,它需要一些時(shí)間來執(zhí)行計(jì)算、查找或轉(zhuǎn)換等任務(wù)。對(duì)一個(gè)服務(wù)提出異步請(qǐng)求的客戶端可以繼續(xù)執(zhí)行其它任務(wù),同時(shí)其Web service交互處于等待狀態(tài)。這就提高了并行性(concurrency)和可擴(kuò)展性。
細(xì)粒度的RPC型的接口需要每個(gè)應(yīng)用程序和服務(wù)都知道它們是如何同其它應(yīng)用程序和服務(wù)進(jìn)行通訊的。例如,一個(gè)訂單接口上可能有諸如getQuantity()、getTotal()或getBillToAddress()等方法。
粗粒度、文本型(document-style)的接口是指,每個(gè)應(yīng)用程序或服務(wù)只需要關(guān)心封裝的XML文檔,該XML文檔包含所有相關(guān)的信息,服務(wù)可以與其它感興趣者共享這些信息,并可以以SOAP envelope的形式將這些信息發(fā)送給他們。同樣,在使用服務(wù)時(shí),我們可以從XML文檔選擇相關(guān)的數(shù)據(jù)(例如<ShipTo>地址),進(jìn)行相應(yīng)的處理,然后將結(jié)果發(fā)送到其它目的地。當(dāng)你將異步交互同文本型、粗粒度的接口結(jié)合起來時(shí),你就為消息傳遞形式的調(diào)用做好了主要準(zhǔn)備了。
得到可靠的異步通訊的最明智的方法就是運(yùn)用基于JMS的消息隊(duì)列。JMS是個(gè)標(biāo)準(zhǔn),它提供了一組通用API讓應(yīng)用程序使用,并提供了一組通用的消息傳遞語義,如確保消息傳遞、消息一定會(huì)被傳遞且僅傳遞一次(once-and-only-once delivery)。JMS將其性能和執(zhí)行原則封裝在自己的層中,這個(gè)層是同應(yīng)用程序?qū)臃蛛x的。一個(gè)將基于JMS消息傳遞系統(tǒng)作為傳輸機(jī)制的應(yīng)用程序不需要關(guān)心消息是如何傳遞的。SOAP消息可以在發(fā)送者和接收者之間列隊(duì)等待(見
圖2)。JMS提供者負(fù)責(zé)處理異步、消息持續(xù)性、交易完整性和接收確認(rèn)等所有方面。
Java 2 Platform、Enterprise Edition(J2EE)架構(gòu)圖總是將JMS看做是個(gè)隱藏在防火墻后面的組件,通過它我們?cè)谝粋€(gè)JSP servlet引擎和消息驅(qū)動(dòng)的bean之間異步地傳遞消息。如果你在制作Web頁面并將它們同中間層商業(yè)邏輯結(jié)合起來,那么這個(gè)架構(gòu)就是個(gè)有效的用例,但這并不是運(yùn)用消息傳遞的唯一的方式。
JMS消息傳遞機(jī)制是跨Internet傳遞數(shù)據(jù)、SOAP或其它信息的一個(gè)完全可行的解決方案。一個(gè)JMS機(jī)制可以在哪里部署以及如何部署的細(xì)節(jié)取決于供應(yīng)商。許多JMS供應(yīng)商都提供某種通道性能(tunneling capabilities)。例如,在SonicMQ例子中,JMS客戶端到JMS服務(wù)器的通訊可以很容易地跨越公開的Internet、跨越防火墻、以一種運(yùn)用HTTP、HTTPS、SSL或TCP sockets的安全的、可靠的方式進(jìn)行。
將JMS作為一種SOAP傳輸機(jī)制
自1998年被引進(jìn)到業(yè)界以來,JMS就作為一種基于標(biāo)準(zhǔn)的消息傳遞方法,在應(yīng)用程序之間傳輸SOAP消息。它有五種消息類型,可以傳遞任何類型的數(shù)據(jù)(基于XML或不基于XML)。XML數(shù)據(jù)可以很容易地作為一個(gè)JMS BytesMessage中的二元數(shù)據(jù)或作為TextMessage中的串?dāng)?shù)據(jù)(string data)來傳遞。
不管對(duì)話兩端運(yùn)用何種API,我們把傳輸層上運(yùn)用的JMS看做是除了普通的、老的HTTP傳輸之外的另一種可選方法。在我們的例子中,我們將JMS的API和Apache SOAP結(jié)合起來了——運(yùn)用JMS API來連接、發(fā)送和接收消息,運(yùn)用Apache SOAP來建構(gòu)(construct)和析構(gòu)(deconstruct)SOAP envelope。在不久的將來,JMS將作為一個(gè)傳輸層被嵌入到Apache Axis中。JMS將會(huì)成為一個(gè)部署選項(xiàng)。
JMS也有一個(gè)內(nèi)置的同步請(qǐng)求/回應(yīng)模式。這個(gè)請(qǐng)求/回應(yīng)模式也可以異步地工作。請(qǐng)求者不需要block請(qǐng)求,等待即時(shí)的響應(yīng)。響應(yīng)可以在稍后的時(shí)間異步地發(fā)送。JMS可以使最初的請(qǐng)求消息同相應(yīng)的響應(yīng)消息關(guān)聯(lián)起來,即使請(qǐng)求和響應(yīng)在時(shí)間上是分離的,或者即使請(qǐng)求過程在失敗后又恢復(fù)正常。
XMethods中列出的BabelFish服務(wù)給AltaVista的很受歡迎的BabelFish翻譯工具提供了一個(gè)典型的PRC SOAP接口(見資源)。下面我們來看一下這個(gè)Web service的一個(gè)異步版本,稱為AsynchBabelFish。
從根本上說,該服務(wù)主要是異步交換SOAP文檔。客戶端給服務(wù)發(fā)送一個(gè)AsynchBabelFish轉(zhuǎn)換請(qǐng)求文檔,一段時(shí)間后,服務(wù)將一個(gè)AsynchBabelFish轉(zhuǎn)換結(jié)果文檔發(fā)送回客戶端。交換是在消息隊(duì)列上完成的。從這個(gè)簡單的例子,我們可以看到的一個(gè)明顯的好處就是,即使服務(wù)出了問題,客戶端也不會(huì)受影響;它可以將請(qǐng)求放到服務(wù)隊(duì)列中,該請(qǐng)求是肯定可以被完成的——即使不是在現(xiàn)在,在稍后時(shí)間服務(wù)恢復(fù)正常時(shí)就可以完成。而且,運(yùn)用一個(gè)異步模式也可以給客戶端和服務(wù)器提供很多的靈活性,使它們可以控制消息的處理速度,例如,服務(wù)器可以從服務(wù)隊(duì)列取出消息,按自己的意愿隨時(shí)處理它們。
在客戶端和AsynchBabelFish服務(wù)之間可能交換了三個(gè)文檔:從客戶端發(fā)送到服務(wù)的轉(zhuǎn)換請(qǐng)求文檔、從服務(wù)發(fā)送回客戶端的轉(zhuǎn)換結(jié)果文檔、以及如果請(qǐng)求出了問題或如果轉(zhuǎn)換請(qǐng)求由于某種原因不能完成,發(fā)送回客戶端的一個(gè)替代結(jié)果文檔的SOAP fault envelope。
要對(duì)AsynchBabelFish服務(wù)提出一個(gè)請(qǐng)求,客戶端需要將轉(zhuǎn)換請(qǐng)求文檔創(chuàng)建成一個(gè)字符串,并將它作為一個(gè)TextMessage列在AsynchBabelFish服務(wù)請(qǐng)求隊(duì)列中。Envelope采用的形式見列表1。
轉(zhuǎn)換請(qǐng)求envelope的body部分包含SourceText元素,它帶有用戶想要轉(zhuǎn)換的文本。Xml:lang屬性為文本語言指定了編碼,traslateTo屬性為用戶想將文本轉(zhuǎn)換到的語言指定了編碼。所支持的語言包括English(en)、German(de)、French(fr)、Italian(it)和Portuguese(pt)。對(duì)于源文(source text),有150個(gè)字的限制。
注意SOAP header中運(yùn)用的WS-Security header元素。AsynchBabelFish服務(wù)要求對(duì)XMethods用戶ID/密碼數(shù)據(jù)庫進(jìn)行驗(yàn)證,該元素用來提供請(qǐng)求者的身份憑證(credentials)。(關(guān)于WS-Security的更多信息,請(qǐng)參閱資源。)轉(zhuǎn)換結(jié)果envelope的body部分包含兩個(gè)子元素,SourceText和TranslationText。例如:
<?xml version="1.0" encoding=
"UTF-8"?>
<soap:Envelope xmlns:soap=
"http://schemas.xmlsoap.org/
soap/envelope/">
<soap:Body>
<SourceText xml:lang="en"
xmlns:xml=http://www.w3.org/
XML/1998/namespace
xmlns="http://xmethods.net/
babelfish">hello</SourceText>
<TranslationText xml:lang="fr"
xmlns:xml=
"http://www.w3.org/XML/1998/
namespace"
xmlns="http://xmethods.net/
babelfish">bonjour
</TranslationText>
</soap:Body>
</soap:Envelope>
|
SourceText只是重復(fù)傳遞到請(qǐng)求的內(nèi)容。TranslationText包含轉(zhuǎn)換的文本及其語言的xml:lang指示符。如果請(qǐng)求envelope有問題,從而不可能完成請(qǐng)求,就會(huì)返回一個(gè)標(biāo)準(zhǔn)的SOAP fault envelope來替代轉(zhuǎn)換結(jié)果文檔。下面的例子顯示的是SOAP fault envelope;如果請(qǐng)求消息帶有的TextMessage不是格式規(guī)范的XML,那么這個(gè)特殊的fault就會(huì)被發(fā)送回客戶端:
<?xml version='1.0' encoding=
'UTF-8'?>
<soap:Envelope xmlns:soap=
'http://schemas.xmlsoap.org/
soap/envelope/'>
<soap:Body>
<soap:Fault>
<faultcode>soap:Client
</faultcode>
<faultstring>Incoming text
is not XML</faultstring>
</soap:Fault>
</soap:Body>
</soap:Envelope>
|
通過一個(gè)基于JMS的Web service(如AsynchBabelFish),客戶端和服務(wù)器就不是直接進(jìn)行交互的了;取而代之的是,SOAP請(qǐng)求可以通過消息隊(duì)列在客戶端和服務(wù)器之間進(jìn)行傳遞,而不受時(shí)間的影響(time-independent)。
現(xiàn)在,讓我們看看一個(gè)消息是如何從客戶端傳遞到服務(wù)器的(見圖3)。客戶端創(chuàng)建一個(gè)轉(zhuǎn)換請(qǐng)求envelope,并將它放在該服務(wù)的請(qǐng)求隊(duì)列中。服務(wù)的所有客戶端都可以運(yùn)用這個(gè)眾所周知的公用隊(duì)列,如同基于HTTP的服務(wù)運(yùn)用固定的、眾所周知的HTTP端點(diǎn)URL一樣。客戶端也給請(qǐng)求消息提供了一個(gè)reply-to JMS header,它包含一個(gè)響應(yīng)隊(duì)列的名字,從而讓服務(wù)知道將響應(yīng)消息發(fā)送到哪里。AsynchBabelFish服務(wù)過程是否立即完成同該步驟(將請(qǐng)求放入隊(duì)列)是無關(guān)的。
然后,服務(wù)從隊(duì)列得到請(qǐng)求。該步驟可以是在客戶端放置請(qǐng)求后立即實(shí)現(xiàn)的,也可以在稍后時(shí)間完成,取決于服務(wù)器什么時(shí)候做好準(zhǔn)備接收請(qǐng)求。然后服務(wù)器處理請(qǐng)求。如果請(qǐng)求在某些方面是無效的——例如,它可能不是一個(gè)有效的請(qǐng)求文檔,或它可能用了不被支持或無效的語言編碼——那么它就會(huì)生成一個(gè)SOAP fault envelope,其中詳細(xì)說明出了什么問題。否則,服務(wù)就執(zhí)行轉(zhuǎn)換并生成適當(dāng)?shù)慕Y(jié)果文檔。如果在轉(zhuǎn)換過程中出現(xiàn)與請(qǐng)求本身的有效性無關(guān)的技術(shù)錯(cuò)誤,那么請(qǐng)求就保留在隊(duì)列中,服務(wù)定期重試它們。最后,服務(wù)器就通過響應(yīng)隊(duì)列給客戶端返回一個(gè)響應(yīng)消息(見
圖4)。
嘗試
服務(wù)將轉(zhuǎn)換結(jié)果文檔(或SOAP fault)放在JMS reply-to指定的響應(yīng)隊(duì)列中。它包含JMS請(qǐng)求消息的ID,并設(shè)置相關(guān)的響應(yīng)消息ID來匹配它,因此客戶端就可以根據(jù)其意愿進(jìn)行相應(yīng)的請(qǐng)求/響應(yīng)操作了。然后,在需要的時(shí)候,客戶端就可以從響應(yīng)隊(duì)列得到完成的轉(zhuǎn)換結(jié)果文檔了(或SOAP fault)。
你自己可以嘗試調(diào)用AsynchBabelFish服務(wù)。(要訪問AsynchBabelFish的client package和相關(guān)的JMS client libraries,請(qǐng)參閱www.xml-mag.com上的在線
資源。)這個(gè)包(package)包括客戶端源代碼、二進(jìn)制代碼和它所依賴的第三方類庫(libraries)。要運(yùn)用這個(gè)客戶端,你也需要?jiǎng)?chuàng)建一個(gè)XMethods帳號(hào)并請(qǐng)求你自己的響應(yīng)隊(duì)列(XMethods給它的用戶提供了一個(gè)作為messagebox的響應(yīng)隊(duì)列,它是對(duì)該服務(wù)以及未來引發(fā)的異步服務(wù)所做的SOAP響應(yīng))。客戶端下載包所包含的指南對(duì)這些步驟的每一步都提供了更詳細(xì)的說明。Client package中的源程序文件在
表1中有簡要的說明。
TranslatorRequestor可以讓你創(chuàng)建請(qǐng)求并將它發(fā)送到AsynchBabelFish請(qǐng)求隊(duì)列中(見列表2)。其運(yùn)行如下所示:
java asynchBabelFish.
TranslatorRequestor <username>
<password> <text> <fromLanguage>
<toLanguage> <responseQueue>
|
讓我們來看看TranslatorRequestor的一些重點(diǎn)。首先,程序?qū)嵗粋€(gè)到AsynchBabelFish服務(wù)隊(duì)列的JMS連接。然后實(shí)例化一個(gè)RequestEnvelope對(duì)象:
RequestEnvelope requestEnv=
new RequestEnvelope(userID,
password,sourceText,
sourceLanguage,toLanguage);
|
RequestEnvelope對(duì)象帶有你傳遞的參數(shù)并在內(nèi)部構(gòu)造一個(gè)SOAP envelope。然后,當(dāng)你調(diào)用它的toString()方法時(shí),它就會(huì)以字符串形式給你返回一個(gè)相應(yīng)的轉(zhuǎn)換請(qǐng)求SOAP文檔。我們用這個(gè)字符串創(chuàng)建一個(gè)JMS TextMessage對(duì)象:
TextMessage message=
queueSession.createTextMessage(
requestEnv.toString());
|
將響應(yīng)隊(duì)列的句柄(handle)添加到消息的JMS reply-to header:
// Get a handle to the reply-to
// queue
Queue replyToQueue=queueSession.
createQueue(replyToQueueName);
// and add it to the reply-to
// field
message.setJMSReplyTo(
replyToQueue);
|
形成的結(jié)果消息就會(huì)在控制臺(tái)打印出來并發(fā)送到服務(wù)隊(duì)列。在這個(gè)例子中,你可以看到,被調(diào)用的TranslatorRequestor程序用了一個(gè)我們?cè)O(shè)置的測(cè)試帳號(hào)“joe”和一個(gè)測(cè)試響應(yīng)隊(duì)列(testResponseQueue)(見圖5)。
然后服務(wù)器處理消息,返回一個(gè)轉(zhuǎn)換結(jié)果文檔或一個(gè)SOAP fault。ReplyProcessor程序負(fù)責(zé)處理響應(yīng)消息(見
列表3)。在這個(gè)例子中,我們用了一個(gè)與請(qǐng)求程序不同的程序來處理響應(yīng)消息,盡管這并不是必需的。ReplyProcessor的調(diào)用方式如下所示:
java asynchBabelFish.
ReplyProcessor <username>
<password> <responseQueue> |
程序首先建立一個(gè)到命令行參數(shù)所指定的響應(yīng)隊(duì)列的JMS連接。然后它將自己注冊(cè)為那個(gè)隊(duì)列的一個(gè)listener。在典型的JMS消息傳遞模式中,傳送的消息被反饋到ReplyProcessor 的onMessage()方法。
得到有趣的信息
對(duì)于進(jìn)入響應(yīng)隊(duì)列的每個(gè)新消息,onMessage()都從消息中讀取SOAP envelope(以一個(gè)TextMessage的形式),然后將來自TextMessage的字符串傳遞到ResponseEnvelope構(gòu)造器中:
ResponseEnvelope response=
new ResponseEnvelope(
((TextMessage)message).
getText());
|
ResponseEnvelope(見列表4)是個(gè)輔助類(helper class),它可以讓我們更容易地處理轉(zhuǎn)換結(jié)果文檔。它的構(gòu)造器以字符串形式讀取SOAP XML,并實(shí)例化一個(gè)Apache SOAP Envelope對(duì)象,它用get()方法處理這個(gè)對(duì)象來提取有趣的參數(shù)(源文本、轉(zhuǎn)換文本等等)。它的toString()方法也可以讓你得到SOAP消息的源XML文檔。然后響應(yīng)消息和SOAP文檔本身的相關(guān)ID(correlation ID)就被打印出來了:
// The correlation ID matches the
// JMS Message ID of the original
// request message
System.out.println("\nResponse:
JMS Correlation ID = "+message.
getJMSCorrelationID()+"\n");
// Print out the response SOAP
// envelope
System.out.println(response);
|
最后,從轉(zhuǎn)換結(jié)果文檔中打印出我們感興趣的四個(gè)參數(shù):源文本、源語言、轉(zhuǎn)換文本和轉(zhuǎn)換語言:
// And print out a summary of the
// translation.
System.out.println("\""+response.
getSourceText()+"\" (
"+response.getSourceLanguage()
+") translates to \""+response.
getTranslationText()+"\" (
"+response.
getTranslationLanguage()
+")\n\n");;
|
如果傳送的消息的確產(chǎn)生了一個(gè)SOAP fault,而不是一個(gè)有效的轉(zhuǎn)換結(jié)果文檔,那么要實(shí)例化ResponseEnvelope就會(huì)拋出一個(gè)異常,會(huì)從SOAP fault中形成錯(cuò)誤字符串,并打印出對(duì)那個(gè)異常的堆棧跟蹤(stack trace)(見列表5)。當(dāng)程序?qū)ξ覀兿惹坝肨ranslatorRequestor發(fā)送的請(qǐng)求的響應(yīng)文檔進(jìn)行處理時(shí),你就可以看到程序的結(jié)果(見圖6)。
RPC機(jī)制限制了Web services的有效性。異步消息傳遞以及它同Web services整合的價(jià)值就在于,SOAP和更高級(jí)的Web services協(xié)議是獨(dú)立于傳輸機(jī)制(transport-neutral)的;對(duì)于分布式計(jì)算(distributed computing)來說,沒有通用的(one-size-fits-all)方法。JMS提供了可靠的異步傳輸,這種傳輸可以與其它受歡迎的SOAP堆棧一起(或隱藏在這些SOAP堆棧下)跨Internet實(shí)現(xiàn)。
不管你選擇哪種SOAP客戶端類庫,你都需要考慮如何運(yùn)用異步消息傳遞來在企業(yè)內(nèi)部或跨企業(yè)實(shí)現(xiàn)有效的數(shù)據(jù)傳輸。AsynchBabelFish例子可能并不是你在日常工作中使用或?qū)崿F(xiàn)的那種服務(wù),但它的確說明了SOAP是如何同JMS整合在一起的。
關(guān)于作者:David Chappel是Sonic Software的副總和主要技術(shù)傳播者。他也是Java Web Services (O’Reilly & Associates, 2002)、Professional ebXML Foundations (Wrox, 2001)和The Java Message Service (O’Reilly & Associates, 2000)的合著者。最近,他獲得了Java Pro雜志的“Java Community個(gè)人杰出貢獻(xiàn)獎(jiǎng)”(請(qǐng)?jiān)L問
www.sonicsoftware.com)。Tony Hong是Xmethods的合著者,是Web services目錄的出版商(請(qǐng)?jiān)L問
www.xmethods.net)。Dave的聯(lián)系方式是
chappell@sonicsoftware.com,Tony的聯(lián)系方式是
thong@xmethods.net。