惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

博客园 - 【当耐特】
L
Lohrmann on Cybersecurity
Google DeepMind News
Google DeepMind News
Schneier on Security
Schneier on Security
Recent Commits to openclaw:main
Recent Commits to openclaw:main
The Last Watchdog
The Last Watchdog
Application and Cybersecurity Blog
Application and Cybersecurity Blog
N
News and Events Feed by Topic
P
Proofpoint News Feed
H
Heimdal Security Blog
云风的 BLOG
云风的 BLOG
H
Hacker News: Front Page
量子位
freeCodeCamp Programming Tutorials: Python, JavaScript, Git & More
L
LINUX DO - 最新话题
F
Full Disclosure
小众软件
小众软件
Martin Fowler
Martin Fowler
Security Latest
Security Latest
The Cloudflare Blog
Hacker News: Ask HN
Hacker News: Ask HN
C
Check Point Blog
Security Archives - TechRepublic
Security Archives - TechRepublic
I
InfoQ
AI
AI
Blog — PlanetScale
Blog — PlanetScale
I
Intezer
H
Hackread – Cybersecurity News, Data Breaches, AI and More
M
MIT News - Artificial intelligence
V
Vulnerabilities – Threatpost
让小产品的独立变现更简单 - ezindie.com
让小产品的独立变现更简单 - ezindie.com
www.infosecurity-magazine.com
www.infosecurity-magazine.com
V
V2EX
N
News and Events Feed by Topic
C
Cybersecurity and Infrastructure Security Agency CISA
T
Troy Hunt's Blog
大猫的无限游戏
大猫的无限游戏
Hacker News - Newest:
Hacker News - Newest: "LLM"
The Register - Security
The Register - Security
Forbes - Security
Forbes - Security
Recent Announcements
Recent Announcements
Threat Intelligence Blog | Flashpoint
Threat Intelligence Blog | Flashpoint
Cisco Talos Blog
Cisco Talos Blog
Microsoft Azure Blog
Microsoft Azure Blog
A
About on SuperTechFans
S
SegmentFault 最新的问题
S
Securelist
Cloudbric
Cloudbric
T
Tenable Blog
D
Docker

元视角

