首页 > 其他分享 >SpringBoot搭建webSocket长链接,实现双向实时通信

SpringBoot搭建webSocket长链接,实现双向实时通信

时间:2024-10-22 15:48:00浏览次数:3  
标签:websocket SpringBoot public token example import 客户端 链接 webSocket

很多网站为了实现推送技术,所用的技术都是轮询。轮询是在特定的时间间隔(如每1秒),由浏览器对服务器发出HTTP请求,然后由服务器返回最新的数据给客户端的浏览器。这种传统的模式带来很明显的缺点,即浏览器需要不断的向服务器发出请求,然而HTTP请求可能包含较长的头部,其中真正有效的数据可能只是很小的一部分,显然这样会浪费很多的带宽等资源。

而比较新的技术去做轮询的效果是Comet。这种技术虽然可以双向通信,但依然需要反复发出请求。而且在Comet中,普遍采用的长链接,也会消耗服务器资源。

在这种情况下,HTML5定义了WebSocket协议,能更好的节省服务器资源和带宽,并且能够更实时地进行通讯。

首选需要配置一个客户端的实体类

package com.example.websocket.entity;

import lombok.Data;

import javax.websocket.Session;
import java.time.LocalDateTime;

/**
 * 客户端实体类
 *
 * @author abdul
 * @since 2024/08/29 19:50
 */
@Data
public class ClientInfoEntity {

    /**
     * 客户端唯一标识
     */
    private String token;
    /**
     * 客户端连接的session
     */
    private Session session;
    /**
     * 连接存活时间
     */
    private LocalDateTime existTime;
}

 创建 ChatEndpoint2  该类负责监听客户端的连接、断开连接、接收消息、发送消息等操作

package com.example.websocket.util;

import cn.hutool.core.util.ObjectUtil;
import com.example.common.easyExcel.exception.ExcelException;
import com.example.websocket.config.GetHttpSessionConfig;
import com.example.websocket.entity.ClientInfoEntity;
import com.example.websocket.exception.ServiceException;
import lombok.extern.slf4j.Slf4j;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
import org.springframework.web.bind.annotation.CrossOrigin;

import javax.annotation.PostConstruct;
import javax.websocket.*;
import javax.websocket.server.PathParam;
import javax.websocket.server.ServerEndpoint;
import java.io.IOException;
import java.text.SimpleDateFormat;
import java.time.LocalDateTime;
import java.util.Date;
import java.util.Iterator;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

/**
 * 该类负责监听客户端的连接、断开连接、接收消息、发送消息等操作。
 *
 * @author abdul
 * @since 2024/08/29 19:50
 */
@Slf4j
@Component
@CrossOrigin(origins = "*")
@ServerEndpoint(value = "/webSocket/{token}", configurator = GetHttpSessionConfig.class)
public class ChatEndpoint2 {
    //key:客户端连接唯一标识(token)
    //value:ClientInfoEntity
    public static final Map<String, ClientInfoEntity> uavWebSocketInfoMap = new ConcurrentHashMap<String, ClientInfoEntity>();

    private static final int EXIST_TIME_HOUR = 6;

    /**
     * 连接建立成功调用的方法
     *
     * @param session 第一个参数必须是session
     * @param sec
     * @param token   代表客户端的唯一标识
     */
    @OnOpen
    public void onOpen(Session session, EndpointConfig sec, @PathParam("token") String token) {
        if (uavWebSocketInfoMap.containsKey(token)) {
            throw new ServiceException("token已建立连接");
        }
        //把成功建立连接的会话在实体类中保存
        ClientInfoEntity entity = new ClientInfoEntity();
        entity.setToken(token);
        entity.setSession(session);
        //默认连接6个小时
        entity.setExistTime(LocalDateTime.now().plusHours(EXIST_TIME_HOUR));
        uavWebSocketInfoMap.put(token, entity);
        //之所以获取http session 是为了获取获取httpsession中的数据 (用户名 /账号/信息)
//        sendMessageFromConsole();
        log.info("WebSocket 连接建立成功: " + token);
    }

    /**
     * 当断开连接时调用该方法
     *
     * @param session
     */
    @OnClose
    public void onClose(Session session, @PathParam("token") String token) {
        // 找到关闭会话对应的用户 ID 并从 uavWebSocketInfoMap 中移除
        if (ObjectUtil.isNotEmpty(token) && uavWebSocketInfoMap.containsKey(token)) {
            uavWebSocketInfoMap.remove(token);
            log.info("WebSocket 连接关闭成功: " + token);
        }
    }

