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

(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>

配置类

@Configuration
public class WebSocketConfig{

    @Bean
    public ServerEndpointExporter serverEndpointExporter(){
        return new ServerEndpointExporter();
    }
}

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();
            }
        }
    }
}

订单实体类

@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;
}

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;
    }
}

前端页面

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</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 Box</el-button>
</div>

<!--1.导入Vue.js-->
<script src="https://cdn.jsdelivr.net/npm/vue@2.5.21/dist/vue.min.js"></script>
<!-- 引入样式element-ui -->
<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>

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

@Configuration
public class AppConfig implements WebMvcConfigurer {

    @Override
    public void addCorsMappings(CorsRegistry registry) {
        registry.addMapping("/**")  // 拦截所有的请求
                .allowedOriginPatterns("*")  // 可跨域的域名,可以为 *
                .allowCredentials(true)
                .allowedMethods("*")   // 允许跨域的方法,可以单独配置
                .allowedHeaders("*");  // 允许跨域的请求头,可以单独配置
    }
}

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

开始测试:
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>

配置类

@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");
    }
    
}

实体类

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

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);
    }

}

前端页面

<!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("<tr><td>" + receiveMessage.content + "</td></tr>");
        });
    }

    /* 断开连接 */
    function disconnect() {
        stompClient.disconnect(function() {
            alert("断开连接");
        });
    }

    /* 发送消息并指定目标地址(这里设置的目标地址为自身订阅消息的地址,当然也可以设置为其它地址) */
    function sendMessageNoParameter() {
        // 设置发送的内容
        var sendContent = $("#content").val();
        // 设置待发送的消息内容
        var message = '{"destination": "' + SUBSCRIBE + '", "content": "' + sendContent + '"}';
        // 发送消息
        stompClient.send(SEND_ENDPOINT, {}, message);
    }
</script>

输入地址 ​​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就可以收到消息了
在这里插入图片描述

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值