實現目標
這一篇文章,就要直接實現聊天的功能,並且,在聊天功能的基礎上,再實現緩存一定聊天記錄的功能。
第一步:聊天實現原理
首先,需要明確我們的需求。通常,網頁上的聊天,都是聊天室的形式,所以,這個例子也就有了一個聊天的空間的概念,只要在這個空間內,就能夠一起聊天。其次,每個人都能夠發言,並且被其他的人看到,所以,每個人都會將自己所要說的內容發送到后台,后台轉發給每一個人。
在客戶端,可以用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 + '\'' +
- '}';
- }
- }
spring mvc的controller層的annotation是RequestMapping大家都知道,同樣的,WebSocket也有同樣功能的annotation,就是MessageMapping,其值就是訪問地址。現在就來看看controller層是怎么實現的吧:
- /**
- * WebSocket聊天的相應接收方法和轉發方法
- *
- * @param userChat 關於用戶聊天的各個信息
- */
- @MessageMapping("/userChat")
- public void userChat(UserChatCommand userChat) {
- //找到需要發送的地址
- String dest = "/userChat/chat" + userChat.getCoordinationId();
- //發送用戶的聊天記錄
- this.template.convertAndSend(dest, userChat);
- }
- @Controller
- public class CoordinationController {
- ......
- //用於轉發數據(sendTo)
- private SimpMessagingTemplate template;
- <pre name="code" class="java"> @Autowired
- public CoordinationController(SimpMessagingTemplate t) {
- template = t;
- }
- .....
- }
第三步:Web端代碼
上一篇文章中已經介紹過了連接WebSocket,所以這里就不重復的說了。
首先我們創建一個頁面,在頁面中寫一個textarea(id=chat_content)用來當做聊天記錄顯示的地方,寫一個input(id=chat_input)當做聊天框,寫一個button當做發送按鈕,雖然簡陋了點,頁面的美化留到功能實現之后吧。
現在要用到上一篇文章中用於連接后台的stompClient了,將這個stompClient定義為全局變量,以方便我們在任何地方使用它。按照邏輯,我們先寫一個發送消息的方法,這樣可以首先測試后台是不是正確。
我們寫一個function叫sendName(寫代碼的時候亂取的
),並且綁定到發送按鈕onclick事件。我們要做的事情大概是以下幾步:
),並且綁定到發送按鈕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
- }));
- }
解釋一下為什么地址是"/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));
- });
- //顯示聊天信息
- 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[]>();
為了簡單起見,可以直接將web端發送過來的UserChatCommand對象存儲到緩存里,而我們的服務器資源有限,既然我用Map放到內存中實現緩存,就不會沒想到這點,我的想法是實現一個固定大小的隊列,當達到隊列大小上限的時候,就彈出最先進的元素,再插入要進入的元素,這樣就保留了最新的聊天記錄。
但是貌似沒有這樣的隊列(
我反正沒在jdk中看到),所以我就自己實現了這樣的一個隊列,實現非常的簡單,類名叫LimitQueue,使用泛型,繼承自Queue,類中定義兩個成員變量:
我反正沒在jdk中看到),所以我就自己實現了這樣的一個隊列,實現非常的簡單,類名叫LimitQueue,使用泛型,繼承自Queue,類中定義兩個成員變量:
- private int limit;
- private Queue<E> 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里,所以更改之后的聊天接收方法如下:
- /**
- * WebSocket聊天的相應接收方法和轉發方法
- *
- * @param userChat 關於用戶聊天的各個信息
- */
- @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);
- }
- //初始化
- 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);
- });
- });
- /**
- * 初始化,初始化聊天記錄
- *
- * @param coordinationId 協同空間的id
- */
- @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
