• Websocket学习


    参考:http://www.mydlq.club/article/86/

    一、WebSocket 简介

    WebSocket 是一种基于 TCP 的网络协议。是一种全双工通信的协议,既允许客户端向服务器主动发送消息,也允许服务器主动向客户端发送消息。在 WebSocket 中,浏览器和服务器只需要完成一次握手,两者之间就可以建立持久性的连接,进行双向数据传输。

    二、WebSocket 特点

    • 连接握手阶段使用 HTTP 协议;
    • 协议标识符是 ws,如果采用加密则是 wss;
    • 数据格式比较轻量,性能开销小,通信高效;
    • 没有同源限制,客户端可以与任意服务器通信;
    • 建立在 TCP 协议之上,服务器端的实现比较容易;
    • 通过 WebSocket 可以发送文本,也可以发送二进制数据;
    • 与 HTTP 协议有着良好的兼容性。默认端口也是 80 和 443,并且握手阶段采用 HTTP 协议,因此握手时不容易屏蔽,能通过各种 HTTP 代理服务器;

    三、为什么需要 WebSocket

    谈起为什么需要 WebSocket 前,那得先了解在没有 WebSocket 那段时间说起,那时候基于 Web 的消息基本上是靠 Http 协议进行通信,而经常有"聊天室"、“消息推送”、"股票信 息实时动态"等这样需求,而实现这样的需求常用的有以下几种解决方案:
    在这里插入图片描述
    (1)、短轮询(Traditional Polling)

    短轮询是指客户端每隔一段时间就询问一次服务器是否有新的消息,如果有就接收消息。这样方式会增加很多次无意义的发送请求信息,每次都会耗费流量及处理器资源。

    • 优点:短连接,服务器处理简单,支持跨域、浏览器兼容性较好。
    • 缺点:有一定延迟、服务器压力较大,浪费带宽流量、大部分是无效请求。

    (2)、长轮询(Long Polling)

    长轮询是段轮询的改进,客户端执行 HTTP 请求发送消息到服务器后,等待服务器回应,如果没有新的消息就一直等待,知道服务器有新消息传回或者超时。这也是个反复的过程,这种做法只是减小了网络带宽和处理器的消耗,但是带来的问题是导致消息实时性低,延迟严重。而且也是基于循环,最根本的带宽及处理器资源占用并没有得到有效的解决。

    • 优点:减少轮询次数,低延迟,浏览器兼容性较好。
    • 缺点:服务器需要保持大量连接。

    (3)、服务器发送事件(Server-Sent Event)

    服务器发送事件是一种服务器向浏览器客户端发起数据传输的技术。一旦创建了初始连接,事件流将保持打开状态,直到客户端关闭。该技术通过传统的 HTTP 发送,并具有 WebSockets 缺乏的各种功能,例如"自动重新连接"、“事件ID” 及 "发送任意事件"的能力。

    服务器发送事件是单向通道,只能服务器向浏览器发送,因为流信息本质上就是下载。

    • 优点:适用于更新频繁、低延迟并且数据都是从服务端发到客户端。
    • 缺点:浏览器兼容难度高。

    显然,上面这几种方式都有各自的优缺点,虽然靠轮询方式能够实现这些一些功能,但是其对性能的开销和低效率是非常致命的,尤其是在移动端流行的现在。现在客户端与服务端双向通信的需求越来越多,且现在的浏览器大部分都支持 WebSocket。所以对实时性和双向通信及其效率有要求的话,比较推荐使用 WebSocket。

    四、WebSocket 连接流程

    (1)、客户端先用带有 Upgrade:Websocket 请求头的 HTTP 请求,向服务器端发起连接请求,实现握手(HandShake)。

    Connection: Upgrade
    Sec-WebSocket-Extensions: permessage-deflate; client_max_window_bits
    Sec-WebSocket-Key: IRQYhWINfX5Fh1zdocDl6Q==
    Sec-WebSocket-Version: 13
    Upgrade: websocket
    
    • 1
    • 2
    • 3
    • 4
    • 5

    (2)、握手成功后,由 HTTP 协议升级成 Websocket 协议,进行长连接通信,两端相互传递信息。

    五、WebSocket 使用场景

    • 数据流状态: 比如说上传下载文件,文件进度,文件是否上传成功。
    • 协同编辑文档: 同一份文档,编辑状态得同步到所有参与的用户界面上。
    • 多玩家游戏: 很多游戏都是协同作战的,玩家的操作和状态肯定需要及时同步到所有玩家。
    • 多人聊天: 很多场景下都需要多人参与讨论聊天,用户发送的消息得第一时间同步到所有用户。
    • 社交订阅: 有时候我们需要及时收到订阅消息,比如说开奖通知,比如说在线邀请,支付结果等。
    • 股票虚拟货币价格: 股票和虚拟货币的价格都是实时波动的,价格跟用户的操作息息相关,及时推送对用户跟盘有很大的帮助。

    六、使用案例

    官方案例:https://spring.io/guides/gs/messaging-stomp-websocket/

    1.提醒客户端有新订单

    pom.xml

    <dependency>
    	<groupId>org.springframework.boot</groupId>
    	<artifactId>spring-boot-starter-websocket</artifactId>
    </dependency>
    <dependency>
    	<groupId>org.projectlombok</groupId>
    	<artifactId>lombok</artifactId>
    	<optional>true</optional>
    </dependency>
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    配置类

    @Configuration
    public class WebSocketConfig{
    
        @Bean
        public ServerEndpointExporter serverEndpointExporter(){
            return new ServerEndpointExporter();
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    websoket类

    @Slf4j
    @Component
    @ServerEndpoint("/websocket")
    public class WebSocket {
    
        private Session session;
    
        private static CopyOnWriteArraySet<WebSocket> webSocketSet = new CopyOnWriteArraySet<>();
    
        @OnOpen
        public void onOpen(Session session) {
            this.session = session;
            webSocketSet.add(this);
            log.info("【websocket消息】有新的连接, 总数:{}", webSocketSet.size());
        }
    
        @OnClose
        public void onClose() {
            webSocketSet.remove(this);
            log.info("【websocket消息】连接断开, 总数:{}", webSocketSet.size());
        }
    
        @OnMessage
        public void onMessage(String message) {
            log.info("【websocket消息】收到客户端发来的消息:{}", message);
        }
    
        public void sendMessage(String message) {
            for (WebSocket webSocket: webSocketSet) {
                log.info("【websocket消息】广播消息, message={}", message);
                try {
                    webSocket.session.getBasicRemote().sendText(message);
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38

    订单实体类

    @Data
    @AllArgsConstructor
    public class Order {
        private String orderId;
        private String buyerName;
        private String buyerPhone;
        private String buyerAddress;
        private Double orderAmount;
        private Integer state;
        private String createDate;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    controller

    @Slf4j
    @RestController
    public class OrderController {
    
        @Autowired
        private WebSocket webSocket;
    
        static List<Order> orderList=new ArrayList();
        {
            orderList.add(new Order("1","张三","13512341234","上海市",11.11,1,"2022-10-01"));
            orderList.add(new Order("2","李四","18367445678","北京市",22.22,1,"2022-10-02"));
            orderList.add(new Order("3","王五","13812345678","天津市",33.33,1,"2022-10-03"));
        }
    
        /**
         * 模拟下单方法
         */
        @GetMapping("/create")
        public void createOrder(){
            //1.扣库存
            //2.插入数据
            orderList.add(new Order("4","赵六","13452057018","辽宁市",44.44,1,"2022-10-04"));
            //3.websocket通知客户端有新订单
            webSocket.sendMessage("有新的订单!");
        }
    
        @GetMapping("/getOrder")
        public List getOrder(){
            return orderList;
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31

    前端页面

    DOCTYPE html>
    <html lang="en">
    <head>
        <meta charset="UTF-8">
        <title>Titletitle>
    head>
    <body>
    <div id="app">
        <el-table :data="items" stripe style="width: 100%">
            <el-table-column prop="orderId" label="订单号" width="180">el-table-column>
            <el-table-column prop="buyerName" label="姓名" width="180">el-table-column>
            <el-table-column prop="buyerPhone" label="电话" width="180">el-table-column>
            <el-table-column prop="buyerAddress" label="地址" width="180">el-table-column>
            <el-table-column prop="orderAmount" label="订单金额" width="180">el-table-column>
            <el-table-column prop="state" label="状态" width="180">el-table-column>
            <el-table-column prop="createDate" label="创建时间" width="180">el-table-column>
        el-table>
        <el-button type="text" @click="open">点击打开 Message Boxel-button>
    div>
    
    
    <script src="https://cdn.jsdelivr.net/npm/vue@2.5.21/dist/vue.min.js">script>
    
    <link rel="stylesheet" href="https://unpkg.com/element-ui/lib/theme-chalk/index.css">
    
    <script src="https://unpkg.com/element-ui/lib/index.js">script>
    <script src="https://unpkg.com/axios/dist/axios.min.js">script>
    <script>
        var vm = new Vue({
            el: "#app",
            data() {
                return {
                    items: [],
                    message: ""
                }
            },
            methods: {
                open() {
                    this.$alert(this.message, {
                        confirmButtonText: '查看',
                        callback: action => {
                            this.init();
                        }
                    });
                },
                init(){
                    axios.get('http://localhost:8080/getOrder').then(response => (this.items = response.data));
                }
            },
            mounted() {
                this.init();
            }
        })
        var websocket = null;
        if ('WebSocket' in window) {
            websocket = new WebSocket('ws://localhost:8080/websocket');
        } else {
            alert('该浏览器不支持websocket!');
        }
    
        websocket.onopen = function (event) {
            console.log('建立连接');
        }
    
        websocket.onclose = function (event) {
            console.log('连接关闭');
        }
    
        websocket.onmessage = function (event) {
            console.log('收到消息:' + event.data)
            vm.message = event.data;
            vm.open();
        }
    
        websocket.onerror = function () {
            alert('websocket通信发生错误!');
        }
    
        window.onbeforeunload = function () {
            websocket.close();
        }
    script>
    body>
    html>
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84

    启动项目,测试报错
    在这里插入图片描述
    开启跨域

    @Configuration
    public class AppConfig implements WebMvcConfigurer {
    
        @Override
        public void addCorsMappings(CorsRegistry registry) {
            registry.addMapping("/**")  // 拦截所有的请求
                    .allowedOriginPatterns("*")  // 可跨域的域名,可以为 *
                    .allowCredentials(true)
                    .allowedMethods("*")   // 允许跨域的方法,可以单独配置
                    .allowedHeaders("*");  // 允许跨域的请求头,可以单独配置
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    效果:
    在这里插入图片描述
    在这里插入图片描述

    开始测试:
    1.请求新增订单
    在这里插入图片描述

    在这里插入图片描述

    在这里插入图片描述
    点击查看
    在这里插入图片描述

    2.客户端交互

    pom.xml

    <dependency>
    	<groupId>org.springframework.boot</groupId>
    	<artifactId>spring-boot-starter-websocket</artifactId>
    </dependency>
    <dependency>
    	<groupId>org.projectlombok</groupId>
    	<artifactId>lombok</artifactId>
    	<optional>true</optional>
    </dependency>
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    配置类

    @Configuration
    @EnableWebSocketMessageBroker
    public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {
    
        /**
         * 配置 WebSocket 进入点,及开启使用 SockJS,这些配置主要用配置连接端点,用于 WebSocket 连接
         *
         * @param registry STOMP 端点
         */
        @Override
        public void registerStompEndpoints(StompEndpointRegistry registry) {
            registry.addEndpoint("/mydlq").withSockJS();
        }
    
        /**
         * 配置消息代理选项
         *
         * @param registry 消息代理注册配置
         */
        @Override
        public void configureMessageBroker(MessageBrokerRegistry registry) {
            // 设置一个或者多个代理前缀,在 Controller 类中的方法里面发生的消息,会首先转发到代理从而发送到对应广播或者队列中。
            // ⽤户可以订阅来⾃以"/topic"为前缀的消息,客户端只可以订阅这个前缀的主题
            registry.enableSimpleBroker("/topic/abc");
            // 配置客户端发送请求消息的一个或多个前缀,该前缀会筛选消息目标转发到 Controller 类中注解对应的方法里
            //客户端发送过来的消息,需要以"/app"为前缀,再经过Broker转发给响应的Controller
            registry.setApplicationDestinationPrefixes("/app");
        }
        
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30

    实体类

    @Data
    @ToString
    public class MessageBody {
        /** 消息内容 */
        private String content;
        /** 广播转发的目标地址(告知 STOMP 代理转发到哪个地方) */
        private String destination;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    cotroller

    @Controller
    public class MessageController {
    
        /** 消息发送工具对象 */
        @Autowired
        private SimpMessageSendingOperations simpMessageSendingOperations;
    
        /** 广播发送消息,将消息发送到指定的目标地址 */
        @MessageMapping("/test")
        public void sendTopicMessage(MessageBody messageBody) {
            // 将消息发送到 WebSocket 配置类中配置的代理中(/topic)进行消息转发
            System.out.println(messageBody);
            simpMessageSendingOperations.convertAndSend(messageBody.getDestination(), messageBody);
        }
    
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16

    前端页面

    <!DOCTYPE html>
    <html>
    <head>
        <title>Hello WebSocket</title>
        <link href="https://cdn.bootcdn.net/ajax/libs/twitter-bootstrap/3.4.1/css/bootstrap.min.css" rel="stylesheet">
        <meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
        <script src="https://cdn.bootcdn.net/ajax/libs/jquery/3.5.1/jquery.js"></script>
        <script src="https://cdn.bootcdn.net/ajax/libs/sockjs-client/1.4.0/sockjs.min.js"></script>
        <script src="https://cdn.bootcdn.net/ajax/libs/stomp.js/2.3.3/stomp.min.js"></script>
    <!--    <script src="app-websocket.js"></script>-->
    </head>
    <body>
    <div id="main-content" class="container" style="margin-top: 10px;">
        <div class="row">
            <form class="navbar-form" style="margin-left:0px">
                <div class="col-md-12">
                    <div class="form-group">
                        <label>WebSocket 连接:</label>
                        <button class="btn btn-primary" type="button" onclick="connect();">进行连接</button>
                        <button class="btn btn-danger" type="button" onclick="disconnect();">断开连接</button>
                    </div>
                    <label>订阅地址:</label>
                    <div class="form-group">
                        <input type="text" id="subscribe" class="form-control" placeholder="订阅地址">
                    </div>
                    <button class="btn btn-warning" onclick="subscribeSocket();" type="button">订阅</button>
                </div>
            </form>
        </div>
        </br>
        <div class="row">
            <div class="form-group">
                <label for="content">发送的消息内容:</label>
                <input type="text" id="content" class="form-control" placeholder="消息内容">
            </div>
            <button class="btn btn-info" onclick="sendMessageNoParameter();" type="button">发送</button>
        </div>
        </br>
        <div class="row">
            <div class="col-md-12">
                <h5 class="page-header" style="font-weight:bold">接收到的消息:</h5>
                <table class="table table-striped">
                    <tbody id="information"></tbody>
                </table>
            </div>
        </div>
    </div>
    </body>
    </html>
    <script>
        // 设置 STOMP 客户端
        var stompClient = null;
        // 设置 WebSocket 进入端点
        var SOCKET_ENDPOINT = "/mydlq";
        // 设置订阅消息的请求前缀
        var SUBSCRIBE_PREFIX = "/topic"
        // 设置订阅消息的请求地址
        var SUBSCRIBE = "";
        // 设置服务器端点,访问服务器中哪个接口
        var SEND_ENDPOINT = "/app/test";
    
        /* 进行连接 */
        function connect() {
            // 设置 SOCKET
            var socket = new SockJS(SOCKET_ENDPOINT);
            // 配置 STOMP 客户端
            stompClient = Stomp.over(socket);
            // STOMP 客户端连接
            stompClient.connect({}, function (frame) {
                alert("连接成功");
            });
        }
    
        /* 订阅信息 */
        function subscribeSocket(){
            // 设置订阅地址
            SUBSCRIBE = SUBSCRIBE_PREFIX + $("#subscribe").val();
            // 输出订阅地址
            alert("设置订阅地址为:" + SUBSCRIBE);
            // 执行订阅消息
            stompClient.subscribe(SUBSCRIBE, function (responseBody) {
                var receiveMessage = JSON.parse(responseBody.body);
                $("#information").append("" + receiveMessage.content + "");
            });
        }
    
        /* 断开连接 */
        function disconnect() {
            stompClient.disconnect(function() {
                alert("断开连接");
            });
        }
    
        /* 发送消息并指定目标地址(这里设置的目标地址为自身订阅消息的地址,当然也可以设置为其它地址) */
        function sendMessageNoParameter() {
            // 设置发送的内容
            var sendContent = $("#content").val();
            // 设置待发送的消息内容
            var message = '{"destination": "' + SUBSCRIBE + '", "content": "' + sendContent + '"}';
            // 发送消息
            stompClient.send(SEND_ENDPOINT, {}, message);
        }
    </script>
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88
    • 89
    • 90
    • 91
    • 92
    • 93
    • 94
    • 95
    • 96
    • 97
    • 98
    • 99
    • 100
    • 101
    • 102
    • 103

    输入地址 ​​http://localhost:8080/index.html​​ 访问测试的前端页面,然后执行下面步骤进行测试:
    1.点击进行连接按钮,连接 WebSocket 服务端;
    2.在订阅地址栏输入订阅地址
    3.点击订阅按钮订阅对应地址的消息;
    4.在发送消息内容的输入框中输入​​hello world!​​,然后点击发送按钮发送消息;

    流程:先连接服务端,订阅一个地址(当这个地址有消息,服务端就会发送过来,实时显示在界面上),然后发送消息

    例:
    连接服务端后,订阅了/topic/qqq这个地址
    在这里插入图片描述

    调用/app/test 接口,参数为MessageBody(content=999, destination=/topic/qqq)在这里插入图片描述

    此接口向/topic/qqq这个地址发送消息999在这里插入图片描述
    WebSocket 配置类中配置的代理中(/topic/abc)进行消息转发,变成了向/topic/abc发消息999
    在这里插入图片描述
    而前端订阅的是/topic/qqq,所以收不到消息

    当订阅的是/topic/abc就可以收到消息了
    在这里插入图片描述

  • 相关阅读:
    数据科学中的数据库简介
    Vue 中 == 和 ===、&& 、|| 等操作符的用法
    异常~~~
    varchar与char区别,以及最大长度
    【气泵方案】SUP桨板冲浪板打气泵芯片方案
    02_CSS样式之背景参数设置
    AlDente Pro for mac最新激活版:电池长续航软件
    我是如何入门网络安全?有什么自学心得?
    AI 挣钱难,科大讯飞营收将突破200亿元
    【网络安全】黑客自学笔记
  • 原文地址:https://blog.csdn.net/qq_42665745/article/details/128032828