直播弹幕系统(一)- SpringCloud网关对WebSocket链接进行负载均衡

直播弹幕系统(一)- SpringCloud网关对WebSocket链接进行负载均衡

  • 前言
  • 一. Gateway网关搭建
    • 1.1 配置文件相关
    • 1.2 网关配置
    • 1.3 负载均衡配置
  • 二. Websocket服务搭建
    • 2.1 配置文件相关
    • 2.2 WebSocket监听
    • 2.3 WebSocket服务集群
    • 2.4 路由分发测试
    • 2.5 后续展望(待更新)

前言

首先我准备用WebSocket去尝试搭建用户和服务端之间的链接。弹幕的发送也是用WebSocket去返回。并且项目的大致架构如下:

  • 网关:用来负载均衡路由请求,以及做一些过滤,拦截等操作。
  • Socket服务:存储WebSocket信息。并进行消息的发送。监听等动作。只负责消息的接收和发送。
  • 弹幕服务:负责弹幕的相关业务逻辑处理。

大致流程图如下:
直播弹幕系统(一)- SpringCloud网关对WebSocket链接进行负载均衡_第1张图片

一. Gateway网关搭建

创建一个maven项目:service-gateway

1.1 配置文件相关

1.pom依赖:

<parent>
    <groupId>org.springframework.bootgroupId>
    <artifactId>spring-boot-starter-parentartifactId>
    <version>2.3.2.RELEASEversion>
    <relativePath/> 
parent>
<properties>
    <java.version>1.8java.version>
    <spring-cloud.version>Hoxton.SR8spring-cloud.version>
properties>
<dependencies>
    <dependency>
        <groupId>org.apache.commonsgroupId>
        <artifactId>commons-lang3artifactId>
        <version>3.12.0version>
    dependency>
    <dependency>
        <groupId>org.springframework.cloudgroupId>
        <artifactId>spring-cloud-starter-gatewayartifactId>
    dependency>
    <dependency>
        <groupId>com.alibaba.cloudgroupId>
        <artifactId>spring-cloud-starter-alibaba-nacos-discoveryartifactId>
        <version>2.2.4.RELEASEversion>
    dependency>
    <dependency>
        <groupId>org.projectlombokgroupId>
        <artifactId>lombokartifactId>
    dependency>
dependencies>
<dependencyManagement>
    <dependencies>
        <dependency>
            <groupId>org.springframework.cloudgroupId>
            <artifactId>spring-cloud-dependenciesartifactId>
            <version>${spring-cloud.version}version>
            <type>pomtype>
            <scope>importscope>
        dependency>
    dependencies>
dependencyManagement>
<build>
    <plugins>
        <plugin>
            <groupId>org.springframework.bootgroupId>
            <artifactId>spring-boot-maven-pluginartifactId>
        plugin>
    plugins>
build>

<repositories>
    <repository>
        <id>spring-milestonesid>
        <name>Spring Milestonesname>
        <url>https://repo.spring.io/milestoneurl>
    repository>
repositories>

2.application.yml

spring:
  cloud:
    gateway:
      routes:
        - id: tv-service-socket
        # lb代表负载均衡,ws是websocket协议,tv-service-socket是我的服务名
          uri: lb:ws://tv-service-socket
          predicates:
            - Path=/websocket/**

3.application.properties

spring.application.name=tv-service-gateway
spring.cloud.nacos.discovery.server-addr=你的Nacos地址和端口
server.port=80

4.bootstrap.yml

spring:
  application:
    name: tv-service-gateway
  cloud:
    nacos:
      discovery:
        server-addr: 你的Nacos地址和端口

1.2 网关配置

1.GatewayConfig

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.reactive.CorsWebFilter;
import org.springframework.web.cors.reactive.UrlBasedCorsConfigurationSource;

/**
 * @author Zong0915
 * @date 2022/10/29 上午11:10
 */
@Configuration
public class GatewayConfig {
    @Bean
    public CorsWebFilter corsWebFilter() {
        UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
        // 配置跨域
        CorsConfiguration corsConfiguration = new CorsConfiguration();
        corsConfiguration.addAllowedHeader("*");
        corsConfiguration.addAllowedMethod("*");
        corsConfiguration.addAllowedOrigin("*");
        corsConfiguration.setAllowCredentials(true);// 是否允许携带cookie跨域
        // 任意路径都需要跨域
        source.registerCorsConfiguration("/**", corsConfiguration);
        return new CorsWebFilter(source);
    }
}

1.3 负载均衡配置

1.核心负载均衡逻辑WeightBalanceConfig