.NET 生态下的 Agent 框架选型:从 ReAct 到原生推理 - 元视角 从「能用」到「好用」:LLM 流式响应实现方式的探索之路 - 元视角 当我用 2000 条聊天记录,让 AI 为我画一幅自画像 - 元视角 基于 Supabase 的 AI 应用开发探索 - 元视角 微博 × MCP:社交媒体新玩法解锁 - 元视角 四点钟海棠花未眠 - 元视角 Semantic Kernel × MCP:智能体的上下文增强探索 - 元视角 基于 K-Means 聚类分析实现人脸照片的快速分类 - 元视角 容器技术驱动下的代码沙箱实践与思考 - 元视角 温故而知新:后端通用查询方案的再思考 - 元视角 浅议 CancellationToken 在前后端协同取消场景中的应用 - 元视角 Semantic Kernel 视角下的 Text2SQL 实践与思考 - 元视角 关于 ChatGPT 的流式传输,你需要知道的一切 - 元视角 RAG 的是与非、Rewrite 和 Rerank - 元视角 使用 EFCore 和 PostgreSQL 实现向量存储及检索 - 元视角 基于 LLaMA 和 LangChain 实践本地 AI 知识库 - 元视角 使用 llama.cpp 在本地部署 AI 大模型的一次尝试 - 元视角 如何为 Git 配置多个 SSH Key - 元视角 C# 使用 LibUsbDotNet 实现 USB 设备检测 - 元视角 基于 C# 实现样式与数据分离的打印方案 - 元视角 基于 SVG 的图形交互方案实践 - 元视角 前端视频播放技术概览 - 元视角 温故而知新,再话 Python 动态导入 - 元视角 后 GPT 时代,NLP 不存在了? - 元视角 视频是不能 P 的系列:使用 Milvus 实现海量人脸快速检索 - 元视角 GDI+下字体大小自适应方案初探 - 元视角 小爱音箱集成 ChatGPT 的不完全教程 - 元视角 程序员视角下的三体世界随想 - 元视角 关于 Docker 容器配置信息的渐进式思考 - 元视角 在 Docker 容器内集成 Crontab 定时任务 - 元视角 为你的服务器集成 LDAP 认证 - 元视角 似花还似非花 - 元视角 视频是不能 P 的系列:使用 Dlib 实现人脸识别 - 元视角 浅议分布式链路追踪与日志的整合 - 元视角 关于 Git 大文件上传这件小事 - 元视角 .NET 进程内队列 Channel 的入门与应用 - 元视角 使用 Fody 实现 .NET 的静态编织 - 元视角 .NET Core + ELK 搭建可视化日志分析平台(下) - 元视角 聊一聊前端图片懒加载背后的故事 - 元视角 支持外部链接跳转的 Vue Router 扩展实现 - 元视角 视频是不能 P 的系列:OpenCV 和 Dlib 实现表情包 - 元视角 不得不说的 ASP.NET Core 集成测试 - 元视角 再议 DDD 视角下的 EFCore 与 领域事件 - 元视角 Vue.js 前端项目容器化部署实践极简教程 - 元视角 再见,人间四月天 - 元视角 Python 图像风格化迁移助力画家梦想 - 元视角 利用 ASP.NET Core 中的标头传播实现分布式链路追踪 - 元视角 利用 gRPC 实现文件的上传与下载 - 元视角 七种武器:延迟队列的原理和实现总结 - 元视角 gRPC 流式传输极简入门指南 - 元视角 Envoy 集成 Jaeger 实现分布式链路追踪 - 元视角 浅议非典型 Web 应用场景下的身份认证 - 元视角 gRPC 借助 Any 类型实现接口的泛化调用 - 元视角 分布式丛林探险系列之 Redis 集群模式 - 元视角 分布式丛林探险系列之 Redis 主从复制模式 - 元视角 通过 Python 预测 2021 年双十一交易额 - 元视角 gRPC 搭配 Swagger 实现微服务文档化 - 元视角 SSL/TLS 加密传输与数字证书的前世今生 - 元视角 使用 Python 自动识别防疫健康码 - 元视角 你不可不知的容器编排进阶技巧 - 元视角 ASP.NET Core 搭载 Envoy 实现 gRPC 服务代理 - 元视角 再话 AOP,从简化缓存操作说起 - 元视角 ASP.NET Core 搭载 Envoy 实现微服务身份认证(JWT) - 元视角 ASP.NET Core 搭载 Envoy 实现微服务的监控预警 - 元视角 ASP.NET Core 搭载 Envoy 实现微服务的反向代理 - 元视角 ASP.NET Core gRPC 打通前端世界的尝试 - 元视角 EFCore 实体命名约定库:EFCore.NamingConventions - 元视角 ASP.NET Core gRPC 集成 Polly 实现优雅重试 - 元视角 ASP.NET Core gRPC 健康检查的探索与实现 - 元视角 ASP.NET Core gRPC 拦截器的使用技巧分享 - 元视角 SnowNLP 使用自定义语料进行模型训练 - 元视角 使用 HttpMessageHandler 实现 HttpClient 请求管道自定义 - 元视角 ABP vNext 的实体与服务扩展技巧分享 - 元视角 ABP vNext 对接 Ant Design Vue 实现分页查询 - 元视角 源代码探案系列之 .NET Core 跨域中间件 CORS - 元视角 源代码探案系列之 .NET Core 限流中间件 AspNetCoreRateLimit - 元视角 源代码探案系列之 .NET Core 并发限制中间件 ConcurrencyLimiter - 元视角 通过 EmbededFileProvider 实现 Blazor 的静态文件访问 - 元视角 低代码,想说爱你不容易 - 元视角 记一次失败的 ThoughtWorks 面试经历 - 元视角 从 C# 1.0 到 C# 9.0,历代 C# 语言特性一览 - 元视角 通过 Python 分析 2020 年全年微博热搜数据 - 元视角 基于 Python 和 Selenium 实现 CSDN 一键三连自动化 - 元视角 使用多线程为你的 Python 爬虫提速的 N 种姿势,你会几种? - 元视角 实现网页长截图的常见思路总结 - 元视角 温故而知新,由 ADO.NET 与 Dapper 所联想到的 - 元视角 视频是不能 P 的系列:OpenCV 人脸检测 - 元视角 作为技术宅的我,是这样追鬼滅の刃的 - 元视角 使用 Python 抽取《半泽直树》原著小说人物关系 - 元视角 厉害了!打工人用 Python 分析西安市职位信息 - 元视角 使用 dotTrace 对 .NET 应用进行性能分析与优化 - 元视角 一道 HashSet 面试题引发的蝴蝶效应 - 元视角 基于选项模式实现.NET Core 的配置热更新 - 元视角 Dapper.Contrib 在 Oracle 环境下引发 ORA-00928 异常问题的解决 - 元视角 .NET Core 中对象池(Object Pool)的使用 - 元视角 利用 MySQL 的 Binlog 实现数据同步与订阅(下):EventBus 篇 - 元视角 利用 MySQL 的 Binlog 实现数据同步与订阅(中):RabbitMQ 篇 - 元视角 利用 MySQL 的 Binlog 实现数据同步与订阅(上):基础篇 - 元视角 记一次从已损坏的 Git 仓库中找回代码的经历 - 元视角 .NET Core 原生 DI 扩展之属性注入实现 - 元视角
基于 WebSocket 和 Redis 实现 Bilibili 弹幕效果 - 元视角
飞鸿踏雪 · 2018-08-22 · via 元视角

嗨,大家好,欢迎大家关注我的博客,我是 Payne,我的博客地址是https://qinyuanpei.github.io。在上一篇博客中,我们使用了.NET Core 和 Vue 搭建了一个基于 WebSocket 的聊天室。在今天这篇文章中,我们会继续深入这个话题。博主研究 WebSocket 的初衷是,我们的项目上有需要实时去推送数据来完成图表展示的业务,而博主本人对这个内容比较感兴趣,因为博主有对爬虫抓取的内容进行数据可视化(ECharts)的想法。可遗憾的是,这些数据量都不算太大,因为难以支持实时推送这个想法,当然更遗憾的是,我无法在项目中验证以上脑洞,所以,最终退而求其次,博主打算用 Redis 和 WebSocket 做一个弹幕的 Demo,之所以用 Redis,是因为博主懒到不想折腾 RabbitMQ。的确,这世界上有很多事情都是没有道理的啊……

其实,作为一个业余的数据分析爱好者,我是非常乐意看到炫酷的 ECharts 图表呈现在我的面前的,可当你无法从一个项目中收获到什么的时候,你唯一的选择就是项目以外的地方啦,所以,在今天这样一个精细化分工的时代,即使你没有机会独立地完成一个项目,我依然鼓励大家去了解项目的“上下文”,因为单单了解一个点并不足以了解事物的全貌。好了,下面我们来简单说明下这个 Demo 整体的设计思路,即我们通过 Redis 来“模拟”一个简单的消息队列,客户端发送的弹幕会被推送到消息队列中。当 WebSocket 完成握手以后,我们定时从消息队列中取出弹幕,并推送到所有客户端。当客户端接收到服务端推送的消息后,我们通过 Canvas API 完成对弹幕的绘制,这样就可以实现一个基本的弹幕系统啦!

编写消息推送中间件

