WebSocket的原理、作用、API、常见注解和生命周期的简单介绍,附带SpringBoot示例

news/2024/12/22 17:17:23/

文章目录

    • 原理
    • 作用
    • 客户端 API
    • 服务端 API
    • 生命周期
    • 常见注解
    • SpringBoot示例

WebSocket是一种 通信协议 ,它在 客户端和服务器之间建立了一个双向通信的网络连接 。WebSocket是一种基于TCP连接上进行 全双工通信协议

WebSocket允许客户端和服务器在 单个TCP连接上 进行 实时 双向通信,而不是传统的 请求-响应模型 。WebSocket协议允许 任意多的数据包任意方向上 发送,因此可以实现 实时的双向的交互式的 数据传输。

原理

下图是使用轮询或WebSocket两种方式向服务器发起请求的简易流程。
轮询和WebSocket

上图的左边图是 轮询 的方式进行获取信息,可以看出在 一定时间间隔 的情况下客户端向服务端发起请求,如果有数据则返回数据,没有数据则返回空数据。

右边图则是使用WebSocket方式,在 一次握手 之后,两者就可以 创建持久性的连接 ,如果服务端有数据的话,就可以实时返回数据。


下图是使用WebSocket的方式向服务器发起请求的详细流程。
WebSocket的原理图
该图解释了WebSocket连接原理。下面是上图的HTTP协议中的请求和响应数据。

请求数据:

  • GET ws://localhost/chat HTTP/1.1
  • Host: localhost
  • Connection: Upgrade
  • Upgrade: websocket
  • Sec-WebSocket-Version: 13
  • Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==Sec-WebSocket-Extensions: permessage-deflate

响应数据:

  • HTTP/1.1 101 Switching Protocols
  • Upgrade: websocket
  • Connection: Upgrade
  • Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=Sec-WebSocket-Extensions: permessage-deflate

作用

  • 实时数据传输:WebSocket允许服务端主动向客户端推送数据。在WebSocket API中,浏览器和服务器只需要 完成一次握手 ,两者之间就可以直接创建 持久性的连接 ,并进行 双向数据传输
  • 实时通知:WebSocket可以保持一个 长连接,当服务器端有新的消息时,能够实时地推送到客户端。这使得诸如知乎的点赞通知、评论等实时交互功能得以实现,从而提供更好的用户体验。
  • 数据收集:一些 次优级别的数据 ,如行为日志、trace、异常执行栈收集等,可以通过WebSocket通道进行传输。这增加了信息的集中度,并能及时针对用户的行为进行合适的配置推送。

WebSocket通常用于 实时Web应用程序 ,如聊天室、游戏、股票价格跟踪等,这些应用需要 实时交互数据更新

但是在实际使用中仍需注意其安全性。例如,某些工具(如Fiddler、Charles等)能够 捕获WebSocket包 ,但这并不意味着WebSocket本身不安全。在使用WebSocket时,应确保采取适当的安全措施,如加密和认证,以保护数据的传输和存储。


全双工和半双工是什么?

全双工(Full Duplex)和半双工(Half Duplex)是 通讯传输 中的两种不同模式,主要区别在于 数据在通信线路上的传输方向和方式

  • 全双工允许数据在 两个方向上同时传输 ,即通信线路可以同时存在 双向信号传输 。这意味着在发送数据的同时,也能接收数据,两者是同步进行的。这种模式类似于我们平时打电话,说话的同时也能听到对方的声音。全双工方式下,通信系统的每一端都设置了 发送器和接收器 ,因此能控制数据同时在两个方向上传送。
  • 半双工模式允许 数据在一个信号载体的两个方向上传输,但不能同时传输 。虽然数据可以沿两个方向传送,但在同一时刻,一个信道只允许单方向传送。若要改变传输方向,需由开关进行切换。半双工通信也被称为 双向交替通信 ,即发送和接收动作是交替进行的,不能同时进行。例如,一条窄窄的马路,同时只能有一辆车通过,当有两辆车对开时,只能一辆先过,另一辆再开。

