实现目标

这一篇文章,就要直接实现聊天的功能,并且,在聊天功能的基础上,再实现缓存一定聊天记录的功能。

第一步:聊天实现原理

首先,需要明确我们的需求。通常,网页上的聊天,都是聊天室的形式,所以,这个例子也就有了一个聊天的空间的概念,只要在这个空间内,就能够一起聊天。其次,每个人都能够发言,并且被其他的人看到,所以,每个人都会将自己所要说的内容发送到后台,后台转发给每一个人。
在客户端,可以用Socket很容易的实现;而在web端,以前都是通过轮询来实现的,但是WebSocket出现之后,就可以通过WebSocket像Socket客户端一样,通过长连接来实现这个功能了。

第二步:服务端基础代码

通过上面的原理分析可以知道,需要发送到后台的数据很简单,就是用户信息,聊天信息,和所在的空间信息,因为是一个简单的例子,所以bean就设计的比较简单了:
  1. public class UserChatCommand {
  2. private String name;
  3. private String chatContent;
  4. private String coordinationId;
  5. public String getName() {
  6. return name;
  7. }
  8. public void setName(String name) {
  9. this.name = name;
  10. }
  11. public String getChatContent() {
  12. return chatContent;
  13. }
  14. public void setChatContent(String chatContent) {
  15. this.chatContent = chatContent;
  16. }
  17. public String getCoordinationId() {
  18. return coordinationId;
  19. }
  20. public void setCoordinationId(String coordinationId) {
  21. this.coordinationId = coordinationId;
  22. }
  23. @Override
  24. public String toString() {
  25. return "UserChatCommand{" +
  26. "name='" + name + '\'' +
  27. ", chatContent='" + chatContent + '\'' +
  28. ", coordinationId='" + coordinationId + '\'' +
  29. '}';
  30. }
  31. }

通过这个bean来接收到web端发送的消息,然后在服务端转发,接下来就是转发的逻辑了,不过首先需要介绍一下Spring WebSocket的一个annotation。

spring mvc的controller层的annotation是RequestMapping大家都知道,同样的,WebSocket也有同样功能的annotation,就是MessageMapping,其值就是访问地址。现在就来看看controller层是怎么实现的吧:
  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的时候,可以在构造方法中进行注入:

  1. @Controller
  2. public class CoordinationController {
  3. ......
  4. //用于转发数据(sendTo)
  5. private SimpMessagingTemplate template;
  6. <pre name="code" class="java">    @Autowired
  7. public CoordinationController(SimpMessagingTemplate t) {
  8. template = t;
  9. }
  10. .....
  11. }

现在就已经将用户发送过来的聊天信息转发到了一个约定的空间内,只要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的整体代码如下:
  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,所以我们订阅这个地址就可以了,当订阅成功后,只要后台有转发消息,就会调用第二个方法,并且,将后台传过来的消息体作为参数。所以订阅的方法如下:
  1. //用户聊天订阅
  2. stompClient.subscribe('/userChat/chat' + coordinationId, function (chat) {
  3. showChat(JSON.parse(chat.body));
  4. });

将消息体转为json,再写一个显示聊天信息的方法就可以了,显示聊天信息的方法不再解释,如下:

  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,值是缓存信息。
  1. private Map<Integer, Object[]> coordinationCache = new HashMap<Integer, Object[]>();

这里我存的是一个Object数组,是因为我写的程序中,除了聊天信息的缓存,还有很多东西要缓存,只是将聊天信息的缓存放在了这个数组中的一个位置里。

为了简单起见,可以直接将web端发送过来的UserChatCommand对象存储到缓存里,而我们的服务器资源有限,既然我用Map放到内存中实现缓存,就不会没想到这点,我的想法是实现一个固定大小的队列,当达到队列大小上限的时候,就弹出最先进的元素,再插入要进入的元素,这样就保留了最新的聊天记录。
但是貌似没有这样的队列(我反正没在jdk中看到),所以我就自己实现了这样的一个队列,实现非常的简单,类名叫LimitQueue,使用泛型,继承自Queue,类中定义两个成员变量:
  1. private int limit;
  2. private Queue<E> queue;

limit代表队列的上限,queue是真正使用的队列。创建一个由这两个参数形成的构造方法,并且实现Queue的所有方法,所有的方法都由queue对象去完成,比如:

  1. @Override
  2. public int size() {
  3. return queue.size();
  4. }
  5. @Override
  6. public boolean isEmpty() {
  7. return queue.isEmpty();
  8. }

其中,有一个方法需要做处理:

  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里,所以更改之后的聊天接收方法如下:
  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端的代码:

  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,再组装一下数据就完成了,后台代码如下:

  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