import com.alibaba.cloud.nacos.NacosDiscoveryProperties;
import com.alibaba.cloud.nacos.NacosServiceManager;
import com.alibaba.cloud.nacos.ribbon.NacosServer;
import com.alibaba.nacos.api.exception.NacosException;
import com.alibaba.nacos.api.naming.NamingService;
import com.alibaba.nacos.api.naming.pojo.Instance;
import com.netflix.client.config.IClientConfig;
import com.netflix.loadbalancer.AbstractLoadBalancerRule;
import com.netflix.loadbalancer.BaseLoadBalancer;
import com.netflix.loadbalancer.Server;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;

import java.util.List;
import java.util.concurrent.atomic.AtomicInteger;

/**
 * @author Zong0915
 * @date 2022/12/11 下午1:56
 */
@Slf4j
public class WeightBalanceConfig extends AbstractLoadBalancerRule {
    @Autowired
    private NacosServiceManager nacosServiceManager;

    @Autowired
    private NacosDiscoveryProperties nacosDiscoveryProperties;
    private static AtomicInteger COUNT = new AtomicInteger(0);

    public WeightBalanceConfig() {
    }

    @Override
    public void initWithNiwsConfig(IClientConfig iClientConfig) {

    }

    @Override
    public Server choose(Object o) {
        try {
            // 1、获取当前服务的分组名称
            String groupName = nacosDiscoveryProperties.getGroup();
            // 2、获取当前服务的负载均衡器
            BaseLoadBalancer baseLoadBalancer = (BaseLoadBalancer) this.getLoadBalancer();
            // 3、获取目标服务的服务名
            String serviceName = baseLoadBalancer.getName();
            // 4、获取nacos提供的服务注册api
            NamingService namingService = nacosServiceManager.getNamingService(nacosDiscoveryProperties.getNacosProperties());
            // 5、根据目标服务名称和分组名称去获取服务实例,nacos实现了权重的负载均衡算法  false: 及时获取nacos注册服务信息
            List<Instance> allInstances = namingService.getAllInstances(serviceName, groupName, false);
            Instance instance = WeightedBalancer.chooseInstanceByRandomWeight(allInstances);
            // 请求总数
            COUNT.incrementAndGet();
            log.info("路由的请求总数: {}。", COUNT.intValue());
            return new NacosServer(instance);
        } catch (NacosException e) {
            log.error("自定义负载均衡策略-权重 调用异常: ", e);
            return null;
        }
    }
}

2.获取权重的工具类WeightedBalancer

import com.alibaba.nacos.api.naming.pojo.Instance;
import com.alibaba.nacos.client.naming.core.Balancer;

import java.util.List;

/**
 * @author Zong0915
 * @date 2022/12/11 下午2:21
 */
public class WeightedBalancer extends Balancer {
    public static Instance chooseInstanceByRandomWeight(List<Instance> instanceList) {
        // 这是父类Balancer自带的根据随机权重获取服务的方法.
        return getHostByRandomWeight(instanceList);
    }
}

3.Ribbon配置类GlobalRibbonConfig

import com.netflix.loadbalancer.IRule;
import kz.lw.wzs.balance.WeightBalanceConfig;
import lombok.Data;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

/**
 * @author Zong0915
 * @date 2022/12/11 下午1:59
 */
@Configuration
@Data
public class GlobalRibbonConfig {
    @Bean
    public IRule getRule() {
        return new WeightBalanceConfig();
    }
}

4.CustomRibbonConfig类:

@Configuration
@RibbonClients(defaultConfiguration = GlobalRibbonConfig.class)
public class CustomRibbonConfig {
}

5.网关启动类(网关一般不需要数据源,所以记得排除掉)

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration;
import org.springframework.boot.autoconfigure.jdbc.DataSourceTransactionManagerAutoConfiguration;
import org.springframework.boot.autoconfigure.orm.jpa.HibernateJpaAutoConfiguration;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;

/**
 * @author Zong0915
 * @date 2022/10/29 上午11:10
 */
@SpringBootApplication(exclude = {DataSourceAutoConfiguration.class,
        DataSourceTransactionManagerAutoConfiguration.class,
        HibernateJpaAutoConfiguration.class})
@EnableDiscoveryClient
public class GatewayServiceApplication {
    public static void main(String[] args) {
        SpringApplication.run(GatewayServiceApplication.class, args);
    }
}

注意:

  1. 包名不要使用com.为开头。否则启动会报错。

二. Websocket服务搭建

创建一个maven项目:service-socket

2.1 配置文件相关

1.pom依赖:

<parent>
    <groupId>org.springframework.bootgroupId>
    <artifactId>spring-boot-starter-parentartifactId>
    <version>2.3.2.RELEASEversion>
    <relativePath/> 
