前言:

之前公司接到项目的需求是使用一台电脑远程控制另外一台电脑,点击操作端电脑的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);}elseDebug.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);}elseDebug.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(){//告知客户端 服务器将关闭channelServerToClient("Server close");UnRegisterChannel();}public InputField serverInput;/// <summary>/// 点击按钮 发送信息,信息内容是场景中Input/// </summary>public void ButtonClickSendToServer(){if (serverInput != null){if (serverInput.text == null || serverInput.text == "")ServerToClient("无内容");elseServerToClient(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);}elseDebug.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(){//告知客户端 服务器将关闭channelClientToServer("client close");UnRegisterChannel();}public InputField clientInput;/// <summary>/// 点击按钮 发送信息,信息内容是场景中Input/// </summary>public void ButtonClickSendToServer() {if (clientInput != null) {if (clientInput.text == null || clientInput.text == "")ClientToServer("无内容");elseClientToServer(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自动分配portchannel = 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

在Unity中使用.Net Remoting实现双向通信相关推荐

  1. Unity中使用WebSocket

    WebSocket是什么? WebSocket协议是为了实现网络客户端和服务器端全双工通信而引入的一种基于消息帧和TCP的通信机制,这个协议本身的目标是为了在http服务器上引入双向通信的机制,从而克 ...

  2. 学习在Unity中创建一个动作RPG游戏

    游戏开发变得简单.使用Unity学习C#并创建您自己的动作角色扮演游戏! 你会学到什么 学习C#,一种现代通用的编程语言. 了解Unity中2D发展的能力. 发展强大的和可移植的解决问题的技能. 了解 ...

  3. 学习用C#在Unity中创建一个2D Metroidvania游戏

    学习用C#在Unity中创建一个2D Metroidvania游戏 你会学到: 构建2D Unity游戏 用C#编程 玩家统计,水平提升,米尔和远程攻击 敌方人工智能系统 制定级别和级别选择 Lear ...

  4. 在Unity中制作4种不同的游戏

    流派:电子学习| MP4 |视频:h264,1280×720 |音频:AAC,48.0 KHz 语言:英语+中英文字幕(根据原英文字幕机译更准确)|大小解压后:8.6 GB 含课程素材 |时长:15h ...

  5. 学会在Unity中创建一个Match-3益智游戏 Learn To Create a Match-3 Puzzle Game in Unity

    MP4 |视频:h264,1280×720 |音频:AAC,44.1 KHz,2 Ch 语言:英语+中英文字幕(根据原英文字幕机译更准确) |时长:48场讲座(6h 38m) |大小解压后:2.8 G ...

  6. Unity中创建本地多人游戏完整案例视频教程 Learn To Create A Local Multiplayer Game In Unity

    Unity中创建本地多人游戏完整案例视频教程 Learn To Create A Local Multiplayer Game In Unity MP4 |视频:h264,1280x720 |音频:A ...

  7. C#开发Unity游戏教程之Unity中方法的参数

    C#开发Unity游戏教程之Unity中方法的参数 Unity的方法的参数 出现在脚本中的方法,无论是在定义的时候,还是使用的时候,后面都跟着一对括号"( )",有意义吗?看起来最 ...

  8. Unity中制作游戏的快照游戏支持玩家拍快照

    Unity中制作游戏的快照游戏支持玩家拍快照 有些游戏支持玩家"拍快照",也就是将游戏的精彩瞬间以图片的形式记录下来的功能.这个功能比较有趣,而且以后的用途也会很广,为此本节打算介 ...

  9. 如何在Unity中添加三维空间声音Spatial Sounds

    hololens的声音模块是头戴环上,靠近耳朵上方的一个红色的模块,三维空间声音感效果还不错,本文主要讲述如何在项目中使用空间声音特性.我们主要讲述必须的插件组件和unity声音组件和属性的设置来确保 ...

  10. unity项目源码_在Unity中使用protobuf

    Protocol Buffers (通常简称为protobuf) 是Google开发的一种格式,这种格式与开发语言无关.与运行平台无关,用于序列化结构数据,并且很容易扩展.这种格式可以用于通信协议.数 ...

最新文章

  1. 【数据结构】所有顶点对的最短路径 Floyd算法
  2. jvm十三:类加载器命名空间
  3. php 获取用户名,PHP如何获取当前windows系统的登录用户名
  4. 华为鸿蒙升级了,鸿蒙系统暂缓升级,但从火爆程度来看,华为基本成了!
  5. 【软件工程第三次作业】
  6. mysql server启动_mysql的启动方式
  7. 整理的一些实用工具网站
  8. Ansible详解(一)——Ansible理论基础
  9. FE File Explorer Pro for mac(文件管理器)
  10. 《物联网开发实战》05 系统实例:怎样设计一个简易物联网系统?(学习笔记)
  11. 云南中医药大学计算机与科学技术,2019年云南科学技术厅-云南中医药大学应用基础-云南中医学院.PDF...
  12. raid卷的作用_raid是什么?为什么要用raid?有好什么好处?
  13. input类型为file时,accept为表格xlsl
  14. pycharm与python的区别_python与pycharm有何区别
  15. Excel数据透视表系列培训--课时2
  16. 微信小程序获取unionID思路整理
  17. 曹则贤:量子力学从来不是什么革命,而是经典物理学的自然延续
  18. 基于STM32的超声波倒车检测装置
  19. 更强大了!开发者演示 iOS 11.3 ARKit 1.5 新增功能
  20. halcon linux qt,Halcon C++混合编程学习之Qt 实现检测焊接点

热门文章

  1. 软件测试(三)--标准的测试用例模板
  2. 手机蓝牙连接51单片机自动开门
  3. 计算机上安装了更新ie版本,安装ie浏览器提示系统有更新的版本怎么办_ie提示有更新版本的解决方法...
  4. VS语音信号处理(4) C语言WAV格式语音存为PCM格式语音工程实例
  5. 文本聚类分析算法_集成聚类系列(三)图聚类算法详解
  6. 程序员常见面试题汇总
  7. EovaJFinal在阿里云K8S实现多环境自动化部署
  8. 【转载】SAP用户出口清单(User Exits)
  9. ML Mastery 博客文章翻译 20220116 更新
  10. ArcGIS Python工具箱集成第三方模块的解决办法