进程通信

所谓进程通信,其本质就是共享内存

一,邮槽

邮槽Mailslot)是一种简单的 进程间通信(IPC)机制,它允许不同进程之间传递信息。邮槽的设计比较简单,适用于 单向通信 的场景,也就是说,一个进程只能向邮槽写入数据,另一个进程才能从中读取数据。它是 Windows 操作系统提供的一个特性。

邮槽的工作原理:

  • 服务端:创建一个邮槽(Mailslot),用于接收客户端的消息。服务端可以是一个持续运行的程序,它会持续等待其他进程向它的邮槽写入数据。
  • 客户端:客户端打开已经存在的邮槽,并向邮槽写入消息。客户端只能写入数据,无法读取数据。

特点:

  1. 单向通信:邮槽只允许单向通信,即客户端只能向邮槽写数据,服务端只能从邮槽读取数据。
  2. 广播通信:邮槽支持广播通信,一个进程可以向多个接收者发送消息。如果多个客户端监听同一个邮槽,它们会收到相同的消息。
  3. 简易性:与其他更复杂的 IPC 机制(如共享内存、管道等)相比,邮槽的实现简单,适用于需要快速且简单实现的场景。
  4. 无连接:邮槽是无连接的通信机制,客户端和服务端不需要事先建立连接,服务端只需要创建一个邮槽,客户端可以随时通过路径打开它。

使用场景:

邮槽适合用于简单的单向消息传递,例如:

  • 客户端向服务器发送一个通知或请求。
  • 服务端向多个客户端广播某个事件。

它在一些简单的应用程序或旧版 Windows 系统中可能仍然使用,但在现代应用程序中,可能会选择更灵活和高效的通信机制,如 管道网络套接字

邮槽的创建与使用:

  1. 创建邮槽:服务端创建一个邮槽,指定它的路径。
    • 使用 CreateMailslot 函数来创建邮槽。
  2. 写入数据:客户端向邮槽写入数据。
    • 使用 CreateFile 打开邮槽,并使用 WriteFile 向其写入数据。
  3. 读取数据:服务端从邮槽中读取数据。
    • 使用 ReadFile 从邮槽读取数据。

示例:

服务端代码如下:Process1.cpp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
#include <iostream>
#include <Windows.h>

/*
单向进程间的通信方式,
只有服务端才能从邮槽中去读取消息,
客户端只能写入消息

步骤:
服务器去创建邮槽
客户端去打开邮槽 ReadFile WirteFile CreateFile

*/

int main()
{
//准备一个缓冲区
CHAR szBuffer[MAX_PATH] = { 0 };


//服务器创建邮槽
HANDLE hMailsolt = CreateMailslot(L"\\\\.\\mailslot\\TesT", 0, MAILSLOT_WAIT_FOREVER, NULL);

//实际读写的字节数
DWORD dwRead;
if(!ReadFile(hMailsolt, szBuffer,MAX_PATH,&dwRead,NULL))
{
//失败了直接关闭这个句柄
CloseHandle(hMailsolt);
}
printf("%s\r\n", szBuffer);
CloseHandle(hMailsolt);
system("pause");
return 0;
}

客户端代码如下:Process2.cpp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#include <iostream>
#include <Windows.h>
int main()
{
//缓冲区
CHAR szBuffer[MAX_PATH] = "Hello World!";

//实际读取
DWORD dwRet;
//客户端,像邮槽内写入内容
HANDLE h = CreateFile(L"\\\\.\\mailslot\\TesT", GENERIC_WRITE, FILE_SHARE_READ, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);
//往里写数据
WriteFile(h, szBuffer, strlen(szBuffer) + 1, &dwRet, NULL);
CloseHandle(h);
system("pause");
return 0;

}

最后成功向邮槽中写入:Hello world!

如图:

等待写入消息:process1.cpp

成功向邮槽中写入内容:如下:

二,管道

管道(Pipe) 是一种常见的进程间通信(IPC)机制,允许不同进程之间通过共享内存区域进行数据交换。管道可以看作是一个“数据通道”,进程可以通过它将数据传递给另一个进程。

管道的类型:

  1. 匿名管道(Anonymous Pipe):
    • 只能在父子进程之间使用,即一个父进程和它的子进程之间传递数据
    • 通常用于在同一台机器上进行进程间的简单通信。
    • 数据传输是单向的,但可以通过创建两个管道(一个用于从父进程到子进程,另一个用于反向通信)来实现双向通信。
  2. 命名管道(Named Pipe):
    • 可以在任意进程之间进行通信,不限于父子进程,可以是不同计算机上的进程。
    • 它通过一个名字在系统中创建,允许不同的进程通过该名字打开管道进行数据交换。
    • 命名管道支持双向通信。

管道的工作原理:

  • 写入端(写入数据):一个进程将数据写入管道的写入端。
  • 读取端(读取数据):另一个进程从管道的读取端读取数据。

匿名管道:

匿名管道通常在父子进程之间使用,可以在创建管道时指定管道的读端和写端。匿名管道适合于同一台计算机上进程间的简单数据传输。

命名管道:

命名管道在系统中有一个名字,可以被任何进程通过该名字访问,因此它不仅可以用于本地进程之间的通信,还可以用于跨网络进行通信。命名管道可以支持双向通信,即允许数据在两个方向上流动。

管道的特点:

  1. 单向或双向通信:匿名管道通常是单向的(写入端和读取端),而命名管道可以支持双向通信。
  2. 进程间通信:管道是为了进程间的通信设计的,可以在父子进程间,或者是跨进程(甚至跨计算机)进行数据交换。
  3. 阻塞性:默认情况下,管道是阻塞的。如果管道的缓冲区满了,写操作将被阻塞;如果管道为空,读取操作会被阻塞。

管道的创建与使用:

  • 创建管道:在 Windows 中,匿名管道使用 CreatePipe 函数创建;命名管道使用 CreateNamedPipe 函数创建。
  • 读写管道:进程可以通过 ReadFileWriteFile 来进行管道的读写操作。

示例:

下面两个进程是不同进程:

process1.cpp:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
#include <iostream>
#include <Windows.h>

int main()
{
//创建管道
HANDLE hPipe = CreateNamedPipe(L"\\\\.\\pipe\\test",//管道名名字
PIPE_ACCESS_DUPLEX| FILE_FLAG_OVERLAPPED,//管道的打开方式;标志,启用重叠模式,异步
PIPE_TYPE_BYTE,//管道模式,字节流
1,//最大实例数,1个就够了
1024,//输入缓冲区的字节数
1024,//输出缓冲区的字节数
0,//超时时间,默认为50毫秒
NULL
);

HANDLE hEvent = CreateEvent(NULL, TRUE, FALSE, NULL);

OVERLAPPED lvLap;
//进行一下清空
ZeroMemory(&lvLap, sizeof(lvLap));
//然后将创建事件放入这个重叠模型中:
lvLap.hEvent = hEvent;
//连接这个管道
ConnectNamedPipe(hPipe,//连接的管道句柄
&lvLap/*重叠模型,因为其是一个异步的所以要使用一个结构体~LPOVERLAPPED~,主要是通过这个事件进行绑定
typedef struct _OVERLAPPED {
ULONG_PTR Internal;
ULONG_PTR InternalHigh;
union {
struct {
DWORD Offset;
DWORD OffsetHigh;
} DUMMYSTRUCTNAME;
PVOID Pointer;
} DUMMYUNIONNAME;

HANDLE hEvent;
} OVERLAPPED, *LPOVERLAPPED;
*/
);
//等待信号,等待其可用
WaitForSingleObject(hEvent, INFINITE);

CloseHandle(hEvent);

char szBuffer[0x100] = { 0 };
DWORD dwSize = 0;

ReadFile(hPipe,szBuffer,0x100,&dwSize,NULL);
printf("%s\r\n", szBuffer);


char WriteBuffer[] = "Hello";
DWORD dwRetSize = 0;
//写数据
WriteFile(hPipe, WriteBuffer, strlen(WriteBuffer)+1, &dwRetSize, NULL);

system("pause");
return 0;
}