全双工和半双工的主要区别在于数据传输的同步性和方向性。全双工能实现数据的瞬时同步双向传输,而半双工则只能实现双向交替传输。


客户端 API

let  ws  =  new WebSocket(URL);  // WebSocket对象创建
  • 格式:协议://ip地址/访问路径
  • 协议:协议名称为 ws

WebSocket对象相关事件:

事件事件处理程序描述
openws.onopen连接建立时触发
messagews.onmessage客户端接收到服务器发送的数据时触发
closews.onclose连接关闭时触发

WebSocket对象提供的方法:

方法名称描述
send()通过websocket对象调用该方法发送数据给服务端

前端客户端使用方式:
<a class=websocket" />

服务端 API

Tomcat的7.0.5 版本开始支持WebSocket,并且实现了Java WebSocket规范。Java WebSocket 应用由一系列的Endpoint 组成。Endpoint 是一个java对象,代表WebSocket链接的一端,对于服务端,我们可以视为处理具体WebSocket 消息的接口。

我们可以通过两种方式定义 Endpoint

  • 第一种是编程式,即继承类 javax.websocket.Endpoint 并实现其方法。
  • 第二种是注解式,即定义一个POJO,并添加 @ServerEndpoint 相关注解。

Endpoint实例 在 WebSocket 握手 时创建,并在客户端与服务端链接过程中有效,最后在链接关闭时结束。在 Endpoint接口 中明确定义了与其生命周期相关的方法, 规范实现者确保生命周期的各个阶段调用实例的相关方法。

生命周期方法如下:

方法描述注解
onOpen()当开启一个新的会话时调用,该方法是客户端与服务端握手成功后调用的方法@OnOpen
onClose()当会话关闭时调用@OnClose
onError()当连接过程异常时调用@OnError

服务端如何接收客户端发送的数据呢?

  • 编程式:通过添加 MessageHandler 消息处理器来接收消息
  • 注解式:在定义 Endpoint 时,通过 @OnMessage 注解指定接收消息的方法

服务端如何推送数据给客户端呢?

发送消息则由 RemoteEndpoint 完成, 其实例由 Session 维护。发送消息有2种方式发送消息:

  • 通过 session.getBasicRemote 获取同步消息发送的实例 , 然后调用其 sendXxx() 方法发送消息。
  • 通过 session.getAsyncRemote 获取异步消息发送实例,然后调用其 sendXxx() 方法发送消息。

服务端使用方式:
在这里插入图片描述

生命周期

该图中右上角应为@OnClose。
在这里插入图片描述

常见注解

JavaWebSocket API 中,常见的注解包括以下几种:

  • @ServerEndpoint :该类级别的注解,用于标记一个类并将其声明为WebSocket服务端点。这意味着此类会被部署为WebSocket服务器并在WebSocket实现的URI空间中可用。
  • @OnOpen :用于标识一个方法,当WebSocket连接建立时被调用。这个方法可以包含连接建立时需要执行的逻辑。
  • @OnClose :用于标识一个方法,当WebSocket连接关闭时被调用。此方法中可以执行与关闭连接相关的操作或资源释放。
  • @OnMessage :用于标识一个方法,在接收到客户端消息时被调用。此方法可以接收不同类型的参数,如String或ByteBuffer,用于处理从客户端接收到的消息。
  • @OnError :用于标识一个方法,在WebSocket通信过程中发生错误时被调用。此方法可以用于处理错误情况,如连接失败、消息解析错误等。

此外,还有一些其他的注解可以用于指定WebSocket的配置,例如:

  • encoders :编码器属性,用于指定将消息从Java对象转换为WebSocket消息格式的编码器。
  • decoders :解码器属性,用于指定将WebSocket消息格式解码为Java对象的解码器。
  • subprotocols :子协议属性,用于指定WebSocket连接支持的子协议。
  • configurator :配置器属性,用于指定WebSocket端点的配置器,可以对端点进行更高级的配置。

