第 37 届全国青少年科技创新大赛 参赛作品

荣获第 37 届全国青少年科技创新大赛 三等奖

1706236406658.webp

# 研究背景

如今,国内已进入后疫情时代,网课掀起的在线会议浪潮尚未停息。越来越多的教育机构转为线上,腾讯会议成为了许多在线教育平台面对面施教的重要平台。作为腾讯会议网上教学的受益者之一,笔者逐渐对腾讯会议的优缺点有了更深的了解,这个过程中,自动打铃逐渐引起了笔者的注意。

学校的网课教学中,教师往往需要时刻注意当前的时间,使网上教学工作量加大,如果将提示上下课的工作交给学生,也会一定程度影响学生的上课质量和上课效率。在教育机构的网课教学中,课程提醒等需求均需要助教或老师手动进行,增大了工作量的同时不易管理。是否可以搭建一个平台,批量管理多个会议室的打铃日程,同时提供稳定可靠的打铃服务呢?这成为了笔者曾思考的问题。

经过仔细的思考和对相关技术的进一步了解和学习,笔者撰写了此文,希望能对未来的后学有启示作用。笔者开展相关研究经历了两个研究周期,项目也进行了多个版本迭代。从仅基于虚拟化的硬编码日程的打铃软件,到现在基于虚拟化和后台集成的打铃管理系统,需要的相关技术也逐渐多样和丰满,最终构建出了这样一个包含客户端、服务端、网页前端的多实例的打铃管理系统。

# 研究目的

为在线会议室提供一套易部署、高灵活、可复制的自动打铃解决方案,包含前端管理面板、基于后端的日程表系统和 VITS 语音合成等部分。

# 研究方法

使用从需求到实现的溯源性思维,结合现有的虚拟化 (VT-d) 技术,虚拟音视频技术,前后端分离的网页开发技术,TCP 网络编程等技术,利用 C#、Python、JavaScript 编程语言开发相关的客户端、前端、后端。并且调用外部 VITS 接口提供语音合成。

探究过程分为项目速览、概念突破、技术解读、研究过程几个部分,从理论到实践全方位进行探究。

# 项目速览

这里笔者将针对项目中的关键部分简单剖析,使读者快速理解本项目的特点

虚拟化:为多会议实例提供可能。在虚拟机容器中运行腾讯会议和虚拟摄像头、虚拟声卡等程序,将打铃程序的音视频呈递到腾讯会议中。单一物理机经由虚拟化可以承载多个会议室,同时做到了稳定可靠和易扩展的特点。

美观的桌面程序:给参会者直观的课程展示和提醒。精美的 UI 界面实时在会议室中展示,方便参会者实时了解课程信息,为课程做好准备。

后台集成:解决了各教师实时调整课程表,管理者统一管理的需求。使用服务端统一分发课程表,并且与前端配合,使教师和管理者可以在前端统一查看会议室状态,随时更改课程。

VITS 语音合成:使用 TTS 语音合成进行课程提醒。上课后使用 AI 语音合成提醒所上课节,可随时切换各角色的语音模型,为枯燥无趣的学习生活加点料。

# 概念突破

# 虚拟化技术与打铃

服务端(也称 wakatta 后端)作为数据源给各客户端(也称 wakaru 客户端)提供信息,使各个客户端在相互平行的虚拟机容器中提供打铃服务。

图片1.png

# 后台集成

为了实现数据的分发,我们搭建了客户端和服务端,而服务端的数据需要做到可视化地修改,因此我们搭建了网页前端来进行数据的增删改查。这里服务端将作为中转的桥梁,承载所有的网络请求和数据库连接。

图片2.png

客户端采用心跳包模型来保活,每秒向服务端发送请求证明自己的存活,并且从服务端的消息队列中拉取最新的消息(Packet),以保证网页前端调用的部分操作即时抵达客户端。

这里客户端的操作包含刷新日程表、重连、手动播放上下课铃、VITS 语音提醒等

# VITS 语音合成

VITS 属于一种基于机器学习的 TTS 技术,在上课前我们希望提醒学生所上课节,在网页前端使用语音提醒时希望客户端可以读出我们的文本内容,所以我们使用了 VITS