    /**
     * 接受消息
     * 这是接收和处理来自用户的消息的地方。我们需要在这里处理消息逻辑,可能包括广播消息给所有连接的用户。
     *
     */
    @OnMessage
    public void onMessage( @PathParam("token") String token, String message) throws IOException {
        ClientInfoEntity entity = uavWebSocketInfoMap.get(token);
        if (entity==null){
         throw new ExcelException("token:"+token+"连接断开");
        }
        //业务逻辑
        //只要接受到客户端的消息就进行续命(时间)
        entity.setExistTime(LocalDateTime.now().plusHours(EXIST_TIME_HOUR));
        if (entity.getSession().isOpen()) {
                entity.getSession().getBasicRemote().sendText(message);

        }
    }

    /**
     * 处理WebSocket中发生的任何异常。可以记录这些错误或尝试恢复。
     */
    @OnError
    public void one rror(Throwable error) {
        log.error("报错信息:" + error.getMessage());
        error.printStackTrace();

    }

    private static final SimpleDateFormat FORMAT = new SimpleDateFormat("yyyy:MM:dd hh:mm:ss");

    /**
     * 发生消息定时器
     */
    @PostConstruct
    @Scheduled(cron = "0/1 * *  * * ? ")
    public void refreshDate() {
        //开启定时任务,1秒一次向前台发送当前时间
        //当没有客户端连接时阻塞等待
        System.out.println(new Date());
        if (!uavWebSocketInfoMap.isEmpty()) {
            //超过存活时间进行删除
            Iterator<Map.Entry<String, ClientInfoEntity>> iterator = uavWebSocketInfoMap.entrySet().iterator();
            while (iterator.hasNext()) {
                Map.Entry<String, ClientInfoEntity> entry = iterator.next();
                if (entry.getValue().getExistTime().compareTo(LocalDateTime.now()) <= 0) {
                    log.info("WebSocket " + entry.getKey() + " 已到存活时间,自动断开连接");
                    try {
                        entry.getValue().getSession().close();
                    } catch (IOException e) {
                        log.error("WebSocket 连接关闭失败: " + entry.getKey() + " - " + e.getMessage());
                    }
                    //过期则进行移除
                    iterator.remove();
                }
            }
            sendMessage(FORMAT.format(new Date()));
        }
    }

    /**
     * 群发信息的方法
     *
     * @param message 消息
     */
    public void sendMessage(String message) {
        System.out.println(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date())
                + "发送全体消息:" + message);
        //循环客户端map发送消息
        uavWebSocketInfoMap.values().forEach(item -> {
            //向每个用户发送文本信息。这里getAsyncRemote()解释一下,向用户发送文本信息有两种方式,
            // 一种是getBasicRemote,一种是getAsyncRemote
            //区别:getAsyncRemote是异步的,不会阻塞,而getBasicRemote是同步的,会阻塞,由于同步特性,第二行的消息必须等待第一行的发送完成才能进行。
            // 而第一行的剩余部分消息要等第二行发送完才能继续发送,所以在第二行会抛出IllegalStateException异常。所以如果要使用getBasicRemote()同步发送消息
            // 则避免尽量一次发送全部消息,使用部分消息来发送,可以看到下面sendMessageToTarget方法内就用的getBasicRemote,因为这个方法是根据用户id来私发的,所以不是全部一起发送。
            item.getSession().getAsyncRemote().sendText(message);
        });
    }

}

创建 GetHttpSessionConfig 类,主要用于WebSocket的握手配置

package com.example.websocket.config;

import org.springframework.context.annotation.Configuration;
import org.springframework.web.socket.config.annotation.EnableWebSocket;

import javax.servlet.http.HttpSession;
import javax.websocket.HandshakeResponse;
import javax.websocket.server.HandshakeRequest;
import javax.websocket.server.ServerEndpointConfig;
import java.util.Map;
import java.util.UUID;

/**
 * 主要用于WebSocket的握手配置
 * @author abdul
 * @since 2024/08/29 19:55
 */
