WebSocket实现HTML+SpringBoot聊天功能,小程序+SpringBoot聊天功能

目录

一、认识WebSocket

二、HTML实现聊天

三、微信小程序实现聊天


一、认识WebSocket

1.首先博主在初学Java时自我感觉走了很多弯路,因为以前见识短,在接触聊天功能时根本就没能想到有WebSocket这个聊天框架,就只能用底层的UDP或TCP实现聊天功能,及其繁琐。

1.在入门Java后的朋友学到网络编程会知道UDP和TCP两个知识点,没错WebSocket是一种在单个TCP连接上进行全双工通信的协议。基于TCP协议的一个框架,TCP知识点比较多,具体咱们就不多说了,直接实践怎么使用吧。

二、HTML实现聊天

首先我先贴出完整代码,然后解释

1.html代码,这里我就不单独写js文件了(这个html实现的是一对一聊天,还有一对多,多对多群聊)

html 复制代码
<script src="https://code.jquery.com/jquery-3.6.4.min.js"></script>
<script src="https://cdn.jsdelivr.net/sockjs/1.1.4/sockjs.min.js"></script>
<script src="https://cdn.bootcdn.net/ajax/libs/stomp.js/2.3.3/stomp.min.js"></script>

<script>
    var stompClient = null;

    function connect() {
	var content = $("#message").val();
        var sender = 'tbb';
        var socket = new SockJS('http://127.0.0.1:8080/chat');
        stompClient = Stomp.over(socket);
        stompClient.connect({}, function (frame) {
            console.log('Connected: ' + frame);
            stompClient.subscribe('/user/tbb/message', function (message) {
                showMessage(JSON.parse(message.body));
            });
        });
    }

    function disconnect() {
        if (stompClient !== null) {
            stompClient.disconnect();
        }
        console.log("Disconnected");
    }

    function sendMessage() {
        var content = $("#message").val();
        var sender = $("#sender").val();
        stompClient.send("/app/chat/send/to/user", {}, JSON.stringify({
            content: content,
            sender: sender,
	    unionId: sender,
        }));
    }

    function showMessage(message) {
        $("#chat").append("<div>" + message.sender + ": " + message.content + "</div>");
    }
</script>

<div>
    <input type="text" id="sender" placeholder="Enter your name">
    <input type="text" id="message" placeholder="Type a message...">
    <button onclick="sendMessage()">Send</button>
    <button onclick="connect()">Connect</button>
    <button onclick="disconnect()">Disconnect</button>
</div>

<div id="chat">123</div>

2.SpringBoot完整代码

(1)WebSocketConfig.java配置文件(关键文件)

java 复制代码
package com.example.mengchuangyuan.common.chat.config;

import org.springframework.context.annotation.Configuration;
import org.springframework.messaging.simp.config.MessageBrokerRegistry;
import org.springframework.web.socket.config.annotation.EnableWebSocketMessageBroker;
import org.springframework.web.socket.config.annotation.StompEndpointRegistry;
import org.springframework.web.socket.config.annotation.WebSocketMessageBrokerConfigurer;

@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {

    @Override
    public void configureMessageBroker(MessageBrokerRegistry config) {
        config.enableSimpleBroker("/user","/agent","/topic"); // 定义消息代理,客户端订阅的地址前缀
        config.setApplicationDestinationPrefixes("/app"); // 定义客户端发送消息的地址前缀
        config.setUserDestinationPrefix("/user");
    }

    @Override
    public void registerStompEndpoints(StompEndpointRegistry registry) {
        registry.addEndpoint("/chat").setAllowedOriginPatterns("*").withSockJS(); // 定义WebSocket端点,客户端连接的地址
    }
}

(2)ChatController.java控制层

java 复制代码
package com.example.mengchuangyuan.common.chat.controller;

import com.example.mengchuangyuan.common.chat.entry.ChatMessage;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.messaging.handler.annotation.MessageMapping;
import org.springframework.messaging.handler.annotation.SendTo;
import org.springframework.messaging.simp.SimpMessagingTemplate;
import org.springframework.messaging.simp.annotation.SendToUser;
import org.springframework.stereotype.Controller;

@Slf4j
@Controller
public class ChatController {
    @Autowired
    private SimpMessagingTemplate simpMessagingTemplate;

    /**
     * 发消息(群发)
     * @param chatMessage 获取用户消息
     * @return 返回要回复的消息
     */
    //第一种方式
    @MessageMapping("/chat/send") // 定义消息映射路径
    @SendTo("/agent/public") // 发送消息到指定的代理路径
    public ChatMessage sendMessage(ChatMessage chatMessage) {
        log.info(chatMessage.toString());
        return chatMessage;
    }

    //方法二
//    @MessageMapping("/agent/send")
//    public void getAgentInfo (@Payload ChatMessage chatMessage) {
//        System.out.println("发送群发消息");
//        // 使用api进行推送
//        simpMessagingTemplate.convertAndSend("/agent/public2", chatMessage);
//    }

    /**
     * 发送给自己?
     * @param chatMessage
     * @return
     */
    @MessageMapping("/agent/send/user")
// 这里的路径必须还是以广播的前缀为前缀,否则无法接收
    @SendToUser("/agent/info")
    public ChatMessage sendUserMessage(ChatMessage chatMessage) {
        log.info(chatMessage.toString());
        return chatMessage;
    }

    /**
     * 发送给指定用户
     * @param chatMessage
     * @return
     */
    @MessageMapping("/chat/send/to/user")
// 这里的路径必须还是以广播的前缀为前缀,否则无法接收
    public void sendToUserMessage(ChatMessage chatMessage) {
        log.info("发送给指定用户:"+chatMessage.toString());
        simpMessagingTemplate.convertAndSendToUser(chatMessage.getUnionId(),"/message",chatMessage);
    }
}
java 复制代码
package com.example.mengchuangyuan.common.chat.entry;

import lombok.Data;

@Data
public class ChatMessage {
    private String unionId;
    private String content;
    private String sender; 
}

以上就是完整代码

3.接下来我来简单解释一下,因为一对一聊天比其他相对绕一点,所以博主就解释它就好了,且看下面四段被截取的代码

javascript 复制代码
 function connect() {
	var content = $("#message").val();//发送的消息内容
        var sender = 'tbb';//接收的人
        var socket = new SockJS('http://127.0.0.1:8080/chat');//连接后端socket的地址
        stompClient = Stomp.over(socket);
        stompClient.connect({}, function (frame) {
            console.log('Connected: ' + frame);
            //这里我写死了,接收来自tbb这个人的消息,'/user/tbb/message'可以改成'/user/'+sender+'/message'
            stompClient.subscribe('/user/tbb/message', function (message) { 
                showMessage(JSON.parse(message.body));
            });
        });
    }

上面的代码可以称之为连接服务器并且实时监听tbb给后端发来的消息

java 复制代码
package com.example.mengchuangyuan.common.chat.config;

import org.springframework.context.annotation.Configuration;
import org.springframework.messaging.simp.config.MessageBrokerRegistry;
import org.springframework.web.socket.config.annotation.EnableWebSocketMessageBroker;
import org.springframework.web.socket.config.annotation.StompEndpointRegistry;
import org.springframework.web.socket.config.annotation.WebSocketMessageBrokerConfigurer;

@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {

    @Override
    public void configureMessageBroker(MessageBrokerRegistry config) {
        config.enableSimpleBroker("/user","/agent","/topic"); // 定义消息代理,客户端订阅的地址前缀
        config.setApplicationDestinationPrefixes("/app"); // 定义客户端发送消息的地址前缀
        config.setUserDestinationPrefix("/user");
    }

    @Override
    public void registerStompEndpoints(StompEndpointRegistry registry) {
        registry.addEndpoint("/chat").setAllowedOriginPatterns("*").withSockJS(); // 定义WebSocket端点,客户端连接的地址
    }
}

