C#实现RPC(远程过程调用)


仿照这位Up主写的:Up主视频
项目地址:在这


首先 什么事RPC

  • 看知乎上这几个回答
  • 就像在本地调用函数一样去调用远程的函数,但是本地和远端拥有不同内存空间直接调用肯定是没有办法的,所以思路就是,我在本地调用方法,内部实现利用网络消息传输,去调用远程的函数。本质还是消息协议,只不过是在解析完协议又封装了一层用于调用具体的方法

注意

  • up主用的的是大端(高尾端),我这个因为习惯用的是小端(低尾端)
  • 剩下的就是写法问题,没什么太大差别
  • 只是为了了解具体的思路,里面存在很多问题,比如没有处理粘包分包问题,字节缓存,运行时直接利用反射等等。

客户端(调用端)

  • 首先我们上面说的就像在本地调用函数一样去调用远程的函数。仔细想想,A调用函数Func实际执行的是B的Func。好了,现在有了一个规范了两端都要实现同样的函数。看红字强调了实现这2个字,所以我们采用接口形式,两端都实现接口IUserInfo需要实现是登陆与加法
    interface IUserInfo
    {
        bool Login(string account, string pwd);

        int Add(int a, int b);
    }
  • 在看具体实现先看一下使用的几个数据结构,内置对象的枚举,ArgTypeInfo是参数附带类型枚举。
    public class ArgTypeInfo
    {
        public TypeEnum argType { get; set; }

        public object value { get; set; }
    }

    public enum TypeEnum
    {
        Void = 0,
        Int,
        Bool,
        String
    }
  • EncodeSendPackage主要用于将接口名称、方法名称、参数个数、返回类型以及参数,然后依照下面的RPC协议格式进行编码得到字节数组。
/* 
	RPC协议格式
	接口名长度(1字节)、接口名, 方法名长度(1字节),方法名,参数长度(1字节),返回类型,
	参数序列(string类型前面有字符串长度),int类型是32位
*/ 

private static byte[] EncodeSendPackage(string interfaceName, string methodName, int argLen, TypeEnum returnType, List<ArgTypeInfo> argTypeInfos
            )
        {
            // 接口名称
            List<byte> byteList = new List<byte>();
            byte[] interfaceBytes = Encoding.UTF8.GetBytes(interfaceName);
            byteList.Add((byte)interfaceBytes.Length);
            byteList.AddRange(interfaceBytes);
            // 方法名称
            byte[] methodNameBytes = Encoding.UTF8.GetBytes(methodName);
            byteList.Add((byte)methodNameBytes.Length);
            byteList.AddRange(methodNameBytes);

            // 参数个数
            byteList.Add((byte)argLen);

            // 返回类型
            byteList.Add((byte)returnType);

            // 参数列表
            foreach (ArgTypeInfo ati in argTypeInfos)
            {
                byteList.Add((byte)ati.argType);
                if (ati.argType == TypeEnum.String)
                {
                    string value = ati.value as string;
                    byte[] stringBytes = Encoding.UTF8.GetBytes(value);
                    byteList.Add((byte)stringBytes.Length);
                    byteList.AddRange(stringBytes);
                }
                else if (ati.argType == TypeEnum.Int)
                {
                    int value = Convert.ToInt32(ati.value);
                    byte[] intBytes = Int2Bytes(value);
                    byteList.AddRange(intBytes);
                }
                else if (ati.argType == TypeEnum.Bool)
                {
                    bool value = Convert.ToBoolean(ati.value);
                    byte boolBytes = value ? (byte)1 : (byte)0;
                    byteList.Add(boolBytes);
                }
            }

            return byteList.ToArray();
        }
    }
  • 里面出现的Int2Bytes和Bytes2Int如下(小端格式)
        private static byte[] Int2Bytes(int val)
        {
            byte[] res = new byte[4];
            res[0] = (byte)(val >> 0);
            res[1] = (byte)(val >> 8);
            res[2] = (byte)(val >> 16);
            res[3] = (byte)(val >> 24);
            return res;
        }

        private static int Bytes2Int(byte[] bytes)
        {
            int res = (bytes[0] | (bytes[1] << 8) | (bytes[2] << 16) | (bytes[3] << 24));
            return res;
        }
  • 然后看具体接口实现,这里我为了图方便,有很多重复代码而且实现的很粗暴不要过于在意,可以看到参数被封装成ArgTypeInfo传递,然后依靠socket发出。
        public bool Login(string account, string pwd)
        {
            byte[] sendBytes = EncodeSendPackage(nameof(IUserInfo), "Login", 2, TypeEnum.Bool, new List<ArgTypeInfo>() {
                new ArgTypeInfo()
                {
                    argType = TypeEnum.String,value = account
                },
                new ArgTypeInfo()
                {
                    argType = TypeEnum.String,value = pwd
                }
            });

            Socket sk = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
            sk.Connect(new IPEndPoint(IPAddress.Parse("127.0.0.1"), 8888));
            sk.Send(sendBytes);

            byte[] rBuffer = new byte[1024];
            int rCount = sk.Receive(rBuffer);
            if (rCount > 0)
            {
                return rBuffer[0] == 1;
            }
            throw new Exception();
        }

        public int Add(int a, int b)
        {
            byte[] sendBytes = EncodeSendPackage(nameof(IUserInfo), "Add", 2, TypeEnum.Int, new List<ArgTypeInfo>() {
                new ArgTypeInfo()
                {
                    argType = TypeEnum.Int,value = a
                },
                new ArgTypeInfo()
                {
                    argType = TypeEnum.Int,value = b
                }
            });

            Socket sk = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
            sk.Connect(new IPEndPoint(IPAddress.Parse("127.0.0.1"), 8888));
            sk.Send(sendBytes);

            byte[] rBuffer = new byte[1024];
            int rCount = sk.Receive(rBuffer);
            if (rCount > 0)
            {
                int res = Bytes2Int(rBuffer);
                return res;
            }
            throw new Exception();
        }

