前言

我们在使用 Spring 做网页开发的时候,我们后端服务器的主要作用就是处理前端、客户端发送来的请求,也就是说我们的服务器是被动接受请求的一方,而在某些时候,需要我们的服务器主动向客户端发送网络数据包,例如购物软件的降价通知,聊天软件别人发送消息时候的通知,这些都需要服务器主动向客户端发送网络数据包。

浏览器像服务器发送网络请求,应用层使用的协议往往是 HTTP 或者 HTTPS 协议,而 HTTP 协议在发展的时候却没有涉及到服务器主动向客户端发送网络数据包的功能,因为 HTTP 的发展初衷就是为了人们通过网络能够看报纸、新闻的,也就没想到服务器能够通过 HTTP 协议主动向客户端发送网络数据包,所以通过 HTTP 协议是无法实现服务器主动向客户端发送网络数据包的功能的。

那么如何实现服务器主动向客户端发送请求的功能呢?这就需要使用到另外一种应用层协议——WebSocket 了。

什么是 WebSocket

来看看百度的解释:

WebSocket是一种在单个TCP连接上进行全双工通信的协议。它允许服务端主动向客户端推送数据,同时也支持客户端向服务端发送数据,实现了真正的双向通信。WebSocket协议于2011年被IETF定为标准RFC 6455,并被RFC7936所补充规范。WebSocket API也被W3C定为标准,使得浏览器和服务器之间的数据交换变得更加简单和高效。

传统的应用层使用其他协议的 web 程序,都是属于”一问一答“形式,客户端给服务器发送 HTTP 请求之后,服务器给客户端返回一个 HTTP 响应,在这种情况下,服务器是属于被动的一方,如果客户端不主动发起请求,服务器无法主动给客户端响应。而 WebSocket 则是更接近于 TCP 这种级别的通信方式,一旦建立连接,客户端和服务端都可以主动向对方发送数据。

WebSocket 协议和 HTTP 协议的区别

  1. 协议性质
  • WebSocket:是一种在单个TCP连接上进行全双工通信的协议。它允许服务端主动向客户端推送数据,使得客户端和服务器之间的数据交换变得更加简单和高效。WebSocket通信协议于2011年被IETF定为标准RFC 6455,并由RFC7936补充规范。WebSocket API也被W3C定为标准。(来源:百度百科)
  • HTTP:全称超文本传输协议(HyperText Transfer Protocol),是一种用于分布式、协作式、超媒体信息系统的应用层协议。HTTP是一个基于TCP/IP通信协议来传递数据的协议,客户端发送请求,服务器返回响应。HTTP是无连接的,即每次连接只处理一个请求,服务器处理完客户请求,并收到客户的应答后,就断开连接。(来源:知乎专栏)
  1. 通信方式
  • WebSocket:支持双向通信,即客户端和服务器可以同时向对方发送数据。这种全双工的通信方式使得WebSocket特别适用于需要实时数据交换的场景,如在线聊天、实时数据更新等。
  • HTTP:是单向的、请求-响应模式的协议。客户端发起请求,服务器返回响应,然后连接断开。如果需要再次交换数据,必须重新建立连接。
  1. 连接状态
  • WebSocket:是有状态的协议。一旦建立了WebSocket连接,客户端和服务器之间的通信就可以持续进行,直到连接被关闭。这种持久连接减少了因频繁建立连接而产生的开销。
  • HTTP:是无状态的协议。HTTP协议对事务处理没有记忆能力,即服务器不会记住任何关于客户端请求的信息。如果需要处理多个请求之间的关联,必须在每个请求中携带必要的状态信息。
  1. 数据传输效率
  • WebSocket:由于使用了长连接和较少的控制开销(如头部信息较小),WebSocket在数据传输效率上优于HTTP。特别是在需要频繁交换小量数据的场景中,WebSocket能够显著减少网络带宽的消耗。
  • HTTP:每次请求都需要携带完整的头部信息,这可能导致在传输小量数据时头部信息的开销占比较大。此外,HTTP请求可能包含较长的头部,其中真正有效的数据可能只是很小的一部分,从而浪费了网络带宽。

WebSocket 原理解析

