《攻城Online》快速原型:服务端设计
“攻城”服务端采用Photon引擎的框架,其主要逻辑如以下UML所示。
服务端的启动入口为ServerApplication,该类包含着相关的Collection数据集合,而Collection内又有与数据库文件夹Database关联的文件。两个文件夹的内容如图。
简单来说,ServerApplication内缓存着各类数据,并完成与数据库等的关联。而本篇的重点是ServerPeer这个类。下面介绍什么是Peer。
每当一个客户端连接到服务端时,服务端会自动生成一个客户端连接实例,称其为Peer。通过Peer,便能完成服务端与客户端之间的数据交互,ServerPeer类便是完成这个任务的类。在这里通过简单代码介绍这个类的内容。
1 //----------------------------------------------------------------------------------------------------------- 2 // Copyright (C) 2015-2016 SiegeOnline 3 // 版权所有 4 // 5 // 文件名:ServerPeer.cs 6 // 7 // 文件功能描述: 8 // 9 // 服务端与客户端的连线实例 10 // 11 // 创建标识:taixihuase 20150712 12 // 13 // 修改标识: 14 // 修改描述: 15 // 16 // 17 // 修改标识: 18 // 修改描述: 19 // 20 //----------------------------------------------------------------------------------------------------------- 21 22 using System; 23 using ExitGames.Logging; 24 using Photon.SocketServer; 25 using PhotonHostRuntimeInterfaces; 26 using SiegeOnlineServer.Protocol; 27 using SiegeOnlineServer.ServerLogic; 28 29 namespace SiegeOnlineServer 30 { 31 /// <summary> 32 /// 类型:类 33 /// 名称:ServerPeer 34 /// 作者:taixihuase 35 /// 作用:用于服务端与客户端之间的数据传输 36 /// 编写日期:2015/7/12 37 /// </summary> 38 public class ServerPeer : PeerBase 39 { 40 // 日志 41 public static readonly ILogger Log = LogManager.GetCurrentClassLogger(); 42 43 // 索引 44 public Guid PeerGuid { get; protected set; } 45 46 // 服务端 47 public readonly ServerApplication Server; 48 49 /// <summary> 50 /// 类型:方法 51 /// 名称:ServerPeer 52 /// 作者:taixihuase 53 /// 作用:构造 ServerPeer 对象 54 /// 编写日期:2015/7/12 55 /// </summary> 56 /// <param name="protocol"></param> 57 /// <param name="unmanagedPeer"></param> 58 /// <param name="server"></param> 59 public ServerPeer(IRpcProtocol protocol, IPhotonPeer unmanagedPeer, ServerApplication server) : base(protocol, unmanagedPeer) 60 { 61 PeerGuid = Guid.NewGuid(); 62 Server = server; 63 64 // 将当前 peer 加入连线列表 65 Server.Users.AddConnectedPeer(PeerGuid, this); 66 } 67 68 /// <summary> 69 /// 类型:方法 70 /// 名称:OnOperationRequest 71 /// 作者:taixihuase 72 /// 作用:响应并处理客户端发来的请求 73 /// 编写日期:2015/7/14 74 /// </summary> 75 /// <param name="operationRequest"></param> 76 /// <param name="sendParameters"></param> 77 protected override void OnOperationRequest(OperationRequest operationRequest, SendParameters sendParameters) 78 { 79 switch (operationRequest.OperationCode) 80 { 81 // 账号登陆 82 case (byte) OperationCode.Login: 83 Login.OnRequest(operationRequest, sendParameters, this); 84 break; 85 86 // 创建新角色 87 case (byte) OperationCode.CreateCharacter: 88 CreateCharacter.OnRequest(operationRequest, sendParameters, this); 89 break; 90 91 // 角色进入场景 92 case (byte) OperationCode.WorldEnter: 93 WorldEnter.OnRequest(operationRequest, sendParameters, this); 94 break; 95 96 97 } 98 } 99 100 /// <summary> 101 /// 类型:方法 102 /// 名称:OnDisconnect 103 /// 作者:taixihuase 104 /// 作用:当与客户端失去连接时进行处理 105 /// 编写日期:2015/7/12 106 /// </summary> 107 /// <param name="reasonCode"></param> 108 /// <param name="reasonDetail"></param> 109 protected override void OnDisconnect(DisconnectReason reasonCode, string reasonDetail) 110 { 111 Server.Players.RemoveCharacter(PeerGuid); 112 Server.Users.UserOffline(PeerGuid); 113 Server.Users.RemovePeer(PeerGuid); 114 } 115 } 116 }
可以看到,ServerPeer的重点在于OnOperationRequest方法,该方法其中一个参数为OperationRequest类型的对象,这个对象中包含着一个byte型的OperationCode对象和一个Dictionary<byte, object>的对象Parameters。其中OperationCode即为客户端的操作请求码,服务端需要通过识别这个操作码才能对特定数据执行正确的处理。Parameters包含着等待处理的数据,这是个字典类型的对象,其键为参数类型码,对应的值即为该参数类型码所说明的数据对象。由于Photon的这个字典中object无法直接对自定义类进行序列化,因此需要通过手动序列化为二进制数据后再传入字典,取出时也要根据操作码或参数类型码手动反序列化为特定实例。这里封装好这两个操作为静态方法,直接调用即可。
1 //----------------------------------------------------------------------------------------------------------- 2 // Copyright (C) 2015-2016 SiegeOnline 3 // 版权所有 4 // 5 // 文件名:Serialization.cs 6 // 7 // 文件功能描述: 8 // 9 // 数据对象二进制序列化及反序列化 10 // 11 // 创建标识:taixihuase 20150714 12 // 13 // 修改标识: 14 // 修改描述: 15 // 16 // 17 // 修改标识: 18 // 修改描述: 19 // 20 //---------------------------------------------------------------------------------------------------------- 21 22 using System.IO; 23 using System.Runtime.Serialization; 24 using System.Runtime.Serialization.Formatters.Binary; 25 26 namespace SiegeOnlineServer.Protocol 27 { 28 /// <summary> 29 /// 类型:类 30 /// 名称:Serialization 31 /// 作者:taixihuase 32 /// 作用:对数据进行二进制序列化与反序列化 33 /// 编写日期:2015/7/14 34 /// </summary> 35 public class Serialization 36 { 37 /// <summary> 38 /// 类型:方法 39 /// 名称:Serialize 40 /// 作者:taixihuase 41 /// 作用:将一个对象二进制序列化 42 /// 编写日期:2015/7/14 43 /// </summary> 44 /// <param name="unSerializedObj"></param> 45 /// <returns></returns> 46 public static byte[] Serialize(object unSerializedObj) 47 { 48 MemoryStream stream = new MemoryStream(); 49 IFormatter formatter = new BinaryFormatter(); 50 formatter.Serialize(stream, unSerializedObj); 51 return stream.ToArray(); 52 } 53 54 /// <summary> 55 /// 类型:方法 56 /// 名称:Deserialize 57 /// 作者:taixihuase 58 /// 作用:将一个二进制序列化数据流反序列化为一个对象 59 /// 编写日期:2015/7/14 60 /// </summary> 61 /// <param name="serializedArray"></param> 62 /// <returns></returns> 63 public static object Deserialize(object serializedArray) 64 { 65 MemoryStream stream = new MemoryStream((byte[])serializedArray); 66 IFormatter formatter = new BinaryFormatter(); 67 stream.Seek(0, SeekOrigin.Begin); 68 object unSerializedObj = formatter.Deserialize(stream); 69 return unSerializedObj; 70 } 71 } 72 }
重新回到OnOperationRequest方法,当判别了操作码类型后,则Peer会调用特定类的一个静态方法,这样便可不需要实例化这些类型的对象后再使用。例如:
1 // 账号登陆 2 case (byte) OperationCode.Login: 3 Login.OnRequest(operationRequest, sendParameters, this); 4 break;
当识别为Login操作后,则调用Login里的OnRequest方法,并把参数原封不动传过去,在另外的文件里进行处理,这样子方便操作。同时OnRequest方法还要求第三个参数,为ServerPeer类型的对象,这样通过把this传过去,便能在其他地方引用到该Peer,而Peer又存放着ServerApplication的引用,方法调用及数据传输便畅通无阻。需要注意的是,不管操作码是什么,都是直接调用相应的OnRequest方法,如上面的代码所示。
接下来是对请求的处理逻辑。
当前实现了对三个不同请求的处理,在此用Login操作讲解。每个逻辑处理文件都包含OnRequest方法,除此之外,还有一个以“Try”开头命名的方法,该方法参数与OnRequest相同,即OnRequest再次将数据传给Try方法,而Try方法则真正进行处理,完成后将回应发生请求的客户端,或者向特定客户端发送广播。
1 //----------------------------------------------------------------------------------------------------------- 2 // Copyright (C) 2015-2016 SiegeOnline 3 // 版权所有 4 // 5 // 文件名:Login.cs 6 // 7 // 文件功能描述: 8 // 9 // 登录用户账号,响应客户端登录账号请求 10 // 11 // 创建标识:taixihuase 20150714 12 // 13 // 修改标识: 14 // 修改描述: 15 // 16 // 17 // 修改标识: 18 // 修改描述: 19 // 20 //----------------------------------------------------------------------------------------------------------- 21 22 using System; 23 using System.Collections.Generic; 24 using Photon.SocketServer; 25 using SiegeOnlineServer.Collection; 26 using SiegeOnlineServer.Protocol; 27 using SiegeOnlineServer.Protocol.Common.Character; 28 using SiegeOnlineServer.Protocol.Common.User; 29 30 namespace SiegeOnlineServer.ServerLogic 31 { 32 /// <summary> 33 /// 类型:类 34 /// 名称:Login 35 /// 作者:taixihuase 36 /// 作用:响应登录请求 37 /// 编写日期:2015/7/14 38 /// </summary> 39 public class Login 40 { 41 /// <summary> 42 /// 类型:方法 43 /// 名称:OnRequest 44 /// 作者:taixihuase 45 /// 作用:当收到请求时,进行处理 46 /// 编写日期:2015/7/14 47 /// </summary> 48 /// <param name="operationRequest"></param> 49 /// <param name="sendParameters"></param> 50 /// <param name="peer"></param> 51 public static void OnRequest(OperationRequest operationRequest, SendParameters sendParameters, ServerPeer peer) 52 { 53 TryLogin(operationRequest, sendParameters, peer); 54 } 55 56 /// <summary> 57 /// 类型:方法 58 /// 名称:TryLogin 59 /// 作者:taixihuase 60 /// 作用:通过登录数据尝试登录 61 /// 编写日期:2015/7/14 62 /// </summary> 63 /// <param name="operationRequest"></param> 64 /// <param name="sendParameters"></param> 65 /// <param name="peer"></param> 66 private static void TryLogin(OperationRequest operationRequest, SendParameters sendParameters, ServerPeer peer) 67 { 68 ServerPeer.Log.Debug("Logining..."); 69 70 LoginInfo login = (LoginInfo) 71 Serialization.Deserialize(operationRequest.Parameters[(byte) ParameterCode.Login]); 72 73 #region 对账号密码进行判断 74 75 ServerPeer.Log.Debug(DateTime.Now + " : Loginning..."); 76 ServerPeer.Log.Debug(login.Account); 77 ServerPeer.Log.Debug(login.Password); 78 79 // 获取用户资料 80 UserBase user = new UserBase(peer.PeerGuid, login.Account); 81 UserCollection.UserReturn userReturn = peer.Server.Users.UserOnline(ref user, login.Password); 82 83 // 若成功取得用户资料 84 if (userReturn.ReturnCode == (byte) UserCollection.UserReturn.ReturnCodeTypes.Success) 85 { 86 ServerPeer.Log.Debug(user.LoginTime + " :User " + user.Nickname + " loginning..."); 87 88 // 用于选择的数据返回参数 89 var parameter = new Dictionary<byte, object>(); 90 91 // 用于选择的字符串信息 92 string message = ""; 93 94 // 用于选择的返回值 95 short returnCode = -1; 96 97 #region 获取角色资料 98 99 Character character = new Character(user); 100 PlayerCollection.CharacterReturn characterReturn = 101 peer.Server.Players.SearchCharacter(ref character); 102 103 // 若取得角色资料 104 if (characterReturn.ReturnCode == (byte) PlayerCollection.CharacterReturn.ReturnCodeTypes.Success) 105 { 106 byte[] playerBytes = Serialization.Serialize(character); 107 parameter.Add((byte) ParameterCode.Login, playerBytes); 108 returnCode = (short) ErrorCode.Ok; 109 message = ""; 110 111 ServerPeer.Log.Debug(character.Occupation.Name); 112 } 113 else if (characterReturn.ReturnCode == 114 (byte) PlayerCollection.CharacterReturn.ReturnCodeTypes.CharacterNotFound) 115 { 116 byte[] userBytes = Serialization.Serialize(user); 117 parameter.Add((byte) ParameterCode.Login, userBytes); 118 returnCode = (short) ErrorCode.CharacterNotFound; 119 message = characterReturn.DebugMessage.ToString(); 120 } 121 122 #endregion 123 124 OperationResponse response = new OperationResponse((byte) OperationCode.Login, parameter) 125 { 126 ReturnCode = returnCode, 127 DebugMessage = message 128 }; 129 peer.SendOperationResponse(response, sendParameters); 130 ServerPeer.Log.Debug(user.LoginTime + " : User " + user.Account + " logins successfully"); 131 } 132 // 若重复登录 133 else if (userReturn.ReturnCode == (byte) UserCollection.UserReturn.ReturnCodeTypes.RepeatedLogin) 134 { 135 OperationResponse response = new OperationResponse((byte) OperationCode.Login) 136 { 137 ReturnCode = (short) ErrorCode.RepeatedOperation, 138 DebugMessage = "账号已登录!" 139 }; 140 peer.SendOperationResponse(response, sendParameters); 141 ServerPeer.Log.Debug(DateTime.Now + " : Failed to login " + user.Account + " Because of " + 142 Enum.GetName(typeof (UserCollection.UserReturn.ReturnCodeTypes), 143 userReturn.ReturnCode)); 144 } 145 else 146 { 147 // 返回非法登录错误 148 OperationResponse response = new OperationResponse((byte) OperationCode.Login) 149 { 150 ReturnCode = (short) ErrorCode.InvalidOperation, 151 DebugMessage = userReturn.DebugMessage.ToString() 152 }; 153 peer.SendOperationResponse(response, sendParameters); 154 ServerPeer.Log.Debug(DateTime.Now + " : Failed to login " + user.Account + " Because of " + 155 Enum.GetName(typeof (UserCollection.UserReturn.ReturnCodeTypes), 156 userReturn.ReturnCode)); 157 } 158 } 159 160 #endregion 161 } 162 }
TryLogin将OperationRequest中的数据反序列化后取出,然后通过ServerPeer对象作为中介,与ServerApplication关联,从而可以通过Application里的数据库关联来获取账号、角色信息等,并将结果和数据返回到Login中。之后需要将结果发送回给客户端接收,ServerPeer通过继承后,包含有一个SendOperationResponse方法,这是为什么需要传给OnRequest和TryLogin方法第三个参数的原因,在这里便能直接调用了。SendOperationResponse方法需要一个OperationResponse类型的对象和一个SendParameters类型的对象,后者一般填入层层调用传过来的那个sendParameters或者自己new一个即可。此处的重点是OperationResponse。
客户端发送Request请求,服务端接收请求并处理后,就要给客户端答应,这就是Response。OperationResponse跟OperationRequest长得相似,同样带一个操作码参数和一个字典,操作码跟Request一般填一样,这样客户端接收后便能知道是发送了什么请求后得到的答应。字典类型的Parameters也是以一个参数类型码为键,以object对象为值,这里的值同样用二进制数据,调用Serialization的Serialize方法即可。此外,还有一个short型的ReturnCode字段,该字段填入请求处理的情况码,即操作正确或者某些不正确操作的错误码,这里封进枚举里表示。最后需要填的是一个DebugMessage字符串,我们可以填入操作信息,支持中文,这样在客户端测试时便能打印出来,对整个操作的执行情况一目了然。如果Response不需要回给客户端数据,则可以省略掉Parameters,但其他的还是要填。
服务端除了给发送请求的客户端进行回应外,还能对其他客户端进行广播。这里用角色进入场景时,服务端给当前正在进行游戏的所有客户端连接发送某个玩家上线的提示信息为例。
1 //----------------------------------------------------------------------------------------------------------- 2 // Copyright (C) 2015-2016 SiegeOnline 3 // 版权所有 4 // 5 // 文件名:WorldEnter.cs 6 // 7 // 文件功能描述: 8 // 9 // 进入游戏场景,响应客户端进入场景请求 10 // 11 // 创建标识:taixihuase 20150722 12 // 13 // 修改标识: 14 // 修改描述: 15 // 16 // 17 // 修改标识: 18 // 修改描述: 19 // 20 //----------------------------------------------------------------------------------------------------------- 21 22 using System.Collections.Generic; 23 using Photon.SocketServer; 24 using SiegeOnlineServer.Protocol; 25 using SiegeOnlineServer.Protocol.Common.Character; 26 27 namespace SiegeOnlineServer.ServerLogic 28 { 29 /// <summary> 30 /// 类型:类 31 /// 名称:WorldEnter 32 /// 作者:taixihuase 33 /// 作用:响应进入场景请求 34 /// 编写日期:2015/7/22 35 /// </summary> 36 public class WorldEnter 37 { 38 /// <summary> 39 /// 类型:方法 40 /// 名称:OnRequest 41 /// 作者:taixihuase 42 /// 作用:当收到请求时,进行处理 43 /// 编写日期:2015/7/22 44 /// </summary> 45 /// <param name="operationRequest"></param> 46 /// <param name="sendParameters"></param> 47 /// <param name="peer"></param> 48 public static void OnRequest(OperationRequest operationRequest, SendParameters sendParameters, ServerPeer peer) 49 { 50 TryEnter(operationRequest, sendParameters, peer); 51 } 52 53 /// <summary> 54 /// 类型:方法 55 /// 名称:TryEnter 56 /// 作者:taixihuase 57 /// 作用:通过角色数据尝试进入场景 58 /// 编写日期:2015/7/22 59 /// </summary> 60 /// <param name="operationRequest"></param> 61 /// <param name="sendParameters"></param> 62 /// <param name="peer"></param> 63 private static void TryEnter(OperationRequest operationRequest, SendParameters sendParameters, ServerPeer peer) 64 { 65 ServerPeer.Log.Debug("Entering"); 66 67 Character character = (Character) 68 Serialization.Deserialize(operationRequest.Parameters[(byte) ParameterCode.WorldEnter]); 69 70 peer.Server.Players.CharacterEnter(ref character); 71 peer.Server.Data.CharacterData.GetCharacterPositionFromDatabase(ref character); 72 73 // 返回数据给客户端 74 75 byte[] data = Serialization.Serialize(character); 76 77 var reponseData = new OperationResponse((byte) OperationCode.WorldEnter, new Dictionary<byte, object> 78 { 79 {(byte) ParameterCode.WorldEnter, data} 80 }); 81 peer.SendOperationResponse(reponseData, sendParameters); 82 83 var eventData = new EventData((byte)EventCode.WorldEnter, new Dictionary<byte, object> 84 { 85 {(byte) ParameterCode.WorldEnter, data} 86 }); 87 eventData.SendTo(peer.Server.Players.GamingClients, sendParameters); 88 } 89 } 90 }
WorldEnter文件对这一操作进行处理,主要逻辑在于TryEnter中。服务端试图获取角色数据,然后通过SendOperationResponse返回给客户端,并且实例化一个EventData对象,该类型需要填入一个byte类型的事件代码,其实跟操作码相似,然后是一个字典类型的对象,传入给接收广播的客户端的所需数据。EventData有一个SendTo方法,第一个参数代表着要广播的客户端集合,此处可以用一个List<ServerPeer>类型的对象表示,该方法会自动遍历每一个Peer,第二个参数没特殊要求的话,照填sendParameters即可。这样一旦某个角色进入了游戏主场景,则所有在线玩家都会接收到提示。
下图是服务端原型的组织结构。
最下端的Protocol项目为协议内容,由客户端和服务端共用,会在后面文章详细介绍。
服务端主体框架就这些,其余内容还待详细设计。
Photon Server有个英文的在线文档,更多的用法可以参照以下网址:
http://doc-api.exitgames.com/en/onpremise/current/server/doc/index.html
转载于:https://www.cnblogs.com/SiegeOL/p/4690954.html
《攻城Online》快速原型:服务端设计相关推荐
- IM开发基础知识补课(三):快速理解服务端数据库读写分离原理及实践建议
1.前言 IM应用从服务端数据的角度来看,它是一种很特殊的应用场景,抛开基础数据.增值业务和附属功能不谈,单从IM聊天工具的立身之本--聊天数据来说,理论上是不需要在服务端存储的(或者说只需要短暂存储 ...
- android 升级带服务端,安卓应用升级服务端设计思路
原标题:安卓应用升级服务端设计思路 前言 去年支付宝集福卡活动玩出新花样,增加了一种花花卡,豪称全年帮你还花呗,一时火热.听到很多人在说要花花卡时,不准备玩集福卡的我准备再玩一把.进入页面功能点直接系 ...
- 在以TCP为连接方式的服务器中,为什么在服务端设计当中需要考虑心跳?
https://www.zhihu.com/question/35013918 在以TCP为连接方式的服务器中,为什么在服务端设计当中需要考虑心跳? 这个心跳包除了告知服务端我在线,还有其他作用吗?比 ...
- vxi11协议服务器的实现,LXI_VXI零槽控制器服务端设计与实现
摘要: LXI_VXI零槽控制器是基于LAN总线综合测试系统的核心控制部件.与其他总线综合零槽控制器相比,LXI_VXI零槽控制器支持多用户.跨平台.实时性和远程控制,具有高数据传输速率.高吞吐率.低 ...
- TYPESDK手游聚合SDK服务端设计思路与架构之一:应用场景分析
TYPESDK 服务端设计思路与架构之一:应用场景分析 作为一个渠道SDK统一接入框架,TYPESDK从一开始,所面对的需求场景就是多款游戏,通过一个统一的SDK服务端,能够同时接入几十个甚至几百个各 ...
- 游戏交流社区BBS论坛APP客户端和网页服务端设计 毕业论文+前后端源码及数据库文件
下载地址:https://download.csdn.net/download/m0_63680064/36065411 项目介绍: 游戏交流社区BBS论坛APP客户端和网页服务端设计 毕业论文+前后 ...
- Redis的IO模型以及客户端与服务端设计
文章目录 IO模型--事件驱动 文件事件(通常是与客户端的交互) 文件事件的处理器 时间事件(服务器的自身触发的一些维护操作) 分类 底层实现 时间事件应用实例:serverCron函数 事件的调度与 ...
- 天气APP服务端——1.APP服务端设计
1.业务的需求 (1)客户端安装后,第一次运行,想服务端发送请求(上传客户手机的设备编号),获取城市站点基本信息 (2)客户端有手机定位功能,自动匹配到最近的城市站点 (3)客户端发送请求报文,根据站 ...
- TYPESDK手游聚合SDK服务端设计思路与架构之二:服务端设计
在前一篇文中,我们对一个聚合SDK服务端所需要实现的功能作了简单的分析.通过两个主要场景的功能流程图,我们可以看到,作为多款游戏要适配多个渠道的统一请求转发中心,TYPESDK服务端主要需要实现的功能 ...
最新文章
- ERROR LNK2019:无法解析的外部的符号 _sscanf或者_vsprintf
- jQuery Ajax: $.post请求示例
- eclipse查看git地址_使用Git进行版本控制
- bbb u-boot mmc总线初始化分析
- 索引-jquery-第二版-pyhui
- mysql挂载数据卷_docker卷挂载技术
- FastJSON、Gson、Jackson(简单了解使用)
- 关于HP C7K的firmware management中的power policy理解
- 如何理解运算放大器的增益带宽积-运放增益
- 我再copy回来。中海真是有心人。只是,你们在哪里?
- 2022年最新的Detectron 2 (0.6) 安装流程(联想笔记本Y9000K+Anaconda+Win 11 +RTX3070)
- PEEK薄膜特性与各型号性能特征分析
- 关系型数据库保证数据完整性和一致性的方法
- 会声会影 2020 23.2.0.587 旗舰版
- 什么是Remoting
- 串行FLASH文件系统FatFs---转自野火论坛
- 初试Cloudxns详解,智能解析如此简单
- doPost请求的用法
- Nginx解决端口问题
- 使用UCF101完成的视频动作分类识别
热门文章
- 离线编译安装lrzsz
- AD20的一些基本操作
- [转]OKR结合CFR的管理模式
- 易飞9安装和授权视频
- 【推荐系统论文精读系列】(八)--Deep Crossing:Web-Scale Modeling without Manually Crafted Combinatorial Features
- Postgresql逻辑复制报错could not start WAL streaming: ERROR: replication slot “x“is active for PID xxx
- SOFAServerless 体系助力业务极速研发
- 英语外刊精读(Part 2):day1,泛读;day2, 精读(上);day3, 精读(下)
- java碰撞检测_java – 在oop中实现碰撞检测器的最佳方法
- 天仙般的王祖贤和林青霞,她们都是用AI修复的