由于承载服务端的 VPS 往往缺乏较高性能的显卡,语音演算往往需要在其他算力机上进行,这里我们使用了 frp 进行了反向代理,使客户端经由服务端作为跳板后,可以间接访问到算力机。

图片3.png

在测试环境中,若算力机与服务端位于同一台主机或处于同一网段,也可省去反向代理的过程

# 技术解读

除虚拟化技术外,我们项目主要由服务端(wakatta)、客户端(wakaru)、网页前端(wakatta-web)以及 vits-simple-api 构成。

# 服务端(wakatta)

服务端主要承载客户端和网页前端的网络请求,使用 Python 编写,基于 FastAPI 异步 Web 框架开发,遵循 restful 接口设计范式。由于设计数据的存取和增删改查,数据库选用了高可靠性的 MySQL。这里我们使用 SQLAlchemy 框架做到了数据库的全 ORM,便于后续开发,提升了代码的可读性。服务端有时需要执行一些诸如每天更新当天日程表、定时清除失活客户端等方法,这里使用了 APScheduler 库,做到了定时的计划任务。

下面是服务端部分关键的设计逻辑:

当客户端的课程在网页前端被修改,向后端发出 PATCH 请求时,后端会先请求数据库的 session,然后异步调用 SQLAlchemy 获取对应课程的 ORM 对象,进行相关修改并且 commit 到数据库中。之后会调用位于 clients.py 的_notify_client 方法,将 “刷新日程表” 这一 “包” 压入暂存队列中,当下次客户端发送心跳包(最慢 1 秒)时,将所有暂存的 “包” 从队列中取出,将包含 packet_id 和 payload 的 “包” 作为 response 传给客户端。客户端收到后会重新请求日程表,做到刷新课程的效果。

当一天过去时,往往需要加载下一天的日程(比如周一跨到周二),APScheduler 会在每天零点调用 schedules.py 的 update_subscription 方法,该方法将替换所有订阅了该日程表的客户端的课程,加载对应星期的课程表。同时会调用 clients.py 的_notify_client 方法,要求所有在线客户端刷新日程表来应用最新的课程表。

当修改日程表中的课程时,进行数据库相关操作后,会调用 schedules.py 的_broadcast_schedule 方法,该方法将会把课程更改应用到所有订阅该课程表的客户端中。

# 客户端(wakaru)

客户端部分主要承载打铃服务,并且与后端通信获取最新的日程表,同时需要有美观且直观的界面,方便会议室中的参会人员可以对当前状态和日程一目了然

界面部分采用 WPF 编写,仅使用 xaml 标记语言便可搭建出美观的界面布局,并且支持动态绑定和组件化编程。Wakaru 中的每一个卡片均对应 Views 中的一个组件。

逻辑部分采用 C# 语言编写,基于.net6.0,运行需要准备好.net 6.0 运行时。客户端使用 Quartz 提供定时任务的功能,Quartz 不仅提供打铃任务(位于 RingJobs.cs)的定时执行,同时也包括每秒发送心跳包(位于 WakattaHeartbeat.cs)等过程。

下面是客户端部分关键的设计逻辑:

在客户端程序启动时,会向后端发送 get_client 请求,后端会返回客户端的所有基本信息,包括当前的日程表、客户端 ID 和名称、上下课铃等。之后客户端会创建 WakattaHeartbeat 的定时任务,向服务端每秒发送心跳包。WakattaHeartbeat 中同时还包含处理收到的 “包” 的 HandlePacket 方法

RingJobs 类中存放着所有关于播放上下课铃的相关代码,通过调用 SoundPlayer 的 Play 方法,即可实现播放指定音频。若 VITS 可用,也会在此时向相关算力机发送合成 VITS 音频的请求,合成的音频传回后也会直接进行播放

# 网页前端(wakatta-web)

网页前端主要提供了美观且便利的数据增删改查操作环境,包含会议管理、日程管理、用户系统等部分。

前端部分使用 Vue 搭建,UI 部分基于 element-plus 框架和 tailwind-css,使用 pinia 进行数据持久化,axios 处理与后端的网络请求。当发送网络请求时,会与后端基于 jwt 进行鉴权,只有具有相关权限才能修改课程等数据。没有相关权限的用户会被 vue-router 路由到 DeniedView.vue 页面中。

# vits-simple-api

