C# 服务器HproseRpc使用

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/qq_18192161/article/details/79240310

Hprose官网:http://www.hprose.com/

下载C#版本Hprose.dll
地址:https://github.com/hprose/hprose-dotnet

文档地址没有C#版本的文档,我是通看java文档来尝试使用C#文档,有相同的地方,网上还有1.3版本的文档,可以进行参考
文档地址:https://github.com/hprose/hprose-doc

先看简单案例:

服务器
这里写图片描述
创建一个http服务器
HproseHttpListenerServer server = new HproseHttpListenerServer("http://localhost:20108/");
注册方法
server.Add("SayHello", hello);
启动服务器
server.Start();

客户端:
这里写图片描述
连接服务器
Hprose.Client.HproseClient client = Hprose.Client.HproseClient.Create("http://localhost:20108/");
两种调用方法的方式
Console.WriteLine(hello.SayHello("World")); //接口方式
Console.WriteLine(client.Invoke("SayHello", new object[] { "World" })); //方法名方式
运行结果这里写图片描述

在客户单就可以调用服务器的方法,并且在客户端实现输出

下面看看我在项目中的应用

首先我定义了一个xml配置表用来控制我所需要的数据

<RpcConfig>
    <RegisterClass>
        <class namespace="NetTableUser" classname ="protos.GameProto.NetTableUser|Proto.dll" />
        <class namespace="NetServerConfig" classname ="protos.GameProto.NetServerConfig|Proto.dll" />
        <class namespace="NetTableRole" classname ="protos.GameProto.NetTableRole|Proto.dll" />
        <class namespace="_Game_Match_RoleInfo" classname="protos.GameProto._Game_Match_RoleInfo|Proto.dll" />
        <class namespace="NetProps" classname ="protos.GameProto.NetProps|Proto.dll" />
        <class namespace="NetTankStruct" classname ="protos.GameProto.NetTankStruct|Proto.dll" />
    </RegisterClass>
        <RegisterChannel>
        <channel id ="GameSuit0" type="tcp" url="127.0.0.1:10003">
          <data type="class" connect="GameSuit.Rpc.GameSuit" />
        </channel>
        </RegisterChannel>
      <ConnectChannel>
        <class name="GameHall0" type="tcp" url="127.0.0.1:10001" />
        <class name="GameData" type="tcp" url="127.0.0.1:4321" />
      </ConnectChannel>
</RpcConfig>

对这个配置表解释

<!-- 配置表拥有参数
RegisterClass 注册类
    namespace:类的别名
    classname:类完整命名空间,如果不是当前程序集需要执行程序集 格式:类完整命名空间|程序集

RegisterChannel 注册信道
     type:信道类型 有两种:tcp|http 
     data:信道注册的数据
        type:数据类型 有三种class(类)|method(方法)|static(静态方法) 
        connect: class类的完整命名空间 method:方法名|类的完整命名空间 static:方法名|类完整命名空间

ConnectChannel 连接信道
    type:信道类型
    url:地址

    实例:
<   RegisterClass>
        <class namespace="NetTableUser" classname ="protos.NetStruct.NetTableUser|Proto.dll" />
    </RegisterClass>
    <RegisterChannel>
        <channel id ="1" type="tcp" url="127.0.0.1:4321">
            <data type="class" connect="GameData.HproseRpc.DataOperation" />
            <data type="method" connect = "add|GameData.HproseRpc.DataOperation" />
        </channel>
    </RegisterChannel>
    <ConnectChannel>
        <class name="GameData" type="tcp" url="127.0.0.1:4321" />
    </ConnectChannel>
-->

然后定义一个配置表解析脚本:

using System.Collections.Generic;
using System.Xml;
using IVServer.Xml;

public class RpcConfig:XMLCache<RpcConfig>
{
    //注册类
    public class RegisterClass
    {
        public string NameSpace { get; set; }
        public string ClassName { get; set; }
    }
    //注册信道
    public class RegisterChannel
    {
        public class ChannelData
        {
            public string DataType { get; set; }
            public string DataConnect { get; set; }
        }
        public string ChannelID { get; set; }
        public string ChannelType { get; set; }
        public string ChannelUrl { get; set; }
        public List<ChannelData> ChannelDatas = new List<ChannelData>();

