你曾经需要在同一台机器的两个.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构造器的第二个参数应该是“.”。如果不在同一台机器上,第二个参数就是服务器的网络名称。
以上,我介绍了命名管道的解决方案,我再重申一下,命名管道最有效的使用是在一个应用程序需要和另一个应用程序进行非常频繁的,短文本的消息通信的情况下,并且是在同一台机器或在局域网内部。如果您遇到了这样的情况,希望我的这些代码能给你启发和参考。