上面两段是对应的,在html的http://127.0.0.1:8080/chat中chat对应上面Java代码的chat,必须一致,要不然连不上,/user/tbb/message路由中user对应上面Java代码的user,必须一致,要不然发消息时对方收不到消息(博主以前踩过这个坑),而这个tbb则是接收发消息的人的消息

javascript 复制代码
    function sendMessage() { //发消息给对方
        var content = $("#message").val();
        var sender = $("#sender").val();
        stompClient.send("/app/chat/send/to/user", {}, JSON.stringify({ //发消息的后端的路由
            content: content,//消息内容
            sender: sender,//消息姓名
	    unionId: sender,//消息id,这里我把消息姓名也作为id
        }));
    }
java 复制代码
 /**
     * 发送给指定用户
     * @param chatMessage
     * @return
     */
    @MessageMapping("/chat/send/to/user")
// 这里的路径必须还是以广播的前缀为前缀,否则无法接收
    public void sendToUserMessage(ChatMessage chatMessage) {
        log.info("发送给指定用户:"+chatMessage.toString());
        
//chatMessage.getUnionId()是发送给某人的id,/message对应四段中第一段的/user/tbb/message中的message
simpMessagingTemplate.convertAndSendToUser(chatMessage.getUnionId(),"/message",chatMessage);
    }

最后两段是对应的,用于给对方发消息。

三、微信小程序实现聊天

首先,这是博主自己摸索了很久出来的一套小程序聊天体系。

1.聊天数据结构及框架

涉及到了redis缓存,因此需要下载redis的依赖包

聊天数据结构如下(自我感觉存在一定缺陷,懒得改进了):

整体储存结构:

聊天界面结构:{openid1+openid2:{linkType:[info1,info2]}},

如图聊天界面:

聊天列表结构:{linkType:[openid1+openid2,openid3+openid4]}

如图聊天列表:


info的结构 :{mid:"",type:"",linkType:"",formUser:fromUser,toUser:toUser,message:"",date:"",nowDate:""}

fromUser和toUser的结构:{openid:"",phone:"",name:"",headImg:""}

openid1和openid2为fromUser和toUser的openid

linkType:属于哪个板块聊天(比如相亲聊天板块或者外卖或商城聊天板块)

type:作用于获取redis缓存的聊天记录和聊天心跳检测(备注:因为获取历史聊天记录和心跳检测是以聊天方式向后端发起请求,因此我用type的聊天要区分是用户发起聊天还是其他请求。由前端自动发动聊天请求,获取历史聊天记录,由前端发起聊天请求检测心跳,检测心跳的目的是为了确保聊天过程不掉线)

date:聊天的时间段(备注:可以设置5分钟显示一个时间段聊天的时间,比如微信隔5分钟后再发一条信息上面会显示时间,这里我设置的date就充当这个角色)

nowDate :每一句话的时间,主要用于计算当前时间是否与上一句聊天记录的时间是否间隔5分钟,如果间隔5分钟那么上面的date记录该时间,如果间隔不到5分钟,则date设置为空。

整体结构上:两个人的openid连接作为获取他们之间所有功能板块历史聊天记录的内容的键(key)。以linkType作为键(key)获取某个功能板块的所有历史聊天记录,这里聊天记录用集合来储存保证了聊天记录顺序。

2.Java SpringBoot代码

复制代码
(1)MiniWebSocketConfig.java文件
java 复制代码
package com.example.mengchuangyuan.common.chat.mini.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.socket.server.standard.ServerEndpointExporter;

@Configuration
public class MiniWebSocketConfig {
    @Bean
    public ServerEndpointExporter serverEndpointExporter() {
        return new ServerEndpointExporter();
    }
}

(2)MiniWebSocketController.java文件

