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


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM