ToB企服应用市场:ToB评测及商务社交产业平台

标题: windows C++:进程间通信高及时性、安全、数据量大的通信方式(一)文件映 [打印本页]

作者: 小秦哥    时间: 2024-8-10 23:21
标题: windows C++:进程间通信高及时性、安全、数据量大的通信方式(一)文件映
       
目录
一、文件映射 (File Mapping)
1. 简单的介绍
2. 文件映射的优势
3. 必要用到的API
CreateFile
CreateFileMapping
MapViewOfFile
UnmapViewOfFile
CloseHandle
4. 安全性
二、文件映射底层原理
1. 文件系统与文件句柄
2. 创建文件映射对象
3. 内存管理器与捏造内存
4. 共享内存
5. 文件映射的拆解
关键数据结构和函数
底层调用流程图
三、父子进程间通信的示例
四、优、缺点总结
长处:
缺点:
总结
五、改进

sharedMemoryW.cpp:


        windows进程间通信是写多进程步伐的必修课,高及时性、安全、数据量大的通信方式是很必要的,今天我们来看看文件映射
一、文件映射 (File Mapping)

1. 简单的介绍

        文件映射通过将文件的部门或全部内容映射到一个或多个进程的捏造地址空间,使得这些进程可以像访问普通内存一样访问文件内容。这个过程涉及以下步骤:
2. 文件映射的优势

3. 必要用到的API

CreateFile

        用于创建或打开一个文件。
  1. HANDLE CreateFile(
  2.     LPCSTR lpFileName,
  3.     DWORD dwDesiredAccess,
  4.     DWORD dwShareMode,
  5.     LPSECURITY_ATTRIBUTES lpSecurityAttributes,
  6.     DWORD dwCreationDisposition,
  7.     DWORD dwFlagsAndAttributes,
  8.     HANDLE hTemplateFile
  9. );
复制代码
参数:

        为了确保文件映射的安全性,可以使用 Windows 的安全属性(SECURITY_ATTRIBUTES)来设置访问控制权限。如许可以限制哪些进程可以访问或修改映射的文件内容。
   为了确保文件映射的安全性,可以使用 Windows 的安全属性(SECURITY_ATTRIBUTES)来设置访问控制权限。这些安全属性答应您指定一个安全形貌符,该形貌符定义了哪些用户和组可以访问或修改映射的文件内容。以下是怎样使用 SECURITY_ATTRIBUTES 结构来设置访问控制权限的步骤:
  
  1.     // 定义安全描述符字符串 (SDDL)
  2.     // 这里设置为允许所有用户读取和写入
  3.     LPCSTR sddl = "D:P(A;;GA;;;WD)";
  4.     // 创建一个安全描述符
  5.     PSECURITY_DESCRIPTOR pSD = NULL;
  6.     if (!ConvertStringSecurityDescriptorToSecurityDescriptorA(sddl, SDDL_REVISION_1, &pSD, NULL)) {
  7.         // 处理错误
  8.         printf("ConvertStringSecurityDescriptorToSecurityDescriptorA 错误: %d\n", GetLastError());
  9.         return 1;
  10.     }
  11.     // 初始化 SECURITY_ATTRIBUTES 结构
  12.     SECURITY_ATTRIBUTES sa;
  13.     sa.nLength = sizeof(SECURITY_ATTRIBUTES);
  14.     sa.lpSecurityDescriptor = pSD;
  15.     sa.bInheritHandle = FALSE;
  16.     // 创建或打开文件
  17.     HANDLE hFile = CreateFile(
  18.         "example.txt",
  19.         GENERIC_READ | GENERIC_WRITE,
  20.         0,
  21.         &sa,  // 使用自定义的安全属性
  22.         CREATE_ALWAYS,
  23.         FILE_ATTRIBUTE_NORMAL,
  24.         NULL
  25.     );
复制代码
   关键点说明:
  
  通过上述步骤和示例代码,您可以设置文件映射的安全属性,控制哪些进程可以访问或修改映射的文件内容。
          总之,文件映射是一种高效、机动的进程间通信机制,特别得当必要处理大块数据而且要求高及时性和高安全性的场景。通过合理的权限设置和内存管理,可以充分利用文件映射的优势,提高系统的整体性能。
