WebSocket 简单连接,基础小 Demo,SpringBoot下

本文详细介绍了WebSocket协议的基本概念及其与SpringBoot框架的集成方法。探讨了WebSocket的全双工特性,如何减少控制开销,提升实时性和二进制支持。文中深入解析了WebSocket在Java中的实现细节,包括非单例的特性、手动注入ApplicationContext的应用上下文获取bean、配置类的设置以及解码器的创建。同时,展示了如何通过切面编程在特定业务场景下利用WebSocket推送更新。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

webSocket demo (webSocket/SpringBoot/myBatis)

记录 WebSocket 学习过程

WebSocket

一种由HTML5 提供的 在单个 TCP 连接上进行的全双工通讯协议。将客户端和服务端之间的数据交换变得简单,允许服务端主动向客户端推送数据。

在 WebSocket API 中,浏览器和服务器只需要完成一次握手即可,两者之间可创建持久性的连接,并进行双向数据传输。

默认使用 80 端口

优势
  • 较少的控制开销:减少了以前HTTP每次请求的请求头长问题,节省了宽带资源
  • 更强的实时性:服务端随时可以主动发送数据给客户端,减少了HTTP的客户端请求后再返回响应
  • 二进制支持:WebSocket 定义了 二进制 帧,相对HTTP,可以更轻松地处理二进制内容
  • 可以支持扩展:WebSocket 定义了扩展,用户可以扩展协议、实现部分自定义的子协议
协议
  • WebSocket 是独立的、创建在 TCP 上的协议
  • WebSocket 通过 HTTP/1.1 协议的 101 状态码进行握手
创建
// 需要先做验证,判断浏览器是否支持
if("WebSocket" in window) {}
// 创建:url,子协议(自定义协议)
var ws = new WebSocket(url,[子协议]);
属性
  • ws.readyState

    • 0 - 连接尚未建立
    • 1 - 表示连接已建立,可以通信
    • 2 - 连接正在关闭
    • 3 - 连接已关闭或连接不能打开
  • ws.bufferedAmount

    • 表示只读属性 bufferedAmount 已被 send() 放入正在队列中等待传输,但是还没有发出 UTF-8 文本字节数
事件
  • open
    • ws.onopen - 连接建立时触发
  • message
    • ws.onmessage - 客户端接收服务端数据时触发
  • error
    • ws.onerror - 通信发生错误时触发
  • onclose
    • ws.onclose - 连接关闭时触发
方法
  • send()
    • ws.send() - 使用连接发送数据
  • close()
    • ws.close() - 关闭连接
JS 代码
var ws;

function onopen() {
    var url = $("#url").val();
    ws = new WebSocket(url);
    ws.onopen = function (ev) {

    }
    ws.onmessage = function (ev) {
        $("#msg").text(ev.data);
    }
    ws.onclose = function () {
        $("#msg").text("");
    }
    ws.onerror = function (ev) {

    }
}

function wsclose() {
    ws.close();
}
Java 代码

1、WebSocket 是非单例的,初始化时注入 bean 成功,后新用户添加,创建新的对象时,不会再注入,所以不能简单的属性注入/构建注入等

  • 带注入属性 静态,@Autowired 在 set() 方法上
  • 手动注入 ApplicationContext 应用上下文获取 bean

2、sendObject() 发送数据时需要解码器,发送的数据为 JsonCommand,需要创建 JsonCommandEncoder 类

3、WebSocket 的容器管理,需要配置config, 注入 bean ServerEndpointExporter,会寻找 @ServerEndpoint 注解,注入 bean

@ServerEndpoint(value = "/websocket", encoders = {JsonCommandEncoder.class})
@Component
public class WebSocketServlet {

    /** 线程安全的,带lock锁,JUC中的 */
    private static CopyOnWriteArraySet<WebSocketServlet> webSocketSet = new CopyOnWriteArraySet<>();

    private Session session = null;

    /**
     * spring bean 默认单例,初始化时注入一次,而 websocket 非单例的,初始化时能注入成功,
     * 后当有新用户加入,将创建新的 websocket 对象,不会再注入,即获取到的 service/dao 将时 null 的
     *
     * 1.以下是一种可行方法,静态的类,通过 set 注入 service,set 方法不能为静态
     * */
    private static IUserService userService;

    @Autowired
    public void setUserService(IUserService userService) {
        WebSocketServlet.userService = userService;
    }
    
    /**
     * 2.这是一种 websocket 注入 bean 的方法 实现 ApplicationContextAware 继承ServerEndpointConfig.Configurator
     * 通过手动注入 applicationContext 应用上下文获取 bean
     * ApplicationContextAware 让 bean 获取到 spring 容器,操作其中的实例,其本身并没有特殊功能,实现继承后传入 ApplicationContext
     * */
    private static volatile BeanFactory context;

    @Override
    public <T> T getEndpointInstance(Class<T> clazz) throws InstantiationException {
        return context.getBean(clazz);
    }

    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        WebSocketServletBean.context = applicationContext;
    }

    /**
     * 方法2注入 bean 的获取
     * */
    public IUserService getUserService() throws InstantiationException {
        return this.getEndpointInstance(IUserService.class);
    }

    /**
     * 开启连接
     * */
    @OnOpen
    public void onOpen(Session session) {
        this.session = session;
        webSocketSet.add(this);
        System.out.println("OnOpen:" + session.getId());
        try {
            sendGroupMessage(session.getId() + ":onOpen");
            sendSingleObject();
        } catch (EncodeException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    /**
     * 断开连接
     * */
    @OnClose
    public void onClose() {
        System.out.println("OnClose:" + session.getId());
        webSocketSet.remove(this);
        try {
            sendGroupMessage(session.getId() + ":onColse");
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    /**
     * 处理消息
     * */
    @OnMessage
    public void onMessage(String message) {
        System.out.println("OnMessage:" + session.getId());
        System.out.println("message:" + message);
        try {
            sendGroupMessage(message);
        } catch (IOException e) {
            try {
                sendSingleMessage(message + "-->发送失败");
            } catch (IOException e1) {
                e1.printStackTrace();
            }
            e.printStackTrace();
        }
    }

    /**
     * 出现错误
     * */
    @OnError
    public void onError(Throwable error) {
        System.out.println("OnError:" + error);
        error.printStackTrace();
    }

    private void sendSingleMessage(String message) throws IOException {
        this.session.getBasicRemote().sendText(message);
    }

    private void sendGroupMessage(String message) throws IOException {
        if (!StringUtils.isEmpty(message)) {
            for (WebSocketServlet wss : webSocketSet) {
                if (wss.equals(this)) {
                    continue;
                }
                wss.session.getBasicRemote().sendText(message);
            }
        }
    }

    /**
     * 直接发送会有问题 - No encoder specified for object of class
     * 需要创建编码器 - JsonCommandEncoder(内部解释)
     * */
    private void sendSingleObject() throws IOException, EncodeException {
        JsonCommand jsonCommand = userService.selectUser();
        this.session.getBasicRemote().sendObject(jsonCommand);
    }

    /**
     * 在切点调用成功后执行
     * */
    public void sendGroupObject() throws IOException, EncodeException {
        JsonCommand jsonCommand = userService.selectUser();
        for (WebSocketServlet wss : webSocketSet) {
            wss.session.getBasicRemote().sendObject(jsonCommand);
        }
    }

    @Test
    public void wssession() throws IOException, EncodeException {
        // websocket  session发送消息

        // 1.getAsyncRemote 异步远程,非阻塞式的
        RemoteEndpoint.Async asyncRemote = this.session.getAsyncRemote();
        asyncRemote.sendText("");
        // 可发送回调信息 SendHandler
        asyncRemote.sendObject(new Object());


        // 2.getBasicRemote 同步远程,阻塞式的
        RemoteEndpoint.Basic basicRemote = this.session.getBasicRemote();
        basicRemote.sendText("");
        // 是否消息的最后一部分
        basicRemote.sendText("", true);
        basicRemote.sendObject(new Object());
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) {
            return true;
        }
        if (o == null || getClass() != o.getClass()) {
            return false;
        }
        WebSocketServlet that = (WebSocketServlet) o;
        return Objects.equals(session, that.session);
    }

    @Override
    public int hashCode() {

        return Objects.hash(session);
    }
}

解码器

/**
 * 在 websocket 中直接发送 obj 会有问题 - No encoder specified for object of class
 * 需要对 obj 创建解码类,实现 websocket 中的 Encoder.Text<>
 * */
public class JsonCommandEncoder implements Encoder.Text<JsonCommand> {
    /**
     * The Encoder interface defines how developers can provide a way to convert their
     * custom objects into web socket messages. The Encoder interface contains
     * subinterfaces that allow encoding algorithms to encode custom objects to:
     * text, binary data, character stream and write to an output stream.
     *
     * Encoder 接口定义了如何提供一种方法将定制对象转换为 websocket 消息
     * 可自定义对象编码为文本、二进制数据、字符流、写入输出流
     *  Text、TextStream、Binary、BinaryStream
     * */
    @Override
    public String encode(JsonCommand object) throws EncodeException {
        return JSON.toJSONString(object);
    }

    @Override
    public void init(EndpointConfig endpointConfig) {

    }

    @Override
    public void destroy() {

    }
}

配置类

@Configuration
public class WebSocketconfig {
    /**
     * 关闭 servlet 容器对 websocket端点的扫描
     * When this class is used, by declaring it in Spring configuration, it should be
     * possible to turn off a Servlet container's scan for WebSocket endpoints.
     * 检测带 ServerEndpoint 注解的 bean 并注入
     * Also detects beans annotated with ServerEndpoint and registers them as well
     * */
    @Bean
    public ServerEndpointExporter serverEndpointExporter() {
        return new ServerEndpointExporter();
    }
}
针对功能

WebSocket 连接成功后,获取 User 用户信息,增加切面类,当用户类有变动, UserDao 的某些方法执行,即推送新数据给 webSocket 连接用户

/**
 * spring-boot 默认开启切面自动代理,不需要手动注入
 * */
@Component
@Aspect
public class WebSocketAspect {

    @Autowired
    private WebSocketServlet webSocketServlet;

    @Pointcut("execution(* com.example.websocket_demo.dao.UserDao.delete(..))" +
            "|| execution(* com.example.websocket_demo.dao.UserDao.insert(..))" +
            "|| execution(* com.example.websocket_demo.dao.UserDao.update(..))")
    public void userInfoChange() {}

    @AfterReturning(pointcut = "userInfoChange()", returning = "ret")
    public void send(Object ret) throws IOException, EncodeException {
        // 执行的insert/update/delete方法成功后,返回值为1的,才执行
        Integer num = 1;
        if (num.equals(ret)) {
            webSocketServlet.sendGroupObject();
        }
    }
}



项目地址:https://gitee.com/fjigww/websocket_Demo
数据库连接修改后,可 test 下 DaoTest直接添加随机数据

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值