springboot集成websokcet+uniapp开发聊天原型验证(一)

1. 整体思路

群组聊天功能实现思路
  • 需要为每个群组维护一个对应的集合(可以是 Set 等数据结构),用来存放该群组内所有在线用户的 WebSocketSession。当有消息发送到群组时,遍历该群组对应的集合,向其中的每个在线用户发送消息。
  • 在消息结构体中新增一个字段用于标识所属群组,以便后端根据这个字段来进行消息的广播分发。
离线用户处理及历史消息推送思路
  • 对于离线用户,当他们重新上线时,需要能够识别出他们之前所在的群组(可以通过用户登录等操作记录其关联群组信息)。
  • 后端要将该群组在其离线期间产生的历史消息查询出来(这可能涉及到数据库操作,将群组聊天消息存储到数据库中以便查询历史记录),然后通过 WebSocket 连接将这些历史消息逐一发送给重新上线的用户。

后端代码修改思路

1. 群组管理与消息处理
  • 群组数据结构 :使用合适的数据结构(如 Map)来存储群组相关信息,以群组 ID 作为键,对应的值可以是包含该群组内在线用户 WebSocketSession 列表以及群组历史消息列表等信息的对象。
  • 消息格式定义:明确消息的格式,使其能区分是文字消息还是图片消息,并且包含必要的元数据,比如发送者、群组 ID、消息内容(文字内容或图片链接等)、时间戳等。
  • 消息分发逻辑:当接收到消息时,根据消息中的群组 ID,找到对应的群组在线用户列表,然后将消息发送给这些用户。
2. 离线用户历史消息处理
  • 用户与群组关联记录 :维护用户与所属群组的关联关系,比如使用 Map 存储用户 ID 和其所属群组 ID 列表的对应关系,以便在用户重新上线时确定需要推送哪些群组的历史消息。
  • 历史消息存储与查询:将群组内的聊天消息持久化存储(实际应用中通常是存入数据库,这里可简单模拟存储结构),当离线用户重新上线时,从存储结构中查询出其所属群组的历史消息并推送给他。

直接 springboot+websokcet,感觉比原生的websocket简单一点。

  1. 集成websokcet
  2. 配置文件
  3. handler
  4. postman测试一下
  5. uniapp

pom添加依赖:

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

application.yml中 端口配置(先复用应用的端口吧)

bash 复制代码
server:
  port: 7877
  tomcat:
    accept-count: 10000
    threads:
      max: 800
      min-spare: 200
  compression:
    enabled: true
  servlet:
    context-path: /chat

配置文件(application.yml)

  • 务器端口及相关配置
    • server.port 配置为 7877,指定了 Spring Boot 应用启动后监听的端口号。
    • server.tomcat.accept-count 设置为 10000,它表示当所有的处理线程都在使用时,能够放到处理队列中的连接请求数量。server.tomcat.threads.max 设为 800 定义了最大线程数,min-spare 设为 200 则是最小备用线程数,这些配置用于优化 Tomcat 处理请求的线程资源分配。
    • server.compression.enabled 设为 true,开启了服务器响应内容的压缩功能,有助于减少网络传输的数据量,提高性能。
    • server.servlet.context-path 配置为 /chat,意味着应用的上下文路径是 /chat,后续访问应用中的资源路径都是基于这个上下文路径来构建的。

WebSocket 配置类(WebSocketConfig)

  • 这个类实现了 WebSocketConfigurer 接口,用于配置 WebSocket 相关的处理。
  • registerWebSocketHandlers 方法中,将自定义的 MyWebSocketHandler 注册到了 WebSocket 处理器注册表 WebSocketHandlerRegistry 中,并且将 WebSocket 的端点路径设置为 /websocket,同时允许来自任意源(setAllowedOrigins("*"))的连接访问该 WebSocket 端点。
java 复制代码
package com.edwin.java.config;