WebSocket 本质上是一个基于 TCP 的协议。为了建立一个 WebSocket 连接,客户端浏览器首先要向服务器发起一个 HTTP 请求,这个请求和通常的 HTTP 请求不同,包含了一些附加的头信息,通过这些附加的头信息来完成握手过程。

那么这些附加的头信息是哪些呢?

对于浏览器发送的请求数据包的头信息中,附加的信息有:

  • Connection: upgrade。表示我需要升级协议
  • Sec-WebSocket-Accept: xxxxxx。服务端与该客户端通讯的钥匙
  • Sec-WebSocket-Version: 13。升级的协议的版本
  • Upgrade: websocket。升级的协议格式

在这里插入图片描述

WebSocket 报文格式

WebSocket 协议的相关信息大家可以去官方文档中查看:https://www.rfc-editor.org/rfc/rfc6455

在这里插入图片描述

  • FIN 表示是否要关闭 WebSocket,为 1 表示断开 WebSocket 连接,这里的 FIN 和 TCP 报文中的 FIN 不是一个概念。
  • RSV1/RSV2/RSV3:保留位,现在先不用,但是不保证后面可能会用到,值一般为 0。
  • opcode:操作代码,决定了如何理解后面的数据载荷。
    • %x0:表示这是一个延续帧。当 opcode 为0,表示本次数据传输采用了数据分片,当前收到的帧为其中一个分片
    • %x1:表示这是文本帧,也就是载荷中的数据是文本类型
    • %x2:表示这是二进制帧,也就是载荷中的数据是二进制类型
    • %x3-7:保留,暂未使用
    • %x8:表示连接断开
    • %x9:表示 ping 帧
    • %xA:表示 pong 帧
    • %xB-F:保留,暂未使用
  • mask:表示是否要对数据载荷进行掩码操作。从客户端向服务端发送数据时,需要对数据进行掩码操作;从服务端向客户端发送数据时,不需要对数据进行掩码操作
  • Payload length:数据载荷的长度,单位是字节,能表示的范围是0-127
  • Extended Payload length:扩展的载荷长度,127字节的大小肯定是不够用的,所以就出现了扩展载荷,当Payload length的值0-125的时候表示扩展载荷的长度为0,Payload length的值为126时,表示扩展载荷的长度为16位,值为127时,表示扩展载荷的长度为64位
  • Masking-key:0或者4字节(32位)所有从客户端传送到服务端的数据帧,数据载荷都进行了掩码操作,mask 为 1,且携带了4字节的Masking-key。如果 mask 为0.则没有 Masking-key
  • Payload Data:报文携带的载荷数据

websocket 载荷的长度可以是 6比特位,单位是字节能表示的大小,也可以是 16比特位、或者64比特位能表示的范围大小。

使用掩码算法的目的主要是从安全角度考虑,避免一些缓冲区溢出攻击。

我们可以直接使用 tomcat 提供的 WebSocket 的原生 API,也可以使用 Spring 内置的 WebSocket,其实这两者区别不大。

Spring 中 WebSocket 的使用

首先我们在创建 Spring 项目的时候需要添加进去 WebSocket 依赖。

在这里插入图片描述
在这里插入图片描述
也可以自己手动添加 websockt 依赖。

添加完依赖之后,我们创建一个类,继承TextWebSocketHandler 类,如果你的 websocket 中的载荷的数据类型是二进制类型的话,就继承 BinaryWebSocketHandler 类:

package com.example.websocket.component;

import org.springframework.stereotype.Component;
import org.springframework.web.socket.handler.TextWebSocketHandler;

@Component
public class TestWebSocketComponent extends TextWebSocketHandler {
}

TextWebSocketHandler 父类中的方法有很多:

在这里插入图片描述

我们需要重写的方法主要就是:afterConnectionEstablishedhandleTextMessagehandleTransportErrorafterConnectionClosed方法。

  • afterConnectionEstablished: 方法表示客户端和服务端建立 websocket 连接之后执行的方法
  • handleTextMessage:方法表示对方传来 websocket 数据帧的时候执行的方法
  • handleTransportError:方法表示 websocket 连接出现异常的时候执行的方法
  • afterConnectionClosed: 方法表示关闭 websocket 连接后执行的方法

