Spring WebSocket教程(二)

實現目標

這一篇文章,就要直接實現聊天的功能,而且,在聊天功能的基礎上,再實現緩存必定聊天記錄的功能。

第一步:聊天實現原理

首先,須要明確咱們的需求。一般,網頁上的聊天,都是聊天室的形式,因此,這個例子也就有了一個聊天的空間的概念,只要在這個空間內,就可以一塊兒聊天。其次,每一個人都可以發言,而且被其餘的人看到,因此,每一個人都會將本身所要說的內容發送到後臺,後臺轉發給每個人。
在客戶端,能夠用Socket很容易的實現;而在web端,之前都是經過輪詢來實現的,可是WebSocket出現以後,就能夠經過WebSocket像Socket客戶端同樣,經過長鏈接來實現這個功能了。

第二步:服務端基礎代碼

經過上面的原理分析能夠知道,須要發送到後臺的數據很簡單,就是用戶信息,聊天信息,和所在的空間信息,由於是一個簡單的例子,因此bean就設計的比較簡單了:
[java]  view plain  copy
 
 在CODE上查看代碼片派生到個人代碼片
  1. public class UserChatCommand {  
  2.     private String name;  
  3.     private String chatContent;  
  4.     private String coordinationId;  
  5.   
  6.     public String getName() {  
  7.         return name;  
  8.     }  
  9.   
  10.     public void setName(String name) {  
  11.         this.name = name;  
  12.     }  
  13.   
  14.     public String getChatContent() {  
  15.         return chatContent;  
  16.     }  
  17.   
  18.     public void setChatContent(String chatContent) {  
  19.         this.chatContent = chatContent;  
  20.     }  
  21.   
  22.     public String getCoordinationId() {  
  23.         return coordinationId;  
  24.     }  
  25.   
  26.     public void setCoordinationId(String coordinationId) {  
  27.         this.coordinationId = coordinationId;  
  28.     }  
  29.   
  30.     @Override  
  31.     public String toString() {  
  32.         return "UserChatCommand{" +  
  33.                 "name='" + name + '\'' +  
  34.                 ", chatContent='" + chatContent + '\'' +  
  35.                 ", coordinationId='" + coordinationId + '\'' +  
  36.                 '}';  
  37.     }  
  38. }  
經過這個bean來接收到web端發送的消息,而後在服務端轉發,接下來就是轉發的邏輯了,不過首先須要介紹一下Spring WebSocket的一個annotation。
spring mvc的controller層的annotation是RequestMapping你們都知道,一樣的,WebSocket也有一樣功能的annotation,就是MessageMapping,其值就是訪問地址。如今就來看看controller層是怎麼實現的吧:
[java]  view plain  copy
 
 在CODE上查看代碼片派生到個人代碼片
  1. /** 
  2.  * WebSocket聊天的相應接收方法和轉發方法 
  3.  * 
  4.  * @param userChat 關於用戶聊天的各個信息 
  5.  */  
  6. @MessageMapping("/userChat")  
  7. public void userChat(UserChatCommand userChat) {  
  8.     //找到須要發送的地址  
  9.     String dest = "/userChat/chat" + userChat.getCoordinationId();  
  10.     //發送用戶的聊天記錄  
  11.     this.template.convertAndSend(dest, userChat);  
  12. }  
怎麼這麼簡單?呵呵,可以這麼簡單的實現後臺代碼,全是Spring的功勞。首先,咱們約定好發送地址的規則,就是chat後面跟上以前發送過來的id,而後經過這個「template」來進行轉發,這個「template」是Spring實現的一個發送模板類:SimpMessagingTemplate,在咱們定義controller的時候,能夠在構造方法中進行注入:
[java]  view plain  copy
 
 在CODE上查看代碼片派生到個人代碼片
  1. @Controller  
  2. public class CoordinationController {  
  3.   
  4.     ......  
  5.   
  6.     //用於轉發數據(sendTo)  
  7.     private SimpMessagingTemplate template;  
  8.     <pre name="code" class="java">    @Autowired  
  9.     public CoordinationController(SimpMessagingTemplate t) {  
  10.         template = t;  
  11.     }  
  12.     .....  
  13. }  
