WebSocket可实现浏览器和服务器之间的通信,如在线聊天,消息推送等,其基于tcp协议来传输数据。而stomp是一种更高级的协议,可以更加方便的实现WebSocket。
broker和客户端
客户端可以是任何语言,如js,php等,只须使用stomp协议来收发消息,broker可对消息进行处理或转发等。本篇将介绍以spring boot实现broker,以js实现客户端。
pom.xml
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-websocket</artifactId>
</dependency>
配置消息端点
spring boot会自动配置好broker,我们只需添加端点如/endpoint1,客户端通过端点建立连接。
@SpringBootConfiguration
@EnableWebSocketMessageBroker //自动配置Broker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {
public void registerStompEndpoints(StompEndpointRegistry registry) {
registry.addEndpoint("/endpoint1").withSockJS();
}
消息广播
java代码:每当请求/sent时向/topic1发送消息hello,world,所有订阅了/topic1的客户端都会收到这个消息。
@Autowired
private SimpMessagingTemplate messagingTemplate;
@GetMapping("/sent")
public void sent(HttpSession session) {
messagingTemplate.convertAndSend("/topic1", "hello,world");
}
js代码:向/endpoint1端点建立连接,同时订阅/topic1的消息,并弹出窗口,显示消息。中间有个{}传参后面再讲。
<script src="https://cdn.bootcss.com/sockjs-client/1.1.4/sockjs.min.js"></script>
<script src="https://cdn.bootcss.com/stomp.js/2.3.3/stomp.min.js"></script>
<script type="text/javascript">
var socket = new SockJS('/endpoint1');
var stompClient = Stomp.over(socket);
stompClient.connect({}, function() {
stompClient.subscribe('/topic1', function(respnose){
alert(respnose.body);
});
});
启动项目,首先打开js的页面即会建立连接,每当服务器收到/sent请求便会向topic1发消息,页面会自己弹出hello,world。
消息接收
java代码:当客户端向/hello发消息时,在服务器控制台打印消息
@MessageMapping("/hello")
public void hello(String message) {
System.out.println(message);
html代码:一个按钮,用来触发发消息事件
<button οnclick="sendTest();">发消息</button>
js代码:加到前面js代码中,向/hello发消息,"test"会传给后台的message。这里也有个{}和connect中的{}是一样的作用,用来传递头部参数,后面会讲。
function sendTest() {
stompClient.send("/hello", {}, "test");
}
权限拦截器
修改端点配置代码,设置拦截器。每当客户端连接端点时都会被拦截,在这里可以读取到session,进行验证。和mvc拦截器类似。当连接建立以后,通过该连接收发消息就不需要再次验证了。
public void registerStompEndpoints(StompEndpointRegistry registry) {
registry.addEndpoint("/endpoint1").withSockJS()
.setInterceptors(new HandshakeInterceptor(){
@Override
public void afterHandshake(ServerHttpRequest arg0, ServerHttpResponse arg1, WebSocketHandler arg2,
Exception arg3) {
}
@Override
public boolean beforeHandshake(ServerHttpRequest arg0, ServerHttpResponse arg1, WebSocketHandler arg2,
Map<String, Object> arg3) throws Exception {
//拿到session,自己完成验证代码
HttpSession session = getSession(arg0);
return true;
}
private HttpSession getSession(ServerHttpRequest request) {
if (request instanceof ServletServerHttpRequest) {
ServletServerHttpRequest serverRequest = (ServletServerHttpRequest) request;
return serverRequest.getServletRequest().getSession();
}
return null;
}
}); 请求
}
请求拦截器
上面客户端建立连接时,可以拿到session。而一但连接建立成功,就可以直接收发消息,但是无法拿到session或request。此时可以使用Principal来识别 每条消息的用户来源。
在前面的WebSocketConfig类中加入以下方法,客户端建立连接和收发消息时都会被它拦截,不影响上面的权限拦截器。这段代码意思是:当用户建立连接时,将前面说的{}中的参数username存入Principa。以后每次客户端发消息时,都可以从Principa读取到其username
@Override
public void configureClientInboundChannel(ChannelRegistration registration) {
//注册拦截器
registration.interceptors(new ChannelInterceptorAdapter() {
@Override
public Message<?> preSend(Message<?> message, MessageChannel channel) {
StompHeaderAccessor accessor = MessageHeaderAccessor.getAccessor(message, StompHeaderAccessor.class);
//判断客户端发出的命令是不是CONNECT
if (StompCommand.CONNECT.equals(accessor.getCommand())) {
//这里的username参数就是从前面的{}中传过来的
Principal user = new MyPrincipal(accessor.getNativeHeader("username").get(0));
//类似于设置session,在控制层获取该参数时,会调用下面的MyPrincipal.getName(),下面会讲
accessor.setUser(user);
}
//拦截处理完后转发message,如果不允许该消息,可以返回null
return message;
}
});
}
//内部类实现Principal接口
class MyPrincipal implements Principal{
private String name;
public MyPrincipal(String name){
this.name=name;
}
@Override
public String getName() {
return name;
}
}
修改/hello接收消息方法的参数,以下的principal.getName()就是上面的MyPrincipal.getName(),可获取username参数值
@MessageMapping("/hello")
public void hello(Principal principal,String message) {
System.out.println(principal.getName());
修改js中的{}传参如下,对应拦截器中的accessor.getNativeHeader("username")
stompClient.connect({username:"tom"}, function() {
此时,客户端连接后,每次点击按钮向/hello发消息时,都会打印出其username,以此确认消息来源用户。
点对点消息
点对点即向特定用户发消息,上面已经确认消息来源,现在只需要确认特定的消息接收者。
修改hello方法,将tom发过来的message单独发给jerry,而不再是公共广播
@MessageMapping("/hello")
public void hello(Principal principal,String message) {
messagingTemplate.convertAndSendToUser("jerry","/topic1", principal.getName()+"say:"+message);
修改js,注意固定语法/user/jerry,指只订阅user用户jerry的消息
stompClient.subscribe('/user/jerry/topic1', function(respnose){
alert(respnose.body);
});
此时,点击消息按钮时,将弹出来自tom的消息。这里发消息和收消息都在一个页面,出于方便我把username写死了,实际可以动态设置用户名,不同用户互发消息,不解释了。