springboot2.0+websocket集成【群发消息+单对单】(二)

第二篇,主要是使用socketjs,stomp模式的websocket简单实现。
第一篇的地址:springboot2.0+websocket集成【群发消息+单对单】
参考:
http://tech.lede.com/2017/03/08/qa/websocket+spring/
https://blog.csdn.net/mr_zhuqiang/article/details/46618197

继续上次的项目。如果对下面的代码有部分看不明白的,请到上一篇看看流程,或者到文末贴出项目的git地址。

1. 先从配置开始,WebStompConfig

代码中的注释基本能够解释清楚每行的意思了,这里就不再细说
完整代码

package com.example.websocketdemo1.stomp;

import org.springframework.beans.factory.annotation.Autowired;
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;

/**
 * EnableWebSocketMessageBroker 注解表明: 这个配置类不仅配置了 WebSocket,还配置了基于代理的 STOMP 消息;
 * registerStompEndpoints() 方法:添加一个服务端点,来接收客户端的连接。将 “/chat” 路径注册为 STOMP 端点。这个路径与之前发送和接收消息的目的路径有所不同, 这是一个端点,客户端在订阅或发布消息到目的地址前,要连接该端点,即用户发送请求 :url=’/127.0.0.1:8080/chat’ 与 STOMP server 进行连接,之后再转发到订阅url;
 * configureMessageBroker() 方法:配置了一个 简单的消息代理,通俗一点讲就是设置消息连接请求的各种规范信息。
 *
 * @author linyun
 * @date 2018/9/13 下午5:15
 */
@Configuration
@EnableWebSocketMessageBroker
public class WebStompConfig implements WebSocketMessageBrokerConfigurer {

    @Autowired
    private WebSocketHandleInterceptor interceptor;

    @Override
    public void registerStompEndpoints(StompEndpointRegistry registry) {
        //添加一个/chat端点,客户端就可以通过这个端点来进行连接;withSockJS作用是添加SockJS支持
        registry.addEndpoint("/chat").setAllowedOrigins("*").withSockJS();
    }

    @Override
    public void configureMessageBroker(MessageBrokerRegistry registry) {
        //定义了两个客户端订阅地址的前缀信息,也就是客户端接收服务端发送消息的前缀信息
        registry.enableSimpleBroker("/message", "/notice");
        //定义了服务端接收地址的前缀,也即客户端给服务端发消息的地址前缀
        registry.setApplicationDestinationPrefixes("/app");
    }

    @Override
    public void configureClientInboundChannel(ChannelRegistration registration) {
        //注册了一个接受客户端消息通道拦截器
        registration.interceptors(interceptor);
    }
}

2. 用户信息注册,WebSocketHandleInterceptor

上一篇里面,用户信息我们是直接存储到session中,然后再通过握手的时候,将用户信息存入WebSocketSession。
这次使用stomp的模式也存在一个单对单的发送消息,就需要知道对方是谁,所以也要注册一下用户信息。

完整的代码

package com.example.websocketdemo1.stomp;

import com.sun.security.auth.UserPrincipal;
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 org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;

import java.security.Principal;

/**
 * @author linyun
 * @date 2018/9/13 下午5:57
 */
@Component
public class WebSocketHandleInterceptor implements ChannelInterceptor {

    /**
     * 绑定user到websocket conn上
     * @param message
     * @param channel
     * @return
     */
    @Override
    public Message<?> preSend(Message<?> message, MessageChannel channel) {
        StompHeaderAccessor accessor = MessageHeaderAccessor.getAccessor(message, StompHeaderAccessor.class);
        if (StompCommand.CONNECT.equals(accessor.getCommand())) {
            String username = accessor.getFirstNativeHeader("username");
            if (StringUtils.isEmpty(username)) {
                return null;
            }
            // 绑定user
            Principal principal = new UserPrincipal(username);
            accessor.setUser(principal);
        }
        return message;
    }
}
注意这里的username信息
String username = accessor.getFirstNativeHeader("username");

username是在页面中传递来的,具体的传递方式在后面的页面中,具体的参数名称可以随意自定义。
另外一种获取用户信息的方式:

Object raw = message.getHeaders().get(SimpMessageHeaderAccessor.NATIVE_HEADERS);
if (raw instanceof Map) {
    System.out.println(raw);
    // 打印raw之后,可以查看头部的参数,包含了username。
}

