WebSocket

WebSocket

  • 一、什么是WebSocket
  • 二、发展
    • http存在的问题
    • Ajax轮询
    • 长轮询
    • WebSocket的改进
  • 三、特点
  • 四、连接建立过程
    • 选择协议
    • 3次握手
    • 协议转换
    • 连接订阅
    • 消息推送与确认
  • 五、实现
    • 基于原生注解实现
      • js部分
      • Java部分
        • 1.pom引入依赖
        • 2.配置websocket的配置项,WebSocketConfig配置类
        • 3.websocket代码


一、什么是WebSocket

WebSocket是一种在单个TCP连接上进行全双工通信的协议。

WebSocket使得客户端和服务器之间的数据交换变得更加简单,允许服务端主动向客户端推送数据

在WebSocket API中,浏览器和服务器通过握手机制,两者之间就直接可以创建持久性的连接,并进行双向数据传输

注:什么是单工、半双工、全双工通信?信息只能单向传送为单工;信息能双向传送但不能同时双向传送称为半双工;信息能够同时双向传送则称为全双工。

二、发展

http存在的问题

  • http是一种无状态协议,每当一次会话完成后,服务端都不知道下一次的客户端是谁,需要每次知道对方是谁,才进行相应的响应,因此本身对于实时通讯就是一种极大的障碍
  • http协议采用一次请求,一次响应,每次请求和响应就携带有大量的header头,对于实时通讯来说,解析请求头也是需要一定的时间,因此,效率也更低下
  • 最重要的是,需要客户端主动发,服务端被动发,也就是一次请求,一次响应,不能实现主动发送

Ajax轮询

基于http的特性,简单点说,就是规定每隔一段时间就由客户端发起一次请求,查询有没有新消息,如果有,就返回,如果没有等待相同的时间间隔再次询问

优点是解决了http不能实时更新的弊端,因为这个时间很短,发起请求即处理请求返回响应,把这个过程放大n倍,本质上还是request = response

例子:假设张三今天有个快递快到了,但是张三忍耐不住,就每隔十分钟给快递员或者快递站打电话,询问快递到了没,每次快递员就说还没到,等到下午张三的快递到了,但是,快递员不知道哪个电话是张三的,(可不是只有张三打电话,还有李四,王五),所以只能等张三打电话,才能通知他,你的快递到了

从例子上来看有两个问题:

  • 假如说,张三打电话的时间间隔为10分钟,当他收到快递前最后一次打电话,快递员说没到,他刚挂掉电话,快递就到了,那么等下一次时间到了,张三打电话知道快递到了,那么这样的通讯算不算实时通讯?很显然,不算,中间有十分钟的时间差,还不算给快递员打电话的等待时间(抽象的解释:每次request的请求时间间隔等同于十分钟,请求解析相当于等待)
  • 假如说张三所在的小区每天要收很多快递,每个人都采取主动给快递员打电话的方式,那么快递员需要以多快的速度接到,其他人打电话占线也是问题(抽象解释:请求过多,服务端响应也会变慢)

总的来看,Ajax轮询存在的问题:

  • 推送延迟。
  • 服务端压力。配置一般不会发生变化,频繁的轮询会给服务端造成很大的压力。
  • 推送延迟和服务端压力无法中和。降低轮询的间隔,延迟降低,压力增加;增加轮询的间隔,压力降低,延迟增高

长轮询

基于http的特性,简单点说,就是客户端发起长轮询,如果服务端的数据没有发生变更,会保持住请求,直到服务端的数据发生变化,或者等待一定时间超时才会返回。返回后,客户端又会立即再次发起下一次长轮询

优点是解决了http不能实时更新的弊端,因为这个时间很短,发起请求即处理请求返回响应,实现了“伪·长连接”

例子:张三取快递,张三今天一定要取到快递,他就一直站在快递点,等待快递一到,立马取走

