Spring Boot 集成 WebSocket [stomp 协议] 点对点&广播

示例代码

WebSocket

WebSocket是在HTML5基础上单个TCP连接上进行全双工通讯的协议,只要浏览器和服务器进行一次握手,就可以建立一条快速通道,两者就可以实现数据互传了。说白了,就是打破了传统的http协议的无状态传输(只能浏览器请求,服务端响应),websocket全双工通讯,就是浏览器和服务器进行一次握手,浏览器可以随时给服务器发送信息,服务器也可以随时主动发送信息给浏览器了。对webSocket原理有兴趣的客官,可以自行百度。

STOMP 协议

stomp协议 官网

STOMP Protocol Specification, Version 1.1

STOMP Protocol Specification, Version 1.2

STOMP协议工作于TCP协议之上,使用了下列命令:

  • SEND 发送
  • SUBSCRIBE 订阅
  • UNSUBSCRIBE 退订
  • BEGIN 开始
  • COMMIT 提交
  • ABORT 取消
  • ACK 确认
  • DISCONNECT 断开

环境搭建

Spring 5.1.8 Release

因为是根据项目的需求来的,所以这里我只介绍在Spring Boot下使用WebSocket的其中一种实现【STOMP协议】。因此整个工程涉及WebSocket使用的大致框架为SpringBoot+Maven+WebSocket,其他框架的基础搭建,我这里就不说了,相信各位也都很熟悉,我就直接集成WebSocket了。

在pox.xml加上对springBoot对WebSocket的支持:

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

这样SpringBoot就和WebSocket集成好了,我们就可以直接使用SpringBoot提供对WebSocket操作的API了

编码实现

①在Spring上下文中添加对WebSocket的配置

import org.springframework.context.annotation.Configuration;
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;

// springBoot2.0版本后使用 实现WebSocketMessageBrokerConfigurer接口;
// 2.0以下版本继承AbstractWebSocketMessageBrokerConfigurer 类;
@Configuration
// 注解开启使用STOMP协议来传输基于代理(message broker)的消息
// 这时控制器支持使用@MessageMapping,就像使用@RequestMapping一样
@EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {
    //注册STOMP协议的节点(endpoint),并映射指定的url
    @Override
    public void registerStompEndpoints(StompEndpointRegistry registry) {
        //注册一个Stomp 协议的endpoint指定URL为socket,并用.withSockJS()指定 SockJS协议。.setAllowedOrigins("*")设置跨域
        registry.addEndpoint("/socket").setAllowedOrigins("*").withSockJS();
    }

    //配置消息代理(message broker)
    @Override
    public void configureMessageBroker(MessageBrokerRegistry config) {

        //客户端订阅地址的前缀   将消息传回给以‘/topic’开头的客户端
        config.enableSimpleBroker("/topic", "/queue", "/user");
        //服务端接收地址的前缀
        config.setApplicationDestinationPrefixes("/app");
        config.setUserDestinationPrefix("/g");
    }
}

介绍以上几个相关的注解和方法:

  1. @EnableWebSocketMessageBroker:开启使用STOMP协议来传输基于代理(message broker)的消息,这时控制器支持使用@MessageMapping,就像使用@RequestMapping一样。
  2. AbstractWebSocketMessageBrokerConfigurer:继承WebSocket消息代理的类,配置相关信息。
  3. registry.addEndpoint("/socket").setAllowedOrigins("*").withSockJS(); 添加一个访问端点/socket,客户端打开双通道时需要的url,允许所有的域名跨域访问,指定使用SockJS协议。
    4. registry.enableSimpleBroker("/topic","/user"); 配置一个/topic广播消息代理和/queue一对一消息代理
  4. registry.setUserDestinationPrefix("/user");点对点使用的订阅前缀(客户端订阅路径上会体现出来),不设置的话,默认也是/user/

②实现服务器主动向客户端推送消息

  1. 一对多推送

java 代码

import com.xyzla.blog.md.t.User;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.messaging.simp.SimpMessagingTemplate;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Controller;

