1. 程式人生 > 其它 >C#之rpc很火麼?

C#之rpc很火麼?

技術標籤:C#教程c#教程

寫在前面:
  RPC,聽過很有段時間了,但是一直都不太清楚是幹嘛的,今天我們來捋一捋。

解釋:
  【Remote Procedure Call Protocol】遠端過程呼叫(就是說,A程式要呼叫一個b方法,然而這個b方法的實現在B程式內部,B程式還可能和A不在一個電腦上面,怎麼呼叫?http可以呼叫/rpc也可以,讓他像呼叫本地方法一樣呼叫)

使用初探:
  用了一下市面上的,rpc框架,步驟如下:

1、寫一個基本的程式碼,告訴有哪些方法。

2、然後服務端整合,

3、客戶端整合,

4、OK呼叫生效了。

感覺有點像TCP在傳輸資料,從A伺服器傳遞,傳遞類名,方法名,引數,值,然後B伺服器拿到資料,計算結果,然後把資料在回傳給A。。。這樣理解一下的話,就很簡單了。

下面動手寫一個吧。

自己動手:
  服務端:

既然服務端是實現的地方,我們寫一個算是實現類的方法試試:寫了一個介面和一個實現,為了演示效果,寫了兩個方法。

   public interface IMyTestService
    {
        int calc(int x, int y);

        bool login(string name, string pwd);
    }

    public class MyTestServiceImpl : IMyTestService
    {
        public int calc(int x, int y)
{ return x + y; } public bool login(string name, string pwd) { if (name == "test" && pwd == "123456") { return true; } return false; } }

OK,服務端的大部分完成了。

然後就是TCP伺服器,TCP伺服器對大家來說,就太簡單不過了,不就是建立一個Socket物件,繫結一個埠,獲取客戶端請求的Socket物件,然後和他互動麼。沒啥多說的。

class Program
{
static void Main(string[] args)
{
Socket server = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
server.Bind(new IPEndPoint(IPAddress.Any, 10000));
server.Listen(1000);

    Thread t = new Thread(Execute);
    t.IsBackground = true;
    t.Start(server);

    Console.WriteLine("rpc伺服器已啟動");
    Console.ReadLine();
}

private static void Execute(Object obj)
{
    Socket server = obj as Socket;
    while (true)
    {
        Socket client = server.Accept();

        Thread t = new Thread(SingleExecute);
        t.IsBackground = true;
        t.Start(client);
    }
}

private static void SingleExecute(object obj)
{
    // 讀取 
    Socket client = obj as Socket;

    byte[] buffer = new byte[8192];
    int count = client.Receive(buffer);
    if (count > 0)
    {
        var data = ServiceHelpercs.Handle(buffer);
        client.Send(data);
    }

    client.Shutdown(SocketShutdown.Both);
}

}

我們假定,所有的客戶端資料,可以在一個請求包裡面解析掉,因為如果一次的資料接收不能解析,那就還要新增一個大小了,客戶端要告訴我你給我了多少訊息,然後我再讀取指定資料,拿到所有的內容

這裡建立,一個ServiceHelpers來幫助對,真實演算法的呼叫。如下:

public class ServiceHelpercs
{
public static byte[] Handle(byte[] buffer)
{
MemoryStream ms = new MemoryStream(buffer);
BinaryReader br = new BinaryReader(ms);

    int inter_len = br.ReadByte();
    string inter_name = Encoding.UTF8.GetString(br.ReadBytes(inter_len));

    int method_len = br.ReadByte();
    string method_name = Encoding.UTF8.GetString(br.ReadBytes(method_len));

    int args_length = br.ReadByte();
    int return_type = br.ReadByte();

    List<object> list = new List<object>();
    for (int i = 0; i < args_length; i++)
    {

// 0:void 忽略 1:int 2:bool 3:string
int arg_type = br.ReadByte();
if (arg_type == 1)
{
byte[] values = br.ReadBytes(4);
list.Add(bytes2int(values));
}
else if (arg_type == 2)
{
bool value = br.ReadByte() == 1;
list.Add(value);
}
else if (arg_type == 3)
{
int str_len = bytes2int(br.ReadBytes(4));
string str = Encoding.UTF8.GetString(br.ReadBytes(str_len));
list.Add(str);
}
}

    Type inter_type = null;
    var types = Assembly.GetExecutingAssembly().GetTypes();
    foreach (var type in types)
    {
        var ts = type.GetInterfaces();
        foreach (var t in ts)
        {
            if (t.Name == inter_name)
            {
                inter_type = type;
                break;
            }
        }
    }

    MethodInfo invokeMethod = null;
    if (inter_type != null)
    {
        var methods = inter_type.GetMethods();
        foreach (var method in methods)
        {
            if (method.Name == method_name)
            {
                invokeMethod = method;
                break;
            }
        }
    }

    if (invokeMethod != null)
    {
        Object thisObj = Activator.CreateInstance(inter_type);
        object result = invokeMethod.Invoke(thisObj, list.ToArray());
        if (return_type == 1)
        {
            int value = Convert.ToInt32(result);

            return int2bytes(value);
        }
        else if (return_type == 2)
        {
            return new byte[1] { Convert.ToBoolean(result) ? (byte)1 : (byte)0 };
        }
        else if (return_type == 2)
        {
            List<byte> result_data = new List<byte>();
            var str = (result == null ? "" : result.ToString());
            var data = Encoding.UTF8.GetBytes(str);

            result_data.AddRange(int2bytes(data.Length));
            result_data.AddRange(data);

            return result_data.ToArray();
        }
    }

    return new byte[1] { 0xFF };
}

public static byte[] int2bytes(int len)
{
    byte[] data_len = new byte[4];
    data_len[0] = (byte)((len >> 8 * 3) & 0xFF);
    data_len[1] = (byte)((len >> 8 * 2) & 0xFF);
    data_len[2] = (byte)((len >> 8 * 1) & 0xFF);
    data_len[3] = (byte)(len & 0xFF);

    return data_len;
}

public static int bytes2int(byte[] buffer)
{
    int value = 0;
    value += (int)(buffer[0] << (8 * 3));
    value += (int)(buffer[1] << (8 * 2));
    value += (int)(buffer[2] << (8 * 1));
    value += (int)(buffer[3]);

    return value;
}

}

複製程式碼
  解析的類很簡單,因為這裡建立的資料結構很簡單。

在這裡插入圖片描述

按照我們的約定,這裡,對資料按照我定義的方式來進行解包即可。

伺服器就完成了,是不是很簡單。當然客戶端也需要按照一樣的方式處理打包即可

客戶端:

客戶端就很簡單了,只需要連線到伺服器,通過我們自動生成的程式碼(這裡沒有寫自動生成,就手動了),然後就直接可以返回結果了

複製程式碼
class Program
{
static void Main(string[] args)
{
IMyService service = new MyServiceProxy();
DateTime startTime = DateTime.Now;
int result = service.add(123, 321);

        int min_seconds = (int)(DateTime.Now - startTime).TotalMilliseconds;

        Console.WriteLine(result + " 耗時 " + min_seconds);
        Console.ReadLine();
    }
}

複製程式碼
  上面直接呼叫了,介面,至於介面的實現,這裡的步驟就三個:1、構造需要請求的資料,2、連線伺服器併發送資料,3、接收返回內容,並解析結果。

複製程式碼
public class MyServiceProxy : IMyService
{
public int add(int x, int y)
{
List argList = new List();
argList.Add(new ArgInfo(TypeEnu.Int, x));
argList.Add(new ArgInfo(TypeEnu.Int, y));

        byte[] send_data = create_send_package("IMyService", "add", 2, TypeEnu.Int, argList);
        Socket client = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
        client.Connect(new IPEndPoint(IPAddress.Parse("192.168.0.105"), 10000));
        client.Send(send_data);

        byte[] buffer = new byte[4];
        int count = client.Receive(buffer);
        if (count > 0)
        {
            return bytes2int(buffer);
        }

        throw new Exception("系統異常");
    }

    public bool login(string name, string pwd)
    {
        List<ArgInfo> argList = new List<ArgInfo>();
        argList.Add(new ArgInfo(TypeEnu.String, name));
        argList.Add(new ArgInfo(TypeEnu.String, pwd));

        byte[] send_data = create_send_package("IMyService", "login", 2, TypeEnu.Bool, argList);
        Socket client = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
        client.Connect(new IPEndPoint(IPAddress.Parse("192.168.0.105"), 10000));
        client.Send(send_data);

        byte[] buffer = new byte[1];
        int count = client.Receive(buffer);
        if (count > 0)
        {
            return buffer[0] == 1;
        }

        throw new Exception("系統異常");
    }

