本文共 19194 字,大约阅读时间需要 63 分钟。
因为Windows中每个进程都有自己的虚拟地址空间,所以一个进程无法访问到别一个进程的地址空间,因此相对来说进程间的通信要复杂一些。Windows操作系统为应用程序之间的进程间通信,数据共享提供了很多机制,称为interprocess communications (IPC)。其中一些机制可以用来在同一台计算机内不同进程之间通信,一些可以在网络中不同计算机上运行的进程之间进行通信。 典型的,应用程序可以通过IPC完成服务器与客户端之间的通信。客户端是一个应用程序或者进程通过向服务器发出请求来完成自己的工作,而服务器则是一个应用程序或者进程用来完成客户端的请求。 Windows可以支持的进程间通信机制包括: • Clipboard • COM • Data Copy • DDE • File Mapping • Mailslots • Pipes • RPC • Windows Sockets 参考: 实际上还有一些其它方法也可以在一此特殊情况下来达到进程间通信的目的。 • 参数 • 内核对象 • 环境变量 • … 一、命令行参数 这一次要说的是一种非常简单的方法,命令行参数。有些人说这不能算是一种进程间通信方法,至少在MSDN中进程间通信机制中没有说到这一方法。但是,在一些需要启动子进程时传递简单数据时还是非常有效的。只要达到不同进程间交换数据这一目的的,我都把其当做为一个通信方法吧。这样来看可能我的文章名字起的不是很贴切。但是没关系我只是尽我所能把可能的方法列出来,也许有些方法在真正做项目时不容易想到,如果列在这里,在需要的时候会给我们一些启发。 二、消息机制 这次我们来说一下通过windows消息机制来进行进程间的通信。 1.如果只是简单的传递一个值,可以定义一个自己的消息类型; 2.如果需要传递较复杂的类型,可以使用Data Copy。 说明: WM_USER常量 WM_USER常量是用来给帮助应用程序定义自己的用户消息,常用的形式如: WM_USER + X,其中X 是一个整数。通过这个常量的定义消息的发送方与接收方就可以行成一个协议。 public const int WM_USER = 0x0400; 更多关于WM_USER的信息请参考: 3.WM_COPYDATA 消息 一个应用程序可以通过WM_COPYDATA发送数据给接收方。WM_COPYDATA只可以能过SendMessage发送,如果使用PostMessage,接收方就不会收到发送的消息,因为PostMessage不是同步的调用,这样就无法保证在接收方收到消息的时个WM_COPYDATA中的数据还正确的保存在内存中。 SendMessage的声明:[DllImport("user32.dll")] public static extern IntPtr SendMessage(IntPtr hWnd,int msg, IntPtr wParam, ref COPYDATASTRUCT lParam); COPYDATASTRUCT结构是真正用来传递数据的结构,我们有必要先来看一个这个结构的样子:
参考:
得到接收消息窗口的句柄 在上边SendMessage的方法声明中第一个参数需要一个hWnd(IntPtr)的数据,其实这应该是接收消息的窗口句柄,如何来得到这个句柄呢?这是一个很关键的问题。有两种方法: 1.通过窗体的Title找到窗体,从而得到句柄,我是通过遍历系统中所有窗体的方法来找到窗体,在这过程中用到的API: public delegate bool EnumWindowsProc(IntPtr p_Handle, int p_Param);[DllImport("user32.dll")] public static extern int EnumWindows(EnumWindowsProc ewp, int lParam); [DllImport("user32.dll")] public static extern int GetWindowText(IntPtr hWnd,StringBuilder title, int size); 部分代码:
2.通过进程名字来得到进程主窗体的句柄。 在C#是这个实现起来比较简单,但是有一点需要注意的是:Visual Studio在调试应用程序和真正运行应用程序的时候进程的名字不一样。 具体代码:
思考:到现在,发送方已经可以把消息发送到接收方的消息队列中了,那么接收方如何接收消息呢?
三、内核对象 这一次开始我们来说一下能过内核对象来进行一些进程间的通信。 Windows中提供了许多种内核对象,内核对象主要要用来供系统和应用程序管理系统资源,像进程、线程、文件等,存取符号对象、事件对象、文件对象、作业对象、互斥对象、管道对象、等待计时器对象等都是内核对象。我们在编程时经常要创建、打开和操作它们。内核对象是操作系统内部有一些数据结构,我们不可以直接操作这些对象,只能通过Windows提供的API函数来对他们进行操作。 我们可以在不同的进程中打开同一个Windows内核对象,这样,就为我们进行进程间的通信提供了一方式。不同的内核对象操作起来有些不同,当我们用到什么样的内核对象的时候我们再来看具体的操作。这里只再说明一点,Windows如何知道有多少个应用来使用一个内核对象呢,还来操作系统对内核对象的使用进行了计数。例如:A进程打开了一个文件H,这个时候只有一个进程再使用它,文件H的使用计数为1,这时B进程也打开了同样的文件,这样,H的计数就增长为2.如果这时A关闭了文件H,那B是不是惨了,不能再使用H了。不用担心,这个时个操作系统会检查使用计数,因为计数减去1后不为零,所以这时不会关闭文件H,B进程依然可以放心使用。但当B也关闭文件H的时候就不一样了,操作系统发现修改计数后,计数为0了,这时才真正关闭了文件。 这们看来操作系统为我们使用内核对象做了很多事情。哈哈。 这里说的太简单了,有些时候我们不能同时在两个进程打开同一文件,如果想了解Windows内核的更从东西,我推荐两本书。 《深入解析-Windows操作系统》理论比较多,但非常经典。 《windows 核心编程》实践性比较强,也很经典。 《自己动手写操作系统》绝对实践,但不是讲Windows,不过可以对操作系统都是干了什么有了最深刻认识。
我们选来说个最简单的东西。使用Mutex来达到单例应用的目的。 当我们的应用启动的时候我们就创建一个内核Mutex对象,当再次启动的时候我们来检测同一个对象,看其是否已经创建,如果没有创建说明这是第一个实例,正常运行,否则退出。 创建Mutex的方法: public Mutex( bool initiallyOwned, string name, out bool createdNew ) initiallyOwned,创建时是否获得当前对象。 name, 对象名称。 createdNew, 当前对象是否为新建,还是打开现有对象。 True,新建。False,打开现有。 那么我们就是根据createdNew来查看是否已经运行了别一个实例。 说明: name, 名字很重要,如果我们不使用名字,那么每次系统都会是创建一个新实例,其它进程也就无法通过打开这种形式来得到同一个对象,所以我们这里必须使用命名的Mutex对象。是不是可以是任意名字呢,要看我们期望是什么样子了,如果我们只是在当前用户内共享,那就没关系了,起个名字就可以,如果要想再不同用户之间也可以共享这一对象,这种做法就会有问题了。例如我们希望在SYSTEM 和administrator之间,那么应该如何办呢? 原来一个命名的Mutex对象可以有两个可见级别,一个是对当前用户,那么名称的开头要以"Local"开头,如果想要对所有用户可见应该使用"Global"前缀,如果不加任何前缀,系统默认为“Local\”。 更多请参考:
OK,现在都清楚了让我们来看一下代码。(只在这里show一下最核心的。)一定不要忘记关闭对象。
上述代码,完成了如下功能,第一次打开,正常显示窗体。第二次,如果发现已经打开一实例了,则找到已经打开的实例,并通过上次我们说的Message方法,发送一个消息到正在打开的窗体上,然后自己退出。这样打开的窗体就会显示一条消息。但是要注意,我们是通过进程名来找的窗体,所以在调试状态下需要改动一下代码才可以哎。 四、Event, Semaphore 使用内核心对象进行进程间同步以及实现生产者和消费者。 我们在多线程编程中常常要进行线程间的同步,使用Event,Semaphore,大家应该都熟悉。其实由于他们都是内核对象,所以也可以实现不同进程间不同线程的同步操作。 由于要在进程间共享这些对象,所以不要忘记给他们起名字。 关于Event,Semaphore,我就不多说了,MSDN上讲的非常清楚。直接给出代码,代码可能写的不是很完善,有些处理不很好的地方,就谅解。 两份代码,一份使用Event,一份使用Semaphore。 Event:
Semaphore: 五、MailSlot 这前我们说的通信方法,无论是消息,内核对象都不能进行大量信息的传递。而入口参数的方法不仅数据量不大,而且只有在通信进程没有启动的情况下才可以。下边我们就来说一下可以进行一些较大数据量的进程通信方法。 一般这样的通信方法都相对来说复杂一些,在这里呢,就不可能把原理的东西一一说清楚,只说个大概,我会给出一个简单实现,这样用的的时候可以知道用这个东东,再来认真学习也不迟。 进程通信MailSlot MailSlot是单向的进程间通信方法,也就是说如果要达到双向通信就需要建立两套同样的机制。MailSlot的拥有进程建立了MailSlot,其它进程就要以打开这个MailSlot并且向其传递信息。这样创建进程就可以监视到这些发送过来的信息,并且对其进行处理。这里也以有多个发送信息的进程为例。下边看一下用到的方法。 创建下个MailSlot并发回个句柄: [DllImport("kernel32.dll")] public static extern IntPtr CreateMailslot(string lpName, uint nMaxMessageSize, uintlReadTimeout, IntPtr lpSecurityAttributes); 检查MailSlot中的信息情况: [DllImport("kernel32.dll")] public static extern bool GetMailslotInfo(IntPtr hMailslot, int lpMaxMessageSize, refint lpNextSize, IntPtr lpMessageCount, IntPtr lpReadTimeout); 读取MailSlot中的数据: [DllImport("kernel32.dll", SetLastError = true)] public static extern bool ReadFile( IntPtr hFile, // handle to file byte[] lpBuffer, // data buffer int nNumberOfBytesToRead, // number of bytes to read ref int lpNumberOfBytesRead, // number of bytes read int overlapped); 关闭句柄: [DllImport("kernel32.dll", SetLastError = true)] [return: MarshalAs(UnmanagedType.Bool)] public static extern bool CloseHandle(IntPtr hObject); 有了以上这些Server端应该就可以工作了。 打开一个存在的MailSlot: [DllImport("Kernel32.dll", SetLastError = true, CharSet = CharSet.Auto)] public static extern IntPtr CreateFile(string fileName, uint fileAccess, uintfileShare, int securityAttributes, uint creationDisposition, int flags, IntPtrtemplate); 向打开的MailSlot中写入数据: [DllImport("kernel32.dll", SetLastError = true)] public static extern bool WriteFile(IntPtr hFile, byte[] lpBuffer, uintnNumberOfBytesToWrite, out uint lpNumberOfBytesWritten, [In] refSystem.Threading.NativeOverlapped lpOverlapped); 有了以上的方法我们的Client端也应该可以工作了。 但是开始代码之前我们还要来说一下MailSlot的名字,这个很重要噢。 Mailslot命名 当一下进程创建MailSlot时,MailSlot的命名可以有以下几种形式:只要进程不退出,循环检查MailSlot的数据情况,有数据处理显示,没有Sleep 10秒。OK. 再看Client端:
六、共享内存(filemapping) 参考:CreateFileMapping的MSDN翻译和使用心得
这时我们使用文件映射实现共享内存。 FileMapping用于将存在于磁盘的文件放进一个进程的虚拟地址空间,并在该进程的虚拟地址空间中产生一个区域用于“存放”该文件,这个空间就叫做File View,系统并同时产生一个File Mapping Object(存放于物理内存中)用于维持这种映射关系,这样当多个进程需要读写那个文件的数据时,它们的File View其实对应的都是同一个File Mapping Object,这样做可节省内存和保持数据的同步性,并达到数据共享的目的。 当然在一个应用向文件中写入数据时,其它进程不应该去读取这个正在写入的数据。这就需要进行一些同步的操作。 下边来看一下具体的API。 CreateFileMaping 的用法: HANDLE CreateFileMapping( //返回File Mapping Object的句柄 HANDLE hFile, // 想要产生映射的文件的句柄 LPSECURITY_ATTRIBUTES lpAttributes, // 安全属性(只对NT和2000生效) DWORD flProtect, // 保护标致 DWORD dwMaximumSizeHigh, // 在DWORD的高位中存放 File Mapping Object // 的大小 DWORD dwMaximumSizeLow, // 在DWORD的低位中存放 File Mapping Object // 的大小(通常这两个参数有一个为0) LPCTSTR lpName // File Mapping Object的名称。 ); 1) 物理文件句柄 任何可以获得的物理文件句柄,如果你需要创建一个物理文件无关的内存映射也无妨,将它设置成为0xFFFFFFFF(INVALID_HANDLE_VALUE)就可以了. 如果需要和物理文件关联,要确保你的物理文件创建的时候的访问模式和"保护设置"匹配,比如: 物理文件只读,内存映射需要读写就会发生错误。推荐你的物理文件使用独占方式创建。 如果使用 INVALID_HANDLE_VALUE,也需要设置需要申请的内存空间的大小,无论物理文件句柄参数是否有效,这样 CreateFileMapping 就可以创建一个和物理文件大小无关的内存空间给你, 甚至超过实际文件大小,如果你的物理文件有效,而大小参数为0,则返回给你的是一个和物理文件大小一样的内存空间地址范围。返回给你的文件映射地址空间是可以通过复制,集成或者命名得到,初始内容为0。 2) 保护设置 就是安全设置, 不过一般设置NULL就可以了, 使用默认的安全配置. 在win2k下如果需要进行限制, 这是针对那些将内存文件映射共享给整个网络上面的应用进程使用是, 可以考虑进行限制. 3) 高位文件大小 32位地址空间, 设置为0。 4) 共享内存名称 命名可以包含 "Global" 或者 "Local" 前缀在全局或者会话名空间初级文件映射. 其他部分可以包含任何除了()以外的字符, 可以参考 Kernel Object Name Spaces. 5) 调用CreateFileMapping的时候GetLastError的对应错误 ERROR_FILE_INVALID 如果企图创建一个零长度的文件映射, 应有此报 ERROR_INVALID_HANDLE 如果发现你的命名内存空间和现有的内存映射, 互斥量, 信号量, 临界区同名就麻烦了 ERROR_ALREADY_EXISTS 表示内存空间命名已经存在 使用函数CreateFileMapping创建一个想共享的文件数据句柄,然后使用MapViewOfFile来获取共享的内存地址,然后使用OpenFileMapping函数在另一个进程里打开共享文件的名称,这样就可以实现不同的进程共享数据。 下边是C#是对使用的接口函数声明:我们在示例里Server端建立的一个FileMapping,命名为:@"Global\MyFileMappingObject";这样我们在Client端就可以打开同名的FileMapping 七、DDE DDE(Dynamic Data Exchange) 动态数据交换, 是一种进程间通信形式。 Windows操作系统系统发提供的DDE数据交换本质上是一组Windows消息,再通过剪贴板或公享内存等方法进行数据交换。 我们在这里不讲解具体的DDE数据交换的协议及消息,有兴趣请参考:
这里我们通过一个例子来简单给出一个应用,只说一下例子中用到的消息及DDE API。例子是这样的,有时我们希望当我们的application打开时,用户如果双击application生成的文件可以使用当前打开的application打开这一文件,而不是新启动一个application打开文件。那么这一需求我们可以通过DDE来实现。 刚才说到DDE本质上是使用Windows的消息机制,那么对于上面的需求,当用户双击要打开的文档时,如果windows消息可以通知到我们的application,并且告知要打开的文件是什么,那么我们就可以在当前已经打开的application中处理这一消息,从而实现需求。Windows可不可以做到这一点呢,方法就是通过注册表。所以先来说一下注册表实现文件关联。 1. 注册表实现文件关联: 参考: 我们要加入的注册表项有: 文件扩展名以其默认值: HKEY_CLASSES_ROOT\.ccc = cccFileAssoc 以文件扩展名默认值为键中保存了对于当前文件类型的描述: HKEY_CLASSES_ROOT\cccFileAssoc = cccDescription 当双击.ccc的文件时,如果DDE没有设置时如何打开该文件: // Command to execute when application is not running or dde is not // present and Open command is issued. HKEY_CLASSES_ROOT\cccFileAssoc\shell\open\command = currentApplication %1 说明:其中 open是动作,也就是说当前是打开文件,也可以有其它动作,如:print, edit等。其中,currentApplication是打开该文件的应用程序的路径,如果设置了环境变量,可以是可执行文件的名称。“%1”会被替换为文件的绝对路径 以下为DDE文件关联的设置: 当打开文件时如何处理(当然也可以是print,edit等) // DDE execute statement for Open. HKEY_CLASSES_ROOT\cccFileAssoc\shell\open\ddeexec = [Open(%1)] 这里的server, topic,当用户双击文件时,Windows会将这两个值送给application,这样application就可以通过这两个值来判断自己是不是可以处理这个消息。在例子中我们可以看到。 // The server name your application responds to. HKEY_CLASSES_ROOT\cccFileAssoc\shell\open\ddeexec\application = cccServer // Topic name your application responds to. HKEY_CLASSES_ROOT\cccFileAssoc\shell\open\ddeexec\topic = cccTopic 好,知道了这些,我们来加注册表就不成问题了。在我们的应用程序启动是我们添加上注册表,实现如下: //HKEY_CLASSES_ROOT\.ccc = cccFileAssoc //File type name. //HKEY_CLASSES_ROOT\cccFileAssoc = cccDescription // Command to execute when application is not running or dde is not // present and Open command is issued. //HKEY_CLASSES_ROOT\cccFileAssoc\shell\open\command = currentApplication %1 // DDE execute statement for Open. //HKEY_CLASSES_ROOT\cccFileAssoc\shell\open\ddeexec = [Open(%1)] // The server name your application responds to. //HKEY_CLASSES_ROOT\cccFileAssoc\shell\open\ddeexec\application = cccServer // Topic name your application responds to. //HKEY_CLASSES_ROOT\cccFileAssoc\shell\open\ddeexec\topic = cccTopic 八、共享DLL 进程在Windows中运行起来的时候,所用到的所有DLL会以内存映射的形式,或都叫做文件映射的形式映射到进程的地址空间中来,从而进程可以找到对应的DLL并且调用其中的方法,使用其中的资源。 因为在不同进程中地址空间不同,所以共享数据段中不可以使用指针。 1. 共享数据段的建立 先建立一个c++ DLL工程: 加入SharedDll.h文件,内容如下: #include <windows.h> #include <tchar.h> #include "WinNT.h" void __stdcall SetData(LPSTR s); void __stdcall GetData(LPSTR s); 可以看到这其中有两个方法,分别是我们用来存取数据的。可以在不同的进程中使用。 那么数据存在哪里呢,这里就是真的的共享数据段放在哪里了。 加入新文件SharedDll.cpp,其中如下的内容。 #include "SharedDll.h" #pragma data_seg(".MYSEC") char MySharedData[4096]={0}; #pragma data_seg() void __stdcall SetData(LPSTR s) { strcpy(MySharedData, s); } void __stdcall GetData(LPSTR s) { strcpy(s, MySharedData); } 说明如下: #include "SharedDll.h" 引用进来我们刚才写好的.h头文件。 void __stdcall SetData(LPSTR s) { strcpy(MySharedData, s); } void __stdcall GetData(LPSTR s) { strcpy(s, MySharedData); } 这两个方法不用说就是我们用来存取数的真正方法了。那么可以看到数据是被保存在MySharedData这个数组中了。那么我们来看它的定义。 #pragma data_seg(".MYSEC") char MySharedData[4096]={0}; #pragma data_seg() 定义没有什么好奇怪的,但是在定义的前后分别的点东西,这个很重要,这个啊就是用来说明这是一个数据段的。(".MYSEC")是这个数据段的标识,或者说是名字。这样是不是就可以了呢,还不行,这样定义之后,我们的数据只是在了一个自己段里,并没有什么其它的不同,如何让其成为一个共享的段呢,还要一点工作。 要想设置其成为一个共享的段有两种方法: • 加入def文件说明之: 加入一个新文件SharedDll.def,其中内容如下: LIBRARY "SharedDll" SECTIONS .MYSEC READ WRITE SHARED EXPORTS SetData @1 GetData @2 说明: SECTIONS .MYSEC READ WRITE SHARED 这一句啊,就是来说明我们当前的段是一个共享的数据段的。下边: EXPORTS SetData @1 GetData @2 是说当前的Dll会导出这两个方法到外边,共com调用。 然后就是告诉编译器这个文件是我们的一个定义文件,做法,右键点击工程,打开属性,configuration perperties->linker->input,将module definition file填入SharedDll.def。 • 第二种方法,那么上边说的文件sharedDll.def还是需要的,但可以把这两句 SECTIONS .MYSEC READ WRITE SHARED 写到代码时去,做法是这样的: 在SharedDll.def文件中删除这两句,在SharedDll.cpp文件中加入如下: #pragma comment(linker, "/SECTION:.MYSEC,RWS") 好了就可以了。和第一种方法达到的效果是一样的。 好了到这里我们就完成了共享数据段的建立,现在让我们来看看如何用它来达到数据传递的目的。 2. 数据传递 引用我们定义的存取方法。 [DllImport("SharedDll.dll")] public extern static void SetData(string s); [DllImport("SharedDll.dll")] public extern static void GetData(StringBuilder s); 当然我们要保证c#的代码可以找到SharedDll.dll文件,所以将C# 工程的输出我SharedDll工程的输出设置为同一文件夹。 这样我们可以在两个不同的进程中使用这一接口来达到数据传递的目的了。 做到来就很简单了。不多写了。 十 NamedPipe 命名管道是一种具有唯一名称,可以在管道服务器以多个客户端之间进行单向及双向通信的进程间通信技术。任何进程都可以做为管道服务器或管道客户端实现命名管道通信。这里所说的管道服务器是指创建管道的进程,而管道客户端是连接已创建管道的进程。 命名管道可以用来在同一机器不同进程之间,也可以实现网络上不同机器进程之间的通信。在MSDN上有对命名管道非常详细的介绍。 请参考: 一. 命名管道的名称。 命名管道的名称是用来与系统中其它命名管道对象进行区分的标识。其规则如下: 其中ServerName可以远程计算机名称,或是“.”表示本机。 PipeName是表示命名管道的名称。 命名管道的服务器不可以在其它机器上创建命名管理,因些对于服务器来说创建管道时必须使用“.”表示本机,例如: 二. Server端代码的实现: 创建一个命名管道: handle = Win32Wrapper.CreateNamedPipe( name, // pipe name Win32Wrapper.PIPE_ACCESS_DUPLEX, // read/write access Win32Wrapper.PIPE_TYPE_MESSAGE | // message type pipe Win32Wrapper.PIPE_READMODE_MESSAGE | // message-read mode Win32Wrapper.PIPE_WAIT, // blocking mode Win32Wrapper.PIPE_UNLIMITED_INSTANCES, // max. instances Win32Wrapper.InAndOutBufferSize, // output buffer size Win32Wrapper.InAndOutBufferSize, // input buffer size Win32Wrapper.NMPWAIT_WAIT_FOREVER, // client time-out secAttr); // default security attrubute 等待客户端的连接: if (!Win32Wrapper.ConnectNamedPipe( namedPipeHandle, IntPtr.Zero)) { AddMessage("Connected named pipe error"); return; } 当客户端连接进入,读取客户端数据并返回处理数据: listen = true; while (listen) { byte[] buffer = new byte[Win32Wrapper.InAndOutBufferSize]; uint outNumberCount; bool result = Win32Wrapper.ReadFile( namedPipeHandle, buffer, Win32Wrapper.InAndOutBufferSize, out outNumberCount, IntPtr.Zero); if (result) { string message = Encoding.ASCII.GetString(buffer).Trim('\0'); AddMessage(message); string returnMessage = "Return message: " + message; uint outWriteByte; Win32Wrapper.WriteFile(namedPipeHandle, Encoding.ASCII.GetBytes(returnMessage), (uint)returnMessage.Length, out outWriteByte, IntPtr.Zero); } //Win32Wrapper.DisconnectNamedPipe(namedPipeHandle); } 三. 客户端连接服务器方法 当服务器端建立成功,客户端可以向连接到服务器: handle = Win32Wrapper.CreateFile( Win32Wrapper.NamedPipePublicName, Win32Wrapper.GENERIC_READ | Win32Wrapper.GENERIC_WRITE, 0, IntPtr.Zero, Win32Wrapper.OPEN_EXISTING, 0, 0); uint mode = Win32Wrapper.PIPE_READMODE_MESSAGE; if (!Win32Wrapper.SetNamedPipeHandleState( handle, ref mode, IntPtr.Zero, IntPtr.Zero)) { AddMessage("set mode error"); return; } 如果SetNamedPipeHandleState成功则说明与服务器连接成功。下边可以向服务器发送数据,并接收服务器的处理数据。
转载地址:http://htiti.baihongyu.com/