@TOC

WebSocket

本文局部援用
参考: 以下两位大佬
**b战i t老齐 (架构巨匠)
https://blog.csdn.net/sanmi82... 讲的很具体**

简介

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_...

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 对象。

语法

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 openedsocket.addEventListener('open', function (event) {    socket.send('Hello Server!');});// Listen for messagessocket.addEventListener('message', function (event) {    console.log('Message from server ', event.data);});

客户端

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

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

HTTP长连贯中,每次数据交换除了真正的数据局部外,服务器和客户端还要大量替换HTTP
header,信息替换效率很低。Websocket协定通过第一个申请建设了TCP连贯之后,之后替换的数据都不须要发送 HTTP
header就能替换数据,这显然和原有的HTTP协定有区别,所以它须要对服务器和客户端都进行降级能力实现(支流浏览器都已反对HTML5)。

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

附录(代码)


仓库地址:https://gitee.com/baichen9187...

附上前端代码

<!DOCTYPE html><html lang="en"><head>    <meta charset="UTF-8">    <title>test</title></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 + "<br/>";    }    function setSelectHtml(list) {        for (let i  in list) {            if (name != list[i]) {                document.getElementById("accepter").innerHTML += '<option value =' + list[i] + '>' + list[i] + '</option>';            }        }    }    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>

后端代码

<dependency>    <groupId>org.springframework.boot</groupId>     <artifactId>spring-boot-starter-websocket</artifactId> </dependency>
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);        }    }}