平时都是在网上找水喝,今天我也当回打井人。上周在搞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端通信并区分数据来源。