如今就已經將用戶發送過來的聊天信息轉發到了一個約定的空間內,只要web端的用戶訂閱的是這個空間的地址,那麼就會收到轉發過來的json。如今來看看web端須要作什麼吧。

第三步:Web端代碼

上一篇文章中已經介紹過了鏈接WebSocket,因此這裏就不重複的說了。
首先咱們建立一個頁面,在頁面中寫一個textarea(id=chat_content)用來當作聊天記錄顯示的地方,寫一個input(id=chat_input)當作聊天框,寫一個button當作發送按鈕,雖然簡陋了點,頁面的美化留到功能實現以後吧。
如今要用到上一篇文章中用於鏈接後臺的stompClient了,將這個stompClient定義爲全局變量,以方便咱們在任何地方使用它。按照邏輯,咱們先寫一個發送消息的方法,這樣能夠首先測試後臺是否是正確。
咱們寫一個function叫sendName(寫代碼的時候亂取的 尷尬),而且綁定到發送按鈕onclick事件。咱們要作的事情大概是如下幾步:
1.獲取input
2.所須要的數據組裝一個string
3.發送到後臺
第一步很簡單,使用jquery一秒搞定,第二步可使用JSON.stringify()方法搞定,第三步就要用到stompClient的send方法了,send方法有三個參數,第一個是發送的地址,第二個參數是頭信息,第三個參數是消息體,因此sendName的總體代碼以下:
[javascript]  view plain  copy
 
 在CODE上查看代碼片派生到個人代碼片
  1. //發送聊天信息  
  2. function sendName() {  
  3.     var input = $('#chat_input');  
  4.     var inputValue = input.val();  
  5.     input.val("");  
  6.     stompClient.send("/app/userChat", {}, JSON.stringify({  
  7.         'name': encodeURIComponent(name),  
  8.         'chatContent': encodeURIComponent(inputValue),  
  9.         'coordinationId': coordinationId  
  10.     }));  
  11. }  
其中,name和coordinationId是相應的用戶信息,能夠經過ajax或者jsp獲取,這裏就很少說了。
解釋一下爲何地址是"/app/userChat":
在第一篇文章中配置了WebSocket的信息,其中有一項是ApplicationDestinationPrefixes,配置的是"/app",從名字就能夠看出,是WebSocket程序地址的前綴,也就是說,其實這個"/app"是爲了區別普通地址和WebSocket地址的,因此只要是WebSocket地址,就須要在前面加上"/app",然後臺controller地址是"/userChat",因此,最後造成的地址就是"/app/userChat"。
如今運行一下程序,在後臺下一個斷點,咱們就能夠看到,聊天信息已經發送到了後臺。可是web端啥都沒有顯示,這是由於咱們尚未訂閱相應的地址,因此後臺轉發的消息根本就沒有去接收。
回到以前鏈接後臺的函數:stompClient.connect('', '', function (frame) {}),能夠注意到,最後一個是一個方法體,它是一個回調方法,當鏈接成功的時候就會調用這個方法,因此咱們訂閱後臺消息就在這個方法體裏作。stompClient的訂閱方法叫subscribe,有兩個參數,第一個參數是訂閱的地址,第二個參數是接收到消息時的回調函數。接下來就來嘗試訂閱聊天信息:
根據以前的約定,能夠獲得訂閱的地址是'/userChat/chat' + coordinationId,因此咱們訂閱這個地址就能夠了,當訂閱成功後,只要後臺有轉發消息,就會調用第二個方法,而且,將後臺傳過來的消息體做爲參數。因此訂閱的方法以下:
[javascript]  view plain  copy
 
 在CODE上查看代碼片派生到個人代碼片
  1. //用戶聊天訂閱  
  2. stompClient.subscribe('/userChat/chat' + coordinationId, function (chat) {  
  3.     showChat(JSON.parse(chat.body));  
  4. });  
