實現目標
這一篇文章,就要直接實現聊天的功能,而且,在聊天功能的基礎上,再實現緩存必定聊天記錄的功能。
第一步:聊天實現原理
首先,須要明確咱們的需求。一般,網頁上的聊天,都是聊天室的形式,因此,這個例子也就有了一個聊天的空間的概念,只要在這個空間內,就可以一塊兒聊天。其次,每一個人都可以發言,而且被其餘的人看到,因此,每一個人都會將本身所要說的內容發送到後臺,後臺轉發給每個人。
在客戶端,能夠用Socket很容易的實現;而在web端,之前都是經過輪詢來實現的,可是WebSocket出現以後,就能夠經過WebSocket像Socket客戶端同樣,經過長鏈接來實現這個功能了。
第二步:服務端基礎代碼
經過上面的原理分析能夠知道,須要發送到後臺的數據很簡單,就是用戶信息,聊天信息,和所在的空間信息,由於是一個簡單的例子,因此bean就設計的比較簡單了:
- public class UserChatCommand {
- private String name;
- private String chatContent;
- private String coordinationId;
-
- public String getName() {
- return name;
- }
-
- public void setName(String name) {
- this.name = name;
- }
-
- public String getChatContent() {
- return chatContent;
- }
-
- public void setChatContent(String chatContent) {
- this.chatContent = chatContent;
- }
-
- public String getCoordinationId() {
- return coordinationId;
- }
-
- public void setCoordinationId(String coordinationId) {
- this.coordinationId = coordinationId;
- }
-
- @Override
- public String toString() {
- return "UserChatCommand{" +
- "name='" + name + '\'' +
- ", chatContent='" + chatContent + '\'' +
- ", coordinationId='" + coordinationId + '\'' +
- '}';
- }
- }
經過這個bean來接收到web端發送的消息,而後在服務端轉發,接下來就是轉發的邏輯了,不過首先須要介紹一下Spring WebSocket的一個annotation。
spring mvc的controller層的annotation是RequestMapping你們都知道,一樣的,WebSocket也有一樣功能的annotation,就是MessageMapping,其值就是訪問地址。如今就來看看controller層是怎麼實現的吧:
- @MessageMapping("/userChat")
- public void userChat(UserChatCommand userChat) {
-
- String dest = "/userChat/chat" + userChat.getCoordinationId();
-
- this.template.convertAndSend(dest, userChat);
- }
怎麼這麼簡單?呵呵,可以這麼簡單的實現後臺代碼,全是Spring的功勞。首先,咱們約定好發送地址的規則,就是chat後面跟上以前發送過來的id,而後經過這個「template」來進行轉發,這個「template」是Spring實現的一個發送模板類:SimpMessagingTemplate,在咱們定義controller的時候,能夠在構造方法中進行注入:
- @Controller
- public class CoordinationController {
-
- ......
-
-
- private SimpMessagingTemplate template;
- <pre name="code" class="java"> @Autowired
- public CoordinationController(SimpMessagingTemplate t) {
- template = t;
- }
- .....
- }
如今就已經將用戶發送過來的聊天信息轉發到了一個約定的空間內,只要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的總體代碼以下:
- function sendName() {
- var input = $('#chat_input');
- var inputValue = input.val();
- input.val("");
- stompClient.send("/app/userChat", {}, JSON.stringify({
- 'name': encodeURIComponent(name),
- 'chatContent': encodeURIComponent(inputValue),
- 'coordinationId': coordinationId
- }));
- }
其中,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,因此咱們訂閱這個地址就能夠了,當訂閱成功後,只要後臺有轉發消息,就會調用第二個方法,而且,將後臺傳過來的消息體做爲參數。因此訂閱的方法以下:
- stompClient.subscribe('/userChat/chat' + coordinationId, function (chat) {
- showChat(JSON.parse(chat.body));
- });
將消息體轉爲json,再寫一個顯示聊天信息的方法就能夠了,顯示聊天信息的方法再也不解釋,以下:
- function showChat(message) {
- var response = document.getElementById('chat_content');
- response.value += decodeURIComponent(message.name) + ':' + decodeURIComponent(message.chatContent) + '\n';
- }
由於以前處理中文問題,因此發到後臺的數據是轉碼了的,從後臺發回來以後,也須要將編碼轉回來。
到這裏,聊天功能就已經作完了,運行程序,會發現,真的能夠聊天了!一個聊天程序,就是這麼簡單。
可是這樣並不能知足,日後的功能能夠發揮咱們的想象力來添加,好比說:我以爲,聊天程序,至少也要緩存一些聊天記錄,否則後進來的用戶都不知道以前的用戶在聊什麼,用戶體驗會很是很差,接下來就看看聊天記錄的緩存是怎麼實現的吧。
第四步:聊天記錄緩存實現
因爲是一個小程序,就不使用數據庫來記錄緩存了,這樣不只麻煩,並且效率也低。我簡單的使用了一個Map來實現緩存。首先,咱們在controller中定義一個Map,這樣能夠保證在程序運行的時候,只有一個緩存副本。Map的鍵是每一個空間的id,值是緩存信息。
- private Map<Integer, Object[]> coordinationCache = new HashMap<Integer, Object[]>();
這裏我存的是一個Object數組,是由於我寫的程序中,除了聊天信息的緩存,還有不少東西要緩存,只是將聊天信息的緩存放在了這個數組中的一個位置裏。
爲了簡單起見,能夠直接將web端發送過來的UserChatCommand對象存儲到緩存裏,而咱們的服務器資源有限,既然我用Map放到內存中實現緩存,就不會沒想到這點,個人想法是實現一個固定大小的隊列,當達到隊列大小上限的時候,就彈出最早進的元素,再插入要進入的元素,這樣就保留了最新的聊天記錄。
可是貌似沒有這樣的隊列(
我反正沒在jdk中看到),因此我就本身實現了這樣的一個隊列,實現很是的簡單,類名叫LimitQueue,使用泛型,繼承自Queue,類中定義兩個成員變量:
- private int limit;
- private Queue<E> queue;
limit表明隊列的上限,queue是真正使用的隊列。建立一個由這兩個參數造成的構造方法,而且實現Queue的全部方法,全部的方法都由queue對象去完成,好比:
- @Override
- public int size() {
- return queue.size();
- }
-
- @Override
- public boolean isEmpty() {
- return queue.isEmpty();
- }
其中,有一個方法須要作處理:
- @Override
- public boolean offer(E e) {
- if (queue.size() >= limit) {
- queue.poll();
- }
- return queue.offer(e);
- }
加入元素的時候,判斷是否達到了上限,達到了的話就先出隊列,再入隊列。這樣,就實現了固定大小的隊列,而且老是保持最新的記錄。
而後,在web端發送聊天消息到後臺的時候,就能夠將消息記錄在這個隊列中,保存在Map裏,因此更改以後的聊天接收方法以下:
- @MessageMapping("/userChat")
- public void userChat(UserChatCommand userChat) {
-
- String dest = "/userChat/chat" + userChat.getCoordinationId();
-
- this.template.convertAndSend(dest, userChat);
-
- Object[] cache = coordinationCache.get(Integer.parseInt(userChat.getCoordinationId()));
- try {
- userChat.setName(URLDecoder.decode(userChat.getName(), "utf-8"));
- userChat.setChatContent(URLDecoder.decode(userChat.getChatContent(), "utf-8"));
- } catch (UnsupportedEncodingException e) {
- e.printStackTrace();
- }
- ((LimitQueue<UserChatCommand>) cache[1]).offer(userChat);
- }
已經有緩存了,只要在頁面上取出緩存就能顯示聊天記錄了,能夠經過ajax或者jsp等方法,不過,WebSocket也有方法能夠實現,由於Spring WebSocket提供了一個叫SubscribeMapping的annotation,這個annotation標記的方法,是在訂閱的時候調用的,也就是說,基本是隻執行一次的方法,很適合咱們來初始化聊天記錄。因此,在訂閱聊天信息的代碼下面,能夠增長一個初始化聊天記錄的方法。咱們先寫好web端的代碼:
- stompClient.subscribe('/app/init/' + coordinationId, function (initData) {
- console.log(initData);
- var body = JSON.parse(initData.body);
- var chat = body.chat;
- chat.forEach(function(item) {
- showChat(item);
- });
- });
此次訂閱的地址是init,仍是加上coordinationId來區分空間,發送過來的數據是一個聊天記錄的數組,循環顯示在對話框中。有了web端代碼的約束,後臺代碼也基本出來了,只要使用SubscribeMapping,再組裝一下數據就完成了,後臺代碼以下:
- @SubscribeMapping("/init/{coordinationId}")
- public Map<String,Object> init(@DestinationVariable("coordinationId") int coordinationId) {
- System.out.println("------------新用戶進入,空間初始化---------");
- Map<String, Object> document = new HashMap<String, Object>();
- document.put("chat",coordinationCache.get(coordinationId)[1]);
- return document;
- }
就這樣,緩存聊天記錄也實現了。
結語
這是個人畢業設計,個人畢業設計是一個在線協同備課系統,用於多人在線同時且實時操做文檔和演示文稿,其中包含了聊天這個小功能,因此使用它來說解一下Spring WebSocket的使用。
我將代碼放到了
github上,有興趣的朋友能夠去看看代碼,接下來,我會考慮將個人畢業設計的源碼介紹一下,其中有不少不足,也但願你們指正。
github地址:https://github.com/xjyaikj/OnlinePreparation
轉自 http://blog.csdn.net/xjyzxx/article/details/38542665