为了承载客户端对 VITS 合成的需求,这里我们需要引入一个项目提供语音合成部分的支持,为了不干扰 wakatta 后端项目,这里我们引入了一个第三方项目来提供我们的需求,vits-simple-api 由 Artrajz 开发,使用 MIT 协议开源,使用该项目可快速调用 VITS 的相关接口
根据需求,我们主要调用了 voice/speakers 获取模型中所有的角色,调用 voice 进行语音合成。VITS 项目本身需要 pytorch 和 NVIDIA CUDA 的支持,这里笔者强烈建议使用一台额外的算力机搭载该项目。

关于模型,我们使用了 zomehwh 基于公开语音训练的 vits-uma-genshin-honkai 模型,需要注意的是,该模型不属于本项目的一部分,详见项目仓库中的 vits-models 目录。

# 关于生产环境

在本地环境调试本项目无需 VPS、云服务器等主机,但若要承载多台宿主机中的虚拟化容器,或希望身处不同网络环境中的教师、管理者在家便能编辑课程等信息,项目上云是必不可少的,在后文中笔者会以个人香港服务器为例,介绍如何基于宝塔面板快速将该项目的后端和网页前端上云。

# 研究过程

这部分将着重讲述一下如何从零开始基于 HyperV 和本地主机打算调试环境,并简要描述如何在生产环境搭建本项目。在研究的过程中,笔者也会探究该项目的思维过程,建立需求引导解决问题的溯源性思维。

# 实现自动打铃

实现自动打铃主要分为两步,即播放音视频和呈现音视频。播放音视频是指将我们预先设置好的音视频在适当的时机播放出来,呈现音视频是指将播放的音视频以某种方式投射或呈现到腾讯会议中。

定时播放音视频是一个十分常见的需求,市面上也有许多现成可复用的解决方案,但对于在线会议来说,我们的播放需求较为复杂,存在切换日程、显示状态、提示课节等需求,这也是笔者决定使用 C# + WPF 来开发客户端程序的原因。

呈现音视频的难度较高,基于我们的需求分析,我们希望腾讯会议能提供一个接口或者方法来捕捉我们提供的音视频。这里读者可能联想到腾讯会议的屏幕分享功能,笔者当然也考虑并尝试过这种方式。实际上,会议室中同时只有一位用户可以屏幕分享,如果需要对打铃程序使用屏幕分享,就会严重妨碍主持人正常使用屏幕分享功能,这显然违背了我们的初心,为线上教学带来了不便,我们固然需要舍弃这种方案。

反向来思考,怎样做能多个用户共享音视频呢,很容易想到,同学们在会议室中打开摄像头、打开麦克风正是一种 “共享音视频” 的形式,如果我们能对这种形式加以利用,便可以在不妨碍正常屏幕共享的前提下提供音频播放和视频播放的功能。显然,我们需要 “伪装” 一个麦克风和摄像头,来让腾讯会议主动地呈现我们的音视频。

伪装麦克风,即虚拟一个麦克风,虚拟麦克风可以模拟一个麦克风硬件,该麦克风将虚拟地捕获到目标音频,这里我们希望将扬声器播放的声音重新传入虚拟麦克风,所以我们使用了 Virtual Audio Cable 软件 (下文简称 “VAC”),该软件可以满足我们的需求

伪装摄像头,即虚拟一个摄像头,虚拟的摄像头模拟了一个摄像头硬件,摄像头捕捉的画面可以由我们提供,这里我们希望客户端程序美观的界面可以被传入摄像头,方便同学们查看课节信息和下课时间,我们使用了开源的 Open Broadcaster Software 软件 (下文简称 “OBS”),该软件可以将预设的场景变为虚拟摄像头,只需将场景中添加窗口捕获,来捕获我们的 GUI 程序,便可以满足我们的需求了

解决了音视频的播放和呈现之后,另一个问题便浮出水面,显然打铃程序需要运行在一台计算机上,并且该计算机需要让 VAC 接管音频,使用 OBS 来捕捉打铃程序的 GUI 画面。该计算机最好不受外界操作的干扰,单独负责腾讯会议自动打铃的服务。不仅如此,设想一下,我们可能希望为多个会议室提供打铃服务,如何使我们的方案变得可扩展而且高灵活呢。显然,购置多台计算机来分布式完成这个工作是及其不合理的,打铃服务显然不需要极强的计算能力,购置计算机也放大了没有必要的成本,虚拟化技术便是解决该问题的唯一答案。