CreateFileMapping

        用于创建一个文件映射对象。
  1. HANDLE CreateFileMapping(
  2.     HANDLE hFile,
  3.     LPSECURITY_ATTRIBUTES lpFileMappingAttributes,
  4.     DWORD flProtect,
  5.     DWORD dwMaximumSizeHigh,
  6.     DWORD dwMaximumSizeLow,
  7.     LPCSTR lpName
  8. );
复制代码
参数:

返回值: 成功返回文件映射对象句柄,失败返回 NULL。

  1. HANDLE hFileMapping = CreateFileMapping(
  2.     hFile,
  3.     NULL,
  4.     PAGE_READWRITE,
  5.     0,
  6.     1024,
  7.     "Local\\MyFileMapping"
  8. );
  9. if (hFileMapping == NULL) {
  10.     // 处理错误
  11. }
复制代码
MapViewOfFile

        将文件映射对象的一个视图映射到进程的地址空间中。
  1. LPVOID MapViewOfFile(
  2.     HANDLE hFileMappingObject,
  3.     DWORD dwDesiredAccess,
  4.     DWORD dwFileOffsetHigh,
  5.     DWORD dwFileOffsetLow,
  6.     SIZE_T dwNumberOfBytesToMap
  7. );
复制代码
参数:

 返回值: 成功返回映射视图的指针,失败返回 NULL。
  1. LPVOID lpBaseAddress = MapViewOfFile(
  2.     hFileMapping,
  3.     FILE_MAP_READ,
  4.     0,
  5.     0,
  6.     0
  7. );
  8. if (lpBaseAddress == NULL) {
  9.     // 处理错误
  10. }
复制代码

UnmapViewOfFile

        排除文件视图的映射。
  1. BOOL UnmapViewOfFile(
  2.     LPCVOID lpBaseAddress
  3. );
复制代码
  1. if (!UnmapViewOfFile(lpBaseAddress)) {
  2.     // 处理错误
  3. }
复制代码
CloseHandle

        关闭文件、文件映射对象或文件视图的句柄。
  1. BOOL CloseHandle(
  2.     HANDLE hObject
  3. );
复制代码
  1. if (!CloseHandle(hFile)) {
  2.     // 处理错误
  3. }
  4. if (!CloseHandle(hFileMapping)) {
  5.     // 处理错误
  6. }
复制代码
4. 安全性

        为了确保文件映射的安全性,可以使用 Windows 的安全属性(SECURITY_ATTRIBUTES)来设置访问控制权限。如许可以限制哪些进程可以访问或修改映射的文件内容。
        总之,文件映射是一种高效、机动的进程间通信机制,特别得当必要处理大块数据而且要求高及时性和高安全性的场景。通过合理的权限设置和内存管理,可以充分利用文件映射的优势,提高系统的整体性能。

二、文件映射底层原理

        在 Windows 底层,文件映射的实现涉及多个关键组件和机制,包罗内存管理器、文件系统、内核对象和捏造内存管理。以下是 Windows 底层实现文件映射的主要步骤和机制:
1. 文件系统与文件句柄

        当应用步伐调用 CreateFile 函数时,Windows 内核通过文件系统驱动步伐将文件打开并创建一个文件句柄。文件系统驱动步伐负责处理文件的底层访问,包罗读写操作、权限查抄和文件缓存。
2. 创建文件映射对象

        调用 CreateFileMapping 时,Windows 内核创建一个文件映射对象。这涉及到以下几个步骤:

3. 内存管理器与捏造内存

        当应用步伐调用 MapViewOfFile 时,Windows 内核的内存管理器开始工作:

4. 共享内存

        多个进程可以通过文件映射对象共享内存。不同进程调用 OpenFileMapping 和 MapViewOfFile 来访问同一个文件映射对象:

5. 文件映射的拆解

        当进程调用 UnmapViewOfFile 时,内存管理器排除文件视图的映射:

关键数据结构和函数


底层调用流程图

  1. graph TD;
  2.     A[应用程序] -->|调用 CreateFile| B[文件系统驱动]
  3.     B -->|返回文件句柄| A
  4.     A -->|调用 CreateFileMapping| C[内核]
  5.     C -->|创建文件映射对象| D[内存管理器]
  6.     D -->|建立文件与内存关联| C
  7.     A -->|调用 MapViewOfFile| D
  8.     D -->|分配虚拟地址空间| A
  9.     A -->|访问映射视图| D
  10.     D -->|通过页错误加载内容| E[物理内存]
  11.     A -->|调用 UnmapViewOfFile| D
  12.     D -->|解除映射视图| A
  13.     C -->|引用计数清零时释放资源| D