以上列出的是基于 Java WebSocket API 的常见注解。如果使用其他编程语言或框架,注解可能会有所不同。

SpringBoot示例

这是一个简单示例,主要了解在SpringBoot中怎么使用WebSocket。

首先,需要在SpringBoot项目中引入依赖

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

注册WebSocket

@Configuration
public class WebsocketConfig {@Beanpublic ServerEndpointExporter serverEndpointExporter() {return new ServerEndpointExporter();}
}

用来封装http请求的响应数据

@Data
public class Result {private boolean flag;private String message;
}

用于接收登录请求的数据

@Data
public class User {private String userId;private String username;private String password;
}

用于封装浏览器发送给服务端的消息数据

@Data
public class Message {private String toName;private String message;
}

用来封装服务端给浏览器发送的消息数据

@Data
public class ResultMessage {private boolean isSystem;private String fromName;private Object message; //如果是系统消息是数组
}

定义ChatEndpoint 类用于处理WebSocket中数据传输的逻辑,生命周期等功能。

@ServerEndpoint(value = "/chat",configurator = GetHttpSessionConfig.class)
@Component
public class ChatEndpoint {// 线程安全private static final Map<String,Session> onlineUsers = new ConcurrentHashMap<>();private HttpSession httpSession;// 建立websocket连接后,被调用@OnOpenpublic void onOpen(Session session, EndpointConfig config) {//1,将session进行保存this.httpSession = (HttpSession) config.getUserProperties().get(HttpSession.class.getName());String user = (String) this.httpSession.getAttribute("user");onlineUsers.put(user,session);//2,广播消息。需要将登陆的所有的用户推送给所有的用户String message = MessageUtils.getMessage(true,null,getFriends());broadcastAllUsers(message);}public Set getFriends() {Set<String> set = onlineUsers.keySet();return set;}private void broadcastAllUsers(String message) {try {//遍历map集合Set<Map.Entry<String, Session>> entries = onlineUsers.entrySet();for (Map.Entry<String, Session> entry : entries) {//获取到所有用户对应的session对象Session session = entry.getValue();//发送消息session.getBasicRemote().sendText(message);}} catch (Exception e) {//记录日志}}// 浏览器发送消息到服务端,该方法被调用@OnMessagepublic void onMessage(String message) {try {//将消息推送给指定的用户Message msg = JSON.parseObject(message, Message.class);//获取 消息接收方的用户名String toName = msg.getToName();String mess = msg.getMessage();//获取消息接收方用户对象的session对象Session session = onlineUsers.get(toName);String user = (String) this.httpSession.getAttribute("user");String msg1 = MessageUtils.getMessage(false, user, mess);session.getBasicRemote().sendText(msg1);} catch (Exception e) {//记录日志}}// 断开 websocket 连接时被调用@OnClosepublic void onClose(Session session) {// 1,从onlineUsers中剔除当前用户的session对象String user = (String) this.httpSession.getAttribute("user");onlineUsers.remove(user);// 2,通知其他所有的用户,当前用户下线了String message = MessageUtils.getMessage(true,null,getFriends());broadcastAllUsers(message);}
}

由于使用user name作为Map的key,所以要实时保存,用于消息的传递标识。如果使用数据库,建议还是使用userId作为key,保证唯一性。

