• websocket技术详解,附带springboot实例


    WebSocket

    本文部分引用
    参考: 以下两位大佬
    b战i t老齐 (架构大师)
    https://blog.csdn.net/sanmi8276/article/details/119995481 讲的很详细

    简介

    WebSocket是一种在单个TCP连接上进行全双工通信的协议。WebSocket通信协议于2011年被IETF定为标准RFC 6455,并由RFC7936补充规范。WebSocket API也被W3C定为标准。
    WebSocket使得客户端和服务器之间的数据交换变得更加简单,允许服务端主动向客户端推送数据。在WebSocket API中,浏览器和服务器只需要完成一次握手,两者之间就直接可以创建持久性的连接,并进行双向数据传输。
    WebSocket通常是基于http1.1来实现的

    ws://localhost:port/path
    http://localhost/path

    原理

    WebSocket 基于HTTP 协议使用tcp进行一次握手 ,握手成功后,使用101状态协议升级,数据就直接从 TCP 通道传输,
    WebSocket中采用了101状态码,进行协议切换

    HTTP 101 Switching Protocol(协议切换)状态码表示服务器应客户端升级协议的请求对协议进行切换。

    在这里插入图片描述

    传统的实现通信

    很多网站为了实现推送技术,所用的技术都是轮询。轮询是在特定的时间间隔(如每1秒),由浏览器对服务器发出HTTP请求,然后由服务器返回最新的数据给客户端的浏览器。这种传统的模式带来很明显的缺点,即浏览器需要不断的向服务器发出请求,然而HTTP请求可能包含较长的头部,其中真正有效的数据可能只是很小的一部分,显然这样会浪费很多的带宽等资源。
    而比较新的技术去做轮询的效果是Comet。这种技术虽然可以双向通信,但依然需要反复发出请求。而且在Comet中,普遍采用的长链接,也会消耗服务器资源。

    Websocket全双工通信

    Websocket是一个应用层协议,它必须依赖 HTTP 协议进行一次握手 ,握手成功后,数据就直接从 TCP 通道传输,与 HTTP 无关了。即:websocket分为握手和数据传输阶段,即进行了HTTP握手 + 双工的TCP连接。既然是基于浏览器端的web技术,那么它的通信肯定少不了http,websocket本身虽然也是一种新的应用层协议,但是它也不能够脱离http而单独存在。具体来讲,我们在客户端构建一个websocket实例,并且为它绑定一个需要连接到的服务器地址,当客户端连接服务端的时候,会向服务端发送一个类似下面的http报文

    ws组成

    这是下方代码中的实际请求,其中的
    Upgrade: websocket
    Connection: Upgrade
    这个是WebSocket的核心,告诉服务器,客户端发起的是WebSocket类型请求。
    Sec-WebSocket-Key是WebSocket客户端发送的一个 base64编码的密文,浏览器随机生成,要求服务端必须返回一个对应加密的Sec-WebSocket-Accept应答,否则客户端会抛出Error during WebSocket handshake错误,并关闭连接。
    Sec-WebSocket-Version 是告诉服务器所使用的 Websocket 协议版本

    GET ws://localhost:8080/p2p?name=miiYe HTTP/1.1
    Host: localhost:8080
    Upgrade: websocket
    Origin: http://localhost:8080
    Sec-WebSocket-Version: 13
    Cookie: XXL_JOB_LOGIN_IDENTITY=7b226964223a312c22757365726e616d65223a2261646d696e222c2270617373776f7264223a226531306164633339343962613539616262653536653035376632306638383365222c22726f6c65223a312c227065726d697373696f6e223a6e756c6c7d
    Sec-WebSocket-Key: 4ijsZfPDtoTJ2AJPEHuAqg==
    Sec-WebSocket-Extensions: permessage-deflate; client_max_window_bits

    下面代码实例
    在这里插入图片描述

    在这里插入图片描述

    浏览器兼容

    在这里插入图片描述

    特点

    较少的控制开销。在连接创建后,服务器和客户端之间交换数据时,用于协议控制的数据包头部相对较小。在不包含扩展的情况下,对于服务器到客户端的内容,此头部大小只有2至10字节(和数据包长度有关);对于客户端到服务器的内容,此头部还需要加上额外的4字节的掩码。相对于HTTP请求每次都要携带完整的头部,此项开销显著减少了。
    更强的实时性。由于协议是全双工的,所以服务器可以随时主动给客户端下发数据。相对于HTTP请求需要等待客户端发起请求服务端才能响应,延迟明显更少;即使是和Comet等类似的长轮询比较,其也能在短时间内更多次地传递数据。
    保持连接状态。与HTTP不同的是,Websocket需要先创建连接,这就使得其成为一种有状态的协议,之后通信时可以省略部分状态信息。而HTTP请求可能需要在每个请求都携带状态信息(如身份认证等)。
    更好的二进制支持。Websocket定义了二进制帧,相对HTTP,可以更轻松地处理二进制内容。
    可以支持扩展。Websocket定义了扩展,用户可以扩展协议、实现部分自定义的子协议。如部分浏览器支持压缩等。
    更好的压缩效果。相对于HTTP压缩,Websocket在适当的扩展支持下,可以沿用之前内容的上下文,在传递类似的数据时,可以显著地提高压缩率。

    http中的长链接

    原文:https://blog.csdn.net/weixin_45904404/article/details/126702817?spm=1001.2014.3001.5502

    HTTP/1.1默认是支持长连接的,有没有这个请求头都行。
    当然了,协议是这样规定的,至于支不支持还得看服务器(比如tomcat)和客户端(比如浏览器)的具体实现。在实践过程中发现谷歌浏览器使用HTTP/1.1协议时请求头中总会带上Connection: keep-alive,另外通过。如果HTTP/1.1版本的http请求报文不希望使用长连接,则要在请求头中加上Connection: closed,接收到这个请求头的对端服务就会主动关闭连接。

    http长连接也不会一直保持,一般服务端都会设置keep-alive超时时间。超过指定的时间间隔,服务端就会主动关闭连接。同时服务端还会设置一个参数叫最大请求数,比如当最大请求数是300时,只要请求次数超过300次,即使还没到超时时间,服务端也会主动关闭连接。

    持久连接的特点是,只要任意一端没有明确提出断开连接,则保持TCP连接状态。
    在这里插入图片描述

    websocketApi

    官方文档入口

    WebSocket()

    WebSocket()**构造函器会返回一个 WebSocket 对象。
    
    • 1

    语法

    var aWebSocket = new WebSocket(url [, protocols]);

    // 要连接的 URL;这应该是 WebSocket 服务器将响应的 URL。
    protocols 可选
    // 一个协议字符串或者一个包含协议字符串的数组。这些字符串用于指定子协议,这样单个服务器可以实现多个 WebSocket 子协议(例如,您可能希望一台服务器能够根据指定的协议(protocol)处理不同类型的交互)。如果不指定协议字符串,则假定为空字符串。

    websocket的一些属性

    WebSocket.binaryType 使用二进制的数据类型连接。
    WebSocket.bufferedAmount 只读 未发送至服务器的字节数。
    WebSocket.extensions 只读服务器选择的扩展。
    WebSocket.onclose 用于指定连接关闭后的回调函数。
    WebSocket.onerror用于指定连接失败后的回调函数。
    WebSocket.onmessage 用于指定当从服务器接受到信息时的回调函数。
    WebSocket.onopen 用于指定连接成功后的回调函数。
    WebSocket.protocol 只读 服务器选择的下属协议。
    WebSocket.readyState 只读 当前的链接状态。
    WebSocket.url 只读 WebSocket 的绝对路径。

    官方文档的事例

    // Create WebSocket connection.
    const socket = new WebSocket('ws://localhost:8080');
    
    // Connection opened
    socket.addEventListener('open', function (event) {
        socket.send('Hello Server!');
    });
    
    // Listen for messages
    socket.addEventListener('message', function (event) {
        console.log('Message from server ', event.data);
    });
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    客户端

     function connect() {
            if ("WebSocket" in window) {
                websocket = new WebSocket("ws://localhost:8080/p2p?name=" + name);
                websocket.onopen = function (event) {
                    setMessageHtml("onopen");
                }
                websocket.onclose = function (event) {
                    setMessageHtml("onclose");
                }
                websocket.onmessage = function (event) {
                    setMessageHtml(event.data);
                }
            } else {
                alert("not find socket")
            }
    
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17

    通过查看WebSocket的原理,与HTTP对比,得出结论:

    HTTP长连接中,每次数据交换除了真正的数据部分外,服务器和客户端还要大量交换HTTP
    header,信息交换效率很低。Websocket协议通过第一个请求建立了TCP连接之后,之后交换的数据都不需要发送 HTTP
    header就能交换数据,这显然和原有的HTTP协议有区别,所以它需要对服务器和客户端都进行升级才能实现(主流浏览器都已支持HTML5)。

    此外还有 multiplexing、不同的URL可以复用同一个WebSocket连接等功能。这些都是HTTP长连接不能做到的。
    基于以上分析,我们可以看到,websocket能够提供低延迟,高性能的客户端与服务端的双向数据通信。它颠覆了之前web开发的请求处理响应模式,并且提供了一种真正意义上的客户端请求,服务器推送数据的模式,特别适合实时数据交互应用开发。

    附录(代码)

    在这里插入图片描述
    仓库地址:https://gitee.com/baichen9187/websocket.git

    附上前端代码

    DOCTYPE html>
    <html lang="en">
    
    <head>
        <meta charset="UTF-8">
        <title>testtitle>
    head>
    <body>
    <div>
        选择发送的对象
        <select name="accepter" id="accepter" onchange="accepterChange(this.value)">
            <option value ='all'>全部option>
        select>
    div>
    <div>
        <input id="text" type="text">
        <button onclick="send()"> 发送button>
        <button onclick="closeWebSocket()"> 断开button>
        <div id="message">div>
    div>
    
    <script type="text/javascript">
        var websocket = null;
        var accepter = "";
        var name = randomString(5)
        var init = function () {
    
            connect();
            select();
            accepterChange("all");
        }();
    
        function connect() {
            if ("WebSocket" in window) {
                websocket = new WebSocket("ws://localhost:8080/p2p?name=" + name);
                websocket.onopen = function (event) {
                    setMessageHtml("onopen");
                }
                websocket.onclose = function (event) {
                    setMessageHtml("onclose");
                }
                websocket.onmessage = function (event) {
                    setMessageHtml(event.data);
                }
            } else {
                alert("not find socket")
            }
    
        }
    
    
    
        function accepterRegistered() {
            let xmlHttp = new XMLHttpRequest;
            xmlHttp.open('GET', 'http://localhost:8080/accepter?initiator=' + name + "&" + "accepter=" + accepter);
            xmlHttp.setRequestHeader('content-type', 'application/json');
            xmlHttp.send();
        }
    
        function setMessageHtml(innerHTML) {
            document.getElementById("message").innerHTML += innerHTML + "
    "
    ; } function setSelectHtml(list) { for (let i in list) { if (name != list[i]) { document.getElementById("accepter").innerHTML += ' + list[i] + ''; } } } function closeWebSocket() { websocket.close(); select() } function send() { let data = document.getElementById("text").value; websocket.send(data); document.getElementById("text").value = ""; } function randomString(len) { len = len || 32; let $chars = 'ABCDEFGHJKMNPQRSTWXYZabcdefhijkmnprstwxyz2345678'; let maxPos = $chars.length; let pwd = ''; for (i = 0; i < len; i++) { pwd += $chars.charAt(Math.floor(Math.random() * maxPos)); } return pwd; } function accepterChange(data) { accepter = data; accepterRegistered(); } function select() { let xmlHttp = new XMLHttpRequest; xmlHttp.open('GET', 'http://localhost:8080/list'); xmlHttp.setRequestHeader('content-type', 'application/json'); xmlHttp.send(); xmlHttp.onreadystatechange = function () { if (xmlHttp.readyState == 4 && xmlHttp.status == 200) { let json = xmlHttp.responseText;//获取到json字符串,还需解析 let list = JSON.parse(json); accepter = list.length > 0 ? list[0] : ""; setSelectHtml(list); } }; }
    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
    • 85
    • 86
    • 87
    • 88
    • 89
    • 90
    • 91
    • 92
    • 93
    • 94
    • 95
    • 96
    • 97
    • 98
    • 99
    • 100
    • 101
    • 102
    • 103
    • 104
    • 105
    • 106
    • 107
    • 108
    • 109
    • 110
    • 111
    • 112
    • 113
    • 114
    • 115
    • 116
    • 117
    • 118

    后端代码

    <dependency>
        <groupId>org.springframework.bootgroupId>
         <artifactId>spring-boot-starter-websocketartifactId>
     dependency>
    
    • 1
    • 2
    • 3
    • 4
    package com.items.websocket;
    
    
    import lombok.Data;
    import lombok.extern.slf4j.Slf4j;
    import org.springframework.stereotype.Component;
    
    import javax.websocket.*;
    import javax.websocket.server.PathParam;
    import javax.websocket.server.ServerEndpoint;
    import java.io.IOException;
    import java.util.*;
    import java.util.stream.Collectors;
    
    @Component
    @Slf4j
    @ServerEndpoint(value = "/p2p")
    public class P2pWebsocket {
    
        private final static Map<String, SessionConnect> sessionConnectMap = new HashMap<>(10);
    
        private final static String[] sessions = new String[10];
    
        @Data
        static class SessionConnect{
            Session session;
            /**
             * 发起人
             */
            String initiator;
    
            /**
             * 承诺人
             */
            String accepter;
        }
    
        /**
         *  一对一连接
         *
         * @param session   会话
         */
        @OnOpen
        public void onOpen(Session session){
            Map<String,String> map = parameter(session.getQueryString());
            String initiator = map.get("name");
            sessions[Integer.parseInt(session.getId())] = initiator;
            if (!sessionConnectMap.containsKey(initiator)){
                SessionConnect sessionConnect = new SessionConnect();
                sessionConnect.setInitiator(initiator);
                sessions[Integer.parseInt(session.getId())] = map.get("name");
                sessionConnectMap.put(initiator,sessionConnect);
            }
            sessionConnectMap.get(initiator).setSession(session);
        }
    
        @OnClose
        public void onClose(Session session){
            Map<String,String> map = parameter(session.getQueryString());
            String initiator = map.get("name");
            if (sessionConnectMap.containsKey(initiator)){
                sessionConnectMap.remove(initiator);
            }
            sessions[Integer.parseInt(session.getId())] = null;
            sessionConnectMap.forEach(
                    (k,v)->{
                        if(v.getAccepter().equals(initiator)){
                            v.setAccepter("");
                        }
                    }
            );
        }
    
    
        private Session getAccepterSession(Session session){
            Map<String,String> map = parameter(session.getQueryString());
            String initiator = map.get("name");
            if (sessionConnectMap.containsKey(initiator)){
                String accepter = sessionConnectMap.get(initiator).getAccepter();
                if (sessionConnectMap.containsKey(accepter)){
                    return sessionConnectMap.get(accepter).getSession();
                }
            }
            return null;
        }
    
        @OnMessage
        public void sendMessage(String message, Session session) throws IOException {
            Map<String,String> map = parameter(session.getQueryString());
            String initiator = map.get("name");
            if(sessionConnectMap.get(initiator).getAccepter().equals("all")){
                for (Session  s:session.getOpenSessions()) {
                    s.getBasicRemote().sendText(message);
                }
            } else {
                Session session1 = this.getAccepterSession(session);
                assert session1 != null;
                session1.getBasicRemote().sendText(message);
            }
    
        }
    
        @OnError
        public void onError(Session session, Throwable throwable){
            log.error(session.getId());
            log.error(throwable.getLocalizedMessage());
        }
    
        public static List<String> getSessions(){
            return Arrays.stream(sessions).filter(Objects::nonNull).collect(Collectors.toList());
        }
    
        public static Map<String,String> parameter(String urlparam){
            Map<String,String> map = new HashMap<String,String>();
            String[] param =  urlparam.split("&");
            for(String keyvalue:param){
                String[] pair = keyvalue.split("=");
                if(pair.length==2){
                    map.put(pair[0], pair[1]);
                }
            }
            return map;
        }
    
        public static void accepter(String initiator ,String accepter){
            if (sessionConnectMap.containsKey(initiator)){
                sessionConnectMap.get(initiator).setAccepter(accepter);
            }
        }
    }
    
    
    • 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
    • 104
    • 105
    • 106
    • 107
    • 108
    • 109
    • 110
    • 111
    • 112
    • 113
    • 114
    • 115
    • 116
    • 117
    • 118
    • 119
    • 120
    • 121
    • 122
    • 123
    • 124
    • 125
    • 126
    • 127
    • 128
    • 129
    • 130
    • 131
  • 相关阅读:
    web练习
    微信小程序7个步骤告诉你怎么投放广告位
    【leetcode】【剑指offer Ⅱ】066. 单词之和
    【深度学习】大模型卷到机器人上了
    Java基础面试题突击系列5
    [ABC098D] Xor Sum 2 【双指针】
    如何在3GPP网站找到自己需要的技术标准/报告
    自然人如何在浙江法院网上完成诉讼——具体流程
    内容翻译-内容高质量批量免费翻译
    极客时间Kafka - 04 Kafka生产者和消费者拦截器
  • 原文地址:https://blog.csdn.net/weixin_45904404/article/details/127729975