import com.edwin.java.config.interceptor.GroupChatInterceptor;
import com.edwin.java.util.MyWebSocketHandler;
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 {

    /**
     * registerWebSocketHandlers 是一个函数或方法,通常用于在 Web 应用程序中注册 WebSocket 处理程序。
     * WebSocket 是一种基于 TCP 的协议,可以实现客户端和服务器之间的双向通信,可以用于实时应用程序,如聊天应用、游戏、实时更新等。在 Java Web 应用程序中,可以使用 Spring 框架提供的 WebSocket 支持来处理 WebSocket 连接。
     * registerWebSocketHandlers 方法是 Spring WebSocket 的一个 API,它允许开发人员在应用程序中注册 WebSocket 处理程序,并将其映射到特定的 URI。在调用 registerWebSocketHandlers 方法时,需要传递一个 WebSocketHandler 实例和一个 URI 路径作为参数。当客户端请求与该 URI 路径对应的 WebSocket 连接时,Spring 将调用相应的 WebSocket 处理程序来处理连接。
     */
    @Override
    public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
        //参数1:注册我们自定义的MyWebSocketHandler类
        //参数2:路径【UniApp中建立连接的路径】如:我的ip是192.168.1.8:8099则UniApp需要输入的url是ws://192.168.1.8:8099/websocket
        //参数3:setAllowedOrigins("*")设置允许全部来源【在WebSocket中,浏览器会发送一个带有Origin头部的HTTP请求来请求建立WebSocket连接。服务器可以使用setAllowedOrigins方法来设置允许的来源,即允许建立WebSocket连接的域名或IP地址。这样,服务器就可以限制建立WebSocket连接的客户端,避免来自不信任的域名或IP地址的WebSocket连接。】
        registry.addHandler(new MyWebSocketHandler(), "/websocket").setAllowedOrigins("*").addInterceptors(new GroupChatInterceptor());;
    }

}

WebSocket 处理器类(MyWebSocketHandler)

  • 连接建立
    • afterConnectionEstablished 方法在 WebSocket 连接建立后被调用,会记录连接成功的日志信息,并将对应的 WebSocketSession 添加到 sessions 列表中,用于后续管理连接会话。
  • 消息处理
    • handleMessage 方法接收到消息时,会记录消息内容日志,然后遍历所有已连接的会话,尝试向每个客户端发送一条固定格式的消息(这里只是简单示例性质的消息)。
    • handleTextMessage 方法针对文本消息做更具体的处理,会对收到的请求消息进行转义和记录日志,然后构造响应消息并发送回对应的客户端会话。
  • 定时消息发送
    • 通过 @Scheduled(fixedRate = 10000) 注解定义了一个定时任务,每隔 10000 毫秒(即 10 秒)会遍历所有连接会话,如果会话处于打开状态,就向其发送一条包含当前时间的广播消息。
  • 连接关闭及其他
    • afterConnectionClosed 方法在 WebSocket 连接关闭时被调用,负责从 sessions 列表中移除对应的会话,并记录连接关闭的日志。
    • supportsPartialMessages 方法返回 false,表示不支持部分消息处理。
    • handleTransportError 方法用于处理 WebSocket 传输过程中的错误,会记录相应的错误日志。
java 复制代码
package com.edwin.java.util;


import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
import org.springframework.web.socket.*;
import org.springframework.web.socket.handler.TextWebSocketHandler;
import org.springframework.web.util.HtmlUtils;

import java.io.IOException;
import java.time.LocalTime;
import java.util.List;
import java.util.concurrent.CopyOnWriteArrayList;


import java.io.IOException;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.web.socket.WebSocketSession;
import org.springframework.web.socket.handler.TextWebSocketHandler;
import com.fasterxml.jackson.databind.ObjectMapper;


@Component
public class MyWebSocketHandler extends TextWebSocketHandler {

    private static final Logger LOGGER = LoggerFactory.getLogger(MyWebSocketHandler.class);
    // 用于存储群组信息,键为群组ID,值包含在线用户会话列表和历史消息列表
    private Map<String, GroupInfo> groupInfos = new HashMap<>();
    // 用于存储用户与群组的关联关系,键为用户ID,值为群组ID列表
    private Map<String, List<String>> userGroups = new HashMap<>();
    private ObjectMapper objectMapper = new ObjectMapper();

    private final List<WebSocketSession> sessions = new CopyOnWriteArrayList<>();

    /**
     * afterConnectionEstablished 是一个 WebSocket API 中的回调函数,它是在建立 WebSocket 连接之后被调用的。
     * 当 WebSocket 连接建立成功后,浏览器会发送一个握手请求给服务器端,如果服务器成功地接受了该请求,那么连接就会被建立起来
     */
    @Override
    public void afterConnectionEstablished(WebSocketSession session) throws Exception {
        LOGGER.info("WebSocket已连接: {}", session.getId());

        // 假设从WebSocket连接的属性或者请求参数中获取用户ID和群组ID列表(实际需按业务逻辑调整获取方式)
        String userId = (String) session.getAttributes().get("userId");
        List<String> groupIds = (List<String>) session.getAttributes().get("groupIds");

        if (userId!= null && groupIds!= null) {
            userGroups.put(userId, groupIds);
            for (String groupId : groupIds) {
                groupInfos.computeIfAbsent(groupId, k -> new GroupInfo()).addSession(session);
            }
        }
    }

