仿B站项目第四章学习(3)

弹幕系统设计

场景分析:客户端针对视频发送弹幕,后端对所有正在观看的用户推送该弹幕

实现方式:短连接通信长连接通信

        短连接实现方案

实现:所有观看的客户端不断轮询后端,若有新的弹幕则拉取显示

缺点:轮询效率低,后端压力较大,非常浪费资源。

        长连接实现方案

实现:采用websocket进行前后端通信,

websocket简介:基于TCP的一种新的网络协议,实现了浏览器与服务器之间的全双工通信

弹幕系统架构设计

  1. WebSocket通信:服务器和客户端之间通过WebSocket建立长连接,实现双向通信。客户端可以发送弹幕内容和时间戳给服务器,并接收其他用户发来的弹幕信息。这种实时通信机制能够确保用户能够即时收发弹幕信息。

  2. Nginx负载均衡:Nginx作为反向代理服务器,可以根据负载均衡策略将客户端的请求分发到多台后端服务器,确保系统的高可用性和可伸缩性。

  3. RocketMQ消息队列:当服务器接收到客户端发送的弹幕信息时,可以通过RocketMQ实现对弹幕信息的异步处理和持久化。弹幕信息被发送到消息队列中,并通过消费者异步地进行数据库操作,将弹幕信息持久化到数据库中。这种异步处理可以提高系统的吞吐量和性能。

  4. Redis快速读写:Redis作为缓存数据库,可以快速地读写弹幕信息。服务器可以将热门的弹幕信息存储在Redis中,以提高读取速度。此外,服务器定时将Redis中的弹幕信息同步到数据库中,确保数据的持久性。

  5. 定时任务与数据库同步:服务器会定时检查Redis中的弹幕信息,并将其持久化到数据库中,以确保数据的一致性和完整性。

  6. 并发推送弹幕消息:服务器通过RocketMQ将弹幕信息推送给客户端,实现并发向多个客户端推送弹幕消息的功能。这种异步推送的方式可以有效减轻服务器压力,提高系统的并发处理能力。

Spring Boot项目整合websocket实现弹幕功能

1. 引入依赖:版本号同步自己springboot版本

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

2. websocketconfig配置类

package com.imooc.bilibili.service.config;

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

@Configuration
public class WebSocketConfig {

    @Bean
    public ServerEndpointExporter serverEndpointExporter(){
        return new ServerEndpointExporter();
    }
}
  1. @Configuration 注解标识这是一个配置类,Spring会在应用程序启动时加载并进行相应的配置。

  2. @Bean 注解用于向Spring容器中注册一个bean,这里注册了一个名为 serverEndpointExporter 的bean。

  3. public ServerEndpointExporter serverEndpointExporter() 方法实例化并返回一个 ServerEndpointExporter 对象,这个对象是用于在Spring中注册WebSocket端点。

  4. ServerEndpointExporter 是Spring框架提供的一个类,用于注册和暴露WebSocket端点。它可以自动注册使用@ServerEndpoint 注解声明的WebSocket端点。

3. websocketserive



@Component
@ServerEndpoint("/imserver/{token}")
public class WebSocketService {

    private final Logger logger =  LoggerFactory.getLogger(this.getClass());

    private static final AtomicInteger ONLINE_COUNT = new AtomicInteger(0);

    public static final ConcurrentHashMap<String, WebSocketService> WEBSOCKET_MAP = new ConcurrentHashMap<>();

    private Session session;

    private String sessionId;

    private Long userId;

    private static ApplicationContext APPLICATION_CONTEXT;

    public static void setApplicationContext(ApplicationContext applicationContext){
        WebSocketService.APPLICATION_CONTEXT = applicationContext;
    }

    @OnOpen
    public void openConnection(Session session, @PathParam("token") String token){
        try{
            this.userId = TokenUtil.verifyToken(token);
        }catch (Exception ignored){}
        this.sessionId = session.getId();
        this.session = session;
        if(WEBSOCKET_MAP.containsKey(sessionId)){
            WEBSOCKET_MAP.remove(sessionId);
            WEBSOCKET_MAP.put(sessionId, this);
        }else{
            WEBSOCKET_MAP.put(sessionId, this);
            ONLINE_COUNT.getAndIncrement();
        }
        logger.info("用户连接成功:" + sessionId + ",当前在线人数为:" + ONLINE_COUNT.get());
        try{
            this.sendMessage("0");
        }catch (Exception e){
            logger.error("连接异常");
        }
    }

