ASP.NET Core SignalR 配置与集成测试究极指南

这篇文章也可以在我的博客中查看

前言

哥们最近都在埋头苦干,沉默是金,有一段时间没更新博客了。然而今儿SignalR集成测试实属是给我整破防了。虽说SignalR是.NET官方维护的实时通信库,已经开发了有十几年,甚至已经编入至了core dll,然而更新迭代异常迅速,导致文档不全,出了事不知所措。这不最近在集成测试SignalR这点上就踩了大坑。

今天就给大伙分享一下如何配置SignalR,并重点讲解如何在 .NET 8 中使用xUnitMicrosoft.AspNetCore.Mvc.Testing.WebApplicationFactory对最新版(ASP.NET Core)SignalR进行集成测试,希望后来者可以少走弯路。

痛点

SignalR测试为何困难,原因有下:

  1. WebApplicationFactory,或者说其背后的TestServer,并不提供真的服务器环境,所有默认配置下的网络客户端(当然包括HttpClient)都无法连接至该模拟的服务器。
    • 然而SignalR客户端所有连接都是在默认网络环境下的,需要替换成TestServer环境下的客户端
  2. HttpClient并不提供WebSocket连接支持。
    • 然而SignalR实时通讯首选的是WebSocket,所以我们还要配个TestServer环境下的WebSocket客户端
  3. Hub受身份验证保护。
    • 替换成TestServer客户端的时候还需要考虑身份验证

汗流浃背了家人们

关于本文

本文按这三个问题为思路逐步进行,最合理的解决方案会在文末给出。

如果你觉得TL;DR、不想关注过程、或者认为看代码比看文章舒服,可以跳转到文章最后获取项目源码👇

本文只介绍SignalR配置与集成测试,阅读本文前建议做以下准备工作(本文可能不会介绍以下内容):

  1. SignalR的使用(只提及部分)
  2. 配置[Authorize]身份认证(只一笔带过)
  3. 配置.NET集成测试框架,如 xUnit
  4. 配置WebApplicationFactory

本文操作环境:

  1. .NET 8
  2. xUnit 测试框架

无身份验证SignalR

在引入复杂性之前,应先处理最核心的配置,因此先不配置身份验证。

基本配置

配置Hub

在 .NET 8 中,SignalR已经集成至ASP.NET Core中,因此不需要下载任何Nuget包就能够使用。

配置也十分简洁,首先需要创建一个HubHub相当于是SignalR中的控制器。
创建Hub非常简单,只需要继承Hub即可。以下例子展示了一个最基本的收发消息ChatHubSendMessage向所有连接广播一条消息:

using Microsoft.AspNetCore.SignalR;namespace SignalR.IntegrationTests;public class ChatHub : Hub
{public async Task SendMessage(string message){await Clients.All.SendAsync("ReceiveMessage", message);}
}
  1. SendMessage是客户端向服务端发送消息的入口
    • 该方法可以有返回值,返回值会传回调用者
  2. ReceiveMessage是服务端向客户端发送消息的入口
    • message是参数,参数不一定只有一个,也不一定为string
  3. A向B发送一条聊天信息其实需要经历两次交互
    1. A向服务器发送消息
    2. 服务器向B发送消息

配置Program.cs

Program.cs中注册SignalR组件,最简单的配置如下:

const string HubsPrefix = "/hubs"; // <-- Grouped by prefix /hubsvar builder = WebApplication.CreateBuilder(args);builder.Services.AddSignalR(); // <-- Add SignalRvar app = builder.Build();app.MapGroup(HubsPrefix).MapHub<ChatHub>("/chat"); // <-- Map your ChatHub to /hubs/chatapp.Run();

强类型Hub

上面的例子中,服务端消息方法ReceiveMessage是字符串,众所周知字符串意味着弱类型,无编译时提示,稍不留神可能就会写错。
.NET提供了一个做法强类型化这些方法。

首先定义一个接口:

public interface IChatClientProxy
{public Task ReceiveMessage(string message);
}

由于客户端还是需要以字符串订阅消息,因此函数应以客户端的角度进行命名:

  1. Receive而不是Send
  2. 虽然是异步方法,但不加Async后缀

然后将ChatHub修改如下:

public class ChatHub : Hub<IChatClientProxy>
{public async Task SendMessage(string message){await Clients.All.ReceiveMessage(message);}
}

SignalR会自动实现IChatClientProxy接口,当调用这个接口的方法时,对应名称的消息就会被发出。

在Hub外向客户端发送消息

更多时候我们会在Hub之外发送消息,就需要借助IHubContext获取Hub上下文。这个接口也支持强类型化。
以下实现了一个简单的服务,先做一系列检测和记录,再使用IHubContext实现实时发送消息:

using Microsoft.AspNetCore.SignalR;namespace SignalR.IntegrationTests;public class ChatService(IHubContext<ChatHub, IChatClientProxy> _hubContext)
{public async Task SendMessageToAllAsync(string message){// Chek for permissions...// Record to database...// ...await _hubContext.Clients.All.ReceiveMessage(message);}
}

为了保持程序中的一致性,通常情况下也会希望在Hub中引用自己的服务,而不是直接发送消息:

public class ChatHub(ChatService _chatService) : Hub<IChatClientProxy>
{public async Task SendMessage(string message){await _chatService.SendMessageToAllAsync(message);}
}

别忘了在Program.cs中为自己的服务注册依赖注入:

builder.Services.AddScoped<ChatService>();

在客户端中接收SignalR消息

呃,严格意义上你无法在服务端中接收SignalR消息,你需要一个客户端接收服务端发出的信息。

以下代码是客户端代码,它可能位于另一个项目,可以是另一种语言实现,甚至可以处于另一个平台(e.g. Android)
但是它也可以碰巧是同一个平台,又碰巧是C#实现,甚至碰巧在同一个项目 😉

总之如果要在C#中接收SignalR消息,你需要安装客户端Nuget包Microsoft.AspNetCore.SignalR.Client
下面的例子展示了如何向服务端的ChatHub收发消息:

using Microsoft.AspNetCore.SignalR.Client;
using System.Diagnostics;var connection = new HubConnectionBuilder().WithUrl("http://localhost/hubs/chat").Build();
// Add receive message handler.
connection.On<string>("ReceiveMessage", (message) => Debug.WriteLine(message));await connection.StartAsync();// Send message.
await connection.InvokeAsync("SendMessage", "Hello World");await connection.StopAsync();
  1. On方法用于接收消息。注意泛型参数一定要与服务端的类型兼容,否则可能收不到对应消息
  2. InvokeAsync方法用于发送消息。第一个参数是远程方法名,第二个起是远程方法对应的参数
    1. 该方法可以有泛型参数TResult,以接受对应类型的返回值
  3. HubConnectionBuilder还可以配置断线重连、身份验证等功能,具体请查阅官方文档

集成测试

准备工作

进行下一步之前,需要先:

  1. 新建一个 xUnit 项目
  2. 添加主项目为依赖项
  3. 在测试项目中安装并配置WebApplicationFactory
  4. 在测试项目中安装Microsoft.AspNetCore.SignalR.ClientNuget包

测试用例

根据含义,我们会尝试使用SignalR客户端发送一条消息,然后断言能够收到消息:

using Microsoft.AspNetCore.Mvc.Testing;
using Microsoft.AspNetCore.SignalR.Client;public class WebAppFactory : WebApplicationFactory<Program> { }public class HubIntegrationTests(WebAppFactory _factory) : IClassFixture<WebAppFactory>
{private HubConnection SetupHubConnection(string path){var uri = new Uri(_factory.Server.BaseAddress, path);return new HubConnectionBuilder().WithUrl(uri).Build();}[Fact]public async Task MessageTest(){// --> Arrangevar connection = SetupHubConnection("/hubs/chat");string? received = null;connection.On<string>("ReceiveMessage", (m) => received = m);await connection.StartAsync();string message = "Hello World";// --> Actawait connection.InvokeAsync("SendMessage", message);// Wait for messages to be received. You may need to increase the delay if you're running in a slow environment.await Task.Delay(1);// --> AssertAssert.Equal(message, received);}
}
  • SetupHubConnection函数用作连接SignalR服务器。
  • 其中等待了1毫秒以确保有足够的时间接收消息
    • 如果你的测试环境是老爷机,可能需要增加等待时间

然而这个用例会失败,错误如下:

System.Net.Http.HttpRequestException : No connection could be made because the target machine actively refused it. (localhost:80)

原因是TestServer并不是真的服务器,它只模拟ASP.NET应用服务器的行为,而不会在宿主机环境中启动真的服务器。因此我们使用常规的方式进行连接是无法访问的。但没有关系……

非WebSocket传输模式的测试

TestServer提供了一个用于连接至测试服务器的HttpMessageHandler对象,也就是任何支持HttpMessageHandler进行Http数据交换的库都可以通过使用该对象访问TestServer
经常接触.NET测试的伙伴此时已经要素察觉了:HttpClientHttpMessageHandler就是原生支持的!

然后还有两个好消息:

  1. WebSocket模式下的SignalR发起的连接使用的就是HttpClient
    • 没错,只是非WebSocket,但总比连接失败要好!
  2. SignalR提供了一个配置项,可以替换内部HttpClient使用的HttpMessageHandler

所以解决方案很简单,只需要将上述SetupHubConnection函数修改成以下形式:

private HubConnection SetupHubConnection(string path)
{var server = _factory.Server;var uri = new Uri(server.BaseAddress, path);return new HubConnectionBuilder().WithUrl(uri, o =>{o.HttpMessageHandlerFactory = _ => server.CreateHandler();}).Build();
}

TestServer.CreateHandler()生成了一个HttpMessageHandler,将它赋值给HttpMessageHandlerFactory,可以改变其内部HttpClient的连接行为,使其得以与TestServer进行交互。

问题

虽然测试是能通过了,但是注意到测试时间长达4秒,这对于本地服务器来讲显然是不正常的:

========== Starting test run ==========
[xUnit.net 00:00:00.00] xUnit.net VSTest Adapter v2.5.3.1+6b60a9e56a (64-bit .NET 8.0.4)
[xUnit.net 00:00:00.04]   Starting:    SignalR.IntegrationTests.Tests
[xUnit.net 00:00:04.37]   Finished:    SignalR.IntegrationTests.Tests
========== Test run finished: 1 Tests (1 Passed, 0 Failed, 0 Skipped) run in 4.4 sec ==========

原因是因为产生了等待。事实上,这个用例并没有建立WebSocket连接,而是在等待WebSocket连接超时后,转为了使用LongPolling模式连接。
如果我们强制限制SignalR客户端使用WebSocket连接:

private HubConnection SetupHubConnection(string path)
{var server = _factory.Server;var uri = new Uri(server.BaseAddress, path);return new HubConnectionBuilder().WithUrl(uri, o =>{o.Transports = Microsoft.AspNetCore.Http.Connections.HttpTransportType.WebSockets; // WebSockets only.o.HttpMessageHandlerFactory = _ => server.CreateHandler();}).Build();
}

这个用例会在4秒后超时失败:

System.AggregateException : Unable to connect to the server with any of the available transports. (WebSockets failed: Unable to connect to the remote server) (ServerSentEvents failed: The transport is disabled by the client.) (LongPolling failed: The transport is disabled by the client.)

轮询并不是一般情况下的连接方式,而且我们也不希望每个连接都等待4秒,所以,有没有办法能够进行Socket连接?

WebSocket传输模式的测试

WebSocket连接失败的原因是WebSocketClient独立于HttpClient,虽然我们构建了SignalR内部HttpClientTestServer之间的连接,但是并没有改变WebSocketClient,它仍然是向真正的宿主机环境建立连接,所以必然会失败。

但是没有关系,这个问题早在几年前就被SignalR团队注意到,并提供了替换WebSocketClient的配置项:

private HubConnection SetupHubConnection(string path)
{var server = _factory.Server;var uri = new Uri(server.BaseAddress, path);return new HubConnectionBuilder().WithUrl(uri, o =>{o.Transports = HttpTransportType.WebSockets;o.HttpMessageHandlerFactory = _ => server.CreateHandler();// Support WebSocket transports.o.WebSocketFactory = async (context, cancellationToken) =>{var wsClient = server.CreateWebSocketClient();return await wsClient.ConnectAsync(context.Uri, cancellationToken);};o.SkipNegotiation = true;}).Build();
}

通过配置WebSocketFactory,可以将默认的WebSocketClient换成TestServer提供的客户端。从而能够对其进行WebSocket访问。
在WebSocket模式下,顺便设置了SkipNegotiation,可以减少协商时间,而不会影响结果。

这里其实可以省略HttpMessageHandlerFactory的配置,因为使用WebSocket时不会用到HttpClient。但如果使用LongPolling则很重要,因此还是保留以供选择。

修改了WebSoketClient配置后,重新运行测试用例,这次可以快速以WebSocket模式通过测试:

========== Starting test run ==========
[xUnit.net 00:00:00.00] xUnit.net VSTest Adapter v2.5.3.1+6b60a9e56a (64-bit .NET 8.0.4)
[xUnit.net 00:00:00.03]   Starting:    SignalR.IntegrationTests.Tests
[xUnit.net 00:00:00.21]   Finished:    SignalR.IntegrationTests.Tests
========== Test run finished: 1 Tests (1 Passed, 0 Failed, 0 Skipped) run in 216 ms ==========

带身份验证SignalR

身份配置

SignalR的身份验证方式

SignalR可以使用CookieToken令牌两种方式进行身份认证。

Cookie是浏览器环境下的首选方式,可以自动传递凭证;而Token则是非浏览器客户端下最简便的做法。
由于Cookie开箱即用,不需要做额外配置,因此本文只重点介绍Token做法。

SignalR Token令牌传递方式

根据SignalR文档,在不同情况下有不同的传达方式:

  1. 在非浏览器环境中,以Authorization请求头的方式传递
  2. 在浏览器环境的WebSocket, Server Side Event模式下,无法使用自定义请求头,需要以查询字符串的方式传递
    • 该查询字符串需要在身份验证服务器自行读取接收

服务端配置接收access_token

所有无法自定义连接请求头的情况下,都约定使用一个写死的(😅微软你也干这事啊)查询字符串access_token作为身份认证的参数。

你写死不要紧,要紧的是我们使用SignalR是需要手动处理这个查询字符串的,否则这种情况下永远无法触发身份验证。

虽然官网有说明,但是总有像我一样的愣头青不喜欢看官方文档然后捣鼓了一整天才发现涅麻麻的要手动配置这个查询字符串。

所以为了减少愣头青,请你务必:
按照以下操作配置查询字符串!
按照以下操作配置查询字符串!
按照以下操作配置查询字符串!

接收查询字符串

需要在SignalR服务端中主动接收这个查询字符串。
使用不同的身份验证库,需要以不同的方式进行接收:

  1. 你使用了内置的JWT库或者Identity Server,可以参照官方文档进行配置
  2. 你使用了Identity内置的BearerToken,可以在Bearer Token中间件进行配置(见下文)
  3. 你使用了其它的身份验证库,基本也是相同的套路:需要在验证请求事件中手动将该查询字符串赋值为用户凭证
Identity 内置Bearer Token身份验证

我这里使用了 .NET 8 Identity的内置BearerToken,所以能够实现目标的最小配置Program.cs是这样的:

using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Identity.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore;
using SignalR.IntegrationTests;const string HubsPrefix = "/hubs";var builder = WebApplication.CreateBuilder(args);builder.Services.AddAuthorization();
builder.Services.AddAuthentication(IdentityConstants.BearerScheme).AddCookie(IdentityConstants.ApplicationScheme).AddBearerToken(IdentityConstants.BearerScheme, o =>{o.Events = new(){OnMessageReceived = context =>{var accessToken = context.Request.Query["access_token"];var path = context.HttpContext.Request.Path;// If the request is for our hub...if (!string.IsNullOrEmpty(accessToken) && path.StartsWithSegments(HubsPrefix)){// Read the token out of the query stringcontext.Token = accessToken;}return Task.CompletedTask;}};});
builder.Services.AddIdentityCore<IdentityUser>().AddApiEndpoints().AddEntityFrameworkStores<IdentityDbContext>();
builder.Services.AddDbContext<IdentityDbContext>(x => x.UseInMemoryDatabase("db"));builder.Services.AddSignalR();var app = builder.Build();app.MapIdentityApi<IdentityUser>();app.MapGroup(HubsPrefix).MapHub<ChatHub>("/chat");app.Run();

使用身份验证保护Hub

与Controller一样,通过使用AuthorizeAllowAnonymous特性控制对Hub的访问

[Authorize]
public class ChatHub(ChatService _chatService) : Hub<IChatClientProxy>
{// ......
}

为Hub连接提供身份验证

Cookie验证
  1. 浏览器环境中,正常使用Cookie登录,凭证会在请求时自动携带
  2. 非浏览器环境中,可以通过手动设置Cookie请求头实现Cookie验证
    • 但这种做法不如使用Token更加正规
Token令牌验证

Token可以在客户端发起连接前使用AccessTokenProvider提供。

var connection = new HubConnectionBuilder().WithUrl("http://localhost/hubs/chat", options =>{options.AccessTokenProvider = () => Task.FromResult(token);}).Build();

考虑到重连与Token过期问题,AccessTokenProvider接受的是一个工厂函数,你可以选择动态获取新Token,而不是写死一个值

集成测试

由于我们替换了默认的WebSocketClient,我们需要手动携带Token令牌,以支持WebSocket模式下的身份验证;非WebSocket的身份验证仍然使用AccessTokenProvider配置项,无需修改。因此修改SetupHubConnection方法:

  1. 配置AccessTokenProvider参数,使非WebSocket连接方式能够携带令牌
  2. token添加至WebSocketClient中,使WebSocket连接方式能够携带令牌。由于是非浏览器环境,有两种方案可以选择:
    1. 添加名为access_token的查询字符串
    2. 添加Authorization请求头

小孩子才做选择,我全都要。

private HubConnection SetupHubConnection(string path, string? token = null)
{var server = _factory.Server;var uri = new Uri(server.BaseAddress, path);return new HubConnectionBuilder().WithUrl(uri, o =>{o.Transports = HttpTransportType.WebSockets;o.HttpMessageHandlerFactory = _ => server.CreateHandler();o.WebSocketFactory = async (context, cancellationToken) =>{var wsClient = server.CreateWebSocketClient();if (token != null){// Authentication for socket transports. (Chooses one of these.)// Option1: Use request headers.wsClient.ConfigureRequest = request => request.Headers.Authorization = new($"Bearer {token}");// Option2: Add access token to query string.uri = new Uri(QueryHelpers.AddQueryString(context.Uri.ToString(), "access_token", token));// I like both ;)}else{uri = context.Uri;}return await wsClient.ConnectAsync(uri, cancellationToken);};o.SkipNegotiation = true;// Authentication for non-socket transports. (Can be omitted here.)o.AccessTokenProvider = () => Task.FromResult(token);}).Build();
}

最后在用例中指定token参数,即可成功通过测试。

Q: 我应该如何生成token令牌?

如何生成令牌取决于你身份验证的实现方式。

在使用WebApplicationFactory的集成测试中,你可以比较容易地使用真实的用户与正常的登录方式获取令牌;如果身份认证本身并不是集成测试的关键,你可以设法使用测试替身替换掉原有的身份验证程序。(但一般情况下这只会更麻烦)

如果你想了解如何以正常登录方式获取令牌,可以查看我的源码👇


至此,所有问题解决!现在我们可以用SignalR WebSocket模式对带身份认证的Hub进行集成测试了!

项目源码

  • Github

参考资料

  1. Authentication and authorization in ASP.NET Core SignalR
  2. [SignalR] Better integration with TestServer
  3. SignalR Hub auth?

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/web/8651.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

MVC与MVVM架构模式

1、MVC MVC&#xff1a;Model-View-Controller&#xff0c;即模型-视图-控制器 MVC模式是一种非常经典的软件架构模式。从设计模式的角度来看&#xff0c;MVC模式是一种复合模式&#xff0c;它将多个设计模式结合在一种解决方案中&#xff0c;从而可以解决许多设计问题。 MV…

【3D目标检测】常见相关指标说明

一、mAP指标 mean Average Precision&#xff08;平均精度均值&#xff09;&#xff0c;它是目标检测和信息检索等任务中的重要性能指标。mAP 通过综合考虑精度和召回率来衡量模型的总体性能。 1.1 精度&#xff08;Precision&#xff09; 表示检索到的目标中实际为正确目标…

Spring Task及订单状态定时处理

1&#xff1a;Spring Task概念&#xff1a; Spring Task 是Spring框架提供的任务调度工具&#xff0c;可以按照约定的时间自动执行某个代码逻辑 定时任务的理解 定时任务即系统在特定时间执行一段代码&#xff0c;它的场景应用非常广泛&#xff1a; 购买游戏的月卡会员后&a…

前端如何给特定的组件设置缓存并处理定位问题?

前端如何给某些组件设置缓存并处理定位? 最近有个需求就是a>b,b页面处理了些操作,返回a页面时, b页面若有操作则a页面需要刷新并定位到上次点击的位置,b若没有操作则无需刷新直接定位上次点击的位置 1.首先在store中存储缓存的组件 vuex代码: const cached {state: {ca…

Centos7网络处理name or service not known

1、编辑->虚拟网络编辑器 2、查看本机的ip 3、 /etc/sysconfig/network-scripts/ 查看文件夹下面的 ifcfg-eth33 后面的33可能不一样 vi /etc/resolv.conf 编辑文件添加以下DNS nameserver 114.114.114.114 4、设置本机的网络 5、ping www.baidu.com 先重启…

第50期|GPTSecurity周报

GPTSecurity是一个涵盖了前沿学术研究和实践经验分享的社区&#xff0c;集成了生成预训练Transformer&#xff08;GPT&#xff09;、人工智能生成内容&#xff08;AIGC&#xff09;以及大语言模型&#xff08;LLM&#xff09;等安全领域应用的知识。在这里&#xff0c;您可以找…

js,JavaScript 类型化数组详解(2024-05-04)

1、JavaScript 类型化数组 在 Javascript 中&#xff0c;类型化数组是二进制数据的类似数组的缓冲区。 不存在名为 TypedArray 的 JavaScript 属性或对象&#xff0c;但属性和方法可以与类型化数组对象一起使用&#xff1a; const myArr new Int8Array(10); // 0,0,0,0,0,0…

全双工音频对讲模块-支持空中升级、多级无线中继

SA618F30是一款高集成的大功率全双工无线音频模块&#xff0c;发射功率高达32dBm。该音频模块简化接口&#xff0c;只需外接音频功放或麦克风即可作为一个小型对讲机&#xff0c;方便快捷嵌入到各类手持设备中。支持多级无线中继&#xff0c;支持OTA空中升级。 SA618F30配备1W…

Java快速入门系列-11(项目实战与最佳实践)

第十一章&#xff1a;项目实战与最佳实践 11.1 项目规划与需求分析项目规划需求分析实例代码 11.2 系统设计考虑实例代码 11.3 代码实现与重构实例代码 11.4 性能优化与监控实例代码 11.5 部署与持续集成/持续部署(CI/CD)实例代码 11.1 项目规划与需求分析 在进行任何软件开发…

06_G1调优配置

本章主要介绍&#xff0c;如果G1默认的一些配置无法满足你的需求&#xff0c;要如何进一步调优。 G1的一般建议 一般建议是使用G1并保持默认设置&#xff0c;如有需要&#xff0c;可以通过使用 -Xmx 来设置最大的Java堆大小&#xff0c;同时也可以通过 -XX:MaxGCPauseMillis来…

MySQL数据库失效:潜在场景、影响与应对策略

在当今数字化时代&#xff0c;数据库作为数据存储和管理的核心组件&#xff0c;其稳定性和可靠性直接影响着业务的连续性和用户体验。MySQL&#xff0c;作为最受欢迎的关系型数据库管理系统之一&#xff0c;广泛应用于互联网、金融、教育等多个行业。然而&#xff0c;即便是这样…

NTP 协议获取网络时间

从github 中找到的一份代码进行的修改 板卡是0区,手动加了8个时区 #include <iostream> #include <netdb.h> #include <netinet/in.h> #include <stdio.h> #include <stdlib.h> #include <string.h> #include <sys/socket.h> #inclu…

Hikyuu-PF-银行股轮动交易策略实现

今天&#xff0c;带来的是“如何使用 Hikyuu 中的投资组合来实现银行股轮动交易策略”。 这个策略的逻辑很简单&#xff1a;持续持有两支市净率最低银行股&#xff0c;然后每月换仓 定义回测周期与回测标的 同样&#xff0c;首先定义回测周期&#xff1a; # 定义回测日期 …

撰写一份详尽的数据治理实施方案

对于拥有15年经验的资深数据治理工程师而言,是一个复杂而细致的任务,应当涵盖策略规划、组织架构调整、技术选型、流程设计、合规性考量、监控与评估等多个维度。本文概述一个高层次的数据治理实施方案框架,并简要说明每个部分的关键内容。如需深入细节,您可以根据这个框架…

了解内存函数

✨✨欢迎&#x1f44d;&#x1f44d;点赞☕️☕️收藏✍✍评论 个人主页&#xff1a;秋邱博客 所属栏目&#xff1a;C语言 前言 内存函数不止malloc、calloc、realloc、free还有memcpy、memmove、memset、memcmp。前四个的头文件是<stdlib.h>,后四个的头文件是<strin…

Ansible----playbook模块之templates模块、tags模块、roles模块

目录 引言 一、templates模块 &#xff08;一&#xff09;关键信息 &#xff08;二&#xff09;实际操作 1.定义主机组 2.设置免密登录 3.分别建立访问目录 4.定义模板文件 5.创建playbook文件 6.执行剧本 7.验证结果 二、tags模块 &#xff08;一&#xff09;创建…

《QT实用小工具·六十一》带动画的三角形指示箭头

1、概述 源码放在文章末尾 该项目实现了一个带动画效果的三角形指示箭头&#xff0c;项目demo演示如下所示&#xff1a; 用法 interestingindicate.h interestingindicate.cpp 放到工程中&#xff0c;直接使用即可。 注意&#xff1a;建议绝对布局&#xff0c;手动指定 wid…

git stash技巧

1.缘由 有时代码写到一半有新bug要修复&#xff0c;这时可以先暂存当前代码&#xff08;使用git stash&#xff09;&#xff0c;修复完bug再回到原先的暂存文件&#xff08;使用git stash pop&#xff09;继续工作。 2.git stash的常用命令&#xff1a; &#xff08;1&#x…

【大数据】containered学习笔记

文章目录 1. Containerd安装1.1 YUM方式安装 【后端&网络&大数据&数据库目录贴】 1. Containerd安装 1.1 YUM方式安装 获取YUM源 获取阿里云YUM源 wget -O /etc/yum.repos.d/docker-ce.repo https://mirrors.aliyun.com/docker-ce/linux/centos/docker-ce.repo 查…

华为车BU迈入新阶段,新任CEO对智能车的3个预判

作者 |张马也 编辑 |德新 4月24日&#xff0c;北京车展前夕&#xff0c;华为召开了新一年的智能汽车解决方案新品发布会。 这次发布会&#xff0c;也是华为智能汽车解决方案BU&#xff08;简称「车BU」&#xff09;CEO 靳玉志的公开首秀。 一开场&#xff0c;靳玉志即抛出了…