从例子上来看有个问题:

  • 假如有好多人一起在快递站等快递,那么这个地方是否足够大,(抽象解释:需要有很高的并发,同时有很多请求等待在这里)

总的来看,长轮询存在的问题

  • 推送延迟。服务端数据发生变更后,长轮询结束,立刻返回响应给客户端。

  • 服务端压力。长轮询的间隔期一般很长,例如 30s、60s,并且服务端保持住连接会消耗太多服务端资源。

WebSocket的改进

一旦WebSocket连接建立后,后续数据都以帧序列的形式传输。在客户端断开WebSocket连接或Server端中断连接前,不需要客户端和服务端重新发起连接请求。在海量并发及客户端与服务器交互负载流量大的情况下,极大的节省了网络带宽资源的消耗,有明显的性能优势,且客户端发送和接受消息是在同一个持久连接上发起,实现了“真·长连接”,实时性优势明显。

WebSocket_第1张图片

三、特点

  • 较少的控制开销。在连接创建后,服务器和客户端之间交换数据时,用于协议控制的数据包头部相对较小。在不包含扩展的情况下,对于服务器到客户端的内容,此头部大小只有2至10字节(和数据包长度有关);对于客户端到服务器的内容,此头部还需要加上额外的4字节的掩码。相对于HTTP请求每次都要携带完整的头部,此项开销显著减少了。

  • 更强的实时性。由于协议是全双工的,所以服务器可以随时主动给客户端下发数据。相对于HTTP请求需要等待客户端发起请求服务端才能响应,延迟明显更少;即使是和Comet等类似的长轮询比较,其也能在短时间内更多次地传递数据。

  • 保持连接状态。与HTTP不同的是,Websocket需要先创建连接,这就使得其成为一种有状态的协议,之后通信时可以省略部分状态信息。而HTTP请求可能需要在每个请求都携带状态信息(如身份认证等)。

  • 更好的二进制支持。Websocket定义了二进制帧,相对HTTP,可以更轻松地处理二进制内容。

  • 可以支持扩展。Websocket定义了扩展,用户可以扩展协议、实现部分自定义的子协议。如部分浏览器支持压缩等。

  • 更好的压缩效果。相对于HTTP压缩,Websocket在适当的扩展支持下,可以沿用之前内容的上下文,在传递类似的数据时,可以显著地提高压缩率。

四、连接建立过程

选择协议

发送选择协议的请求,客户端向服务端发送如下请求,用于咨询使用哪类协议建立连接。

WebSocket_第2张图片

请求响应如下

WebSocket_第3张图片

通过响应结果,发现,该连接可使用websocket协议进行建立。

3次握手

通过响应结果,客户端开始基于websocket协议建立连接。

1 客户端向服务端发送连接请求包

WebSocket_第4张图片

2 服务端接收客户端报文

服务端接收客户端发送的报文,通过SYN=1,确认客户端需要建立连接。于是向客户端发送SYN=1,ACK=1的报文。同时将Acknowledgement number序号加1

WebSocket_第5张图片

3 客户端接收服务端发送的报文,并确认

 客户端接收服务发送的报文,检查序列号是否正确(第一次发送的SYN报文的序号 + 1),以及ACK位是否为1。若正确,则发送一个确认包。

WebSocket_第6张图片

协议转换

接着,客户端向服务端发送一个特殊的HTTP请求。

WebSocket_第7张图片

响应如下所示

WebSocket_第8张图片

101响应码,表明服务器了解客户端请求,开始切换Upgrade请求头中定义的协议。

Websocket协议本质上,是一个基于TCP的协议,连接前,完成3次握手后,客户端向服务端发起一个特殊http请求,server端解析后,应答给客户端,至此一个websocket连接完成建立。

连接订阅

客户端通过websocket协议,项服务端发送一个CONNECT命令帧

WebSocket_第9张图片

服务端接收后,返回一个CONNECTED命令帧