首先,我们来实现服务端的消息推送,其基本原理是:在客户端和服务端完成“握手”后,我们循环地从消息队列中取出消息,并将消息群发至每一个客户端,这样就完成了消息的推送。同上一篇文章一样,我们继续基于“中间件”的形式,来编写消息推送相关的服务。这样,两个 WebSocket 服务可以独立运行而不受到相互的干扰,因为我们将采用两个不同的路由。在上一篇文章中,我们给“聊天”中间件 WebSocketChat 配置的路由为**/wsws。这里,我们将“消息推送”中间件 WebSocketPush 配置的路由为/push**。这块儿我们做了简化,不再对所有 WebSocket 的连接状态进行维护,因为对一个弹幕系统而言,它不需要让别人了解某个用户的状态是否发生了变化。所以,这里我们给出关键的代码。

public async Task Invoke(HttpContext context)
{
    if (!IsWebSocket(context))
    {
        await _next.Invoke(context);
        return;
    }

    var webSocket = await context.WebSockets.AcceptWebSocketAsync();
    _socketList.Add(webSocket);
    while (webSocket.State == WebSocketState.Open)
    {
        var message = _messageQueue.Pull("barrage",TimeSpan.FromMilliseconds(2));
        foreach(var socket in _socketList)
        {
            await SendMessage(socket,message);
        }
    }

    await webSocket.CloseAsync(WebSocketCloseStatus.NormalClosure, "Close", default(CancellationToken));
}

同样地,我们需要在 Startup 类中添加 WebSocketPush 中间件。按照 ASP.NET Core 中的惯例,我们为 IAppBuilder 接口增加一个名为 UseWebSocketPush 的扩展方法。这样,可以让我们直接使用该方法完成中间件的注册。

public static void UseWebSocketPush(this IApplicationBuilder app)
{
    app.UseMiddleware<WebSocketPush>();
}

Redis 打造的消息队列

OK,在编写“消息推送”中间件的时候,我们会注意到,我们使用了一个名为 SimpleMessageQueue 的类来取得消息,而服务端会负责将该消息群发到所有的客户端。这个其实就是博主写的一个简单的消息队列啦,如此简洁直白的命名证明它的确非常简单。有多简单呢?我想一会儿大家就会找到答案。在此之前,我想和大家讨论这样一个问题。其实,聊天室和弹幕挺像的吧,理论上服务端接收到客户端发的消息,就可以直接群发过去啊,为什么要搞一个消息队列在这里呢?而且更扯的一点是,既然博主你选择用 Redis 啦,你难道不知道 Redis 天生就支持发布订阅(Pub-Sub)吗?为什么要搞一个消息队列在这里呢?

对这个问题,我的想法其实是这样的,我最初想做的是:后端定期推送数据到前端,再由前端通过这些数据来绘制图表。此时,无论后端还是前端,其实都是数据的消费者,这些数据当然不能一股脑儿全给它们啊,这吃撑着了可怎么办,所以,为了避免它们消化不良,我得有一个东西帮助它维持秩序啊,这就是消息队列啊。简单来说,如果数据量超过程序的处理能力,这个时候我们就需要消息队列在前面帮忙“挡”一下。想象一下,如果去银行办理业务的人,都不排队一股脑儿涌向柜台,银行柜员大概会感到崩溃。我们的程序模拟的是现实生活,所以,我们需要消息队列。

为什么需要消息队列 为什么需要消息队列

那么,有朋友要问啦,就算你要用消息队列,那博主你为什么不用 RabbitMQ,再不济可以考虑微软自带的 MQ 啊,为什么要用 Redis 做一个 MQ 呢?就算你坚持要用 Redis 做 MQ,为什么不考虑用的 Redis 的发布-订阅(Pub-Sub)呢?对于第一个问题,你可以理解为我穷或者懒(穷个什么鬼啊,你特么就是懒_(:з」∠)_)。我就是懒得去搞 RabbitMQ,谁让我电脑 C 盘都快爆炸了呢,自从我把玩了几次Docker for Windows以后,而且我们项目上还真有不被允许用 MQ 的情况。所以,基于以上原因,我选择了 Redis。

Redis中的Pub-Sub Redis中的Pub-Sub

那么,为什么不用发布-订阅(Pub-Sub)呢,因为观察者模式的一个前提是,订阅者和主题必须在同一个上下文,即消息的发送方和接受方都必须同时“在线”。可 Bilibili 的弹幕和用户的在线与否无关,这意味着发弹幕与接收弹幕可以不在同一个时刻,所以,在设计上我们是提供了一个 API 接口来发送弹幕,而不是直接通过 WebSocket 来发送。否则,消息都到达服务端了,再通过一个消息队列来取消息,这就真的有点奇怪了不是吗?

下面给出这个消息队列的实现,原理上是这样的,每一个消息所在的 Channel,实际上都是一个列表,我们使用 Channel 的名称作为这个列表的键。接下来,ServiceStack 提供的 Redis 客户端中,提供了名为 BlockingListItem()的方法,它可以提供类似消息队列的功能,我们在这个基础上实现了一个简单的消息队列。

public class SimpleMessageQueue
{
    private string _connectionString;
    private readonly BasicRedisClientManager _clientManager;
    public SimpleMessageQueue(string connectionString)
    {
        _connectionString = connectionString;
        _clientManager = new BasicRedisClientManager(_connectionString);
    }

    public void Push(string channel, string messsage)
    {
        using (var client = _clientManager.GetClient())
        {
            client.PushItemToList(channel, messsage);
        }
    }

    public void Push(string channel, IEnumerable<string> messages)
    {
        using (var client = _clientManager.GetClient())
        {
            client.AddRangeToList(channel, messages.ToList());
        }
    }

    public string Pull(string channel,TimeSpan interval)
    {
        using (var client = _clientManager.GetClient())
        {
            return client.BlockingDequeueItemFromList(channel,interval);
        }
    }
}

相应地,在 WebSocketPush 中间件中,我们通过 Pull()方法来取得消息,时间间隔为 2s。在 MessageController 中,我们提供了用以发送弹幕的 API 接口,它实际上调用了 Push()方法,这个非常简单啦,我们不再做详细说明。

[HttpPost]
[Route("/api/message/publish/barrage")]
public IActionResult Publish()
{
    Stream stream = HttpContext.Request.Body;
    byte[] buffer = new byte[HttpContext.Request.ContentLength.Value];
    stream.Read(buffer, 0, buffer.Length);
    string message = System.Text.Encoding.UTF8.GetString(buffer);
    _redisPublisher.Push("barrage", message);
    Response.Headers.Add("Access-Control-Allow-Origin", "*");
    return Ok();
}

使用 Canvas 绘制弹幕

好啦,截止到目前为止,我们所有后端的开发已基本就绪。现在,我们来关注下前端的实现。关于 WebSocket 原生 API 的使用,在上一篇文章中,我们已经讲过啦,这里我们重点放在客户端提交弹幕以及绘制弹幕。

首先来说,客户端提交弹幕到服务器,因为我们已经编写了相应的 Web API,所以这里我们简单调用下它就好。和上一篇文章一样,我们继续使用 Vue 作为我们的前端框架,这对一个不会写 ES6 和 CSS 的伪前端来说,是非常友好的一种体验。因为现在是 2018 年,所以,我们要坚决地放弃 jQuery,虽然它的 ajax 的确很好用,可这里我们还是要使用 Axios:

axios.post("http://localhost:8002/api/message/publish/barrage",{
    value: self.value,
    color: self.color,
    time: self.video.currentTime
}).then(function (response) {
    console.log(response);
})
.catch(function (error) {
    console.log(error);
});