    /**
     * handleMessage 是 WebSocket API 中的回调函数,它是用来处理从客户端接收到的 WebSocket 消息的。
     * 当客户端通过 WebSocket 连接发送消息到服务器端时,服务器端会自动调用 handleMessage 函数并传递收到的消息作为参数,你可以在该函数中处理这个消息,并根据需要向客户端发送一些响应消息。
     */
    @Override
    public void handleMessage(WebSocketSession session, WebSocketMessage<?> message) throws Exception {
        LOGGER.info("WebSocket收到的消息: {}", message.getPayload());

        // 将消息反序列化,假设消息是JSON格式,这里解析为Message对象(下面定义)
        Message msg = objectMapper.readValue(message.getPayload().toString(), Message.class);

        String groupId = msg.getGroupId();
        GroupInfo groupInfo = groupInfos.get(groupId);
        if (groupInfo == null) {
            // 如果群组信息不存在,则创建新的群组信息,并添加当前用户的WebSocketSession
            groupInfo = new GroupInfo();
            groupInfo.addSession(session);
            groupInfos.put(groupId, groupInfo);

            // 同时,假设这里从WebSocket连接的属性或者请求参数中获取用户ID(实际需按业务逻辑调整获取方式)
            String userId = (String) session.getAttributes().get("userId");
            List<String> groupIds = userGroups.computeIfAbsent(userId, k -> new ArrayList<>());
            groupIds.add(groupId);
        }

        // 将消息添加到群组历史消息列表
        groupInfo.addHistoryMessage(msg);

        // 向群组内所有在线用户发送消息
        List<WebSocketSession> sessions = groupInfo.getSessions();
        for (WebSocketSession s : sessions) {
            try {
                s.sendMessage(new TextMessage(objectMapper.writeValueAsString(msg)));
            } catch (IOException e) {
                LOGGER.error("无法发送WebSocket消息", e);
            }
        }
    }


    @Override
    public void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception {
        handleMessage(session, message);
    }



    @Scheduled(fixedRate = 10000)
    void sendPeriodicMessages() throws IOException {
        // 这里可扩展定时向群组推送系统消息等功能,暂不做详细修改
        for (GroupInfo groupInfo : groupInfos.values()) {
            List<WebSocketSession> sessions = groupInfo.getSessions();
            for (WebSocketSession s : sessions) {
                if (s.isOpen()) {
                    String broadcast = "server periodic message " + LocalDateTime.now();
                    LOGGER.info("Server sends: {}", broadcast);
                    s.sendMessage(new TextMessage(broadcast));
                }
            }
        }
    }


    // 处理用户重新上线,推送历史消息的方法
    public void handleUserReconnect(String userId, WebSocketSession session) {
        List<String> groupIds = userGroups.get(userId);
        if (groupIds!= null) {
            for (String groupId : groupIds) {
                GroupInfo groupInfo = groupInfos.get(groupId);
                if (groupInfo!= null) {
                    List<Message> historyMessages = groupInfo.getHistoryMessages();
                    for (Message historyMessage : historyMessages) {
                        try {
                            session.sendMessage(new TextMessage(objectMapper.writeValueAsString(historyMessage)));
                        } catch (IOException e) {
                            LOGGER.error("无法发送历史消息给重新上线用户", e);
                        }
                    }
                }
            }
        }
    }

    /**
     * afterConnectionClosed 是 WebSocket API 中的回调函数,它是在 WebSocket 连接关闭后被调用的。
     * 当客户端或服务器端主动关闭 WebSocket 连接时,afterConnectionClosed 回调函数会被调用,你可以在该函数中执行一些资源释放、清理工作等操作,比如关闭数据库连接、清理缓存等。
     */

    @Override
    public void afterConnectionClosed(WebSocketSession session, CloseStatus status) throws Exception {
        LOGGER.info("WebSocket已断开连接: {}", session.getId());

        // 移除用户会话在各个群组中的关联
        String userId = (String) session.getAttributes().get("userId");
        List<String> groupIds = userGroups.get(userId);
        if (groupIds!= null) {
            for (String groupId : groupIds) {
                GroupInfo groupInfo = groupInfos.get(groupId);
                if (groupInfo!= null) {
                    groupInfo.removeSession(session);
                }
            }
            userGroups.remove(userId);
        }
    }

