平时都是在网上找水喝,今天我也当回打井人。上周在搞IPC通讯的项目,一个星期下来,觉得还是有些收获的,在此总结一下开发中我遇到的问题,希望能为其他人提供一些经验。
首先说说IPC通讯有什么特点。第一,就是IPC只能用于同一台PC的多个进程间的通讯,这个是一个前提,如果想在多台电脑上进行通讯,那还是老老实实的用socket或管道来做。第二,简单。相比于socket,IPC通讯的准备工作很少。server端建立一个有名字的IPC通道,注册通道实例,再给这个通道指定一个共享数据类型和对应的实例,就算OK啦,够简单吧。client端也是一样的道理,首先建立一个管道,注册,引用通道的共享数据实例,现在就可以通讯了。第三,由于仅限于PC内部进程间的通讯,所以不需要绑定端口,并且通讯速度和稳定性都很好。速度和稳定性都很好理解,一台机器,都是人民内部矛盾,所以处理起来基本没问题。不需要绑定到硬件资源就可以避免硬件资源的冲突,想象一下,如果用socket的话就要绑定一个监听端口,如果是多个实例在一台机器上运行,这个端口该怎么分配,就是一个比较麻烦问题啦。
再说说IPC的通讯载体,共享数据实例。我们可以想象成是sever和client共用的一个盒子,这个盒子的格局由数据类型确定,需要传递什么数据,都可以通过声明共享数据类型来确定。这个里面有个事情要注意,就是共享数据的实例是在server或client一端建立的,所以用来传数据是可以的,但是不能用来传递数据地址。因为这个共享数据只能在一个进程中被创建,在另个进程中引用,如果进程A把自己的地址传给进程B,进程B就会非常郁闷,因为进程B没有这个地址。这就好像一个超市里有1到100号储物箱,但是一个人打开储物箱发现让他到200号箱里去取东西是一样的。
基本的东西说完了,现在看看具体的实现,限于篇幅关系,只贴基本的东西,其他的内容可以看附件中的代码:
共享数据模块:
public class DataOperator : MarshalByRefObject
{
// Data
private string InformationStr = string.Empty;
// event
public event ClientHasConnectedHandler OnConnectedEvent = null;
public event WriteOptionFinishedHandler OnServerWriteEvent = null;
public event WriteOptionFinishedHandler OnClientWriteEvent = null;
public event ClientHasConnectedHandler OnDisconnectedEvent = null;
// client call this function to implement connected event
public void SendConnectEventByClient(string ipcName){
try{
if (OnConnectedEvent != null)
OnConnectedEvent(ipcName);
}
catch(Exception ex)
{
IPCLog.WriteLog(string.Format("error msg is {0}, DTid = {1}", ex.Message, id));
}
}
#region Buffer Operator about
public bool Writeflag{
get{
if (InformationStr == string.Empty) return true;
else return false;
}
}
public string Write{
set {
InformationStr = string.Empty;
InformationStr = value;}
}
public string Read{
get{
string tmpRes = InformationStr;
InformationStr = string.Empty;
return tmpRes;}
}
#endregion
}
共享数据类中主要包括两个部分,一是用来传递数据的InformationStr,一是用来传递控制状态的事件。前面说过要想用IPC传递数据是非常容易的,但是我们不能总是循环去判断数据段里是不是有数据,所以需要用事件进行控制。以DataOperator 类中的OnConnectedEvent为例,在server端绑定该事件,在client端调用SendConnectEventByClient方法,server端就知道,有一个client建立了连接。
再看server端的处理:
BinaryServerFormatterSinkProvider serverProvider =
new BinaryServerFormatterSinkProvider();
serverProvider.TypeFilterLevel = System.Runtime.Serialization.Formatters.TypeFilterLevel.Full;
IDictionary props = new Hashtable();
props["name"] = m_ipcName;
props["portName"] = m_ipcName;
props["typeFilterLevel"] = TypeFilterLevel.Full;
// create a server channel
svrChl = new IpcServerChannel(props, serverProvider, null);
ChannelServices.RegisterChannel(svrChl);
m_optDt = new DataOperator(m_ipcName);
objRef = RemotingServices.Marshal(m_optDt, m_ipcName + "_DataOperator");
m_optDt.OnConnectedEvent += new ClientHasConnectedHandler(ClientHasConnected);
这里解释一下,serverProvider实例用来设定序列化格式,由于我们需要在共享数据中增加事件类型,所以需要重指定序列化等级。props用来设定通道信息,如果没有特殊的要求可以直接使用IpcServerChannel的构造函数来设定通道信息。RemotingServices.Marshal方法用来把m_optDt建立一个引用名,这样,其他的进程就可以通过这个名字引用到server建立的这个实例。
最后client端:
// client channel
IDictionary props = new Hashtable();
props["name"] = m_ipcName;
props["typeFilterLevel"] = TypeFilterLevel.Full;
props["portName"] = m_ipcName;
// create a new client channel
cltChl = new IpcClientChannel(props, null);
ChannelServices.RegisterChannel(cltChl);
// Get DataOperator instance of server
m_optDt = (DataOperator)Activator.GetObject(typeof(DataOperator), chlName);
m_optDt.SendConnectEventByClient(this.IPCName);
client端相对比较简单,不做过多解释了。现在就可以在通过调用DataOperator的Write和Read属性发信和收信了。当然现在功能只能算是完成了一半,因为现在只能由server监控client的动作,如果需要client也响应server的动作,就需要在做一条通道,只要server端和client端方向调换一下就OK了。
最后再说一下通道的释放。当通道的历史使命完成后,需要把通道释放,当然直接退出程序是可以的。代码如下:
RemotingServices.Disconnect(m_optDt);
// unregister all channels
ChannelServices.UnregisterChannel(svrChl);
代码很简单,就两句话,需要注意的是第一句。RemotingServices.Disconnect用来释放我们绑定的共享数据引用。当我们注销通道之后,共享数据的引用是不会自动消失的,需要手动释放,否则下次再建立连接的时候,RemotingServices.Marshal会毫不客气地告诉你,你指定的名字已经有人用啦。
附件里提供了完整的代码和测试测序,能够实现server和client的双向通信及状态控制,IPC通信模块做了封装,server端可以建立多个通道与不通的client端通信并区分数据来源。