基于SpringBoot、STOMP使用WebSocket实现聊天室功能

WebSocket:

新项目中有一个模块需求使用到了WebSocket,因为之前没用过,所以做了一些研究。

关于WebSocket,简单说两句:最初前端与后端交互都是基于Http协议,前端发送request,后端返回response。存在的问题就是:response永远是被动的,不能主动发起。如果要想保持与后端的长连接,最初的实现方式基本都是ajax轮询或者http long poll。这两种方式都需要占用很多的资源,并且Http还是一个无状态协议。而WebSocket是HTML5出的东西,通俗点就是可以实现前端只发送一次请求,后端就可以与前端保持长连接,并实时的传输数据。最直白的例子就是聊天系统的实现,点对点两个人聊天,你一句我一句。也可以群嗨,一大群人叽叽歪歪,这样每个人对于其他人来说可以理解为一个广播系统,其他人可以理解为自己的订阅者。

项目中基于SpringBoot和STOMP,其中的逻辑相对复杂,这儿仅写一个简单的Demo:实现点对点聊天功能。话不多说,直接开搞。

代码中的具体解释看注释,都描述的很清楚。

首先了解下STOMP:
Stomp是一种简单(流)文本定向消息协议,提供了一个可互操作的链接格式。允许stomp客户端与任意stomp消息代理(Broker)进行交互。

一:新建一个SpringBoot项目,选择Security、Thymeleaf和WebSocket依赖。

WechatIMG45

二:Spring Security的简单配置

本例只是实现了一个简单的聊天室程序。例子中有两个用户,互相发送消息给彼此,所以我们在这里我们先对Spring Security做一些简单的配置。对于Spring Security这里不做特别多的解释,Spring Security是专门针对基于Spring的项目安全框架,和Shiro一样可以实现程序的认证和权限控制。

这里主要是分配两个用户,名字为“Michael”和“Janet”,密码都是“freedom”。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
package cn.js.websocket.config;

import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.builders.WebSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
@Configuration
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

protected void configure(HttpSecurity httpSecurity) throws Exception {
httpSecurity
.authorizeRequests()
.antMatchers("/","/login")//设置Spring Security对/和/"login"路径不拦截
.permitAll()
.anyRequest()
.authenticated()
.and()
.formLogin()
.loginPage("/login")//设置SpringSecurity的登录页面为/login
.defaultSuccessUrl("/chat")//登录成功后转向/chat路径
.permitAll()
.and()
.logout()
.permitAll();
}
//在内存中分配两个用户Michael和Janet,密码都为freedom,角色都是USER
protected void configure(AuthenticationManagerBuilder authenticationManagerBuilder) throws Exception {
authenticationManagerBuilder
.inMemoryAuthentication()
.withUser("Michael").password("freedom").roles("USER")
.and()
.withUser("Janet").password("freedom").roles("USER");
}
// /resources/static/目录下的静态资源,Spring Security不拦截
public void configure(WebSecurity webSecurity)throws Exception{
webSecurity.ignoring().antMatchers("/resources/static/**");
}

}

三:配置WebSocket

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23

package cn.js.websocket.config;

import org.springframework.context.annotation.Configuration;
import org.springframework.messaging.simp.config.MessageBrokerRegistry;
import org.springframework.web.socket.config.annotation.AbstractWebSocketMessageBrokerConfigurer;
import org.springframework.web.socket.config.annotation.EnableWebSocketMessageBroker;
import org.springframework.web.socket.config.annotation.StompEndpointRegistry;
@Configuration
//此注解表示开启WebSocket支持。通过此注解开启使用STOMP协议来传输基于代理(message broker)的消息。
@EnableWebSocketMessageBroker
public class WebSocketConfig extends AbstractWebSocketMessageBrokerConfigurer {
@Override
public void registerStompEndpoints(StompEndpointRegistry stompEndpointRegistry) {
//注册一个名为/endpointChat的节点,并指定使用SockJS协议。
stompEndpointRegistry.addEndpoint("/endpointChat").withSockJS();
}
//配置消息代理(Message Broker),可以理解为信息传输的通道
public void configureMessageBroker(MessageBrokerRegistry messageBrokerRegistry) {
//点对点式应增加一个/queue的消息代理。相应的如果是广播室模式可以设置为"/topic"
messageBrokerRegistry.enableSimpleBroker("/queue");
}
}

四:编写控制器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
package cn.js.websocket.controller;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.messaging.handler.annotation.MessageMapping;
import org.springframework.messaging.simp.SimpMessagingTemplate;
import org.springframework.stereotype.Controller;
import java.security.Principal;