@Controller
public class WebSocketController {

    @Autowired
    private SimpMessagingTemplate template;

    //广播推送消息
    @Scheduled(fixedRate = 10000)
    public void sendTopicMessage() {
        User user = new User();
        user.setUserName("Jack");
        user.setAge(10);
        this.template.convertAndSend("/topic/getResponse", user);
    }
}
  • SimpMessagingTemplate:SpringBoot提供操作WebSocket的对象
  • @Scheduled(fixedRate = 10000):为了测试,定时10S执行这个方法,向客户端推送
  • template.convertAndSend("/topic/getResponse",new AricResponse("后台实时推送:,Oyzc!")); :直接向前端推送消息。
    • 参数一:客户端监听指定通道时,设定的访问服务器的URL
    • 参数二:发送的消息(可以是对象、字符串等等)

html代码

<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8">
    <meta http-equiv="Pragma" content="no-cache"/>
    <meta http-equiv="Cache-Control" content="no-cache"/>
    <meta http-equiv="Expires" content="0"/>
    <meta name="keywords" content="websocket|java|springmvc">
    <meta name="description" content="This is an example of a Java websocket that uses the Spring MVC framework.">
    <title>STOMP 1</title>
</head>
<body>
<div>
    <p id="response"></p>
</div>

<!-- 独立JS -->
<script src="../js/jquery/jquery-3.3.1.js"></script>
<script src="../js/ws/stomp.js"></script>
<script src="../js/ws/sockjs.js"></script>

<script type="text/javascript">
    var stompClient = null;
    //加载完浏览器后  调用connect(),打开双通道
    $(function(){
        //打开双通道
        connect()
    })
    //强制关闭浏览器  调用websocket.close(),进行正常关闭
    window.onunload = function() {
        disconnect()
    }
    function connect(){
        var socket = new SockJS('https://xyzla.com/socket'); //连接SockJS的endpoint名称为"socket"
        stompClient = Stomp.over(socket);//使用STMOP子协议的WebSocket客户端
        var headers = {
            username: "13611212304",
            password: "123456"
        };
        stompClient.connect(headers,function(frame){//连接WebSocket服务端
            console.log('Connected:' + frame);
            //通过stompClient.subscribe订阅/topic/getResponse 目标(destination)发送的消息
            stompClient.subscribe('/topic/getResponse',function(response){
                showResponse(JSON.parse(response.body));
            });
        });
    }

    //关闭双通道
    function disconnect(){
        if(stompClient != null) {
            stompClient.disconnect();
        }
        console.log("Disconnected");
    }
    function showResponse(message){
        var response = $("#response");
        response.append("<p>姓名:"+message.userName+"  年龄:"+message.age+"</p>");
    }
</script>
</body>
</html>

值得注意的是,只需要在连接服务器注册端点endPoint时,写访问服务器的全路径URL:

new SockJS('https://xyzla.com/socket');

其他监听指定服务器广播的URL不需要写全路径

stompClient.subscribe('/topic/getResponse',function(response){
    showResponse(JSON.parse(response.body));
});
  1. 一对一推送

Java 代码

import com.xyzla.blog.md.t.User;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.messaging.simp.SimpMessagingTemplate;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;

@Controller
public class WebSocketController {

    @Autowired
    private SimpMessagingTemplate template;

    //一对一推送消息
    @Scheduled(fixedRate = 10000)
    public void sendQueueMessage() {
        User user = new User();
        user.setUserId(1);
        user.setUserName("DaPeng");
        user.setAge(10);
        this.template.convertAndSendToUser(user.getUserId() + "", "/queue/getResponse", user);
    }
}

简单介绍一下:

  • SimpMessagingTemplate:SpringBoot提供操作WebSocket的对象
  • @Scheduled(fixedRate = 10000):为了测试,定时10S执行这个方法,向客户端推送
  • template.convertAndSendToUser(user.getUserId()+"","/queue/getResponse",user); :直接向前端推送消息。
    • 参数一:指定客户端接收的用户标识(一般用用户ID)
    • 参数二:客户端监听指定通道时,设定的访问服务器的URL(客户端访问URL跟广播有些许不同)
    • 参数三:向目标发送消息体(实体、字符串等等)