重写完成 TextWebSocketHandler 类之后,就需要将这个实例给注册到 spring 中,进行路由的配置:

创建一个类,实现 WebSocketConfigurer 接口,并且实现接口中的 registerWebSocketHandlers 方法:

package com.example.websocket.config;


import com.example.websocket.component.TestWebSocketComponent;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.socket.config.annotation.EnableWebSocket;
import org.springframework.web.socket.config.annotation.WebSocketConfigurer;
import org.springframework.web.socket.config.annotation.WebSocketHandlerRegistry;

@Configuration
@EnableWebSocket
public class WebSocketConfig implements WebSocketConfigurer {
    @Autowired
    private TestWebSocketComponent testWebSocketComponent;
    @Override
    public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
        registry.addHandler(testWebSocketComponent,"/test");
    }
}

@EnableWebSocket 注解用于开启Spring应用程序对WebSocket协议的支持。

registry.addHandler() 方法中的参数分别上我们上面继承并重写了 TextWebSocketHandler 类中的方法的实例,而后面的字符串则是路由配置,当客户端发送的请求的路由定位到这个字符串的时候,就会执行 TextWebSocketHandler 中的对应方法。

当配置完成服务端的代码之后,我们来实现一个简单的页面:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>websocket测试</title>
</head>
<body>
    <input type="text" id="message">
    <button id="sendBtn">发送</button>
</body>
</html>

在这里插入图片描述
然后编写 js 这边的 websocket 代码:

<script>
    // 创建一个 websocket 实例
    // ws 是 websocket 的缩写,然后后面就是我们的服务器的ip地址以及前面服务端配置的路由地址
    let websocket = new WebSocket("ws://127.0.0.1/test");

    // 为 websocket 注册一些回调函数
    websocket.onopen = function() {
        // websocket 连接建立完成之后,自动执行到
        console.log('websocket 连接成功');
    }

    websocket.onclose = function() {
        // websocket 连接断开后,自动执行到
        console.log('websocket 连接断开');
    }

    websocket.onerror = function() {
        // websocket 连接异常时,自动执行到
        console.log('websocket 连接异常');
    }

    websocket.onmessage = function(e) {
        // 收到对端消息时,自动执行到
        console.log('websocket 收到消息' + e.data);
    }
</script>

这是 websocket 相关的 js 代码完成了,让后我们为在这个 button 创建一个事件:

let sendBtn = document.querySelector('#sendBtn');
sendBtn.onclick = function() {
    let input = document.querySelector('#message');
    websocket.send(input.value)
}

js 通过 WenSocket 的实例中的 send 方法向对端发送消息,而 spring 则通过类 WebSocketSession 实例中的 sendMessage 方法来向对端发送消息。

@Override
protected void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception {
    log.info("websocket 接收到消息:" + message.toString());
    //sendMessage方法中的参数是类WebSocketMessage及其子类
    session.sendMessage(message);
}

前后端发送的数据的数据类型是对象该如何做

在网络传输中,不存在什么对象这样的概念,如果传输的数据类型是对象的话,首先需要将对象转换为 json 字符串,然后再传输。

前端发送的消息的数据类型是对象的话,就需要将对象转换为 JSON 字符串,而我们前面使用 Ajax 的时候是因为 Ajax 帮助我们完成了 JSON 的转换,所以才不需要手动转换:

let req = {
    type: 'message',
    data: input.value
}
websocket.send(JSON.stringify(req));

然后后端通过 User user = objectMapper.readValue(message.asBytes(),User.class); 来将 json 字符串转换为 Java 对象。

如果后端发送的消息的数据类型是 Java 对象的话,就需要将 Java 对象转换为 json 字符串然后再发送:

@Override
protected void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception {
    log.info("websocket 接收到消息:" + message.toString());
    //向对端发送消息
    User user = new User();
    user.setUserId(1);
    user.setUserName("zhangsan");
    String respJson = objectMapper.writeValueAsString(user);
    session.sendMessage(new TextMessage(respJson));
}

然后前端通过 e = JSON.parse(e) 来将 json 字符串转换为对象。

在这里插入图片描述
在这里插入图片描述

使用websocket协议如何获取到HTTP协议中的HttpSession

在很多时候,当我们登录的时候,如果登录成功,往往会将用户信息存放在 session 会话中,而后面升级了 websocket 协议之后,如果我们需要使用到 session 中的信息该怎么办呢?这里 websocket 的开发者也想到了这里。当我们在注册 TextWebSocketHandler 的时候,再注册一个 HttpSession 拦截器就可以了,这样就可以把用户给 HttpSession 中添加的 Attributes 键值对往我们的 WebSocketSession 中也添加一份。

@Override
public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
    registry.addHandler(testWebSocketComponent,"/test")
            .addInterceptors(new HttpSessionHandshakeInterceptor());
}

在 spring 中,通过 WebSocketSession 的实例中的 getAttributes() 方法得到一个类似哈希表的结构,里面存放 HttpSession 中的设置的所有属性的键值对,然后再从这个哈希表结构中通过 get(key) 方法获取指定 key 的value。

@Override
protected void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception {
    log.info("websocket 接收到消息:" + message.toString());
    session.sendMessage(message);
    session.getAttributes().get("user");
}

WebSocket使用的完整代码

WebSocketComponent.java 中的代码

package com.example.websocket.component;

import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.web.socket.CloseStatus;
import org.springframework.web.socket.TextMessage;
import org.springframework.web.socket.WebSocketSession;
import org.springframework.web.socket.handler.TextWebSocketHandler;

@Slf4j
@Component
public class TestWebSocketComponent extends TextWebSocketHandler {
    @Override
    public void afterConnectionEstablished(WebSocketSession session) throws Exception {
        log.info("websocket 连接成功");
    }

    @Override
    protected void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception {
        log.info("websocket 接收到消息:" + message.toString());
        //向对端发送消息
        session.sendMessage(message);
        //获取HttpSession中的属性
        session.getAttributes().get("user");
    }

    @Override
    public void handleTransportError(WebSocketSession session, Throwable exception) throws Exception {
        log.info("websocket 连接异常:" + exception.toString());
    }

    @Override
    public void afterConnectionClosed(WebSocketSession session, CloseStatus status) throws Exception {
        log.info("websocket 断开连接");
    }
}

WebSocketConfig.java 中的代码:

package com.example.websocket.config;

import com.example.websocket.component.TestWebSocketComponent;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.socket.config.annotation.EnableWebSocket;
import org.springframework.web.socket.config.annotation.WebSocketConfigurer;
import org.springframework.web.socket.config.annotation.WebSocketHandlerRegistry;
import org.springframework.web.socket.server.support.HttpSessionHandshakeInterceptor;

@Configuration
@EnableWebSocket
public class WebSocketConfig implements WebSocketConfigurer {
    @Autowired
    private TestWebSocketComponent testWebSocketComponent;
    @Override
    public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
        registry.addHandler(testWebSocketComponent,"/test")
                .addInterceptors(new HttpSessionHandshakeInterceptor());
    }
}

js 中的代码:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>websocket测试</title>
</head>
<body>
    <input type="text" id="message">
    <button id="sendBtn">发送</button>

    <script>
        // 创建一个 websocket 实例
        // ws 是 websocket 的缩写,然后后面就是我们的服务器的ip地址以及前面服务端配置的路由地址
        let websocket = new WebSocket("ws://127.0.0.1:8080/test");

        // 为 websocket 注册一些回调函数
        websocket.onopen = function() {
            // websocket 连接建立完成之后,自动执行到
            console.log('websocket 连接成功');
        }

        websocket.onclose = function() {
            // websocket 连接断开后,自动执行到
            console.log('websocket 连接断开');
        }

        websocket.onerror = function() {
            // websocket 连接异常时,自动执行到
            console.log('websocket 连接异常');
        }

        websocket.onmessage = function(e) {
            // 收到对端消息时,自动执行到
            console.log('websocket 收到消息' + e.data);
        }

        let sendBtn = document.querySelector('#sendBtn');
        sendBtn.onclick = function() {
            let input = document.querySelector('#message');
            websocket.send(input.value)
        }
    </script>
</body>
</html>
Logo

助力广东及东莞地区开发者,代码托管、在线学习与竞赛、技术交流与分享、资源共享、职业发展,成为松山湖开发者首选的工作与学习平台

更多推荐