stomp本质是广播模式,所以次socket更适用于广播
使用方法:
依赖:
<!--websocket-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-websocket</artifactId>
</dependency>
配置类:
package com.matrix.npdm.config;
import org.springframework.context.annotation.Configuration;
import org.springframework.messaging.converter.MessageConverter;
import org.springframework.messaging.handler.invocation.HandlerMethodArgumentResolver;
import org.springframework.messaging.handler.invocation.HandlerMethodReturnValueHandler;
import org.springframework.messaging.simp.config.ChannelRegistration;
import org.springframework.messaging.simp.config.MessageBrokerRegistry;
import org.springframework.web.socket.config.annotation.EnableWebSocketMessageBroker;
import org.springframework.web.socket.config.annotation.StompEndpointRegistry;
import org.springframework.web.socket.config.annotation.WebSocketMessageBrokerConfigurer;
import org.springframework.web.socket.config.annotation.WebSocketTransportRegistration;
import java.util.List;
/**端点配置
* 被动与主动前缀限定
* 关于webSocket配置
*/
@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {
/**
* 添加端点
* 前端据次路径与后台之间开启socket连接
* @param registry
*/
@Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
//webSocket添加跨域和SocketJs支持
registry.addEndpoint("/socket").setAllowedOrigins("*").withSockJS();
}
@Override
public void configureMessageBroker(MessageBrokerRegistry registry) {
//前端被动订阅 地址前缀
registry.enableSimpleBroker("/back");
//前端主动推送 地址前缀
registry.setApplicationDestinationPrefixes("/app");
}
/**
* 配置发送与接收的消息参数,可以指定消息字节大小,缓存大小,发送超时时间
* @param webSocketTransportRegistration
*/
@Override
public void configureWebSocketTransport(WebSocketTransportRegistration webSocketTransportRegistration) {
webSocketTransportRegistration.setMessageSizeLimit(10240)
.setSendBufferSizeLimit(10240)
.setSendTimeLimit(10000);
}
/**
* 设置输入消息通道的线程数,默认线程为1,可以自己自定义线程数,最大线程数,线程存活时间
* @param channelRegistration
*/
@Override
public void configureClientInboundChannel(ChannelRegistration channelRegistration) {
/*
* 配置消息线程池
* 1. corePoolSize 配置核心线程池,当线程数小于此配置时,不管线程中有无空闲的线程,都会产生新线程处理任务
* 2. maxPoolSize 配置线程池最大数,当线程池数等于此配置时,不会产生新线程
* 3. keepAliveSeconds 线程池维护线程所允许的空闲时间,单位秒
*/
channelRegistration.taskExecutor().corePoolSize(1)
.maxPoolSize(5)
.keepAliveSeconds(60);
/*
* 添加stomp自定义拦截器,可以根据业务做一些处理
* springframework 4.3.12 之后版本此方法废弃,代替方法 interceptors(ChannelInterceptor... interceptors)
* 消息拦截器,实现ChannelInterceptor接口
*/
// channelRegistration.setInterceptors(webSocketChannelInterceptor());
}
/**
*设置输出消息通道的线程数,默认线程为1,可以自己自定义线程数,最大线程数,线程存活时间
* @param registration
*/
@Override
public void configureClientOutboundChannel(ChannelRegistration registration) {
registration.taskExecutor().corePoolSize(1)
.maxPoolSize(5)
.keepAliveSeconds(60);
//registration.setInterceptors(new WebSocketChannelInterceptor());
}
/**
* 添加自定义的消息转换器,spring 提供多种默认的消息转换器,
* 返回false,不会添加消息转换器,返回true,会添加默认的消息转换器,当然也可以把自己写的消息转换器添加到转换链中
* @param list
* @return
*/
@Override
public boolean configureMessageConverters(List<MessageConverter> list) {
return true;
}
/**
* 自定义控制器方法的参数类型,有兴趣可以百度google HandlerMethodArgumentResolver这个的用法
* @param list
*/
@Override
public void addArgumentResolvers(List<HandlerMethodArgumentResolver> list) {
}
/**
* 自定义控制器方法返回值类型,有兴趣可以百度google HandlerMethodReturnValueHandler这个的用法
* @param list
*/
@Override
public void addReturnValueHandlers(List<HandlerMethodReturnValueHandler> list) {
}
}
拦截器(可在配置类中配置拦截器后生效,可做增强)
package com.matrix.npdm.config;
import org.apache.commons.lang.StringUtils;
import org.apache.log4j.Logger;
import org.springframework.messaging.Message;
import org.springframework.messaging.MessageChannel;
import org.springframework.messaging.simp.stomp.StompCommand;
import org.springframework.messaging.simp.stomp.StompHeaderAccessor;
import org.springframework.messaging.support.ChannelInterceptor;
import org.springframework.messaging.support.MessageHeaderAccessor;
import javax.servlet.http.HttpSession;
import static org.springframework.messaging.simp.stomp.StompCommand.CONNECT;
/**
* <websocke消息监听,用于监听websocket用户连接情况>
* <功能详细描述>
**/
public class WebSocketChannelInterceptor implements ChannelInterceptor {
// Logger log = Logger.getLogger(WebSocketChannelInterceptor.class);
// 在消息发送之前调用,方法中可以对消息进行修改,如果此方法返回值为空,则不会发生实际的消息发送调用
@Override
public Message<?> preSend(Message<?> message, MessageChannel messageChannel) {
StompHeaderAccessor accessor = MessageHeaderAccessor.getAccessor(message, StompHeaderAccessor.class);
/**
* 1. 判断是否为首次连接请求,如果已经连接过,直接返回message
* 2. 网上有种写法是在这里封装认证用户的信息,本文是在http阶段,websockt 之前就做了认证的封装,所以这里直接取的信息
*/
if(StompCommand.CONNECT.equals(accessor.getCommand()))
{
/*
* 1. 这里获取就是JS stompClient.connect(headers, function (frame){.......}) 中header的信息
* 2. JS中header可以封装多个参数,格式是{key1:value1,key2:value2}
* 3. header参数的key可以一样,取出来就是list
* 4. 样例代码header中只有一个token,所以直接取0位
*/
// 和前端连接时的值保持一致
// String token = accessor.getFirstNativeHeader("token");
String token = accessor.getNativeHeader("token").get(0);
/*
* 1. 这里直接封装到StompHeaderAccessor 中,可以根据自身业务进行改变
* 2. 封装大搜StompHeaderAccessor中后,可以在@Controller / @MessageMapping注解的方法中直接带上StompHeaderAccessor
* 就可以通过方法提供的 getUser()方法获取到这里封装user对象
* 2. 例如可以在这里拿到前端的信息进行登录鉴权
*/
// WebSocketUserAuthentication user = (WebSocketUserAuthentication) accessor.getUser();
if (StringUtils.isEmpty(token)) {
return null;
}
// 绑定user
// Principal principal = new UserPrincipal(username);
// accessor.setUser(principal);
// System.out.println("认证用户:" + user.toString() + " 页面传递令牌" + token);
System.out.println("用户:"+accessor.getUser()+"开始连接");
}else if (StompCommand.DISCONNECT.equals(accessor.getCommand()))
{
System.out.println("用户:" + accessor.getUser() + " 断开连接");
}
return message;
}
// 在消息发送后立刻调用,boolean值参数表示该调用的返回值
@Override
public void postSend(Message<?> message, MessageChannel messageChannel, boolean b) {
//重复发送一次
StompHeaderAccessor accessor = StompHeaderAccessor.wrap(message);
/*
* 拿到消息头对象后,我们可以做一系列业务操作
* 1. 通过getSessionAttributes()方法获取到websocketSession,
* 就可以取到我们在WebSocketHandshakeInterceptor拦截器中存在session中的信息
* 2. 我们也可以获取到当前连接的状态,做一些统计,例如统计在线人数,或者缓存在线人数对应的令牌,方便后续业务调用
*/
HttpSession httpSession = (HttpSession) accessor.getSessionAttributes().get("HTTP_SESSION");
// 这里只是单纯的打印,可以根据项目的实际情况做业务处理
log.info("postSend 中获取httpSession key:" + httpSession.getId());
// 忽略心跳消息等非STOMP消息
if(accessor.getCommand() == null)
{
return;
}
// 根据连接状态做处理,这里也只是打印了下,可以根据实际场景,对上线,下线,首次成功连接做处理
System.out.println(accessor.getCommand());
switch (accessor.getCommand())
{
// 首次连接
case CONNECT:
log.info("httpSession key:" + httpSession.getId() + " 首次连接");
break;
// 连接中
case CONNECTED:
break;
// 下线
case DISCONNECT:
log.info("httpSession key:" + httpSession.getId() + " 下线");
break;
default:
break;
}
}
/*
* 1. 在消息发送完成后调用,而不管消息发送是否产生异常,在次方法中,我们可以做一些资源释放清理的工作
* 2. 此方法的触发必须是preSend方法执行成功,且返回值不为null,发生了实际的消息推送,才会触发
*/
@Override
public void afterSendCompletion(Message<?> message, MessageChannel messageChannel, boolean b, Exception e) {
}
/* 1. 在消息被实际检索之前调用,如果返回false,则不会对检索任何消息,只适用于(PollableChannels),
* 2. 在websocket的场景中用不到
*/
@Override
public boolean preReceive(MessageChannel messageChannel) {
return true;
}
/*
* 1. 在检索到消息之后,返回调用方之前调用,可以进行信息修改,如果返回null,就不会进行下一步操作
* 2. 适用于PollableChannels,轮询场景
*/
@Override
public Message<?> postReceive(Message<?> message, MessageChannel messageChannel) {
return message;
}
/*
* 1. 在消息接收完成之后调用,不管发生什么异常,可以用于消息发送后的资源清理
* 2. 只有当preReceive 执行成功,并返回true才会调用此方法
* 2. 适用于PollableChannels,轮询场景
*/
@Override
public void afterReceiveCompletion(Message<?> message, MessageChannel messageChannel, Exception e) {
}
}
控制层写法(不建议)
@Resource
private SimpMessagingTemplate simpMessagingTemplate;
/**
* 转发imei到socket
* @param imei
*/
@PostMapping("/redirectImeiToSocket/{accountId}")
public void greeting(@RequestBody String imei, @PathVariable("accountId") String accountId) {
//转发到socket中,通过路径完成点对点发送
simpMessagingTemplate.convertAndSend("/back/backImei/"+accountId, com.alibaba.fastjson.JSONObject.toJSON(imei));
}
控制层写法
package com.example.websocketdemo1.stomp;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.messaging.Message;
import org.springframework.messaging.MessageHeaders;
import org.springframework.messaging.handler.annotation.*;
import org.springframework.messaging.simp.SimpMessagingTemplate;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import java.security.Principal;
import java.util.Map;
/**
* @author linyun
* @date 2018/9/13 下午5:42
*/
@Slf4j
@Controller
public class GreetingController {
@Autowired
private SimpMessagingTemplate simpMessagingTemplate;
* 主动返回消息。
* @param message
*/
@MessageMapping("/hello")
public void hello(@Payload com.example.websocketdemo1.stomp.Message message) {
System.out.println(message);
com.example.websocketdemo1.stomp.Message returnMessage = new com.example.websocketdemo1.stomp.Message();
returnMessage.setContent("转发," + message.getContent());
simpMessagingTemplate.convertAndSend("/message/public", returnMessage);
}
/**
* 使用注解的方式返回消息
* @param message
* @return
*/
@MessageMapping("/hello1")
@SendTo("/message/public")
public com.example.websocketdemo1.stomp.Message hello1(@Payload com.example.websocketdemo1.stomp.Message message) {
System.out.println(message);
com.example.websocketdemo1.stomp.Message returnMessage = new com.example.websocketdemo1.stomp.Message();
returnMessage.setContent("转发2," + message.getContent());
return returnMessage;
}
/*** 点对点 ***/
/**
* 点对点发送消息。接收消息的人是从消息中获取的。
* @param message
* @param principal
*/
@MessageMapping("/hello2")
public void hello2(@Payload com.example.websocketdemo1.stomp.Message message, Principal principal) {
System.out.println(message);
System.out.println(principal);
com.example.websocketdemo1.stomp.Message returnMessage = new com.example.websocketdemo1.stomp.Message();
returnMessage.setContent("转发3," + message.getContent());
simpMessagingTemplate.convertAndSendToUser(message.getTo(), "/notice/msg", returnMessage);
}
}
前端界面
<!DOCTYPE html>
<html lang="en">
<head>
<title>测试websocket</title>
<meta charset="UTF-8">
<meta name="viewport"
content="width=device-width, initial-scale=1, minimum-scale=1, maximum-scale=1, user-scalable=no">
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1"/>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/4.1.2/css/bootstrap.min.css">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/jquery-toast-plugin/1.3.2/jquery.toast.min.css">
</head>
<body>
<div class="container">
<button type="button" class="btn btn-primary" onclick="connect()">链接</button>
<button type="button" class="btn btn-primary" onclick="disconnect()">断开</button>
</div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/1.12.4/jquery.min.js" type="text/javascript"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/4.1.2/js/bootstrap.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery-toast-plugin/1.3.2/jquery.toast.min.js"></script>
<script src="http://cdn.sockjs.org/sockjs-0.3.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/stomp.js/2.3.3/stomp.min.js"></script>
<script language=javascript>
var username = 'tom';
var sendMessage = null;
var disConnect = null;
function connect() {
var socket = new SockJS("http://127.0.0.1:8080/chat");
var client = Stomp.over(socket);
client.connect({
username: username
}, function (succ) {
console.log('client connect success:', succ);
client.subscribe("/message/public", function (res) {
console.log('收到消息---/message/public:',res);
});
client.subscribe("/user/notice/msg", function (res) {
console.log('个人消息:',res)
});
}, function (error) {
console.log('client connect error:', error);
});
sendMessage = function (destination, headers, body) {
client.send(destination, headers, body)
};
disConnect = function () {
client.disconnect();
console.log('client connect break')
}
}
function disconnect() {
disConnect();
}
//发送聊天信息
function send(roomId, ct) {
var messageModel = {};
messageModel.type = 1;
messageModel.content = ct;
messageModel.from = username;
sendMessage("/app/hello/" + roomId, {}, JSON.stringify(messageModel));
}
/**
* 测试发送一个消息,如果订阅了/sub/public的用户都会收到消息。
*/
function send1() {
var messageModel = {};
messageModel.type = 1;
messageModel.content = '你好,' + new Date().getTime();
messageModel.from = username;
sendMessage("/app/hello", {}, JSON.stringify(messageModel));
}
function send2() {
var messageModel = {};
messageModel.type = 1;
messageModel.content = 'hello1,' + new Date().getTime();
messageModel.from = username;
sendMessage("/app/hello1", {}, JSON.stringify(messageModel));
}
/** 发送消息给个人,接收者 to **/
function send3() {
var messageModel = {};
messageModel.to = 'jerry';
messageModel.type = 1;
messageModel.content = 'hello1,' + new Date().getTime();
messageModel.from = username;
sendMessage("/app/hello2", {}, JSON.stringify(messageModel));
}
}
</script>
</body>
</html>
借鉴地址:springboot2.0+websocket集成【群发消息+单对单】(二)_websocket stomp 单对单_林子曰的博客-CSDN博客