服务端

  • 可以看到客户端已经将消息发出了,服务端就需要开启监听套接字,监听连接请求以及解析,并根据解析的信息去调用相应的方法。
  • 首先来看服务端具体接口如何实现,简单的判断账号密码是否符合要求,以及加法功能。
    public class MyUserInfo : IUserInfo
    {
        public int Add(int a, int b)
        {
            return a + b;
        }

        public bool Login(string account, string pwd)
        {
            if (account == "abc" && pwd == "123")
            {
                return true;
            }
            return false;
        }
    }
  • 然后是让服务器开启监听,并处理相应的连接请求,代码很基础有注释
        private static Socket listenSocket;

        static void Main(string[] args)
        {
            listenSocket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
            IPAddress ipadr = IPAddress.Parse("0.0.0.0");
            IPEndPoint iPEnd = new IPEndPoint(ipadr, 8888);
            listenSocket.Bind(iPEnd);
            listenSocket.Listen(0);

            Thread t = new Thread(Execute);
            // 后台线程随着主线程结束而结束
            t.IsBackground = true;
            t.Start();

            Console.WriteLine("Server Lauch");
            Console.Read();
        }

        private static void Execute()
        {
            while (true)
            {
                Socket client = listenSocket.Accept();

                Thread cThread = new Thread(SingleClientExecute);
                cThread.IsBackground = true;
                cThread.Start(client);
            }
        }
  • SingleClientExecute就是具体的解析了,里面是在运行时进行反射调用效率很低,这里只是为了验证,下面是具体代码,注释都有
private static void SingleClientExecute(object obj)
        {
            Socket client = obj as Socket;

            byte[] rBuffer = new byte[1024];
            int rCount = client.Receive(rBuffer);
            if (rCount > 0)
            {
                MemoryStream ms = new MemoryStream(rBuffer);
                BinaryReader br = new BinaryReader(ms);

                // RPC协议格式
                // 接口名长度(1字节)、接口名, 方法名长度(1字节),方法名,参数长度(1字节),返回类型,
                // 参数序列(string类型前面有字符串长度),int类型是32位
                // 接口名
                int interfaceLen = br.ReadByte();
                byte[] interfaceBytes = br.ReadBytes(interfaceLen);
                string interfaceName = Encoding.UTF8.GetString(interfaceBytes);
                // 方法名
                int methodNameLen = br.ReadByte();
                byte[] methodNameBytes = br.ReadBytes(methodNameLen);
                string methodName = Encoding.UTF8.GetString(methodNameBytes);
                // 参数长度
                int argsLen = br.ReadByte();
                // 返回类型
                int returnType = br.ReadByte();
                // 解析argsLen个参数
                List<object> argsList = new List<object>();
                for (int i = 0; i < argsLen; i++)
                {
                    int singleArgType = br.ReadByte();
                    if (singleArgType == 1)
                    { // int
                        byte[] intBytes = br.ReadBytes(4);
                        int value = Bytes2Int(intBytes);
                        argsList.Add(value); //涉及装箱
                    }
                    else if (singleArgType == 2)
                    { // bool
                        bool value = br.ReadByte() == 1;
                        argsList.Add(value);
                    }
                    else if (singleArgType == 3)
                    { // string
                        int stringBytesLen = br.ReadByte();
                        byte[] stringBytes = br.ReadBytes(stringBytesLen);
                        string value = Encoding.UTF8.GetString(stringBytes);
                        argsList.Add(value);
                    }
                }


                // 下面通过得到的信息,去调用相关方法
                Type interfaceType = Type.GetType(MethodBase.GetCurrentMethod().DeclaringType.Namespace + "." + interfaceName);
                if (interfaceType == null)
                { //没有这个接口
                    throw new Exception();
                }

                Type subClassType = null;
                var types = Assembly.GetExecutingAssembly().GetTypes();
                foreach (var type in types)
                {
                    if (interfaceType.IsAssignableFrom(type) && type != interfaceType)
                    {
                        subClassType = type;
                        break;
                    }
                }

                if (subClassType == null) throw new Exception();

                MethodInfo[] methodInfos = subClassType.GetMethods();
                MethodInfo method = null;
                foreach (var mi in methodInfos)
                {
                    if (mi.Name == methodName)
                    {
                        method = mi;
                        break;
                    }
                }

                if (method == null) throw new Exception();

                // 执行
                object instance = Activator.CreateInstance(subClassType);

                // 得到结果 用于发送
                object res = method.Invoke(instance, argsList.ToArray());
                if (returnType == 1)
                { // int
                    int value = Convert.ToInt32(res);
                    byte[] resBytes = Int2Bytes(value);
                    client.Send(resBytes);
                    return;
                }
                else if (returnType == 2)
                { // bool
                    bool value = Convert.ToBoolean(res);
                    byte[] boolBytes = new byte[1] { value ? (byte)1 : (byte)0 };
                    client.Send(boolBytes);
                    return;
                }
                else if (returnType == 3)
                { // string
                    List<byte> sendBytes = new List<byte>();
                    byte[] strBytes = Encoding.UTF8.GetBytes(res.ToString());
                    sendBytes.Add((byte)strBytes.Length);
                    sendBytes.AddRange(strBytes);
                    client.Send(sendBytes.ToArray());
                    return;
                }
            }
        }
  • 具体代码在上面的Git项目。在Exe文件中有生成的exe文件可以跑一下试试。

  • 3
    点赞
  • 14
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值