Spring boot项目WebSocket+SockJS+Stomp进行客服联系消息实时通信

pom.xml先引入spingboot的websocket包:

<!--WebSocket-->
<dependency>
	<groupId>org.springframework.boot</groupId>
	<artifactId>spring-boot-starter-websocket</artifactId>
</dependency>

页面引用两个js文件,分别是sockjs.js和stomp.js:

<script src="https://cdnjs.cloudflare.com/ajax/libs/sockjs-client/1.1.4/sockjs.js"></script>
<script type="application/javascript" src="http://cdn.bootcss.com/stomp.js/2.3.3/stomp.min.js"></script>

编写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<String, Object> 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实现通信,可能还存在很多问题,可以看看其他文章

  • 0
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值