UE4 C++联网RPC教程笔记(三)(第8~9集)完结

UE4 C++联网RPC教程笔记(三)(第8~9集)完结

  • 8. exe 后缀实现监听服务器
  • 9. C++ 实现监听服务器

8. exe 后缀实现监听服务器

前面我们通过蓝图节点实现了局域网连接的功能,实际上我们还可以给项目打包后生成的 .exe 文件创建一个快捷方式,然后修改这个快捷方式的属性中的目标就可以实现简易的联网功能。

下面内容截取自梁迪老师准备的 RPC 联网文档:

使用 .exe 后缀输入和 open IP 地址联网

注意:这里只讨论 NM_Standalone、NM_ListenServer 以及 NM_Client 的情况

  1. NM_Standalone:打包出 exe 后,不创建快捷方式直接运行 exe,执行 GetNetMode() 返回 NM_Standalone 类型,在这个状态下打印 GetWorld()->IsServer() 以及 HasAuthority() 都是 true,如果在存在监听服务器的情况下,运行命令行 open 127.0.0.1,该单独端就会链接上监听服务器,执行 GetNetMode() 返回 NM_Client 类型,打印 GetWorld()->IsServer() 以及 HasAuthority() 都是 false,如果再次运行命令行 open 127.0.0.1,该端会先断开服务器,然后再链接一次
  2. NM_ListenServer:打包出来的 exe 生成快捷方式,并且在快捷方式的属性下在 exe 结尾添加 (空格)?listen,如:RPCProject.exe ?listen,运行该快捷方式就会运行监听服务器端,执行 GetNetMode() 返回 NM_ListenServer 类型,打印 GetWorld()->IsServer() 以及 HasAuthority() 都是true,如果运行命令行 open 127.0.0.1,该监听服务器端就会变成单独端 NM_Standalone
  3. NM_Client:打包出来的 exe 生成快捷方式,并且在快捷方式的属性下在 exe 结尾添加 (空格)127.0.0.1 -game,如:RPCProject.exe 127.0.0.1 -game,运行该快捷方式,如果存在监听端,就会链接上监听服务器,成为客户端,执行 GetNetMode() 返回 NM_Client 类型,打印 GetWorld()->IsServer() 以及 HasAuthority() 都是 false

接下来我们在 GameMap 里用到的玩家控制器中添加一些打印当前端的逻辑。

RPCController.h

protected:.void EchoNetMode();

RPCController.cpp

// 引入头文件
#include "RPCHelper.h"void ARPCController::BeginPlay()
{Super::BeginPlay();// 限定打开的窗口尺寸。此处老师将变量名拼写错成 “Src”FString ScreenCommand = FString("r.setres 1280x720w");ConsoleCommand(ScreenCommand);bShowMouseCursor = false;FInputModeGameOnly InputMode;SetInputMode(InputMode);// 打印当前的端EchoNetMode();
}void ARPCController::EchoNetMode()
{ENetMode NetMode = GetNetMode();switch (NetMode){case NM_Standalone:DDH::Debug() << "NM_Standalone" << DDH::Endl();break;case NM_DedicatedServer:DDH::Debug() << "NM_DedicatedServer" << DDH::Endl();break;case NM_ListenServer:DDH::Debug() << "NM_ListenServer" << DDH::Endl();break;case NM_Client:DDH::Debug() << "NM_Client" << DDH::Endl();break;case NM_MAX:DDH::Debug() << "NM_MAX" << DDH::Endl();break;}
}

编译后,将默认关卡设置为 GameMap。随后开始打包。

在这里插入图片描述
打包成功后,运行 .exe 文件,可以看到左上角打印了当前的端名以及控制器名。

在这里插入图片描述
创建 .exe 文件的一个快捷方式,命名为 RPCCourseServer (保留 .exe 后缀),随后修改该文件的属性。

在这里插入图片描述
运行 RPCCourseServer.exe,可以看到左上角已经变成了聆听服务器。

在这里插入图片描述
再创建一个快捷方式,命名为 RPCCourseClient,这次给属性里的目标添加后缀 (空格)127.0.0.1 -game。先运行 RPCCourseServer,然后再运行 RPCCourseClient.exe,可以看到后者的窗口里左上角显示是客户端。

