视觉中国网站福州短视频seo方法
引言:最近不是团队继续把以前的一个项目拿来继续完善了一下嘛,但是本人就非常反骨,想着什么好玩的,就立马悄悄马上去添加,实现它!!!等到伙伴拉到新的代码,总是会说:你怎么又添加功能了啊?什么时候加的啊?怎么又不给我们商量啊?哈哈,我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 帧格式。 -
双向实时通信
连接建立后,双方可随时主动发送数据,无需等待请求-响应模式。 -
低开销
数据传输采用轻量级的二进制帧格式(相比 HTTP 头部开销显著减少)。 -
持久化连接
默认保持连接状态,避免重复握手(可通过心跳包维持连接活性)
示例部分代码(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);},});},
后端部分:
@Configuration
@EnableWebSocket
public class WebSocketConfig implements WebSocketConfigurer {private final MyWebSocketHandler myWebSocketHandler;public WebSocketConfig(MyWebSocketHandler myWebSocketHandler) {this.myWebSocketHandler = myWebSocketHandler;}@Overridepublic 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 {@Autowiredprivate Offline_messagesServiceImpl offline_messagesServiceImpl;@Autowiredprivate 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;}@Overridepublic 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());
// }
// }}}@Overrideprotected 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());}}@Overridepublic 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的时候,就会去悄悄去加点功能,藏在某个不起眼的地方,避免伙伴发现 !!!