Spring WebSocket教程(二)的更多相关文章

  1. Spring Security教程(二):自定义数据库查询

    Spring Security教程(二):自定义数据库查询   Spring Security自带的默认数据库存储用户和权限的数据,但是Spring Security默认提供的表结构太过简单了,其实就 ...

  2. Spring Security教程(二):通过数据库获得用户权限信息

    上一篇博客中,Spring Security教程(一):初识Spring Security,我把用户信息和权限信息放到了xml文件中,这是为了演示如何使用最小的配置就可以使用Spring Securi ...

  3. Spring Security教程(二)

    上一篇博客中,Spring Security教程(一),我把用户信息和权限信息放到了xml文件中,这是为了演示如何使用最小的配置就可以使用Spring Security,而实际开发中,用户信息和权限信 ...

  4. Spring WebSocket教程(一)

    学习背景 很久以前就知道WebSocket,但那时不论是浏览器还是开发技术对它的支持都还很少.但是,Spring4突然发布,让我眼前一亮,Spring4直接支持WebSocket. 对于Spring我 ...

  5. WebSocket教程(二)

    运行环境:jdk8 tomcat8 无须其他jar包. package com.reach.socketController; import java.io.IOException; import j ...

  6. Spring WebSocket入门(二) 转载

    本文转载自:http://www.jianshu.com/p/8500ad65eb50 WebSocket前端准备 前端我们需要用到两个js文件:sockjs.js和stomp.js SockJS:S ...

  7. web即时通讯2--基于Spring websocket达到web聊天室

    如本文所用,Spring4和websocket要构建web聊天室,根据框架SpringMVC+Spring+Hibernate的Maven项目,后台使用spring websocket进行消息转发和聊 ...

  8. Spring Security教程(三):自定义表结构

    在上一篇博客中讲解了用Spring Security自带的默认数据库存储用户和权限的数据,但是Spring Security默认提供的表结构太过简单了,其实就算默认提供的表结构很复杂,也不一定能满足项 ...

  9. Spring Security教程(三)

    在上一篇博客中讲解了用Spring Security自带的默认数据库存储用户和权限的数据,但是Spring Security默认提供的表结构太过简单了,其实就算默认提供的表结构很复杂,也不一定能满足项 ...

随机推荐

  1. 一个“蝇量级” C 语言协程库

    协程(coroutine)顾名思义就是“协作的例程”(co-operative routines).跟具有操作系统概念的线程不一样,协程是在用户空间利用程序语言的语法语义就能实现逻辑上类似多任务的编程 ...

  2. Java 8 Streams filter examples

    1. Streams filter() and collect() package com.mkyong.java8; import java.util.Arrays;import java.util ...

  3. C#基础第二天-作业答案-九九乘法表-打印星星

    题一:九九乘法表的答案 //正三角 ; i < ; i++) { ; j <= i; j++) { Console.Write("{0}*{1}={2} ", j, i ...

  4. jmeter 签名MD5生成

    请求接口需要同时发送签名,签名定义为: 可以看出签名就是把用户的密码 .用户名 和签名key生成一个md5串就可以了 刚好jmeter 有个md5 生成,生成前需要获取name ,password k ...

  5. php分享十六:php读取大文件总结

    一:file函数读取 file()函数的效率很底下 如果是有规律的文件.比如每行一条相应数据.那么尽量不要是用file()函数,可以使用file_get_contents()然后用explode切割. ...

  6. 安装SQL Server提示“等待数据库引擎恢复句柄失败”

    1.如果MSSQLSERVER服务已经启动,则停止. 2.以管理员身份打开命令行,执行命令:"C:\Program Files\Microsoft SQL Server\MSSQL10_50 ...

  7. Atitit atiplat_reader 基于url阅读器的新特性

    Atitit atiplat_reader 基于url阅读器的新特性 1.1. feature功能特性1 1.2. note1 1.1. feature功能特性 支持url数据源,实际就是只支持一层连 ...

  8. [svc][dhcp]关于无线ap自动发现选项dhcp option43配置

    有人问到option43的问题,查了点资料.总结了下win和linux的option43的配置: windows2003 http://www.h3c.com.cn/Service/Channel_S ...

  9. RDD PAPER

    https://cs.stanford.edu/~matei/ https://www2.eecs.berkeley.edu/Pubs/TechRpts/2014/EECS-2014-12.pdf h ...

  10. Java Base64 编码解码方案总结

    Base64是一种能将任意Binary资料用64种字元组合成字串的方法,而这个Binary资料和字串资料彼此之间是可以互相转换的,十分方便.在实际应用上,Base64除了能将Binary资料可视化之外 ...