pom.xml先引入spingboot的websocket包:
org.springframework.boot
spring-boot-starter-websocket
页面引用两个js文件,分别是sockjs.js和stomp.js:
编写WebSocket的配置文件,WebSocketConfig类:
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
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;
/**
* Created by GvG on 2018/10/13.
*/
@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {
/**
* registerStompEndpoints() 方法:添加一个服务端点,来接收客户端的连接。将 "/endpointChat" 路径注册为 STOMP 端点。
*/
@Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
//添加一个/endpointChat端点,客户端就可以通过这个端点来进行连接;withSockJS作用是添加SockJS支持
registry.addEndpoint("/endpointChat")
.setAllowedOrigins("*")
.withSockJS();
}
/**
* configureMessageBroker() 方法:
* 配置了一个 简单的消息代理,通俗一点讲就是设置消息连接请求的各种规范信息。
*/
@Override
public void configureMessageBroker(MessageBrokerRegistry registry) {
//定义了一个(或多个)客户端订阅地址的前缀信息,也就是客户端接收服务端发送消息的前缀信息
registry.enableSimpleBroker("/queue", "/topic");
//定义了服务端接收地址的前缀,也即客户端给服务端发消息的地址前缀
registry.setApplicationDestinationPrefixes("/app");
// 点对点使用的订阅前缀(客户端订阅路径上会体现出来),不设置的话,默认也是/user/
registry.setUserDestinationPrefix("/user/");
}
/**
* 配置客户端入站通道拦截器
*/
@Override
public void configureClientInboundChannel(ChannelRegistration registration) {
registration.setInterceptors(createUserInterceptor());
}
/**
*
* @Title: createUserInterceptor
* @Description: 将客户端渠道拦截器加入spring ioc容器
* @return
*/
@Bean
public UserInterceptor createUserInterceptor() {
return new UserInterceptor();
}
}
1.registerStompEndpoints():用来配置客户端连接端口时的路径
例如配置的是/endpointChat:
var socket = new SockJS("/endpointChat");
stompClient = Stomp.over(socket);
…
-
2.configureMessageBroker(): 用来配置订阅以及发送信息时的路径规范
registry.enableSimpleBroker("/queue", “/topic”); 在客户端订阅地址、服务端发送–>客户端时的地址前缀
registry.setApplicationDestinationPrefixes("/app"); 在客户端发送–>服务端时的地址前缀
registry.setUserDestinationPrefix("/user/"); 可以不设置,客户端订阅点对点地址时的地址前缀,默认不设置时是/user/
-
3.本来只需重写前两个方法即可,因为自己想要把用户的ID作为用户标识,在服务端发回信息时能精确点对点通信,所以配置了客户端连接服务端时的拦截器UserInterceptor.java:
/**
* 客户端渠道拦截适配器
* Created by GvG on 2018/10/13.
*/
public class UserInterceptor extends ChannelInterceptorAdapter {
/**
* 获取包含在stomp中的用户信息
*/
@SuppressWarnings("rawtypes")
@Override
public Message> preSend(Message> message, MessageChannel channel) {
StompHeaderAccessor accessor = MessageHeaderAccessor.getAccessor(message, StompHeaderAccessor.class);
if (StompCommand.CONNECT.equals(accessor.getCommand())) {
Object raw = message.getHeaders().get(SimpMessageHeaderAccessor.NATIVE_HEADERS);
if (raw instanceof Map) {
Object name = ((Map) raw).get("name");
if (name instanceof LinkedList) {
// 设置当前访问器的认证用户
accessor.setUser(new ChatUser(((LinkedList) name).get(0).toString()));
}
}
}
return message;
}
}
Object name = ((Map) raw).get(“name”);
获取key为name的值说明需要在客户端连接时需要附加name所对应的值,我们待会连接时加上name:userID来进行连接;
注册认证用户时需要实现Principal接口,Principal相当于一个认证的用户信息
import java.security.Principal;
/**
* Created by GvG on 2018/10/13.
*/
public final class ChatUser implements Principal {
private final String name;
public ChatUser(String name) {
this.name = name;
}
@Override
public String getName() {
return name;
}
}
accessor.setUser(new ChatUser(((LinkedList) name).get(0).toString()));
实现该接口属性name可以存储userID,在服务端发送给客户端信息是可以以userID来标识某个确认的客户端
–
在弄好配置文件后,先自定义规范一下通信消息的格式:
public class Message {
private String message; //消息内容
private String datetime; //发送时间
private Integer type; //1为用户消息,2为管理员信息
private String from; //消息来源ID
private String to; //发送消息给ID
public Message(String message, String datetime, Integer type,String from, String to) {
this.message = message;
this.datetime = datetime;
this.type = type;
this.from = from;
this.to = to;
}
public Message(){
super();
}
//..Get() and Set()..
...
}
订阅地址与接收消息WebSocketController接口:
(关键点template.convertAndSendToUser(要发送给用户的userID,用户订阅的地址,信息Message))
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.messaging.handler.annotation.MessageMapping;
import org.springframework.messaging.handler.annotation.SendTo;
import org.springframework.messaging.simp.SimpMessagingTemplate;
import org.springframework.messaging.simp.annotation.SendToUser;
import org.springframework.stereotype.Controller;
import org.xgun.kissolive.pojo.ChatMessage;
import org.xgun.kissolive.service.ISilentService;
import org.xgun.kissolive.utils.DateTimeUtil;
import org.xgun.kissolive.vo.Message;
import java.security.Principal;
/**
* Created by GvG on 2018/10/13.
*/
@Controller
public class WebSocketController {
@Autowired
public SimpMessagingTemplate template;
@Autowired
private ISilentService iSilentService;
//管理员发送消息的地址,用户订阅的地址,消息发送到/message会执行此方法
@MessageMapping("/message")
public void userMessage(Message adminMessage, Principal principal) throws Exception {
System.out.println("管理员ID:"+ principal.getName()+" 发送了一条信息给用户ID:"+adminMessage.getTo());
Integer userId = Integer.parseInt(adminMessage.getTo());
//存储进数据库未读状态
ChatMessage chatMessage = new ChatMessage(null,adminMessage.getMessage(),0,userId,
2, DateTimeUtil.strToDate(adminMessage.getDatetime()));
iSilentService.sendingNewMessage(chatMessage);
/**
*不用注解@SendToUser方式,而是
*template.convertAndSendToUser(要发送给用户的ID,用户订阅的地址,信息Message)
*/
template.convertAndSendToUser(adminMessage.getTo(), "/topic/message",
new Message(adminMessage.getMessage(),adminMessage.getDatetime(), 2,
adminMessage.getTo(),adminMessage.getFrom()));
}
//用户发送消息的地址,管理员订阅的地址,消息发送到/toAdmin会执行此方法
@MessageMapping("/toAdmin")
public void getUserMessage(Message userMessage, Principal principal) throws Exception {
System.out.println("用户ID:"+ principal.getName()+" 发送了一条信息");
Integer userId = Integer.parseInt(userMessage.getFrom());
//存储进数据库未读状态
ChatMessage chatMessage = new ChatMessage(null,userMessage.getMessage(),0,
userId,1, DateTimeUtil.strToDate(userMessage.getDatetime()));
iSilentService.sendingNewMessage(chatMessage);
/**
*这里可以使用注解方式@SendTo("/topic/toAdmin")方式
*不过使用注解需要返回消息return new Message(..);
*template.convertAndSend(管理员订阅地址,消息)
*/
template.convertAndSend("/topic/toAdmin",new Message(userMessage.getMessage(),userMessage.getDatetime(), 1,
userMessage.getTo(),userMessage.getFrom()));
}
}
详细订阅与发送消息方式见下面;
用户客户端连接过程(先要加上面两个js):
订阅地址stompClient.subscribe(地址,成功后方法,失败后方法)
var stompClient;
function connect() {
var socket = new SockJS("/endpointChat"); //之前配置时的路径
stompClient = Stomp.over(socket);
stompClient.connect(
{
name:ID //在此加上name:userId ,在之前配置的客户端拦截器中会获取到该值,作为该客户端的标识
},
//连接成功后调用函数
function connectCallback(frame){
console.log("link success!"),
//获取历史信息
...
//将数据显示到聊天框
...
//链接成功后订阅通信,订阅通信地址:/user/topic/message /user代表订阅点对点方式
stompClient.subscribe("/user/topic/message",function(data) {
console.log("收到信息"+data.body);
//收到信息判断:
var message = $.parseJSON(data.body);
if( message.type === 2) //为管理员发来信息
{
showMessage(message); //聊天框显示消息
//设置已读
..
}
})
},
//连接失败后调用函数
function errorCallBack(response){console.log("link error!");}
);
}
管理员客户端连接,和用户一样,只是订阅地址不同:
订阅/topic/toAdmin地址,只要用户发消息到这个地址,有多个管理员账号存在都可以收到消息
stompClient.subscribe("/topic/toAdmin",function(data) {
console.log("管理员收到用户发来信息"+data.body);
var message = $.parseJSON(data.body);
//判断当前聊天框是否为该用户
if( message.to !== id ){
//更新消息列表
setlist.getList();
}else if( message.to === id && document.getElementById("U"+message.to) ){
//显示消息
showMessage(message);
..
//设置已读
..
}
})
用户发送消息方式:stompClient.send(地址,{},消息(规范的格式))
/app前缀是客户端发送消息到->服务端地址前缀 /toAdmin是接口
//获取输入框消息
var inputText = $('.emotion').val();
//发送给管理员测试
stompClient.send("/app/toAdmin",{},JSON.stringify({
message : inputText,
datetime :currentTime(), //获取当前时间的字符串
type :1, //1标志为用户信息
from :"1",//userId
to : "admin"
}));
管理员发送消息方式:
/app前缀+/message接口地址
//管理员发送信息测试
stompClient.send("/app/message",{},JSON.stringify({
message : inputText,
datetime :currentTime(),
type :2, //2标志为管理员消息
from :"0", //管理员ID
to : 所要发送到的用户userId
}));
–
–
以上已经可以实现用户客户端与多管理员管理端的通信,管理员端单一对用户通信
可能存在安全问题,首先可以在WebSocketConfig配置类里加个握手拦截器,将session里信息判断下
addInterceptors(new SessionAuthHandshakeInterceptor())
@Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
//添加一个/endpointChat端点,客户端就可以通过这个端点来进行连接;withSockJS作用是添加SockJS支持
registry.addEndpoint("/endpointChat")
.setAllowedOrigins("*")
.addInterceptors(new SessionAuthHandshakeInterceptor())
.withSockJS();
}
SessionAuthHandshakeInterceptor实现HandshakeInterceptor握手时的拦截器:
mport org.springframework.http.server.ServerHttpRequest;
import org.springframework.http.server.ServerHttpResponse;
import org.springframework.web.socket.WebSocketHandler;
import org.springframework.web.socket.server.HandshakeInterceptor;
import java.util.Map;
/**
* Created by GvG on 2018/10/13.
*/
public class SessionAuthHandshakeInterceptor implements HandshakeInterceptor {
@Override
public boolean beforeHandshake(ServerHttpRequest request, ServerHttpResponse response, WebSocketHandler wsHandler,
Map attributes) throws Exception {
//握手之前
//获取session里用户信息userId
if (userId == null) {
//用户未登录
return false;
}
attributes.put("WEBSOCKET_USERID", userId);
return true;
}
@Override
public void afterHandshake(ServerHttpRequest request, ServerHttpResponse response, WebSocketHandler wsHandler,
Exception exception) {
//握手之后
}
}
这篇文章只是新手刚刚接触websokect实现通信,可能还存在很多问题,可以看看其他文章