    private byte[] create_send_package(string inter_name, string method_name, int arg_length, TypeEnu return_type, List<ArgInfo> argList)
    {
        List<byte> list = new List<byte>();
        list.Add((byte)inter_name.Length);
        list.AddRange(Encoding.UTF8.GetBytes(inter_name));

        list.Add((byte)method_name.Length);
        list.AddRange(Encoding.UTF8.GetBytes(method_name));

        list.Add((byte)arg_length);
        list.Add((byte)return_type);

        foreach (var arg in argList)
        {
            list.Add((byte)arg.type);
            if (arg.type == TypeEnu.Int)
            {
                list.AddRange(int2bytes(Convert.ToInt32(arg.value)));
            }
            else if (arg.type == TypeEnu.Bool)
            {
                bool value = Convert.ToBoolean(arg.value);
                list.Add(value ? (byte)1 : (byte)0);
            }
            else if (arg.type == TypeEnu.String)
            {
                string value = arg.value.ToString();
                list.AddRange(int2bytes(value.Length));
                list.AddRange(Encoding.UTF8.GetBytes(value));
            }
        }

        return list.ToArray();
    }

    public byte[] int2bytes(int len)
    {
        byte[] data_len = new byte[4];
        data_len[0] = (byte)((len >> 8 * 3) & 0xFF);
        data_len[1] = (byte)((len >> 8 * 2) & 0xFF);
        data_len[2] = (byte)((len >> 8 * 1) & 0xFF);
        data_len[3] = (byte)(len & 0xFF);

        return data_len;
    }

    public int bytes2int(byte[] buffer)
    {
        int value = 0;
        value += (int)(buffer[0] << (8 * 3));
        value += (int)(buffer[1] << (8 * 2));
        value += (int)(buffer[2] << (8 * 1));
        value += (int)(buffer[3]);

        return value;
    }
}

public class ArgInfo
{
    public TypeEnu type { get; set; }

    public object value { get; set; }

    public ArgInfo(TypeEnu type, object value)
    {
        this.type = type;
        this.value = value;
    }
}

public enum TypeEnu
{
    Void = 0,
    Int = 1,
    Bool = 2,
    String = 3
}

介面的定義沿用服務端的即可。說明一點:MyServiceProxy這個類,這裡我是手寫的,真實的環境,這個類,應該是由我們定義的某種格式,然後寫一個程式碼生成器,讓他自動生成,然後就可以不用費力,相容所有的呼叫了,

當然這裡只支援了四種類型,我們還可以擴充更多型別,只需要找到傳遞資料的方式即可。譬如一種物件,我們不知道如何傳遞,可以直接把物件定義成一個json字串,或者序列化成二進位制,只要兩端,都知道了這個型別,就可以了。

相當於設計模式裡面的(約定大於配置了)

知識點梳理
  這裡有一些知識點,是不常用的,這裡梳理出來了。

1、MemoryStream ms = new MemoryStream(buffer); BinaryReader br = new BinaryReader(ms); 通過binaryReader的方式,可以像C/C++指標一樣取資料

2、var types = Assembly.GetExecutingAssembly().GetTypes(); 通過Assembly可以得到當前exe或者dll的所有型別(類介面都是一種型別)

3、Object thisObj = Activator.CreateInstance(inter_type); 通過Activator呼叫預設構造,實現物件的初始化

總結:
  這樣一個rpc框架,本身並沒有優化,還有很多地方是可以優化的,比如:快取(不用每次遍歷查詢型別等),udp支援(這裡僅僅只是對tcp進行了支援),

自動程式碼生成(定義一種規範和支援程式,進行支援),錯誤c#教程重試,資料唯一性,資料包的大小處理,等等,所以想要開發一個易用的框架,還需要不斷演進,這裡只是對他的原理進行了簡單剖析。

最後還原大家拍磚。。。。。動起來

程式碼:git:https://github.com/supperlitt/tcp_all