从零实现APP实时聊天功能:WebSocket+离线消息+多媒体传输全解析

引言: 最近不是团队继续把以前的一个项目拿来继续完善了一下嘛,但是本人就非常反骨,想着什么好玩的,就立马悄悄马上去添加,实现它!!!等到伙伴拉到新的代码,总是会说:你怎么又添加功能了啊?什么时候加的啊?怎么又不给我们商量啊?哈哈,我y已经习惯了!!然后这个功能对于当前项目(关于古诗APP)来说确实可有可无,但是我想着,没有互动太无聊!!!后续如果有时间,可以去扩展,可以把整个项目功能联系起来,比如用户自己创建的古诗可以发布到古诗的论坛上,大众可以评论,收藏,私发,恶搞图之类对吧(当然这些是要有时间,最近可能也没有太多时间了,也没有太多精力放在这上面了 。),当然目前这个好友功能,还是一个初稿,还有很多功能可以去扩展,其实理解了,在通过技术选型一下也就是时间去实现了,我就暂时随便做了一下。欧克。look | | | | 。

我知道此刻我的伙伴的表情应该是:

一、功能概述

  1. 核心功能亮点

    • 基于WebSocket的实时双向通信

    • 完善的离线消息存储机制

    • 多媒体消息支持(表情包、图片等)

    • 未读消息提醒与通知

  2. 应用场景

    • 社交APP好友互动

    • 即时通讯场景

    • 需要低延迟反馈的场景

好友添加:

登录2个账号一个是MN_HZ , 另一个账号是 mn


添加成功 :


好友聊天:支持表情,图片:(后续也可以加自制表情包!!!)


同框


未读消息显示:

二、技术选型

核心技术组件

  • 通信层:WebSocket协议

  • 存储层:Mysql(消息存储)+MQ(异步存储)

  • 文件存储:阿里云OSS

开发工具:

  • 前端: Hbuilder X

  • 后端IDE:IntelliJ IDEA 2023.1.3

三. 关键技术解析:

WebSocket 是一种全双工通信协议,允许客户端和服务器在单个 TCP 连接上建立持久化的双向实时数据交换。它是 HTML5 规范的一部分,旨在解决传统 HTTP 协议在实时通信中的局限性(如轮询效率低、延迟高)。


核心特性与工作原理
协议升级机制

客户端通过 HTTP 发起 WebSocket 握手请求(Upgrade: websocket 头字段)。服务器响应 101 Switching Protocols 完成协议切换,后续通信基于 WebSocket 帧格式。
2.

双向实时通信

连接建立后,双方可随时主动发送数据,无需等待请求-响应模式。
3.

低开销

数据传输采用轻量级的二进制帧格式(相比 HTTP 头部开销显著减少)。
4.

持久化连接

默认保持连接状态,避免重复握手(可通过心跳包维持连接活性)

示例部分代码(JavaScript)

javascript 复制代码
    this.socket = uni.connectSocket({
                url: `ws://xxxxx:8080/xxxxx`, 
                complete: () => {
                    console.log('WebSocket 连接请求已完成');
                }
            });

            this.socket.onOpen(() => {
                console.log('WebSocket 连接成功');
                this.socket.onMessage((event) => {
                    console.log('收到消息:', event);
                    this.handleReceivedMessage(event.data);
                });
            });
             
            this.socket.onClose(() => {
                console.log('WebSocket 连接关闭');
            });

            this.socket.onError((error) => {
                console.error('WebSocket 连接错误:', error);
            });
            send() {
            if (this.content.trim() === '') return;
            this.socket.send(),
                success: () => {
                    this.list.push({
                        content: this.content,
                        userType: 'self',
                        avatar: this._selfAvatar,
                        messageType: 'text'
                    });

                    this.content = '';
                    this.scrollToBottom();
                },
                fail: (err) => {
                    console.log('消息发送失败', err);
                },
            });
        },

后端部分:

java 复制代码
@Configuration
@EnableWebSocket
public class WebSocketConfig implements WebSocketConfigurer {

    private final MyWebSocketHandler myWebSocketHandler;

    public WebSocketConfig(MyWebSocketHandler myWebSocketHandler) {
        this.myWebSocketHandler = myWebSocketHandler;
    }

    @Override
    public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
        registry.addHandler(myWebSocketHandler, "/friend").setAllowedOrigins("*");
    }
}


package com.example.websocket;

import cn.hutool.json.JSONException;
import cn.hutool.json.JSONObject;
import cn.hutool.json.JSONUtil;
import com.example.mapper.Offline_messagesMapper;
import com.example.modle.pojo.Offline_messages;
import com.example.service.impl.Offline_messagesServiceImpl;
import com.example.util.MessageService;
import org.springframework.beans.factory.annotation.Autowired;
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.io.IOException;
import java.net.URI;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

@Component
public class MyWebSocketHandler extends TextWebSocketHandler {