process2.cpp:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
#include <iostream>
#include <Windows.h>
int main()
{

//首先是连接管道
WaitNamedPipe(L"\\\\.\\pipe\\test",
NMPWAIT_USE_DEFAULT_WAIT
);

//打开管道
HANDLE hpipe = CreateFile(L"\\\\.\\pipe\\test", GENERIC_READ | GENERIC_WRITE, NULL, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);


char WriteBuffer[] = "Hello World!";
DWORD dwRetSize = 0;
//写数据
WriteFile(hpipe, WriteBuffer, strlen(WriteBuffer) + 1, &dwRetSize, NULL);

//然后我们再读
char szBuffer[0x100] = { 0 };
DWORD dwSize = 0;

ReadFile(hpipe, szBuffer, 0x100, &dwSize, NULL);
printf("%s\r\n", szBuffer);

system("pause");
return 0;

}

最后运行结果如下:

三,剪切板

进程通信的第一种方式:剪切板

剪切板(Clipboard)是操作系统提供的一种临时存储区域,用于在不同应用程序之间传输数据。剪切板可以保存文本、图像、文件、格式化数据等信息,允许用户通过复制、剪切和粘贴操作在不同程序或不同进程之间交换数据

剪切板的工作原理:

  • 复制(Copy):用户选择要复制的数据,操作系统将其保存到剪切板。
  • 剪切(Cut):用户选择要剪切的数据,操作系统将数据从源位置移至剪切板,并标记源位置为空(如删除)。
  • 粘贴(Paste):用户从剪切板粘贴数据到目标位置,操作系统将数据从剪切板传输到目标应用程序或文件。

常见的剪切板数据类型:

  • 文本数据:普通文本(如字符串)或富文本(带格式的文本,如加粗、斜体)。
  • 图像数据:位图、图形图像(如JPEG、PNG)。
  • 文件数据:文件或文件路径。
  • 自定义数据格式:特定应用程序定义的数据格式。

使用剪切板进行进程间通信:

通过剪切板,两个进程(应用程序)可以共享数据。在一个进程中将数据放入剪切板,另一个进程可以读取这些数据。

示例:

process1.cpp:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
#include <iostream>
#include <Windows.h>
//进程通信的本质就是共享内存
/*
1.剪切板:
其是有操作系统维护的一段内存区域,
这块内存区域,每个进程都可以访问这块内存,
A进程可以打开剪切板,然后写入数据
B进程可以打开剪切板,然后获取数据
*/


int main()
{
//首先准备一个要传送的一个数据,共享数据
const char * pStr = "this is a string";//共享数据
//1.打开剪切板
if (OpenClipboard(NULL))//NULL代表当前进程打开剪切板
{
char * szBuffer = NULL;
//剪切板数据清空
EmptyClipboard();

HGLOBAL hClip = GlobalAlloc(GHND,strlen(pStr)+1);
//锁定
szBuffer = (char*)GlobalLock(hClip);
//拷贝这段内存
//strcpy(szBuffer,pStr);

//使用 strcpy_s 替代 strcpy
strcpy_s(szBuffer, strlen(pStr) + 1, pStr); // 安全的拷贝
//然后我们就可以解锁这段内存了
GlobalUnlock(hClip);

SetClipboardData(CF_TEXT, hClip);
//关闭掉
CloseClipboard();

}
system("pause");
return 0;
}

process2.cpp:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
#include <iostream>
#include <Windows.h>
int main()
{

//1.打开剪切板
if (OpenClipboard(NULL))//NULL代表当前进程打开剪切板
{
//略有不同,需要判断一下格式是否正确
if (IsClipboardFormatAvailable(CF_TEXT))
{
//获取数据:
HGLOBAL hClip = GetClipboardData(CF_TEXT);
//拷贝出这段内存
char * szBuffer = NULL;
//锁定
szBuffer = (char*)GlobalLock(hClip);
//然后我们就可以解锁这段内存了
GlobalUnlock(hClip);
//然后将其打印出来
printf("%s\r\n",szBuffer);

}
//关闭掉剪切板
CloseClipboard();

}

system("pause");
return 0;

}

我们可以直接运行这段代码,可以看到,剪切板多了如下内容:

