进程间通信 (IPC) 入门和示例代码
本文将介绍All-In-One Code Framework中的通用IPC技术。IPC技术包括命名管道、文件映射、邮件槽等。
引言
进程间通信(IPC)是一组用于在同一进程的一个或多个线程之间交换数据,或者在通过网络连接的一个或多个计算机的多个进程之间交换数据的技术。IPC技术包括命名管道、文件映射、邮件槽、远程过程调用(RPC)等。
在All-In-One Code Framework中,我们已经实现了命名管道、文件映射、邮件槽和Remoting的示例(C++和C#)。我们还将添加更多技术,例如:剪贴板、Winsock等。您可以从http://cfx.codeplex.com/下载最新代码。
背景
All-In-One Code Framework(简称AIO)使用不同编程语言(例如Visual C#、VB.NET、Visual C++)的典型示例代码,概述了大多数Microsoft开发技术(例如COM、数据访问、IPC)的框架和骨架。
Using the Code
按照以下步骤查找示例
- 下载zip文件并解压缩。
- 打开[Visual Studio 2008]文件夹。
- 打开解决方案文件IPC.sln。您必须在计算机上预先安装Visual Studio 2008。
- 在解决方案资源管理器中,打开[进程] \ [IPC和RPC]文件夹。
示例结构和关系
命名管道
命名管道是一种用于在本地机器或内网中的计算机之间,通过管道服务器和一个或多个管道客户端进行单向或双向进程间通信的机制。
PIPE_ACCESS_INBOUND:
Client (GENERIC_WRITE) ---> Server (GENERIC_READ)
PIPE_ACCESS_OUTBOUND:
Client (GENERIC_READ) <--- Server (GENERIC_WRITE)
PIPE_ACCESS_DUPLEX:
Client (GENERIC_READ or GENERIC_WRITE, or both)
<--> Server (GENERIC_READ and GENERIC_WRITE)
本示例演示了一个命名管道服务器,\\.\pipe\HelloWorld,它支持PIPE_ACCESS_DUPLEX
。它首先创建一个命名管道,然后监听客户端连接。当客户端连接后,服务器尝试从管道读取客户端请求并写入响应。
命名管道客户端尝试以GENERIC_READ
和GENERIC_WRITE
权限连接到管道服务器\\.\pipe\HelloWorld。客户端将消息写入管道服务器并接收其响应。
代码逻辑
服务器端逻辑
- 创建命名管道。(
CreateNamedPipe
) - 等待客户端连接。(
ConnectNamedPipe
) - 从管道读取客户端请求并写入响应。(
ReadFile
,WriteFile
) - 断开管道连接并关闭句柄。(
DisconnectNamedPipe
,CloseHandle
)
客户端逻辑
- 尝试打开命名管道。(
CreateFile
) - 设置指定命名管道的读取模式和阻塞模式。(
SetNamedPipeHandleState
) - 向管道服务器发送消息并接收其响应。(
WriteFile
,ReadFile
) - 关闭管道。(
CloseHandle
)
代码 - CreateNamedPipe (C++)
// Create the named pipe.
HANDLE hPipe = CreateNamedPipe(
strPipeName, // The unique pipe name. This string must
// have the form of \\.\pipe\pipename
PIPE_ACCESS_DUPLEX, // The pipe is bi-directional; both
// server and client processes can read
// from and write to the pipe
PIPE_TYPE_MESSAGE | // Message type pipe
PIPE_READMODE_MESSAGE | // Message-read mode
PIPE_WAIT, // Blocking mode is enabled
PIPE_UNLIMITED_INSTANCES, // Max. instances
// These two buffer sizes have nothing to do with the buffers that
// are used to read from or write to the messages. The input and
// output buffer sizes are advisory. The actual buffer size reserved
// for each end of the named pipe is either the system default, the
// system minimum or maximum, or the specified size rounded up to the
// next allocation boundary. The buffer size specified should be
// small enough that your process will not run out of nonpaged pool,
// but large enough to accommodate typical requests.
BUFFER_SIZE, // Output buffer size in bytes
BUFFER_SIZE, // Input buffer size in bytes
NMPWAIT_USE_DEFAULT_WAIT, // Time-out interval
&sa // Security attributes
)
有关更多代码示例,请下载AIO源代码。
命名管道的安全属性
如果CreateNamedPipe
的lpSecurityAttributes
为NULL
,则命名管道将获得默认的安全描述符,并且句柄不能被继承。命名管道默认安全描述符中的ACL授予LocalSystem帐户、管理员和创建者所有者完全控制权限。它们还授予Everyone组和匿名帐户成员读取权限。换句话说,当安全属性为NULL
时,命名管道无法通过网络或从作为较低完整性级别的本地客户端进行写入权限连接。在此,我们填充了安全属性,授予EVERYONE(不仅仅是连接权限)对服务器的所有访问权限。这解决了跨网络和跨IL的问题,但同时也带来了安全漏洞:客户端拥有WRITE_OWNER权限,然后服务器就会失去对管道对象的控制。
代码 - 安全属性 (C++)
SECURITY_ATTRIBUTES sa;
sa.lpSecurityDescriptor = (PSECURITY_DESCRIPTOR)malloc(SECURITY_DESCRIPTOR_MIN_LENGTH);
InitializeSecurityDescriptor(sa.lpSecurityDescriptor, SECURITY_DESCRIPTOR_REVISION);
// ACL is set as NULL in order to allow all access to the object.
SetSecurityDescriptorDacl(sa.lpSecurityDescriptor, TRUE, NULL, FALSE);
sa.nLength = sizeof(sa);
sa.bInheritHandle = TRUE;
.NET命名管道
.NET通过两种方式支持命名管道
- P/Invoke原生API。
通过从.NET P/Invoke原生API,我们可以模仿
CppNamedPipeServer
中的代码逻辑,创建支持PIPE_ACCESS_DUPLEX
的命名管道服务器\\.\pipe\HelloWorld。PInvokeNativePipeServer
首先创建一个命名管道,然后监听客户端连接。当客户端连接后,服务器尝试从管道读取客户端请求并写入响应。 System.IO.Pipes
命名空间在.NET Framework 3.5中,向.NET BCL添加了
System.IO.Pipes
命名空间以及一组类(例如PipeStream
、NamedPipeServerStream
)。这些类使得在.NET中进行命名管道编程比直接P/Invoke原生API更加简单和安全。BCLSystemIOPipeServer
首先创建一个命名管道,然后监听客户端连接。当客户端连接后,服务器尝试从管道读取客户端请求并写入响应。
代码 - 创建命名管道 (C#)
// Prepare the security attributes
// Granting everyone the full control of the pipe is just for
// demo purpose, though it creates a security hole.
PipeSecurity pipeSa = new PipeSecurity();
pipeSa.SetAccessRule(new PipeAccessRule("Everyone",
PipeAccessRights.ReadWrite, AccessControlType.Allow));
// Create the named pipe
pipeServer = new NamedPipeServerStream(
strPipeName, // The unique pipe name.
PipeDirection.InOut, // The pipe is bi-directional
NamedPipeServerStream.MaxAllowedServerInstances,
PipeTransmissionMode.Message, // Message type pipe
PipeOptions.None, // No additional parameters
BUFFER_SIZE, // Input buffer size
BUFFER_SIZE, // Output buffer size
pipeSa, // Pipe security attributes
HandleInheritability.None // Not inheritable
);
文件映射
文件映射是一种用于本地机器上两个或多个进程之间进行单向或双向进程间通信的机制。为了共享文件或内存,所有进程都必须使用同一文件映射对象的名称或句柄。
要共享文件,第一个进程使用CreateFile
函数创建或打开文件。接下来,它使用CreateFileMapping
函数创建文件映射对象,指定文件句柄和文件映射对象的名称。事件、信号量、互斥体、可等待计时器、作业和文件映射对象的名称共享同一命名空间。因此,如果CreateFileMapping
和OpenFileMapping
函数指定了另一个类型对象正在使用的名称,则它们会失败。
要共享与文件无关的内存,进程必须使用CreateFileMapping
函数,并将hFile
参数指定为INVALID_HANDLE_VALUE
,而不是现有文件句柄。相应的文件映射对象会访问由系统分页文件支持的内存。在对CreateFileMapping
的调用中指定INVALID_HANDLE_VALUE
的hFile
时,必须指定大于零的大小。
共享文件或内存的进程必须使用MapViewOfFile
或MapViewOfFileEx
函数创建文件视图。它们必须使用信号量、互斥体、事件或其他互斥技术来协调其访问。
本示例演示了一个命名共享内存服务器Local\HelloWorld,它使用INVALID_HANDLE_VALUE
创建文件映射对象。通过使用PAGE_READWRITE
标志,进程可以通过创建的任何文件视图获得读/写权限。
命名共享内存客户端Local\HelloWorld可以访问第一个进程写入共享内存的字符串。控制台显示从第一个进程创建的文件映射中读取的消息“Message from the first process”。
代码逻辑
服务侧逻辑
- 创建文件映射。(
CreateFileMapping
) - 将文件映射的视图映射到当前进程的地址空间。(
MapViewOfFile
) - 向文件视图写入消息。(
CopyMemory
) - 取消映射文件视图并关闭文件映射对象。(
UnmapViewOfFile
,CloseHandle
)
客户端逻辑
- 尝试打开命名文件映射。(
OpenFileMapping
) - 将文件映射的视图映射到当前进程的地址空间。(
MapViewOfFile
) - 从共享内存视图中读取消息。
- 取消映射文件视图并关闭文件映射对象。(
UnmapViewOfFile
,CloseHandle
)
代码 - CreateFileMapping (C++)
// In terminal services: The name can have a "Global\" or "Local\" prefix
// to explicitly create the object in the global or session namespace.
// The remainder of the name can contain any character except the
// backslash character (\). For details, please refer to:
// http://msdn.microsoft.com/en-us/library/aa366537.aspx
TCHAR szMapFileName[] = _T("Local\\HelloWorld");
// Create the file mapping object
HANDLE hMapFile = CreateFileMapping(
INVALID_HANDLE_VALUE, // Use paging file instead of existing file.
// Pass file handle to share in a file.
NULL, // Default security
PAGE_READWRITE, // Read/write access
0, // Max. object size
BUFFER_SIZE, // Buffer size
szMapFileName // Name of mapping object
);
目前.NET仅支持P/Invoke原生API。通过P/Invoke,.NET可以模拟原生代码中的类似行为。
示例代码4 (C# - P/Invoke)
/// <summary>
/// Creates or opens a named or unnamed file mapping object for
/// a specified file.
/// </summary>
/// <param name="hFile">A handle to the file from which to create
/// a file mapping object.</param>
/// <param name="lpAttributes">A pointer to a SECURITY_ATTRIBUTES
/// structure that determines whether a returned handle can be
/// inherited by child processes.</param>
/// <param name="flProtect">Specifies the page protection of the
/// file mapping object. All mapped views of the object must be
/// compatible with this protection.</param>
/// <param name="dwMaximumSizeHigh">The high-order DWORD of the
/// maximum size of the file mapping object.</param>
/// <param name="dwMaximumSizeLow">The low-order DWORD of the
/// maximum size of the file mapping object.</param>
/// <param name="lpName">The name of the file mapping object.
/// </param>
/// <returns>If the function succeeds, the return value is a
/// handle to the newly created file mapping object.</returns>
[DllImport("Kernel32.dll", SetLastError = true)]
public static extern IntPtr CreateFileMapping(
IntPtr hFile, // Handle to the file
IntPtr lpAttributes, // Security Attributes
FileProtection flProtect, // File protection
uint dwMaximumSizeHigh, // High-order DWORD of size
uint dwMaximumSizeLow, // Low-order DWORD of size
string lpName // File mapping object name
);
邮件槽
邮件槽是一种用于本地机器或内网计算机之间进行单向进程间通信的机制。任何客户端都可以将消息存储在邮件槽中。槽的创建者,即服务器,会检索存储在那里的消息。
Client (GENERIC_WRITE) ---> Server (GENERIC_READ)
本示例演示了一个邮件槽服务器\\.\mailslot\HelloWorld。它首先创建一个邮件槽,然后每五秒读取槽中的新消息。之后,邮件槽客户端连接并写入邮件槽\\.\mailslot\HelloWorld。
代码逻辑
服务器端逻辑
- 创建邮件槽。(
CreateMailslot
) - 检查邮件槽中的消息。(
ReadMailslot
)- 检查邮件槽中的消息数量。(
GetMailslotInfo
) - 逐个从邮件槽检索消息。在读取时,更新邮件槽中剩余消息的数量。(
ReadFile
,GetMailslotInfo
)
- 检查邮件槽中的消息数量。(
- 关闭邮件槽实例的句柄。(
CloseHandle
)
客户端逻辑
- 打开邮件槽。(
CreateFile
) - 将消息写入邮件槽。(
WriteMailslot
,WriteFile
) - 关闭槽。(
CloseHandle
)
代码 - GetMailslotInfo (C++)
/////////////////////////////////////////////////////////////////////////
// Check for the number of messages in the mailslot.
//
bResult = GetMailslotInfo(
hMailslot, // Handle of the mailslot
NULL, // No maximum message size
&cbMessageBytes, // Size of next message
&cMessages, // Number of messages
NULL); // No read time-out
代码 - CreateMailslot (C# - P/Invoke)
/// <summary>
/// Creates an instance of a mailslot and returns a handle for subsequent
/// operations.
/// </summary>
/// <param name="lpName">mailslot name</param>
/// <param name="nMaxMessageSize">The maximum size of a single message
/// </param>
/// <param name="lReadTimeout">The time a read operation can wait for a
/// message</param>
/// <param name="lpSecurityAttributes">Security attributes</param>
/// <returns>If the function succeeds, the return value is a handle to
/// the server end of a mailslot instance.</returns>
[DllImport("kernel32.dll", SetLastError = true)]
public static extern IntPtr CreateMailslot(
string lpName, // Mailslot name
uint nMaxMessageSize, // Max size of a single message in bytes
int lReadTimeout, // Timeout of a read operation
IntPtr lpSecurityAttributes // Security attributes
);
Remoting
.NET Remoting是一种用于本地机器或内网及互联网计算机上.NET应用程序之间进行单向进程间通信和RPC的机制。
.NET Remoting允许应用程序在不同的应用程序域、进程,甚至不同的计算机之间(通过网络连接)公开一个可远程处理的对象。 .NET Remoting向客户端应用程序提供可远程处理对象的引用,客户端应用程序可以像使用本地对象一样实例化和使用可远程处理的对象。但是,实际的代码执行发生在服务器端。所有对可远程处理对象的请求都通过Channel对象进行代理,这些Channel对象封装了实际的传输模式,包括TCP流、HTTP流和命名管道。因此,通过实例化适当的Channel对象,可以使.NET Remoting应用程序支持不同的通信协议,而无需重新编译应用程序。运行时本身负责管理对象在客户端和服务器应用程序域之间的序列化和封送。
代码 - 创建和注册Channel (C#)
/////////////////////////////////////////////////////////////////////
// Create and register a channel (TCP channel in this example) that
// is used to transport messages across the remoting boundary.
//
// Properties of the channel
IDictionary props = new Hashtable();
props["port"] = 6100; // Port of the TCP channel
props["typeFilterLevel"] = TypeFilterLevel.Full;
// Formatters of the messages for delivery
BinaryClientFormatterSinkProvider clientProvider = null;
BinaryServerFormatterSinkProvider serverProvider =
new BinaryServerFormatterSinkProvider();
serverProvider.TypeFilterLevel = TypeFilterLevel.Full;
// Create a TCP channel
TcpChannel tcpChannel = new TcpChannel(props, clientProvider, serverProvider);
// Register the TCP channel
ChannelServices.RegisterChannel(tcpChannel, true);
代码 - 注册可远程处理类型 (VB.NET)
'''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''
' Register the remotable types on the service end as
' server-activated types (aka well-known types) or client-activated
' types.
' Register RemotingShared.SingleCallObject as a SingleCall server-
' activated type.
RemotingConfiguration.RegisterWellKnownServiceType(GetType(RemotingShared.SingleCallObject), _
"SingleCallService", WellKnownObjectMode.SingleCall)
' Register RemotingShared.SingletonObject as a Singleton server-
' activated type.
RemotingConfiguration.RegisterWellKnownServiceType(GetType(RemotingShared.SingletonObject), _
"SingletonService", WellKnownObjectMode.Singleton)
' Register RemotingShared.ClientActivatedObject as a client-
' activated type.
RemotingConfiguration.ApplicationName = "RemotingService"
RemotingConfiguration.RegisterActivatedServiceType(_
GetType(Global.RemotingShared.ClientActivatedObject))
关注点
在AIO项目的试点阶段,我们专注于五种技术:COM、库、IPC、Office和数据访问。项目已有42个代码示例。目前该集合每周以7个示例的速度增长。
历史
本文创建于2009年3月12日。