前言
本篇笔记着重于两方面:
1.开源项目实现的功能。关键功能具体代码。
2.在此架构上做二次开发,具体操作。(业务扩展,架构优化)
更详细开源项目信息请查看《参考文献》;
开源背景
SONA 简介
- sona 是一个由比心语音技术团队开发,用于快速搭建语音房产品的全端解决方案,支撑了比心聊天室、直播、游戏房等业务。
- sona 提供了包括房间管理、实时音视频、房间IM在内的丰富的能力,快捷的接入方式,以及可商用的性能和稳定性。
- sona 用统一的接入方式封装了不同RTC厂商sdk,方便使用各厂家的用户能以最小成本接入和切换。
SONA 核心功能 - 基础房间管理能力:房间操作、人员权限、房间管理、在线列表等。
- RTC(实时音视频):rtc推拉流、转推CDN拉流、混流、录制等。
- 房间IM:聊天室消息、群组消息、点对点消息、广播消息。
SONA 优势 - 一站式解决语音房间底层技术问题,让你可以专注于业务功能开发。
- 集成了多家RTC云商,细粒度到可以支持单房间一键热切云商sdk。
- 高吞吐低延迟的IM,通过消息分级控制,确保重要消息如高价值礼物不会丢失。
项目概述
完整版比心
项目流程图(完整版~~)~~
部署架构
系统结构
比心陪玩业务(完整版)
模块依赖图
图讲项目
连接方式
发送消息的频率是否是配置的
SONA架构图
核心功能
RTC(实时音视频)
云商回调
音视频主要流程
房间IM
消息全链路查询
SONA一共支持 4 种消息:
- 聊天室消息
当前房间是聊天室模式,比如直播间这种,一条消息发送给房间内的所有在线用户
- 群组消息
当前房间是群组模式,比如游戏房这种,一条消息发送给房间内的所有用户
- 点对点消息
不论是群组模式还是聊天室模式,都可以使用点对点消息,一条消息指定发送给房间内的某个或者某些用户,未被指定的用户是收不到的
- 广播消息
只支持聊天室模式,一条消息同时发送给多个房间,比如给所有聊天室都发送横幅等场景,就可以使用
消息发送(长短连接)
发送流程图
基础房间管理能力
在线列表
- 对于群组模式的房间,获取列表就是 当前群组的所有用户列表,不管用户是否长连在线
- 对于聊天室模式的房间,获取在线列表,是拿到当前房间内所有长链在线的用户列表
对于sona来说,用户是否在线,完全依赖长连接,长连在就认为在线,长链不在就认为离线
模块介绍
gateway网关
网关流程
Netty 请求处理
Netty 使用 主从Reactor模型 bossGroup 负责处理 accept 事件 , workerGroup 负责处理 read 、write 事件 其中 ChannelPipeline 是一个双向链表,netty 里面定义了十几种事件,触发之后会顺序调用所有 ChannelHandler 的指定方法,ChannelHandler的调用都是由 workerGroup中的同一个 eventloop 线程执行,不存在线程之间的切换。
Mercury 请求处理
Netty 中 ChannelPipeline 这种无锁化的设计,避免了上下文切换,在海量请求的情况下能提供很高的性能。但是也存在风险,如果执行某个 ChannelHandler 出现了阻塞,会拖累这个 eventloop 线程所负责的其他请求。 在实际场景中,ChannelHandler里面一般都会执行一些 IO 操作,比如RPC调用,MQ等,无法保证不会出现阻塞的情况。所以很多高性能的分布式框架中都会使用三层处理模型,额外增加一个业务线程池,将耗时的IO操作放在这个业务线程池里面执行,这样就不会阻塞 workerGroup 中的线程了,Mercury 中也是这么设计的。
调用代码流程
代码流程图
关键代码
NettyServerInitializer
netty启动时,初始化handler,encoder,decoder.
关键代码如下:
public ChannelFuture start() {
ChannelFuture cf = null;
ChannelFuture cfWs = null;
try {
ServerBootstrap bootstrap = new ServerBootstrap();
bossGroup = NettyFactory.eventLoopGroup(1, "bossLoopGroup");
workerGroup = NettyFactory.eventLoopGroup(4, "workerLoopGroup");
bootstrap.group(bossGroup, workerGroup)
.channel(NettyFactory.serverSocketChannelClass())
.option(ChannelOption.SO_BACKLOG, 2048)
.childOption(ChannelOption.TCP_NODELAY, true)
.childOption(ChannelOption.SO_KEEPALIVE, true)
.option(ChannelOption.SO_REUSEADDR, Boolean.TRUE)
.childOption(ChannelOption.ALLOCATOR, PooledByteBufAllocator.DEFAULT)
.childOption(ChannelOption.WRITE_BUFFER_WATER_MARK, new WriteBufferWaterMark(64 * 1024, 128 * 1024))
//初始化handle
.childHandler(new NettyServerInitializer());
cf = bootstrap.bind(PORT).sync();
channel = cf.channel();
cfWs = bootstrap.bind(PORT_WS).sync();
channelWs = cfWs.channel();
} catch (Exception e) {
log.error("Netty server start fail", e);
} finally {
if (cf != null && cf.isSuccess() && cfWs != null && cfWs.isSuccess()) {
log.info("Netty server listening ready for connections...");
} else {
log.error("Netty server start up error!");
}
}
printNettyConfig();
return cf;
}
//handled调用链
private static final NettyServerHandler NETTY_SERVER_HANDLER = new NettyServerHandler(ChannelHandlerWrap.wrap(new MercuryServerHandler()));
//编码、解码
private static fi