parent>
<dependencies>
    <dependency>
        <groupId>org.springframework.bootgroupId>
        <artifactId>spring-boot-starter-amqpartifactId>
    dependency>
    <dependency>
        <groupId>org.springframework.bootgroupId>
        <artifactId>spring-boot-starter-webartifactId>
    dependency>
    <dependency>
        <groupId>org.springframework.bootgroupId>
        <artifactId>spring-boot-starter-websocketartifactId>
    dependency>
    <dependency>
        <groupId>com.alibaba.cloudgroupId>
        <artifactId>spring-cloud-starter-alibaba-nacos-discoveryartifactId>
        <version>2.2.1.RELEASEversion>
        <exclusions>
            <exclusion>
                <artifactId>archaius-coreartifactId>
                <groupId>com.netflix.archaiusgroupId>
            exclusion>
            <exclusion>
                <artifactId>commons-ioartifactId>
                <groupId>commons-iogroupId>
            exclusion>
            <exclusion>
                <artifactId>commons-lang3artifactId>
                <groupId>org.apache.commonsgroupId>
            exclusion>
            <exclusion>
                <artifactId>fastjsonartifactId>
                <groupId>com.alibabagroupId>
            exclusion>
            <exclusion>
                <artifactId>guavaartifactId>
                <groupId>com.google.guavagroupId>
            exclusion>
            <exclusion>
                <artifactId>httpclientartifactId>
                <groupId>org.apache.httpcomponentsgroupId>
            exclusion>
            <exclusion>
                <artifactId>servo-coreartifactId>
                <groupId>com.netflix.servogroupId>
            exclusion>
        exclusions>
    dependency>
    <dependency>
        <groupId>commons-collectionsgroupId>
        <artifactId>commons-collectionsartifactId>
        <version>3.2.2version>
    dependency>
    <dependency>
        <groupId>org.apache.commonsgroupId>
        <artifactId>commons-lang3artifactId>
        <version>3.12.0version>
    dependency>
    <dependency>
        <groupId>com.alibabagroupId>
        <artifactId>fastjsonartifactId>
        <version>1.2.79version>
    dependency>
    <dependency>
        <groupId>org.projectlombokgroupId>
        <artifactId>lombokartifactId>
        <version>1.18.24version>
    dependency>
dependencies>

2.application.properties文件:

spring.application.name=tv-service-socket
spring.cloud.nacos.discovery.server-addr=你的Nacos地址
server.port=81

3.boostrap.yml文件:

spring:
  application:
    name: tv-service-socket
  cloud:
    nacos:
      discovery:
        server-addr: 你的Nacos地址和端口

2.WebSocket配置WebSocketConfig

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

/**
 * @author Zong0915
 * @date 2022/12/10 下午9:46
 */
@Configuration
public class WebSocketConfig {
    @Bean
    public ServerEndpointExporter serverEndpointExporter(){
        return new ServerEndpointExporter();
    }
}

3.启动类:

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;

/**
 * @author Zong0915
 * @date 2022/12/10 下午9:44
 */
@SpringBootApplication
@EnableDiscoveryClient
public class SocketApplication {
    public static void main(String[] args) {
        SpringApplication.run(SocketApplication.class, args);
    }
}

2.2 WebSocket监听

先写一个简略的缓存工具类SocketCache,用来缓存WebSocket信息。

import com.socket.BulletScreenServer;
import org.apache.commons.lang3.StringUtils;

import java.util.concurrent.ConcurrentHashMap;

/**
 * @Date 2022/12/12 15:25
 * @Created by jj.lin
 */
public class SocketCache {
    /**
     * 分段的缓存长度
     */
    private static final Integer SEGMENT_LENGTH = 16;
    /**
     * 分段存储
     */
    private static final ConcurrentHashMap<Integer, ConcurrentHashMap<String, BulletScreenServer>> CACHE_SEGMENT =
            new ConcurrentHashMap<>(SEGMENT_LENGTH);

    public static void put(String sessionId, BulletScreenServer bulletScreenServer) {
        if (StringUtils.isBlank(sessionId)) {
            return;
        }
        // 取余数,根据16取模
        Integer index = getIndex(sessionId);
        if (!CACHE_SEGMENT.containsKey(index)) {
            ConcurrentHashMap<String, BulletScreenServer> cache = createCache();
            cache.put(sessionId, bulletScreenServer);

            CACHE_SEGMENT.put(index, cache);
        } else {
            ConcurrentHashMap<String, BulletScreenServer> cache = CACHE_SEGMENT.get(index);
            cache.put(sessionId, bulletScreenServer);
        }
    }

    public static void remove(String sessionId) {
        Integer index = getIndex(sessionId);
        if (!CACHE_SEGMENT.containsKey(index)) {
            return;
        }
        ConcurrentHashMap<String, BulletScreenServer> cache = CACHE_SEGMENT.get(index);
        cache.remove(sessionId);
    }