將消息體轉爲json,再寫一個顯示聊天信息的方法就能夠了,顯示聊天信息的方法再也不解釋,以下:
[javascript]  view plain  copy
 
 在CODE上查看代碼片派生到個人代碼片
  1. //顯示聊天信息  
  2. function showChat(message) {  
  3.     var response = document.getElementById('chat_content');  
  4.     response.value += decodeURIComponent(message.name) + ':' + decodeURIComponent(message.chatContent) + '\n';  
  5. }  
由於以前處理中文問題,因此發到後臺的數據是轉碼了的,從後臺發回來以後,也須要將編碼轉回來。
到這裏,聊天功能就已經作完了,運行程序,會發現,真的能夠聊天了!一個聊天程序,就是這麼簡單。
可是這樣並不能知足,日後的功能能夠發揮咱們的想象力來添加,好比說:我以爲,聊天程序,至少也要緩存一些聊天記錄,否則後進來的用戶都不知道以前的用戶在聊什麼,用戶體驗會很是很差,接下來就看看聊天記錄的緩存是怎麼實現的吧。

第四步:聊天記錄緩存實現

因爲是一個小程序,就不使用數據庫來記錄緩存了,這樣不只麻煩,並且效率也低。我簡單的使用了一個Map來實現緩存。首先,咱們在controller中定義一個Map,這樣能夠保證在程序運行的時候,只有一個緩存副本。Map的鍵是每一個空間的id,值是緩存信息。
[java]  view plain  copy
 
 在CODE上查看代碼片派生到個人代碼片
  1. private Map<Integer, Object[]> coordinationCache = new HashMap<Integer, Object[]>();  
這裏我存的是一個Object數組,是由於我寫的程序中,除了聊天信息的緩存,還有不少東西要緩存,只是將聊天信息的緩存放在了這個數組中的一個位置裏。
爲了簡單起見,能夠直接將web端發送過來的UserChatCommand對象存儲到緩存裏,而咱們的服務器資源有限,既然我用Map放到內存中實現緩存,就不會沒想到這點,個人想法是實現一個固定大小的隊列,當達到隊列大小上限的時候,就彈出最早進的元素,再插入要進入的元素,這樣就保留了最新的聊天記錄。
可是貌似沒有這樣的隊列( 尷尬我反正沒在jdk中看到),因此我就本身實現了這樣的一個隊列,實現很是的簡單,類名叫LimitQueue,使用泛型,繼承自Queue,類中定義兩個成員變量:
[java]  view plain  copy
 
 在CODE上查看代碼片派生到個人代碼片
  1. private int limit;  
  2. private Queue<E> queue;  
limit表明隊列的上限,queue是真正使用的隊列。建立一個由這兩個參數造成的構造方法,而且實現Queue的全部方法,全部的方法都由queue對象去完成,好比:
[java]  view plain  copy
 
 在CODE上查看代碼片派生到個人代碼片
  1. @Override  
  2. public int size() {  
  3.     return queue.size();  
  4. }  
  5.   
  6. @Override  
  7. public boolean isEmpty() {  
  8.     return queue.isEmpty();  
  9. }  
其中,有一個方法須要作處理:
[java]  view plain  copy
 
 在CODE上查看代碼片派生到個人代碼片
  1. @Override  
  2. public boolean offer(E e) {  
  3.     if (queue.size() >= limit) {  
  4.         queue.poll();  
  5.     }  
  6.     return queue.offer(e);  
  7. }  
