WebSocket基于STOMP

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博客

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值