@Controller
public class WebSocketController {
@Autowired
//通过SimpMessagingTemplate模板向浏览器发送消息。如果是广播模式,可以直接使用注解@SendTo
private SimpMessagingTemplate simpMessagingTemplate;

//开启STOMP协议来传输基于代理的消息,这时控制器支持使用@MessageController,就像使用@RequestMapping 是一样的
//当浏览器向服务端发送请求时,通过@MessageController映射/chat这个路径
@MessageMapping("/chat")
//在SpringMVC中,可以直接在参数中获得principal,其中包含当前用户的信息
public void handleChat(Principal principal,String msg) {
//下面的代码就是如果发送人是Michael,接收人就是Janet,发送的信息是message,反之亦然。
if(principal.getName().equals("Michael")){
//通过SimpMessagingTemplate的convertAndSendToUser向用户发送消息。
//第一参数表示接收信息的用户,第二个是浏览器订阅的地址,第三个是消息本身
simpMessagingTemplate.convertAndSendToUser("Janet","/queue/notifications",
principal.getName() + "-发送:" + msg);
} else {
simpMessagingTemplate.convertAndSendToUser("Michael","/queue/notifications",
principal.getName() + "-发送:" + msg);
}
}
}

五:添加脚本

将sockjs.min.js(SockJS的客户端脚本)、stomp.js(STOMP协议的客户端脚本)、jquery-3.1.1.js(jQuery)放置在src/main/resources/static下,(可在文末GitHub中Clone项目得到三个文件)

六:编写登录和聊天室页面

因为此Demo基于Thymeleaf,所以在src/main/resources/templates下新建一个最简单的login.html页面,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.w3.org/1999/xhtml">
<head>
<meta charset="UTF-8">
<title>聊天室登录页面</title>
</head>
<body>
<div th:if="${param.error}">
无效的账号和密码
</div>
<div th:if="${param.logout}">
您已注销
</div>
<form th:action="@{/login}" method="post">
<div><label>账号:<input type="text" name="username"/></label></div>
<div><label>密码:<input type="password" name="password"/></label></div>
<div><input type="submit" value="登录"/></div>
</form>
</body>
</html>

在src/main/resources/templates下新建chat.html页面,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8" />
<title>聊天页面</title>
<script th:src="@{sockjs.min.js}"></script>
<script th:src="@{stomp.js}"></script>
<script th:src="@{jquery-3.1.1.js}"></script>
</head>
<body>
<p>
聊天室
</p>

<form id="JanetForm">
<textarea rows="4" cols="60" name="text"></textarea>
<input type="submit"/>
</form>

<script th:inline="javascript">
$('#JanetForm').submit(function (e) {
e.preventDefault();
var text = $('#JanetForm').find('textarea[name="text"]').val();
sendSpittle(text);
});
// 连接endpoint为"/endpointChat"的节点
var sock = new SockJS("/endpointChat");
var stomp = Stomp.over(sock);
// 连接WebSocket服务端
stomp.connect('guest','guest',function (frame) {
// 订阅/user/queue/notifications发送的消息,这里与在控制器的messagingTemplate.convertAndSendToUser中定义的订阅地址保持一致。
// 这里多了一个/user,并且这个user是必须的,使用了/user才会发送消息到指定的用户
stomp.subscribe("/user/queue/notifications",handleNotification);
});
function handleNotification(message) {
$('#output').append("<b>收到了:" + message.body + "</b><br/>")
}
function sendSpittle(text) {
// 表示向后端路径/chat发送消息请求,这个是在控制器中@MessageMapping中定义的。
stomp.send("/chat",{},text);
}
$('#stop').click(function () {
{sock.close()}
});
</script>
<div id="output"></div>
</body>
</html>

七:增加页面的viewController,指定页面的跳转

1
2
3
4
5
6
7
8
9
10
11
12
13
package cn.js.websocket.config;

import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.ViewControllerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter;

@Configuration
public class WebMvcConfig extends WebMvcConfigurerAdapter{
public void addViewControllers(ViewControllerRegistry viewControllerRegistry) {
viewControllerRegistry.addViewController("/login").setViewName("/login");
viewControllerRegistry.addViewController("/chat").setViewName("/chat");
}
}

八:测试

这样我们开两个浏览器窗口,地址为localhost:8080/login,分别用Michael和Janet两个用户登录,就可以互相发送消息聊天了。

WechatIMG48

WechatIMG50

项目Github地址:https://github.com/jia-shun/websocket

欢迎关注我的GitHub。

参考资料:Java EE开发的颠覆者:SpringBoot 实战

快掏出你的大手机扫我

快掏出你的大手机扫我