WebSocket学习笔记以及用户与客服聊天案例简单实现(springboot+vue)

一:介绍:

WebSocket学习笔记以及用户与客服聊天案例简单实现(springboot+vue)_第1张图片

二:http协议与websocket对比: 

WebSocket学习笔记以及用户与客服聊天案例简单实现(springboot+vue)_第2张图片

WebSocket学习笔记以及用户与客服聊天案例简单实现(springboot+vue)_第3张图片 三:websocket协议:

WebSocket学习笔记以及用户与客服聊天案例简单实现(springboot+vue)_第4张图片

WebSocket学习笔记以及用户与客服聊天案例简单实现(springboot+vue)_第5张图片 WebSocket学习笔记以及用户与客服聊天案例简单实现(springboot+vue)_第6张图片

四:实现:

4.1客户端:

WebSocket学习笔记以及用户与客服聊天案例简单实现(springboot+vue)_第7张图片 WebSocket学习笔记以及用户与客服聊天案例简单实现(springboot+vue)_第8张图片

4.2服务端:

WebSocket学习笔记以及用户与客服聊天案例简单实现(springboot+vue)_第9张图片 WebSocket学习笔记以及用户与客服聊天案例简单实现(springboot+vue)_第10张图片

五:案例: 

环境:做一个书店项目时想加一个用户联系客服的功能,寻思可以实现消息的实时通信和把聊天记录保存下来的效果,从网上找了找,决定使用websocket,并把消息保存到redis中。

5.1功能分析:

        历史聊天记录查询:客服在打开消息中心点击某个用户的时候,可以向后端发送请求,从redis中查询自己与该用户的消息记录并渲染到页面上

        发送消息与接收消息:需要用户先向客服发送消息,客服才能回应。在前端上,我们可以写一个方法来发送消息,由服务端接收消息,处理保存在redis中,并转发给客服,客服的回复也是这样的流程

5.3代码概要:

5.3.1服务端代码:

一个message实体类(ChatMessage)用于规定消息的格式,

一个配置类(WebSocketConfig)启用WebSocket功能,注册处理器,添加拦截器,指定访问路径,

一个拦截器(WebSocketInterceptor),拦截websocket请求,从URL中获取用户id,方便在处理消息逻辑中设置转发消息的目标用户,

一个消息接收处理的类(ChatMessageHandler),接收前端的消息,发送到redis中,转给目标用户,

一个控制器(ChatController),前端发送http请求从redis中获取数据

5.3.2客户端代码:

一个用户端的页面,满足基本的发送接收消息和渲染页面

一个客服端的页面,满足发送接收消息和渲染页面,当接收到用户发送的消息时会更新用户列表,显示有对话消息的用户

 5.4全部代码:

5.4.提示:服务端记得导入依赖

        
            org.springframework.boot
            spring-boot-starter-websocket
            2.7.3
        
5.4.提示:前端使用vue native websocket库,在main.js中 
import VueNativeSock from 'vue-native-websocket';

Vue.use(VueNativeSock, 'ws://localhost:8082/chat', {
  format: 'json',
});
5.4.1服务端代码:
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;


/**
 * 这是一个实体类,用于表示聊天消息的数据结构。包括消息的ID、发送者ID、接收者ID、消息内容和发送时间等信息。
 */

@Data
@NoArgsConstructor
@AllArgsConstructor
public class ChatMessage {

    private String senderId;

    private String receiverId;

    private String content;

}
import com.bookstore.handler.ChatMessageHandler;
import com.bookstore.interceptor.WebSocketInterceptor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.messaging.Message;
import org.springframework.messaging.MessageChannel;
import org.springframework.messaging.simp.SimpMessagingTemplate;
import org.springframework.messaging.simp.config.MessageBrokerRegistry;
import org.springframework.web.socket.config.annotation.EnableWebSocket;
import org.springframework.web.socket.config.annotation.WebSocketConfigurer;
import org.springframework.web.socket.config.annotation.WebSocketHandlerRegistry;
import org.springframework.web.socket.server.HandshakeInterceptor;

import java.util.Arrays;
import java.util.List;