        public string GetUrl
        {
            get {
                return string.Format("{0}://{1}/",ChannelType,ChannelUrl);
            }
        }

    }
    //连接信道
    public class ConnectChannel
    {
        public string ConnectName { get; set; }
        public string ConnectType { get; set; }
        public string ConnectUrl { get; set; } 

        public string GetUrl
        {
            get {
                return string.Format("{0}://{1}/", ConnectType, ConnectUrl);
            }
        }

    }

    public List<RegisterClass> RegisterClassList = new List<RpcConfig.RegisterClass>();
    public Dictionary<string, RegisterChannel> RegisterChannelDic = new Dictionary<string, RpcConfig.RegisterChannel>();
    public Dictionary<string, ConnectChannel> ConnectChannelDic = new Dictionary<string, ConnectChannel>();

    //解析配置表
    public override void ParseXml(XmlNode xmlNode)
    {
        XmlNodeList child = null;
        switch (xmlNode.Name)
        {
            case "RegisterClass":
                child = xmlNode.ChildNodes;
                for (int i = 0; i < child.Count; i++)
                {
                    RegisterClass rc = new RegisterClass();
                    rc.NameSpace = child[i].Attributes["namespace"].Value;
                    rc.ClassName = child[i].Attributes["classname"].Value;
                    RegisterClassList.Add(rc);
                }
                break;
            case "RegisterChannel":
                child = xmlNode.ChildNodes;
                for (int i = 0; i < child.Count; i++)
                {
                    RegisterChannel rc = new RegisterChannel();
                    rc.ChannelID = child[i].Attributes["id"].Value;
                    rc.ChannelType = child[i].Attributes["type"].Value;
                    rc.ChannelUrl = child[i].Attributes["url"].Value;
                    XmlNodeList datas = child[i].ChildNodes;
                    for (int j = 0; j < datas.Count; j++)
                    {
                        RegisterChannel.ChannelData cd = new RegisterChannel.ChannelData();
                        cd.DataType = datas[j].Attributes["type"].Value;
                        cd.DataConnect = datas[j].Attributes["connect"].Value;
                        rc.ChannelDatas.Add(cd);
                    }
                    RegisterChannelDic.Add(rc.ChannelID,rc);
                }
                break;
            case "ConnectChannel":
                child = xmlNode.ChildNodes;
                for (int i = 0; i < child.Count; i++)
                {
                    ConnectChannel cc = new ConnectChannel();
                    cc.ConnectName = child[i].Attributes["name"].Value;
                    cc.ConnectType = child[i].Attributes["type"].Value;
                    cc.ConnectUrl = child[i].Attributes["url"].Value;
                    ConnectChannelDic.Add(cc.ConnectName,cc);
                }
                break;
        }
        OnParseFinsh(xmlNode.Name, this);
    }
}

xml配置表解析父类

using System;
using System.Collections.Generic;
using System.Xml;

namespace IVServer.Xml
{
    public abstract class XMLCache<T> where T:XMLCache<T>
    {
        /// <summary>
        /// 数据字段
        /// </summary>
        private static Dictionary<string, T> dataDic = new Dictionary<string, T>();
        /// <summary>
        /// xml数据信息
        /// </summary>
        private static XmlInfo<T> xmlInfo = new XmlInfo<T>();

        static XMLCache()
        {
            string name = typeof(T).Name;
            xmlInfo.InitXmlInfo(name);
            //ResterConfig();
        }
        /// <summary>
        /// 重置配置表
        /// </summary>
        public static void ResterConfig()
        {
            dataDic.Clear();
            string name = typeof(T).Name;
            xmlInfo.InitXmlInfo(name);
        }

        /// <summary>
        /// 是否存在
        /// </summary>
        /// <param name="key"></param>
        /// <returns></returns>
        public static bool IsHave(string key)
        {
            return dataDic.ContainsKey(key);
        }

        /// <summary>
        /// 是否存在
        /// </summary>
        /// <param name="key"></param>
        /// <param name="value"></param>
        /// <returns></returns>
        public static bool IsHave(string key, out T value)
        {
            if (!IsHave(key))
            {
                value = null;
                return false;
            }
            value = dataDic[key];
            return true;
        }

        /// <summary>
        /// 尝试获取
        /// </summary>
        /// <param name="key"></param>
        /// <param name="value"></param>
        public static void TryGet(string key, out T value)
        {
            if (!IsHave(key, out value))
            {
                value = null;
            }
        }