    @Autowired
    private Offline_messagesServiceImpl offline_messagesServiceImpl;

    @Autowired
    private Offline_messagesMapper offlineMessagesMapper;

    private final Map<String, WebSocketSession> sessions = new ConcurrentHashMap<>();
    private final Map<String, String> userChannels = new ConcurrentHashMap<>(); // 用户当前所在的频道
    private final MessageService messageService;

    public MyWebSocketHandler(MessageService messageService) {
        this.messageService = messageService;
    }

    @Override
    public void afterConnectionEstablished(WebSocketSession session) throws Exception {
        URI uri = session.getUri();
        System.out.println(uri);
        String sendId = null;
        String id = null;
        if (uri != null) {
            String query = uri.getQuery();
            if (query != null) {
                for (String param : query.split("&")) {
                    if (param.startsWith("sendId=")) {
                        sendId = param.substring("sendId=".length());
                    } else if (param.startsWith("id=")) {
                        id = param.substring("id=".length());
                    }
                }

                if (sendId != null && id != null) {
                    sessions.put(id, session);
                    String channel = generateChannelId(id, sendId);
                    userChannels.put(id, channel); // 记录用户当前所在的频道

                    // 通知对方用户当前用户已上线
                    notifyUserOnlineStatus(sendId, id, true);

                    System.out.println("用户 " + sendId + " 已连接,id=" + id);
                } else {
                    session.close();
                    return;
                }
            } else {
                session.close();
                return;
            }
        } else {
            session.close();
            return;
        }

        System.out.println("连接已建立:" + session.getId());
       // TODO 发送离线消息
        Integer Sender_Id = Integer.valueOf(sendId);
        Integer Receiver_ID = Integer.valueOf(id);
        List<String> listMessage = offlineMessagesMapper.selectMessageByReceiverId(Sender_Id, Receiver_ID);
        System.out.println("离线消息:" + listMessage);
        if (listMessage != null && !listMessage.isEmpty()) {
            offlineMessagesMapper.updateIsDelivered(Sender_Id, Receiver_ID);
//            for (String msg : listMessage) {
//                try {
//                    session.sendMessage(new TextMessage(msg));
//                    System.out.println("发送离线消息:" + msg);
//                } catch (IOException e) {
//                    System.err.println("发送离线消息失败:" + e.getMessage());
//                }
//            }
        }
    }

    @Override
    protected void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception {
        String payload = message.getPayload();
        System.out.println("收到的原始消息内容: " + payload);

        if (payload == null || !payload.trim().startsWith("{")) {
            System.err.println("无效的消息格式: " + payload);
            return;
        }

        try {
            JSONObject jsonObject = JSONUtil.parseObj(payload);
            String toUserId = jsonObject.getStr("sendId");
            String content = jsonObject.getStr("message");
            String userId = jsonObject.getStr("id");

            if (toUserId == null || toUserId.isEmpty() || content == null || content.isEmpty()) {
                System.err.println("无效的消息内容或目标用户 ID");
                return;
            }

            // 检查发送者和接收者是否在同一个频道
            String senderChannel = userChannels.get(userId);
            String receiverChannel = userChannels.get(toUserId);

            if (senderChannel != null && senderChannel.equals(receiverChannel)) {
                // 在同一频道,实时发送消息
                WebSocketSession toUserSession = sessions.get(toUserId);
                // 同时存储离线消息到数据库
                offline_messagesServiceImpl.save(Offline_messages.builder()
                        .sender_id(Integer.valueOf(userId))
                        .receiver_id(Integer.valueOf(toUserId))
                        .message(content)
                        .is_delivered(1)
                        .build());
                // 发送消息到对方用户会话
                if (toUserSession != null && toUserSession.isOpen()) {
                    toUserSession.sendMessage(new TextMessage(content));
                }
            } else {
                // 不在同一频道,存储为离线消息
                Offline_messages offlineMessage = Offline_messages.builder()
                        .sender_id(Integer.valueOf(userId))
                        .receiver_id(Integer.valueOf(toUserId))
                        .message(content)
                        .is_delivered(0)
                        .build();
                offline_messagesServiceImpl.save(offlineMessage);
            }
        } catch (JSONException e) {
            System.err.println("消息解析失败: " + e.getMessage());
        }
    }

    @Override
    public void afterConnectionClosed(WebSocketSession session, CloseStatus status) throws Exception {
        String userId = sessions.entrySet().stream()
                .filter(entry -> entry.getValue().equals(session))
                .map(Map.Entry::getKey)
                .findFirst()
                .orElse(null);

        if (userId != null) {
            sessions.remove(userId);
            String channel = userChannels.remove(userId);

            // 通知对方用户当前用户已离线
            if (channel != null) {
                String[] users = channel.split("_");
                String otherUserId = users[0].equals(userId) ? users[1] : users[0];
                notifyUserOnlineStatus(otherUserId, userId, false);
            }
        }

        System.out.println("连接已关闭:" + session.getId());
    }