复制代码
三、父子进程间通信的示例

fileMap底子共享通信代码:
  1. #include <windows.h>
  2. #include <iostream>
  3. #include <string>
  4. #include <stdexcept>
  5. #include <memory>
  6. class SharedMemory {
  7. public:
  8.     SharedMemory(const std::wstring& name, size_t size);
  9.     ~SharedMemory();
  10.     bool create();
  11.     bool open();
  12.     void write(const std::wstring& data);
  13.     std::wstring read();
  14.     void close();
  15.     void signal();  // 用于通知另一个进程数据已写入
  16.     void wait();    // 用于等待另一个进程写入数据
  17. private:
  18.     std::wstring name_;
  19.     size_t size_;
  20.     HANDLE hFile_;
  21.     HANDLE hMapFile_;
  22.     LPVOID lpBase_;
  23.     HANDLE hEvent_;
  24.     void cleanup();
  25.     void checkAndThrow(bool condition, const std::wstring& errorMessage);
  26. };
  27. #include "sharedMemoryW.h"
  28. // 构造函数
  29. SharedMemory::SharedMemory(const std::wstring& name, size_t size)
  30.     : name_(name), size_(size), hFile_(NULL), hMapFile_(NULL), lpBase_(NULL), hEvent_(NULL) {}
  31. // 析构函数
  32. SharedMemory::~SharedMemory() {
  33.     close();
  34. }
  35. // 创建文件映射对象和事件对象
  36. bool SharedMemory::create() {
  37.     hFile_ = CreateFileW(name_.c_str(), GENERIC_READ | GENERIC_WRITE, 0, NULL, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL);
  38.     checkAndThrow(hFile_ != INVALID_HANDLE_VALUE, L"Unable to create file.");
  39.     hMapFile_ = CreateFileMappingW(hFile_, NULL, PAGE_READWRITE, 0, static_cast<DWORD>(size_), name_.c_str());
  40.     checkAndThrow(hMapFile_ != NULL, L"Unable to create file mapping object.");
  41.     lpBase_ = MapViewOfFile(hMapFile_, FILE_MAP_ALL_ACCESS, 0, 0, size_);
  42.     checkAndThrow(lpBase_ != NULL, L"Unable to map view of file.");
  43.     hEvent_ = CreateEventW(NULL, TRUE, FALSE, (name_ + L"_Event").c_str());
  44.     checkAndThrow(hEvent_ != NULL, L"Unable to create event object.");
  45.     return true;
  46. }
  47. // 打开现有的文件映射对象和事件对象
  48. bool SharedMemory::open() {
  49.     hMapFile_ = OpenFileMappingW(FILE_MAP_ALL_ACCESS, FALSE, name_.c_str());
  50.     checkAndThrow(hMapFile_ != NULL, L"Unable to open file mapping object.");
  51.     lpBase_ = MapViewOfFile(hMapFile_, FILE_MAP_ALL_ACCESS, 0, 0, size_);
  52.     checkAndThrow(lpBase_ != NULL, L"Unable to map view of file.");
  53.     hEvent_ = OpenEventW(EVENT_ALL_ACCESS, FALSE, (name_ + L"_Event").c_str());
  54.     checkAndThrow(hEvent_ != NULL, L"Unable to open event object.");
  55.     return true;
  56. }
  57. // 写入数据到映射内存
  58. void SharedMemory::write(const std::wstring& data) {
  59.     if (lpBase_ != NULL) {
  60.         memcpy(lpBase_, data.c_str(), (data.size() + 1) * sizeof(wchar_t));  // 包括终止符
  61.         signal(); // 通知另一个进程数据已写入
  62.     }
  63. }
  64. // 读取数据从映射内存
  65. std::wstring SharedMemory::read() {
  66.     if (lpBase_ != NULL) {
  67.         return std::wstring(static_cast<wchar_t*>(lpBase_));
  68.     }
  69.     return L"";
  70. }
  71. // 关闭文件映射和事件对象
  72. void SharedMemory::close() {
  73.     cleanup();
  74. }
  75. // 用于通知另一个进程数据已写入
  76. void SharedMemory::signal() {
  77.     if (hEvent_ != NULL) {
  78.         SetEvent(hEvent_);
  79.     }
  80. }
  81. // 用于等待另一个进程写入数据
  82. void SharedMemory::wait() {
  83.     if (hEvent_ != NULL) {
  84.         WaitForSingleObject(hEvent_, INFINITE);
  85.     }
  86. }
  87. void SharedMemory::cleanup() {
  88.     if (lpBase_ != NULL) {
  89.         UnmapViewOfFile(lpBase_);
  90.         lpBase_ = NULL;
  91.     }
  92.     if (hMapFile_ != NULL) {
  93.         CloseHandle(hMapFile_);
  94.         hMapFile_ = NULL;
  95.     }
  96.     if (hFile_ != NULL) {
  97.         CloseHandle(hFile_);
  98.         hFile_ = NULL;
  99.     }
  100.     if (hEvent_ != NULL) {
  101.         CloseHandle(hEvent_);
  102.         hEvent_ = NULL;
  103.     }
  104. }
  105. void SharedMemory::checkAndThrow(bool condition, const std::wstring& errorMessage) {
  106.     if (!condition) {
  107.         DWORD errorCode = GetLastError();
  108.         throw std::runtime_error(std::string(errorMessage.begin(), errorMessage.end()) + " Error code: " + std::to_string(errorCode));
  109.     }
  110. }