    public static Integer getIndex(String sessionId) {
        return sessionId.hashCode() & (SEGMENT_LENGTH - 1);
    }

    private static ConcurrentHashMap<String, BulletScreenServer> createCache() {
        return new ConcurrentHashMap<>(10000);
    }

}

创建BulletScreenServer类。

import com.cache.SocketCache;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.stereotype.Component;

import javax.websocket.OnClose;
import javax.websocket.OnMessage;
import javax.websocket.OnOpen;
import javax.websocket.Session;
import javax.websocket.server.PathParam;
import javax.websocket.server.ServerEndpoint;
import java.util.concurrent.atomic.AtomicLong;

/**
 * @author Zong0915
 * @date 2022/12/9 下午3:45
 */
@Component
@ServerEndpoint("/websocket/live/{roomId}/{userId}")
@Slf4j
public class BulletScreenServer {
    private static final AtomicLong count = new AtomicLong(0);

    private Session session;
    private String sessionId;
    private String userId;
    private String roomId;

    /**
     * 打开连接
     *
     * @param session
     * @OnOpen 连接成功后会自动调用该方法
     * @PathParam("token") 获取 @ServerEndpoint("/imserver/{userId}") 后面的参数
     */
    @OnOpen
    public void openConnection(Session session, @PathParam("roomId") String roomId, @PathParam("userId") String userId) {
        // 如果是游客观看视频,虽然有弹幕,但是没有用户信息,所以需要用try
        count.incrementAndGet();
        log.info("*************WebSocket连接次数: {} *************", count.longValue());
        this.userId = userId;
        this.roomId = roomId;
        // 保存session相关信息到本地
        this.sessionId = session.getId();
        this.session = session;
        SocketCache.put(sessionId, this);
    }

    /**
     * 客户端刷新页面,或者关闭页面,服务端断开连接等等操作,都需要关闭连接
     */
    @OnClose
    public void closeConnection() {
        SocketCache.remove(sessionId);
    }

    /**
     * 客户端发送消息给服务端
     *
     * @param message
     */
    @OnMessage
    public void onMessage(String message) {
        if (StringUtils.isBlank(message)) {
            return;
        }
        // 发送消息,更新在线人数以及弹幕
        // sendMessage(message, 2);
    }
}

最终的项目架构:
直播弹幕系统(一)- SpringCloud网关对WebSocket链接进行负载均衡_第2张图片

启动好之后可以去Nacos上观看对应的服务

2.3 WebSocket服务集群

写这个标题就是提醒大家拷贝一份上面的项目,只需要改变端口号即可。记得服务名称使用同一个,这样注册到Nacos上的时候,就会有一个服务对应多个机器,即集群的效果。如图:
直播弹幕系统(一)- SpringCloud网关对WebSocket链接进行负载均衡_第3张图片
查看详情之后,就可以看到每个服务具体的IP地址,还可以设置权重、元数据等信息:
直播弹幕系统(一)- SpringCloud网关对WebSocket链接进行负载均衡_第4张图片

2.4 路由分发测试

我们这次的WebSocket监听地址为:/websocket/live/{roomId}/{userId},我们可以利用WebSocket在线测试

向网关发起建立Socket的请求:ws://localhost:80/websocket/live/100/1,如图:
直播弹幕系统(一)- SpringCloud网关对WebSocket链接进行负载均衡_第5张图片
此时网关接收到的请求数:
在这里插入图片描述
WebSocket服务A:
在这里插入图片描述

WebSocket服务B:
在这里插入图片描述

2.5 后续展望(待更新)

  1. 本篇文章只把基于WebSocket的微服务架构搭建了起来。后续的弹幕发送和共享功能还没有实现,准备放到下一篇文章来写。
  2. 我写的时候,我心里也是清楚,真正的弹幕系统是不会用这种纯WebSocket的形式去干的。后期可能会在写一套,使用Netty去替代。
  3. 拥有本地缓存的分布式系统,不可避免的有一个共性问题:分布式系统下的缓存不一致问题。而原生的Session是不可以被序列化的,如图:
    在这里插入图片描述
  4. 如果只能用本地缓存(硬杠到底),那么弹幕进行共享分发的时候,在分布式的情况下,怎么通知到所有的机器?这里我想的是使用RabbitMQ做一个广播通知(包含直播间号)。让所有机器感知到发送消息的动作。然后每台机器从本地缓存中拿到当前这个直播间的所有用户。再去循环分发弹幕消息。
  5. 本地缓存中的用户信息、直播间号可以存入Redis,减少内存消耗。
  6. 分发信息通过for循环遍历,有无更好的替代方案,有待探究。

欢迎大家指出毛病,或者对这方面有什么思考,可以一起交流一下。

你可能感兴趣的:(RabbitMQ,弹幕系统设计,java,spring,boot,websocket)