    /**
     * supportsPartialMessages 是 WebSocket API 中的方法,它用来指示 WebSocket 消息是否支持分段传输。
     * WebSocket 消息可以分段传输,也就是说一个消息可以被分成多个部分依次传输,这对于大型数据传输和流媒体传输非常有用。当消息被分成多个部分传输时,WebSocket 会自动将这些部分合并成完整的消息。
     * supportsPartialMessages 方法用来指示服务器是否支持分段消息传输,如果支持,则可以在接收到部分消息时开始处理消息,否则需要等待接收到完整消息后才能开始处理。
     */
    @Override
    public boolean supportsPartialMessages() {
        return false;
    }

    /**
     * handleTransportError 是 WebSocket API 中的回调函数,它用来处理 WebSocket 传输层出现错误的情况。
     *当 WebSocket 传输层出现错误,比如网络中断、协议错误等,WebSocket 会自动调用 handleTransportError 函数,并传递相应的错误信息。在该函数中,我们可以处理这些错误,比如关闭 WebSocket 连接、记录错误日志等。
     */
    @Override
    public void handleTransportError(WebSocketSession session, Throwable exception) throws Exception {
        LOGGER.error("WebSocket错误", exception);
    }



    // 在MyWebSocketHandler类内部定义
    private class GroupInfo {
        // 存储群组内所有在线用户的WebSocketSession列表
        private List<WebSocketSession> sessions = new ArrayList<>();
        // 存储群组的历史消息列表,消息以自定义的Message对象形式存储(前面代码中已定义Message类)
        private List<Message> historyMessages = new ArrayList<>();

        // 添加一个用户的WebSocketSession到群组的在线用户列表中
        public void addSession(WebSocketSession session) {
            sessions.add(session);
        }

        // 从群组的在线用户列表中移除指定用户的WebSocketSession
        public void removeSession(WebSocketSession session) {
            sessions.remove(session);
        }

        // 向群组的历史消息列表中添加一条消息
        public void addHistoryMessage(Message message) {
            historyMessages.add(message);
        }

        // 获取群组内所有在线用户的WebSocketSession列表
        public List<WebSocketSession> getSessions() {
            return sessions;
        }

        // 获取群组的历史消息列表
        public List<Message> getHistoryMessages() {
            return historyMessages;
        }
    }


    // 定义消息类,包含必要的消息属性,可根据实际需求扩展
    private static class Message {
        private String type; // 消息类型,如 "text" 或 "image"
        private String groupId; // 群组ID
        private String sender; // 发送者(可根据实际情况完善,比如用户ID等)
        private String content; // 消息内容,文字或图片链接等
       // private LocalDateTime timestamp = LocalDateTime.now(); // 时间戳

        // 生成必要的Getter和Setter方法(可使用Lombok简化代码,此处为清晰展示手动编写)
        public String getType() {
            return type;
        }

        public void setType(String type) {
            this.type = type;
        }

        public String getGroupId() {
            return groupId;
        }

        public void setGroupId(String groupId) {
            this.groupId = groupId;
        }

        public String getSender() {
            return sender;
        }

        public void setSender(String sender) {
            this.sender = sender;
        }

        public String getContent() {
            return content;
        }

        public void setContent(String content) {
            this.content = content;
        }

//        public LocalDateTime getTimestamp() {
//            return timestamp;
//        }
//
//        public void setTimestamp(LocalDateTime timestamp) {
//            this.timestamp = timestamp;
//        }
    }

}

postman验证:

注意,路径中要加chat ,因为application.yml中配置了

uniapp端页面:

javascript 复制代码
<template>
  <view class="chat-room">
    <!-- 聊天记录 -->
    <scroll-view scroll-y="true" class="message-list">
      <view v-for="(message, index) in messages" :key="index" class="message-item">
        <view v-if="message.type === 'text'" class="text-message">
      <!--    <view class="avatar">{{ message.senderAvatar }}</view> -->
          <view class="text">{{ message.content }}</view>
        </view>
        <view v-else-if="message.type === 'image'" class="image-message">
          <image :src="message.content" class="message-image" @click="previewImage(message.content)"></image>
        </view>
      </view>
    </scroll-view>

    <!-- 输入框 -->
    <view class="input-area">
      <input v-model="inputContent" placeholder="输入内容" class="input" />
      <button @click="sendMessage">发送</button>
    </view>
  </view>
</template>