复制代码
        父进程:
  1. #include <windows.h>
  2. #include <iostream>
  3. #include <string>
  4. #include "sharedMemoryW.h"
  5. void CreateChildProcess(const std::wstring& sharedMemoryName, size_t sharedMemorySize) {
  6.     STARTUPINFOW si;
  7.     PROCESS_INFORMATION pi;
  8.     ZeroMemory(&si, sizeof(si));
  9.     si.cb = sizeof(si);
  10.     ZeroMemory(&pi, sizeof(pi));
  11.     std::wstring commandLine = L"ChildProcess.exe " + sharedMemoryName + L" " + std::to_wstring(sharedMemorySize);
  12.     if (!CreateProcessW(NULL, &commandLine[0], NULL, NULL, FALSE, 0, NULL, NULL, &si, &pi)) {
  13.         std::wcerr << L"CreateProcess failed (" << GetLastError() << L")." << std::endl;
  14.         return;
  15.     }
  16.     CloseHandle(pi.hProcess);
  17.     CloseHandle(pi.hThread);
  18. }
  19. int main() {
  20.     const std::wstring sharedMemoryName = L"SharedMemoryExample";
  21.     const size_t sharedMemorySize = 1024;
  22.     SharedMemory sharedMemory(sharedMemoryName, sharedMemorySize);
  23.     try {
  24.         sharedMemory.create();
  25.     }
  26.     catch (const std::exception& e) {
  27.         std::cerr << "Error: " << e.what() << std::endl;
  28.         return 1;
  29.     }
  30.     CreateChildProcess(sharedMemoryName, sharedMemorySize);
  31.     std::wstring message = L"Hello from Parent Process";
  32.     sharedMemory.write(message);
  33.     std::wcout << L"Parent process waiting for child process to read the message..." << std::endl;
  34.     sharedMemory.wait();  // 等待子进程读取数据
  35.     std::wcout << L"Parent process read: " << sharedMemory.read() << std::endl;
  36.     sharedMemory.close();
  37.     system("pause");
  38.     return 0;
  39. }
复制代码


子进程:
  1. #include <windows.h>
  2. #include <iostream>
  3. #include <string>
  4. #include "sharedMemoryW.h" // 假设SharedMemory类的代码放在这个头文件中
  5. int wmain(int argc, wchar_t* argv[]) {
  6.     if (argc != 3) {
  7.         std::wcerr << L"Usage: ChildProcess <SharedMemoryName> <SharedMemorySize>" << std::endl;
  8.         return 1;
  9.     }
  10.     std::wstring sharedMemoryName = argv[1];
  11.     size_t sharedMemorySize = std::stoull(argv[2]);
  12.     SharedMemory sharedMemory(sharedMemoryName, sharedMemorySize);
  13.     try {
  14.         sharedMemory.open();
  15.     }
  16.     catch (const std::exception& e) {
  17.         std::cerr << "Error: " << e.what() << std::endl;
  18.         return 1;
  19.     }
  20.     std::wcout << L"Child process read: " << sharedMemory.read() << std::endl;
  21.     std::wstring response = L"Hello from Child Process";
  22.     sharedMemory.write(response);
  23.     std::wcout << L"Child process waiting for parent process to read the message..." << std::endl;
  24.     sharedMemory.signal();  // 通知父进程数据已写入
  25.     sharedMemory.close();
  26.     system("pause");
  27.     return 0;
  28. }