在这里插入图片描述
此时在服务端按下 J 键,只有在客户端能看到角色处生成了红色的数字 1。说明联网方法和变量都是可以用的。

关掉客户端,服务端左上角会显示客户端的控制器登出了。

在这里插入图片描述
保持服务端开启,运行 RPCCourse.exe,按 ~ 键(波浪符)呼出控制台,输入 open 127.0.0.1。可以看到独立端加入了服务端,并且原独立端左上角输出了当前为客户端。

在这里插入图片描述
此时在服务端按下 J 键,也是只有原独立端可以看到自己角色位置生成了一个红色的数字 1。

在服务端呼出控制台然后输入 open 127.0.0.1,服务器会关闭。

在这里插入图片描述

9. C++ 实现监听服务器

前面我们用蓝图和快捷方式实现聆听服务器联机,接下来我们尝试下用 C++ 来实现同样的效果。下面内容截取自梁迪老师准备的 RPC 联网文档:

创建寻找加入会话 C++ 模式(这里只实现局域网)

C++ 联网步骤和蓝图基本相同,但是中间多了一个 StartSession() 方法需要调用,主要可以参考

  1. UCreateSessionCallbackProxy
  2. UStartSessionCallbackProxy
  3. UFindSessionsCallbackProxy
  4. UJoinSessionCallbackProxy
  5. UDestroySessionCallbackProxy

这些类的实现,具体实现参考项目里的 URPCInstance 类。

UE4 官方推荐将联网的逻辑放在 GameInstance 下处理,GameInstance 在整个游戏所有关卡中都存在,用来传递关卡数据,在保存数据方面起作用,联网数据放在 GameInstance 下方便在任何关卡去操作联网

接下来开始实操。在默认路径下创建一个 C++ 的 Instance 类,命名为 RPCInstance

将默认地图设置成 MenuMap。

来到主界面 UI 类,声明一个 URPCInstance 的指针和相应的注册方法,用来保存对 GameInstance 的引用,并且声明两个蓝图可调用的方法用于接入主界面的按钮点击事件。

MenuWidget.h

// 提前声明
class URPCInstance;UCLASS()
class RPCCOURSE_API UMenuWidget : public UUserWidget
{GENERATED_BODY()public:void AssignRPCInstance(URPCInstance* InInstance);UFUNCTION(BlueprintCallable)void LANServerEvent();UFUNCTION(BlueprintCallable)void LANClientEvent();public:URPCInstance* RPCInstance;
};

MenuWidget.cpp

// 引入头文件
#include "RPCInstance.h"void UMenuWidget::AssignRPCInstance(URPCInstance* InInstance)
{RPCInstance = InInstance;
}void UMenuWidget::LANServerEvent()
{RPCInstance->HostSession();
}void UMenuWidget::LANClientEvent()
{RPCInstance->ClientSession();
}

RPCInstance 里承载着联网相关的逻辑,主要都是调用网络模块的 API 和利用委托绑定回调函数。

RPCInstance.h

// 引入头文件
#include "Interfaces/OnlineSessionInterface.h"	// 如果这个不行就用下面这句
//#include "../Plugins/Online/OnlineSubsystem/Source/Public/Interfaces/OnlineSessionInterface.h"	
#include "Delegates/IDelegateInstance.h"#include "RPCInstance.generated.h"// 提前声明
class IOnlineSubsystem;
class APlayerController;UCLASS()
class RPCCOURSE_API URPCInstance : public UGameInstance
{GENERATED_BODY()public:URPCInstance();// 注册玩家控制器,并且获取联网系统和端 IDvoid AssignPlayerController(APlayerController* InController);// 创建会话void HostSession();// 寻找会话void ClientSession();// 销毁会话void DestroySession();protected:// 开启服务器回调函数void OnCreateSessionComplete(FName SessionName, bool bWasSuccessful);void OnStartOnlineGameComplete(FName SessionName, bool bWasSuccessful);// 加入服务器回调函数void OnFindSessionsComplete(bool bWasSuccessful);void OnJoinSessionComplete(FName SessionName, EOnJoinSessionCompleteResult::Type Result);// 销毁会话回调函数void OnDestroySessionComplete(FName SessionName, bool bWAsSuccessful);protected:APlayerController* PlayerController;// 开启服务器委托与句柄FOnCreateSessionCompleteDelegate OnCreateSessionCompleteDelegate;FOnStartSessionCompleteDelegate OnStartSessionCompleteDelegate;FDelegateHandle OnCreateSessionCompleteDelegateHandle;FDelegateHandle OnStartSessionCompleteDelegateHandle;// 加入服务器委托与句柄FOnFindSessionsCompleteDelegate OnFindSessionsCompleteDelegate;FOnJoinSessionCompleteDelegate OnJoinSessionCompleteDelegate;FDelegateHandle OnFindSessionsCompleteDelegateHandle;FDelegateHandle OnJoinSessionCompleteDelegateHandle;// 销毁会话委托与句柄FOnDestroySessionCompleteDelegate OnDestroySessionCompleteDelegate;FDelegateHandle OnDestroySessionCompleteDelegateHandle;// 联网系统IOnlineSubsystem* OnlineSub;// 端的 IDTSharedPtr<const FUniqueNetId> UserID;// 保存寻找到的 SessionsTSharedPtr<FOnlineSessionSearch> SearchObject;
};

