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

文章目录

WebSocket是一种 通信协议 ,它在 客户端和服务器之间建立了一个双向通信的网络连接 。WebSocket是一种基于TCP连接上进行 全双工通信协议
WebSocket允许客户端和服务器在 单个TCP连接上 进行 实时 双向通信,而不是传统的 请求-响应模型 。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

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

WebSocket对象相关事件:

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

WebSocket对象提供的方法:

方法名称 描述
send() 通过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项目中引入依赖

xml 复制代码
<dependency>
	<groupId>org.springframework.boot</groupId>
	<artifactId>spring-boot-starter-websocket</artifactId>
</dependency>

注册WebSocket

java 复制代码
@Configuration
public class WebsocketConfig {
    @Bean
    public ServerEndpointExporter serverEndpointExporter() {
        return new ServerEndpointExporter();
    }
}

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

java 复制代码
@Data
public class Result {
    private boolean flag;
    private String message;
}

用于接收登录请求的数据

java 复制代码
@Data
public class User {

    private String userId;
    private String username;
    private String password;
}

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

java 复制代码
@Data
public class Message {
    private String toName;
    private String message;
}

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

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

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

java 复制代码
@ServerEndpoint(value = "/chat",configurator = GetHttpSessionConfig.class)
@Component
public class ChatEndpoint {
	// 线程安全
    private static final Map<String,Session> onlineUsers = new ConcurrentHashMap<>();
    private HttpSession httpSession;

    // 建立websocket连接后,被调用
    @OnOpen
    public 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) {
            //记录日志
        }
    }

    // 浏览器发送消息到服务端,该方法被调用
    @OnMessage
    public 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 连接时被调用
    @OnClose
    public 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,保证唯一性。

java 复制代码
public class GetHttpSessionConfig extends ServerEndpointConfig.Configurator {

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

controller请求类

java 复制代码
@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;
    }
}

项目目录结构:

相关推荐
群联云防护小杜10 分钟前
如何给负载均衡平台做好安全防御
运维·服务器·网络·网络协议·安全·负载均衡
ihengshuai10 分钟前
HTTP协议及安全防范
网络协议·安全·http
武昌库里写JAVA23 分钟前
Java成长之路(一)--SpringBoot基础学习--SpringBoot代码测试
java·开发语言·spring boot·学习·课程设计
Q_192849990630 分钟前
基于Spring Boot的九州美食城商户一体化系统
java·spring boot·后端
Q_192849990634 分钟前
基于Spring Boot的营销项目系统
spring boot
路在脚下@3 小时前
Spring Boot @Conditional注解
java·spring boot·spring
陶庵看雪3 小时前
Spring Boot注解总结大全【案例详解,一眼秒懂】
java·spring boot·后端
Q_19284999063 小时前
基于Spring Boot的图书管理系统
java·spring boot·后端
ss2734 小时前
基于Springboot + vue实现的汽车资讯网站
vue.js·spring boot·后端
a_weng084 小时前
CS 144 check6: buiding an IP router
网络·网络协议·计算机网络