java 复制代码
package com.example.mengchuangyuan.common.chat.mini.controller;

import com.example.mengchuangyuan.common.redis.tool.SufferVariable;
import com.example.mengchuangyuan.common.tool.Result;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;

import java.io.File;
import java.io.IOException;
import java.util.*;

@Slf4j
@RestController
@RequestMapping("/mini/socket")
public class MiniWebSocketController {
    @Value("${upload.messageChatUrl}")
    private String messageChatUrl;

    @Value("${upload.messageChatPath}")
    private String messageChatPath;

    private final List<Map<String,Object>> mapList = new ArrayList<>();

    @GetMapping("/get/openid/history")
    private Result getOpenidHistory(@RequestParam("openid") String openid,@RequestParam("linkType") String linkType){
//        log.info(openid);
        List<String> openids = (List<String>) SufferVariable.openidKeys.get(linkType);
//        if (keyMap==null){
//            return Result.error();
//        }
//        List<String> openids = (List<String>) keyMap.get(openid);
//        log.info("所以openid"+String.valueOf(openids));
        if (openids==null){
            return Result.error();
        }
        mapList.clear();
        for (String openid1: openids) {
            if (openid1.contains(openid)) {
//            if (SufferVariable.messageMap.get(openid1)!=null){
                mapList.add((Map<String, Object>) SufferVariable.messageMap.get(openid1));
//                log.info("聊天记录缓存"+openid);
//                log.info(String.valueOf(SufferVariable.messageMap.get(openid1)));

//            }
            }
        }
        return Result.success(mapList);
    }


//上传聊天图片
    @PostMapping("/img/upload")
    public Result imgUpDown(@RequestParam("file") MultipartFile file,
                            @RequestParam("filename") String filename) throws IOException {
        System.out.println(filename);
        File file1 = new File(messageChatPath,filename);
        if(!file1.exists()) {
            if(!file1.mkdirs()){ //创建目录
                return Result.error();
            }
        }
        //获取文件名
        String fileName = file.getOriginalFilename();
        //获取文件后缀名。也可以在这里添加判断语句,规定特定格式的图片才能上传,否则拒绝保存。
        String suffixName = fileName.substring(fileName.lastIndexOf("."));
        //为了避免发生图片替换,这里使用了文件名重新生成
        fileName = UUID.randomUUID()+suffixName;

        file.transferTo(new File(file1,fileName));

        return Result.success(messageChatUrl+filename+"/"+fileName);
    }
}

(3)WebSocketEndPoint.java文件

java 复制代码
package com.example.mengchuangyuan.common.chat.mini.mapper;

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import com.example.mengchuangyuan.common.redis.mapper.RedisUtils;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

import javax.annotation.PostConstruct;
import javax.websocket.OnClose;
import javax.websocket.OnMessage;
import javax.websocket.OnOpen;
import javax.websocket.Session;
import javax.websocket.server.PathParam;
import javax.websocket.server.ServerEndpoint;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;

//对外公布的一个后端站点
//ws://localhost:8080/websocket/用户id
@ServerEndpoint(value = "/websocket/{userId}")
@Component
@Slf4j
public class WebSocketEndPoint {
    //与某个客户端的连接会话,需要他来给客户端发送数据
    private Session session;

    @Autowired
    private SessionPool sessionPool;
//    @Autowired
//    private RedisUtils redisUtils;
    private static WebSocketEndPoint webSocketEndPoint;
    //初始化 ②
    @PostConstruct
    public void init() {
        webSocketEndPoint = this;
        webSocketEndPoint.sessionPool = this.sessionPool;
    }

    //连接建立成功调用的方法
    @OnOpen
    public void onOpen(Session session, @PathParam("userId") String userId) {
        //把会话加入连接池中
        //userId通过用户传入,session是系统自动产生
        SessionPool.sessions.put(userId, session);

        //TODO 可以添加日志操作
    }

