雲端聊天:第一部分

工程 | Mark Fisher | 2011 年 8 月 16 日 | ...

上週,RabbitMQ 作為 Cloud Foundry 上的服務正式發布了。任何在 Cloud Foundry 上運行的應用程式現在都可以透過 RabbitMQ Broker 發送和接收訊息,RabbitMQ Broker 可以透過單一指令(例如 'vmc create-service rabbitmq')作為服務進行配置。訊息服務的實例可以在應用程式之間共享,而且由於 RabbitMQ 是基於協議的 Broker,這些應用程式甚至可以用不同的語言編寫。因此,對於那些對在雲端運行的模組化、多語言、事件驅動應用程式感興趣的人來說,這是一個令人興奮的公告。我將發布一系列部落格文章,重點介紹這些類型的應用程式。在這篇文章中,我將保持簡單,重點關注 Spring 開發人員的初始體驗。

首先,即使您之前沒有 Cloud Foundry 的經驗,我也鼓勵您查看本教學,這是入門的最佳方式。在那裡您將看到如何使用 Maven 構建一個簡單的 Spring 應用程式,並使用 VMC 命令列工具將其部署到 Cloud Foundry。然後,該應用程式介紹了 RabbitMQ,其 MVC 控制器已得到增強,可以發布和檢索訊息。它展示了如何透過 Spring AMQP 函式庫配置和使用 RabbitMQ 服務。

此外,在最初 Cloud Foundry 公告發布的當天(與這些 部落格 文章 同一天),我發布了另一篇部落格,介紹了「cloud」命名空間支援的基礎知識。閱讀該文章也可能有助於為您即將看到的內容奠定基礎。具體來說,我們擴展了「cloud」命名空間,以包含對 RabbitMQ ConnectionFactory 的支援,這將在我們稍後介紹配置概述時涵蓋。

現在,我想介紹另一個範例應用程式,它示範了一個簡單的聊天伺服器。RabbitMQ 為多功能的聊天應用程式提供了出色的骨幹,因為它支援不同類型的交換器,例如「direct」/點對點、「topic」為基礎的發布/訂閱,以及用於簡單廣播的「fanout」。此外,RabbitMQ 還支援各種語言綁定。再加上在雲端啟用訊息傳遞基本上只需撥動開關,現在許多不同的應用程式都可以輕鬆共享該服務。如上所述,我將逐步增強範例,並在稍後發布更多部落格文章,以涵蓋這些交換器類型和一些多語言聊天,但目前我的目標是提供一個可訪問的起點,僅透過 fanout 交換器進行全局廣播。該應用程式目前的狀態並不比教學中介紹的複雜多少。我將逐步介紹一些配置和程式碼,但如果您想跟著操作並深入了解更多細節,我建議從 SpringSource cloudfoundry-samples 在 github 上的儲存庫複製範例。

「rabbit-chat」範例應用程式

以下是正在運行的應用程式外觀

該表單將使用 jQuery 提交 HTTP POST 請求


$('#chatForm').submit(
	function() {
		$.post(
			$('#chatForm').attr("action"),
			$('#chatForm').serialize(),
			function(response) {
				if (response) {
					confirm(response.id);
				}
			});
		$('#text').val("");
		return false;
	});

而且,聊天記錄將透過輪詢定期更新 - 也使用 jQuery 的 AJAX 支援


$.ajax({
	url : "chatlog",
	success : function(message) {
		if (message && message.length) {
			var messagesDiv = $('#messages');
			messagesDiv.html(message);
			messagesDiv.animate({ scrollTop: messagesDiv.attr("scrollHeight") - messagesDiv.height() }, 150);
		}
		timer = poll();
	},
	error : function() {
		timer = poll();
	},
	cache : false
});

Java 程式碼

如果您複製儲存庫並 cd 進入 'rabbit-chat' 目錄,您將看到以下結構

├── pom.xml
├── src
│   └── main
│       ├── java
│       │   └── org
│       │       └── cloudfoundry
│       │           └── samples
│       │               └── rabbitmq
│       │                   └── chat
│       │                       └── ChatController.java
│       ├── resources
│       │   └── static
│       │       └── js
│       │           └── jquery.min.js
│       └── webapp
│           └── WEB-INF
│               ├── spring
│               │   └── servlet-context.xml
│               ├── views
│               │   └── chat.jsp
│               └── web.xml

pom.xml 檔案宣告了依賴項。特別值得關注的是以下內容

  • spring-webmvc (3.0.5.RELEASE)
  • spring-rabbit (1.0.0.RC3)
  • cloudfoundry-runtime (0.7.1)

web.xml 檔案宣告了一個 Spring MVC DispatcherServlet 和一個單一的 catch-all servlet-mapping ("/")。

如您所見,有一個名為「ChatController」的單一控制器。它是透過註解配置的。它使用了 @Controller 和 @RequestMapping 註解以及 @Autowired。由於 ChatController 實際上是應用程式的核心(也是其唯一的 Java 程式碼),讓我們快速瀏覽一下整個實作