        /// <summary>
        /// 尝试获取
        /// </summary>
        /// <param name="key"></param>
        /// <returns></returns>
        public static T TryGet(string key)
        {
            T data = default(T);
            if (IsHave(key, out data))
                return data;
            return default(T);
        }

        /// <summary>
        /// 数据数量
        /// </summary>
        public static int Count {
            get {
                return dataDic.Count;
            }
        }

        /// <summary>
        /// 获取数据列表
        /// </summary>
        public static List<T> GetData
        {
            get {
                List<T> dataList = new List<T>(dataDic.Values);
                return dataList;
            }
        }

        /// <summary>
        /// 解析数据
        /// </summary>
        /// <param name="xmlNode"></param>
        /// <param name="data"></param>
        public abstract void ParseXml(XmlNode xmlNode);

        /// <summary>
        /// 数据解析完毕后
        /// </summary>
        /// <param name="key"></param>
        /// <param name="data"></param>
        protected static void OnParseFinsh(string key, T data)
        {
            if (dataDic.ContainsKey(key)) return;
            dataDic.Add(key,data);
        }

    }

    public class XmlInfo<T> where T: XMLCache<T>
    {
        public void InitXmlInfo(string xmlName)
        {
            try
            {
                //xml 路径
                string filePath = string.Format(GameConfig.GetConfig("Config.XML.Data.Path"), xmlName);
                XmlDocument xmlDoc = new XmlDocument();
                XmlReader reader = XmlReader.Create(filePath);
                if (reader != null)
                {
                    xmlDoc.Load(reader);
                }
                reader.Close();

                XmlNodeList nodeList = xmlDoc.SelectSingleNode(xmlName).ChildNodes;

                for (int i = 0; i < nodeList.Count; i++)
                {
                    if (nodeList[i].NodeType == XmlNodeType.Comment) continue;
                    Type type = typeof(T);
                    T obj = Activator.CreateInstance<T>();
                    obj.ParseXml(nodeList[i]);
                }
            }
            catch (Exception e)
            {
                Log.LogError("XML解析错误,Exception:{0}",e.Message);
            }
        }
    }

}

然后在创建一个类来控制所有的数据

using System;
using System.Collections.Generic;
using Hprose.IO;
using Hprose.Server;
using Hprose.Client;
using System.Reflection;

//RPC类型
public enum RpcType
{
    tcp,
    http
}

//服务器类型
public enum RpcSuitType
{
    //匹配服
    Match,
    //战斗服
    Fight,
    //物理连接服
    PhySuit,
    //数据服
    DBSuit,

}

/// <summary>
/// Rpc信息
/// </summary>
public class RpcInfo
{
    //Rpc名称
    public string RpcName;

    //服务器类型
    public RpcSuitType RpcSuitType;

    //服务器优先级
    public int RcpLevel;

    //Rpc类型
    public RpcType RpcType;
    //rpc地址
    public string RpcUrl;

    public string GetUrl {
        get {
            return string.Format("{0}://{1}/",RpcType,RpcUrl);
        }
    }

}


/// <summary>
/// Rpc管理类
/// </summary>
public class RpcManager
{
    private static Dictionary<string, HproseService> ServerDic = new Dictionary<string, HproseService>();
    private static Dictionary<string, HproseClient> ClientDic = new Dictionary<string, HproseClient>();

    /// <summary>
    /// 注册的RPC列表
    /// </summary>
    private static Dictionary<string, HproseClient> rpcNameClientDic = new Dictionary<string, HproseClient>();
    /// <summary>
    /// 注册rpc列表
    /// </summary>
    private static Dictionary<string, RpcInfo> rpcNameDic = new Dictionary<string, RpcInfo>();