    //关闭会话的时候
    @OnClose
    public void onClose(Session session) throws IOException {
        webSocketEndPoint.sessionPool.close(session.getId());
        session.close();
    }

    //接收客户端的消息后调用的方法,在这里可以进行各种业务逻辑的操作
    @OnMessage
    public void onMessage(String message, Session session) {
        System.out.println(message);
//        log.info("redisUtils:"+redisUtils);
        //心跳检测
        if (message.equalsIgnoreCase("ping")) {
            try {
                Map<String, Object> params = new HashMap<>();
                params.put("type", "pong");
                session.getBasicRemote().sendText(JSON.toJSONString(params));
            } catch (Exception e) {
                e.printStackTrace();
            }
            return;
        }
        //将Json字符串转为键值对
//        HashMap params = JSON.parseObject(message, HashMap.class);
        JSONObject params =  JSON.parseObject(message);
        webSocketEndPoint.sessionPool.sendMessage(params);

        //这里的业务逻辑仅仅是把收到的消息返回给前端
//        SessionPool.sendMessage(message);
    }
}

(4)SessionPool.java文件

java 复制代码
package com.example.mengchuangyuan.common.chat.mini.mapper;

import com.alibaba.fastjson.JSON;
import com.example.mengchuangyuan.common.redis.mapper.RedisUtils;
import com.example.mengchuangyuan.common.redis.tool.SufferVariable;
import com.example.mengchuangyuan.common.tool.DateYMDms;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.stereotype.Controller;

import javax.websocket.Session;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;

@Component
@Slf4j
public class SessionPool {
    @Autowired
    private RedisUtils redisUtils;

    //key-value : userId - 会话(系统创建)
    public static Map<String, Session> sessions = new ConcurrentHashMap<>();//避免多线程问题

    public void close(String sessionId) {
        //sessionId是在session中添加了一个标识,准确定位某条session
        for (String userId : SessionPool.sessions.keySet()) {
            Session session = SessionPool.sessions.get(userId);
            if (session.getId().equals(sessionId)) {
                sessions.remove(userId);
                break;
            }
        }
    }

    public void sendMessage(String userId, String message) {
        sessions.get(userId).getAsyncRemote().sendText(message);
    }


    //消息的群发,业务逻辑的群发
    public void sendMessage(String message) {
//        redisUtils.cacheValue("chatMessage","String.valueOf(SufferVariable.messageMap)");
        for (String sessionId : SessionPool.sessions.keySet()) {
            SessionPool.sessions.get(sessionId).getAsyncRemote().sendText(message);
        }
    }