虚拟化技术可以将一台物理机拆分为几个虚拟层,即做到一台电脑模拟多台电脑的效果。借助 Intel 的 VT-d 技术,我们可以高性能地将一台计算机的硬件 IO 虚拟化,使用统一的层来处理物理机和虚拟机的指令,大大减少虚拟化的性能损耗。这里,我们可以将物理计算机拆分为多台虚拟机,为每一个会议室单独提供虚拟的一台计算机,在虚拟计算机中运行我们上述提到的音视频播放和呈现功能。

Hyper-V 是 Windows 附带的虚拟机平台,它可以批量管理多台虚拟机,并且方便地进行虚拟机复制和容器内操作,并且不需要支付额外的使用费用,这里我们使用 HyperV 来作为虚拟机容器,实际上,也可以使用 VirtualBox 等开源免费虚拟机平台。

总结一下,我们可以将实现自动打铃的需求抽象成下面的流程图,这个流程图其实就是上文概念突破中的流程图 1 中的一部分

图片4.png

接下来我们进行实践,动手创建 HyperV 环境并且部署客户端相关程序,首先我们要准备 HyperV 虚拟机容器,并且将所需程序复制入虚拟机容器中

首先需要在 Windows 的可选功能中开启 Hyper-V 和虚拟机平台,开启后需要重启计算机,Windows 将以 Windows 更新的方式来安装所需的组件,安装过后,就可以在开始菜单打开 Hyper-V 管理器了

图片2.png

图片3.png

这里笔者的 Hyper-V 中已经包含了一些虚拟机了,为了创建专门用于自动打铃的虚拟机,我们可以新建一个空白的虚拟机,点击新建 -> 虚拟机来创建,具体参数笔者这里不做过多介绍,仅给出一些推荐参数给读者参考

内存:推荐分配 1024-2048MB,防止腾讯会议因内存不足而发生错误。硬盘:推荐分配 30GB 以上,Hyper-V 默认会按需扩张磁盘大小。系统:推荐使用 LTSB 或 LTSC 来避免 Windows 更新,同时缩减因虚拟机系统带来的不必要的性能损失

接下来我们启动创建的虚拟机,完成第一次安装 Windows 的 OOBC 过程,直至成功进入系统桌面。之后我们需要安装自动打铃中涉及的一些软件,这部分笔者不做过多赘述,读者可以按照在物理机安装软件的方法来安装软件

接下来我们重点介绍有关客户端开发的内容,由于代码量较大,这里笔者只能抓住几个关键点略讲一二,如若希望深入了解,可以 clone 本项目仓库,在 wakaru 目录找到相关代码,也可以在原始实验数据文档中找到所有源代码。

首先,我们先总览一下项目结构,并且关注一下程序的入口

Snipaste_2024-01-26_10-15-36.png

项目主要由 Online 包、Quartz 包、Views 包构成,Online 包主要包含了与后端的网络请求相关代码,是整个项目数据承载的核心。WakattaClient 描述了整个客户端,其中包含客户端的 Json 模型等内容,在右侧图 1 可以看出,WakattaClient.Create () 方法是项目的入口之一,右侧图 2 主要展示了客户端创建的过程,在软件启动之后,客户端会尝试获取 HardwareID 并且向后端请求客户端相关数据,之后会准备日程表和合成相关信息,应用当前上下课状态,并且准备 VITS。很容易看出,所有的过程都是异步方法,这意味着所有网络请求不会阻塞客户端的主线程,获取数据都在后台进行。

Quartz 包主要包含了关于打铃的相关内容,ScheduleManager 类储存了日程表的实例,同时也提供一些便利的方法方便客户端创建打铃的定时任务,RingJobs 中存放了打铃的相关实现。这里打铃时会检测 VITS 是否可用,如果可用,也会将合成的音频在铃声播放过后一并播放

public class ClassBeginRingJob : RingJob
{
    public override Task Execute(IJobExecutionContext context)
    {
        if (WakattaClient.CurrentClient != null)
        {
            var label = context.Get("label")?.ToString();
            Task.Run(async () => {
                PlaySoundSync(WakattaClient.CurrentClient.ClassBeginRingtone);
                if (label != null) await VITSManager.PlaySoundFormat(label);
            });
            WakattaClient.ApplyStatus();
        }
        return Task.CompletedTask;
    }
}