    private String generateChannelId(String userId1, String userId2) {
        return userId1.compareTo(userId2) < 0 ? userId1 + "_" + userId2 : userId2 + "_" + userId1;
    }

    /**
     * 通知用户在线状态变化
     * @param userId 当前用户ID
     * @param otherUserId 对方用户I D
     * @param isOnline 是否在线
     */
    private void notifyUserOnlineStatus(String userId, String otherUserId, boolean isOnline) {
        System.out.println("通知用户 " + userId + " 对方用户 " + otherUserId + " 的在线状态: " + (isOnline ? "上线" : "离线"));
        WebSocketSession session = sessions.get(userId);
        if (session != null && session.isOpen()) {
            try {
//                JSONObject jsonObject = new JSONObject();
//                jsonObject.put("type", "status");
//                jsonObject.put("userId", otherUserId);
//                jsonObject.put("isOnline", isOnline);
                session.sendMessage(new TextMessage(isOnline ? "已在线" : "已离线"));
            } catch (IOException e) {
                System.err.println("通知用户在线状态失败: " + e.getMessage());
            }
        }
    }
}

消息存储优化方案分析:

当前:基于用户比较少,是直接存储到数据库的,这样子其实有很多问题的:

性能瓶颈: 每次消息都是直接写到数据库的,用户多了,高并发时,数据库压力大

响应延迟:同步写入数据库,我们知道一次消息的插入,细一点,也是一次数据库的连接操作,并且高并发的时候,导致发送延迟增加。

方案一:可以基于MQ异步存储

因为我们当前用户,肯定是追求于消息的同步实时性啊,把消息的插入,离线存储这些消息,扔给消息队列,让它来帮助我们消息存储,并且RabbitMQ基于毫秒级别的。

综上优点:

  • 解耦业务逻辑与存储逻辑

  • 削峰填谷,应对流量高峰

  • 可实现消息重试机制

  • 发送端响应更快

当然也设计到了消息丢失啊,这点其实做好相应的配置可以应对的:

这需要生产消息、存储消息和消费消息三个阶段共同努力才能保证消息不丢失。
**生产者的消息确认:**生产者在发送消息时,需要通过消息确认机制来确保消息成功到达。

存储消息: broker 收到消息后,需要将消息持久化到磁盘上,避免消息因内存丢失。即使消息队列服务器重启或宕机,也可以从磁盘中恢复消息。
**消费者的消息确认:**消费者在处理完消息后,再向消息队列发送确认(ACK),如果消费者未发送确认,消息队列需要重新投递该消息。

除此之外 如果消费者持续消费失败,消息队列可以自动进行重试或将消息发送到死信队列(DLQ)或通过日志等其他手段记录异常的消息,避免因一时的异常导致消息丢失。

比如 消息重复消费? 只有让消费者处理逻辑具有幂等性,保证消息同一条被消费多次,结果都是一样的作用,比如可以给每个消息加一个唯一标识(ID)去重:

在消息中引I入全局唯一ID,例如UUID、订单号等,利用redis等缓存,或者数据库来存储消息ID,然后消费者在处理消息时可以检查该消息ID是否存在代表此消息是否已经处理过。

如果有需求,也可以根据**冷热数据,**最近的聊天存储到redis中去,这样子查询历史消息也比较块,然后限制多少条,通过分页查询嘛,如果超过了,就从数据库中在去查询了。

四.功能演示:

从零实现APP实时聊天功能:WebSocket+离线消息+多

欧克,!!!就到这里把,功能还有很多可以优化的地方,看看后面有没有时间,平常没事的时候,EMO的时候,就会去悄悄去加点功能,藏在某个不起眼的地方,避免伙伴发现 !!!

相关推荐
了不起的码农13 分钟前
几种常见的HTTP方法之GET和POST
网络·网络协议·http
车载测试工程师1 小时前
SOMEIP通信矩阵解读
服务器·网络·经验分享·网络协议·车载系统
pingxiaozhao2 小时前
在内网环境中为 Gogs 配置 HTTPS 访问
网络协议·http·https
weisian1512 小时前
消息队列篇--通信协议篇--理解HTTP、TLS和TCP如何协同工作
网络协议·tcp/ip·http
车载测试工程师4 小时前
ARXML文件解析-2
java·服务器·网络·数据库·经验分享·网络协议·车载系统
阿土sap4 小时前
【ABAP】REST/HTTP技术(一)
网络·网络协议·http
天才奇男子7 小时前
VLAN(虚拟局域网)
网络·网络协议
iOS技术狂热者11 小时前
wireshak抓手机包 wifi手机抓包工具
websocket·网络协议·tcp/ip·http·网络安全·https·udp
zru_960219 小时前
Java 连接 WebSocket 入门教程
java·python·websocket
LUCIAZZZ21 小时前
计算机网络-TCP的重传机制
java·网络·网络协议·tcp/ip·计算机网络·操作系统·springboot