    @OnClose
    public void closeConnection(){
        if(WEBSOCKET_MAP.containsKey(sessionId)){
            WEBSOCKET_MAP.remove(sessionId);
            ONLINE_COUNT.getAndDecrement();
        }
        logger.info("用户退出:" + sessionId + "当前在线人数为:" + ONLINE_COUNT.get());
    }

    @OnMessage
    public void onMessage(String message){
        logger.info("用户信息:" + sessionId + ",报文:" + message);
        if(!StringUtil.isNullOrEmpty(message)){
            try{
                //群发消息
                for(Map.Entry<String, WebSocketService> entry : WEBSOCKET_MAP.entrySet()){
                    WebSocketService webSocketService = entry.getValue();
                    DefaultMQProducer danmusProducer = (DefaultMQProducer)APPLICATION_CONTEXT.getBean("danmusProducer");
                    JSONObject jsonObject = new JSONObject();
                    jsonObject.put("message", message);
                    jsonObject.put("sessionId", webSocketService.getSessionId());
                    Message msg = new Message(UserMomentsConstant.TOPIC_DANMUS, jsonObject.toJSONString().getBytes(StandardCharsets.UTF_8));
                    RocketMQUtil.asyncSendMsg(danmusProducer, msg);
                }
                if(this.userId != null){
                    //保存弹幕到数据库
                    Danmu danmu = JSONObject.parseObject(message, Danmu.class);
                    danmu.setUserId(userId);
                    danmu.setCreateTime(new Date());
                    DanmuService danmuService = (DanmuService)APPLICATION_CONTEXT.getBean("danmuService");
                    danmuService.asyncAddDanmu(danmu);
                    //保存弹幕到redis
                    danmuService.addDanmusToRedis(danmu);
                }
            }catch (Exception e){
                logger.error("弹幕接收出现问题");
                e.printStackTrace();
            }
        }
    }

    @OnError
    public void onError(Throwable error){
    }

    public void sendMessage(String message) throws IOException {
        this.session.getBasicRemote().sendText(message);
    }

    //或直接指定时间间隔,例如:5秒
    @Scheduled(fixedRate=5000)
    private void noticeOnlineCount() throws IOException {
        for(Map.Entry<String, WebSocketService> entry : WebSocketService.WEBSOCKET_MAP.entrySet()){
            WebSocketService webSocketService = entry.getValue();
            if(webSocketService.session.isOpen()){
                JSONObject jsonObject = new JSONObject();
                jsonObject.put("onlineCount", ONLINE_COUNT.get());
                jsonObject.put("msg", "当前在线人数为" + ONLINE_COUNT.get());
                webSocketService.sendMessage(jsonObject.toJSONString());
            }
        }
    }

    public Session getSession() {
        return session;
    }

    public String getSessionId() {
        return sessionId;
    }
}

多例模式bean注入

  1. setApplicationContext 方法:静态方法,用于设置应用程序上下文。

  2. openConnection 方法:在客户端与服务器建立WebSocket连接时被调用。验证用户的token,处理连接相关逻辑,将新连接添加到WEBSOCKET_MAP中,并发送初始消息给客户端。

  3. closeConnection 方法:在连接关闭时被调用。从WEBSOCKET_MAP中移除相应的连接,并更新在线人数。

  4. onMessage 方法:当接收到客户端传来的消息时被调用。处理消息内容,群发消息给所有在线客户端,保存弹幕数据到数据库和Redis中。

  5. onError 方法:当发生WebSocket连接错误时被调用。可用于处理连接错误的情况。

  6. sendMessage 方法:向客户端发送文本消息。

  7. noticeOnlineCount 方法:定时任务方法,每隔5秒发送在线用户数给所有在线客户端。

  8. getSession 方法:获取WebSocket连接的会话对象Session。

  9. getSessionId 方法:获取当前WebSocket连接的会话ID。

弹幕系统实现

弹幕数据库表

总结

用户观看视频时,前端会访问danmu接口,从弹幕数据库表中取出该视频的历史弹幕并展示。

当用户发送弹幕时,由于使用了WebSocket建立长连接,是的服务器可以向客户端发起通信,所以当用户发送弹幕时,服务器会群发该条弹幕给正在观看这个视频的客户端

定时通知在线人数的功能(noticeOnlineCount方法),通过固定频率(例如,每5秒)向所有在线用户推送当前的在线人数信息。这增加了实时交互性和用户的参与感。

  • 12
    点赞
  • 17
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值