C#Like是Unity的热更方案,使用纯C#语言写出可以热更新的代码,就像可以在所有平台使用DLL(动态链接库)文件一样.遵从KISS设计原则,让用户轻松构建或升级成Unity的热更新项目.
简介
本篇主要介绍KissServerFramework:这是一个最简洁易用的IOCP服务器框架,包含WebSocket/Socket/HTTP/MySQL,基于'Keep It Simple,Stupid'设计原则.用户逻辑单线程, 后台数据库多线程,面向对象,操作极简,包含WebSocket/Socket/HTTP/MySQL,你不会用到SQL的,只需定义数据库表结构, 即可使用数据且自动和客户端和数据库三者间同步数据.
本章是可选的,如果你只关注客户端,可以跳过本章.
支持WebSocket/Socket同时连接, 服务器无需理会客户端以何种方式连接
即把客户端的连接当作是黑箱操作,反正有个接口直接就是接收客户端传过来的JSONData对象,发给客户端的也是发一个JSONData对象过去.
/// <summary>
/// 这个类是用于客户端和服务器之间传输JSON对象, 无论客户端使用WebSocket还是Socket.
/// 1 通过'void OnMessage(JSONData jsonData)'接收客户端发来的JSON对象.
/// 2 通过'void Send(JSONData jsonData)'发送JSON对象.
/// 3 玩家对应的主对象为account, 它包含所有与玩家相关的数据库对象.
/// </summary>
public sealed class Player : PlayerBase
{
/// <summary>
/// 已加载的玩家数据,它包含所有与玩家相关的数据库对象
/// </summary>
public Account account;
/// <summary>
/// 接收客户端发来的JSON对象, 本函数在主线程中运行.
/// </summary>
/// <param name="jsonData">客户端发来的JSON对象</param>
public override void OnMessage(JSONData jsonData)
{
}
/// <summary>
/// 玩家断线事件, 本函数在主线程中运行.
/// </summary>
public override void OnDisconnect()
{
Logger.LogInfo("Player:OnDisconnect");
}
/// <summary>
/// 玩家连接事件, 本函数在主线程中运行.
/// </summary>
public override void OnConnect()
{
Logger.LogInfo("Player:OnConnect");
}
/// <summary>
/// 玩家连接发生错误事件, 本函数在主线程中运行.
/// </summary>
public override void OnError(string msg)
{
Logger.LogInfo("Player:OnError:"+ msg);
}
}
极简HTTP
-
定义网络函数示范
using CSharpLike;
using KissFramework;
using System;
namespace KissServerFramework
{
/// <summary>
/// 我们示范处理HTTP请求
/// 你可以在任何代码处添加网络代码
/// 1: 定义函数为静态
/// 2: 定义函数返回值为'string'.
/// 3: 添加[WebMethod]到函数.
/// 4: (可选)自定义Uri,例如'[WebMethod(UriName = "ReqGateway")]',如果不设置,则直接使用当前函数名字,不区分大小写
/// 5: (可选)设置cookie作为输入参数,例如'[WebMethod(cookieAsParam = true)]',默认true, cookie输入作为参数,优先参数
/// 6: 你可设置0~N个参数. 参数类型支持'byte/sbyte/short/ushort/int/uint/DateTime/string/float/double/bool/IHttpContext'.
/// 7: (可选)'string ip'为自动替换为客户端ip.
/// 8: (可选)'Action<string> delayCallback'用来异步返回数据.
/// 9: (可选)参数类型为IHttpContext的为当前网络内容,你可以通过它来获取HttpRequest/HttpRespone.
/// </summary>
public static class HttpManager
{
/// <summary>
/// 示范立即返回的网络函数
/// </summary>
/// <param name="uid">客户端传来的参数uid</param>
/// <param name="token">客户端传来的参数token</param>
/// <param name="sign">客户端传来的参数sign</param>
[WebMethod]
static string TestThirdPartyAccount(int uid, string token, string sign)
{
//例如 GET : url = 'http://ip[:port]/TestThirdPartyAccount?uid=123456789&token=xxxxxx&sign=yyyyyy'
//例如 POST : url = 'http://ip[:port]/TestThirdPartyAccount' post = 'uid=123456789&token=xxxxxx&sign=yyyyyy'
//例如 POST : url = 'http://ip[:port]/TestThirdPartyAccount' post = '{"uid":"123456789","token":"xxxxxx","sign":"yyyyyy"}'
JSONData jsonReturn = JSONData.NewDictionary();
//sign = md5(uid+token+key)
string signCalc = Framework.GetMD5(uid + token + "zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz");
if (signCalc != sign)
{
jsonReturn["state"] = 1;
jsonReturn["msg"] = "验签失败";
}
else
jsonReturn["state"] = 0;
//我们简单验签后直接返回
return jsonReturn.ToJson();//如果成功则返回{"state":0}, 否则返回{"state":1,"msg":"验签失败"}
}
/// <summary>
/// 示范延迟异步返回数据, 本函数先返回"", 然后最终调用'Action<string> delayCallback'来返回实际的数据.
/// 例如 GET : url = 'http://ip[:port]/TestDelayCallback
/// </summary>
[WebMethod]
static string TestDelayCallback(string ip, Action<string> delayCallback)
{
Logger.LogInfo($"TestDelayCallback:client ip = {ip}");
//我们异步请求一个网络请求
new ThreadPoolHttp("http://www.google.com",
(msg) =>//该网络请求在后台线程处理,回调是在主线程运行的.
{
JSONData jsonReturn = JSONData.NewDictionary();
jsonReturn["state"] = 0;
jsonReturn["msg"] = msg;
delayCallback(jsonReturn);//这里是真正返回给客户端的数据
});
return "";//先返回空白字符串,表示你将晚点再返回数据的.
}
}
}
-
下面以Demo系统为例
Demo系统,是一个以Bootstrap作为HTML5前端, KissFrameworkServer作为后端的Demo系统,包含用户登录注册和部分统计数据.
后端代码示范:完整代码详见GitHub,
namespace KissServerFramework
{
public class HttpAccountManager : Singleton<HttpAccountManager>
{
/// <summary>
/// 获取登录信息
/// </summary>
[WebMethod]
public static string HttpGetLogAccount(int uid, string token, Action<string> delayCallback);
/// <summary>
/// 获取登录统计信息
/// </summary>
[WebMethod]
public static string HttpGetLogAccountStat(int uid, string token, int day, Action<string> delayCallback);
/// <summary>
/// 获取邮件信息
/// </summary>
[WebMethod]
public static string HttpGetMail(int uid, string token, Action<string> delayCallback);
/// <summary>
/// 刷新Token令牌
/// </summary>
[WebMethod]
public static string HttpAccountToken(int uid, string token, string ip, Action<string> delayCallback);
/// <summary>
/// 登录Demo系统
/// </summary>
[WebMethod]
public static string HttpAccountLogin(string name, string password, string token, string validCode, string ip,
Action<string> delayCallback, int acctType = (int)Account.AccountType.BuildInHTTP);
/// <summary>
/// 登出Demo系统
/// </summary>
[WebMethod]
public static string HttpAccountLogout(int uid, string token, string ip, Action<string> delayCallback);
/// <summary>
/// 修改用户密码
/// </summary>
[WebMethod]
public static string HttpAccountChangePassword(int uid, string token, string passwordOld, string passwordNew,
string ip, Action<string> delayCallback);
/// <summary>
/// 注册Demo系统
/// </summary>
[WebMethod]
public static string HttpAccountRegister(string name, string password, string email, string token, string validCode,
string lang, string ip, Action<string> delayCallback, int acctType = (int)Account.AccountType.BuildInHTTP);
/// <summary>
/// 确认修改用户密码
/// </summary>
[WebMethod]
public static string HttpAccountConfirmResetPassword(string token, string passwordNew, string ip, Action<string> delayCallback);
/// <summary>
/// 请求通过邮箱重置用户密码
/// </summary>
[WebMethod]
public static string HttpAccountRequestResetPassword(string email, string token, string validCode, string lang,
string ip, Action<string> delayCallback);
/// <summary>
/// 修改用户邮箱
/// </summary>
[WebMethod]
public static string HttpAccountModifyEmail(int uid, string token, string email, string lang, string ip, Action<string> delayCallback);
/// <summary>
/// 确认用户邮箱
/// </summary>
[WebMethod]
public static string HttpAccountConfirmEmail(string token, string ip, Action<string> delayCallback);
}
}
public class ValidCode
{
/// <summary>
/// 获取由服务器生成的随机验证码图片,大小72*24.
/// 用于防止客户端过快调用服务器接口.
/// </summary>
[WebMethod]
static string GetValidCode(JSONData data, string ip, IHttpContext context, Action<string> action);
}
前端代码示范:Demo,下面是重要的html文件和对应js文件
Demo
|
|--js
| |--confirm.js //确认用户邮箱
| |--forget.js //请求通过邮箱重置用户密码
| |--index.js //主页面
| |--login.js //登录界面
| |--register.js //注册界面
| |--reset.js //重置用户密码
| |--sha1.js //sha1密码
| |--utils.js //utils工具集
|
|--confirm.html //确认用户邮箱
|--forget.html //请求通过邮箱重置用户密码
|--index.html //主页面
|--login.html //登录界面
|--register.html //注册界面
|--reset.html //重置用户密码
|--upload.html //测试上传文件到数据库和显示保存后的文件
测试上传文件到数据库和显示保存后的文件
//上传图片文件到服务器->服务器保存文件到数据库->返回在数据库里的文件名->客户端根据文件名显示图片
function onChangeUploadFile(e)
{
if (e.target.files.length === 0)
return;
var formData = new FormData();
for(var i=0; i<e.target.files.length; i++)
{
formData.append('0', e.target.files[i]);
}
//通过JQuery上传文件
$.ajax({
url: 'DBFileUpload',//内置服务器接口保存数据库
type: 'POST',
cache: false,
data: formData,
processData: false,
contentType: false,
success: function (data) {
console.log('success:'+data);//success:["DBFile/20230530215323184kf4fs48hq4.png"]
data = JSON.parse(data);//服务器返回来保存的文件名,不是数据文件
$("#images").empty();
for(var j=0; j<data.length; j++)
{
$("#images").append('<img src="'+data[j]+'" width="300" alt="..."></e.target.files.length;>');//直接通过文件名访问数据库图片
}
},
error: function (error) {
console.log(error);//上传失败
}
});
}
用户逻辑全单线程, 无需关注多线程(后台多线程处理,例如数据库,网络,日志)
基于KISS设计原则,越简单越好.
-
用户逻辑全单线程,是否会有性能问题?
不会的,我们把所有耗时的数据库读写/网络读写/日志读写都采用多线程处理了
//数据库读写
所有的数据库对象均由KissEditor.exe自动生成的代码封装完毕.
直接调用各对象的Select/Insert函数,无需调用Update语句,因为修改对象值的时候会自动保存数据库. 而且自动同步到客户端
//网络读写
Socket/WebSocket共用接口,以下收发网络接口在主线程内运行,后台传输使用IOCP模型
public sealed class Player : PlayerBase
{
public override void Player::OnMessage(JSONData jsonData);
}
public abstract class PlayerBase
{
public void Send(JSONData jsonData);
}
HTTP/HTTPS请求
string strURL = $"http://127.0.0.1:9002/TestThirdPartyAccount";
JSONData jsonPost = JSONData.NewDictionary();
jsonPost["uid"] = jsonData["name"];
jsonPost["token"] = jsonData["password"];
jsonPost["sign"] = FrameworkBase.GetMD5((string)jsonPost["uid"] + jsonPost["token"] + "zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz");
//请求一个异步HTTP的POST请求,然后在回调里处理返回的数据.
new ThreadPoolHttp(strURL, jsonPost.ToJson(),
(callback) =>//HTTP请求在后台线程运行,回调在主线程运行
{
//把返回的数据转成JSONData对象
JSONData jsonCallback = KissJson.ToJSONData(callback);
//...
});
处理HTTP请求
设置[WebMethod]来处理请求.参考上面的"定义网络函数示范"
//日志读写,不会阻塞主线程
Logger.LogInfo("主线程里调用:打印log到文件和控制台");
Logger.LogInfo("多线程里调用:打印log到文件和控制台", false);
-
如果我的用户逻辑过于耗时,如何是好?
使用ThreadPoolEvent来调起多线程,不使用Task.Run(),是因为确保能回调到主线程,尽管ThreadPoolEvent实际上也是调用Task.Run()的
//下面示范使用多线程处理耗时工作:在多线程里读取一个文件,然后返回主线程里使用读取的文件
string strFile = "./KissFramework.dll";
string result = "";
new ThreadPoolEvent(() =>//该函数在多线程里运行,可以做繁重工作
{
Logger.LogInfo($"start read file {strFile}", false);//多线程里打印log需要传入参数false.
byte[] buff = System.IO.File.ReadAllBytes(strFile);
System.Threading.Thread.Sleep(1000);//我们这里模拟工作耗时较久
result = $"{strFile} file length = {buff.Length}";
},
() =>//该函数在主线程里运行,是多线程工作完成的回调.
{
Logger.LogInfo(result);
});
面向对象设计, 客户端和服务器端传输的是JSONData对象或自定义类对象
- 数据库对象使用KissEditor来编辑,自动生成各个对象,服务器全自动加载对象和保存数据
- 自动生成的数据库对象,在客户端和服务器均只需面对这些对象即可
- 除了这些自定义数据库对象,客户端和服务器传输的指令是一个JSONData对象
无需用到SQL知识, 仅需定义数据库表结构,即可使用自动获取的数据库数据. 修改数据后, 后台会全自动异步更新至数据库和客户端
-
首先按需设计所需的类对象,下面以示范为例:
- LogManager类: 记录各个系统的有价值的事件到数据库,以备后面管理游戏,例如登录记录之类的,它只负责写入数据库
- AccountManager类: 登录系统,所有登录相关的操作在里面进行
- Account类: 记录玩家的基本信息,里面包含各个子系统,通过它来访问各个子系统
- Mail类: 邮件系统,用于示范每个玩家存在0~n个的数据的子系统,新加一条数据不会发生叠加的
- Item类: 物品系统,用于示范每个玩家存在0~n个的数据的子系统,新加一条数据可能根据相同键值会发生叠加的
- SignIn类: 签到系统,用于示范每个玩家都存在唯一一个的数据的子系统
- 对应的类图:
-
根据上面设计的类对象,分析设计出各个系统对应的数据库的数据结构:
LogManager类: 这个在KissEditor的界面暂不能可视化地修改添加, 暂时手动修改'KissEditor.json'文件里的'classes.LogManager.logs'里添加或修改或删除
//设置的JSON信息如下:
"logs": [
{
"name": "LogAccount",
"dbName": "LogAccount",
"params": [
{
"type": "int",
"name": "acctId"
},
{
"type": "int",
"name": "logType"
},
{
"type": "string",
"name": "ip"
}
],
"createTime": true,
"key": [
{
"name": "index_acctId",
"list": [
"acctId"
]
}
]
}
]
//生成的代码如下:
namespace KissServerFramework
{
public class LogManager
{
public static void LogAccount(int acctId, int logType, string ip);
}
}
//生成的SQL如下:
--
-- Table structure for table `logaccount` in LogManager
--
DROP TABLE IF EXISTS `logaccount`;
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!40101 SET character_set_client = utf8 */;
CREATE TABLE `logaccount` (
`uid` int(11) NOT NULL AUTO_INCREMENT,
`acctId` int(11) NOT NULL,
`logType` int(11) NOT NULL,
`ip` text NOT NULL,
`createTime` datetime NOT NULL,
PRIMARY KEY (`uid`),
KEY `index_acctId` (`acctId`)) ENGINE=InnoDB AUTO_INCREMENT=5 DEFAULT CHARSET=utf8mb4;
/*!40101 SET character_set_client = @saved_cs_client */;
AccountManager类: 这个类是用来处理登录流程的,用到的数据库表为Account
Account类: 记录玩家的基本属性,其他逻辑的尽量放在子系统内,保持本类代码清爽
- uid : 唯一ID
- acctType : 账号类型,区分不同渠道的账号
- name : 登录名,和账号类型组成唯一索引
- createTime : 本账号的创建时间
- password : 密码,只对内置账号有效,其他第三方的以第三方核验为准
- nickname : 玩家的昵称
- money : 游戏代币
- token : 登录令牌,在特定时间内直接验证令牌来代替验证密码
- tokenExpireTime : 登录令牌过期时间
- score : 游戏积分
- scoreTime : 游戏积分改变的时间
- lastLoginTime : 上一次登录的时间
- lastLoginIP : 上一次登录的IP
- email : 玩家的邮箱地址,用于找回密码
Mail类: 记录玩家之间或者系统发给玩家的信件信息
- uid : 唯一ID
- acctId : 玩家ID(即Account表的uid)
- senderId : 发送者ID
- senderName : 发送者昵称
- title : 邮件标题
- content : 邮件正文
- appendix : 邮件附件
- createTime : 邮件创建的时间
- wasRead : 是否已读
- received : 是否已经领取过附件了
Item类: 记录玩家获得的道具信息,注意:这里无需记录固定的物品信息,例如物品名字,物品叠加数量上限等等固定信息. 因为这些信息可以根据物品ID来读取CSV来获取
- uid : 唯一ID
- acctId : 玩家ID(即Account表的uid)
- itemId : 物品ID
- itemCount : 物品数量
SignIn类: 记录玩家每天签到信息,每次签到给予一定奖励,而且VIP还可以获得额外的奖励
- acctId : 玩家ID(即Account表的uid)
- month : 签到的月份
- signInList : 签到信息
- vipSignInList : VIP签到信息
-
按需分析完数据结构后,然后就在KissEditor里编辑各个类对象:
编辑器简介:鼠标悬浮一些组件上会提示用法,强烈建议查看一下
- KissEditor编辑器是一个使用跨平台UI库GtkSharp和.NET制作的跨平台的编辑器,目前暂时只导出Window平台的版本
- Window平台需要先安装Gtk的环境,可以跳转到GitHub下载, 在GitHub的下载地址是:gtk3-runtime-3.24.24-2021-01-30-ts-win64.exe 在官网的下载地址是:gtk3-runtime-3.24.24-2021-01-30-ts-win64.exe
- 顶部是常用操作按钮
- 左边是涉及的类
- 右上部是通用设置,例如命名空间,注解,导出放哪里
- 中部是类的修改
- 左下角是代码预览
- 右下角是SQL预览
异步加载子系统是按左方的排列顺序的,我想调整顺序怎么办?
- 左侧选中特定的类后,在右侧的上下按钮可以调整顺序. 注意,不是等加载完后再加载下一个的同步加载,而是异步地加载的
我想新增一个类,要怎么做?
- 先选中一个你想复制的类(3种不同种类的类,例如Mail类/Item类/SignIn类),点'Duplicate Class'按钮,然后按需修改.
我想删除一个类,要怎么做?
- 选中想要删除的类,点击'Delete Class'按钮,注意,这个不可撤销的哦
-
登录流程
- 客户端发起登录请求
- 登录请求到达Player类
- Player类根据封包类型转到AccountManager处理登录流程
- 如果是第三方的账号,则通过HTTP(s)向第三方验证登录
- 从内存中获取指定Account类对象,如果内存中不存在则读自数据库,数据库内不存在则自动创建新号
- 如果是自建的账号则核验密码或令牌
- 发送Account类对象到客户端
- 调用'account.LoadAllSubSystem(player)',下面的子流程是异步,不阻塞登录流程
- 异步加载邮件系统,且自动同步到客户端的,完成后会调用Account类的OnMailLoaded,供用户自定义加载完毕的后的操作
- 异步加载物品系统,且自动同步到客户端的,完成后会调用Account类的OnItemLoaded,供用户自定义加载完毕的后的操作
- 异步加载签到系统,且自动同步到客户端的,完成后会调用Account类的OnSignInLoaded,供用户自定义加载完毕的后的操作
- 当所有子系统加载完成后会调用Account类的OnAllSubSystemLoaded,供用户自定义加载完毕的后的操作
- 登录流程结束
-
我在服务器代码中要如何使用这些数据?下面以Item类为例说明
- KissEditor会导出2个服务器用的类文件: 'Item.cs'和'Item_Base.cs'
namespace KissServerFramework
{
public sealed class Item : Item_Base
{
}
}
namespace KissServerFramework
{
/// <summary>
/// 本类由KissEditor自动生成,封装和数据库的交互,请勿修改本文件
/// </summary>
public abstract class Item_Base : NetObject<Item, Account>
{
/// <summary>
/// 仅限内部调用.
/// </summary>
public override void Update(ref string _strSQL_, ref MySqlParameter[] _mySqlParameters_);
/// <summary>
/// 从数据库读取所有的物品,在后台线程读取数据库,在主线程回调.
/// </summary>
/// <param name="callback">返回主线程处理回调.</param>
public static void SelectAll(Action<List<Item>, string> callback);
/// <summary>
/// 根据acctId从数据库读取所有的物品.在后台线程读取数据库,在主线程回调.
/// </summary>
/// <param name="_callback_">返回主线程处理回调.</param>
public static void SelectByAcctId(int acctId, Action<List<Item>, string> _callback_);
/// <summary>
/// 在数据库里删除自身对象. 在后台线程删除,在主线程回调.
/// </summary>
/// <param name="_callback_">返回主线程处理回调. 默认回调为空(我们无需关注它)</param>
public void Delete(Action<int, string> _callback_ = null);
/// <summary>
/// 在数据库里删除特定acctId的数据. 在后台线程删除,在主线程回调.
/// </summary>
/// <param name="_callback_">返回主线程处理回调.</param>
public static void DeleteByAcctId(int acctId, Action<int, string> _callback_);
/// <summary>
/// 数据库里删除特定uid的数据. 在后台线程删除,在主线程回调.
/// </summary>
/// <param name="_callback_">返回主线程处理回调.</param>
public static void DeleteByUid(int uid, Action<int, string> _callback_);
/// <summary>
/// 插入一条新数据到数据库. 在后台线程插入,在主线程回调. (这个是全参数的)
/// </summary>
/// <param name="_callback_">返回主线程处理回调. 默认回调为空(我们无需关注它)</param>
public static void Insert(int itemId, int acctId, int count, Action<Item, string> _callback_ = null);
//下面是属性,修改它们会自动更新到数据库内,这里是简略的示意代码,修改它们会自动异步保存数据库的哦
public int uid;
public int itemId;
public int acctId;
public int count;
/// <summary>
/// 本对象转为JSONData对象
/// </summary>
/// <param name="mask">指定遮罩</param>
public override JSONData ToJSONData(ulong mask = 0ul);
/// <summary>
/// 从指定对象克隆数据
/// <param name="_source_">源对象</param>
/// <param name="_mask_">复制哪些数据, 默认为全部.</param>
/// </summary>
public void Clone(Item _source_, ulong _mask_ = ulong.MaxValue);
}
}
-
- 读取数据
- 读取数据库算是全自动的,你无需插手
- 你可以自定义加载完成的事件:
-
namespace KissServerFramework { public sealed class Account : Account_Base { public override void OnItemLoaded() { Logger.LogInfo("OnItemLoaded:count=" + items.Count);//这里我们只打印一下当前物品数量多少 } } }
-
- 获取所有的物品信息: 直接访问Account类的属性'public Dictionary<int, Item> items = new Dictionary<int, Item>();'
- 获取指定物品id的物品信息: 直接访问Account类的函数'public Item GetItem(int itemId)'
-
- 修改数据
根据游戏逻辑,我们修改数据只要一个接口,就是修改物品数量'public bool ChangeItem(int itemId, int count, int logType = 0)', 里面包含了插入数据库和修改数据库和删除数据库的操作了,如下代码:
namespace KissServerFramework
{
public sealed class Account : Account_Base
{
/// <summary>
/// 修改物品数量
/// </summary>
/// <param name="itemId">物品ID</param>
/// <param name="count">改变的物品数量</param>
/// <param name="logType">物品变化记录类型, 默认0</param>
/// <returns>修改操作是否成功</returns>
public bool ChangeItem(int itemId, int count, int logType = 0)
{
if (count == 0)
return false;//这个物品数量没变化,操作失败
Item item = GetItem(itemId);//根据物品ID获取物品
if (count > 0)//加物品
{
if (item != null)//物品已存在,叠加数量
{
item.count += count;//修改物品数量,注意:这里会触发后台线程自动保存数据库的哦!!!
LogManager.LogItem(uid, logType, count, item.count);//我们记录日志到数据库(这个是异步的哦)
}
else//物品不存在,新加
{
//这个是数据库插入操作哦,是后台线程异步操作的哦
Item.Insert(itemId, uid, count, (newItem, error) =>
{
//这里是后台线程的数据库插入完毕了,返回到主线程处理了
if (string.IsNullOrEmpty(error))//这个表示插入数据库报错了哦
return;
item = GetItem(itemId);//这里要重新获取一次物品
if (item != null)//这里可能变成非空了哦,因为是在异步插入的时候,另外一个物品也同样也在插入数据库,这是有概率发生的!!!
{
//我们修改当前的物品的数量,然后删除掉多余的物品
item.count += newItem.count;//修改物品数量,注意:这里会触发后台线程自动保存数据库的哦!!!
newItem.Delete();//这个是物品删除,它会自动异步进行数据库删除操作的
}
else//物品不存在,我们赋值到items
{
//把物品添加到items去,要调用SetItems这个即可哦(为何不直接'items[newItem.id] = newItem;',是因为下面这样子才会自动同步到客户端呀)
SetItems(new List<Item>() { newItem });
item = newItem;
}
LogManager.LogItem(uid, logType, count, item.count);//我们记录日志到数据库(这个是异步的哦)
});
}
}
else//移除物品
{
if (item == null || item.count < count)//物品不存在或物品数量不足,我们就返回移除物品失败
{
return false;
}
else
{
item.count -= count;//扣除物品数量,注意:这里会触发后台线程自动保存数据库的哦!!!
LogManager.LogItem(uid, logType, count, item.count);//我们记录日志到数据库(这个是异步的哦)
}
}
return true;
}
}
}
-
- 插入数据
直接调用'Account.ChangeItem(1, 2)'. 本质上是调用函数'public static void Insert(int itemId, int acctId, int count, Action<Item, string> _callback_ = null)'
-
- 更新数据
直接调用'Account.ChangeItem(1, 3)'. 本质上是直接修改属性'item.count'
-
- 删除数据
直接调用'Account.ChangeItem(1, -5)'. 本质上是调用函数'public void Delete(Action<int, string> _callback_ = null)'
-
我在客户端代码中要如何使用这些数据?下面以Item类为例说明
- KissEditor会导出2个Unity用的类文件(免费版是1个)
- KissEditor会导出2个C#Like完整用的类文件:'Item.cs'和'Item_Base.cs',以及1个C#Like免费版用类文件'Item.cs'(为何免费版的只有1个文件?是因为免费版不支持类的继承)
- 下面其中展示部分示意代码,完整代码的请看项目
- 完整版示意代码:
namespace CSharpLike { public class Item : Item_Base { #region 下面是属性发生改变的时候事件 public override void OnChanged() { //服务器里的本对象,有任意属性改变的时候,会触发调用本函数 } public override void OnUidChanged() { //服务器里的本对象,有uid属性改变的时候,会触发调用本函数 } public override void OnCountChanged() { //服务器里的本对象,有count属性改变的时候,会触发调用本函数 } public override void OnDeleted() { //服务器里的本对象被删除,会触发调用本函数 } #endregion } } namespace CSharpLike { //这个基类自动生成,千万别手动修改,否则下次生成会自动覆盖掉的哦 public class Item_Base { public int uid; public int itemId; public int acctId; public int count; public static Item ToItem(JSONData jsonData); public static List<Item> ToItems(JSONData jsonData); public override string ToString(); public void Clear(); public virtual void OnChanged() {} public virtual void OnUidChanged(){} public virtual void OnCountChanged(){} public virtual void OnDeleted(){} public void NotifyValuesChanged(); } }
- 免费版示意代码
namespace CSharpLike { //因为免费版没有类继承的功能,所以只有一个文件,有改动的时候,需要手动合并 public class Item { public int uid; public int itemId; public int acctId; public int count; public static Item ToItem(JSONData jsonData); public static List<Item> ToItems(JSONData jsonData); public override string ToString(); public void Clear(); public void OnChanged() { //服务器里的本对象,有任意属性改变的时候,会触发调用本函数 } public void OnUidChanged() { //服务器里的本对象,有uid属性改变的时候,会触发调用本函数 } public void OnCountChanged() { //服务器里的本对象,有count属性改变的时候,会触发调用本函数 } public void OnDeleted() { //服务器里的本对象被删除,会触发调用本函数 } public void NotifyValuesChanged(); } }
- 读取数据
- 客户端的数据原则上是服务器自动更新同步的,你无需插手
- 你可以自定义数据变化的事件: 例如有新增Item事件/Item内各属性变动事件/删除Item事件,你可以根据各种事件刷新你的界面
namespace CSharpLike { public class Account : Account_Base { //新增或变化的物品事件 public override void OnCallbackObjectItems(List<Item> data) { //我们这个打印每个变化的物品, 你应该刷新你的界面 foreach (Item item in data) Debug.Log("OnCallbackObjectItems:changed item:" + item.ToString()); //打印一下物品数量 Debug.Log("OnCallbackObjectItems:now all item count = " + items.Count); } //删除的物品事件 public override void OnCallbackDeleteItems(List<Item> data) { //我们这个打印每个删除的物品, 你应该刷新你的界面 foreach (Item item in data) Debug.Log("OnCallbackDeleteItems:delete item:" + item.ToString()); //打印一下物品数量 Debug.Log("OnCallbackDeleteItems:now all item count = " + items.Count); } } } namespace CSharpLike { public class Item : Item_Base { #region Event for property value changed public override void OnCountChanged() { //物品数量变化的事件,你可以删除本函数如果你不需要 } public override void OnDeleted() { //物品被删除的事件,你可以删除本函数如果你不需要. } #endregion //Event for property value changed } }
- 获取所有的物品信息: 直接访问Account类的属性'public Dictionary<int, Item> items = new Dictionary<int, Item>();'
- 获取指定物品id的物品信息: 直接访问Account类的函数'public Item GetItem(int itemId)'
- 修改数据
- 尽量不要手动修改Item对象数据,而是由服务器自动同步数据,客户端只是显示服务器端修改的数据
-
(可选知识)它是如何做到自动读取数据库,自动更新数据库,自动插入数据库,自动同步到客户端?下面以Item类为例说明
- 自动读取数据库
- 主线程调用Account类的LoadAllSubSystem函数,通知后台多线程要查询数据库了
- 后台多线程异步执行Select语句,然后把查询的结果投递到主线程
- 主线程把查询结果添加到Account类的items,通知Item数据已加载完毕了.
- 同时通知需要同步到客户端
- 自动更新数据库
- 当修改Item类里特定的数据,将会通知后台线程,本对象需要更新数据库了. 特别地,KissEditor里标为'A.I.'或'DontUpdate'的不会触发更新操作)
- 当后台线程收到更新通知后,将在JSON配置里的updateDelaySeconds的时间左右(默认300秒),才会启动更新操作.
- 为什么要等?因为我们预期你短时间内还会接着修改本类的数据的,我们等一起再写入数据库,这样可以减轻服务器的压力.当然风险还是有的,例如服务器停电或者应用程序被强制关闭会导致数据没有写入数据库.
- 后台线程执行Update语句更新数据库
- 同时通知需要同步到客户端
- 自动插入数据库
- 主线程调用'Item.Insert'通知后台线程要插入数据
- 后台多线程收到通知后,开始执行Insert语句,并且将插入结果通知主线程
- 主线程收到后台线程的结果后,添加到Account类的items.
- 同时通知需要同步到客户端
- 自动同步到客户端
- 当前面的加载/改动/新加物品后,经过KissEditor的Item类的Delay设置的毫秒后(默认100毫秒),开始执行更新操作
- 把所有的需要同步的物品,打包一起发送JSON串到客户端(仅改动部分的数据),每次最多发送N个对象(KissEditor的Limit数值)
- 客户端收到JSON数据后,触发各种对应事件.
命令行快速调试函数,在主线程里运行
namespace KissServerFramework
{
/// <summary>
/// 我们这里放命令行处理函数
/// 你可以放在任意代码处,不是一定在本类内添加
/// 非常容易添加命令行处理函数:
/// 1: 定义函数为静态.
/// 2: 函数添加[CommandMethod].
/// 3: (可选)自定义命令行名'[CommandMethod(Command = "CustomName")]', 如果不设置则直接使用函数名作为命令名,不区分大小写
/// 4: 你可以设置0~N个参数. 支持参数类型为'byte/sbyte/short/ushort/int/uint/DateTime/string/float/double/bool'
/// </summary>
public class CommandManager
{
/// <summary>
/// 命令行里输入 'testcommand aa "bb ""Cc" 1 1.5'
/// 会在主线程调用函数TestCommand, 然后传入的参数为:
/// param1 = "aa";
/// param2 = "bb \"Cc";
/// param3 = 1;
/// param4 = 1.5f;
/// </summary>
/// <param name="param1">命令行参数1</param>
/// <param name="param2">命令行参数2</param>
/// <param name="param3">命令行参数3, 必须类型为整形</param>
/// <param name="param4">命令行参数4, 必须类型为浮点数</param>
[CommandMethod]
public static void TestComman(string param1, string param2, int param3, float param4)
{
Logger.LogInfo($"TestComman {param1} {param2} {param3} {param4}");
}
[CommandMethod]
public static void ReloadCsv()
{
Framework.Instance.InitializeCSV();
}
[CommandMethod]
public static void Quit()
{
Framework.Running = false;
}
[CommandMethod]
public static void Exit()
{
Framework.Running = false;
}
[CommandMethod]
public static void ReloadWWW()
{
Framework.Instance.ForceCheckCacheFile();
}
[CommandMethod]
public static void TestThread()
{
//我们测试在多线程里读取一个文件,然后在主线程打印读取的内容.
string strFile = "./KissFramework.dll";
string result = "";
new ThreadPoolEvent(() =>//这个函数在多线程里运行,可以做繁重工作,例如IO读取操作
{
Logger.LogInfo($"start read file {strFile}", false);//在多线程里打印log,参数useInMainThread必须传入false
byte[] buff = System.IO.File.ReadAllBytes(strFile);
System.Threading.Thread.Sleep(1000);//我们模拟工作花费很长时间
result = $"{strFile} file length = {buff.Length}";
},
() =>//这个函数在主线程运行,回调函数
{
Logger.LogInfo(result);
});
}
}
}
使用定时事件(定时器),在主线程里运行
//通过添加[EventMethod(IntervalTime=60f,RepeatCount=123,UniqueKey="aaaaaa")]方式来添加定时器
public class ValidCode
{
//定时本函数每60秒执行1次,本函数在主线程执行
[EventMethod(IntervalTime = 60f)]
public static void UpdateCode()
{
DateTime now = DateTime.Now;
while (codeList.Count > 0)
{
ValidCode code = codeList[0];
if (code.expireTime > now)
{
codeList.RemoveAt(0);
codeDic.Remove(code.code);
}
else
break;
}
}
}
//通过过函数方式调用
namespace KissFramework
{
public abstract class FrameworkBase
{
public static string RaiseEvent(Action<float> action, float intervalTime = 0, int repeatCount = int.MaxValue);
public static string RaiseEvent(Action action, float intervalTime = 0, int repeatCount = int.MaxValue);
public static void RaiseUniqueEvent(Action<float> action, string uniqueKey, float intervalTime = 0, int repeatCount = int.MaxValue);
public static void RaiseUniqueEvent(Action action, string uniqueKey, float intervalTime = 0, int repeatCount = int.MaxValue);
}
}
//上面的[EventMethod]的方式可以等效于下面方式
Framework.RaiseEvent(ValidCode.UpdateCode, 60f);
快速搭建KissFramework服务器
-
搭建KissFrameworkServer服务器
- 到github下载整个项目到本地
- 使用VS编译成KissServerFramework.exe
- 如果目标机器没有安装.NET5运行时库(这个因为使用VS2019编译,所以选了这个,您可以根据个人情况选择)的需要去微软官网dotnet-runtime-5.0.17-win-x64.exe下载且安装它
- 复制KissServerFramework.exe和KissServerFramework.json文件到你的服务器,例如C:\MyServer目录内
- 复制测试用的CSV到你的服务器,例如C:\MyServer目录内
- 复制上面Unity导出的WebGL平台的示范(CSharpLikeDemo和CSharpLikeFreeDemo)到你的服务器,例如C:\MyServer\wwwroot目录内
- 复制Demo系统到你的服务器(public/Demo),例如C:\MyServer\wwwroot目录内
Demo系统访问链接: Home
Demo系统简介:Demo系统,前端由Bootstraps制作H5,包含登录/注册/修改密码功能,获取一些统计数据. 完整前端详见public/Demo/目录
- 登录界面: https://www.csharplike.com/Demo/login.html
验证码图片由服务器生成发给客户端的,服务器代码为Logic\ValidCode.cs - 注册界面: https://www.csharplike.com/Demo/register.html
验证邮件的,需要配置KissServerFramework.json的邮箱信息,默认"mailDontSend"为true表示不发邮件,只打印一个日志 - 主界面: Home
示范显示账号信息和一些统计信息 - 测试上传文件界面: https://www.csharplike.com/Demo/upload.html
示范上传文件保存到数据库及客户端使用数据库文件
- 登录界面: https://www.csharplike.com/Demo/login.html
- 最终服务器目录结构如下图所示(本官网的用就是如图所示):
C:\MyServer |--CSV //测试用的CSV文件 | |--Item.csv | |--TestCsv.csv | |--Log //生成的log会放里面 | |--wwwroot | |--CSharpLikeDemo //C#Like导出的Demo示范 | | |--Build | | |--StreamingAssets | | |--TemplateData | | |--index.html | | | |--CSharpLikeFreeDemo //C#Like免费版导出的Demo示范 | | |--Build | | |--StreamingAssets | | |--TemplateData | | |--index.html | | | |--Demo //HTTP的Demo系统(下面展示部分主要文件) | | |--js | | | |--confirm.js //确认用户邮箱 | | | |--forget.js //请求通过邮箱重置用户密码 | | | |--index.js //主页面 | | | |--login.js //登录界面 | | | |--register.js //注册界面 | | | |--reset.js //重置用户密码 | | | | | |--confirm.html //确认用户邮箱 | | |--forget.html //请求通过邮箱重置用户密码 | | |--index.html //主页面 | | |--login.html //登录界面 | | |--register.html //注册界面 | | |--reset.html //重置用户密码 | | |--upload.html //测试上传文件到数据库和显示保存后的文件 | | | |--index.html //C#Like官网主页(对应相关文件略) | |--KissServerFramework.exe //编译出来的exe文件(采用单个exe的方式,里面包含了所需的dll,看起来清爽点) |--KissServerFramework.json //配置文件
-
搭建MySQL
- 安装MySQL,如果您已经安装MySQL,可跳过本步骤.我们这里采用XAMPP来安装,您也可以使用其他方式安装的.
- 到XAMPP的官网下载您想要的版本,下载地址:xampp-windows-x64-8.2.4-0-VS16-installer.exe
- 一直"下一步"地安装XAMPP直至安装完毕(默认安装到C:/xampp)
- 安装完毕后,我们只需确保MySQL是开着的,其他Apache的没用就关着,对应的目录为C:\xampp\mysql
- MySQL创建数据库"kiss"且导入表
- Window平台需要先安装Gtk的环境,可以到github下载,下载地址是:gtk3-runtime-3.24.24-2021-01-30-ts-win64.exe
- 打开项目的KissServerFramework/editor/KissEditor.exe后点击"Export All SQL",保存sql文件以备下一步使用,例如"C:/SQL/kiss.sql",如修改位置文件名请在下一步也要对应修改
- 在MySQL里创建数据库"kiss",我们使用命令行"C:\xampp\mysql\bin\mysqladmin -u root -p create kiss",您可以执行批处理文件:"KissServerFramework/public/CreateDatabase.bat"
- 在MySQL里导入数据库"kiss",我们使用命令行"C:\xampp\mysql\bin\mysql kiss < C:/SQL/kiss.sql -u root -p",您可以执行批处理文件:"KissServerFramework/public/ImportDatabase.bat"
-
(可选步骤)升级HTTP到HTTPS,升级WS到WSS,更换Socket的RSA证书
- 准备SSL证书,可以到腾讯云或阿里云或"Let's Encrypt"申请免费的SSL证书或购买.我自己是腾讯云申请的免费SSL证书,下载Nginx版证书备用.
- 配置Nginx代理
- Nginx官网下载里面的稳定版本,我们这里示范选nginx-1.22.1.zip
- 下载的回来的nginx-1.22.1.zip解压在C盘中
- 复制"QuitNginx.bat" "RestartNginx.bat" "StartNginx.bat" "StopNginx.bat" 4个文件到Nginx目录下
- 配置C:\nginx-1.22.1\conf\nginx.conf. 下面的server_name修改为您的域名,ssl_certificate和ssl_certificate_key修改为您刚刚申请的SSL证书.注意端口. 该文件在public/内可查看
server { listen 443 ssl; server_name www.csharplike.com; ssl_certificate C:/nginx-1.22.1/conf/csharplike.com/csharplike.com_bundle.crt; ssl_certificate_key C:/nginx-1.22.1/conf/csharplike.com/csharplike.com.key; ssl_session_timeout 5m; ssl_session_cache shared:SSL:10m; ssl_protocols TLSv1 TLSv1.1 TLSv1.2 SSLv2 SSLv3; ssl_ciphers ALL:!ADH:!EXPORT56:RC4+RSA:+HIGH:+MEDIUM:+LOW:+SSLv2:+EXP; ssl_prefer_server_ciphers on; ssl_verify_client off; location / { proxy_pass http://127.0.0.1:9002; proxy_set_header x_forwarded_for $remote_addr; } } #HTTP => KissFrameworkServer HTTP, (您也直接可以设置KissFrameworkServer的HTTP端口为80) server { listen 80; server_name www.csharplike.com; location / { proxy_pass http://127.0.0.1:9002; proxy_set_header x_forwarded_for $remote_addr; } } #WSS => WS server { listen 10000 ssl; server_name www.csharplike.com; ssl_certificate C:/nginx-1.22.1/conf/csharplike.com/csharplike.com_bundle.crt; ssl_certificate_key C:/nginx-1.22.1/conf/csharplike.com/csharplike.com.key; ssl_session_timeout 5m; ssl_session_cache shared:SSL:10m; ssl_protocols TLSv1 TLSv1.1 TLSv1.2 SSLv2 SSLv3; ssl_ciphers ALL:!ADH:!EXPORT56:RC4+RSA:+HIGH:+MEDIUM:+LOW:+SSLv2:+EXP; ssl_prefer_server_ciphers on; ssl_verify_client off; add_header Access-Control-Allow-Origin *; location / { proxy_pass http://127.0.0.1:9000; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection "upgrade"; proxy_set_header x_forwarded_for $remote_addr; proxy_connect_timeout 1800s; proxy_send_timeout 1800s; proxy_read_timeout 1800s; } }
- 最终Nginx的文件树结构如下
C:\nginx-1.22.1 | |--conf | |--xxxx.com //Nginx版的证书 | | |--xxxx.com_bundle.crt | | |--xxxx.com.key | | | |--nginx.conf //配置文件 | |--contrib |--docs |--html |--logs |--temp |--nginx.exe | |--QuitNginx.bat //双击这个是退出Nginx |--RestartNginx.bat //双击这个是重启Nginx,例如修改配置后用 |--StartNginx.bat //双击这个是启动Nginx,例如首次打开时候用 |--StopNginx.bat //双击这个是停止Nginx
- 如果存在防火墙,需开放80/443/10000端口对外
- 最后记得执行StartNginx.bat启动Nginx,如果前面已经启动过可以执行RetartNginx.bat
- 更换Socket的RSA证书. 我们的Socket是采用先RSA后AES加密,默认设置的证书是内置的,你需要更换成自己独一无二的RSA证书
- 生成证书.在'Menu/Window/C#Like'打开C#Like设置面板,点"Generate RSA"按钮,会生成'Assets\C#Like\Editor\RSAPublicKey.txt'和'Assets\C#Like\Editor\RSAPrivateKey.txt'两个文件
- 修改服务器KissServerFramework里的设置. 修改C:\KissServerFramework\KissServerFramework.json内的socketServerRSAPrivateKey成'Assets\C#Like\Editor\RSAPrivateKey.txt'的内容
- 修改客户端C#Like里的设置. 修改'Assets\C#Like\HotUpdateScripts\Sample\SampleSocket.cs内的socketRSAPublicKey成'Assets\C#Like\Editor\RSAPublicKey.txt'的内容
-
最终双击执行C:\MyServer\KissServerFramework.exe启动KissServerFramework服务器
本系列文章导读:
- Unity热更新方案C#Like(一)-序言
- Unity热更新方案C#Like(二)-导出官方示范的例子,确认方案可行性
- Unity热更新方案C#Like(三)-详解支持的C#特性:类
- Unity热更新方案C#Like(四)-详解支持的C#特性:委托和Lambda
- Unity热更新方案C#Like(五)-详解支持的C#特性:运算表达式
- Unity热更新方案C#Like(六)-详解支持的C#特性:循环语法
- Unity热更新方案C#Like(七)-详解支持的C#特性:get/set访问器
- Unity热更新方案C#Like(八)-详解支持的C#特性:多线程
- Unity热更新方案C#Like(九)-详解支持的C#特性:Using和命名空间
- Unity热更新方案C#Like(十)-详解支持的C#特性:宏和区域
- Unity热更新方案C#Like(十一)-详解支持的C#特性:枚举
- Unity热更新方案C#Like(十二-详解支持的C#特性:参数修饰符
- Unity热更新方案C#Like(十三)-详解支持的C#特性:函数重载和默认参数
- Unity热更新方案C#Like(十四)-详解支持的C#特性:异常处理
- Unity热更新方案C#Like(十五)-详解支持的C#特性:关键字:unsafe typeof nameof $ @ #pragma #warning #error
- Unity热更新方案C#Like(十六)-详解支持的C#特性:其他杂项:初始值设定项,表达式主体,内联变量声明
- Unity热更新方案C#Like(十七)-详解支持的长链接Socket和WebSocket
- Unity热更新方案C#Like(十八)-详解如何和Unity交互
- Unity热更新方案C#Like(十九)-详解KissJSON:唯一可以在本热更新框架使用的JSON库
- Unity热更新方案C#Like(二十)-详解KissCSV:一个简易实用的CSV表格读取方式
- Unity热更新方案C#Like(廿一)-详解KissFrameworkServer:对应的示范例子和官网所用的服务器框架
- Unity热更新方案C#Like(廿二)-详解内置的例子C#Like Demo:飞机大战,简易聊天室,简易账号/物品/邮件系统
- Unity热更新方案C#Like(廿三)-实战:示范如何把Unity官方免费例子Tanks! Tutorial转成可热更新项目
- Unity热更新方案C#Like(廿四)-实战:示范如何把Unity官方免费例子Platformer Microgame转成可热更新项目
- Unity热更新方案C#Like(廿五)-实战:示范如何建立初始包CSharpLikeFreeDemo项目
- Unity热更新方案C#Like(廿六)-(可选)详解免费版的演示如何升级到完整版的演示