仿照这位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协议格式进行编码得到字节数组。
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);
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();
List<object> argsList = new List<object>();
for (int i = 0; i < argsLen; i++)
{
int singleArgType = br.ReadByte();
if (singleArgType == 1)
{
byte[] intBytes = br.ReadBytes(4);
int value = Bytes2Int(intBytes);
argsList.Add(value);
}
else if (singleArgType == 2)
{
bool value = br.ReadByte() == 1;
argsList.Add(value);
}
else if (singleArgType == 3)
{
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 value = Convert.ToInt32(res);
byte[] resBytes = Int2Bytes(value);
client.Send(resBytes);
return;
}
else if (returnType == 2)
{
bool value = Convert.ToBoolean(res);
byte[] boolBytes = new byte[1] { value ? (byte)1 : (byte)0 };
client.Send(boolBytes);
return;
}
else if (returnType == 3)
{
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文件可以跑一下试试。