韩版传奇 2 源码分析与 Unity 重制(二)客户端启动与交互流程
专题介绍
该专题将会分析 LOMCN 基于韩版传奇 2,使用 .NET 重写的传奇源码(服务端 + 客户端),分析数据交互、状态管理和客户端渲染等技术,此外笔者还会分享将客户端部分移植到 Unity 和服务端用现代编程语言重写的全过程。
概览
在这一篇文章中,我们将从客户端入手,分析从 TCP 连接建立、登录鉴权、角色选择、开始游戏到游戏内交互的全过程。
客户端启动
WinForm 入口 Program.cs
与服务端类似,客户端也是一个 WinForm 应用程序,在 Application 启动后,会先跳转到 AMain 检查是否有热更新,随后再跳转到 CMain 开启客户端主逻辑:
// Program.cs
[STAThread]
private static void Main(string[] args)
{// ...Application.EnableVisualStyles();Application.SetCompatibleTextRenderingDefault(false);if (Settings.P_Patcher) Application.Run(PForm = new Launcher.AMain());else Application.Run(Form = new CMain());// ...
}
监听事件循环
在 CMain 的构造函数中,我们监听了 Application Idle 事件作为事件循环:
// CMain.cs
public CMain()
{InitializeComponent();Application.Idle += Application_Idle;// ...
}
在 Application_Idle 中,我们通过 UpdateTime 更新客户端全局的时间戳,通过 UpdateEnviroment 处理网络数据,通过 RenderEnvironment 处理客户端渲染:
private static void Application_Idle(object sender, EventArgs e)
{try{while (AppStillIdle){UpdateTime();UpdateEnviroment();RenderEnvironment();}}catch (Exception ex){SaveError(ex.ToString());}
}
客户端场景划分
在用户登录之前,UpdateEnviroment 发现连接实例为空不会做任何操作,因此我们先跳过这个函数来看 RenderEnvironment 的处理过程,这里实际上就是基于 Direct 3D 的客户端的渲染循环,请大家注意 MirScene.ActiveScene.Draw 这个调用,传奇通过 Scene 去区分不同的场景,例如登录页面、角色选择页面和游戏页面,每个页面都是一个独立的 Scene:
private static void RenderEnvironment()
{try{if (DXManager.DeviceLost){DXManager.AttemptReset();Thread.Sleep(1);return;}DXManager.Device.Clear(ClearFlags.Target, Color.CornflowerBlue, 0, 0);DXManager.Device.BeginScene();DXManager.Sprite.Begin(SpriteFlags.AlphaBlend);DXManager.SetSurface(DXManager.MainSurface);// Note hereif (MirScene.ActiveScene != null)MirScene.ActiveScene.Draw();DXManager.Sprite.End();DXManager.Device.EndScene();DXManager.Device.Present();}catch (Direct3D9Exception ex){DXManager.DeviceLost = true;}catch (Exception ex){SaveError(ex.ToString());DXManager.AttemptRecovery();}
}
那么当前的 ActiveScene 是在哪里设置的呢?实际上在 MirScene 初始化时它会被指定为 LoginScene:
public abstract class MirScene : MirControl
{public static MirScene ActiveScene = new LoginScene();// ...
}
因此上面的 Draw 方法其实会将登录页面绘制出来,我们这里先跳过 GUI 相关的部分,直接来看一下当用户输入完账号密码后是如何建立连接和发起登录的。
TCP 连接建立
传奇中的每个 Scene 都是继承自 MirControl 的 UI 对象,MirControl 提供了 Shown 回调用于监听 UI 的展示,在 LoginScene 展示时我们会开启 TCP 连接:
public LoginScene()
{// ...Shown += (sender, args) =>{Network.Connect();_connectBox.Show();};
}
Network 是客户端的网络管理类,在 Connect 方法中我们会创建一个 TcpClient 对象并发起连接,服务端的信息通过配置获取:
public static void Connect()
{if (_client != null)Disconnect();ConnectAttempt++;_client = new TcpClient {NoDelay = true};_client.BeginConnect(Settings.IPAddress, Settings.Port, Connection, null);
}
与服务端的处理方式类似,在 BeginConnect 的异步回调中,我们会开启 receiveList 和 sendList 两个队列,然后通过 BeginReceive 接收服务端数据、处理成 Packet 并加入 receiveList 等待处理。在客户端每帧 Process 的过程中,我们会处理 receiveList 更改客户端状态,同时根据用户输入产生数据包加入到 sendList 发送到服务端。
第一个数据包
服务端发送 S.Connected
通过上面的分析我们知道客户端启动的第一步是发起 TCP 连接请求,服务端在对 Client 进行 Accept 时会创建 MirConnection 对象(如果对此没有印象可以参考第一篇文章),在 MirConnection 的构造方法中我们会向客户端发送 Connected 数据包,这便是客户端与服务端交流的第一个数据包啦:
public MirConnection(int sessionID, TcpClient client)
{// ..._receiveList = new ConcurrentQueue<Packet>();_sendList = new ConcurrentQueue<Packet>();_sendList.Enqueue(new S.Connected());_retryList = new Queue<Packet>();Connected = true;BeginReceive();
}
客户端处理 S.Connected
前面我们提到在 TCP 连接建立之前基于 Application Idle 的事件循环对 UpdateEnviroment 的调用会被忽略,而在连接建立之后这里会通过 Network.Process 处理服务端数据包和发送这一帧产生的数据包,数据包会被路由到 ActiveScene 进行处理,因此这里的 ProcessPacket 会调用到 LoginScene:
public static void Process()
{// ...while (_receiveList != null && !_receiveList.IsEmpty){if (!_receiveList.TryDequeue(out Packet p) || p == null) continue;MirScene.ActiveScene.ProcessPacket(p);}if (CMain.Time > TimeOutTime && _sendList != null && _sendList.IsEmpty)_sendList.Enqueue(new C.KeepAlive());if (_sendList == null || _sendList.IsEmpty) return;TimeOutTime = CMain.Time + Settings.TimeOut; // 5000msList<byte> data = new List<byte>();while (!_sendList.IsEmpty){if (!_sendList.TryDequeue(out Packet p)) continue;data.AddRange(p.GetPacketBytes());}CMain.BytesSent += data.Count;BeginSend(data);
}
在 LoginScene 的 ProcessPacket 中包含了对客户端初始化和账户相关的数据处理,由于当前数据包是 S.Connected 自然会进入到 ServerPacketIds.Connected 这个 case,随后客户端通过 SendVersion 发送数据完整性检查请求(这里会对 Executable 进行 hash):
public override void ProcessPacket(Packet p)
{switch (p.Index){case (short)ServerPacketIds.Connected:Network.Connected = true;SendVersion();break;case (short)ServerPacketIds.ClientVersion:ClientVersion((S.ClientVersion) p);break;// ...default:base.ProcessPacket(p);break;}
}
数据完整性检查与 Connected 数据包类似,首先客户端发送 hash 到服务端,服务端校验后将结果返回到客户端,这是一个初级的逆向对抗策略,可通过修改发送的 hash 或忽略返回的错误跳过。
客户端登录过程
在上述检查通过以后,客户端会展示账号密码输入页面,用户输入账号密码后点击登录会调用 Login 方法发起登录请求:
// LoginScene.cs
private void Login()
{OKButton.Enabled = false;Network.Enqueue(new C.Login {AccountID = AccountIDTextBox.Text, Password = PasswordTextBox.Text});
}
作为一款早期的游戏,传奇的密码采用了明文传输(囧),服务端收到 C.Login 数据包后,会尝试从 Account Database 中查询与之匹配的账户,如果校验失败会发送 S.Login 返回登录失败的原因,成功则发送 S.LoginSuccess:
// Envir.cs
public void Login(ClientPackets.Login p, MirConnection c)
{// ...if (!AccountIDReg.IsMatch(p.AccountID)){c.Enqueue(new ServerPackets.Login { Result = 1 });return;}if (!PasswordReg.IsMatch(p.Password)){c.Enqueue(new ServerPackets.Login { Result = 2 });return;}var account = GetAccount(p.AccountID);if (account == null){c.Enqueue(new ServerPackets.Login { Result = 3 });return;}// ...if (string.CompareOrdinal(account.Password, p.Password) != 0){if (account.WrongPasswordCount++ >= 5){account.Banned = true;account.BanReason = "Too many Wrong Login Attempts.";account.ExpiryDate = DateTime.Now.AddMinutes(2);c.Enqueue(new ServerPackets.LoginBanned{Reason = account.BanReason,ExpiryDate = account.ExpiryDate});return;}c.Enqueue(new ServerPackets.Login { Result = 4 });return;}account.WrongPasswordCount = 0;lock (AccountLock){account.Connection?.SendDisconnect(1);account.Connection = c;}c.Account = account;c.Stage = GameStage.Select;account.LastDate = Now;account.LastIP = c.IPAddress;MessageQueue.Enqueue(account.Connection.SessionID + ", " + account.Connection.IPAddress + ", User logged in.");c.Enqueue(new ServerPackets.LoginSuccess { Characters = account.GetSelectInfo() });
}
相应地,在客户端侧也包含了对 Login 和 LoginSuccess 的处理:
// LoginScene.cs
public override void ProcessPacket(Packet p)
{switch (p.Index){// ...case (short)ServerPacketIds.Login:Login((S.Login) p);break;case (short)ServerPacketIds.LoginSuccess:Login((S.LoginSuccess) p);break;default:base.ProcessPacket(p);break;}
}
在登录失败时会调用到 private void Login(S.Login p) 这个重载方法展示登录失败原因(事实上出于安全考虑,登录失败的原因应当尽可能模糊):
// LoginScene.cs
private void Login(S.Login p)
{_login.OKButton.Enabled = true;switch (p.Result){case 0:MirMessageBox.Show("Logging in is currently disabled.");_login.Clear();break;case 1:MirMessageBox.Show("Your AccountID is not acceptable.");_login.AccountIDTextBox.SetFocus();break;case 2:MirMessageBox.Show("Your Password is not acceptable.");_login.PasswordTextBox.SetFocus();break;case 3:MirMessageBox.Show(GameLanguage.NoAccountID);_login.PasswordTextBox.SetFocus();break;case 4:MirMessageBox.Show(GameLanguage.IncorrectPasswordAccountID);_login.PasswordTextBox.Text = string.Empty;_login.PasswordTextBox.SetFocus();break;}
}
在登录成功时会调用到 private void Login(S.LoginSuccess p) 这个重载方法切换到角色选择 Scene 等待用户的下一步操作,为了避免额外的数据交互,服务端在登录成功后会返回角色列表:
// LoginScene.cs
private void Login(S.LoginSuccess p)
{Enabled = false;_login.Dispose();if(_ViewKey != null && !_ViewKey.IsDisposed) _ViewKey.Dispose();SoundManager.PlaySound(SoundList.LoginEffect);_background.Animated = true;_background.AfterAnimation += (o, e) =>{Dispose();ActiveScene = new SelectScene(p.Characters);};
}
开始游戏
服务端同步角色数据
在用户选择完角色点击开始游戏后,客户端会发送包含角色选择信息的 C.StartGame 数据包到服务端:
// SelectScene.cs
public void StartGame()
{// ...Network.Enqueue(new C.StartGame{CharacterIndex = Characters[_selected].Index});
}
服务端在接收到 C.StartGame 后会读从数据库读取角色数据,随后新建一个 PlayerObject 调用 StartGame 方法:
// MirConnection.cs
private void StartGame(C.StartGame p)
{// ...CharacterInfo info = null;for (int i = 0; i < Account.Characters.Count; i++){if (Account.Characters[i].Index != p.CharacterIndex) continue;info = Account.Characters[i];break;}if (info == null){Enqueue(new S.StartGame { Result = 2 });return;}// ...Player = new PlayerObject(info, this);Player.StartGame();
}
在 PlayerObject 的 StartGame 方法中,服务端将角色添加到地图中,随后发送游戏开始和玩家数据到客户端:
// PlayerObject.cs
public void StartGame()
{Map temp = Envir.GetMap(CurrentMapIndex);if (temp != null && temp.Info.NoReconnect){Map temp1 = Envir.GetMapByNameAndInstance(temp.Info.NoReconnectMap);if (temp1 != null){temp = temp1;CurrentLocation = GetRandomPoint(40, 0, temp);}}if (temp == null || !temp.ValidPoint(CurrentLocation)){temp = Envir.GetMap(BindMapIndex);if (temp == null || !temp.ValidPoint(BindLocation)){SetBind();temp = Envir.GetMap(BindMapIndex);if (temp == null || !temp.ValidPoint(BindLocation)){StartGameFailed();return;}}CurrentMapIndex = BindMapIndex;CurrentLocation = BindLocation;}temp.AddObject(this);CurrentMap = temp;Envir.Players.Add(this);StartGameSuccess();//Call Login NPCCallDefaultNPC(DefaultNPCType.Login);//Call Daily NPCif (Info.NewDay){CallDefaultNPC(DefaultNPCType.Daily);}
}
随后在 StartGameSuccess 的调用中向客户端发送游戏开发和角色数据,这里的每个 Get 方法的作用都是将地图和角色数据同步到客户端:
// PlayerObject.cs
private void StartGameSuccess()
{Connection.Stage = GameStage.Game;// ...Enqueue(new S.StartGame { Result = 4, Resolution = Settings.AllowedResolution });ReceiveChat(string.Format(GameLanguage.Welcome, GameLanguage.GameName), ChatType.Hint);// ...Spawned();SetLevelEffects();GetItemInfo();GetMapInfo();GetUserInfo();GetQuestInfo();GetRecipeInfo();GetCompletedQuests();GetMail();GetFriends();GetRelationship();if ((Info.Mentor != 0) && (Info.MentorDate.AddDays(Settings.MentorLength) < DateTime.Now))MentorBreak();elseGetMentor();CheckConquest();GetGameShop();// ...
}private void GetUserInfo()
{string guildname = MyGuild != null ? MyGuild.Name : "";string guildrank = MyGuild != null ? MyGuildRank.Name : "";S.UserInformation packet = new S.UserInformation{ObjectID = ObjectID,RealId = (uint)Info.Index,Name = Name,GuildName = guildname,GuildRank = guildrank,NameColour = GetNameColour(this),Class = Class,Gender = Gender,Level = Level,Location = CurrentLocation,Direction = Direction,Hair = Hair,HP = HP,MP = MP,Experience = Experience,MaxExperience = MaxExperience,LevelEffects = LevelEffects,Inventory = new UserItem[Info.Inventory.Length],Equipment = new UserItem[Info.Equipment.Length],QuestInventory = new UserItem[Info.QuestInventory.Length],Gold = Account.Gold,Credit = Account.Credit,HasExpandedStorage = Account.ExpandedStorageExpiryDate > Envir.Now ? true : false,ExpandedStorageExpiryTime = Account.ExpandedStorageExpiryDate};Info.Inventory.CopyTo(packet.Inventory, 0);Info.Equipment.CopyTo(packet.Equipment, 0);Info.QuestInventory.CopyTo(packet.QuestInventory, 0);//IntelligentCreaturefor (int i = 0; i < Info.IntelligentCreatures.Count; i++)packet.IntelligentCreatures.Add(Info.IntelligentCreatures[i].CreateClientIntelligentCreature());packet.SummonedCreatureType = SummonedCreatureType;packet.CreatureSummoned = CreatureSummoned;Enqueue(packet);
}
客户端开始游戏
客户端目前处于 SelectScene,在收到游戏启动成功的数据包 S.StartGame 后会根据返回数据调整分辨率并切换到 GameScene:
public void StartGame(S.StartGame p)
{StartGameButton.Enabled = true;switch (p.Result){case 0:MirMessageBox.Show("Starting the game is currently disabled.");break;case 1:MirMessageBox.Show("You are not logged in.");break;case 2:MirMessageBox.Show("Your character could not be found.");break;case 3:MirMessageBox.Show("No active map and/or start point found.");break;case 4:if (p.Resolution < Settings.Resolution || Settings.Resolution == 0) Settings.Resolution = p.Resolution;switch (Settings.Resolution){default:case 1024:Settings.Resolution = 1024;CMain.SetResolution(1024, 768);break;case 1280:CMain.SetResolution(1280, 800);break;case 1366:CMain.SetResolution(1366, 768);break;case 1920:CMain.SetResolution(1920, 1080);break;}ActiveScene = new GameScene();Dispose();break;}
}
在 GameScene 中客户端会处理来自服务端的角色信息、地图数据以及 NPC 和其他玩家数据等,例如在收到游戏开始时服务端发送的 S.UserInformation 后会创建当前玩家的角色:
// GameScene.cs
public override void ProcessPacket(Packet p)
{switch (p.Index){// ...case (short)ServerPacketIds.UserInformation:UserInformation((S.UserInformation)p);break;// ...}
}private void UserInformation(S.UserInformation p)
{User = new UserObject(p.ObjectID);User.Load(p);MainDialog.PModeLabel.Visible = User.Class == MirClass.Wizard || User.Class == MirClass.Taoist;Gold = p.Gold;Credit = p.Credit;InventoryDialog.RefreshInventory();foreach (SkillBarDialog Bar in SkillBarDialogs)Bar.Update();
}
下一步
到这里整个客户端的启动流程就分析完了,接下来的逻辑主要集中在服务端向客户端同步状态和客户端发送角色行为,在接下来的文章中我们将深入分析这些交互的处理过程。
相关文章:
韩版传奇 2 源码分析与 Unity 重制(二)客户端启动与交互流程
专题介绍 该专题将会分析 LOMCN 基于韩版传奇 2,使用 .NET 重写的传奇源码(服务端 客户端),分析数据交互、状态管理和客户端渲染等技术,此外笔者还会分享将客户端部分移植到 Unity 和服务端用现代编程语言重写的全过…...
JVM面试——运行时数据区
一:JVM的运行时内存区域是怎样的? 根据Java虚拟机规范的定义,JVM的运行时内存区域主要由程序计数器、虚拟机栈、本地方法 栈、Java堆、方法区和以及运行时常量池组成。其中堆、方法区以及运行时常量池是线程之间共享的区域,而栈(…...
ssh工具 向指定的ssh服务器配置公钥
此文分享一个python脚本,用于向指定的ssh服务器配置公钥,以达到免密登录ssh服务器的目的。 效果演示 🔥完整演示效果 👇第一步,显然,我们需要选择功能 👇第二步,确认 or 选择ssh服务器 👇第三步,输入ssh登录密码,以完成公钥配置 👇验证,我们通过ssh登录…...
uni-app pages.json之globalStyle全局页面样式配置
锋哥原创的uni-app视频教程: 2023版uniapp从入门到上天视频教程(Java后端无废话版),火爆更新中..._哔哩哔哩_bilibili2023版uniapp从入门到上天视频教程(Java后端无废话版),火爆更新中...共计23条视频,包括:第1讲 uni…...
Blazor 混合开发_MAUI+Vue_WPF+Vue
Blazor 混合开发_MAUIVue_WPFVue 背景混合开发的核心为什么必须使用 wwwroot 文件夹放置 Web 项目文件 创建 MAUI 项目创建 wwwroot 文件夹服务注册创建 _import.razor添加 Main.razor 组件修改 MainPage.xaml 文件 创建 WPF 项目创建 wwwroot 文件夹服务注册创建 _import.razo…...
udp异步方式接收消息
C#实现 //定义结构体 public struct UdpState { public UdpClient u; public IPEndPoint e; } private UdpClient _client; //_client的初始化请参考其他资料 IPEndPoint remoteEP null; //TODO //public static bool mess…...
【RocketMQ笔记01】安装RocketMQ消息队列运行环境
这篇文章,主要介绍如何安装RocketMQ消息队列运行环境。 目录 一、RocketMQ消息队列 1.1、下载RocketMQ 1.2、解压安装包 1.3、配置RocketMQ环境变量 1.4、修改启动脚本 1.5、启动RocketMQ (1)启动NameServer (2࿰…...
使用 Privoxy 实现对多域名的定向转发
需求与思路 内网一台主机想要访问公网的两个不同站点, 想要实现访问两个站点时表现出不同的公网 IP 地址. 即在公网的站点服务器端看到的客户端 IP 是不同的. 思路是搭建两台具有不同公网 IP 的服务器, 分别安装配置 Privoxy 后进行串联, 并将其中一台作为主服务器暴露给内网…...
《PySpark大数据分析实战》-19.NumPy介绍ndarray介绍
📋 博主简介 💖 作者简介:大家好,我是wux_labs。😜 热衷于各种主流技术,热爱数据科学、机器学习、云计算、人工智能。 通过了TiDB数据库专员(PCTA)、TiDB数据库专家(PCTP…...
图解LRU缓存
图解LRU缓存 OJ链接 介绍 LRU 缓存机制可以通过哈希表辅以双向链表实现,我们用一个哈希表和一个双向链表维护所有在缓存中的键值对。 双向链表按照被使用的顺序存储了这些键值对,靠近尾部的键值对是最近使用的,而靠近头部的键值对是最久未…...
FFmpeg常见命令行
1、ffmpeg命令行 视频生成图片 ffmpeg -i test.mp4 -r 25 -f image2 data/image%3d.jpg这个命令行使用FFmpeg工具将视频文件(test.mp4)转换为一系列图像文件。 让我们逐个解释每个参数的含义: -i test.mp4: 指定输入文件为test.mp4。-i是F…...
智能优化算法应用:基于斑马算法3D无线传感器网络(WSN)覆盖优化 - 附代码
智能优化算法应用:基于斑马算法3D无线传感器网络(WSN)覆盖优化 - 附代码 文章目录 智能优化算法应用:基于斑马算法3D无线传感器网络(WSN)覆盖优化 - 附代码1.无线传感网络节点模型2.覆盖数学模型及分析3.斑马算法4.实验参数设定5.算法结果6.参考文献7.MA…...
《C++避坑神器·二十五》简单搞懂json文件的读写之遍历json文件读写
json.hpp库放在文章末尾 1、遍历json文件读写 (1)插入新键值对到json之情形1 原来json文件如下所示: {"Connection": {"IpAddress": "192.168.20.1","Rock": 0,"Solt": 1}, "Data…...
使用 fixture 机制重构 appium_helloworld
一、前置说明 在 pytest 基础讲解 章节,介绍了 pytest 的特性和基本用法,现在我们可以使用 pytest 的一些机制,来重构 appium_helloworld 。 appium_helloworld 链接: 编写第一个APP自动化脚本 appium_helloworld ,将脚本跑起来 代码目录结构: pytest.ini 设置: [pyt…...
基于python的excel检查和读写软件
软件版本:python3.6 窗口和界面gui代码: class mygui:def _init_(self):passdef run(self):root Tkinter.Tk()root.title(ExcelRun)max_w, max_h root.maxsize()root.geometry(f500x500{int((max_w - 500) / 2)}{int((max_h - 300) / 2)}) # 居中显示…...
Podman配置mongodb
文章目录 查询镜像拉取镜像查看镜像运行容器创建root用户 查询镜像 podman search mongo拉取镜像 podman pull docker.io/library/mongo查看镜像 podman images运行容器 podman run -d -p 27017:27017 --namemongodb-test docker.io/library/mongo创建root用户 podman exe…...
java实现矩阵谱峰搜索算法
矩阵谱峰搜索算法,也称为矩阵谱峰查找算法,是一种用于搜索二维矩阵中谱峰的方法。谱峰是指在矩阵中的一个元素,它比其上下左右四个相邻元素都大或相等。 该算法的基本思想是从矩阵的中间列开始,找到该列中的最大元素,…...
Jenkins的特殊操作定时自动执行任务以及测试报告调优
java -Dhudson.model.DirectoryBrowserSupport.CSP -jar Jenkins.war 测试报告 不美丽 执行上面的代码 重启jenkins 就好了...
【Grafana】Grafana匿名访问以及与LDAP连接
上一篇文章利用Docker快速部署了Grafana用来展示Zabbix得监控数据,但还需要给用户去创建账号允许他们登录后才能看展示得数据,那有什么办法让非管理员更方便得去访问Grafana呢?下面介绍两个比较方便实现的: 在开始设置前ÿ…...
elasticsearch-py 8.x的一些优势
早在 2022 年 2 月,当 Elasticsearch 8.0 发布时,Python 客户端也发布了 8.0 版本。它是对 7.x 客户端的部分重写,并带有许多不错的功能(如下所述),但也带有弃用警告和重大更改。今天,客户端的 7.17 版本仍然相对流行,每月下载量超过 100 万次,占 8.x 下载量的 ~50…...
R语言AI模型部署方案:精准离线运行详解
R语言AI模型部署方案:精准离线运行详解 一、项目概述 本文将构建一个完整的R语言AI部署解决方案,实现鸢尾花分类模型的训练、保存、离线部署和预测功能。核心特点: 100%离线运行能力自包含环境依赖生产级错误处理跨平台兼容性模型版本管理# 文件结构说明 Iris_AI_Deployme…...
MVC 数据库
MVC 数据库 引言 在软件开发领域,Model-View-Controller(MVC)是一种流行的软件架构模式,它将应用程序分为三个核心组件:模型(Model)、视图(View)和控制器(Controller)。这种模式有助于提高代码的可维护性和可扩展性。本文将深入探讨MVC架构与数据库之间的关系,以…...
A2A JS SDK 完整教程:快速入门指南
目录 什么是 A2A JS SDK?A2A JS 安装与设置A2A JS 核心概念创建你的第一个 A2A JS 代理A2A JS 服务端开发A2A JS 客户端使用A2A JS 高级特性A2A JS 最佳实践A2A JS 故障排除 什么是 A2A JS SDK? A2A JS SDK 是一个专为 JavaScript/TypeScript 开发者设计的强大库ÿ…...
AI+无人机如何守护濒危物种?YOLOv8实现95%精准识别
【导读】 野生动物监测在理解和保护生态系统中发挥着至关重要的作用。然而,传统的野生动物观察方法往往耗时耗力、成本高昂且范围有限。无人机的出现为野生动物监测提供了有前景的替代方案,能够实现大范围覆盖并远程采集数据。尽管具备这些优势…...
嵌入式常见 CPU 架构
架构类型架构厂商芯片厂商典型芯片特点与应用场景PICRISC (8/16 位)MicrochipMicrochipPIC16F877A、PIC18F4550简化指令集,单周期执行;低功耗、CIP 独立外设;用于家电、小电机控制、安防面板等嵌入式场景8051CISC (8 位)Intel(原始…...
MySQL的pymysql操作
本章是MySQL的最后一章,MySQL到此完结,下一站Hadoop!!! 这章很简单,完整代码在最后,详细讲解之前python课程里面也有,感兴趣的可以往前找一下 一、查询操作 我们需要打开pycharm …...
jdbc查询mysql数据库时,出现id顺序错误的情况
我在repository中的查询语句如下所示,即传入一个List<intager>的数据,返回这些id的问题列表。但是由于数据库查询时ID列表的顺序与预期不一致,会导致返回的id是从小到大排列的,但我不希望这样。 Query("SELECT NEW com…...
React父子组件通信:Props怎么用?如何从父组件向子组件传递数据?
系列回顾: 在上一篇《React核心概念:State是什么?》中,我们学习了如何使用useState让一个组件拥有自己的内部数据(State),并通过一个计数器案例,实现了组件的自我更新。这很棒&#…...
简单介绍C++中 string与wstring
在C中,string和wstring是两种用于处理不同字符编码的字符串类型,分别基于char和wchar_t字符类型。以下是它们的详细说明和对比: 1. 基础定义 string 类型:std::string 字符类型:char(通常为8位)…...
职坐标物联网全栈开发全流程解析
物联网全栈开发涵盖从物理设备到上层应用的完整技术链路,其核心流程可归纳为四大模块:感知层数据采集、网络层协议交互、平台层资源管理及应用层功能实现。每个模块的技术选型与实现方式直接影响系统性能与扩展性,例如传感器选型需平衡精度与…...