    /// <summary>
    /// 初始化Rpc
    /// </summary>
    public static void InitRpc()
    {
        //注册类
        List<RpcConfig.RegisterClass> rcs = RpcConfig.TryGet("RegisterClass").RegisterClassList;
        for (int i = 0; i < rcs.Count; i++)
        {
            Assembly assem = Assembly.GetEntryAssembly();
            Type type = null;
            if (rcs[i].ClassName.Contains("|"))
            {
                string[] ary = rcs[i].ClassName.Split('|');
                assem = Assembly.LoadFrom(ary[1]);
                type = assem.GetType(ary[0]);
            }else
                type = assem.GetType(rcs[i].ClassName);

            HproseClassManager.Register(type, rcs[i].NameSpace);
        }

        //注册通道
        Dictionary<string, RpcConfig.RegisterChannel> rcDic = RpcConfig.TryGet("RegisterChannel").RegisterChannelDic;
        List<RpcConfig.RegisterChannel> rcList = new List<RpcConfig.RegisterChannel>(rcDic.Values);
        for (int i = 0; i < rcList.Count; i++)
        {
            switch (rcList[i].ChannelType)
            {
                case "tcp":
                    try
                    {
                        HproseTcpListenerServer tcpServer = new HproseTcpListenerServer(rcList[i].GetUrl);

                        AddData(tcpServer, rcList[i].ChannelDatas);
                        tcpServer.Start();
                        if (tcpServer.IsStarted)
                        {
                            Log.LogInfo("信道 {0}-{1} 启动成功...", rcList[i].ChannelID, rcList[i].GetUrl);
                        }
                        else
                            Log.LogInfo("信道 {0}-{1} 启动失败...", rcList[i].ChannelID, rcList[i].GetUrl);
                        ServerDic.Add(rcList[i].ChannelID, tcpServer);
                    }catch(Exception e)
                    {
                        Log.LogError("信道 {0}-{1} 启动失败,Error:{2}", rcList[i].ChannelID, rcList[i].GetUrl,e.Message);
                    }
                    break;
                case "http":
                    try
                    {
                        HproseHttpListenerServer httpServer = new HproseHttpListenerServer(rcList[i].GetUrl);
                        AddData(httpServer, rcList[i].ChannelDatas);
                        httpServer.Start();
                        if (httpServer.IsStarted)
                        {
                            Log.LogInfo("信道 {0}-{1} 启动成功...", rcList[i].ChannelID, rcList[i].GetUrl);
                        }
                        else
                            Log.LogInfo("信道 {0}-{1} 启动失败...", rcList[i].ChannelID, rcList[i].GetUrl);
                        ServerDic.Add(rcList[i].ChannelID, httpServer);
                    }
                    catch (Exception e)
                    {
                        Log.LogError("信道 {0}-{1} 启动失败,Error:{2}", rcList[i].ChannelID, rcList[i].GetUrl, e.Message);
                    }
                    break;
            }
        }
    }

    /// <summary>
    /// 获取注册信道信息
    /// </summary>
    /// <param name="ChannelID"></param>
    /// <returns></returns>
    public static RpcConfig.RegisterChannel GetChannel(string ChannelID)
    {
        Dictionary<string, RpcConfig.RegisterChannel> rpcDic = RpcConfig.TryGet("RegisterChannel").RegisterChannelDic;
        if (rpcDic.ContainsKey(ChannelID))
            return rpcDic[ChannelID];
        return null;
    }

    /// <summary>
    /// 添加数据
    /// </summary>
    /// <param name="server"></param>
    /// <param name="datas"></param>
    private static void AddData(HproseService server,List<RpcConfig.RegisterChannel.ChannelData> datas)
    {
        for (int i = 0; i < datas.Count; i++)
        {
            string[] ary = null;
            switch (datas[i].DataType)
            {
                case "class":
                    ary = datas[i].DataConnect.Split(',');
                    for (int j = 0; j < ary.Length; j++)
                    {
                        Assembly assem = Assembly.GetEntryAssembly();
                        server.Add(Activator.CreateInstance(assem.GetType(ary[j])));
                    }
                    break;
                case "method":
                    ary = datas[i].DataConnect.Split(',');
                    for (int j = 0; j < ary.Length; j++)
                    {
                        if (ary[j].Equals("")) continue;
                        string[] dataAry = ary[j].Split('|');
                        Assembly assem = Assembly.GetEntryAssembly();
                        server.Add(dataAry[0],Activator.CreateInstance(assem.GetType(dataAry[1])));
                    }
                    break;
                case "static":
                    ary = datas[i].DataConnect.Split(',');
                    for (int j = 0; j < ary.Length; j++)
                    {
                        if (ary[j].Equals("")) continue;
                        string[] dataAry = ary[j].Split('|');
                        Assembly assem = Assembly.GetEntryAssembly();
                        server.Add(dataAry[0], assem.GetType(dataAry[1]));
                    }
                    break;
            }
        }
    }