@Controller
public class ChatController {

	@Autowired
	private volatile AmqpTemplate amqpTemplate;

	private final Queue<String> messages = new LinkedBlockingQueue<String>();

	@RequestMapping(value = "/")
	public String home() {
		return "WEB-INF/views/chat.jsp";
	}

	@RequestMapping(value = "/publish", method = RequestMethod.POST)
	@ResponseStatus(value = HttpStatus.OK)
	public void publish(@RequestParam String username, @RequestParam String text) {
		this.amqpTemplate.convertAndSend(username + ": " + text);
	}

	@RequestMapping(value = "/chatlog")
	@ResponseBody
	public String chatlog() {
		return StringUtils.arrayToDelimitedString(this.messages.toArray(), "<br/>");
	}

	/**
	 * This method is invoked when a RabbitMQ Message is received.
	 */
	public void handleMessage(String message) {
		if (messages.size() > 100) {
			messages.remove();
		}
		messages.add(message);
	}
}

有 3 個控制器方法(那些使用 @RequestMapping 註解的方法),每個方法都只有一行程式碼。其中最簡單的是 home(),它返回要呈現的 JSP 位置。我通常會使用 Spring MVC ViewResolver,但由於它只是一個使用 AJAX 的單頁應用程式,因此這是唯一直接呈現的視圖。如您所見,當您請求應用程式根目錄時,將會調用 home()

publish(..) 方法會針對相對 URL "/publish" 的 HTTP POST 請求調用,並且它期望請求中有兩個參數:username 和 text。這些參數由您在上一節中看到的 HTML 表單提供。它由 chat.jsp 呈現。publish 方法所做的只是將 username + text 值串連成一個單一的 String,以便轉換為 AMQP 訊息並由 AmqpTemplate 發送,之後它會以簡單的 HTTP 200 (OK) 狀態回應。範本實例已自動注入到控制器中。我們稍後將查看 AmqpTemplate 配置以及底層的 ConnectionFactory,它使 RabbitMQ 服務能夠在 Cloud Foundry 上使用。

chatlog() 方法僅返回最多 100 條最近的聊天訊息。它是上一節中顯示的 AJAX 請求輪詢的方法。handleMessage(..) 方法負責將這些聊天訊息排隊,因此它是連接到基礎訊息偵聽器的那個。這幾乎涵蓋了應用程式的功能。

Spring 配置

現在,我們可以逐步了解此應用程式的配置。這可以使用註解完全在 Java 中完成,但希望您同意這是一個相當簡潔的配置文件


<context:component-scan base-package="org.cloudfoundry.samples.rabbitmq.chat"/>

<mvc:annotation-driven/>

<mvc:resources location="file:./src/main/resources/static/,classpath:/static/" mapping="static/**"/>

<rabbit:queue id="chatQueue"/>

<rabbit:fanout-exchange name="chatExchange">
	<rabbit:bindings>
		<rabbit:binding queue="chatQueue"/>
	</rabbit:bindings>
</rabbit:fanout-exchange>

<rabbit:template connection-factory="rabbitConnectionFactory" exchange="chatExchange"/>

<rabbit:admin connection-factory="rabbitConnectionFactory"/>

<rabbit:listener-container>
	<rabbit:listener queues="chatQueue" ref="chatController" method="handleMessage"/>
</rabbit:listener-container>

<cloud:rabbit-connection-factory id="rabbitConnectionFactory"/>

「component-scan」元素使使用 @Controller 註解的類別能夠註冊為 Spring 管理的物件,並且它還啟用了對 @Autowired 的支援。帶有「mvc」前綴的兩個元素只是簡單地設置 MVC @RequestMapping 支援,並啟用靜態資源的載入(在本例中用於 'resources/static/js' 目錄中提供的 jQuery 支援)。

其餘元素與 RabbitMQ 配置相關。「rabbit:admin」產生一個 RabbitAdmin 的實例,它負責識別在同一應用程式上下文中定義的 Exchanges、Queues 和 Bindings。請注意,queue 元素將「chatQueue」作為其 id,但該 id 沒有「name」屬性。這將觸發創建一個具有唯一、產生名稱的 Queue,該 Queue 專屬於此特定應用程式。換句話說,「id」屬性的值不映射到 Queue 的名稱;它是 Spring bean 的 id,而不是 RabbitMQ Queue 的 id。即使它有一個產生的名稱,它也需要是可識別的,以便在此應用程式上下文中引用。例如,您可以看到它在「chatExchange」的綁定中被引用,該交換器在此處定義為 Fanout Exchange。由於存在「rabbit:admin」元素,因此該交換器也將針對 Broker 宣告。

「rabbit:template」非常簡單明瞭。它需要引用 ConnectionFactory(別擔心,我們稍後會介紹),如果您希望其「send」方法發布到非無名稱預設 Exchange 的 Exchange,您可以在此處提供。我們正在發布到我們剛討論的「chatExchange」。