<script>
export default {
  data() {
    return {
      messages: [], // 聊天记录
      inputContent: '', // 输入框内容
      socketOpen: false, // WebSocket连接状态
      socket: null, // WebSocket对象
    };
  },
  methods: {
    // 初始化WebSocket连接
    initWebSocket() {
		   uni.setStorageSync('userId',1);  // only test
		   uni.setStorageSync('groupIds',2);  // only test
		    const userId = uni.getStorageSync('userId');
		    const groupIds = uni.getStorageSync('groupIds');
			console.log(groupIds);
		// /wechat/client/chat
      this.socket = uni.connectSocket({
		url: 'ws://127.0.0.1:7877/chat/websocket',
        data: {
            userId: userId,
            groupIds: groupIds
        },
        success: () => {
          console.log('WebSocket连接成功');
          this.socketOpen = true;
        },
        fail: () => {
          console.error('WebSocket连接失败');
        },
      });

      // 监听WebSocket消息
      this.socket.onMessage((res) => {
        const message = JSON.parse(res.data);
        this.messages.push(message); // 将新消息添加到聊天记录中
        this.$forceUpdate(); // 强制更新视图,确保新消息显示
      });

      // 监听WebSocket连接关闭
      this.socket.onClose(() => {
        console.log('WebSocket连接关闭');
        this.socketOpen = false;
      });
    },
    // 发送消息
sendMessage() {
    if (this.inputContent.trim() === '') {
        uni.showToast({
            title: '请输入内容',
            icon: 'none',
        });
        return;
    }

    const message = {
        type: 'text', // 消息类型,可以是text或image,这里发送文字消息示例,发送图片时修改相应字段
        groupId: uni.getStorageSync('groupIds'), // 从本地存储获取当前所在群组ID(需按实际情况调整获取方式)
        sender: uni.getStorageSync('userId'), // 从本地存储获取用户ID(需按实际情况调整获取方式)
        content: this.inputContent
    };

    // 通过WebSocket发送消息(或HTTP请求,根据后端接口决定)
    if (this.socketOpen) {
        this.socket.send({
            data: JSON.stringify(message)
        });
    } else {
		//todo
    }
},
    // 预览图片
    previewImage(url) {
      uni.previewImage({
        current: url, // 当前显示图片的http链接
        urls: [url], // 需要预览的图片http链接列表
      });
    },
  },
  mounted() {
    // 页面加载时初始化WebSocket连接
    this.initWebSocket();

    // 可以从服务器获取历史聊天记录并初始化messages数组(根据需求实现)
  },
};
</script>

<style>
.chat-room {
  padding: 10px;
}

.message-list {
  height: 500px; /* 根据需要调整高度 */
  border-bottom: 1px solid #ccc;
  padding-right: 10px; /* 留出空间给滚动条 */
  overflow-y: auto;
}

.message-item {
  margin-bottom: 10px;
  display: flex;
  align-items: center;
}

.avatar {
  width: 40px;
  height: 40px;
  border-radius: 50%;
  margin-right: 10px;
}

.text-message .text {
  background-color: #fff;
  padding: 5px 10px;
  border-radius: 5px;
  max-width: 60%; /* 根据需要调整宽度 */
  word-wrap: break-word; /* 防止长文本溢出 */
}

.image-message .message-image {
  width: 100px;
  height: 100px;
  object-fit: cover;
  border-radius: 5px;
}

.input-area {
  display: flex;
  margin-top: 10px;
}

.input {
  flex: 1;
  padding: 5px;
  border: 1px solid #ccc;
  border-radius: 5px;
}

button {
  padding: 5px 10px;
  margin-left: 10px;
  border: none;
  background-color: #1aad19;
  color: #fff;
  border-radius: 5px;
}
</style>

原型效果:

后期再补充~~~~~

相关推荐
长山的随笔3 分钟前
最适合智能体的身份认证技术:对比OpenID Connect、API keys、did:wba
后端
柔弱女子爱java18 分钟前
spring专题笔记(五):依赖注入--p命名空间注入、c命名空间注入、util命名空间
java·笔记·后端·spring·架构·系统架构
湫qiu44 分钟前
6.5840 Lab-Key/Value Server 思路
后端·go
Hello Dam1 小时前
整合 Knife4j 于 Spring Cloud 网关:实现跨服务的 API 文档统一展示
后端·spring·spring cloud·knife4j
李三醒1 小时前
Apache Tomcat 漏洞CVE-2024-50379条件竞争文件上传漏洞 servlet readonly spring boot 修复方式
spring boot·tomcat·apache
星月前端2 小时前
随记:springboot的xml中sql数据库表名动态写法
xml·数据库·spring boot
月光晒了很凉快3 小时前
Django REST framework(DRF)在处理不同请求方法时的完整流程
后端·python·django
小码编匠3 小时前
C# 使用心跳机制实现TCP客户端自动重连
后端·c#·.net
以后不吃煲仔饭3 小时前
面试小札:Java后端闪电五连鞭_9
java·后端·面试