加入元素的時候,判斷是否達到了上限,達到了的話就先出隊列,再入隊列。這樣,就實現了固定大小的隊列,而且老是保持最新的記錄。
而後,在web端發送聊天消息到後臺的時候,就能夠將消息記錄在這個隊列中,保存在Map裏,因此更改以後的聊天接收方法以下:
[java]  view plain  copy
 
 在CODE上查看代碼片派生到個人代碼片
  1. /** 
  2.      * WebSocket聊天的相應接收方法和轉發方法 
  3.      * 
  4.      * @param userChat 關於用戶聊天的各個信息 
  5.      */  
  6.     @MessageMapping("/userChat")  
  7.     public void userChat(UserChatCommand userChat) {  
  8.         //找到須要發送的地址  
  9.         String dest = "/userChat/chat" + userChat.getCoordinationId();  
  10.         //發送用戶的聊天記錄  
  11.         this.template.convertAndSend(dest, userChat);  
  12.         //獲取緩存,並將用戶最新的聊天記錄存儲到緩存中  
  13.         Object[] cache = coordinationCache.get(Integer.parseInt(userChat.getCoordinationId()));  
  14.         try {  
  15.             userChat.setName(URLDecoder.decode(userChat.getName(), "utf-8"));  
  16.             userChat.setChatContent(URLDecoder.decode(userChat.getChatContent(), "utf-8"));  
  17.         } catch (UnsupportedEncodingException e) {  
  18.             e.printStackTrace();  
  19.         }  
  20.         ((LimitQueue<UserChatCommand>) cache[1]).offer(userChat);  
  21.     }  
已經有緩存了,只要在頁面上取出緩存就能顯示聊天記錄了,能夠經過ajax或者jsp等方法,不過,WebSocket也有方法能夠實現,由於Spring WebSocket提供了一個叫SubscribeMapping的annotation,這個annotation標記的方法,是在訂閱的時候調用的,也就是說,基本是隻執行一次的方法,很適合咱們來初始化聊天記錄。因此,在訂閱聊天信息的代碼下面,能夠增長一個初始化聊天記錄的方法。咱們先寫好web端的代碼:
[javascript]  view plain  copy
 
 在CODE上查看代碼片派生到個人代碼片
  1. //初始化  
  2. stompClient.subscribe('/app/init/' + coordinationId, function (initData) {  
  3.     console.log(initData);  
  4.     var body = JSON.parse(initData.body);  
  5.     var chat = body.chat;  
  6.     chat.forEach(function(item) {  
  7.         showChat(item);  
  8.     });  
  9. });  
此次訂閱的地址是init,仍是加上coordinationId來區分空間,發送過來的數據是一個聊天記錄的數組,循環顯示在對話框中。有了web端代碼的約束,後臺代碼也基本出來了,只要使用SubscribeMapping,再組裝一下數據就完成了,後臺代碼以下:
[java]  view plain  copy
 
 在CODE上查看代碼片派生到個人代碼片
  1. /** 
  2.      * 初始化,初始化聊天記錄 
  3.      * 
  4.      * @param coordinationId 協同空間的id 
  5.      */  
  6.     @SubscribeMapping("/init/{coordinationId}")  
  7.     public Map<String,Object> init(@DestinationVariable("coordinationId") int coordinationId) {  
  8.         System.out.println("------------新用戶進入,空間初始化---------");  
  9.         Map<String, Object> document = new HashMap<String, Object>();  
  10.         document.put("chat",coordinationCache.get(coordinationId)[1]);  
  11.         return document;  
  12.     }  
就這樣,緩存聊天記錄也實現了。

結語

這是個人畢業設計,個人畢業設計是一個在線協同備課系統,用於多人在線同時且實時操做文檔和演示文稿,其中包含了聊天這個小功能,因此使用它來說解一下Spring WebSocket的使用。
我將代碼放到了 github上,有興趣的朋友能夠去看看代碼,接下來,我會考慮將個人畢業設計的源碼介紹一下,其中有不少不足,也但願你們指正。 大笑
github地址:https://github.com/xjyaikj/OnlinePreparation
 
轉自 http://blog.csdn.net/xjyzxx/article/details/38542665
相關文章
相關標籤/搜索