3.处理消息的类,GreetingController

用来接收和发送消息。
先来一个消息的model,用来包装消息,使用lombok插件,省去了getset了。
代码:

package com.example.websocketdemo1.stomp;

import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;

/**
 * @author linyun
 * @date 2018/9/13 下午5:44
 */
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class Message {
    private String to;
    private Long date;
    private String from;
    private String content;
}

controller的完整代码

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;


    /**
     * 测试页面
     * @return
     */
    @RequestMapping("/chat4")
    public String chat4() {
        return "chat4";
    }

    /**
     * 测试页面2
     * @return
     */
    @RequestMapping("/chat5")
    public String chat5() {
        return "chat5";
    }

    /**
     * 测试订阅
     * @param message
     * @param messageHeaders
     * @param destination
     * @param headers
     * @param id
     * @param body
     */
    @MessageMapping("/hello/{id}")
    public void hello(Message message,
                      MessageHeaders messageHeaders,
                      @Header("destination") String destination,
                      @Headers Map<String, Object> headers,
                      @DestinationVariable long id,
                      @Payload String body) {
        log.info("message:{}", message);
        log.info("messageHeaders:{}", messageHeaders);
        log.info("destination:{}", destination);
        log.info("headers:{}", headers);
        log.info("id:{}", id);
        log.info("body:{}", body);
    }


    /***  群消息   ***/

    /**
     * 主动返回消息。
     * @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);
    }

}

稍微解释一下代码中的几个方法
第一个方法,/hello/{id},主要是用来测试在一次发送消息的请求中能够获取到那些参数,合理的利用这些参数于自己的业务中。

    @MessageMapping("/hello/{id}")
    public void hello(Message message,
                      MessageHeaders messageHeaders,
                      @Header("destination") String destination,
                      @Headers Map<String, Object> headers,
                      @DestinationVariable long id,
                      @Payload String body) {
        log.info("message:{}", message);
        log.info("messageHeaders:{}", messageHeaders);
        log.info("destination:{}", destination);
        log.info("headers:{}", headers);
        log.info("id:{}", id);
        log.info("body:{}", body);
    }

4.结合页面测试

新建2个页面,页面中设置用户的信息

http://127.0.0.1:8080/chat4
username='tom';
http://127.0.0.1:8080/chat5
username='jerry';

主要是引入 stomp.js和socketjs这2个js。
页面完整代码:


<html lang="en">
<head>
    <title>测试websockettitle>
    <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>

点击链接按钮,主要是做了几个操作,

  • 1. 链接到websocket
  • 2. 订阅/message/public
  • 3. 订阅/user/notice/msg

5. 跑起来测试

进入页面后,打开控制台,直接输入命令。如果觉得不方便,可以在页面加几个按钮,美观点=)

  • send(‘123456’,‘hello’);

此方法会发起一个消息,推送到/app/hello/123456,这个地址,并带上参数messageModel。
监控到后台 @MessageMapping("/hello/{id}") 接收到的消息。

  • send1();

发送一条消息给/app/hello,后台接收到之后通过 simpMessagingTemplate.convertAndSend("/message/public", returnMessage); 广播一条消息给所有订阅了/message/public的用户。
所以为了测试,最好多开几个浏览器。观察一下console的打印信息。

  • send3();

发送一条消息给 @MessageMapping("/hello2") ,注意这里的消息messageModel中加入了to=jerry。后台接收到参数之后,使用 simpMessagingTemplate.convertAndSendToUser(message.getTo(), “/notice/msg”, returnMessage); 将消息发送给jerry,从而实现了单对单的消息推送。

6.总结一下

  1. stomp底层实现都是广播,单对单只是表面看起来特殊一点,本质其实也是生成的一个唯一的广播地址。测试也简单,打开2个页面登录tom,一个登录jerry。用jerry发送消息给tom。2个tom都会收到消息。
  2. 页面中注册订阅地址,后台通过注册的地址发送广播。所以/message和/notice后面的参数其实可以随便写。

完整项目 git:https://gitlab.com/tulongx/websocketdemo1

以上。
springboot2.0+websocket集成【群发消息+单对单】(二)_第1张图片

你可能感兴趣的:(springboot,websocket)