WebSocket_第10张图片

客户端向服务端发送SUBSCRIBE命令帧

在此命令帧中,客户端提供需要订阅的目的地地址。

WebSocket_第11张图片

消息推送与确认

服务端推送消息

服务端,通过MESSAGE命令帧,向客户端推送数据。

WebSocket_第12张图片

消息内容如下所示

WebSocket_第13张图片

客户端确认收到消息

客户端收到消息后,向服务端发送一个ack消息,用于确认该消息已收到。

WebSocket_第14张图片

五、实现

基于原生注解实现

js部分

DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>websockettitle>
head>
<body>
Welcome<br/>
<input id="text" type="text">
<button onclick="send()">发送消息button>
<hr/>
<button onclick="closeWebSocket()">关闭WebSocket连接button>
<hr/>
<div id="message">div>
body>
<script>
    var id = "20220616102800";
    var websocket = null;

    if (window.WebSocket) {
        websocket = new WebSocket("ws://localhost:8000/webSocketFirst/" + id);
    } else {
        alert('当前浏览器 Not support websocket')
    }

    // 连接成功建立的回调方法
    websocket.onopen = function () {
        setMessageInnerHTML("WebSocket连接成功");
    };

    // 连接关闭的回调方法
    websocket.onclose = function () {
        setMessageInnerHTML("WebSocket连接关闭");
    };

    // 接收到消息的回调方法
    websocket.onmessage = function (event) {
        setMessageInnerHTML(event.data);
    };

    // 连接发生错误的回调方法
    websocket.onerror = function () {
        setMessageInnerHTML("WebSocket连接发生错误");
    };

    // 监听窗口关闭事件,当窗口关闭时,主动去关闭websocket连接,防止连接还没断开就关闭窗口,server端会抛异常。
    window.onbeforeunload = function () {
        closeWebSocket();
    };

    //将消息显示在网页上
    function setMessageInnerHTML(innerHTML) {
        document.getElementById('message').innerHTML += innerHTML + '
'
; } //关闭WebSocket连接 function closeWebSocket() { websocket.close(); } //发送消息 function send() { var message = '{"to": "' + id + '", "msg": "' + document.getElementById('text').value + '"}'; websocket.send(message); }
script> html>

Java部分

1.pom引入依赖

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

2.配置websocket的配置项,WebSocketConfig配置类

package com.example.springbootdemo.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.socket.server.standard.ServerEndpointExporter;

/**
 * WebSocketConfig
 * 通过这个配置 spring boot 才能去扫描后面的关于 websocket 的注解
 *
 * @author: wcx
 * @date: 2022年06月15日 19:17
 */
@Configuration
public class WebSocketConfig {

	/**
	 * 注入ServerEndpointExporter,这个bean会自动注册使用了@ServerEndpoint注解声明的Websocket endpoint
	 */
	@Bean
	public ServerEndpointExporter serverEndpointExporter() {
	    return new ServerEndpointExporter();
	}
}

3.websocket代码

package com.example.springbootdemo.config;

import com.alibaba.fastjson.JSONObject;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
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.Map;
import java.util.concurrent.ConcurrentHashMap;

/**
 * WebSocket
 *
 * @author: wcx
 * @date: 2022年06月15日 19:24
 */
@ServerEndpoint("/webSocketFirst/{id}")
@Component
public class WebSocketFirst {

    /**
     * @ServerEndpoint 注解是一个类层次的注解,它的功能主要是将目前的类定义成一个websocket服务器端,
     * 注解的值将被用于监听用户连接的终端访问URL地址,客户端可以通过这个URL来连接到WebSocket服务器端
     * {}中的数据代表一个参数,多个参数用/分隔
     */

    /**
     * 静态变量,用来记录当前在线连接数。应该把它设计成线程安全的。
     */
    private static int onlineCount = 0;


    /**
     * CopyOnWriteArraySet它是线程安全的无序的集合,可以将它理解成线程安全的HashSet
     * 用来存放每个客户端对应的MyWebSocket对象
     * 若要实现服务端与单一客户端通信的话,可以使用Map来存放,其中Key可以为用户标识
     */
    // private static CopyOnWriteArraySet webSocketSet = new CopyOnWriteArraySet();
    /**
     * ConcurrentHashMap 的优势在于兼顾性能和线程安全,
     * 一个线程进行写操作时,它会锁住一小部分,其他部分的读写不受影响,其他线程访问没上锁的地方不会被阻塞。
     * 用来存放每个客户端对应的MyWebSocket对象
     */
    private static Map<String, WebSocketFirst> clients = new ConcurrentHashMap<String, WebSocketFirst>();

    /**
     * 与某个客户端的连接会话,需要通过它来给客户端发送数据
     */
    private Session session;

    /**
     * 用户标识
     */
    private String id;

    private final static Logger logger = LoggerFactory.getLogger(WebSocketFirst.class);

    /**
     * 连接建立成功调用的方法
     *
     * @param id      用户标识
     * @param session session为与某个客户端的连接会话,需要通过它来给客户端发送数据
     * @throws IOException
     */
    @OnOpen
    public void onOpen(@PathParam("id") String id, Session session) throws IOException {
        this.id = id;
        this.session = session;
        // 在线数 +1
        addOnlineCount();
        // 放入map中
        clients.put(id, this);
        logger.info("有新连接加入!当前在线人数为" + getOnlineCount());
    }

    /**
     * 连接关闭调用的方法
     *
     * @throws IOException
     */
    @OnClose
    public void onClose() throws IOException {
        // 从map中移除
        clients.remove(id);
        // 在线数 -1
        subOnlineCount();
        logger.info("有一连接关闭!当前在线人数为" + getOnlineCount());
    }

    /**
     * 收到客户端消息后调用的方法
     *
     * @param message 客户端发送过来的消息
     * @throws IOException
     */
    @OnMessage
    public void onMessage(String message) throws IOException {

        JSONObject jsonTo = JSONObject.parseObject(message);
        logger.info(jsonTo.getString("to") + ":" + jsonTo.getString("msg"));

        if (!"all".equals(jsonTo.getString("to").toLowerCase())) {
            sendMessageTo(jsonTo.getString("msg"), jsonTo.getString("to"));
        } else {
            sendMessageAll(jsonTo.getString("msg"));
        }
    }

    /**
     * 发生错误时调用
     *
     * @param session
     * @param error
     */
    @OnError
    public void onError(Session session, Throwable error) {
        logger.info("发生错误");
        error.printStackTrace();
    }

    /**
     * 单个发送
     *
     * @param message 要发送的消息
     * @param To      要发送给谁
     * @throws IOException
     */
    public void sendMessageTo(String message, String To) throws IOException {
        /**
         * getAsyncRemote()和getBasicRemote()区别
         * getAsyncRemote是非阻塞式的,getBasicRemote是阻塞式的,异步与同步的区别
         */
        // session.getBasicRemote().sendText(message);
        // session.getAsyncRemote().sendText(message);

        for (WebSocketFirst item : clients.values()) {
            if (item.id.equals(To)) {
                item.session.getAsyncRemote().sendText(message);
            }
        }
    }

    /**
     * 群发
     *
     * @param message 要发送的消息
     * @throws IOException
     */
    public void sendMessageAll(String message) throws IOException {
        for (WebSocketFirst item : clients.values()) {
            item.session.getAsyncRemote().sendText(message);
        }
    }

    public static synchronized int getOnlineCount() {
        return onlineCount;
    }

    public static synchronized void addOnlineCount() {
        WebSocketFirst.onlineCount++;
    }

    public static synchronized void subOnlineCount() {
        WebSocketFirst.onlineCount--;
    }
}

你可能感兴趣的:(websocket)