平時都是在網上找水喝,今天我也當回打井人。上週在搞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端通信並區分數據來源。