在Unity中使用.Net Remoting实现双向通信

前言:

之前公司接到项目的需求是使用一台电脑远程控制另外一台电脑,点击操作端电脑的Button,使显示端显示指定的画面.这个需求本质上两台电脑之间的相互通信,第一个想到的方案就是开发两个app,一个是服务器,一个是客户端,使用Tcp进行通信.本人没有使用这个方案的原因有以下两个:

1.项目需求只是远程控制,本人使用互联网进行连接有一种杀鸡用牛刀的感觉.

2.本人是个前端程序员,该项目开发周期短,又写服务器又写客户端觉得太麻烦.

联想到在unity中经常使用Unity Remote 以及Unity Console 和移动设备连接,所以想要使用处在同一个局域网内的远程控制方案.于是搜索Remote到了.Net Remoting.最终花费1天时间在unity中实现远程控制,下面进去正题.

什么是.Net Remoting?

在Windows操作系统中,是将应用程序分离为单独的进程(Progress)。这个进程形成了应用程序代码和数据周围的一道边界.如果不采用进程间通信(IPC)机制,则在一个进程中执行的代码就不能访问另一进程。这是一种操作系统对应用程序的保护机制。然而在某些情况下,我们需要跨过应用程序域,与另外的应用程序域进行通信,即穿越边界。Microsoft .NET Remoting 提供了一种允许对象通过应用程序域与另一对象进行交互的框架.

在Remoting中,对于要传递的对象,设计者除了需要了解通道的类型和端口号之外,无需再了解数据包的格式。这既保证了客户端和服务器端有关对象的松散耦合,同时也优化了通信的性能。

 

如何在Unity中安装.Net Remoting?

一般在传统的C#项目中,我们引用某个DLL的时候,都是通过在VS解决方案的引用项目上右键 -> 添加新引用来导入某个DLL.

      当在unity中使用上述方法引入System.Runtime.Remoting后,ChannelServices等类依然报红. 众所周知,Unity的.NET是基于 Mono 的,因为一些原因,导致Mono并不是包含了所有微软原生的.NET库中的内容。也就是说有些你在Winform、WPF等工程中用到的类库并不能完美地在Mono中使用.此时应当直接把dll 拷贝到 "Plugins"目录下,VS就会自动把DLL引用到我们的项目中了.现在我们需要在windows系统中找到System.Runtime.Remoting这个dll,在windows的framework中找到该dll(我的路径是C:\Windows\Microsoft.NET\Framework\v4.0.30319)

 

 

如何使用.Net Remoting?

在Remoting中是通过通道(channel)来实现两个应用程序和域之间对象的通信的。首先,客户端通过Remoting,访问通道以获得服务端对象,再通过代理解析为客户端对象。这就提供一种可能性,即以服务的方式来发布服务器对象。远程对象代码可以运行在服务器上(如服务器激活的对象和客户端激活的对象),然后客户端再通过Remoting连接服务器,获得该服务对象并通过序列化在客户端运行,也就是说 服务器应该先创建对象,客户端才能够获取对象。

读到这里,大家都应该有个疑问:客户端是如何获取到服务器对象的?

前面说到操作系统为了保护应用程序,为应用程序设计了App Domain,一般情况下,应用程序之前是不能通信的,现在我们有了访问别的程序域的需求,所以就需要跨越这个边界,这就依赖MarshalByRefObject类了.

  • MarshalByRefObject:

MarshalByRefObject 是那些通过使用代理交换消息来跨越应用程序域边界进行通信的对象的基类。

不是从 MarshalByRefObject 继承的对象会以隐式方式按值封送。

当远程应用程序引用一个按值封送的对象时,将跨越远程处理边界传递该对象的副本。

因为您希望使用代理方法而不是副本方法进行通信,因此需要继承MarshalByRefObject
 
在Remoting中能够传递的远程对象可以是各种类型,包括复杂的DataSet对象,只要它能够被序列化。远程对象也可以包含事件,但服务器端对于事件的处理比较特殊。

服务器创建成功MarshalByRefObject 的子类之后,由ObjRef类来序列化MarshalByRefObject运输它.

  • ObjRef:

ObjRef是扩展MarshalByRefObject (MBR) 的对象的可序列化表示形式。 ObjRef用于AppDomain跨边界传输对象引用。封送类实现MarshalByRefObject 后, 表示它ObjRef的将通过通道传输到另一个应用程序域中, 可能是在另一个进程或计算机中。 当在ObjRef目标应用程序域中对进行反序列化 时, 将对其进行分析, 以创建远程 MBR 对象的透明代理。 此操作称为 "取消封送"。

    如果将MarshalByRefObject 比作”快递”,ObjRef比作”快递公司”,快递公司对快递包装并运输,这是相当不合适的.因为ObjRef只是将MarshalByRefObject 反序列化一份. 由客户端获取后在客户端本地生成的一个对象,学名叫做透明代理,服务端存在的MarshalByRefObject 则叫真实代理.透明代理是一个对象, 该对象提供实际对象驻留在客户端空间中的假象。 它通过使用远程处理基础结构将对其进行的调用转发到实际对象来实现此目标。 透明代理(客户端)是本身的真实代理(服务端)理托管运行时类的实例驻留。客户端通过服务器端的ObjRef调用服务器对象MarshalByRefObject ,所以透明代理需要有真实的实例化和真实的字段属性方法,但是方法体内不需要有内容(即使有内容透明代理也不会走自己的代码),因为它实际调用的还是真实代理的方法(一般情况下跨越程序域的MarshalByRefObject 类在客户端服务端使用的是同一个类,现在为了证明客户端透明代理调用的是真实代理的方法,下文的代码会是两个MarshalByRefObject ,分别存在于客户端服务端)

 

 

接下来是本教程最绕人最不好理解的部分了(卧槽,我看了好几遍文章都没理解作者要表达的意思,希望看到我文章的朋友能快速理解这里)

说到这就已经实现了客户端单向调用服务端代码(客户端调用透明代理,实际调用了真实代理的方法),那服务端如何调用客户端呢?直接将客户端代码绑定到透明代理是不会生效的,因为它根本不会运输到服务器端.客户端调用服务端是通过服务端的MarshalByRefObject (该MarshalByRefObject 实例被ObjRef包装并运输)实现.要实现服务端调用客户端,客户端也要有一个MarshalByRefObject 存放客户端的事件,再通过ObjRef包装并运输. 我们叫它中转站.简单的来说就是客户端服务端位置调换一下 原理图如下:

客户端调用服务器单向通信:

 

 

向通信:

 

 

 

 

  • 最后说一下用于运输的通道Channel:

Remoting的通道主要有两种:Tcp和Http,IChannel 包含TcpChannel,HttpChannel

TcpChannel

Tcp通道提供了基于Socket 的传输工具,使用Tcp协议来跨越Remoting边界传输序列化的消息流。默认使用二进制格式序列化消息对象,具有更高的传输性能。适用局域网。

HttpChannel:

它提供了一种使用 Http协议,使其能在Internet上穿越防火墙传输序列化消息流。HttpChannel类型使用Soap格式序列化消息对象,因此它具有更好的互操作性。适用万维网。

出于安全性考虑,使用channel进行远程处理是有限制的, Channel可以设置反序列化的等级(Low ,Full ,默认是low).如果把typeFilterLevel设为Low,Remoting会反序列化Remoting基本功能相关的对象。而设为Full则意味着Remoting会反序列化所有类.前面说到的MarshalByRefObject就是low level所不支持的类型,如果你想知道那些类型是在Low Level下被限制,请参考http://msdn2.microsoft.com/en-us/library/5dxse167.aspx

下面贴上实际代码:

服务端MarshalByRefObject:

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using System;
using System.Runtime.Remoting;
using System.Runtime.Remoting.Lifetime;

    public delegate void SendMessage(string msg);
    public class ServerObject : MarshalByRefObject
    {
        //在客户端触发,在服务器订阅的事件
        public event SendMessage SubscribeAtServer;
        //在服务器触发,在客户端订阅的事件
        public event SendMessage SubscribeAtClient;
        /// <summary>
        /// 获取当前的命名空间,表示方法在哪里被执行
        /// </summary>
        public void GetNameSpace() {

            Debug.Log("当前的命名空间:" + System.Reflection.MethodBase.GetCurrentMethod().DeclaringType.Namespace); 
        }
        //服务器触发事件
        public void TriggerAtClient(string msg)
        {
            Debug.Log("由真实代理(服务端)执行");
            if (SubscribeAtServer != null)
                SubscribeAtServer(msg);
        }
        //客户端触发事件
        public void TriggerAtServer(string msg)
        {
           
            if (SubscribeAtClient != null) {
                Debug.Log("由真实代理(服务端)执行");
                SubscribeAtClient(msg);
            }else
                Debug.Log("serverObject中没有客户端连入");

        }
        //无限生命周期
        public override object InitializeLifetimeService()
        {

            return null;
        }
    }
