原文链接:Windows进程间的通信 - FlyingPig007 - 博客园
一、进程与进程通信
进程间通信(Interprocess Communication, IPC)是指不同的进程之间进行数据共享和数据交换。
二、进程间通信方式
1. 文件映射
注:文件映射是在多个进程间共享数据的非常有效方法,有较好的安全性。但文件映射只能用于本地机器的进程之间,不能用于网络中,而开发者还必须控制进程间的同步。
使用内存映射文件的一般流程:(reference from: https://blog.csdn.net/qq_20183489/article/details/54646794)
另外,内存映射文件在处理大数据量的文件时表现出了良好的性能(实际上,文件越大,内存映射的优势就越明显)。示例可以参考:内存映射文件(专门读写大文件)_大文件内存映射-CSDN博客
2.2 共享内存
Win32 API中共享内存(Shared Memory)实际就是文件映射的一种特殊情况。进程在创建文件映射对象时用0xFFFFFFFF(INVALID_HANDLE_VALUE)来代替文件句柄(HANDLE),就表示了对应的文件映射对象是从操作系统页面文件访问内存,其它进程打开该文件映射对象就可以访问该内存块。由于共享内存是用文件映射实现的,所以它也有较好的安全性,也只能运行于同一计算机上的进程之间。
共享内存实现数据共享示例如下:
1 //发送数据的进程先启动,用于发送数据,即将数据写入视图 。
2
3 #include "stdafx.h"
4 #include <Windows.h>
5 #include <conio.h>
6
7 #define BUFFER_SIZE 256
8 TCHAR szMapFileName[] = TEXT("MyFileMappingName"); //映射文件名,即共享内存的名称
9 TCHAR szSendData[] = TEXT("Message from the send process.");
10
11 int main()
12 {
13 HANDLE hMapFile = NULL;
14 LPCTSTR pBuf = NULL;
15
16 //1. 创建一个文件映射内核对象
17 hMapFile = ::CreateFileMapping(INVALID_HANDLE_VALUE, NULL, PAGE_READWRITE, 0, BUFFER_SIZE, szMapFileName); //INVALID_HANDLE_VALUE表示创建一个进程间共享的对象
18 if (NULL == hMapFile)
19 {
20 _tprintf(TEXT("Could not create file mapping object (%d).\n"), GetLastError());
21 return -1;
22 }
23
24 //2. 将文件数据映射到进程的地址空间
25 pBuf = (LPTSTR)MapViewOfFile(hMapFile, FILE_MAP_ALL_ACCESS, 0, 0, BUFFER_SIZE);
26 if (NULL == pBuf)
27 {
28 _tprintf(TEXT("Could not map view of file (%d). \n"), GetLastError());
29
30 CloseHandle(hMapFile);
31 hMapFile = NULL;
32
33 return -1;
34 }
35
36 //3. 写入到内存中
37 CopyMemory((void*)pBuf, szSendData, _tcslen(szSendData) * sizeof(TCHAR));
38 _getch(); //这个函数是一个不回显函数,当用户按下某个字符时,函数自动读取,无需按回车
39
40 //4. 从进程的地址空间中撤消文件数据的映像
41 UnmapViewOfFile(pBuf);
42
43 //5. 关闭文件映射对象和文件对象
44 CloseHandle(hMapFile);
45
46 getchar();
47 return 0;
48 }
1 //接收数据的进程后启动,用于接收数据,即读取视图的数据
2
3 #include "stdafx.h"
4 #include <Windows.h>
5
6 #define BUFFER_SIZE 256
7 TCHAR szMapFileName[] = TEXT("MyFileMappingName");
8
9 int main()
10 {
11 HANDLE hMapFile = NULL;
12 LPCTSTR pBuf = NULL;
13
14 //1. 打开一个命名的文件映射内核对象
15 hMapFile = ::OpenFileMapping(FILE_MAP_ALL_ACCESS, FALSE, szMapFileName);
16 if (NULL == hMapFile)
17 {
18 _tprintf(TEXT("Could not open file mapping object (%d).\n"), GetLastError());
19 return -1;
20 }
21
22 //2. 将文件映射内核对象hFileMapping映射到当前应用程序的进程地址pBuf,通过该指针可以读写共享的内存区域
23 pBuf = (LPTSTR)MapViewOfFile(hMapFile, FILE_MAP_ALL_ACCESS, 0, 0, BUFFER_SIZE);
24 if (NULL == pBuf)
25 {
26 _tprintf(TEXT("Could not map view of file (%d). \n"), GetLastError());
27
28 CloseHandle(hMapFile);
29 hMapFile = NULL;
30
31 return -1;
32 }
33
34 //3. 显示接收到的数据
35 for (int i = 0; i < _tcsclen(pBuf); i++)
36 {
37 _tprintf(TEXT("%c"), *(pBuf + i));
38 }
39 printf("\n");
40
41 //4. 从进程的地址空间中撤消文件数据的映像
42 UnmapViewOfFile(pBuf);
43
44 //5. 关闭文件映射对象和文件对象
45 CloseHandle(hMapFile);
46
47 getchar();
48 return 0;
49 }
2.3 管道
管道(Pipe)是一种具有两个端点的通信通道:有一端句柄的进程可以和有另一端句柄的进程通信。管道可以是单向-一端是只读的,另一端点是只写的;也可以是双向的-管道的两端点既可读也可写。
(1) 匿名管道(Anonymous Pipe)是在父进程和子进程之间,或同一父进程的两个子进程之间传输数据的无名字的单向管道。通常由父进程创建管道,然后由要通信的子进程继承通道的读端点句柄或写 端点句柄,然后实现通信。父进程还可以建立两个或更多个继承匿名管道读和写句柄的子进程。这些子进程可以使用管道直接通信,不需要通过父进程。
匿名管道是单机上实现子进程标准I/O重定向的有效方法,它不能在网上使用,也不能用于两个不相关的进程之间。
匿名管道通信过程:
>>父进程读写过程
①创建匿名管道
②创建子进程,并对子进程相关数据进行初始化(用匿名管道的读取/写入句柄赋值给子进程的输入/输出句柄)。
③关闭子进程相关句柄。(进程句柄,主线程句柄)
④对管道读写
>>子进程读写过程
①获得输入输出句柄
②对管道读写
相关函数:
CreatePipe 管道创建
函数原型
BOOL CreatePipe(
PHANDLE hReadPipe, // pointer to read handle
PHANDLE hWritePipe, // pointer to write handle
LPSECURITY_ATTRIBUTES lpPipeAttributes, // pointer to security attributes
DWORD nSize // pipe size
);
参数说明:
-
- hReadPipe 作为返回类型使用,返回管道读取句柄
- hWritePipe 作为返回类型使用,返回管道写入句柄
- lpPipeAttributes 指向SECURITY_ATTRIBUTES结构体的指针,检测返回句柄是否能被子进程继承,如果此参数为NULL,则句柄不能被继承
- nSize 指定管道的缓冲区大小,改大小只是个建议值,系统将用这个值来计算一个适当的缓存区大小,如果此参数是0,系统会使用默认的缓冲区大小
- hReadPipe 作为返回类型使用,返回管道读取句柄
返回值:
若函数成功返回非零值
若函数失败返回0,详细消息可以调用GetLastError函数获得
1 // 父进程.cpp : Defines the entry point for the console application.
2 //
3
4 #include "stdafx.h"
5 #include <Windows.h>
6 #include <iostream>
7 using namespace std;
8
9 int main()
10 {
11 const int nBufferLen = 256;
12
13 SECURITY_ATTRIBUTES sa;
14 HANDLE hRead = NULL;
15 HANDLE hWrite = NULL;
16
17 STARTUPINFO sui;
18 PROCESS_INFORMATION pi;
19
20 char szBuffer[nBufferLen] = { 0 };
21 DWORD dwReadLen = 0;
22
23 BOOL bRet = FALSE;
24
25 //1. 创建匿名管道
26 sa.bInheritHandle = TRUE;
27 sa.lpSecurityDescriptor = NULL;
28 sa.nLength = sizeof(SECURITY_ATTRIBUTES);
29 bRet = ::CreatePipe(&hRead, &hWrite, &sa, 0);
30 if (!bRet)
31 {
32 cout << "创建匿名管道失败!" << endl;
33 system("pause");
34 return -1;
35 }
36
37 //2. 创建子进程,并对子进程相关数据进行初始化(用匿名管道的读取写入句柄赋予子进程的输入输出句柄)
38 ZeroMemory(&sui, sizeof(STARTUPINFO));
39 sui.cb = sizeof(STARTUPINFO);
40 sui.dwFlags = STARTF_USESTDHANDLES;
41 sui.hStdInput = hRead;
42 sui.hStdOutput = hWrite;
43 sui.hStdError = GetStdHandle(STD_ERROR_HANDLE);
44 bRet = ::CreateProcess(L"..\\x64\\Debug\\子进程.exe", NULL, NULL, NULL, TRUE, CREATE_NEW_CONSOLE, NULL, NULL, &sui, &pi);
45 if (!bRet)
46 {
47 cout << "创建子进程失败!" << endl;
48 system("pause");
49 return -1;
50 }
51
52 //3. 关闭子进程相关句柄(进行句柄,进程主线程句柄)
53 CloseHandle(pi.hProcess);
54 CloseHandle(pi.hThread);
55
56 Sleep(2000);
57
58 //4. 读取数据
59 bRet = ::ReadFile(hRead, szBuffer, nBufferLen, &dwReadLen, NULL);
60 if (!bRet)
61 {
62 cout << "读取数据失败!" << endl;
63 system("pause");
64 return -1;
65 }
66
67 cout << "从子进程接收到到数据: " << szBuffer << endl;
68
69 system("pause");
70 return 0;
71 }
1 // 子进程.cpp : Defines the entry point for the console application.
2 //
3
4 #include "stdafx.h"
5 #include <Windows.h>
6 #include <iostream>
7 using namespace std;
8
9 int main()
10 {
11 HANDLE hRead = NULL;
12 HANDLE hWrite = NULL;
13
14 BOOL bRet = FALSE;
15
16 //1. 获得匿名管道输入输出句柄
17 hRead = GetStdHandle(STD_INPUT_HANDLE);
18 hWrite = GetStdHandle(STD_OUTPUT_HANDLE);
19
20 char szSendBuffer[] = "子进程写入管道成功!";
21 DWORD dwWriteLen = 0;
22
23 //2. 写入数据
24 bRet = WriteFile(hWrite, szSendBuffer, (DWORD)strlen(szSendBuffer), &dwWriteLen, NULL);
25 if (!bRet)
26 {
27 system("pause");
28 return -1;
29 }
30
31 Sleep(500);
32
33 system("pause");
34 return 0;
35 }
(2) 命名管道(Named Pipe)是服务器进程和一个或多个客户进程之间通信的单向或双向管道。命名管道可以在不相关的进程之间和不同计算机之间使用,服务器建立命名管道时给它指定一个名字,任何进程都可以通过该名字打开管道的另一端,根据给定的权限和服务器进程通信。
命名管道提供了相对简单的编程接口,使通过网络传输数据并不比同一计算机上两进程之间通信更困难,不过如果要同时和多个进程通信它就力不从心了。
命名管道有两种通信方式:
A. 字节模式:在字节模式下,数据以一个连续的字节流的形式在客户机和服务器之间流动
B. 消息模式:在消息模式下,客户机和服务器则通过一系列不连续的数据单位,进行数据收发,每次在管道上发一条消息后,它必须作为一条完整的消息读入
通信流程:
服务器端: 创建命名管道 -> 服务器等待用户连接 -> 读写数据
客户端:连接命名管道 -> 打开命名管道 -> 读写数据
相关函数:
显示相关函数:
一、
[CreateNamePine 创建命名管道][ConnectNamePipe 创建连接命名管道][WaitNamedPipe 进行命名管道连接]
CreateNamePipe 创建命名管道
函数原型:
HANDLE CreateNamedPipe(
LPCTSTR lpName, // pipe name
DWORD dwOpenMode, // pipe open mode
DWORD dwPipeMode, // pipe-specific modes
DWORD nMaxInstances, // maximum number of instances
DWORD nOutBufferSize, // output buffer size
DWORD nInBufferSize, // input buffer size
DWORD nDefaultTimeOut, // time-out interval
LPSECURITY_ATTRIBUTES lpSecurityAttributes // SD
);
1、参数说明:
1.lpName
一个指向空终止的字符串,该字符串的格式必须是:"\\.\pine\pinename"其中该字符串开始是两个连续的反斜杠,其后的原点表示是本地机器,如果想要与远程的服务器建立连接连接,那么在原点这个位置应该指定这个远程服务器的名称.接下来是"pine"这个固定的字符串,也就是说这个字符串的内容不能修改,但其大小写是无所谓的,最后是所创建的命名管道的名称
2.dwOpenMode
指定管道的访问方式,重叠方式.写直通方式,还有管道句柄的安全访问方式()
用来指定管道的访问方式的标志取值如下(下面这三个值只能够取其中一个),并且管道的每一个实例都必须具有同样的访问方式
PIPE_ACCESS_INBOUND 管道只能用作接收数据(服务器只能读数据,客户端只能写数据),相当于在CreateFile中指定了GENERIC_READ
PIPE_ACCESS_OUTBOUND 管道只能用作发送数据(服务器只能写数据,客户端只能读数据),相当于在CreateFile中指定了GENERIC_WRITE
PIPE_ACCESS_DUPLEX 管道既可以发送也可以接收数据,相当于在CreateFile中指定了GENERIC_READ | GENERIC_WRITE
用来指定写直通方式和重叠方式的标志,取值可以是一下一个或多个组合
FILE_FLAG_WRITE_THROUGH 管道用于同步发送和接收数据,只有在数据被发送到目标地址时发送函数才会返回,如果不设置这个参数那么在系统内部对于命名管道的处理上可能会因为减少网络附和而在数据积累到一定量时才发送,并且对于发送函数的调用会马上返回
FILE_FLAG_OVERLAPPED 管道可以用于异步输入和输出,异步读写的有关方法和文件异步读写是相同的
用来指定管道安全访问方式的标志,取值可以是一下一个或多个组合
WRITE_DAC 调用者对命名管道的任意范围控制列表(ACL)都可以进行写入访问
WRITE_OWNER 调用者对命名管道的所有者可以进行写入访问
ACCESS_SYSTEM_SECURITY 调用者对命名管道打安全范围控制列表(SACL)可以进行写入访问
3.dwPipeMode
指定管道类型,,读取和等待方式可以是下面值的组合(0为字节写字节读阻塞方式)
用于指定管道句柄的写入的标志
PIPE_TYPE_BYTE 数据在通过管道发送时作为字节流发送,不能与PIPE_READMODE_MESSAGE共用
PIPE_TYPE_MESSAGE 数据在通过管道发送时作为消息发送,不能与PIPE_READMODE_BYTE共用
用于指定管道句柄的读取方式
PIPE_READMODE_BYTE 在接收数据时接收字节流该方式在PIPE_TYPE_BYTE和PIPE_TYPE_MESSAGE类型均可以使用
PIPE_READMODE_MESSAGE 在接收数据时接收消息该方式只用在PIPE_TYPE_MESSAGE类型下才可以使用
用于指定管道句柄的等待方式(同一管道的不同实例可以采取不同的等待方式)
PIPE_WAIT 使用等待模式(阻塞方式),在读,写和建立连接时都需要管道的另一方完成相应动作后才会返回
PIPE_NOWAIT 使用非等待模式(非阻塞方式),在读,写和建立连接时不需要管道的另一方完成相应动作后就会立即返回
4.nMaxInstances
为管道的的最大数量,在第一次建立服务器方管道时这个参数表明该管道可以同时存在的数量。PIPE_UNLIMITED_INSTANCES表明不对数量进行限制
5.nOutBufferSize
表示输出缓冲区的大小
6.nInBufferSize
表示输入缓冲区的大小
7.nDefaultTimeOut
表示在等待连接时最长的等待时间(以毫秒为单位),如果在创建时设置为NMPWAIT_USE_DEFAULT_WAIT表明无限制的等待,而以后服务器方的其他管道实例也需要设置相同的值
8.lpSecurityAttributes
为安全属性,一般设置为NULL。如果创建或打开失败则返回INVALID_HANDLE_VALUE。可以通过GetLastError得到错误
2、返回值
若函数成功,返回值是一个命名通道实例的句柄,如果命名通道已经存在则返回一个以存在的命名通道的句柄,并调用GetLastError函数的返回值为 ERROR_ALREADY_EXISTS
若函数失败,返回值为INVALID_HANDLE_VALUE若想获得更多信息调用GetLastError函数获得
二、
ConnectNamePipe 创建命名管道连接
函数原型
BOOL ConnectNamedPipe(
HANDLE hNamedPipe, // handle to named pipe
LPOVERLAPPED lpOverlapped // overlapped structure
);
1、参数说明:
lpNamedPipe : 指向一个命名管道实例的服务的句柄,该句柄由CreateNamedPipe函数返回
lpOverlapped: 指向一个OVERLAPPED结构的指针,如果hNamedPipe参数所标识的管道是用FILE_FLAG_OVERLAPPED标志打开的,则这个参数不能是NULL,必须是一个有效的指向一个OVERLAPPED的结构指针;否则函数则会错误的执行.如果hNampdPipe参数标志的管道用FILE_FLAG_OVERLAPPED标志打开的,并且这个参数不是NULL,则这个OVERLAPPED结构体必须包含人工重置对象句柄.
2、返回值
如果函数成功返回非零值如果失败返回0详细消息可以调用GetLastError函数获得
三、
WaitNamedPipe 进行命名管道连接
函数原型
BOOL WaitNamedPipe(
LPCTSTR lpNamedPipeName, // pipe name
DWORD nTimeOut // time-out interval
);
1、参数说明:
lpNamedPipeName
用来指定管道的名称,这个名称必须包括创建该命名管道的服务器进程所在的机器的名称,该名称的格式必须是"\\.\pine\pinename".
如果在同一台机器上编写的命名管道的服务器端程序和客户端程序,则应该指定这个名称时,在开始的两个反斜杆后可以设置一个圆点,表示服务器进程在本地机器上运行;如果是跨网络通信,则在这个圆点位置处应该指定服务器端程序所在的主机名
nTimeOut
指定超时间隔.
NMPWAIT_USE_DEFAULT_WAIT 超时间隔就是服务器端创建该命名管道时指定的超时值
NWPWAIT_WAIT_FOREVER 一直等待,直到出现了一个可用的命名管道的实例
也就是说,如果这个参数的值是NMPWAIT_USE_DEFAULT_WAIT,并且在服务器端调用CreateNamedPipe函数创建命名管道时,设置的超时间隔为1000ms,那么一个命名管道的所有实例来说,它们必须使用同样的超时间隔
2、返回值
如果函数成功返回非零值如果失败返回0详细消息可以调用GetLastError函数获得
1 // 客户端.cpp : Defines the entry point for the console application.
2 //
3
4 #include "stdafx.h"
5 #include <Windows.h>
6 #include <iostream>
7 using namespace std;
8
9 int main()
10 {
11 HANDLE hPipe = NULL;
12 HANDLE hEvent = NULL;
13 DWORD dwReadLen = 0;
14 DWORD dwWriteLen = 0;
15 char senbuf[] = "This is client!";
16 char rebuf[100];
17
18 //1. 连接命名管道
19 if (!WaitNamedPipe(L"\\\\.\\pipe\\Communication", NMPWAIT_WAIT_FOREVER))
20 {
21 cout << "当前没有可利用的命名管道实例!" << endl;
22 system("pause");
23 return -1;
24 }
25
26 //2. 打开命名管道
27 hPipe = CreateFile(L"\\\\.\\pipe\\Communication", GENERIC_READ | GENERIC_WRITE, 0, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);
28 if (INVALID_HANDLE_VALUE == hPipe)
29 {
30 cout << "打开命名管道失败!" << endl;
31 hPipe = NULL;
32 system("pause");
33 return -1;
34 }
35
36 //3. 读写管道数据
37 //3.1 写入数据
38 if (!WriteFile(hPipe, senbuf, strlen(senbuf) + 1, &dwWriteLen, NULL))
39 {
40 cout << "写入数据失败!" << endl;
41 system("pause");
42 return -1;
43 }
44
45 //3.2 读取数据
46 if (!ReadFile(hPipe, rebuf, 100, &dwReadLen, NULL))
47 {
48 cout << "读取数据失败!" << endl;
49 system("pause");
50 return -1;
51 }
52 cout << rebuf << endl;
53
54 system("pause");
55 return 0;
56 }
2.5 邮件槽
通信流程:
服务器端: 创建邮槽对象 -> 读取数据 -> 关闭邮槽对象
客户端:打开邮槽对象 -> 写入数据 -> 关闭邮槽对象
注意:
(1)邮槽是基于广播通信体系设计出来的,它采用无连接的不可靠的数据传输。
(2)邮槽可以实现一对多的单向通信,我们可以利用这个特点编写一个网络会议通知系统,而且实现这一的系统所需要编写的代码非常少.如果读者是项目经理,就可以给你手下每一位员工的机器上安装上这个系统中的邮槽服务器端程序,在你自己的机器上安装油槽的客户端程序,这样,当你想通知员工开会,就可以通过自己安装的邮槽客户端程序.将开会这个消息发送出去,因为机器上都安装了邮槽服务器端的程序,所以他们都能同时收到你发出的会议通知.采用邮槽实现这一的程序非常简单的,如果采用Sockets来实现这一的通信,代码会比较复杂。
(3)邮槽是一种单向通信机制,创建邮槽的服务器进程只能读取数据,打开邮槽的客户机进程只能写入数据。
(4)为保证邮槽在各种Windows平台下都能够正常工作,我们传输消息的时候,应将消息的长度限制在424字节以下。
CreateMailslot函数详解
函数原型:
HANDLE CreateMailslot(
LPCTSTR lpName, // mailslot name
DWORD nMaxMessageSize, // maximum message size
DWORD lReadTimeout, // read time-out interval
LPSECURITY_ATTRIBUTES lpSecurityAttributes // inheritance option
);
参数说明:
lpName
指向一个空终止字符串的指针,该字符串指定了油槽的名称,该名称的格式必须是:"\\.\mailslot\[path]name ",其中前两个反斜杠之后的字符表示服务器所在机器的名称,圆点表示是主机;接着是硬编码的字符串:"mailslot",这个字符串不能改变,但大小写无所谓;最后是油槽的名称([path]name)由程序员起名
nMaxMessageSize
用来指定可以被写入到油槽的单一消息的最大尺寸,为了可以发送任意大小的消息,卡伊将该参数设置为0
lReadTimeout
指定读写操作的超时间间隔,以ms为单位,读取操作在超时之前可以等待一个消息被写入到这个油槽之中.
如果这个值设置为0,那么若没有消息可用,该函数立即返回;
如果这个值设置为MAILSOT_WAIT_FOREVER,则函数一直等待,直到有消息可用
lpSecurityAttributes
指向一个SECURITY_ATTRIBUTES结构的指针,可以简单地给这个参数传递NULL值,让系统为所创建的油槽赋予默认的安全描述符
Sample Code
1 // 服务端.cpp : Defines the entry point for the console application.
2 //
3
4 #include "stdafx.h"
5 #include<windows.h>
6 #include<iostream>
7 using namespace std;
8
9 int main()
10 {
11 HANDLE hMailslot = INVALID_HANDLE_VALUE;
12 char buf[100] = { '\0' };
13 DWORD dwReadLen = 0;
14
15 //1. 创建邮槽对象
16 hMailslot = CreateMailslot(L"\\\\.\\mailslot\\Communication", 0, MAILSLOT_WAIT_FOREVER, NULL);
17 if (INVALID_HANDLE_VALUE == hMailslot)
18 {
19 cout << "创建邮槽失败!" << endl;
20 system("pause");
21 return -1;
22 }
23
24 //2. 读取数据
25 if (!ReadFile(hMailslot, buf, 100, &dwReadLen, NULL))
26 {
27 cout << "读取数据失败!" << endl;
28 CloseHandle(hMailslot);
29 system("pause");
30 return -1;
31 }
32 cout << buf << endl;
33
34 //3. 关闭邮槽对象
35 CloseHandle(hMailslot);
36
37 system("pause");
38 return 0;
39 }
1 // 客户端.cpp : Defines the entry point for the console application.
2 //
3
4 #include "stdafx.h"
5 #include<windows.h>
6 #include<iostream>
7 using namespace std;
8
9 int main()
10 {
11 HANDLE hMailslot = INVALID_HANDLE_VALUE;
12 char buf[100] = "This is a test message for Mailslot!";
13 DWORD dwWriteLen = 0;
14
15 //1. 打开邮槽对象
16 hMailslot = CreateFile(L"\\\\.\\mailslot\\Communication", GENERIC_WRITE, FILE_SHARE_READ, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);
17 if (INVALID_HANDLE_VALUE == hMailslot)
18 {
19 cout << "打开邮槽失败!" << endl;
20 system("pause");
21 return -1;
22 }
23
24 //2. 向邮槽写入数据
25 if (!WriteFile(hMailslot, buf, strlen(buf) + 1, &dwWriteLen, NULL))
26 {
27 cout << "写入数据失败!" << endl;
28 CloseHandle(hMailslot);
29 system("pause");
30 return -1;
31 }
32
33 //3. 关闭邮槽对象
34 CloseHandle(hMailslot);
35
36 system("pause");
37 return 0;
38 }
2.6 剪贴板
剪贴板(Clipped Board)实质是Win32 API中一组用来传输数据的函数和消息,为Windows应用程序之间进行数据共享提供了一个中介,Windows已建立的剪切(复制)-粘贴的机制为不同应用程序之间共享不同格式数据提供了一条捷径。当用户在应用程序中执行剪切或复制操作时,应用程序把选取的数据用一种或多种格式放在剪贴板上。然后任何其它应用程序都可以从剪贴板上拾取数据,从给定格式中选择适合自己的格式。
剪贴板是一个非常松散的交换媒介,可以支持任何数据格式,每一格式由一无符号整数标识,对标准(预定义)剪贴板格式,该值是Win32 API定义的常量;对非标准格式可以使用Register Clipboard Format函数注册为新的剪贴板格式。利用剪贴板进行交换的数据只需在数据格式上一致或都可以转化为某种格式就行。但剪贴板只能在基于Windows的程序中使用,不能在网络上使用。
剪贴板(ClipBoard)是内存中的一块区域,是Windows内置的一个非常有用的工具,通过小小的剪贴板,架起了一座彩桥,使得在各种应用程序之间,传递和共享信息成为可能。然而美中不足的是,剪贴板只能保留一份数据,每当新的数据传入,旧的便会被覆盖。
通信流程:
发送数据:
(1)打开剪贴板
(2)清空并占据剪贴板
(3)向剪贴板放入数据:
①获得一块堆内存GlobalAlloc
②锁定堆内存GlobalLock并获得堆内存首地址
③向剪贴板放入数据SetClipboardData
④释放堆内存GlobalUnlock
(4)关闭剪贴板
接受数据:
(1)打开剪贴板
(2)检查剪贴板中的数据格式
(3)从剪贴板中获取数据:
①接受数据GetClipboardData
②用GlobalLock获得地址并锁定内存
③用GlobalUnlock解除锁定内存
(4)关闭剪贴板
相关函数
显示相关函数
[打开/关闭剪贴板][清空剪贴板][向剪贴板写入数据][从剪贴板读取数据][判断剪贴板数据格式]
打开/关闭剪贴板
函数原型
//1.打开剪贴板
BOOL OpenClipboard();
//2.关闭剪贴板
BOOL CloseClipboard();
//3.
EmptyClipboard 清空剪贴板
函数原型
BOOL EmptyClipboard();
说明
只有调用了EmptyClipboard函数后,打开剪贴板的当前窗口才拥有剪贴板.EmptyClipboard函数将清空剪贴板,并释放剪贴板中的句柄,然后剪贴板的所有权分配给当前窗口.
//4.
SetClipboardData 向剪贴板写入数据
函数原型
HANDLE SetClipboardData(UINT uFormat,HANDLE hMem);
参数说明
uFormat 指定剪贴板的格式,这个格式可以是以注册的格式,或者是任一种标准的形式的剪贴板详细参见MSDN
hMem 具有指定格式的句柄.该参数可以为NULL,指示调用窗口直到有对剪贴板数据的请求时候才提供指定的剪贴板格式的数据.如果窗口采用延时提交技术,则该窗口必须处理WM_RENDERFORMAT和WM_RENDERALLFORMATS消息
返回值
如果函数成功返回的是数据句柄
如果函数失败返回的是NULL,详细消息可以调用GetLastError函数获得
说明 当前调用的SetClipboardData函数的窗口必须是剪贴板的拥有着,而且在这个之前,该程序已经调用了OpenClipboard函数打开剪贴板
当一个提供的进程创建了剪贴板数据之后,知道其他进程获取剪贴板数据前,这些数据都是要占据内存空间的,如果在剪贴板上放置的数据过大,就会浪费内存空间,降低资源利用率.为了避免这种浪费,就可以采用延迟提交技术,也就是有数据提供进程先提供一个指定格式的空剪贴板数据块,即把SetClipboardData函数的hMem参数设置为NULL.当需要获取数据的进程想要从剪贴板上得到数据时,操作系统会向数据提供进程发送WM_RENDERFORMAT消息,而数据提供进程可以响应这个消息,并在此消息的响应函数中,再一次调用SetClipboardData函数,将实际的数据放到剪贴板上,当再次调用SetClipboardData函数时就不需要调用OpenClipboard函数,也不需要调用EmptyClipboard函数.也就是说为了提高资源利用率,避免浪费内存空间,可以采用延迟提交技术.第一次调用SetClipboard函数时,将其hMem参数设置为NULL,在剪贴板上以指定的剪贴板放置一个空剪贴板数据块
//5.
GetClipboardData从剪贴板读取数据
函数原型
HANDLE GetClipboardData( UINT uFormat );
参数说明
uFormat 指定返回数据的句柄格式这个格式可以是以注册的格式,或者是任一种标准的形式的剪贴板详细参见MSDN
返回值
若函数成功返回的是剪贴板数据内容的指定格式的句柄
若函数失败返回值是NULL,详细消息可以调用GetLastError函数获得
//6.
IsClipboardFormatAvailable 判断剪贴板的数据格式
函数原型
BOOL IsClipboardFormatAvailable(UINT format);
参数说明
uFormat 判断剪贴板里的数据的句柄格式这个格式可以是以注册的格式,或者是任一种标准的形式的剪贴板详细参见MSDN
返回值
若剪贴板中的数据格式句柄为uFormat格式返回非零值
若剪贴板中的数据格式句柄不为uFormat格式返回零
Sample Code
1 // 发送数据.cpp : Defines the entry point for the console application.
2 // 发送数据:即向剪贴板中写入数据, 复制(Ctrl + C)动作
3
4 #include "stdafx.h"
5 #include <Windows.h>
6 #include <iostream>
7 using namespace std;
8
9 int main()
10 {
11 const char *pStrData = "This is a test string for Clipboard!";
12
13 //1. 打开剪贴板: 参数指定为 NULL,表明为当前进程打开剪贴板
14 if (OpenClipboard(NULL))
15 {
16 char *pDataBuf = NULL;
17
18 //2. 清空剪贴板
19 EmptyClipboard();
20
21 //3. 向剪贴板中放入数据
22 //3.1 获得一块堆内存
23 HGLOBAL hGlobalClip = GlobalAlloc(GHND, strlen(pStrData) + 1);
24
25 //3.2 锁定堆内存并获得堆内存首地址
26 pDataBuf = (char*)GlobalLock(hGlobalClip);
27 strcpy_s(pDataBuf, strlen(pStrData) + 1, pStrData);
28
29 //3.3 释放内存
30 GlobalUnlock(hGlobalClip);
31
32 //3.4 向剪贴板放入数据
33 SetClipboardData(CF_TEXT, hGlobalClip);
34
35 //4. 关闭剪贴板
36 CloseClipboard();
37 }
38
39 system("pause");
40 return 0;
41 }
1 // 接收数据.cpp : Defines the entry point for the console application.
2 // 接收数据:即从剪贴板中读取数据, 黏贴(Ctrl + V)动作
3
4 #include "stdafx.h"
5 #include <Windows.h>
6 #include <iostream>
7 using namespace std;
8
9 int main()
10 {
11 //1. 打开剪贴板: 参数指定为 NULL,表明为当前进程打开剪贴板
12 if (OpenClipboard(NULL))
13 {
14 //2. 判断剪贴板中的数据格式是否为 CF_TEXT
15 if (IsClipboardFormatAvailable(CF_TEXT))
16 {
17 //3. 从剪贴板中获取数据
18 //3.1 从剪贴板中获取格式为 CF_TEXT 的数据
19 HGLOBAL hGlobalClip = GetClipboardData(CF_TEXT);
20
21 //3.2 用GlobalLock获得地址并锁定内存
22 char *pDataBuf = NULL;
23 pDataBuf = (char *)GlobalLock(hGlobalClip);
24
25 //3.3 用GlobalUnlock解除锁定内存
26 GlobalUnlock(hGlobalClip);
27
28 cout << "从剪贴板中获取到数据是: " << pDataBuf << endl;
29 }
30
31 //4. 关闭剪贴板
32 CloseClipboard();
33 }
34
35 system("pause");
36 return 0;
37 }
2.7 信号量
什么是信号量?
信号量(Semaphore)是一个非常重要的同步机制,用于控制多个线程或进程对共享资源的访问。其基本思想是使用一个整数变量来表示可用资源的数量,通过对这个整数的操作来控制资源的分配,从而实现对竞态条件的管理。信号量的操作是原子的,即在一个操作进行的过程中,不会被其他进程或线程中断。
信号量类型
信号量主要有两种类型:
1.二进制信号量(或互斥锁):它的值只能是0或1,用来实现互斥,即在同一时刻只允许一个线程访问共享资源。
2.计数信号量:可以取一个整数值,表示可用资源的数量。这允许多个线程同时访问同一资源,但总数不超过最大资源数。
主要操作
信号量有两个基本操作,通常被称为P(等待)操作和V(释放)操作。
P操作(等待):
1.当线程想要访问资源时,它会执行P操作。
2.如果信号量的值大于0,表示有资源可用,信号量的值将减一,线程继续执行。
3.如果信号量的值为0,线程将进入阻塞状态,等待信号量值大于0。
V操作(释放):
1.当线程释放资源时,执行V操作。
2.信号量的值加一。
3.如果有其他线程因信号量的值为0而阻塞,这些线程中的一个将被唤醒。
信号量在线程和进程中的区别
信号量这一概念可以应用在线程通信和进程通信上,基本的操作是一样的,都是PV操作,但在具体的实现和使用场景上还是有很大的区别的:
线程间的信号量
1.存储位置:线程间使用的信号量通常存储在同一进程的内存空间,因为线程共享同一进程的地址空间。
2.资源共享:线程信号量主要用于同一个应用程序内部的多个线程之间的同步,因为所有线程可以直接访问到相同的内存变量和数据结构。
3.实现方式:在多线程环境中,可以使用较轻量级的同步机制,例如C++11提供的std::mutex和std::condition_variable等。
进程间的信号量
1.存储位置:进程间信号量必须存储在所有相关进程都可以访问到的位置,通常是操作系统的内核空间。这样做是为了保证数据的一致性和安全性。
2.资源共享:进程间信号量用于不同进程之间的同步,这些进程可能运行相同或不同的应用程序代码,它们彼此之间不共享内存空间。
3.实现方式:需要操作系统提供的特定API支持,例如Windows的CreateSemaphore和ReleaseSemaphore。
接口介绍
创建信号量
CreateSemaphore
功能:创建或打开一个命名或无名的信号量对象。
声明:
HANDLE CreateSemaphore(
LPSECURITY_ATTRIBUTES lpSemaphoreAttributes,
LONG lInitialCount,
LONG lMaximumCount,
LPCWSTR lpName
)
参数:
lpSemaphoreAttributes:指向一个SECURITY_ATTRIBUTES结构,该结构决定返回的句柄是否可以被子进程继承。如果为NULL,则句柄不可继承。
lInitialCount:指定信号的初始计数值。
lMaximumCount:指定信号量的最大计数值。
lpName:信号量的名称,如果为空,则创建一个无名信号量。
等待信号量
WaitForSingleObject
功能:等待信号量对象变为可用(即信号量的计数大于0),该函数就是信号量的P操作。
声明:
DWORD WaitForSingleObject(
HANDLE hHandle,
DWORD dwMilliseconds
)
参数:
hHandle:信号量对象的句柄。
dwMilliseconds:等待时间,可以是一个具体的毫秒数,或者使用特殊值INFINITE来表示无限等待。
释放信号量
ReleaseSemaphore
功能:释放信号量对象,增加信号量的计数,相当于信号量的V操作。
声明:
BOOL ReleaseSemaphore(
HANDLE hSemaphore,
LONG lReleaseCount,
LPLONG lpPreviousCount
)
参数:
hSemaphore:信号量对象的句柄。
lReleaseCount:要增加的信号量计数。
lpPreviousCount:可选参数,用于接收调用前的信号量计数。
关闭信号量句柄
CloseHandle
功能:关闭一个打开的对象句柄,例如信号量句柄。
声明:
BOOL CloseHandle( HANDLE hObject )
参数:
hObject:需要关闭的信号量句柄对象。
实现
两个进程分别对一个存储在共享内存的整数进行累加,代码如下:
#pragma once
// 进程1
#include <windows.h>
#include <iostream>
#include <ctime>
int semaphoreImpl() {
srand((unsigned)time(NULL));
// 创建或打开共享内存
HANDLE fileMapping = CreateFileMapping(
INVALID_HANDLE_VALUE,
NULL,
PAGE_READWRITE,
0,
sizeof(int),
L"SharedMemoryForInteger");
if (fileMapping == NULL) {
std::cerr << "Could not create file mapping object: " << GetLastError() << std::endl;
return 1;
}
// 映射共享内存区域
int* sharedInt = (int*)MapViewOfFile(
fileMapping,
FILE_MAP_ALL_ACCESS,
0,
0,
sizeof(int));
if (sharedInt == NULL) {
std::cerr << "Could not map view of file: " << GetLastError() << std::endl;
CloseHandle(fileMapping);
return 1;
}
// 初始化共享整数
*sharedInt = 0;
// 创建或打开信号量
HANDLE semaphore = CreateSemaphore(
NULL,
1,
1,
L"MySemaphore");
if (semaphore == NULL) {
std::cerr << "CreateSemaphore error: " << GetLastError() << std::endl;
UnmapViewOfFile(sharedInt);
CloseHandle(fileMapping);
return 1;
}
while (*sharedInt < 10) {
WaitForSingleObject(semaphore, INFINITE); // 等待信号量, 即P操作
std::cout << "Incrementor1: Current value = " << *sharedInt << ". Incrementing..." << std::endl;
(*sharedInt)++;
Sleep(rand() % 1000 + 500); // 随机延时500到1500毫秒
ReleaseSemaphore(semaphore, 1, NULL); // 释放信号量,即V操作
}
// 清理
UnmapViewOfFile(sharedInt);
CloseHandle(fileMapping);
CloseHandle(semaphore);
return 0;
}
进程2的代码和上面代码几乎一致,除了下面这行代码:
std::cout << "Incrementor2: Current value = " << *sharedInt << ". Incrementing..." << std::endl;
结果
进程1输出:
进程2输出:
原文链接:https://blog.csdn.net/LeoLei8060/article/details/139851630
2.8 动态连接库
Win32动态连接库(DLL)中的全局数据可以被调用DLL的所有进程共享,这就又给进程间通信开辟了一条新的途径,当然访问时要注意同步问题。
虽然可以通过DLL进行进程间数据共享,但从数据安全的角度考虑,我们并不提倡这种方法,使用带有访问权限控制的共享内存的方法更好一些。
2.9 远程过程调用
Win32 API提供的远程过程调用(RPC)使应用程序可以使用远程调用函数,这使在网络上用RPC进行进程通信就像函数调用那样简单。RPC既可以在单机不同进程间使用也可以在网络中使用。
由于Win32 API提供的RPC服从OSF-DCE(Open Software Foundation Distributed Computing Environment)标准。所以通过Win32 API编写的RPC应用程序能与其它操作系统上支持DEC的RPC应用程序通信。使用RPC开发者可以建立高性能、紧密耦合的分布式应用程序。
2.10 Sockets
Windows Sockets规范是以U.C.Berkeley大学BSD UNIX中流行的Socket接口为范例定义的一套Windows下的网络编程接口。除了Berkeley Socket原有的库函数以外,还扩展了一组针对Windows的函数,使程序员可以充分利用Windows的消息机制进行编程。
现在通过Sockets实现进程通信的网络应用越来越多,这主要的原因是Sockets的跨平台性要比其它IPC机制好得多,另外WinSock 2.0不仅支持TCP/IP协议,而且还支持其它协议(如IPX)。Sockets的唯一缺点是它支持的是底层通信操作,这使得在单机的进程间进行简单数据传递不太方便,这时使用下面将介绍的WM_COPYDATA消息将更合适些。
2.11 WM_COPYDATA消息
WM_COPYDATA是一种非常强大却鲜为人知的消息。当一个应用向另一个应用传送数据时,发送方只需使用调用SendMessage函数,参数是目的窗口的句柄、传递数据的起始地址、WM_COPYDATA消息。接收方只需像处理其它消息那样处理WM_COPY DATA消息,这样收发双方就实现了数据共享。
WM_COPYDATA是一种非常简单的方法,它在底层实际上是通过文件映射来实现的。它的缺点是灵活性不高,并且它只能用于Windows平台的单机环境下。
1 HWND hReceiveWnd = NULL;
2 hReceiveWnd = ::FindWindow(NULL, L"ReceiveData");
3 if (NULL == hReceiveWnd)
4 {
5 MessageBox(L"没有找到接受窗口!", L"ERROR", MB_ICONERROR);
6 return;
7 }
8
9 CString strSendData = _T("This is test for WM_COPYDATA between processes!");
10 COPYDATASTRUCT copyData = { 0 };
11 copyData.lpData = strSendData.GetBuffer();
12 copyData.cbData = (strSendData.GetLength() + 1) * sizeof(TCHAR);
13
14 ::SendMessage(hReceiveWnd, WM_COPYDATA, (WPARAM)GetSafeHwnd(), (LPARAM)©Data);
15
16 strSendData.ReleaseBuffer();
1 switch (message)
2 {
3 case WM_COPYDATA:
4 {
5 COPYDATASTRUCT *pCDS = (COPYDATASTRUCT*)lParam;
6 CString strRecvData = _T("");
7 strRecvData.Format(_T("%s"), pCDS->lpData);
8
9 MessageBox(strRecvData, L"接收到的数据", MB_OK);
10 }
11 break;
12 default:
13 break;
14 }
篇二、发送消息 WM_COPYDATA
不建议使用 SendMessage 函数发送消息
使用SendMessage函数实现进程间最简单的数据传输。SendMessage是Windows API中的一个函数,它可以将消息发送到指定窗口的消息队列中,并等待接收该消息的线程处理完毕。因为Windows中每个进程都至少拥有一个主窗口,所以可以通过在不同的进程中创建主窗口来实现进程间的通信。
使用 SendMessage 发送消息虽然简单,但也有一些缺陷:
1.同步阻塞:SendMessage 函数会阻塞发送消息的进程,直到接收到回应为止。如果接收进程无响应,发送进程就会一直阻塞。这种同步阻塞的方式可能会导致进程之间的通信效率较低。
2.只能在同一个桌面窗口之间进行通信:SendMessage 函数只能在同一个桌面窗口中的不同线程之间进行通信,不能跨越不同的桌面窗口进行通信。
3.数据大小限制:SendMessage 函数发送的数据大小不能超过 WPARAM 类型的数据大小,即 32 位系统下最大只能传输 4 字节的数据,64 位系统下最大只能传输 8 字节的数据。
WM_COPYDATA是一个Windows消息,用于在进程之间传递数据。它使用的结构体是COPYDATASTRUCT,其中包含了以下三个参数:
dwData:一个可以自定义的数据值,可以用于标识传递的数据类型或其他用途。
cbData:传递数据的字节数,不能超过64KB。
lpData:指向实际数据的指针。
使用:通过SendMessage发送
缺点:
1.数据的传输是单向的。
2.效率低:从A进程拷贝到B进程 数据拷贝了 2 次。先将数据拷贝到高 2 G内存中,然后再从 高 2 G拷贝到目标进程中(发生两次拷贝,所以效率比较低)
3.必须是带有窗口之间才能通信(且必须有标题);
原理:通过系统的高2G内存来达到传输,因为高 2 G的内存是所有引用共享的。
适用场景:数据小,发送频繁等不建议使用,大小不限制。
当一个进程发送WM_COPYDATA消息给另一个进程时,它需要填充COPYDATASTRUCT结构体的这三个参数,并使用SendMessage或PostMessage函数将消息发送给目标进程的窗口句柄。接收进程在处理WM_COPYDATA消息时,可以使用CopyMemory或其他函数从COPYDATASTRUCT结构体中读取传递的数据。
需要注意的是,因为数据是通过拷贝传递的,所以当接收进程修改数据时,不会对发送进程产生影响。另外,由于数据拷贝的性质,WM_COPYDATA在传递大量数据时可能会导致性能问题,因此需要注意数据大小的限制。
typedef struct tagCOPYDATASTRUCT {
ULONG_PTR dwData; //对缓冲区类型的描述【相当于一个标志】
DWORD cbData; //缓冲区地址
PVOID lpData; //缓冲区大小 二者合二为一描述一个缓冲区
} COPYDATASTRUCT, *PCOPYDATASTRUCT;
使用WM_COPYDATA消息进行进程间通信,我修改A进程的发送数据,B进程接收到数据会受影响吗?
如果A进程通过WM_COPYDATA消息向B进程发送数据,那么B进程接收到的数据不会受到A进程后续的修改影响。因为WM_COPYDATA消息是将A进程的数据拷贝一份并发送给B进程的,之后B进程操作的是自己的一份拷贝,和A进程的数据无关。所以,A进程修改数据不会影响B进程接收到的数据。
其中,发送进程通过发送WM_COPYDATA消息向接收进程发送数据。在接收进程接收到数据后,发生了两次内存拷贝,第一次是将数据拷贝至共享内存中,第二次是从共享内存中读取数据。最后,接收进程向发送进程发送响应消息。WM_COPYDATA可以携带少量数据;效率比较低 ,是因为WM_COPYDATA先将数据拷贝到高2G系统内存中,再从高2G内存中拷贝到目标进程中,发生两次拷贝。
WM_COPYDATA 消息进行进程间通信的主要缺陷有:
1.消息大小受限:WM_COPYDATA 消息传递的数据大小受到系统限制,默认为 4MB。如果需要传递更大的数据,就需要将数据分成多个块来传递。
2.性能问题:WM_COPYDATA 消息需要进行两次内存拷贝,一次是从发送进程的地址空间复制数据到内核缓冲区,另一次是从内核缓冲区复制数据到接收进程的地址空间,这可能会影响系统性能。
3.安全性问题:WM_COPYDATA 消息传递的数据不进行加密,如果传递的数据包含敏感信息,可能会被非法获取。
原文链接:https://blog.csdn.net/qq_61553520/article/details/134609190