• Windows进程间的通信


    一、进程与进程通信 

        进程间通信(Interprocess Communication, IPC)是指不同的进程之间进行数据共享和数据交换。

     二、进程间通信方式 

       1.  文件映射

        注:文件映射是在多个进程间共享数据的非常有效方法,有较好的安全性。但文件映射只能用于本地机器的进程之间,不能用于网络中,而开发者还必须控制进程间的同步

        使用内存映射文件的一般流程:(reference from: https://blog.csdn.net/qq_20183489/article/details/54646794)

        

        另外,内存映射文件在处理大数据量的文件时表现出了良好的性能(实际上,文件越大,内存映射的优势就越明显)。示例可以参考:https://blog.csdn.net/zzq060143/article/details/54619571

      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).
    "), 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). 
    "), 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 }
    View Code
     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).
    "), 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). 
    "), 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("
    ");
    40 
    41     //4. 从进程的地址空间中撤消文件数据的映像
    42     UnmapViewOfFile(pBuf);
    43 
    44     //5. 关闭文件映射对象和文件对象
    45     CloseHandle(hMapFile);
    46 
    47     getchar();
    48     return 0;
    49 }
    View Code

       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,系统会使用默认的缓冲区大小

         返回值

           若函数成功返回非零值

          若函数失败返回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 }
    View 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 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 }
    View Code

         (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
    );
    
    参数说明:
    
    lpName
    一个指向空终止的字符串,该字符串的格式必须是:"\.pinepinename"其中该字符串开始是两个连续的反斜杠,其后的原点表示是本地机器,如果想要与远程的服务器建立连接连接,那么在原点这个位置应该指定这个远程服务器的名称.接下来是"pine"这个固定的字符串,也就是说这个字符串的内容不能修改,但其大小写是无所谓的,最后是所创建的命名管道的名称
    
    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)可以进行写入访问
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    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    使用非等待模式(非阻塞方式),在读,写和建立连接时不需要管道的另一方完成相应动作后就会立即返回
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    nMaxInstances
    为管道的的最大数量,在第一次建立服务器方管道时这个参数表明该管道可以同时存在的数量。PIPE_UNLIMITED_INSTANCES表明不对数量进行限制
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    nOutBufferSize
    表示输出缓冲区的大小
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    nInBufferSize
    表示输入缓冲区的大小
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    nDefaultTimeOut
    表示在等待连接时最长的等待时间(以毫秒为单位),如果在创建时设置为NMPWAIT_USE_DEFAULT_WAIT表明无限制的等待,而以后服务器方的其他管道实例也需要设置相同的值
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    lpSecurityAttributes
    为安全属性,一般设置为NULL。如果创建或打开失败则返回INVALID_HANDLE_VALUE。可以通过GetLastError得到错误
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    返回值
    
    若函数成功,返回值是一个命名通道实例的句柄,如果命名通道已经存在则返回一个以存在的命名通道的句柄,并调用GetLastError函数的返回值为 ERROR_ALREADY_EXISTS
    若函数失败,返回值为INVALID_HANDLE_VALUE若想获得更多信息调用GetLastError函数获得
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    ConnectNamePipe 创建命名管道连接
    函数原型
    
    BOOL ConnectNamedPipe(
      HANDLE hNamedPipe, // handle to named pipe
      LPOVERLAPPED lpOverlapped // overlapped structure
    );
    
    参数说明:
    
    lpNamedPipe :  指向一个命名管道实例的服务的句柄,该句柄由CreateNamedPipe函数返回
    lpOverlapped:  指向一个OVERLAPPED结构的指针,如果hNamedPipe参数所标识的管道是用FILE_FLAG_OVERLAPPED标志打开的,则这个参数不能是NULL,必须是一个有效的指向一个OVERLAPPED的结构指针;否则函数则会错误的执行.如果hNampdPipe参数标志的管道用FILE_FLAG_OVERLAPPED标志打开的,并且这个参数不是NULL,则这个OVERLAPPED结构体必须包含人工重置对象句柄.
    
    返回值
    如果函数成功返回非零值如果失败返回0详细消息可以调用GetLastError函数获得
    WaitNamedPipe 进行命名管道连接
    函数原型
    
    BOOL WaitNamedPipe(
    LPCTSTR lpNamedPipeName, // pipe name
    DWORD nTimeOut // time-out interval
    );
    
    参数说明:
    
    lpNamedPipeName
    用来指定管道的名称,这个名称必须包括创建该命名管道的服务器进程所在的机器的名称,该名称的格式必须是"\.pinepinename".如果在同一台机器上编写的命名管道的服务器端程序和客户端程序,则应该指定这个名称时,在开始的两个反斜杆后可以设置一个圆点,表示服务器进程在本地机器上运行;如果是跨网络通信,则在这个圆点位置处应该指定服务器端程序所在的主机名
    
    nTimeOut
    指定超时间隔.
    
    
    
    NMPWAIT_USE_DEFAULT_WAIT 超时间隔就是服务器端创建该命名管道时指定的超时值
    NWPWAIT_WAIT_FOREVER 一直等待,直到出现了一个可用的命名管道的实例
    
    
    
    也就是说,如果这个参数的值是NMPWAIT_USE_DEFAULT_WAIT,并且在服务器端调用CreateNamedPipe函数创建命名管道时,设置的超时间隔为1000ms,那么一个命名管道的所有实例来说,它们必须使用同样的超时间隔
    
    
    返回值
    如果函数成功返回非零值如果失败返回0详细消息可以调用GetLastError函数获得
    View Code

           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 hPipe  = NULL;
    12     HANDLE hEvent = NULL;
    13     DWORD  dwReadLen  = 0;
    14     DWORD  dwWriteLen = 0;
    15     OVERLAPPED ovlap;
    16     char senbuf[] = "This is server!";
    17     char rebuf[100];
    18 
    19     //1. 创建命名管道
    20     hPipe = CreateNamedPipe(L"\\.\pipe\Communication", PIPE_ACCESS_DUPLEX | FILE_FLAG_OVERLAPPED, 0, 1, 1024, 1024, 0, NULL);
    21     if (INVALID_HANDLE_VALUE == hPipe)
    22     {
    23         cout << "创建命名管道失败!" << endl;
    24         hPipe = NULL;
    25         system("pause");
    26         return -1;
    27     }
    28 
    29     hEvent = CreateEvent(NULL, TRUE, FALSE, NULL);
    30     if (NULL == hEvent)
    31     {
    32         cout << "创建事件对象失败!" << endl;
    33         CloseHandle(hPipe);
    34         hPipe = NULL;
    35         system("pause");
    36         return -1;
    37     }
    38 
    39     ZeroMemory(&ovlap, sizeof(OVERLAPPED));
    40     ovlap.hEvent = hEvent;
    41 
    42     //2. 创建管道连接
    43     if (!ConnectNamedPipe(hPipe, &ovlap))
    44     {
    45         if (ERROR_IO_PENDING != GetLastError())
    46         {
    47             cout << "等待客户端连接失败!" << endl;
    48             CloseHandle(hPipe);
    49             CloseHandle(hEvent);
    50             hPipe = NULL;
    51             system("pause");
    52             return -1;
    53         }
    54     }    
    55 
    56     //3. 等待客户端连接
    57     if ( WAIT_FAILED == WaitForSingleObject(hEvent, INFINITE))
    58     {
    59         cout << "等待对象失败!" << endl;
    60         CloseHandle(hPipe);
    61         CloseHandle(hEvent);
    62         hPipe = NULL;
    63         system("pause");
    64         return -1;
    65     }
    66     CloseHandle(hEvent);
    67 
    68     //4. 读写管道数据
    69     //4.1 读取数据
    70     if (!ReadFile(hPipe, rebuf, 100, &dwReadLen, NULL))
    71     {
    72         cout << "读取数据失败!" << endl;
    73         system("pause");
    74         return -1;
    75     }
    76     cout << rebuf << endl;
    77 
    78     //4.2 写入数据
    79     if (!WriteFile(hPipe, senbuf, (DWORD)strlen(senbuf) + 1, &dwWriteLen, NULL))
    80     {
    81         cout << "写入数据失败!" << endl;
    82         system("pause");
    83         return -1;
    84     }
    85 
    86     system("pause");
    87     return 0;
    88 }
    View 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 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 }
    View Code

      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
    );
    View Code

      参数说明:
      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] = { '' };
    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 }
    View 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] = "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 }
    View Code

       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)关闭剪贴板

       相关函数

    显示相关函数
    
    [打开/关闭剪贴板][清空剪贴板][向剪贴板写入数据][从剪贴板读取数据][判断剪贴板数据格式]
    打开/关闭剪贴板
    函数原型
    
    //打开剪贴板
    BOOL OpenClipboard();
    //关闭剪贴板
    BOOL CloseClipboard(); 
    
    EmptyClipboard 清空剪贴板
    函数原型
    
    BOOL EmptyClipboard();
    
    说明
    只有调用了EmptyClipboard函数后,打开剪贴板的当前窗口才拥有剪贴板.EmptyClipboard函数将清空剪贴板,并释放剪贴板中的句柄,然后剪贴板的所有权分配给当前窗口.
    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,在剪贴板上以指定的剪贴板放置一个空剪贴板数据块
    GetClipboardData从剪贴板读取数据
    函数原型
    
    HANDLE GetClipboardData( UINT uFormat );
    
    参数说明
    
     uFormat  指定返回数据的句柄格式这个格式可以是以注册的格式,或者是任一种标准的形式的剪贴板详细参见MSDN
    
    返回值
    若函数成功返回的是剪贴板数据内容的指定格式的句柄
    若函数失败返回值是NULL,详细消息可以调用GetLastError函数获得
    IsClipboardFormatAvailable 判断剪贴板的数据格式
    函数原型
    
    BOOL IsClipboardFormatAvailable(UINT format);
    
    参数说明
    
     uFormat  判断剪贴板里的数据的句柄格式这个格式可以是以注册的格式,或者是任一种标准的形式的剪贴板详细参见MSDN
    
    返回值
    若剪贴板中的数据格式句柄为uFormat格式返回非零值
    若剪贴板中的数据格式句柄不为uFormat格式返回零
    View Code

        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 }
    View Code
     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 }
    View Code

      2.7 动态连接库
        Win32动态连接库(DLL)中的全局数据可以被调用DLL的所有进程共享,这就又给进程间通信开辟了一条新的途径,当然访问时要注意同步问题。
        虽然可以通过DLL进行进程间数据共享,但从数据安全的角度考虑,我们并不提倡这种方法,使用带有访问权限控制的共享内存的方法更好一些。

      2.8 远程过程调用
        Win32 API提供的远程过程调用(RPC)使应用程序可以使用远程调用函数,这使在网络上用RPC进行进程通信就像函数调用那样简单。RPC既可以在单机不同进程间使用也可以在网络中使用。
        由于Win32 API提供的RPC服从OSF-DCE(Open Software Foundation Distributed Computing Environment)标准。所以通过Win32 API编写的RPC应用程序能与其它操作系统上支持DEC的RPC应用程序通信。使用RPC开发者可以建立高性能、紧密耦合的分布式应用程序。

      2.9 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.10 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)&copyData);
    15 
    16     strSendData.ReleaseBuffer();
    View Code
     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     }
    View Code
  • 相关阅读:
    【ABAP系列】SAP LSMW(摘自官网)
    【ABAP系列】SAP ABAP POPUP弹出框自建内容
    【ABAP系列】SAP ABAP ALV中的TOP_OF_PAGE添加任意图标
    彻底关闭Windows Defender丨Win10
    word中怎样设置页码包含总页数
    10款流行的Markdown编辑器,总有一款适合你
    MyEclipse安装插件
    Eclipse集成SonarLint
    MyEclipse中阿里JAVA代码规范插件(P3C)的安装及使用
    详述 IntelliJ IDEA 插件的安装及使用方法
  • 原文地址:https://www.cnblogs.com/YQ2014/p/9151813.html
Copyright © 2020-2023  润新知