RPCInstance.cpp

// 引入头文件
#include "../Plugins/Online/OnlineSubsystem/Source/Public/OnlineSubsystem.h"
#include "../Plugins/Online/OnlineSubsystem/Source/Public/OnlineSessionSettings.h"
#include "../Plugins/Online/OnlineSubsystem/Source/Public/Interfaces/OnlineSessionInterface.h"
#include "../Plugins/Online/OnlineSubsystemUtils/Source/OnlineSubsystemUtils/Public/OnlineSubsystemUtils.h"
#include "GameFramework/PlayerController.h"
#include "RPCHelper.h"
#include "Kismet/GameplayStatics.h"URPCInstance::URPCInstance()
{// 绑定回调函数OnCreateSessionCompleteDelegate = FOnCreateSessionCompleteDelegate::CreateUObject(this, &URPCInstance::OnCreateSessionComplete);OnStartSessionCompleteDelegate = FOnStartSessionCompleteDelegate::CreateUObject(this, &URPCInstance::OnStartOnlineGameComplete);OnFindSessionsCompleteDelegate = FOnFindSessionsCompleteDelegate::CreateUObject(this, &URPCInstance::OnFindSessionsComplete);OnJoinSessionCompleteDelegate = FOnJoinSessionCompleteDelegate::CreateUObject(this, &URPCInstance::OnJoinSessionComplete);OnDestroySessionCompleteDelegate = FOnDestroySessionCompleteDelegate::CreateUObject(this, &URPCInstance::OnDestroySessionComplete);
}void URPCInstance::AssignPlayerController(APlayerController* InController)
{PlayerController = InController;// 获取 OnlineSub// 获取方式一:Online::GetSubsystem(GetWorld(), NAME_None),推荐方式// 获取方式二:使用 IOnlineSubsystem::Get(),直接获取可以 CreateSession 但是 JoinSession 后客户端没有跳转场景OnlineSub = Online::GetSubsystem(PlayerController->GetWorld(), NAME_None);// 获取 UserID// 获取方式一:UGameplayStatics::GetGameInstance(GetWorld())->GetLocalPlayers()[0]->GetPreferredUniqueNetId()if (GetLocalPlayers().Num() == 0) {DDH::Debug() << "No LocalPlayer Exists, Can't Get UserID" << DDH::Endl();}else {UserID = (*GetLocalPlayers()[0]->GetPreferredUniqueNetId()).AsShared();}#if 0// 获取方式二:使用 PlayerState 获取,该方式在打包成 exe 运行无问题,但是在编辑器模式下运行多个窗口,就会找不到 PlayerStateif (PlayerController->PlayerState)UserID = PlayerController->PlayerState->UniqueId.GetUniqueNetId();elseDDH::Debug() << "No PlayerState Exists, Can't Get UserID" << DDH::Endl();
#endif// 如果在这里直接获取 Session 运行时会报错,生命周期的问题
}void URPCInstance::HostSession()
{if (OnlineSub) {IOnlineSessionPtr Session = OnlineSub->GetSessionInterface();if (Session.IsValid()) {// 会话设置FOnlineSessionSettings Settings;// 连接数Settings.NumPublicConnections = 10;Settings.bShouldAdvertise = true;Settings.bAllowJoinInProgress = true;// 使用局域网Settings.bIsLANMatch = true;Settings.bUsesPresence = true;Settings.bAllowJoinViaPresence = true;// 绑定委托OnCreateSessionCompleteDelegateHandle = Session->AddOnCreateSessionCompleteDelegate_Handle(OnCreateSessionCompleteDelegate);// 创建会话Session->CreateSession(*UserID, NAME_GameSession, Settings);}}
}void URPCInstance::ClientSession()
{if (OnlineSub) {IOnlineSessionPtr Session = OnlineSub->GetSessionInterface();if (Session.IsValid()) {// 实例化搜索结果指针并且设定参数SearchObject = MakeShareable(new FOnlineSessionSearch);// 返回结果数SearchObject->MaxSearchResults = 10;// 是否是局域网,就是 IsLANSearchObject->bIsLanQuery = true;SearchObject->QuerySettings.Set(SEARCH_PRESENCE, true, EOnlineComparisonOp::Equals);// 绑定寻找会话委托OnFindSessionsCompleteDelegateHandle = Session->AddOnFindSessionsCompleteDelegate_Handle(OnFindSessionsCompleteDelegate);// 进行会话寻找Session->FindSessions(*UserID, SearchObject.ToSharedRef());}}
}void URPCInstance::DestroySession()
{if (OnlineSub) {IOnlineSessionPtr Session = OnlineSub->GetSessionInterface();if (Session.IsValid()) {// 绑定销毁会话委托OnDestroySessionCompleteDelegateHandle = Session->AddOnDestroySessionCompleteDelegate_Handle(OnDestroySessionCompleteDelegate);// 执行销毁会话Session->DestroySession(NAME_GameSession);}}
}void URPCInstance::OnCreateSessionComplete(FName SessionName, bool bWasSuccessful)
{if (OnlineSub) {IOnlineSessionPtr Session = OnlineSub->GetSessionInterface();if (Session.IsValid()) {// 解绑创建会话完成回调函数Session->ClearOnCreateSessionCompleteDelegate_Handle(OnCreateSessionCompleteDelegateHandle);// 判断创建会话是否成功if (bWasSuccessful) {DDH::Debug() << "CreateSession Succeed" << DDH::Endl();// 绑定开启会话委托OnStartSessionCompleteDelegateHandle = Session->AddOnStartSessionCompleteDelegate_Handle(OnStartSessionCompleteDelegate);// 执行开启会话Session->StartSession(NAME_GameSession);}elseDDH::Debug() << "CreateSession Failed" << DDH::Endl();}}
}void URPCInstance::OnStartOnlineGameComplete(FName SessionName, bool bWasSuccessful)
{if (OnlineSub) {IOnlineSessionPtr Session = OnlineSub->GetSessionInterface();if (Session.IsValid()) {// 注销开启会话委托绑定Session->ClearOnStartSessionCompleteDelegate_Handle(OnStartSessionCompleteDelegateHandle);if (bWasSuccessful) {DDH::Debug() << "StartSession Succeed" << DDH::Endl();// 服务端跳转场景UGameplayStatics::OpenLevel(PlayerController->GetWorld(), FName("GameMap"), true, FString("listen"));}elseDDH::Debug() << "StartSession Failed" << DDH::Endl();}}
}void URPCInstance::OnFindSessionsComplete(bool bWasSuccessful)
{if (OnlineSub) {IOnlineSessionPtr Session = OnlineSub->GetSessionInterface();if (Session.IsValid()) {// 取消寻找会话委托绑定Session->ClearOnStartSessionCompleteDelegate_Handle(OnStartSessionCompleteDelegateHandle);if (bWasSuccessful) {// 如果收集的结果存在并且大于 1if (SearchObject.IsValid() && SearchObject->SearchResults.Num() > 0) {DDH::Debug() << "Find Sessions Succeed" << DDH::Endl();// 绑定加入 Session 委托OnJoinSessionCompleteDelegateHandle = Session->AddOnJoinSessionCompleteDelegate_Handle(OnJoinSessionCompleteDelegate);// 执行加入 SessionSession->JoinSession(*UserID, NAME_GameSession, SearchObject->SearchResults[0]);}elseDDH::Debug() << "Find Sessions Succeed But Num == 0" << DDH::Endl();}elseDDH::Debug() << "Find Sessions Failed" << DDH::Endl();}}
}void URPCInstance::OnJoinSessionComplete(FName SessionName, EOnJoinSessionCompleteResult::Type Result)
{if (OnlineSub) {IOnlineSessionPtr Session = OnlineSub->GetSessionInterface();if (Session.IsValid()) {// 取消加入会话委托绑定Session->ClearOnJoinSessionCompleteDelegate_Handle(OnJoinSessionCompleteDelegateHandle);// 如果加入成功if (Result == EOnJoinSessionCompleteResult::Success) {// 传送玩家到新地图FString ConnectString;if (Session->GetResolvedConnectString(NAME_GameSession, ConnectString)) {DDH::Debug() << "Join Sessions Succeed" << DDH::Endl();// 客户端切换到服务器的关卡PlayerController->ClientTravel(ConnectString, TRAVEL_Absolute);}}else DDH::Debug() << "Join Sessions Failed" << DDH::Endl();}}
}void URPCInstance::OnDestroySessionComplete(FName SessionName, bool bWAsSuccessful)
{if (OnlineSub) {IOnlineSessionPtr Session = OnlineSub->GetSessionInterface();if (Session.IsValid()) {// 注销销毁会话委托Session->ClearOnDestroySessionCompleteDelegate_Handle(OnDestroySessionCompleteDelegateHandle);// 其他逻辑...}}
}

读者可能会发现上面的代码中,绑定委托后直接就通过 Session 执行相应逻辑了,然后在接下来的逻辑里解绑委托。这里笔者倾向于将这一过程理解为 装弹 —> 发射 —> 退弹壳。UE4 已经将网络模块的细枝末节都为我们封装好了,我们只需要知道如何使用就够了,当然,喜欢探索的读者也可以查阅源码去深入理解。

在主界面控制器里注册自己到 RPCInstance,并且将 RPCInstance 注册到主界面 UI。

MenuController.cpp

// 引入头文件
#include "Kismet/GameplayStatics.h"
#include "RPCInstance.h"void AMenuController::BeginPlay()
{// 获取 GameInstanceURPCInstance* RPCInstance = Cast<URPCInstance>(UGameplayStatics::GetGameInstance(GetWorld()));RPCInstance->AssignPlayerController(this);UClass* MenuWidgetClass = LoadClass<UMenuWidget>(NULL, TEXT("WidgetBlueprint'/Game/Blueprint/MenuWidget_BP.MenuWidget_BP_C'"));UMenuWidget* MenuWidget = CreateWidget<UMenuWidget>(GetWorld(), MenuWidgetClass);MenuWidget->AddToViewport();MenuWidget->AssignRPCInstance(RPCInstance);	// 注册 RPCInstance 到 MenuWidget
}

最后添加一些网络模块相关的依赖。

RPCCourse.Build.cs

public RPCCourse(ReadOnlyTargetRules Target) : base(Target){PCHUsage = PCHUsageMode.UseExplicitOrSharedPCHs;PublicDependencyModuleNames.AddRange(new string[] { "Core", "CoreUObject", "Engine", "InputCore", "HeadMountedDisplay", "Slate", "UMG", "HeadMountedDisplay", "OnlineSubsystem", "OnlineSubsystemUtils" });	// 添加这三个模块依赖// 添加动态加载模组DynamicallyLoadedModuleNames.AddRange(new string[] {"OnlineSubsystemNull",});}

编译后,在项目设置里将默认的 GameInstance 设置为 RPCInstance。

来到 MenuWidget_BP 的图表,重新调整两个按钮点击事件的连接节点如下:

在这里插入图片描述
此时运行玩家数应该是 3。运行后,在服务端创建服务器,创建成功;让另外两个客户端加入服务器,也能进入成功。并且在服务端按 J 键,另外两个客户端各自能看到自己角色处生成红色数字。

不过如果在客户端创建服务器,另外一个客户端可以加入,但是服务端加入会显示找到会话和加入成功,但不会跳转到 GameMap。所以必须要让服务端创建服务器才能正常运作。

至此,梁迪老师的 RPC 课程到这里就结束了,衷心感谢梁迪老师提供的优质课程 : )

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

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

相关文章

基于Java+小程序点餐系统设计与实现(源码+部署文档)

博主介绍&#xff1a; ✌至今服务客户已经1000、专注于Java技术领域、项目定制、技术答疑、开发工具、毕业项目实战 ✌ &#x1f345; 文末获取源码联系 &#x1f345; &#x1f447;&#x1f3fb; 精彩专栏 推荐订阅 &#x1f447;&#x1f3fb; 不然下次找不到 Java项目精品实…

【Linux基础】vim、常用指令、组管理和组权限

Linux基础 1、目录结构2、vi和vim3、常用指令运行级别找回密码帮助指令时间日期指令搜索查找文件目录操作磁盘管理指令压缩和解压缩 4、组管理和组权限用户操作指令权限 1、目录结构 Linux的文件系统是采用级层式的树状目录结构&#xff0c;在此结构中的最上层是根目录“/”&a…

解决IDEA搜不到插件

File -> Settings -> Plugins https://plugins.jetbrains.com/ 完成以上操作即可搜到插件

week04day02(爬虫02)

<span>: 通常用于对文本的一部分进行样式设置或脚本操作。<a>: 定义超链接&#xff0c;用于创建链接到其他页面或资源的文本。<img>: 用于插入图像。<br>: 用于插入换行。 姓名&#xff1a;<input type"text" value"lisi">…

云呐智能维运技术有哪些?智能运维活动有哪些

智能运维&#xff08;AIOps&#xff09;技术是指利用人工智能、机器学习、大数据分析等先进技术手段&#xff0c;来提高IT运维效率和质量的一系列技术和工具。目前常见的智能运维技术核心功能和应用场景。一些具体的智能运维活动案例&#xff0c;包括但不限于故障预测、自动化修…

【AI链接】 大模型语言模型网站链接

目录 GPT类1. chatgpt2. GROP3. Google AI Studio4. Moonshot AI (国内) 解读论文类&#xff1a;1. txyz 编程辅助插件&#xff1a;1. Fitten Code GPT类 1. chatgpt https://chat.openai.com/ 2. GROP https://groq.com/ 3. Google AI Studio https://aistudio.google…

Pytorch 复习总结 3

Pytorch 复习总结&#xff0c;仅供笔者使用&#xff0c;参考教材&#xff1a; 《动手学深度学习》Stanford University: Practical Machine Learning 本文主要内容为&#xff1a;Pytorch 多层感知机。 本文先介绍了多层感知机的用法&#xff0c;再就训练过程中经常出现的过拟…

HarmonyOS4.0系统性深入开发36 媒体查询(mediaquery)

媒体查询&#xff08;mediaquery&#xff09; 概述 媒体查询作为响应式设计的核心&#xff0c;在移动设备上应用十分广泛。媒体查询可根据不同设备类型或同设备不同状态修改应用的样式。媒体查询常用于下面两种场景&#xff1a; 针对设备和应用的属性信息&#xff08;比如显…

【关于深度学习的一些资料】

曾梦想执剑走天涯&#xff0c;我是程序猿【AK】 目录 动手学深度学习Awesome Deep LearningTensorFlow Official ModelsPyTorch Image ModelsDeep Reinforcement LearningNeural Style Transfer 动手学深度学习 动手学深度学习 https://zh.d2l.ai/chapter_installation/index.…

【CKA 学员预约考试流程】

CKA 学员预约考试流程 一、获取考试码、 我们需要去 Linux 基金会中文版网页注册账号并根据优惠码获取考试码&#xff0c;步骤如下&#xff1a; 1.复制链接去浏览器打开首页&#xff1a;https://training.linuxfoundation.cn/ 2.接下来点击 注册 3.根据要求 填写注册信息 …

一文了解大数据生态

大数据一词最早指的是传统数据处理应用软件无法处理的过于庞大或过于复杂的数据集。 现在&#xff0c;对“大数据”一词的使用倾向于使用预测分析、用户行为分析或者其他一些从大数据中提取价值的高级数据分析方法&#xff0c;很少用于表示特定规模的数据集。 定义 大数据是…

Spring Security学习(六)——配置多个Provider(存在两种认证规则)

前言 《Spring Security学习&#xff08;五&#xff09;——账号密码的存取》一文已经能满足一般应用的情况。但实际商业应用也会存在如下的情况&#xff1a;用户提交的账号密码&#xff0c;能在本地的保存的账号密码匹配上&#xff0c;或者能在远端服务认证中匹配上&#xff…