接下来,说说弹幕绘制。我们知道,HTML5 中提供了基于 Canvas 的绘图 API,所以,我们这里可以用它来完成弹幕的绘制。基本思路是:根据 video 标签计算出弹幕出现的范围,然后让弹幕从右侧向左逐渐移动,而弹幕的垂直位置则可以是顶部/底部/随机,当弹幕移动到屏幕左侧时,我们从弹幕集合中移除掉这个元素即可。下面给出基本代码,绘图相关的接口可以参考这里,弹幕相关参考了这篇文章

var context = canvas.getContext('2d');
context.shadowColor = 'rgba(0,0,0,' + this.opacity + ')';
context.shadowBlur = 2;
context.font = this.fontSize + 'px "microsoft yahei", sans-serif';
if (/rgb\(/.test(this.color)) {
 context.fillStyle = 'rgba(' + this.color.split('(')[1].split(')')[0] + ',' + this.opacity + ')';
} else {
 context.fillStyle = this.color;
}
context.fillText(this.value, this.x, this.y);

翻滚吧,弹幕

OK,现在我们来一起看看最终的效果,如你所见,在视频播放过程中,我们可以通过视频下方的输入框发送弹幕,弹幕会首先经由 Redis 缓存起来,当到达一定的时间间隔以后,我们就会将消息推送到客户端,这样所有的客户端都会看到这条弹幕,而对于客户端来说,它在和服务端建立 WebSocket 连接以后,唯一要做的事情就是在 onmessage 回调中取得弹幕数据,并将其追加到弹幕数组中,关于弹幕绘制的细节,我们在本文的第三节已经做了相关说明,在此不再赘述。 弹幕效果展示 弹幕效果展示 这里,我们采用了前后端分离的设计,即使我们没有并使用主流的 ES6 去实现客户端。因此,这是客户端实际上是一个静态页面,在本地开发阶段,我们可以通过打开多个浏览器窗口来模拟多用户。那么,如果我们希望让更多人来访问这个页面该怎么做呢?这就要说到 ASP.NET Core 中的静态文件中间件。无论是 IIS 还是 Apache,对静态页面进行展示,是一个 Web 服务器最基本的能力。在 ASP.NET Core 中,我们是通过静态文件中间件来实现这个功能,简而言之,通过这个功能,我们就可以让别人通过 IP 或者域名来访问 wwwroot 目录下的内容。具体代码如下:

app.UseDirectoryBrowser();
app.UseStaticFiles(); 

当然,这里有一个细节是为了让别人可以通过 IP 或者域名来访问你的服务,你需要修改下 WebHostBuilder 中 URL。此外,因为我们在前端界面中使用了绝对的 URL 去访问 WebAPI,因此,当前端页面和 WebAPI 不在一个域中时,就会出现所谓垮域的问题,这方面的内容非常丰富,因为这是一个再常见不过的问题,身处在这个时代,80%的问题都已经被解决过了,这到底是我们的幸运还是不幸呢?

WebHost.CreateDefaultBuilder(args)
   .UseStartup<Startup>()
   .UseUrls("http://*:8002"); 

本文小结

本文在上一篇的基础上,借助 Redis 和 WebSocket 实现了一个简单的弹幕系统。博主的初衷是想一个数据可视化的小项目,可以通过 WebSocket 实时地刷新图表,因为在博主看来,数据分析同样是有趣的事情。这篇文章选取博主在工作中遇到的实际场景作为切入点,试图发掘出 WebSocket 在实时应用方面更多的可能性。首先,我们编写了“消息推送”中间件,并通过不同的路由来处理各自的业务,实现了模块间的相互独立。接下来,我们讨论了 Redis 作为消息队列的可行性,并基于 Redis 编写了一个简单的消息队列。最终,通 Canvas API 完成客户端弹幕的绘制,实现了从后端到前端的方案整合。藉由这个小项目,可以引出 ASP.NET Core 相关的话题,譬如静态文件中间件、部署、跨域等等的话题,感兴趣的朋友可以自己去做进一步的了解,以上就是这篇博客的全部内容啦,谢谢大家!