public class Swap : MarshalByRefObject
{
    //在服务器触发,在客户端订阅的事件
    public event SendMessage SwapSubscribeAtClient;
    //服务器触发事件
    public void TriggerAtServerSwapEvent(string msg)
    {
        Debug.Log("服务端的swap间接调用这里");
        if (SwapSubscribeAtClient != null)
        {
            SwapSubscribeAtClient(msg);
        }
        else
            Debug.Log("swap中没有客户端接入");

    }

    //无限生命周期
    public override object InitializeLifetimeService()
    {
        return null;
    }
}

服务端Server:

using System;
using System.Runtime.Remoting;
using System.Runtime.Remoting.Channels;
using System.Runtime.Remoting.Channels.Tcp;
using UnityEngine;
using System.Runtime.Serialization.Formatters;
using System.Collections;
using UnityEngine.UI;
namespace NetRemotingServer
{

    public class Server : MonoBehaviour
    {
        //unity声明周期的第一步
        void Awake()
        {
            InitChannel();
            RegisterChannel();
        }
        //OnDestroy 只有 主动退出的时候触发   OnApplicationQuit实测 关闭进程也能触发
        void OnApplicationQuit()
        {
            //告知客户端 服务器将关闭channel
            ServerToClient("Server close");
          
            UnRegisterChannel();
        }
        public InputField serverInput;
        /// <summary>
        /// 点击按钮 发送信息,信息内容是场景中Input
        /// </summary>
        public void ButtonClickSendToServer()
        {
            if (serverInput != null)
            {
                if (serverInput.text == null || serverInput.text == "")
                    ServerToClient("无内容");
                else
                    ServerToClient(serverInput.text);
                serverInput.text = "";
                //清空输入
            }
        }
        TcpChannel channel;
        ServerObject marshal_obj;
        ObjRef objRef;
        /// <summary>
        /// 生成chennel并设置属性
        /// </summary>
        void InitChannel()
        {
            BinaryServerFormatterSinkProvider serverProvider = new BinaryServerFormatterSinkProvider();
            BinaryClientFormatterSinkProvider clientProvider = new BinaryClientFormatterSinkProvider();
            serverProvider.TypeFilterLevel = TypeFilterLevel.Full;//支持所有类型的反序列化,级别很高

            //使用代码更改属性(还可以通过配置文件生成)
            IDictionary tcpProp = new Hashtable();
            tcpProp["name"] = "UnityServerChannel";
            tcpProp["port"] = 9554;

            channel = new TcpChannel(tcpProp, clientProvider, serverProvider);
            

        }
        /// <summary>
        /// 注册Channel,在生成channel和ServerObject后调用
        /// </summary>
        void RegisterChannel()
        {
            //服务器生成远程对象
            marshal_obj = new ServerObject();
            /*注册通道服务端*/
            ChannelServices.RegisterChannel(channel, false);

            //将给定的 MarshalByRefObject 转换为 ObjRef 类的实例,可以将该实例序列化以便在应用程序域之间以及通过网络进行传输
            objRef = RemotingServices.Marshal(marshal_obj, "UnityTestServer");

            //服务器绑定客户端触发的事件
            marshal_obj.SubscribeAtServer += new SendMessage(marshal_obj_SubscribeAtServer);

        }
        /// <summary>
        /// 注销Channel ,生命周期完结时调用或收到客户端
        /// </summary>
        void UnRegisterChannel()
        {
            if (channel != null)
            {
                channel.StopListening(null);
                Debug.Log("关闭服务端信道");
                ChannelServices.UnregisterChannel(channel);
            }
        }
        /// <summary>
        /// 服务端调用客户端代码
        /// </summary>
        /// <param name="content"></param>
        private void ServerToClient(string content)
        {
            if (marshal_obj == null) return;
            Debug.Log("服务器向客户端发送msg:" + content);
            marshal_obj.TriggerAtServer(content);

        }
        /// <summary>
        /// 客户端调用的服务端代码
        /// </summary>
        /// <param name="msg"></param>
        private void marshal_obj_SubscribeAtServer(string msg)
        {
            Debug.Log("收到客户端的msg:" + msg);
           
        }
    }
}