@Configuration
@EnableWebSocket
public class GetHttpSessionConfig extends ServerEndpointConfig.Configurator {
    /**
     * 注意:  每一个客户端发起握手,端点就有一个新的实列,那么引用的这个配置也是新的实列,这里sec的用户属性也不同就不会产生冲突。
     * 修改握手机制  就是第一次http发送过来的握手
     * @param sec   服务器websocket端点的配置
     * @param request
     * @param response
     */
    @Override
    public void modifyHandshake(ServerEndpointConfig sec, HandshakeRequest request, HandshakeResponse response) {
//        将从握手的请求中获取httpsession
        HttpSession httpSession =(HttpSession) request.getHttpSession();


        /**
         * 一般会在请求头中添加token 解析出来id作为键值对
         */
        Map<String, Object> properties = sec.getUserProperties();
        /**
         * 一个客户端和和服务器发起一次请求交互 就有一个唯一session
         * 设置唯一标识:为每个客户端生成一个唯一的UUID作为连接标识,并将其存储在UserProperties中,便于后续跟踪与管理
         */
//        properties.put(HttpSession.class.getName(),httpSession);
        String sessionKey = UUID.randomUUID().toString().replaceAll("-", "");
        properties.put("Connected",sessionKey);
    }
}

创建controller方法

package com.example.websocket.controller;

//import com.example.easyexcel.feign.RemoteServiceClient;
import com.example.websocket.entity.ClientInfoEntity;
import com.example.websocket.entity.Message;
import com.example.websocket.service.MessageService;
import com.example.websocket.util.ChatEndpoint2;
import com.example.websocket.util.ClientEndpoint1;
import org.apache.ibatis.annotations.Param;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.bind.annotation.*;

import javax.annotation.Resource;
import javax.websocket.ContainerProvider;
import javax.websocket.DeploymentException;
import javax.websocket.Session;
import javax.websocket.WebSocketContainer;
import java.io.IOException;
import java.net.URI;
import java.util.Date;
import java.util.concurrent.CountDownLatch;


/**
 * @version 1.0
 * @Author Abdul
 * @Date 2024/10/14 11:22
 * @注释
 */

@RestController
@RequestMapping("/websocket")
public class ServerController {
    @Resource
    private ChatEndpoint2 server;

    private Session session;

    @Resource
    private MessageService service;

    /**
     * 客户端获取连接
     */
    @GetMapping("/conn")
    public Session conn(String token) {
        try {
            WebSocketContainer container = ContainerProvider.getWebSocketContainer();
            // 创建连接的阻塞锁
            final CountDownLatch latch = new CountDownLatch(1);
            // 连接到服务器
            session = container.connectToServer(ClientEndpoint1.class, URI.create("ws://localhost:9000/webSocket/"+token));
            // 等待连接关闭
            latch.await();
        } catch (DeploymentException | IOException | InterruptedException e) {

        }
        return session;
    }

    /**
     * 服务端 发送信息调用的接口
     */
    @PostMapping("/server/send")
    public String SendMessage(@Param("token") String token, Message message) throws IOException {
        String serverMessage = "服务端:" + message.getText();
        message.setSender("服务端");
        save(message);
        server.onMessage(token, serverMessage);
        return "success";
    }
    /**
     * 客户端 发送信息调用的接口
     */
    @GetMapping("/client/send")
    public String SendClientMessage(Message message,String token) throws IOException {
        try {
            ClientInfoEntity clientInfoEntity = ChatEndpoint2.uavWebSocketInfoMap.get(token);
            Session session1 = clientInfoEntity.getSession();
            String clientMessage = "客户端:" + message.getText();
            message.setSender("客户端");
            save(message);
            session1.getBasicRemote().sendText(clientMessage);
        } catch (Exception e) {
            e.printStackTrace();
        }
        return "success";
    }
    //将对话记录保存到数据库
    public void save(Message message) {
        message.setCreateTime(new Date());
        service.save(message);
    }
}

首先客户端获取连接 需要传入自己唯一token的参数

 可以看见客户端成功连接上了服务端

 接着客户端向服务端发送信息:

 服务端接着对客户端进行发送信息响应

 至此 客户端 服务端实现双向实时通信 并把对话信息保存到数据库中

标签:websocket,SpringBoot,public,token,example,import,客户端,链接,webSocket
From: https://blog.csdn.net/No_speak/article/details/143159255