VITSManager 类中的 PlaySound 方法会调用上文提到的 vits-simple-api 接口,将获得的 wav 文件暂时储存在内存流中,并且让 SoundPlayer 播放该内存流。

public static async Task PlaySound(string content)
{
    if (!vitsObject.Enabled) return;
    if (WakattaClient.CurrentClient == null) return;
    var vitsId = WakattaClient.CurrentClient.VITSId;
    using HttpClient client = new();
    string url = vitsObject.Entrypoint + $"voice?id={vitsId}&text={content}";
    HttpResponseMessage response = await client.GetAsync(url);
    MemoryStream memoryStream = new();
    using (Stream stream = await response.Content.ReadAsStreamAsync())
    {
        await stream.CopyToAsync(memoryStream);
    }
    memoryStream.Seek(0, SeekOrigin.Begin);

    SoundPlayer player = new(memoryStream);
    player.Play();

    response.Dispose();
    memoryStream.Dispose();
}

Views 包中主要包含了主页中显示的所有卡片对应的组件,这里我们以 SchedulePanel 为例简要说明一下,SchedulePanel 组件中运用了 WPF 的动态绑定,在 ListView 中展示所有的课节

<ListView Name="SchedulerListView">
    <ListView.ItemTemplate>
        <DataTemplate DataType="{x:Type local:WakattaClass}">
            <DockPanel>
                <materialDesign:PackIcon Margin="0 1 5 0" Kind="Book" />
                <TextBlock Text="{Binding Label}" HorizontalAlignment="Center"></TextBlock>
                <Separator Visibility="Hidden" Width="20"/>
                <StackPanel Orientation="Horizontal" HorizontalAlignment="Right">
                    <materialDesign:Chip Content="{Binding TimeString}" Padding="0" Margin="0" Height="20"></materialDesign:Chip>
                    <materialDesign:Chip Content="{Binding TimeDurationString}" Padding="0" Margin="0" Height="20"></materialDesign:Chip>
                </StackPanel>
            </DockPanel>
        </DataTemplate>
    </ListView.ItemTemplate>
</ListView>
public WakattaSchedule()
{
    Instance = this;
    InitializeComponent();
    SchedulerListView.ItemsSource = ClassList;
    SetSelectedIndex(Utils.IndexClass(ClassList));
}

只需给 ListView 的 ItemsSource 指定一个集合,便可做到动态绑定,当集合中元素被修改时,UI 中对应的列表也会随着修改

接下来运行 wakaru 项目,并且运行必要的服务端,即可看到客户端界面

图片5.png

接下来我们将把客户端部署到虚拟机中,注意的是,由于该项目经历过多次迭代,虚拟机中的 UI 已经与目前开发进度中的 UI 样式不同,不过这并不影响部署到虚拟机中这一过程的说明与讲解。

我们将编译好的打铃软件复制到虚拟机中,并且安装.NET 6.0 运行时,保证虚拟机能够运行打铃软件,为了使 SoundPlayer 播放的音频正确输入到虚拟声卡中,我们需要将系统输出音频设置为 CABLE Input (VB-Audio Virtual Cable),C# 的 SoundPlayer 将在系统当前默认扬声器中播放声音。

这里读者可能遇到虚拟机的音频仅有 “远程音频” 这一个选项,这是因为 Hyper-V 增强会话默认会启用音频转发,将虚拟机中的音频自动转发到宿主机,这显然不满足我们的预期,这里我们可以在连接虚拟机的时候不使用增强会话,即出现 “连接至 XX” 对话框时不点击连接,直接关掉该对话框,即可正确识别虚拟机内安装的虚拟声卡服务。

增强会话同时也会导致关闭虚拟机连接时使远程计算机自动休眠,这会导致腾讯会议的麦克风自动关闭,自动打铃受到阻碍,也可以通过上述方法来禁用增强会话,从而使自动打铃服务可以完全隐藏在宿主机的后台静默运行。

接下来我们需要配置 OBS 的场景,来将打铃程序的界面呈现到腾讯会议中,注意,为了使 OBS 支持虚拟摄像头功能,需要安装版本号为 26 以上的 OBS。