复制代码

更多代码请访问我的GitHub:
   GitHub - bowenliu1996/SharedMemoryW: Windows file mapping communication, including demo
  
四、优、缺点总结

长处:

缺点:

总结

        共享内存通信在高效数据传输方面有显着优势,但其复杂的同步机制、内存管理、安全性标题、平台依靠性和调试困难都是使用过程中必须慎重考虑的缺点。这些缺点不仅增长了开发和维护的难度,还大概带来潜在的系统稳定性和安全性风险。因此,在使用共享内存时,必要综合权衡其优缺点,选择得当具体应用场景的通信机制。

五、改进

        根据复杂的同步机制、内存管理难度、平台依靠性、安全性标题、调试困难等标题。
我们优化代码:
sharedMemoryW.h
  1. #include <windows.h>
  2. #include <iostream>
  3. #include <string>
  4. #include <stdexcept>
  5. #include <memory>
  6. class SharedMemory {
  7. public:
  8.     SharedMemory(const std::wstring& name, size_t size);
  9.     ~SharedMemory();
  10.     bool create();
  11.     bool open();
  12.     void write(const std::wstring& data);
  13.     std::wstring read();
  14.     void close();
  15.     void signal();  // 用于通知另一个进程数据已写入
  16.     void wait();    // 用于等待另一个进程写入数据
  17. private:
  18.     std::wstring name_;
  19.     size_t size_;
  20.     HANDLE hFile_;
  21.     HANDLE hMapFile_;
  22.     LPVOID lpBase_;
  23.     HANDLE hEvent_;
  24.     HANDLE hMutex_;  // 新增的互斥锁
  25.     void cleanup();
  26.     void checkAndThrow(bool condition, const std::wstring& errorMessage);
  27. };
复制代码


sharedMemoryW.cpp:

  1. #include "sharedMemoryW.h"
  2. // 构造函数
  3. SharedMemory::SharedMemory(const std::wstring& name, size_t size)
  4.     : name_(name), size_(size), hFile_(NULL), hMapFile_(NULL), lpBase_(NULL), hEvent_(NULL), hMutex_(NULL) {}
  5. // 析构函数
  6. SharedMemory::~SharedMemory() {
  7.     close();
  8. }
  9. // 创建文件映射对象和事件对象
  10. bool SharedMemory::create() {
  11.     SECURITY_ATTRIBUTES sa;
  12.     sa.nLength = sizeof(SECURITY_ATTRIBUTES);
  13.     sa.bInheritHandle = FALSE;
  14.     sa.lpSecurityDescriptor = NULL;  // 可以自定义安全描述符
  15.     hFile_ = CreateFileW(name_.c_str(), GENERIC_READ | GENERIC_WRITE, 0, &sa, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL);
  16.     checkAndThrow(hFile_ != INVALID_HANDLE_VALUE, L"Unable to create file.");
  17.     hMapFile_ = CreateFileMappingW(hFile_, NULL, PAGE_READWRITE, 0, static_cast<DWORD>(size_), name_.c_str());
  18.     checkAndThrow(hMapFile_ != NULL, L"Unable to create file mapping object.");
  19.     lpBase_ = MapViewOfFile(hMapFile_, FILE_MAP_ALL_ACCESS, 0, 0, size_);
  20.     checkAndThrow(lpBase_ != NULL, L"Unable to map view of file.");
  21.     hEvent_ = CreateEventW(&sa, TRUE, FALSE, (name_ + L"_Event").c_str());
  22.     checkAndThrow(hEvent_ != NULL, L"Unable to create event object.");
  23.     hMutex_ = CreateMutexW(&sa, FALSE, (name_ + L"_Mutex").c_str());
  24.     checkAndThrow(hMutex_ != NULL, L"Unable to create mutex object.");
  25.     return true;
  26. }
  27. // 打开现有的文件映射对象和事件对象
  28. bool SharedMemory::open() {
  29.     hMapFile_ = OpenFileMappingW(FILE_MAP_ALL_ACCESS, FALSE, name_.c_str());
  30.     checkAndThrow(hMapFile_ != NULL, L"Unable to open file mapping object.");
  31.     lpBase_ = MapViewOfFile(hMapFile_, FILE_MAP_ALL_ACCESS, 0, 0, size_);
  32.     checkAndThrow(lpBase_ != NULL, L"Unable to map view of file.");
  33.     hEvent_ = OpenEventW(EVENT_ALL_ACCESS, FALSE, (name_ + L"_Event").c_str());
  34.     checkAndThrow(hEvent_ != NULL, L"Unable to open event object.");
  35.     hMutex_ = OpenMutexW(MUTEX_ALL_ACCESS, FALSE, (name_ + L"_Mutex").c_str());
  36.     checkAndThrow(hMutex_ != NULL, L"Unable to open mutex object.");
  37.     return true;
  38. }
  39. // 写入数据到映射内存
  40. void SharedMemory::write(const std::wstring& data) {
  41.     WaitForSingleObject(hMutex_, INFINITE);  // 加锁
  42.     if (lpBase_ != NULL) {
  43.         memcpy(lpBase_, data.c_str(), (data.size() + 1) * sizeof(wchar_t));  // 包括终止符
  44.         signal(); // 通知另一个进程数据已写入
  45.         std::wcout << L"Data written: " << data << std::endl;  // 日志记录
  46.     }
  47.     ReleaseMutex(hMutex_);  // 解锁
  48. }
  49. // 读取数据从映射内存
  50. std::wstring SharedMemory::read() {
  51.     WaitForSingleObject(hMutex_, INFINITE);  // 加锁
  52.     std::wstring result;
  53.     if (lpBase_ != NULL) {
  54.         result = std::wstring(static_cast<wchar_t*>(lpBase_));
  55.         std::wcout << L"Data read: " << result << std::endl;  // 日志记录
  56.     }
  57.     ReleaseMutex(hMutex_);  // 解锁
  58.     return result;
  59. }
  60. // 关闭文件映射和事件对象
  61. void SharedMemory::close() {
  62.     cleanup();
  63. }
  64. // 用于通知另一个进程数据已写入
  65. void SharedMemory::signal() {
  66.     if (hEvent_ != NULL) {
  67.         SetEvent(hEvent_);
  68.         ResetEvent(hEvent_);  // 重置事件
  69.     }
  70. }
  71. // 用于等待另一个进程写入数据
  72. void SharedMemory::wait() {
  73.     if (hEvent_ != NULL) {
  74.         WaitForSingleObject(hEvent_, INFINITE);
  75.     }
  76. }
  77. void SharedMemory::cleanup() {
  78.     if (lpBase_ != NULL) {
  79.         UnmapViewOfFile(lpBase_);
  80.         lpBase_ = NULL;
  81.     }
  82.     if (hMapFile_ != NULL) {
  83.         CloseHandle(hMapFile_);
  84.         hMapFile_ = NULL;
  85.     }
  86.     if (hFile_ != NULL) {
  87.         CloseHandle(hFile_);
  88.         hFile_ = NULL;
  89.     }
  90.     if (hEvent_ != NULL) {
  91.         CloseHandle(hEvent_);
  92.         hEvent_ = NULL;
  93.     }
  94.     if (hMutex_ != NULL) {
  95.         CloseHandle(hMutex_);
  96.         hMutex_ = NULL;
  97.     }
  98. }
  99. void SharedMemory::checkAndThrow(bool condition, const std::wstring& errorMessage) {
  100.     if (!condition) {
  101.         DWORD errorCode = GetLastError();
  102.         throw std::runtime_error(std::string(errorMessage.begin(), errorMessage.end()) + " Error code: " + std::to_string(errorCode));
  103.     }
  104. }
复制代码


        如许改进优化,代码似乎安全了些。。。

免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。




欢迎光临 ToB企服应用市场:ToB评测及商务社交产业平台 (https://dis.qidao123.com/) Powered by Discuz! X3.4