html代码

<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8">
    <meta http-equiv="Pragma" content="no-cache"/>
    <meta http-equiv="Cache-Control" content="no-cache"/>
    <meta http-equiv="Expires" content="0"/>
    <meta name="keywords" content="websocket|java|springmvc">
    <meta name="description" content="This is an example of a Java websocket that uses the Spring MVC framework.">
    <title>STOMP 2</title>
</head>
<body>
<div>
    <p id="response"></p>
</div>
<!-- 独立JS -->
<script src="../js/jquery/jquery-3.3.1.js"></script>
<script src="../js/ws/stomp.js"></script>
<script src="../js/ws/sockjs.js"></script>

<script type="text/javascript">
    var stompClient = null;
    //加载完浏览器后  调用connect(),打开双通道
    $(function () {
        //打开双通道
        connect()
    })
    //强制关闭浏览器  调用websocket.close(),进行正常关闭
    window.onunload = function () {
        disconnect()
    }

    function connect() {
        var userId = 1;
        var socket = new SockJS('https://xyzla.com/socket'); //连接SockJS的endpoint名称为"socket",项目根路径为 /
        stompClient = Stomp.over(socket);//使用STMOP子协议的WebSocket客户端
        var headers = {
            username: "13611212304",
            password: "123456"
        };
        stompClient.connect(headers, function (frame) {//连接WebSocket服务端
            console.log('Connected:' + frame);
            //通过stompClient.subscribe订阅/topic/getResponse 目标(destination)发送的消息
            stompClient.subscribe('/user/' + userId + '/queue/getResponse', function (response) {
                var code = JSON.parse(response.body);
                showResponse(code)
            });
        });
    }

    //关闭双通道
    function disconnect() {
        if (stompClient != null) {
            stompClient.disconnect();
        }
        console.log("Disconnected");
    }

    function showResponse(message) {
        var response = $("#response");
        response.append("<p>只有userID为" + message.userId + "的人才能收到</p>");
    }
</script>
</body>
</html>

与广播不同的是,在指定通道的URL加个用户标识:

stompClient.subscribe('/user/' + userId + '/queue/getResponse', function (response) {
    var code = JSON.parse(response.body);
    showResponse(code)
});

该标识userId必须与服务器推送消息时设置的用户标识一致

以上就是实现服务器实时向客户端推送消息,各位可以按照各自的需求进行配合使用。

③实现客户端与服务器之间的直接交互,聊天室demo[在②的基础上添加了一些代码]

修改WebSocketConfig,增加 /mass,/alone

import org.springframework.context.annotation.Configuration;
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;

//springBoot2.0版本后使用 实现WebSocketMessageBrokerConfigurer接口;
//2.0以下版本继承AbstractWebSocketMessageBrokerConfigurer 类;
@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {
    @Override
    public void registerStompEndpoints(StompEndpointRegistry registry) {
        //注册一个Stomp 协议的endpoint指定URL为myWebSocket,并用.withSockJS()指定 SockJS协议。.setAllowedOrigins("*")设置跨域
        registry.addEndpoint("/socket").setAllowedOrigins("*").withSockJS();
    }

    @Override
    public void configureMessageBroker(MessageBrokerRegistry config) {
        //配置消息代理(message broker)
        //客户端订阅地址的前缀   将消息传回给以‘/topic’开头的客户端   群发(mass)  单独聊天(alone)
        config.enableSimpleBroker("/topic", "/queue", "/user", "/mass", "/alone");

        //服务端接收地址的前缀
        config.setApplicationDestinationPrefixes("/app");

        config.setUserDestinationPrefix("/user");
    }
}

Java 代码

import com.xyzla.blog.md.t.ChatRoomRequest;
import com.xyzla.blog.md.t.ChatRoomResponse;
import com.xyzla.blog.md.t.User;
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.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;

