首先我准备用WebSocket
去尝试搭建用户和服务端之间的链接。弹幕的发送也是用WebSocket
去返回。并且项目的大致架构如下:
Socket
服务:存储WebSocket
信息。并进行消息的发送。监听等动作。只负责消息的接收和发送。创建一个maven
项目:service-gateway
。
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.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.核心负载均衡逻辑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);
}
}
注意:
com.
为开头。否则启动会报错。创建一个maven
项目:service-socket
。
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);
}
}
先写一个简略的缓存工具类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);
}
}
启动好之后可以去Nacos
上观看对应的服务
写这个标题就是提醒大家拷贝一份上面的项目,只需要改变端口号即可。记得服务名称使用同一个,这样注册到Nacos
上的时候,就会有一个服务对应多个机器,即集群的效果。如图:
查看详情之后,就可以看到每个服务具体的IP地址,还可以设置权重、元数据等信息:
我们这次的WebSocket
监听地址为:/websocket/live/{roomId}/{userId}
,我们可以利用WebSocket在线测试
向网关发起建立Socket
的请求:ws://localhost:80/websocket/live/100/1
,如图:
此时网关接收到的请求数:
WebSocket
服务A:
WebSocket
的微服务架构搭建了起来。后续的弹幕发送和共享功能还没有实现,准备放到下一篇文章来写。WebSocket
的形式去干的。后期可能会在写一套,使用Netty
去替代。Session
是不可以被序列化的,如图:RabbitMQ
做一个广播通知(包含直播间号)。让所有机器感知到发送消息的动作。然后每台机器从本地缓存中拿到当前这个直播间的所有用户。再去循环分发弹幕消息。Redis
,减少内存消耗。欢迎大家指出毛病,或者对这方面有什么思考,可以一起交流一下。