相关文章

  • springboot+vue毕业设计管理系统【开题+程序+论文】
    系统程序文件列表开题报告内容研究背景随着高等教育的发展,毕业设计作为本科教育的关键环节,其管理效率与质量直接影响到学生的培养质量及学校的整体教学水平。传统的手工管理模式不仅效率低下,还容易出错,难以满足当前教育信息化的发展需求。近年来,随着信息技术的飞速进步,尤其......
  • springboot+vue北工国际健身俱乐部【开题+程序+论文】
    系统程序文件列表开题报告内容研究背景在当今社会,随着人们生活水平的提高和健康意识的增强,健身已成为现代人追求健康生活的重要方式之一。北工国际健身俱乐部作为一家致力于提供高品质健身服务的机构,面临着日益增长的会员需求和激烈的市场竞争。为了更好地满足会员的个性化......
  • springboot+vue北部湾职业技术学校学生档案管理系统【开题+程序+论文】
    系统程序文件列表开题报告内容研究背景随着信息技术的迅猛发展和教育信息化的不断推进,职业技术学校的学生档案管理工作面临着前所未有的挑战与机遇。北部湾职业技术学校作为培养专业技能人才的重要基地,其学生档案管理工作不仅关乎学生的个人信息安全,还直接影响到学校的教学......
  • Springboot南京房价展示平台3hvmf(程序+源码+数据库+调试部署+开发环境)
    本系统(程序+源码+数据库+调试部署+开发环境)带论文文档1万字以上,文末可获取,系统界面在最后面。系统程序文件列表用户,房主,房型,房源信息,区域,预约看房,看房信息,房源打分,房源网站,在线咨询,成交量统计,地区统计开题报告内容一、选题背景及意义随着中国城市化进程的加速......
  • Springboot魔方教学网站5x5q5(程序+源码+数据库+调试部署+开发环境)
    本系统(程序+源码+数据库+调试部署+开发环境)带论文文档1万字以上,文末可获取,系统界面在最后面。系统程序文件列表用户,魔方种类,魔方教学,商品信息开题报告内容一、选题背景及意义在当今信息爆炸的时代,互联网已经成为人们获取知识的主要途径之一。魔方作为一种智力游戏,备受......
  • Springboot民族近代英雄人物科普网站q7koy(程序+源码+数据库+调试部署+开发环境)
    本系统(程序+源码+数据库+调试部署+开发环境)带论文文档1万字以上,文末可获取,系统界面在最后面。系统程序文件列表用户,科普信息,投稿信息开题报告内容一、选题意义及依据选题意义在信息爆炸的今天,关于民族近代英雄人物的详尽事迹和深刻内涵往往被淹没在海量数据中,难以被广......
  • SpringBoot个人理财系统:构建你的数字钱包
    摘要随着信息技术在管理上越来越深入而广泛的应用,管理信息系统的实施在技术上已逐步成熟。本文介绍了个人理财系统的开发全过程。通过分析个人理财系统管理的不足,创建了一个计算机管理个人理财系统的方案。文章介绍了个人理财系统的系统分析部分,包括可行性分析等,系统设计部......
  • SpringBoot与个人理财:打造现代财务管理工具
    摘要随着信息技术在管理上越来越深入而广泛的应用,管理信息系统的实施在技术上已逐步成熟。本文介绍了个人理财系统的开发全过程。通过分析个人理财系统管理的不足,创建了一个计算机管理个人理财系统的方案。文章介绍了个人理财系统的系统分析部分,包括可行性分析等,系统设计部......
  • vue-springboot基于JavaWeb的智慧养老院管理系统的设计与实现 附源码
    目录项目介绍系统实现截图源码获取地址下载技术栈开发核心技术介绍:为什么选择最新的Vue与SpringBoot技术核心代码部分展示项目介绍该系统从三个对象:由管理员和家属、护工来对系统进行设计构建。主要功能包括:个人信息修改,对家属信息、护工信息、老人入住、外出报备、......
  • SpringBoot 面试常见问答总结(一)
    1.什么是SpringBoot?SpringBoot是Spring开源组织下的子项目,是Spring组件一站式解决方案,主要是简化了使用Spring的难度,简省了繁重的配置,提供了各种启动器,使开发者能快速上手。2.为什么要用SpringBoot?快速开发,快速整合,配置简化、内嵌服务容器3.SpringBoot与Spring......