@Controller
public class WebSocketController {

    @Autowired
    private SimpMessagingTemplate template;
 
    //群发
    @MessageMapping("/massRequest")
    //SendTo 发送至 Broker 下的指定订阅路径
    @SendTo("/mass/getResponse")
    public ChatRoomResponse mass(ChatRoomRequest chatRoomRequest) {
        //方法用于群发测试
        System.out.println("name = " + chatRoomRequest.getName());
        System.out.println("chatValue = " + chatRoomRequest.getChatValue());
        ChatRoomResponse response = new ChatRoomResponse();
        response.setName(chatRoomRequest.getName());
        response.setChatValue(chatRoomRequest.getChatValue());
        return response;
    }

    //单独聊天
    @MessageMapping("/aloneRequest")
    public ChatRoomResponse alone(ChatRoomRequest chatRoomRequest) {
        //方法用于一对一测试
        System.out.println("userId = " + chatRoomRequest.getUserId());
        System.out.println("name = " + chatRoomRequest.getName());
        System.out.println("chatValue = " + chatRoomRequest.getChatValue());
        ChatRoomResponse response = new ChatRoomResponse();
        response.setName(chatRoomRequest.getName());
        response.setChatValue(chatRoomRequest.getChatValue());
        this.template.convertAndSendToUser(chatRoomRequest.getUserId() + "", "/alone/getResponse", response);
        return response;
    }
}

html代码

<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8">
    <meta http-equiv="Pragma" content="no-cache"/>
    <meta http-equiv="Cache-Control" content="no-cache"/>
    <meta http-equiv="Expires" content="0"/>
    <meta name="keywords" content="websocket|java|springmvc">
    <meta name="description" content="This is an example of a Java websocket that uses the Spring MVC framework.">
    <title>STOMP 3</title>
    <!-- 独立css -->
    <style type="text/css">
        .chatWindow{width:100%;height:500px;border:1px solid blue}
        .chatRecord{width:100%;height:400px;border-bottom:1px solid blue;line-height:20px;overflow:auto;overflow-x:hidden}
        .sendWindow{width:100%;height:50px}
        .sendChatValue{width:98%;height:37px}
    </style>
</head>

<body>
<div>
    <div style="float:left;width:40%">
        <p>请选择你是谁:</p>
        <select id="selectName" onchange="sendAloneUser();">
            <option value="1">请选择</option>
            <option value="avy">avy</option>
            <option value="oaf">oaf</option>
            <option value="TTT">TTT</option>
            <option value="bigbong">bigbong</option>
        </select>
        <div class="chatWindow">
            <p style="color:darkgrey">群聊:</p>
            <section id="chatRecord" class="chatRecord">
                <p style="color:#CD2626;"></p>
            </section>
            <section class="sendWindow">
                <textarea name="sendChatValue" id="sendChatValue" class="sendChatValue"></textarea>

            </section>
            <input type="button" class="sendMessage" onclick="sendMassMessage()" value="发送">
        </div>
    </div>


    <div style="float:right; width:40%">
        <p>请选择你要发给谁:</p>
        <select id="selectName2">
            <option value="1">请选择</option>
            <option value="avy">avy</option>
            <option value="oaf">oaf</option>
            <option value="TTT">TTT</option>
            <option value="bigbong">bigbong</option>
        </select>
        <div class="chatWindow">
            <p style="color:darkgrey">单独聊:</p>
            <section id="chatRecord2" class="chatRecord">
                <p style="color:#CD2626;"></p>
            </section>
            <section class="sendWindow">
                <textarea name="sendChatValue2" id="sendChatValue2" class="sendChatValue"></textarea>
                <input type="button" class="sendMessage" onclick="sendAloneMessage()" value="发送">
            </section>
        </div>
    </div>
</div>
<!-- 独立JS -->
<script src="../js/jquery/jquery-3.3.1.js"></script>
<script src="../js/ws/stomp.js"></script>
<script src="../js/ws/sockjs.js"></script>