Hyper-V 默认没有虚拟化 NVENC QSV 等编码器的能力,这意味着我们开启虚拟摄像头时需要利用 CPU 的 x264 进行软件编码,我们最好适量降低 OBS 的编码质量来达到最大的能效比,这里给出笔者使用的参数来供读者参考。编码器:软件(x264),比特率:200 Kbps,输出分辨率:786x446,缩小方法:双直线法,帧率:10FPS。这里输出分辨率可以根据打铃程序界面的大小进行合理的调整。

之后我们创建 “窗口采集” 作为来源,并且选中打铃程序的 GUI 窗体,接下来便能看到 OBS 中已经成功捕获打铃程序的界面,这里需要注意的是,OBS 无法捕捉最小化的窗口,这里我们需要保证打铃程序位于前台。

勾选启动虚拟摄像机并启动腾讯会议,接下来我们将在腾讯会议中应用我们配置的虚拟摄像头和虚拟声卡。

摄像头中选中 OBS Virtual Camera,麦克风选中 CABLE Output (VB-Audio Virtual Cable),音频选项可以按照下图设置,以保证铃声音乐播放的流畅性

图片10.png

至此,我们完成了客户端部分的搭建

如果读者有多会议室的需求,也可以克隆虚拟机,同时运行多个虚拟机以承载多个会议室的自动打铃服务,这里笔者不再过多赘述。除此之外,当读者想要关闭虚拟机时,可以使用 Hyper-V 虚拟机的 “保存” 功能,该功能将拍摄当前虚拟机状态为一个快照,方便下次启动虚拟机时还原快照,避免重复配置多次。

# 搭建后端与网页前端

项目仓库地址:https://github.com/TrueRou/wakatta
关于安装 Python3 和 Nodejs 开发环境这里笔者不再过多赘述,笔者假设读者具有完成基本软件开发的能力,下面读者可以同笔者一起搭建该项目的后端与网页前端,并且将其部署到生产环境

首先,为了部署项目,需要先拉取前后端的代码,需要注意的是,该项目已经在 Github 上开源,读者可以 Fork 或者 Clone 项目的源码到自己的仓库,或者调试机器、生产环境机器上。注意,本项目遵循 GPLv3 开源协议。这里的部署代码适用于 Linux,如果在 Windows 上部署大同小异,这里笔者不过多赘述

首先,打开终端,克隆项目并安装相关依赖,并且配置后端的 config.py

git clone https://github.com/TrueRou/wakatta.git
cd wakatta; pip install -r requirements.txt
cd wakatta-web; npm install

之后,在 wakatta 项目下运行 python3 main.py 即可启动后端项目,在 wakatta-web 下运行 npm run dev 即可在调试环境下临时运行前端项目,实际上,我们可以将前端项目使用 npm run build 语句构建成静态文件,使用 nginx 进行托管,这部分笔者将在下面生产环境部署中详细讲解

接下来我们将抽出后端项目中的几个细节进行简单讲解,实际上,部分逻辑在上面的技术解读中已经讲解过,这里我们将展示相关的代码。

图片6.png

在 models.py 中,我们定义了数据库 ORM 中的实体类,实际上,根据笔者的经验,使用 Python 构建前后端项目时,实体类往往有两种存在形式,即服务于 SQLAlchemy 的实体类,这里笔者习惯命名为 models.py,还有服务于 FastAPI 的 Pydantic 实体类,这部分主要用于后端请求的类型检查和 response_model,这里笔者习惯命名为 schemas.py

利用全 ORM 的好处便是易于指定 relationship,这也是关系型数据的关键之一,这里比如客户端应该拥有多个课程,通过外键 class.id 连接,客户端可能有一个订阅的日程表,通过外键 schedule.id 连接,这里读者可以自行阅读相关的代码,或者查阅 SQLAlchemy 的文档,需要注意的是,这里笔者使用的是 SQLAlchemy2.0,读者需要自行阅读较新的文档,或者阅读了开启 future=True 的较旧文档

图片7.png

在 sessions.py 中,储存了 online_clients 列表,client_data 字典,以及 client_packets 字典,client_data 主要包含客户端运行返回的实时数据,通常包含在心跳包的请求中,client_packets 则是上文提到过的消息缓存队列,会在客户端发送心跳包时取出

