基于websocket简单实现消息小红点

804 阅读3分钟

消息推送

消息推送一般分为移动端的消息推送和web端消息的推送,这里主要将web端消息的推送,实现我们常见的小红点;

环境部署

后端

SpringBoot 2.3.7

依赖

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-websocket</artifactId>
</dependency>

启动服务

  1. 配置类
@Configuration
public class WebSocketConfig {
    @Bean
    public ServerEndpointExporter serverEndpointExporter() {
        return new ServerEndpointExporter();
    }
}
  1. 服务类
@ServerEndpoint("/websocket/{userId}")
@Component
@Slf4j
public class WebSocketServer {

    private static MessageChatService messageChatService;

    @Autowired
    public void setMessageChatService(MessageChatService messageChatService) {
        this.messageChatService = messageChatService;
    }

    /**
     * 连接成功的方法
     *
     * @param session
     * @param userId
     */
    @OnOpen
    public void onOpen(Session session, @PathParam("userId") String userId) {
        ManagerWebSocketSession.addOnlineUser(userId, session);
    }

    /**
     * 断开连接的方法
     */
    @OnClose
    public void onClose(Session session, @PathParam("userId") String userId) {
        ManagerWebSocketSession.removeOnlineUser(userId);
        log.info("用户sessionId为{}退出了", session.getId());
    }

    /**
     * 收到客户端的信息
     *
     * @param message
     * @param session
     */
    @OnMessage
    public void onMessage(String message, Session session) {
        messageChatService.sendPrivateMessage(message);
        log.info("收到的信息为:{}, sessionId为{}", message, session.getId());
    }

    /**
     * 错误处理
     *
     * @param session
     * @param error
     */
    @OnError
    public void onError(Session session, Throwable error) {
        log.warn("用户sessionId为{}发生了错误,错误原因为{}", session.getId(), error.toString());
    }
}
  1. 管理session类
@Slf4j
public class ManagerWebSocketSession {

    // 存储用户连接的session
    private static final ConcurrentHashMap<String, Session> webSocketSessionMap = new ConcurrentHashMap<>();

    // 在线用户的数量
    private static final AtomicInteger onlineUsersCount = new AtomicInteger();

    /**
     * 添加在线用户
     * @param userId
     * @param session
     */
    public static void addOnlineUser(String userId, Session session) {
        // 存储用户的session
        if(!webSocketSessionMap.containsKey(userId)) {
            webSocketSessionMap.put(userId, session);
            log.info("用户id为{},sessionId为{}", userId, session.getId());
            onlineUsersCount.getAndIncrement();
        }
    }

    /**
     * 获取用户的session
     * @param userId
     * @return
     */
    public static Session getSessionByUserId(String userId) {
        return webSocketSessionMap.getOrDefault(userId, null);
    }

    /**
     * 移除在线用户
     * @param userId
     */
    public static void removeOnlineUser(String userId) {
        if (webSocketSessionMap.containsKey(userId)) {
            webSocketSessionMap.remove(userId);
            onlineUsersCount.decrementAndGet();
        }
    }

    /**
     * 判断用户是否在线
     * @param userId
     * @return
     */
    public static boolean isUserOnline(String userId) {
        return webSocketSessionMap.containsKey(userId);
    }

    /**
     * 获取session的集合
     * @return
     */
    public static ConcurrentHashMap<String, Session> getWebSocketSessionMap() {
        return webSocketSessionMap;
    }

    /**
     * 获取在线人数
     * @return
     */
    public static int getOnlineUsersCount() {
        return onlineUsersCount.get();
    }
}

前端

基于React的实现

const WebSocket = () => {
    const [webSocket, setWebSocket] = useState(new WebSocket(WEB_SOCKET_URL + getUserId()))

    const openWebSocket = () => {
        if (!window.WebSocket) {
            alert('浏览器不支持websocket')
            return
        }
        webSocket.onopen = (ev) => {
            console.log('开启websocket')
        }

        webSocket.onclose = (ev) => {
            console.log(`关闭websocket 关闭的原因:${ev.reason},关闭的状态码:${ev.code}`)
        }

        webSocket.onmessage = (ev) => {
            console.log(`收到客户端的消息为${ev.data}`)
          
        }
    }
 
    return (
        <>
            <div></div>
        </>
    );
};

export default WebSocket;

存储系统设计

数据库表

create table message
(
    id              bigint auto_increment comment '自增主键'
        primary key,
    from_id         varchar(255)                        not null comment '发送者id',
    to_id           varchar(255)                        not null comment '接收者id',
    message_type    tinyint                             not null comment '消息类型',
    owner_id        bigint    default -1                not null comment '资源类型',
    message_content varchar(512)                        null comment '消息内容',
    is_read         char      default '0'               null comment '是否已读 1-已读 0-未读',
    create_time     timestamp default CURRENT_TIMESTAMP not null comment '创建时间'
)
    comment '消息表';

Redis键值设计

我这里考虑使用hash数据结构,一共有三类消息(评论,点赞,关注),所以可以使用三个hash,对应的三个key分别是:

message:reminder:comment

message:reminder:like

message:reminder:follow

每个hash里面的hashkey 为userId,值为对应用户所接受到的消息个数即可

实现逻辑

后端

  1. 当用户评论、点赞、关注之后,调用方法封装消息体,并存入数据库(便于未来寻找详细的消息信息)
  2. 然后通过websocket把评论、点赞、关注的消息传递到客户端
  3. 如果不在线,就把消息的个数存入到Redis中,用户登录后再请求固定的接口即可
  4. 如果在线,就让前端重新渲染,让红点+1即可

若用户持续在线,使用websocket就可以实时地将消息发送到客户端,客户端就可以根据消息的数量显示小红点的数量;

发送消息的代码

 /**
     * 发送消息
     * @param fromId 发送者
     * @param toId 接受者
     * @param content 内容
     * @param ownerId 资源类型
     * @param messageType 消息类型
     */
    public void sendRedDotReminder(String fromId, String toId, String content, Long ownerId, int messageType) {


        // 将信息存入数据库
        storeMessage2DB(fromId, toId, content, ownerId, messageType);
        // 构造信息传递对象
        MessageTypeVo messageTypeVo = new MessageTypeVo(messageType, toId);
        String toJson = GsonUtils.gson.toJson(messageTypeVo, MessageTypeVo.class);

        Session session = ManagerWebSocketSession.getSessionByUserId(toId);

        // 如果在线
        if (session != null) {
            try {
                session.getBasicRemote().sendText(toJson);
            } catch (IOException e) {
                e.printStackTrace();
            }
        } else {
            // 不在线就 存储进入redis中 存储数量即可
            String messageKey = hashMap.get(messageType);

            Boolean aBoolean = redisTemplate.opsForHash().putIfAbsent(messageKey, toId, 1);
            if (!aBoolean) {
                redisTemplate.opsForHash().increment(messageKey, toId, 1);
            }
        }
    }

前端

以下是实现的用例图

  1. 用户点击相应的红点,则取消;
  2. 如果点入子列表,要删除Redis中存储的数量,然后加载具体的消息内容即可;