我们再写一段代码,来读取剪切板:

process2.cpp:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
#include <iostream>
#include <Windows.h>
int main()
{

//1.打开剪切板
if (OpenClipboard(NULL))//NULL代表当前进程打开剪切板
{
//略有不同,需要判断一下格式是否正确
if (IsClipboardFormatAvailable(CF_TEXT))
{
//获取数据:
HGLOBAL hClip = GetClipboardData(CF_TEXT);
//拷贝出这段内存
char * szBuffer = NULL;
//锁定
szBuffer = (char*)GlobalLock(hClip);
//然后我们就可以解锁这段内存了
GlobalUnlock(hClip);
//然后将其打印出来
printf("%s\r\n",szBuffer);

}
else
{
printf("格式不正确\r\n");
}
//关闭掉剪切板
CloseClipboard();

}
system("pause");
return 0;

}



我们可以看到其内容如下:

四,WM_COPYDATA

进程通信的第二种方式:利用WM_COPYDATA

WM_COPYDATA 是 Windows 消息机制中的一种特殊消息,用于进程间通信。它通常用于一个进程将数据发送到另一个进程。与其他消息不同,WM_COPYDATA 允许一个进程通过一个结构体传递数据,而无需通过文件或剪贴板等中介。

基本概念:

WM_COPYDATA 消息的发送者和接收者之间通过一个 COPYDATASTRUCT 结构体进行数据交换。这个结构体包含了数据的指针数据的大小等信息。

结构体 COPYDATASTRUCT

1
2
3
4
5
6
typedef struct tagCOPYDATASTRUCT {
ULONG_PTR dwData; // 自定义的标识符,可以用于传递特定的信息(如版本号等)
DWORD cbData; // 数据的大小(字节数)
PVOID lpData; // 指向数据的指针
} COPYDATASTRUCT;

  • dwData:可以用来传递一些额外的标识信息,通常是一个指针或者整数。

  • cbData:要发送的数据的大小(字节数)。

  • lpData:指向要传输的数据的指针。

发送 WM_COPYDATA 消息:

WM_COPYDATA 消息通过 SendMessagePostMessage 发送。发送者通过该消息将数据传递给接收者。

示例:

首先我们创建两个基于对话框的新项目:如下:

创建完毕以后点击资源视图,再点击Dialog,如下:

他就会弹出一个可视化对话框,然后双击确定按钮:

然后我写入如下代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
void CwmcopydataDlg::OnBnClickedOk()
{
// TODO: 在此添加控件通知处理程序代码
//wm_copydata其就是一个消息,第一个参数:传递的窗口句柄;第二个参数:是一个结构体tagCOPYDATASTRUCT
/*
typedef struct tagCOPYDATASTRUCT {
ULONG_PTR dwData; //自定义数据
DWORD cbData; //数据的大小
PVOID lpData; //数据的指针
} COPYDATASTRUCT, *PCOPYDATASTRUCT;

其本质上来说还是共享内存
*/

//首先找到窗口句柄
HWND hRecv = ::FindWindow(NULL, _T("123"));
//发送内容
CString strDataToSend = _T("HELLO COPY DATA");

if (hRecv != NULL)//判断该窗口是否有效
{
COPYDATASTRUCT cpd;
cpd.cbData = strDataToSend.GetLength() * sizeof(TCHAR);
cpd.dwData = 0;
cpd.lpData = (PVOID)strDataToSend.GetBuffer(0);
::SendMessage(hRecv, WM_COPYDATA, (WPARAM)AfxGetApp()->m_pMainWnd, (LPARAM)&cpd);
}
CDialogEx::OnOK();
}

继续打开另外一个项目,然后右键,点击类向导

然后将其Caption设置为123,如下:

点击消息,然后输入WM_COPYDATA:如下

写入如下代码:

1
2
3
4
5
6
7
8
9
10
11
BOOL Cwmcopydata2Dlg::OnCopyData(CWnd* pWnd, COPYDATASTRUCT* pCopyDataStruct)
{
// TODO: 在此添加消息处理程序代码和/或调用默认值
LPCTSTR szText = (LPCTSTR)pCopyDataStruct->lpData;
DWORD dwLen = pCopyDataStruct->cbData;
TCHAR szRecvText[1024] = { 0 };
memcpy(szRecvText, szText, dwLen);
MessageBox(szRecvText);

return CDialogEx::OnCopyData(pWnd, pCopyDataStruct);
}