<script type="text/javascript">
    var stompClient = null;

    //加载完浏览器后  调用connect(),打开双通道
    $(function () {
        //打开双通道
        connect()
    })

    //强制关闭浏览器  调用websocket.close(),进行正常关闭
    window.onunload = function () {
        disconnect()
    }

    //打开双通道
    function connect() {
        // var socket = new SockJS('http://127.0.0.1:80/projectName/socket'); //连接SockJS的endpoint名称为"socket"
        var socket = new SockJS('https://xyzla.com/socket'); //连接SockJS的endpoint名称为"socket"
        stompClient = Stomp.over(socket);//使用STMOP子协议的WebSocket客户端
        var headers = {
            username: "13611212304",
            password: "123456"
        };
        stompClient.connect(headers, function (frame) {//连接WebSocket服务端
            console.log('Connected:' + frame);
            //广播接收信息
            stompTopic();
        });
    }

    //关闭双通道
    function disconnect() {
        if (stompClient != null) {
            stompClient.disconnect();
        }
        console.log("Disconnected");
    }

    //广播(一对多)
    function stompTopic() {
        //通过stompClient.subscribe订阅/topic/getResponse 目标(destination)发送的消息(广播接收信息)
        stompClient.subscribe('/mass/getResponse', function (response) {
            var message = JSON.parse(response.body);
            //展示广播的接收的内容接收
            var response = $("#chatRecord");
            response.append("<p><span>" + message.name + ":</span><span>" + message.chatValue + "</span></p>");
        });
    }

    //列队(一对一)
    function stompQueue() {
        var userId = $("#selectName").val();
        alert("监听:" + userId)
        stompClient.subscribe('/user/' + userId + '/alone/getResponse', function (response) {
            var message = JSON.parse(response.body);
            //展示一对一的接收的内容接收
            var response = $("#chatRecord2");
            response.append("<p><span>" + message.name + ":</span><span>" + message.chatValue + "</span></p>");
        });
    }

    //选择发送给谁的时候触发连接服务器
    function sendAloneUser() {
        stompQueue();
    }

    //群发
    function sendMassMessage() {
        var postValue = {};
        var chatValue = $("#sendChatValue");
        var userName = $("#selectName").val();
        postValue.name = userName;
        postValue.chatValue = chatValue.val();
        if (userName == 1 || userName == null) {
            alert("请选择你是谁!");
            return;
        }
        if (chatValue == "" || userName == null) {
            alert("不能发送空消息!");
            return;
        }
        stompClient.send("/app/massRequest", {}, JSON.stringify(postValue));
        chatValue.val("");
    }

    //单独发
    function sendAloneMessage() {
        var postValue = {};
        var chatValue = $("#sendChatValue2");
        var userName = $("#selectName").val();
        var sendToId = $("#selectName2").val();
        var response = $("#chatRecord2");
        postValue.name = userName;
        postValue.chatValue = chatValue.val();
        postValue.userId = sendToId;
        if (userName == 1 || userName == null) {
            alert("请选择你是谁!");
            return;
        }
        if (sendToId == 1 || sendToId == null) {
            alert("请选择你要发给谁!");
            return;
        }
        if (chatValue == "" || userName == null) {
            alert("不能发送空消息!");
            return;
        }
        stompClient.send("/app/aloneRequest", {}, JSON.stringify(postValue));
        response.append("<p><span>" + userName + ":</span><span>" + chatValue.val() + "</span></p>");
        chatValue.val("");
    }
</script>
</body>
</html>

SpringBoot webSocket 发送广播、点对点消息,Android接收
SpringBoot集成WebSocket【基于STOMP协议】进行点对点[一对一]和广播[一对多]实时推送,内附简易聊天室demo

  • qq_43638135
    妲己再美究为妃: 博主没有想过自己接一些私活干吗?我现在还没毕业,但是我也确实听说外挂市场自动化游戏脚本市场挺火热的,并且报酬也很丰厚,但是具体的我也不是很清楚,求解答。 (1个月前 #47楼) 查看回复(2) 举报 回复
    22