WebSocket是在单个TCP连接上进行全双工通讯的协议,只要浏览器和服务器进行一次握手,就可以建立一条通道,两者之间实现数据互传。
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-websocket</artifactId>
</dependency>
websocket的两种实现方式
一,使用Java提供的@ServerEndpoint注解实现
webSocketConfig配置类
@Configuration
public class WebSocketConfig {
@Bean
public ServerEndpointExporter getServerEndpointExporter(){
return new ServerEndpointExporter();
}
}
webSocketServer服务端类
@Component
@Slf4j
@ServerEndpoint("/webSocket/{id}")
public class WebSocketServer {
//与某个客户端的连接会话,需要通过它来给客户端发送数据
private Session session;
//当前用户id
private String id="";
private static ConcurrentHashMap<String,Session> webSocketMap = new ConcurrentHashMap<>();
/**
* 连接建立
* @param id
* @param session
*/
@OnOpen
public void open(@PathParam("oid") String id, Session session){
this.session = session;
this.id=id;
webSocketMap.put(id,session);
log.info("========>建立连接"+id);
}
/**
* 连接关闭
*/
@OnClose
public void close(){
webSocketMap.remove(id);
log.info("=========》关闭连接"+id);
}
/**
*客户端发送消息
* @param message
* @param session
*/
@OnMessage
public void onMessage(String message, Session session) {
/*
message为json格式字符串
例: {"text":"这是个例子", "toId":"001"}
*/
if(StringUtils.isNotBlank(message)){
try {
//转化为object,解析发送的报文
JSONObject jsonObject = JSON.parseObject(message);
//得到接受者id
String toId=jsonObject.getString("toId");
//在发送的报文中加入发送人id
//{"text":"这是个例子", "toId":"001", "fromId":"002"}
jsonObject.put("fromId",this.id);
//发送给toId对应的用户
if(StringUtils.isNotBlank(toId)&&webSocketMap.containsKey(toId)){
webSocketMap.get(toId).getBasicRemote().sendText(jsonObject.toJSONString());
}else{
log.info("信息接收者"+toId+"不在服务器上");
//该id用户不在服务器上,可选择存储在redis中,如果不做此操作就只能两人同时在线才可以互相接收到信息
}
}catch (Exception e){
e.printStackTrace();
}
}
}
/**
* 服务器主动推送信息
*/
public static void sendMsg(String toId,String msg){
Session session = WebSocketServer.webSocketMap.get(toId);
try {
session.getBasicRemote().sendText(msg);
} catch (IOException e) {
e.printStackTrace();
}
}
}
客户端JS
var websocket = null;
//判断当前浏览器是否支持WebSocket
if ('WebSocket' in window) {
websocket = new WebSocket("ws://ip:端口/webSocket/"+id);
} else {
alert('此浏览器不支持WebSocket')
}
//连接成功建立的回调方法
websocket.onopen = function() {
console.log("连接成功");
}
//接收到消息的回调方法
websocket.onmessage = function(event) {
console.log(event.data);
}
//连接关闭的回调方法
websocket.onclose = function() {
console.log("连接失败");
}
//监听窗口关闭事件,当窗口关闭时关闭websocket连接
window.onbeforeunload = function() {
websocket.close();
}
//发送信息
function sendMessage() {
websocket.send("{"text":"这是个例子", "toId":"001"}");
}
二,基于STOMP协议实现
STOMP是一种消息队列模式,发送消息的是生产者,接收消息的是消费者。而消费者可以通过订阅不同的destination,来获得不同的推送消息,不需要开发人员去管理这些订阅与推送目的地之前的关系
WebSocketConfig配置类
如果项目中使用JWT令牌才需重写configureClientInboundChannel方法做相应处理,如果没有使用忽略此方法
@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer{
@Autowired
private JwtTokenUtil jwtTokenUtil;
@Autowired
private UserDetailsService userDetailsService;
/**
* 添加这个端点,网页通过这个端点连接上服务
* 也就是websocket服务地址,并且可指定是否使用socketJs
* @param registry
*/
@Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
/*
将/ws/ep注册为stomp的端点,用户连接这个端点就可以进行websocket通信
允许跨域
使用sockjs访问
*/
registry.addEndpoint("/ws/ep").setAllowedOriginPatterns("*").withSockJS();
}
/**
* 如果使用J项目中使用WT令牌则需重写这个方法做配置,如果是普通项目则不用重写这个方法
* 输入通道参数配置
* @param registration
*/
@Override
public void configureClientInboundChannel(ChannelRegistration registration) {
registration.interceptors(new ChannelInterceptor() {
@Override
public Message<?> preSend(Message<?> message, MessageChannel channel) {
StompHeaderAccessor accessor = MessageHeaderAccessor.getAccessor(message, StompHeaderAccessor.class);
//判断是否为连接,如果是则获取token,并设置用户对象
if (StompCommand.CONNECT.equals(accessor.getCommand())){
String token = accessor.getFirstNativeHeader("Auth-Token");
String username = jwtTokenUtil.getUserNameFromToken(token);
if (!StringUtils.isEmpty(username)){
//登录
UserDetails userDetails = userDetailsService.loadUserByUsername(username);
//判断token是否有效,重新设置用户对象
if (jwtTokenUtil.validateToken(token,userDetails)){
UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
SecurityContextHolder.getContext().setAuthentication(authenticationToken);
accessor.setUser(authenticationToken);
}
}
}
return message;
}
});
}
/*
配置消息代理
*/
@Override
public void configureMessageBroker(MessageBrokerRegistry registry) {
//配置代理域,可以配置多个,配置代理目的地为/queue,可以在配置域上向客户端推送消息
registry.enableSimpleBroker("/queue");
}
}
提供请求接口
@Controller
public class WebSocketController {
@Autowired
private SimpMessagingTemplate simpMessagingTemplate;
@MessageMapping("/ws/chat")
private void handleMsg(ChatMsg chatMsg){
//ChatMsg 为自定义的信息内容的对象
simpMessagingTemplate.convertAndSendToUser(chatMsg.getTo(),"/queue/chat",chatMsg);
}
}
客户端JS
function connect() {
var socket = new SockJS('/ws/ep');// 连接端点
stompClient = Stomp.over(socket);
stompClient.connect({}, function() {
console.log('开始连接');
stompClient.subscribe('/queue/chat', function(respnose){ // 订阅消息频道
console.log(JSON.parse(respnose.body))
});
});
}
function disconnect() {
if (stompClient != null) {
stompClient.disconnect();
}
console.log("断开连接");
}
function sendMsg() {
//chatMsg为自定义发送消息的对象,与后端的对应
let chatMsg={};
stompClient.send('/ws/chat', {}, JSON.stringify(chatMsg));//5
}