tick_session 方法会在每秒执行,主要为了踢出不活跃的客户端,保持 online_clients 列表有效,而 tick_client 方法则是每次心跳包送达时被执行,主要是为了记录客户端活跃时间,方便超时踢出

图片8.png

在 schedule.py 中,当日程表有应用操作时,会调用 apply_schedule 方法,具体逻辑可以参考技术解读部分

前端部分基于 vue3 和 element-plus,鉴于前端项目较差的组织性,笔者这里不单独展开分析前端的代码,仅放出前端的部分界面,使读者感受到前端的可操作性

图片9.png

图片10.png

图片11.png

图片12.png

图片13.png

图片14.png

图片15.png

# 生产环境搭建

接下来我们讲述一下如何在生产环境搭建该项目,我们主要以反向代理 nginx 和 frp 的配置为主,其他搭建方式可以参考上文调试环境搭建的相关内容

在配置前,请确保云服务器已经搭载宝塔面板,并且安装了 Nginx 和 MySQL 的较新版本,这里笔者不推荐使用 MySQL8,MySQL8 的性能弱于 5.5。之后,将 MySQL 生成的账号密码填入 wakatta 项目的 config.py 中,之后运行后端项目

接下来我们为后端项目配置反代,我们希望在 “https:// 域名 /wakatta/api” 路径中访问到后端,所以我们修改对应域名的配置文件

图片16.png

使用 nginx 提供的 proxy_pass,即可完成反代

接下来我们部署网页前端,进入 wakatta-web 目录,输入 npm run build 来构建出静态页面,并且把静态页面复制到宝塔对应网站目录下,这里需要注意的是,请保证前端项目 config.js 中对应的后端地址已经指向生产环境的后端地址,而不是之前调试环境中的。为了免除不必要的跨域问题,请不要把 API 和前端放在不同域名下,在 nginx 中,我们可以使用 alias 进行目录的路由

图片17.png

这样当我们访问 https:// 域名 /wakatta,就会被跳转到我们的静态页面中,至此,前后端的部署完毕。

Frp 的配置要点主要在于配置 frps.ini 和 frpc.ini,这里限于篇幅,笔者决定不再展开叙述,这部分内容读者可以自行阅读 frp 的相关文档

# 后记

# 性能调优

我们希望打铃服务能尽可能减少性能的占用,以提高能效比,在单位物理机中可容纳更多的打铃服务,这里笔者给出自己的几点看法和思路。

首先,腾讯会议本身会随着会议室人数的增多而占用更多处理单位,我们可以通过关闭接收参会者视频的方式来缓解这个问题。在会议室的常规设置中勾选语音模式,即可暂停接收参会者视频,降低性能消耗。

同时,腾讯会议可能默认会对虚拟摄像头的画面进行降噪或者美颜处理,这也会导致没有必要的性能开销,这里我们进入视频设置,取消勾选所有的对钩,来关闭所有的视频处理功能。
对于 OBS,其内置的场景预览也会造成一定的性能消耗,这里可以右键预览的部分,将开启预览的功能关闭。如果读者不需要虚拟摄像头展示打铃情况,仅仅需要播放打铃音乐的功能,也可以跳过 OBS 的部署,彻底消除编码器造成的性能消耗。

针对操作系统层面,精简程度较高的系统能减少额外开销,如果可以的话可以选择 32 位的系统来降低内存需求,但这里笔者不推荐使用 Windows 7,Windows 8 等操作系统,这会降低程序运行的稳定性,因为这些系统已经不再被微软维护,在其环境中运行.NET 6.0 程序也可能产生不必要的麻烦。这里笔者使用的是极限精简的 Tiny10 镜像,这里不再过多赘述

# 后记

从 2023 年 2 月,笔者完成了 Wakaru 的第一个版本,到 2023 年 5 月,笔者将项目完全重构。从单一的 C# + WPF 桌面项目,到综合三个语言的客户端 + 后台集成项目,笔者在紧张的开发周期中也学到了许多,获得了许多,笔者认为,创新本身不仅仅在于研究结果,更在于创新的研究思路和研究过程。在这个过程中,我们创新的不仅仅是知识本身,更是获取知识的方式。