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

一、功能概述
-
核心功能亮点
-
基于WebSocket的实时双向通信
-
完善的离线消息存储机制
-
多媒体消息支持(表情包、图片等)
-
未读消息提醒与通知
-
-
应用场景
-
社交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的时候,就会去悄悄去加点功能,藏在某个不起眼的地方,避免伙伴发现 !!!
