你曾經需要在同一臺機器的兩個.net應用程序間進行數據交換嗎?例如,一個web站點和一個windows服務?.net框架提供了幾種好的選擇來完成進程間通信(ipc):web service,remoting。最快的是remoting,因為它使用tcp通道和二進制格式。
然而,如果需要頻繁地從一個應用程序調用另外一個應用程序,并且你主要關心的是性能,remoting還是顯得慢了一點。讓remoting變慢的,不是協議,而是序列化。
通常來說,remoting是很不錯的,但如果僅限于本地機器的兩個進程間相互通信,其處理機制增加了不必要的開銷。所以要考慮一些別的選擇,比較好的是命名管道(named pipes),不會進行二進制序列化,所以提供了更快的ipc。
要記住,這個解決方案最有效的使用是在一個應用程序需要和另一個應用程序進行非常頻繁的、短文本的消息通信的情況下,并且是在同一臺機器或在同一局域網內部。對于結構化的數據交換,這些文本消息也可以是xml文檔或序列化的.net對象。通信時沒有安全層,因為命名管道最多只能在局域網中運行,所以假定安全問題由別的層進行處理。
一、實現命名管道
以下是.net命名管道解決方案中幾個主要的類。
. namedpipenative:這個類和kernal32.dll聯系實現命名管道的通信,其中包含一些常用方法和常量。
. namedpipewrapper :這個類是namedpipenative的一個包裝。
. apipeconnection:這是一個抽象類,定義了命名管道連接、讀、寫數據的方法。這個類是從clientpipeconnection 和serverpipeconnection 繼承的,分別在客戶端和服務器端應用程序中使用。
. clientpipeconnection:被客戶端應用程序使用,使用命名管道和服務器通信。
. serverpipeconnection:允許命名管道服務器創建連接,和客戶端進行通信。
. pipehandle:保存操作系統的本地句柄,以及管道連接的當前狀態。
了解上述的類之后,需要了解一下命名管道的操作。
二、創建一個服務器端命名管道
服務器端管道名的語法是://./pipe/pipename。“pipename”.. 部分是管道的具體名字。要連接管道,客戶端應用程序需要創建一個同樣名稱的客戶端命名管道。如果客戶端在不同的機器上,服務器端管道的名稱應該是//server/pipe/pipename。下面的代碼是namedpipewrapper的一個靜態方法,被用來實例化一個服務器端命名管道。
public static pipehandle create(string name,uintoutbuffer, uintinbuffer){
name = @"/./pipe/" + name;
pipehandle handle = new pipehandle();
for(inti=1;i<=attempts;i++){
handle.state=interprocessconnectionstate.creating;
handle.handle = namedpipenative.createnamedpipe( name,
namedpipenative.pipe_access_duplex,
namedpipenative.pipe_type_message |
namedpipenative.pipe_readmode_message |
namedpipenative.pipe_wait,
namedpipenative.pipe_unlimited_instances,
outbuffer,
inbuffer,
namedpipenative.nmpwait_wait_forever,
intptr.zero);
if(handle.handle.toint32()!=namedpipenative.invalid_handle_value){
handle.state=interprocessconnectionstate.created;
break;
}
if (i >= attempts) {
handle.state = interprocessconnectionstate.error;
throw new namedpipeioexception("error creating named
pipe"+name+".internalerror:"+namedpipenative.getlasterror().tostring(),namedpipenative.getlasterror());
}
}
returnhandle;
}
通過調用namedpipenative.createnamedpipe方法,上面的方法創建了一個雙方互通的命名管道,并且指定管道可以有無限制的實例。常量的名稱都是英語,不難看懂,就不一一解釋了。
假定服務器端命名管道創建成功,它就可以開始監聽客戶端連接了。
三、連接到客戶端管道
命名管道服務器需要設置成監聽狀態,以使客戶端管道能夠連接它。這可以由調用namedpipenative.connectnamedpipe方法完成。
調用namedpipenative.createfile方法,就可以創建一個命名管道客戶端,并且連接到一個監聽的服務器管道。下面的代碼是namedpipewrapper.connecttopipe的一部分,可以闡釋這一點。
public static pipehandle connecttopipe(string pipename, string servername) {
pipehandle handle = new pipehandle();
//buildthename ofthe pipe.
string name = @"/" + servername + @"/pipe/" + pipename;
for(inti=1;i<=attempts;i++){
handle.state = interprocessconnectionstate.connectingtoserver;
// try to connect to the server
handle.handle = namedpipenative.createfile(name, namedpipenative.generic_read | namedpipenative.
generic_write, 0,null,namedpipenative.open_existing,0,0);
在創建一個pipehandle對象并建立管道名稱后,我們調用namedpipenative.createfile方法來創建一個客戶端命名管道,并連接到指定的服務器端管道。在我們的例子中,客戶端管道被配置為可讀可寫的。
如果客戶端管道被成功創建,namedpipenative.createfile方法返回其對應的本地句柄,這在以后的操作中會用到。如果由于某種原因創建失敗,方法會返回1, 并把namedpipenative設為invalid_handle_value常量。
在客戶端命名管道可以用來讀和寫之前,還要做一件事情。我們需要把handle 設為pipe_readmode_message。可以調用namedpipenative.setnamed-pipehandlestate 實現。
if (handle.handle.toint32() != namedpipenative.invalid_handle_value){
// the client managed to connect to the server pipe
handle.state = interprocessconnectionstate.
connectedtoserver;
// set the read mode of the pipe channel
uint mode = namedpipenative.pipe_readmode_message;
if(namedpipenative.setnamedpipehandlestate(handle.handle,refmode,intptr.zero,intptr.zero)){
break;
}
每個客戶端管道和一個服務器管道的實例通信。若服務器端的實例達到最大數目,創建客戶端管道會失敗。
四、讀寫數據
從命名管道讀數據時我們不能提前知道消息的長度。我們的解決方案不需要處理很長的消息,所以使用system.int32變量來指定消息的長度。
namedpipewrapper.writebytes 方法可以將消息寫到一個命名管道,消息按utf8編碼,然后按字節數組傳遞。
public static void writebytes(pipehandle handle, byte[]bytes) {
byte[] numreadwritten = new byte[4];
uint len;
if(bytes==null){
bytes=newbyte[0];
}
if (bytes.length == 0) {
bytes = new byte[1];
bytes = system.text.encoding.utf8.getbytes(" ");
}
// 獲取消息的長度:
len= (uint)bytes.length;
handle.state = interprocessconnectionstate.writing;
// 獲取消息長度的字節表示,先寫這四字節
if(namedpipenative.writefile(handle.handle,bitconverter.getbytes(len),4,numreadwritten,0)){
// 寫余下的消息
if(!namedpipenative.writefile(handle.handle,bytes,len,numreadwritten,0)){
handle.state=interprocessconnectionstate.error;
thrownewnamedpipeioexception("errorwritingtopipe. internalerror:"+namedpipenative.getlasterror().tostring(), namedpipenative.getlasterror());
}
}
else{
handle.state=interprocessconnectionstate.error;
thrownewnamedpipeioexception("errorwritingtopipe.internalerror:"+namedpipenative.getlasterror().tostring(),
namedpipenative.getlasterror());
}
handle.state =interprocessconnectionstate.flushing;
// 激活管道,保證任何緩存數據都被寫入管道,不會丟失:
flush(handle);
handle.state = interprocessconnectionstate.flusheddata;
}
要從一個命名管道讀數據,先要把前四個字節轉化為整數以確定消息的長度。接著,就可以讀余下的數據了,請看下面的namedpipewrapper.readbytes方法。
public static byte[] readbytes(pipehandle handle, int maxbytes) {
byte[]numreadwritten=newbyte[4];
byte[]intbytes=newbyte[4];
byte[]msgbytes=null;
intlen;
handle.state=interprocessconnectionstate.reading;
handle.state=interprocessconnectionstate.flushing;
// 讀前四個字節并轉化為整數:
if(namedpipenative.readfile(handle.handle, intbytes,4, numreadwritten, 0)) {
len=bitconverter.toint32(intbytes,0);
msgbytes=newbyte[len];
handle.state=interprocessconnectionstate.flushing;
// 讀余下的數據或拋出異常:
if(!namedpipenative.readfile(handle.handle,msgbytes,(uint) len,numreadwritten,0)){
handle.state=interprocessconnectionstate.error;
thrownewnamedpipeioexception("error readingfrompipe. internalerror:"+namedpipenative.getlasterror().tostring(), namedpipenative.getlasterror());
}
}
else {
handle.state=interprocessconnectionstate.error;
thrownewnamedpipeioexception("errorreadingfrompipe. internalerror:"+namedpipenative.getlasterror().tostring(), namedpipenative.getlasterror());
}
handle.state=interprocessconnectionstate.readdata;
if(len>maxbytes){
returnnull; }
returnmsgbytes;
}
以上就是命名管道的實現和一些主要的方法,下面介紹如何創建進行文本消息通信的命名管道服務器和客戶端應用程序。
五、創建命名管道服務器
命名管道服務器是一個多線程的引擎,用來為并發的請求服務,創建新的線程和管道連接。
appmodule.namedpipes assembly包含了一個基類apipeconnection,是對普通命名管道操作的封裝,例如創建管道、讀寫數據等等,這是一個抽象類。
另外,有兩個從apipeconnection繼承的管道連接類clientpipeconnection 和 serverpipeconnection。它們重載了一些方法(例如連接和關閉)并為服務器和客戶端命名管道分別提供實現。clientpipeconnection 和serverpipeconnection都有調用dispose方法的析構器,
清除非管控的資源。
命名管道服務器負責創建命名管道,處理客戶端連接。有兩個主要的類提供了服務功能: servernamedpipe和pipemanager。
(1)servernamedpipe類
其構造器如下:..
internal servernamedpipe(stringname, uint outbuffer,uintinbuffer,intmaxreadbytes){
pipeconnection=newserverpipeconnection(name,outbuffer,inbuffer,maxreadbytes);
pipethread=newthread(newthreadstart(pipelistener));
pipethread.isbackground=true;
pipethread.name ="pipethread "+this.pipeconnection.nativehandle.tostring();
lastaction=datetime.now;
}
構造器創建了一個新的serverpipeconnection實例,并調用pipelistener方法。隨后的主要部分是循環監聽客戶端連接,以及讀寫數據。
private void pipelistener() {
checkifdisposed();
try{
listen=form1.pipemanager.listen;
form1.activityref.appendtext("pipe"+this.pipeconnection.nativehandle.tostring() + ": new pipe started" + environment.newline);
while(listen){
lastaction=datetime.now;
// 從客戶端管道讀取數據:
stringrequest=pipeconnection.read();
lastaction=datetime.now;
if(request.trim()!=""){
//pipemanager.handlerequest 方法接受客戶端請求處理之,
// 然后進行響應,這個響應接著就被寫入管道。
pipeconnection.write(form1.pipemanager.handlerequest(request));
form1.activityref.appendtext("pipe"+this.pipeconnection.nativehandle.tostring()+ ":requesthandled"+environment.newline);
}
else{ pipeconnection.write("error:badrequest");}
lastaction=datetime.now;
// 從客戶端管道斷開連接
pipeconnection.disconnect();
if(listen){
form1.activityref.appendtext("pipe"+this.pipeconnection. nativehandle.tostring()+":listening"+environment.newline);
// 開始監聽一個新的連接:
connect(); }
form1.pipemanager.wakeup();
}
}
catch(system.threading.threadabortexceptionex){}
catch(system.threading.threadstateexceptionex){}
catch(exceptionex){
//logexception
}
finally{
this.close();}
}
請注意不要關閉服務器管道,因為創建一個服務器管道是一個相對昂貴的操作,會引起比較昂貴的開銷。
(2)pipemanager 類
pipemanager 類負責在必要的時候創建服務器管道,管理線程,并生成客戶端請求的響應。下面代碼中initialize方法調用start方法創建一個新的線程:
public void initialize() {
pipes=hashtable.synchronized(_pipes);
mre =newmanualresetevent(false);
mainthread =newthread(newthreadstart(start));
mainthread.isbackground=true;
mainthread.name = "mainpipethread";
mainthread.start();
thread.sleep(1000);
}
pipemanager類只在獲得請求的時候才創建新的管道連接和線程。這意味著serverpipeconnection對象只在沒有連接存在或所有連接都忙于響應請求的時候才被創建。通常2-3個命名管道實例就能處理很大負載的并發客戶端請求,但這個主要取決于處理客戶端請求和生
成響應的時間。
創建serverpipeconnection對象的引用被保存在管道哈希表中。
private void start() {
try{
while(_listen){
int[]keys=newint[pipes.keys.count];
pipes.keys.copyto(keys,0);
// 循環檢驗serverpipeconnection 對象是否還是可用:
foreach(intkeyinkeys){
servernamedpipeserverpipe=(servernamedpipe)pipes[key];
if(serverpipe!=null&& datetime.now.subtract(serverpipe.lastaction).milliseconds>
pipe_max_stuffed_time && serverpipe.pipeconnection.getstate()!=interprocessconnectionstate.waitingforclient){
serverpipe.listen=false;
serverpipe.pipethread.abort();
removeserverchannel(serverpipe.pipeconnection.nativehandle);
}
}
//numberpipes 字段包含了可以在服務器上擁有的命名管道最大數目
if(numchannels<=numberpipes){
servernamedpipe pipe = new servernamedpipe(pipename,outbuffer,inbuffer,max_read_bytes);
try{
//connect 方法將新生成的管道置為監聽模式。
pipe.connect();
pipe.lastaction=datetime.now;
system.threading.interlocked.increment(refnumchannels);
// 開始serverpipeconnection 線程
pipe.start();
pipes.add(pipe.pipeconnection.nativehandle,pipe);
}
catch (interprocessioexception ex) {
removeserverchannel(pipe.pipeconnection.nativehandle);
pipe.dispose();
}
}
else{ mre.reset(); mre.waitone(1000,false); }
}
}
catch { //logexception }
}
六、創建客戶端管道連接
要使用命名管道把一個客戶端應用程序連接到服務器,我們必須創建clientpipeconnection類的一個實例,使用它的方法來讀寫數據。
iinterprocessconnectionclientconnection=null;
try{
clientconnection=newclientpipeconnection("mypipe",".");
clientconnection.connect();
clientconnection.write(textbox1.text);
clientconnection.close();
}
catch{
clientconnection.dispose();
}
管道名稱“mypipe” 必須和服務器管道的名稱一樣,如果命名管道服務器也在同一臺機器上,clientpipeconnection構造器的第二個參數應該是“.”。如果不在同一臺機器上,第二個參數就是服務器的網絡名稱。
以上,我介紹了命名管道的解決方案,我再重申一下,命名管道最有效的使用是在一個應用程序需要和另一個應用程序進行非常頻繁的,短文本的消息通信的情況下,并且是在同一臺機器或在局域網內部。如果您遇到了這樣的情況,希望我的這些代碼能給你啟發和參考。