    //点对点的消息推送
    public void sendMessage(Map<String, Object> params) {
        log.info("消息内容:"+String.valueOf(params));
        long mid = System.currentTimeMillis();
        Map<String,Object> formUser = (Map<String, Object>) params.get("formUser");
        Map<String,Object> toUser = (Map<String, Object>) params.get("toUser");

        String userId = formUser.get("openid").toString();
        String toUserId = toUser.get("openid").toString();
//        String msg = params.get("message").toString();
        String type  = params.get("type").toString();
        String linkType = params.get("linkType").toString();
        //获取用户session
        Session session = sessions.get(toUserId);

        Map<String,Object> keyMap;
        List<String> setOpenid;
        Map<String,Object> map;
        List<Object> list;
        String uid = userId+toUserId;
        params.put("mid",mid);

        if (SufferVariable.messageMap.get(userId+toUserId)==null&&SufferVariable.messageMap.get(toUserId+userId)==null){
//            messageMap.put(userId+toUserId,userId+toUserId);
            map = new HashMap<>();
            list = new ArrayList<>();

            map.put(linkType,list);
            SufferVariable.messageMap.put(uid,map);

//            list.add(params);

        }else {
            if (SufferVariable.messageMap.get(userId+toUserId)!=null){
                uid = userId+toUserId;

            }else {
               uid = toUserId+userId;
            }
            map = (Map<String, Object>) SufferVariable.messageMap.get(uid);
            list = (List<Object>) map.get(linkType);

        }

        //获取历史记录
        if (type.equalsIgnoreCase("history")){
            if (sessions.get(userId) != null) {
                Map<String, Object> map2 = new HashMap<>();
//                map = (Map<String, Object>) SufferVariable.messageMap.get(uid);
                map2.put("type", "isHistory");
                map2.put("message",list);
                sessions.get(userId).getAsyncRemote().sendText(JSON.toJSONString(map2));
            }
            return;
        }
        String nowDate = DateYMDms.getUtilDate();
        if (list.size()!=0) {
            Map<String, Object> lastMap = (Map<String, Object>) list.get(list.size() - 1);
            String date = DateYMDms.getYMDms(5, lastMap.get("nowDate").toString());
            params.put("date", date);
        }else {
            params.put("date", nowDate);
        }
//        if (SufferVariable.openidKeys.get(linkType)==null){
//            keyMap = new HashMap<>();
//        }else {
//            keyMap = (Map<String, Object>) SufferVariable.openidKeys.get(linkType);
//        }
//        if (keyMap.get(userId)==null){
//            setOpenid = new ArrayList<>();
//        }else {
//            log.info(String.valueOf(SufferVariable.openidKeys));
//            setOpenid = (List<String>) keyMap.get(userId);
//        }
        if (SufferVariable.openidKeys.get(linkType) == null){
            setOpenid = new ArrayList<>();
        }else {
            setOpenid = (List<String>) SufferVariable.openidKeys.get(linkType);
        }
        setOpenid.add(uid);
        Set<String> set = new LinkedHashSet<>(setOpenid);
        setOpenid.clear();
        setOpenid.addAll(set);
//        keyMap.put(userId,setOpenid);
        SufferVariable.openidKeys.put(linkType,setOpenid);

        params.put("nowDate",nowDate);
        list.add(params);
        map.put(linkType,list);
        SufferVariable.messageMap.put(uid,map);
        redisUtils.cacheValue("chatMessage",SufferVariable.messageMap);
        redisUtils.cacheValue("chatOpenidKeys",SufferVariable.openidKeys);

        if (session != null) {
            log.info(uid+":"+String.valueOf(SufferVariable.messageMap.get(uid)));
            session.getAsyncRemote().sendText(JSON.toJSONString(params));
        }
//        log.info(String.valueOf(messageMap));
//        params.remove("formUserId");
        //session不为空的情况下进行点对点推送
    }
}

以上小程序相关代码存在一些缺陷,并且未完整,

需要小程序及SpringBoot完整代码的朋友可以私信博主,

好了本次分享就到此结束。

相关推荐
用户8307196840821 小时前
Spring Boot 集成 RabbitMQ :8 个最佳实践,杜绝消息丢失与队列阻塞
spring boot·后端·rabbitmq
Java水解2 小时前
Spring Boot 视图层与模板引擎
spring boot·后端
Java水解2 小时前
一文搞懂 Spring Boot 默认数据库连接池 HikariCP
spring boot·后端
洋洋技术笔记6 小时前
Spring Boot Web MVC配置详解
spring boot·后端
初次攀爬者1 天前
Kafka 基础介绍
spring boot·kafka·消息队列
用户8307196840821 天前
spring ai alibaba + nacos +mcp 实现mcp服务负载均衡调用实战
spring boot·spring·mcp
Java水解1 天前
SpringBoot3全栈开发实战:从入门到精通的完整指南
spring boot·后端
YuMiao1 天前
gstatic连接问题导致Google Gemini / Studio页面乱码或图标缺失问题
服务器·网络协议
初次攀爬者2 天前
RocketMQ在Spring Boot上的基础使用
java·spring boot·rocketmq
花花无缺2 天前
搞懂@Autowired 与@Resuorce
java·spring boot·后端