    /// <summary>
    /// 连接服务器
    /// </summary>
    /// <param name="connectName"></param>
    /// <returns></returns>
    public static HproseClient Connect(string connectName)
    {
        if (ClientDic.ContainsKey(connectName))
            return ClientDic[connectName];
        Dictionary<string,RpcConfig.ConnectChannel> connDic = RpcConfig.TryGet("ConnectChannel").ConnectChannelDic;
        if (connectName.Contains(":"))
        {
            HproseClient client = HproseClient.Create(connectName);
            ClientDic.Add(connectName, client);
            return client;
        }

        if (connDic.ContainsKey(connectName))
        {
            RpcConfig.ConnectChannel cc = connDic[connectName];
            HproseClient client = HproseClient.Create(string.Format("{0}://{1}/", cc.ConnectType, cc.ConnectUrl));
            ClientDic.Add(connectName, client);
            return client;
        }
        return null;   
    }

    /// <summary>
    /// 连接服务器
    /// </summary>
    /// <typeparam name="T"></typeparam>
    /// <param name="connectName"></param>
    /// <returns></returns>
    public static T Connect<T>(string connectName) where T:class
    {
        HproseClient client =Connect(connectName);
        return client.UseService<T>();
    }

    /// <summary>
    /// 注册Rpc信息
    /// </summary>
    /// <param name="rpcInfo"></param>
    public static void RegisterRpc(RpcInfo rpcInfo)
    {
        if (!rpcNameDic.ContainsKey(rpcInfo.RpcName))
        {
            rpcNameDic.Add(rpcInfo.RpcName, rpcInfo);
            Log.LogInfo("服务器:{0}-{1} 注册本服务器...",rpcInfo.RpcName,rpcInfo.GetUrl);
        }
    }

    /// <summary>
    /// 移除Rpc信息
    /// </summary>
    /// <param name="rpcInfo"></param>
    public static void RemoveRpc(RpcInfo rpcInfo)
    {
        RemoveRpc(rpcInfo.RpcName);
    }


    /// <summary>
    /// 移除Rpc信息
    /// </summary>
    /// <param name="RpcName"></param>
    public static void RemoveRpc(string RpcName)
    {
        if (rpcNameDic.ContainsKey(RpcName))
        {
            Log.LogInfo("服务器:{0}-{1} 移除本服务器注册...",RpcName, rpcNameDic[RpcName].GetUrl);
            rpcNameDic.Remove(RpcName);
        }
    }

    /// <summary>
    /// 获取所有RpcInfo列表
    /// </summary>
    /// <returns></returns>
    public static List<RpcInfo> GetRpcInfo()
    {
        return new List<RpcInfo>(rpcNameDic.Values);
    }

    ///获取特定类型服务器
    public static List<RpcInfo> GetRpcInfo(RpcSuitType suitType)
    {
        List<RpcInfo> all = new List<RpcInfo>(rpcNameDic.Values);
        List<RpcInfo> suits = new List<RpcInfo>();
        for(int i=0;i<all.Count;i++)
        {
            if(all[i].RpcSuitType==suitType)
                suits.Add(all[i]);
        }
        return suits;
    }

    /// <summary>
    /// 获取RpcInfo
    /// </summary>
    /// <param name="rpcName"></param>
    /// <returns></returns>
    public static RpcInfo GetRpcInfo(string rpcName)
    {
        if (rpcNameDic.ContainsKey(rpcName))
            return rpcNameDic[rpcName];
        return null;
    }

    /// <summary>
    /// 连接Rpc
    /// </summary>
    /// <param name="rpcID"></param>
    /// <returns></returns>
    public static HproseClient RpcConnect(string rpcName)
    {
        if (!rpcNameDic.ContainsKey(rpcName))
            return null;
        if (rpcNameClientDic.ContainsKey(rpcName))
            return rpcNameClientDic[rpcName];
        //连接
        RpcInfo info = rpcNameDic[rpcName];
        HproseClient client = HproseClient.Create(info.GetUrl);
        rpcNameClientDic.Add(rpcName,client);
        return client;
    }