/**
 * 通过@EnableWebSocket注解启用WebSocket功能,
 * 实现WebSocketConfigurer接口来注册WebSocket处理器。在这里,
 * 我们将ChatMessageHandler注册为处理WebSocket消息的处理器,
 * 并指定了访问路径为"/chat"。
 */

@Configuration
@EnableWebSocket
@Slf4j
public class WebSocketConfig implements WebSocketConfigurer {

    @Autowired
    private ChatMessageHandler chatMessageHandler;
    @Autowired
    private WebSocketInterceptor webSocketInterceptor;
    @Bean
    public SimpMessagingTemplate messagingTemplate() {
        SimpMessagingTemplate template = new SimpMessagingTemplate(new MessageChannel() {
            @Override
            public boolean send(Message message, long timeout) {
                // 设置超时时间为5秒
                return false;
            }

            @Override
            public boolean send(Message message) {
                // 设置超时时间为5秒
                return false;
            }
        });

        template.setSendTimeout(5000); // 设置发送消息的超时时间为5秒
        return template;
    }


    @Override
    public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
        System.out.println("注册WebSocket处理器...");
        registry.addHandler(chatMessageHandler, "/chat").setAllowedOrigins("*").addInterceptors(webSocketInterceptor);
    }

}
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.server.ServerHttpRequest;
import org.springframework.http.server.ServerHttpResponse;
import org.springframework.http.server.ServletServerHttpRequest;
import org.springframework.stereotype.Component;
import org.springframework.web.socket.WebSocketHandler;
import org.springframework.web.socket.server.HandshakeInterceptor;

import java.util.Map;

/**
 * 拦截器,拦截websocket连接请求,从URL中获取传过来的用户id,添加到属性中,方便消息处理的逻辑
 */

@Component
@Slf4j
public class WebSocketInterceptor implements HandshakeInterceptor {
    @Override
    public boolean beforeHandshake(ServerHttpRequest request, ServerHttpResponse response, WebSocketHandler wsHandler, Map attributes) throws Exception {
        System.out.println(request);
        if (request instanceof ServletServerHttpRequest) {
            // 获取连接中的 URL 参数
            String userId = ((ServletServerHttpRequest) request).getServletRequest().getParameter("userId");

            System.out.println("WebSocket连接用户: " + userId);

            // 将参数存储到 attributes 中,以便后续处理
            attributes.put("userId", userId);
        }
        return true;
    }

    @Override
    public void afterHandshake(ServerHttpRequest request, ServerHttpResponse response, WebSocketHandler wsHandler, Exception exception) {
        // Do nothing
    }
}
import com.bookstore.pojo.entity.ChatMessage;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.messaging.simp.SimpMessagingTemplate;
import org.springframework.stereotype.Component;
import org.springframework.web.socket.CloseStatus;
import org.springframework.web.socket.TextMessage;
import org.springframework.web.socket.WebSocketSession;
import org.springframework.web.socket.handler.TextWebSocketHandler;

import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

/**
 * 这是一个WebSocket处理器,继承自TextWebSocketHandler。
 * 它负责接收前端发送的消息,并将消息内容解析为ChatMessage对象,然后将该消息发布到Redis中。
 * 转发给目标用户
 */

@Component
public class ChatMessageHandler extends TextWebSocketHandler {

    @Autowired
    private RedisTemplate redisTemplate;
    @Autowired
    private SimpMessagingTemplate messagingTemplate; // 注入SimpMessagingTemplate
    // 定义一个存储WebSocketSession的Map   根据这个找到转发消息的目的地    admin=StandardWebSocketSession[id=7754d01c-1283-e1c4-aeac-20cd7febba77, uri=ws://localhost:8082/chat?userId=admin]
    private Map userSessions = new ConcurrentHashMap<>();


    // 在连接建立时将 WebSocketSession 添加到 Map 中
    @Override
    public void afterConnectionEstablished(WebSocketSession session) throws Exception {
        // 获取属性中的用户id
        Object userIdObj = session.getAttributes().get("userId");
        String userId = userIdObj != null ? userIdObj.toString() : null;

        // 将 WebSocketSession 添加到 Map 中
        userSessions.put(userId, session);
        System.out.println("userSessions--------->" + userSessions);
    }


