实现目标
这一篇文章,就要直接实现聊天的功能,而且,在聊天功能的基础上,再实现缓存必定聊天记录的功能。
第一步:聊天实现原理
首先,须要明确咱们的需求。一般,网页上的聊天,都是聊天室的形式,因此,这个例子也就有了一个聊天的空间的概念,只要在这个空间内,就可以一块儿聊天。其次,每一个人都可以发言,而且被其余的人看到,因此,每一个人都会将本身所要说的内容发送到后台,后台转发给每个人。
在客户端,能够用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