    /// <summary>
    /// 连接Rpc
    /// </summary>
    /// <typeparam name="T"></typeparam>
    /// <param name="rpcName"></param>
    /// <returns></returns>
    public static T RpcConnect<T>(string rpcName) where T : class
    {
        HproseClient client = RpcConnect(rpcName);
        return client.UseService<T>();
    }

    //连接Rpc
    public static T RpcConnect<T>(RpcInfo rpcInfo)
    {
        HproseClient client = RpcConnect(rpcInfo.RpcName);
        return client.UseService<T>();
    }
}

然后定义接口文件,


public interface IGameMatch
{
    int GetFree();
    //注册连接服务器服务器
    void ResgisterSuit(RpcInfo rpcInfo);
    //移除连接服务器
    void RemoveSuit(RpcInfo rpcInfo);
    //移除连接服务器
    void RemoveSuit(string rpcName);

    //加入匹配
    int Match_JoinMatch(int matchType, List<_Game_Match_RoleInfo> roleInfo);
    //离开匹配
    int Match_ExitMatch(List<_Game_Match_RoleInfo> roleInfo);
    //匹配结果推送
    void Match_MatchResultPush(List<_Game_Match_RoleInfo> roleInfo);
    //是否加入战斗
    void Match_IsJoin(string roleUuid,bool isJoin);
    //移除匹配
    void Match_RemoveRole(string roleUuid);


    //英雄选择模块
    //选择英雄
    void SelHero_SelHero(_Game_Match_RoleInfo roleInfo);
    //确认英雄
    void SelHero_ConfirmHero(_Game_Match_RoleInfo roleInfo);
}

在服务器端创建借口实现类

using System;
using System.Collections.Generic;
using protos.GameProto;
using GameMatch.Server;

namespace GameMatch.Rpc
{
    public class GameMatch : IGameMatch
    {
        public int GetFree()
        {
            return 0;
        }

        public void RemoveSuit(string rpcName)
        {
            RpcManager.RemoveRpc(rpcName);
        }

        public void RemoveSuit(RpcInfo rpcInfo)
        {
            RpcManager.RemoveRpc(rpcInfo);
        }

        public void ResgisterSuit(RpcInfo rpcInfo)
        {
            RpcManager.RegisterRpc(rpcInfo);
        }

        public int Match_ExitMatch(List<_Game_Match_RoleInfo> roleInfo)
        {
           return MathcCenter.ExitMatch(roleInfo);
        }

        public void Match_IsJoin(string roleUuid, bool isJoin)
        {
            MathcCenter.IsJoin(roleUuid,isJoin);
        }

        public int Match_JoinMatch(int matchType, List<_Game_Match_RoleInfo> roleInfo)
        {
            return MathcCenter.JoinMatch(matchType,roleInfo);
        }

        public void Match_MatchResultPush(List<_Game_Match_RoleInfo> roleInfo)
        {
            throw new NotImplementedException();
        }

        public void Match_RemoveRole(string roleUuid)
        {
            MathcCenter.RemoveRole(roleUuid);
        }

        /*********选择英雄******************/

        public void SelHero_ConfirmHero(_Game_Match_RoleInfo roleInfo)
        {
            RoomCenter.ConfirmHero(roleInfo);
        }

        public void SelHero_SelHero(_Game_Match_RoleInfo roleInfo)
        {
            RoomCenter.SelHero(roleInfo);
        }
    }
}

在服务器启动的时候,开启服务器本身信道
这里写图片描述

以及连接其他服务器的信道
这里写图片描述

调用接口中的相应方法即可
这里写图片描述

运行结果:
这里写图片描述

Hprose中使用的方法
注册类,type是类型,alias是别名
public static void Register(Type type, string alias);
注册tcp服务器链接
HproseTcpListenerServer tcpServer = new HproseTcpListenerServer("url");
httpServer.Start(); //启动服务器
注册http服务器链接
HproseHttpListenerServer httpServer = new HproseHttpListenerServer(rcList[i].GetUrl);
httpServer.Start(); //启动服务器
添加实现类
HproseService.Add();

需要注意的是,Hprose获取List为List 格式,需要自己进行强转。
这里写图片描述

太复杂的功能我没有制作,但是在我自己写服务器中实现了通信(没测过外网)。
这是几个月前研究的,已经忘的差不多了。

猜你喜欢

转载自blog.csdn.net/qq_18192161/article/details/79240310