「rabbit:listener-container」與 Spring JMS 支援中同名的元素幾乎相同。這個監聽「chatQueue」,請記住,這只是對 bean id 的引用,該特定 Queue 的真實名稱是由 Broker 產生的。每當有訊息到達該 Queue 時,我們之前看到的「handleMessage」方法將會被調用。在方法參數為 String 的情況下,監聽器容器的適配器將自動處理訊息主體的轉換。由於該方法參數不需要接受實際的 Message 實例,並且方法名稱可以是我們想要的任何名稱,因此我們將其稱為「訊息驅動的 POJO」。換句話說,它與訊息傳遞 API 沒有直接依賴關係。它被監聽器容器調用是一種控制反轉的形式。

最後,是 Connection Factory 配置。在本例中,我們使用「cloud」命名空間及其「rabbit-connection-factory」元素。只要您的應用程式綁定到 Cloud Foundry 中的單一「rabbitmq」服務,就不需要其他資訊來創建 ConnectionFactory 實例。該命名空間支援底層的程式碼將從環境本身確定憑證。該命名空間支援由「cloudfoundry-runtime」函式庫提供,您可以在此應用程式的 pom.xml 檔案中看到宣告。

運行應用程式

您可以使用 vmc 命令列工具SpringSource Tool Suite 運行應用程式。使用 vmc,您將得到類似以下的內容

$ vmc push
Would you like to deploy from the current directory? [Yn]: y
Application Name: rabbit-chat-sample
Application Deployed URL: 'rabbit-chat-sample.cloudfoundry.com'? 
Detected a Java SpringSource Spring Application, is this correct? [Yn]: y
Memory Reservation [Default:512M](64M, 128M, 256M, 512M or 1G)   
Creating Application: OK
Would you like to bind any services to 'rabbit-chat-sample'? [yN]: y
Would you like to use an existing provisioned service [yN]? n
The following system services are available:
1. mongodb
2. mysql
3. postgresql
4. rabbitmq
5. redis
Please select one you wish to provision: 4
Specify the name of the service [rabbitmq-5e262]:          
Creating Service: OK
Binding Service: OK
Uploading Application:
  Checking for available resources: OK
  Processing resources: OK
  Packing application: OK
  Uploading (3K): OK   
Push Status: OK
Staging Application: OK                                                         
Starting Application: OK

如果使用 STS,您只需要啟用 Cloud Foundry 支援(可從儀表板的「Extensions」選項卡中獲得),然後創建一個新的伺服器實例(有關所有詳細資訊,請參閱入門指南),您可以簡單地將應用程式拖曳到該實例。將應用程式添加到伺服器實例後,您可以透過 UI 配置和綁定服務。以下螢幕截圖顯示了「rabbit-chat」範例應用程式。

擴展應用程式

還記得關於「chatQueue」id 的討論,以及由於它使用 id 而不是 name 屬性,Queue 的名稱將如何產生嗎?那麼,我們在這裡使用 id 而不是 name 的原因是我們希望應用程式的每個實例都有自己的獨有且實際上是匿名的 Queue。Fanout Exchange 只有一個命名的實例。應用程式的每個實例都將其自己的 Queue 綁定到該 Exchange。Exchange 和 Queue 的這種解耦非常適合可擴展的雲端應用程式(尤其是因為那些名稱產生的 Queue 將在其擁有實例關閉時自動刪除)。

要擴展應用程式,您可以使用命令列中的 VMC

$ vmc instances rabbit-chat-sample +1
Scaling Application instances up to 2: OK

或者,您可以使用 STS 支援。以下是從上面發布的同一螢幕截圖中截取的「instances」配置的重點視圖

接下來是什麼?

本部落格旨在成為系列文章的第一篇。在接下來的文章中,我們將探討以下內容

  • 增強訊息傳遞功能,使其超出目前的全局廣播,以示範點對點訊息傳遞(用於與單一指定用戶聊天)以及使用動態分配的 Exchange 進行發布/訂閱,以表示「聊天室」。
  • 新增一個 Node.js 應用程式,它與 Java 應用程式並排存在,並參與相同的聊天,因為這兩個應用程式都綁定到同一個 RabbitMQ 服務實例。
  • 包含 Spring 3.1 profile 支援,以展示如何修改同一個配置文件,使其在 Cloud Foundry 或替代部署(例如本地 Tomcat 實例)上都能同樣良好地工作。

獲取 Spring 電子報

保持與 Spring 電子報的聯繫

訂閱

領先一步

VMware 提供培訓和認證,以加速您的進展。

了解更多

取得支援

Tanzu Spring 在一個簡單的訂閱中為 OpenJDK™、Spring 和 Apache Tomcat® 提供支援和二進制檔案。

了解更多

即將到來的活動

查看 Spring 社群中所有即將到來的活動。

查看全部