    // 在连接关闭时从 Map 中移除 WebSocketSession
    @Override
    public void afterConnectionClosed(WebSocketSession session, CloseStatus status) throws Exception {
        Object userIdObj = session.getAttributes().get("userId");
        String userId = userIdObj != null ? userIdObj.toString() : null;
        userSessions.remove(userId);
    }

    // 处理收到的文本消息
    @Override
    protected void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception {
        // 解析消息内容
        ObjectMapper objectMapper = new ObjectMapper();
        ChatMessage chatMessage = objectMapper.readValue(message.getPayload(), ChatMessage.class);
        System.out.println(chatMessage);

        // 获取消息中的相关数据
        String senderId = chatMessage.getSenderId();
        String receiverId = chatMessage.getReceiverId();
        String chatMessageJson = objectMapper.writeValueAsString(chatMessage);

        // 将消息添加到有序集合中  admin:user1
        String key = senderId.compareTo(receiverId) < 0 ? senderId+":"+receiverId : receiverId+":"+senderId;
        // 获取当前时间戳作为消息的分值
        long timestamp = System.currentTimeMillis();
        // 将消息添加到有序集合中,将时间戳作为分值     保持聊天记录的顺序
        redisTemplate.opsForZSet().add(key, chatMessageJson, timestamp);

        // 构造要转发的消息
        ChatMessage newMessage = new ChatMessage();
        newMessage.setSenderId(senderId);
        newMessage.setReceiverId(receiverId);
        newMessage.setContent(chatMessage.getContent());

        // 将消息转换为 JSON 格式
        ObjectMapper objectMapper1 = new ObjectMapper();
        String jsonMessage = objectMapper1.writeValueAsString(newMessage);

        // 获取目标用户的 WebSocketSession
        WebSocketSession targetSession = findTargetSession(receiverId);
        if (targetSession == null || !targetSession.isOpen()) {
            // 目标用户不在线,或者连接已经关闭
            return;
        }

        // 发送消息
        targetSession.sendMessage(new TextMessage(jsonMessage));

    }

    // 根据接收者的 ID 查找对应的 WebSocketSession
    private WebSocketSession findTargetSession(String receiverId) {
            return userSessions.get(receiverId);
    }
}
import com.alibaba.fastjson.JSON;
import com.bookstore.pojo.entity.ChatMessage;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.web.bind.annotation.*;

import java.util.ArrayList;
import java.util.List;
import java.util.Set;

/**
 * 前端发送请求从redis中获取数据
 */

@CrossOrigin(origins = "http://localhost:8081")
@RestController
@RequestMapping("/api/chat")
public class ChatController {

    @Autowired
    private RedisTemplate redisTemplate;

    @GetMapping("/messages/{senderId}/{receiverId}")
    public List getMessages(@PathVariable String senderId, @PathVariable String receiverId) {
        // 获取最近的30条消息
        String key = senderId.compareTo(receiverId) < 0 ? senderId+":"+receiverId : receiverId+":"+senderId;
        Set recentMessages = redisTemplate.opsForZSet().range(key, -30, -1);
        List chatMessages = new ArrayList<>();
        for (String messageJson : recentMessages) {
            ChatMessage chatMessage = JSON.parseObject(messageJson, ChatMessage.class);
            chatMessages.add(chatMessage);
        }
        return chatMessages;
    }

    @GetMapping("/messages/all")
    public List getAllMessages() {
        String keyPattern = "admin:*"; // 匹配所有管理员和用户的键名
        Set keys = redisTemplate.keys(keyPattern);
        List chatMessages = new ArrayList<>();
        for (String key : keys) {
            Set messages = redisTemplate.opsForZSet().range(key, 0, -1);
            for (String msgJson : messages) {
                ChatMessage chatMessage = JSON.parseObject(msgJson, ChatMessage.class);
                chatMessages.add(chatMessage);
            }
        }
        return chatMessages;
    }

}
5.4.2前端用户端代码:

前端这两个代码中有一些是我自己写好的组件,对聊天这部分逻辑没什么影响,大家主要看看script中的逻辑就行,style里面的样式也很杂乱,我也不太懂,凑合看吧,样式部分太难了我也不会





5.4.3前端客服端代码:

  
  
  

你可能感兴趣的:(学习,笔记,websocket)