public class GetHttpSessionConfig extends ServerEndpointConfig.Configurator {@Overridepublic void modifyHandshake(ServerEndpointConfig sec, HandshakeRequest request, HandshakeResponse response) {//获取HttpSession对象HttpSession httpSession = (HttpSession) request.getHttpSession();//将httpSession对象保存起来sec.getUserProperties().put(HttpSession.class.getName(),httpSession);}
}

controller请求类

@RestController
@RequestMapping("user")
public class UserController {/*** 登陆* @param user 提交的用户数据,包含用户名和密码* @param session* @return*/@PostMapping("/login")public Result login(@RequestBody User user, HttpSession session) {Result result = new Result();if(user != null && "123".equals(user.getPassword())) {result.setFlag(true);//将数据存储到session对象中session.setAttribute("user",user.getUsername());} else {result.setFlag(false);result.setMessage("登陆失败");}return result;}/*** 获取用户名* @param session* @return*/@GetMapping("/getUsername")public String getUsername(HttpSession session) {String username = (String) session.getAttribute("user");return username;}
}

项目目录结构:
在这里插入图片描述


http://www.ppmy.cn/news/1433680.html

相关文章

Axure引用ECharts图表 解决火狐浏览器出错

Axure原型添加Echarts图表&#xff0c;没耐心看文章的可以直接下载示例 Axure中使用ECharts图表示例 1. 打开Axure新建页面 2. 页面添加元件 元件类型随意&#xff0c;矩形、动态面板、热区、图片 甚至段落都可以3. 命名元件 随意命名&#xff0c;单个页面用到多个图表时名…

18-HW_PWM(硬件改变 PWM 占空比)

ESP32-S3的HW_PWM详解 引言 硬件脉冲宽度调制&#xff08;HW_PWM&#xff09;是ESP32-S3的一个重要特性&#xff0c;它可以用于控制LED的亮度、电机的速度等。硬件脉冲宽度调制与软件改变 PWM 占空比实验的驱动一致 软件改变 PWM 占空比 硬件脉冲宽度调制和软件脉冲宽度调制…

使用linux,c++,创作一个简单的五子棋游戏

#include <iostream> #include <vector> #include <unordered_map> using namespace std; // 棋盘大小 const int BOARD_SIZE 15; // 棋子类型 enum ChessType { EMPTY, BLACK, WHITE }; // 棋盘类 class ChessBoard { private: vect…

QML中使用正则表达式

我想在TextField控件中使用正则表达式&#xff0c;然后GPT4给出的回答是这样的&#xff1a; TextField {id: versionInputplaceholderText: qsTr("输入版本号")validator: RegExpValidator { regExp: /^[a-zA-Z0-9]*$/ } // 仅允许字母和数字width: 120 // 设置合…

MapReduce排序机制(Hadoop)

在MapReduce中&#xff0c;排序的目的是为了方便Reduce阶段的处理&#xff0c;通常是为了将相同键的键值对聚合在一起&#xff0c;以便进行聚合操作或其他处理。 1. Map阶段的局部排序&#xff08;Local Sorting&#xff09;&#xff1a; 对于MapTask&#xff0c;它会将处理的…

nvm使用指定镜像安装node和npm包

场景 使用nvm时&#xff0c;默认的安装源经常碰到找不到可用版本的问题&#xff0c;这时就需要指定镜像源。比如如果你在学习鸿蒙ArkTs项目的开发&#xff0c;就需要指定从华为官方镜像上安装指定版本的node和npm包 命令 以windows为例&#xff0c;以管理员身份运行cmd工具&…

Android UI底层绘制原理

自定义View继承自View类&#xff0c;需要重写onDraw方法&#xff0c;通过canvas和paint进行绘制 Overrideprotected void onDraw(Canvas canvas) {super.onDraw(canvas);canvas.drawLine();} public void drawLine(float startX, float startY, float stopX, float stopY,NonN…

C++ | Leetcode C++题解之第45题跳跃游戏II

题目&#xff1a; 题解&#xff1a; class Solution { public:int jump(vector<int>& nums) {int maxPos 0, n nums.size(), end 0, step 0;for (int i 0; i < n - 1; i) {if (maxPos > i) {maxPos max(maxPos, i nums[i]);if (i end) {end maxPos;s…