客户端 MarshalByRefObject:

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using System;
using System.Runtime.Remoting;
using System.Runtime.Remoting.Lifetime;


    public delegate void SendMessage(string msg);
    public class ServerObject : MarshalByRefObject
    {
        //在客户端触发,在服务器订阅的事件
        public event SendMessage SubscribeAtServer;
        //在服务器触发,在客户端订阅的事件
        public event SendMessage SubscribeAtClient;
        /// <summary>
        /// 获取当前的命名空间,表示方法在哪里被执行
        /// </summary>
        public void GetNameSpace()
        {

            Debug.Log("当前的命名空间:" + System.Reflection.MethodBase.GetCurrentMethod().DeclaringType.Namespace);
        }
        //服务器触发事件
        public void TriggerAtClient(string msg)
        {
        //实测 客户端不会进入此方法,会直接进入服务端的方法体,此方法体内不需要存在任何内容
            Debug.Log("由透明代理(客户端)执行");
            if (SubscribeAtServer != null)
                SubscribeAtServer(msg);
        }
        //客户端触发事件
        public void TriggerAtServer(string msg)
        {
//实测 客户端不会进入此方法,会直接进入服务端的方法体,此方法体内不需要存在任何内容
            Debug.Log("由透明代理(客户端)执行");
            if (SubscribeAtClient != null)
                SubscribeAtClient(msg);
        }
        //无限生命周期
        public override object InitializeLifetimeService()
        {

            return null;
        }
    }


    public class Swap : MarshalByRefObject
    {
        //在服务器触发,在客户端订阅的事件
        public event SendMessage SwapSubscribeAtClient;
        //服务器触发事件
        public void TriggerAtServerSwapEvent(string msg)
        {
            Debug.Log("客户端的swap间接调用这里");
            if (SwapSubscribeAtClient != null)
            {
                SwapSubscribeAtClient(msg);
            }
            else
                Debug.Log("swap中没有客户端接入");

        }

        //无限生命周期
        public override object InitializeLifetimeService()
        {
            return null;
        }
    }

客户端 Client:

using System;
using System.Runtime.Remoting;
using System.Runtime.Remoting.Channels;
using System.Runtime.Remoting.Channels.Tcp;
using UnityEngine;
using System.Runtime.Serialization.Formatters;
using System.Collections;
using UnityEngine.UI;
namespace NetRemotingClient
{
    public class Client : MonoBehaviour
    {
      
        //unity声明周期的第一步
        void Awake()
        {
            InitChannel();
            RegisterChannel();
        }
        //OnDestroy 只有 主动退出的时候触发   OnApplicationQuit实测 关闭进程也能触发
        void OnApplicationQuit()
        {
            //告知客户端 服务器将关闭channel
            ClientToServer("client close");
            UnRegisterChannel();
        }
        public InputField clientInput;
        /// <summary>
        /// 点击按钮 发送信息,信息内容是场景中Input
        /// </summary>
        public void ButtonClickSendToServer() {
            if (clientInput != null) {
                if (clientInput.text == null || clientInput.text == "")
                    ClientToServer("无内容");
                else
                    ClientToServer(clientInput.text);
                //清空输入
                clientInput.text = "";
            }
        }
  
        //用于中转透明代理传过来的事件
        Swap swap;
        TcpChannel channel;
        ServerObject serverObj;
        //通过信道名称,ip地址,端口找到指定的程序域,当前本机测试,地址为本机地址(windows 打开cmd.exe,输入ip config)
        string targetIpAddress = "172.28.20.243";
        private void InitChannel()
        {
            BinaryServerFormatterSinkProvider serverProvider = new BinaryServerFormatterSinkProvider();
            BinaryClientFormatterSinkProvider clientProvider = new BinaryClientFormatterSinkProvider();
            serverProvider.TypeFilterLevel = TypeFilterLevel.Full;//支持所有类型的反序列化,级别很高


            IDictionary tcpProp = new Hashtable();
            tcpProp["port"] = 0;//0自动分配port

            channel = new TcpChannel(tcpProp, clientProvider, serverProvider);
        }
        /// <summary>
        /// 注册信道 并获取服务器对象
        /// </summary>
        void RegisterChannel()
        {
            //注意!这里需要服务器先打开并创建了服务器对象 ,客户端才会获取到该对象的透明代理
            try
            {
                ChannelServices.RegisterChannel(channel, false);
                //通过url从服务端获取代理,url组成: channel种类(tcp lpc,http)+服务器ip地址 (指示主机)+服务器的端口(指示应用程序)+applicationname(指定程序域);
                serverObj = (ServerObject)Activator.GetObject(typeof(ServerObject), "tcp://" + targetIpAddress + ":9554/UnityTestServer");

                if (serverObj != null)
                {
                    //订阅服务器事件,使用swap中转
                    swap = new Swap();
                    serverObj.SubscribeAtClient += new SendMessage(swap.TriggerAtServerSwapEvent);
                    //服务器对客户端的调用间接的绑定在obj_SubscribeAtClient上
                    swap.SwapSubscribeAtClient += new SendMessage(obj_SubscribeAtClient);
                  
                }

            }
            catch (Exception e)
            {

                Debug.LogError(e);
            }
        }

        void UnRegisterChannel()
        {
            if (channel != null)
            {
                channel.StopListening(null);
                Debug.Log("关闭客户端信道");
                ChannelServices.UnregisterChannel(channel);
            }
        }
        /// <summary>
        /// 服务器调用客户端
        /// </summary>
        /// <param name="msg"></param>
        private void obj_SubscribeAtClient(string msg)
        {
            Debug.Log("客户端收到服务端的msg:" + msg);
      
        }
       

        /// <summary>
        /// 客户端调用服务器
        /// </summary>
        private void ClientToServer(string msg)
        {
            Debug.Log("客户端向服务器发送msg" + msg);
            serverObj.TriggerAtClient(msg);
        }
    }

}

运行效果如下:

客户端向服务器发送信息:

 

客户端打印:

服务端打印:

结论:说明客户端不会运行自己透明代理中的方法,直接调用了服务器端的MarshalByRefObject的方法.

服务端向客户端回信:

 

服务端打印:

 

客户端打印:

 

还存在的问题:

1.上文中的代码我尽可能的不放入任何与在unity中使用remoting功能不相关的代码.所以断开连接逻辑不完善,各位可以加入自己的断开逻辑.

2.Net Remoting互相通信是使用单独的线程通信的 ,它的方法内不能调用Unity主线程中的方法.解决方案是 创建state变量,在通信方法中改变state变量 在Update中监听变量,实现调用unity中的方法

 

3.在查找资料写这个文章的时候才发现.Net Remoting 都已经是十几年前的技术了...做项目过程中我也在思考.net remoting应该也是不能跨平台的,如果以后有什么移动设备控制pc或者移动设备互相调用的需求,又要寻求别的解决方案.于是我又接着查了下资料.找到了微软一直在更新的 新的跨平台通信框架WCF(Windows Communication Foundation),百度百科:https://baike.baidu.com/item/Wcf/7374854?fr=aladdin 大家如果有需求可以继续深入了解.

 

项目工程(为什么在csdn上传了需要c币才下载...分享个百度云链接):

链接:https://pan.baidu.com/s/1ZVmZB9bD6H62Mr6m4Cx9Xg 
提取码:cuez

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

  • 0
    点赞
  • 2
    收藏
  • 打赏
    打赏
  • 0
    评论

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

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
©️2022 CSDN 皮肤主题:大白 设计师:CSDN官方博客 返回首页
评论

打赏作者

朱大星

你的鼓励将是我创作的最大动力

¥2 ¥4 ¥6 ¥10 ¥20
输入1-500的整数
余额支付 (余额:-- )
扫码支付
扫码支付:¥2
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值