先运行后者,再运行前者,然后点击前者的确定按钮我们会发现,其弹窗在后者窗口上,成功实现了进程通信。

如下:

左侧是发送端,右侧是接受端,我们可以看到点击发送端确定按钮,就成功发送过去了。

说明:

  • 发送端使用 SendMessage 将数据通过 WM_COPYDATA 消息发送给接收端。
  • 接收端的窗口过程会处理 WM_COPYDATA 消息,提取数据并可以进行后续处理。

优点:

  • WM_COPYDATA 是一种相对简单且高效的进程间通信方式,特别适用于跨进程的数据交换。
  • 与管道、套接字等方法相比,它更适合小规模的数据传递。

注意:

  • 发送的数据需要遵循约定的格式,以便接收端能够正确解码。
  • 数据大小和类型需要通过 COPYDATASTRUCT 明确指定。

五,文件映射

进程通信的第三种方式:文件映射

文件映射(File Mapping)是Windows操作系统提供的一种机制,允许将磁盘上的文件映射到进程的虚拟内存空间使得文件内容可以像访问内存一样被进程操作。文件映射的一个重要优势是,它能够在多个进程之间共享文件内容,从而实现进程间通信(IPC),并且可以避免频繁的磁盘I/O操作,提高性能。

文件映射的工作原理:

通过文件映射,操作系统将磁盘上的文件内容映射到内存中,进程可以直接访问这些映射的内存区域,就像访问普通的内存一样。文件内容并不会立即加载到内存,而是根据访问需要按需加载。这种机制可以减少内存的消耗,尤其是在处理大文件时。

文件映射的关键概念:

  1. 文件映射对象(File Mapping Object): 文件映射对象是一个在内存中的结构,它表示了文件映射的映射区域。通过文件映射对象,操作系统能够管理文件的内存映射。
  2. 内存视图(Memory View): 内存视图是应用程序映射到其地址空间的文件的一部分。多个进程可以共享同一个文件映射对象,但每个进程都有自己的内存视图。

文件映射的主要步骤:

  1. 创建或打开文件映射对象:首先,使用 CreateFileMapping 函数创建一个文件映射对象,或者打开一个已存在的文件映射对象。
  2. 映射文件到进程的内存:通过 MapViewOfFile 函数将文件的某一部分映射到进程的虚拟地址空间。
  3. 操作映射的内存:映射到内存中的文件内容可以直接通过指针进行读写。
  4. 解除映射并关闭文件映射:操作完毕后,通过 UnmapViewOfFile 解除映射,并通过 CloseHandle 关闭文件映射对象。

常用API:

CreateFileMapping:创建或打开一个文件映射对象。

1
2
3
4
5
6
7
8
HANDLE CreateFileMapping(
HANDLE hFile, // 文件句柄
LPSECURITY_ATTRIBUTES lpAttributes,// 安全描述符
DWORD flProtect, // 文件保护标志
DWORD dwMaximumSizeHigh, // 最大文件大小(高32位)
DWORD dwMaximumSizeLow, // 最大文件大小(低32位)
LPCTSTR lpName // 映射对象的名称
);

MapViewOfFile:将文件映射到进程的内存空间。

1
2
3
4
5
6
7
LPVOID MapViewOfFile(
HANDLE hFileMappingObject, // 文件映射对象句柄
DWORD dwDesiredAccess, // 访问权限
DWORD dwFileOffsetHigh, // 文件偏移(高32位)
DWORD dwFileOffsetLow, // 文件偏移(低32位)
SIZE_T dwNumberOfBytesToMap // 映射字节数
);

UnmapViewOfFile:解除文件映射。

1
BOOL UnmapViewOfFile(LPCVOID lpBaseAddress);

CloseHandle:关闭文件映射对象。

1
BOOL CloseHandle(HANDLE hObject);

示例: