【小沐学C++】C++实现进程通信(管道pipe)

61 篇文章 44 订阅
16 篇文章 0 订阅

《吃鱼篇》

狄狄:最近什么都奇奇怪怪的。
戈戈:这世界本就奇怪。
狄狄:不是说八点半?
戈戈:狄狄每天第一个来,最后一个走!
狄狄:算了,最后一次了
戈戈:啊?!
狄狄:时间就是money,浪费时间等于浪费生命。
戈戈:赞同。
狄狄:该花的钱还得花。
戈戈:得先有才行。
狄狄:看来真的很喜欢。
戈戈:过了这个村,没这个店了。
狄狄:听到要排队,我就头大。
戈戈:您什么时候不头大?
狄狄:人为刀殂我为鱼肉。咱得了解一点刀的嘛。
戈戈:那您想做酸菜鱼,还是清蒸鱼?
狄狄:我想做刀。
戈戈:发给了个红包给您,去买条鱼🐟吧。
狄狄:完了,您也变成刀殂了。
戈戈:五一去哪里玩?
狄狄:在家吃鱼。
戈戈:您的铂金快掉下去啦。
狄狄:咱钻石!
戈戈:啧啧。

在这里插入图片描述

1、功能简介

管道用于进程间共享数据,其实质是共享内存,常用IPC之一。管道不仅可以用于本机进程间通信,还可实现跨网络进程间通信,如同Socket通信,管道同样封装计算机底层网络实现,提供一个良好的API接口。
在这里插入图片描述

管道(Pipe)实际是用于进程间通信的一段共享内存,创建管道的进程称为管道服务器,连接到一个管道的进程为管道客户机。一个进程在向管道写入数据后,另一进程就可以从管道的另一端将其读取出来。
在这里插入图片描述

管道分为匿名管道和命名管道。

1.1 匿名管道

匿名管道只能用于父子进程间通信 ,不能跨网络通信,并且通信是单向。
命名管道可用于任意进程间通信,支持跨网络通信,并且通信是双向,任意一段都可以接收和发送数据。命名管道通信进程分为服务端和客户端,服务端创建Pipe,客户端通过管道名连接该Pipe之后,双方均可通过该Pipe发送和接收数据。

1.2 命名管道

命名管道充分利用了windowsNT和windows2000内建的安全机制。我们在建立管道的时候可以指定那一种用户可以访问这个管道,那一种用户不能访问这个管道。用socket编写网络程序,要完成用户身份的验证需要自己编码实现。而采用命名管道就不需要编写用户身份验证的代码了。
将命名管道作为网络编程方案时,它实际上建立了一个客服机/服务器通信体系,并在其中进行可靠的传输数据。
在这里插入图片描述

命名管道是围绕windows文件系统设计的一种机制,采用“命名管道文件系统(Named Pipe File System, NPFS)”接口,因此客户机服务器可以利用标准的Win32文件系统函数, 例如ReadFile, WriteFile来收发数据。
命名管道的服务器和客户机的区别在于:服务器是唯一一个有权创建命名管道的进程,也只有他才能接受管道客户机的连接请求。而客户机只能同一个线程的命名管道服务器建立连接。

命名管道提供了两种基本的通信模式:字节模式和消息模式。

  • 在字节模式中,数据以一个字节流的形式在客户机和服务器之间流动。
  • 而在消息模式中,客户机和服务器则通过一系列不连续的数据单位,进行数据的收发,每次在管道上发出一条消息后,它必须作为一条完整的消息读入。
    在这里插入图片描述

CreateNamedPipe函数接口中的第一个参数lpName: \.\pipe\pipename, 必须为这种格式。中间的“.”表示本地机器,如果要跟远程机器建立连接,则需要设定远程服务器的名字。
参数nMaxInstances:指定这个管道能够创建的最大数目的实例。如果一个管道想同时接受5个客户端的连接,那该数值设置为5, 且按照同样的参数创建5个管道实例。每个实例只能同时接受一个客户端连接。
参数nDefaultTimeOut:缺省的超时值,以毫秒为单位。对与命名管道的每一个实例,必须采用相同的超时值。
ConnectNamedPipe函数接口是允许服务器端的一个命名管道进程去等待一个客户端连接到该命名管道的实例。当connectnamedpipe返回零后,并不一定表示函数执行出错,如果返回结果为ERROR_IO_PENDING表示这个操作是个未结的操作,可能这个操作在随后会完成。表示函数执行没有失败。
本机上格式:\ServerName\Pipe\name
如:\.\Pipe\my_pipe
在这里插入图片描述

命名管道具有很好的使用灵活性,表现在:
  1) 既可用于本地,又可用于网络。
  2) 可以通过它的名称而被引用。
  3) 支持多客户机连接。
  4) 支持双向通信。
5)支持异步重叠I/O操作。
当前只有Windows NT支持服务端的命名管道技术。

2、匿名管道

匿名管道(Anonymous Pipes)是在父进程和子进程间单向传输数据的一种没有名字的管道,只能在本地计算机中使用,而不可用于网络间的通信。
匿名管道由CreatePipe()函数创建,该函数在创建匿名管道的同时返回两个句柄:管道读句柄和管道写句柄。通过hReadPipe和hWritePipe所指向的句柄可分别以只读、只写的方式去访问管道。但是在同一个进程中去读写管道是没有意义的,我们常常需要的是在父子进程中传递数据,也就是父写数据,子读数据,或者子写数据,子读数据。我们这里仅讨论后一种情况,一个典型是示例就是一个有窗口的程序调用控制台程序,把控制台的输出信息在父窗口中输出。

匿名管道,当父进程执行CreateProcess()启动子进程时,系统会检查父进程内可以继承的内核对象句柄,复制到子进程空间,这样子进程就有了和父进程一样的匿名管道句柄,子进程对管道的写端放入数据,父进程就可以从读端取到数据。同样,父进程在写端放入数据,子进程也可以从读端取出数据。也就是说,一个匿名管道同时拥有了两个写端和读端。当父子进程任何一个关闭的时候,无论时候显式的关闭读写句柄,系统都会帮进程关闭所拥有的管道句柄。正常情况下,控制台进程的输输入出是在控制台窗口的,但是如果我们在创建子进程的时候指定了其输入输出,那么子进程就会从我们的管道读数据,把输出数据写到我们指定的管道。

匿名管道,在创建子进程时候,指定了子进程的输出管道,我们在管道另一端读取就可以了。那么存在一个问题,我们什么时候能知道子进程结束了,或者说不再写数据了呢?ReadFile()函数会阻塞到读到数据或者出错的后才会返回,也就是说当管道的所有写端都关闭的时候,读会出错,能够使函数在一个循环中返回,那么,我们应该在创建子进程后立即关闭父进程所拥有的写句柄,那么当子进程结束时候,读到0字节返回。

匿名管道,同样的道理,在用WriteFile()函数向管道写入数据时,只有在向管道写完指定字节的数据后或是在有错误发生时函数才会返回。如管道缓冲已满而数据还没有写完,WriteFile()将要等到另一进程对管道中数据读取以释放出更多可用空间后才能够返回。管道服务器在调用CreatePipe()创建管道时以参数nSize对管道的缓冲大小作了设定。 匿名管道并不支持异步读、写操作,这也就意味着不能在匿名管道中使用ReadFileEx()和WriteFileEx(),而且ReadFile()和WriteFile()中的lpOverLapped参数也将被忽略。

命名一个对象有助于进程间共享对象的处理。名字区分大小写。已命名对象是内核对象,内核对象是特定进程相关的,这个进程使用它们之前需要先创建。

管道遵循先进先出原则,有两种类型的管道:匿名管道,命名管道

创建匿名管道使用函数CreatePipe(),匿名管道存在与本地,不能用于在网络上传输。匿名管道没有名字,单向的,通常用来从父进程向子进程传递数据。

BOOL CreatePipe(
  [out]          PHANDLE               hReadPipe,
  [out]          PHANDLE               hWritePipe,
  [in, optional] LPSECURITY_ATTRIBUTES lpPipeAttributes,
  [in]           DWORD                 nSize
);

2.1 父进程

The following is the code for the parent process. It takes a single command-line argument: the name of a text file.

#include <windows.h> 
#include <tchar.h>
#include <stdio.h> 
#include <strsafe.h>

#define BUFSIZE 4096 
 
HANDLE g_hChildStd_IN_Rd = NULL;
HANDLE g_hChildStd_IN_Wr = NULL;
HANDLE g_hChildStd_OUT_Rd = NULL;
HANDLE g_hChildStd_OUT_Wr = NULL;

HANDLE g_hInputFile = NULL;
 
void CreateChildProcess(void); 
void WriteToPipe(void); 
void ReadFromPipe(void); 
void ErrorExit(PTSTR); 
 
int _tmain(int argc, TCHAR *argv[]) 
{ 
   SECURITY_ATTRIBUTES saAttr; 
 
   printf("\n->Start of parent execution.\n");

// Set the bInheritHandle flag so pipe handles are inherited. 
 
   saAttr.nLength = sizeof(SECURITY_ATTRIBUTES); 
   saAttr.bInheritHandle = TRUE; 
   saAttr.lpSecurityDescriptor = NULL; 

// Create a pipe for the child process's STDOUT. 
 
   if ( ! CreatePipe(&g_hChildStd_OUT_Rd, &g_hChildStd_OUT_Wr, &saAttr, 0) ) 
      ErrorExit(TEXT("StdoutRd CreatePipe")); 

// Ensure the read handle to the pipe for STDOUT is not inherited.

   if ( ! SetHandleInformation(g_hChildStd_OUT_Rd, HANDLE_FLAG_INHERIT, 0) )
      ErrorExit(TEXT("Stdout SetHandleInformation")); 

// Create a pipe for the child process's STDIN. 
 
   if (! CreatePipe(&g_hChildStd_IN_Rd, &g_hChildStd_IN_Wr, &saAttr, 0)) 
      ErrorExit(TEXT("Stdin CreatePipe")); 

// Ensure the write handle to the pipe for STDIN is not inherited. 
 
   if ( ! SetHandleInformation(g_hChildStd_IN_Wr, HANDLE_FLAG_INHERIT, 0) )
      ErrorExit(TEXT("Stdin SetHandleInformation")); 
 
// Create the child process. 
   
   CreateChildProcess();

// Get a handle to an input file for the parent. 
// This example assumes a plain text file and uses string output to verify data flow. 
 
   if (argc == 1) 
      ErrorExit(TEXT("Please specify an input file.\n")); 

   g_hInputFile = CreateFile(
       argv[1], 
       GENERIC_READ, 
       0, 
       NULL, 
       OPEN_EXISTING, 
       FILE_ATTRIBUTE_READONLY, 
       NULL); 

   if ( g_hInputFile == INVALID_HANDLE_VALUE ) 
      ErrorExit(TEXT("CreateFile")); 
 
// Write to the pipe that is the standard input for a child process. 
// Data is written to the pipe's buffers, so it is not necessary to wait
// until the child process is running before writing data.
 
   WriteToPipe(); 
   printf( "\n->Contents of %S written to child STDIN pipe.\n", argv[1]);
 
// Read from pipe that is the standard output for child process. 
 
   printf( "\n->Contents of child process STDOUT:\n\n");
   ReadFromPipe(); 

   printf("\n->End of parent execution.\n");

// The remaining open handles are cleaned up when this process terminates. 
// To avoid resource leaks in a larger application, close handles explicitly. 

   return 0; 
} 
 
void CreateChildProcess()
// Create a child process that uses the previously created pipes for STDIN and STDOUT.
{ 
   TCHAR szCmdline[]=TEXT("child");
   PROCESS_INFORMATION piProcInfo; 
   STARTUPINFO siStartInfo;
   BOOL bSuccess = FALSE; 
 
// Set up members of the PROCESS_INFORMATION structure. 
 
   ZeroMemory( &piProcInfo, sizeof(PROCESS_INFORMATION) );
 
// Set up members of the STARTUPINFO structure. 
// This structure specifies the STDIN and STDOUT handles for redirection.
 
   ZeroMemory( &siStartInfo, sizeof(STARTUPINFO) );
   siStartInfo.cb = sizeof(STARTUPINFO); 
   siStartInfo.hStdError = g_hChildStd_OUT_Wr;
   siStartInfo.hStdOutput = g_hChildStd_OUT_Wr;
   siStartInfo.hStdInput = g_hChildStd_IN_Rd;
   siStartInfo.dwFlags |= STARTF_USESTDHANDLES;
 
// Create the child process. 
    
   bSuccess = CreateProcess(NULL, 
      szCmdline,     // command line 
      NULL,          // process security attributes 
      NULL,          // primary thread security attributes 
      TRUE,          // handles are inherited 
      0,             // creation flags 
      NULL,          // use parent's environment 
      NULL,          // use parent's current directory 
      &siStartInfo,  // STARTUPINFO pointer 
      &piProcInfo);  // receives PROCESS_INFORMATION 
   
   // If an error occurs, exit the application. 
   if ( ! bSuccess ) 
      ErrorExit(TEXT("CreateProcess"));
   else 
   {
      // Close handles to the child process and its primary thread.
      // Some applications might keep these handles to monitor the status
      // of the child process, for example. 

      CloseHandle(piProcInfo.hProcess);
      CloseHandle(piProcInfo.hThread);
      
      // Close handles to the stdin and stdout pipes no longer needed by the child process.
      // If they are not explicitly closed, there is no way to recognize that the child process has ended.
      
      CloseHandle(g_hChildStd_OUT_Wr);
      CloseHandle(g_hChildStd_IN_Rd);
   }
}
 
void WriteToPipe(void) 

// Read from a file and write its contents to the pipe for the child's STDIN.
// Stop when there is no more data. 
{ 
   DWORD dwRead, dwWritten; 
   CHAR chBuf[BUFSIZE];
   BOOL bSuccess = FALSE;
 
   for (;;) 
   { 
      bSuccess = ReadFile(g_hInputFile, chBuf, BUFSIZE, &dwRead, NULL);
      if ( ! bSuccess || dwRead == 0 ) break; 
      
      bSuccess = WriteFile(g_hChildStd_IN_Wr, chBuf, dwRead, &dwWritten, NULL);
      if ( ! bSuccess ) break; 
   } 
 
// Close the pipe handle so the child process stops reading. 
 
   if ( ! CloseHandle(g_hChildStd_IN_Wr) ) 
      ErrorExit(TEXT("StdInWr CloseHandle")); 
} 
 
void ReadFromPipe(void) 

// Read output from the child process's pipe for STDOUT
// and write to the parent process's pipe for STDOUT. 
// Stop when there is no more data. 
{ 
   DWORD dwRead, dwWritten; 
   CHAR chBuf[BUFSIZE]; 
   BOOL bSuccess = FALSE;
   HANDLE hParentStdOut = GetStdHandle(STD_OUTPUT_HANDLE);

   for (;;) 
   { 
      bSuccess = ReadFile( g_hChildStd_OUT_Rd, chBuf, BUFSIZE, &dwRead, NULL);
      if( ! bSuccess || dwRead == 0 ) break; 

      bSuccess = WriteFile(hParentStdOut, chBuf, 
                           dwRead, &dwWritten, NULL);
      if (! bSuccess ) break; 
   } 
} 
 
void ErrorExit(PTSTR lpszFunction) 

// Format a readable error message, display a message box, 
// and exit from the application.
{ 
    LPVOID lpMsgBuf;
    LPVOID lpDisplayBuf;
    DWORD dw = GetLastError(); 

    FormatMessage(
        FORMAT_MESSAGE_ALLOCATE_BUFFER | 
        FORMAT_MESSAGE_FROM_SYSTEM |
        FORMAT_MESSAGE_IGNORE_INSERTS,
        NULL,
        dw,
        MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT),
        (LPTSTR) &lpMsgBuf,
        0, NULL );

    lpDisplayBuf = (LPVOID)LocalAlloc(LMEM_ZEROINIT, 
        (lstrlen((LPCTSTR)lpMsgBuf)+lstrlen((LPCTSTR)lpszFunction)+40)*sizeof(TCHAR)); 
    StringCchPrintf((LPTSTR)lpDisplayBuf, 
        LocalSize(lpDisplayBuf) / sizeof(TCHAR),
        TEXT("%s failed with error %d: %s"), 
        lpszFunction, dw, lpMsgBuf); 
    MessageBox(NULL, (LPCTSTR)lpDisplayBuf, TEXT("Error"), MB_OK); 

    LocalFree(lpMsgBuf);
    LocalFree(lpDisplayBuf);
    ExitProcess(1);
}

2.2 子进程

The following is the code for the child process. It uses the inherited handles for STDIN and STDOUT to access the pipe created by the parent. The parent process reads from its input file and writes the information to a pipe. The child receives text through the pipe using STDIN and writes to the pipe using STDOUT. The parent reads from the read end of the pipe and displays the information to its STDOUT.

#include <windows.h>
#include <stdio.h>

#define BUFSIZE 4096 
 
int main(void) 
{ 
   CHAR chBuf[BUFSIZE]; 
   DWORD dwRead, dwWritten; 
   HANDLE hStdin, hStdout; 
   BOOL bSuccess; 
 
   hStdout = GetStdHandle(STD_OUTPUT_HANDLE); 
   hStdin = GetStdHandle(STD_INPUT_HANDLE); 
   if ( 
       (hStdout == INVALID_HANDLE_VALUE) || 
       (hStdin == INVALID_HANDLE_VALUE) 
      ) 
      ExitProcess(1); 
 
   // Send something to this process's stdout using printf.
   printf("\n ** This is a message from the child process. ** \n");

   // This simple algorithm uses the existence of the pipes to control execution.
   // It relies on the pipe buffers to ensure that no data is lost.
   // Larger applications would use more advanced process control.

   for (;;) 
   { 
   // Read from standard input and stop on error or no data.
      bSuccess = ReadFile(hStdin, chBuf, BUFSIZE, &dwRead, NULL); 
      
      if (! bSuccess || dwRead == 0) 
         break; 
 
   // Write to standard output and stop on error.
      bSuccess = WriteFile(hStdout, chBuf, dwRead, &dwWritten, NULL); 
      
      if (! bSuccess) 
         break; 
   } 
   return 0;
}

3、命名管道

3.1 注意事项

命名管道程式设计的注意事项

  1. 假如命名管道客户端已打开,函数将会强迫关闭管道,用DisconnectNamedPipe关闭的管道,其客户端还必须用CloseHandle来关闭最后的管道。

  2. ReadFile和WriteFile的hFile句柄是由CreateFile及ConnectNamedPipe返回得到。 !

  3. 一个已被某客户端连接的管道句柄在被另一客户通过ConnectNamedPipe建立连接之前,服务端必须用DisconnectNamedPipe函数对已存在的连接进行强行拆离。服务端拆离管道会造成管道中数据的丢失,用FlushFileBuffers函数能够确保数据不被丢失。

  4. 命名管道服务端能够通过新创建的管道句柄或已被连接过其他客户的管道句柄来使用ConnectNamedPipe函数,但在连接新的客户端之前,服务端必须用函数DisconnectNamedPipe切断之前的客户句柄,否则ConnectNamedPipe 将会返回False。

  5. 阻塞模式,这种模式仅对“字节传输管道"操作有效,并且需要客户端和服务端不在同一机器上。假如用这种模式,则只有当函数通过网络向远端电脑管道缓冲器写数据成功时,才能有效返回。假如不用这种模式,系统会运行缺省方式以提高网络的工作效率。

  6. 用户必须用FILE—CREATE—PIPE—INSTANCE 来访问命名管道对象。新的命名管道建立后,来自安全参数的访问控制列表定义了访问该命名管道的权限。任何命名管道实例必须使用统一的管道传输方式、管道模式等参数。客户端未启动,管道服务端不能执行阻塞读操作,否则会发生空等的阻塞状态。当最后的命名管道实例的最后一个句柄被关闭时,就应该删除该命名管道。

3.2 官方示例

3.2.1 服务端

  • 命名管道服务器,步骤如下:
    1)使用API函数CreateNamedPipe,创建一个命名管道实例句柄。
    2)使用API函数ConnectNamedPipe,在命名管道实例上监听客户机连接请求。
    3)分别使用ReadFile和WriteFile这两个API函数,从客户机接收数据,或将数据发给客户机。
    4)使用API函数DisconnectNamedPipe,关闭命名管道连接。
    5)使用API函数CloseHandle,关闭命名管道实例句柄。
#include <windows.h> 
#include <stdio.h> 
#include <tchar.h>
#include <strsafe.h>

#define BUFSIZE 512
 
DWORD WINAPI InstanceThread(LPVOID); 
VOID GetAnswerToRequest(LPTSTR, LPTSTR, LPDWORD); 
 
int _tmain(VOID) 
{ 
   BOOL   fConnected = FALSE; 
   DWORD  dwThreadId = 0; 
   HANDLE hPipe = INVALID_HANDLE_VALUE, hThread = NULL; 
   LPCTSTR lpszPipename = TEXT("\\\\.\\pipe\\mynamedpipe"); 
 
// The main loop creates an instance of the named pipe and 
// then waits for a client to connect to it. When the client 
// connects, a thread is created to handle communications 
// with that client, and this loop is free to wait for the
// next client connect request. It is an infinite loop.
 
   for (;;) 
   { 
      _tprintf( TEXT("\nPipe Server: Main thread awaiting client connection on %s\n"), lpszPipename);
      hPipe = CreateNamedPipe( 
          lpszPipename,             // pipe name 
          PIPE_ACCESS_DUPLEX,       // read/write access 
          PIPE_TYPE_MESSAGE |       // message type pipe 
          PIPE_READMODE_MESSAGE |   // message-read mode 
          PIPE_WAIT,                // blocking mode 
          PIPE_UNLIMITED_INSTANCES, // max. instances  
          BUFSIZE,                  // output buffer size 
          BUFSIZE,                  // input buffer size 
          0,                        // client time-out 
          NULL);                    // default security attribute 

      if (hPipe == INVALID_HANDLE_VALUE) 
      {
          _tprintf(TEXT("CreateNamedPipe failed, GLE=%d.\n"), GetLastError()); 
          return -1;
      }
 
      // Wait for the client to connect; if it succeeds, 
      // the function returns a nonzero value. If the function
      // returns zero, GetLastError returns ERROR_PIPE_CONNECTED. 
 
      fConnected = ConnectNamedPipe(hPipe, NULL) ? 
         TRUE : (GetLastError() == ERROR_PIPE_CONNECTED); 
 
      if (fConnected) 
      { 
         printf("Client connected, creating a processing thread.\n"); 
      
         // Create a thread for this client. 
         hThread = CreateThread( 
            NULL,              // no security attribute 
            0,                 // default stack size 
            InstanceThread,    // thread proc
            (LPVOID) hPipe,    // thread parameter 
            0,                 // not suspended 
            &dwThreadId);      // returns thread ID 

         if (hThread == NULL) 
         {
            _tprintf(TEXT("CreateThread failed, GLE=%d.\n"), GetLastError()); 
            return -1;
         }
         else CloseHandle(hThread); 
       } 
      else 
        // The client could not connect, so close the pipe. 
         CloseHandle(hPipe); 
   } 

   return 0; 
} 
 
DWORD WINAPI InstanceThread(LPVOID lpvParam)
// This routine is a thread processing function to read from and reply to a client
// via the open pipe connection passed from the main loop. Note this allows
// the main loop to continue executing, potentially creating more threads of
// of this procedure to run concurrently, depending on the number of incoming
// client connections.
{ 
   HANDLE hHeap      = GetProcessHeap();
   TCHAR* pchRequest = (TCHAR*)HeapAlloc(hHeap, 0, BUFSIZE*sizeof(TCHAR));
   TCHAR* pchReply   = (TCHAR*)HeapAlloc(hHeap, 0, BUFSIZE*sizeof(TCHAR));

   DWORD cbBytesRead = 0, cbReplyBytes = 0, cbWritten = 0; 
   BOOL fSuccess = FALSE;
   HANDLE hPipe  = NULL;

   // Do some extra error checking since the app will keep running even if this
   // thread fails.

   if (lpvParam == NULL)
   {
       printf( "\nERROR - Pipe Server Failure:\n");
       printf( "   InstanceThread got an unexpected NULL value in lpvParam.\n");
       printf( "   InstanceThread exitting.\n");
       if (pchReply != NULL) HeapFree(hHeap, 0, pchReply);
       if (pchRequest != NULL) HeapFree(hHeap, 0, pchRequest);
       return (DWORD)-1;
   }

   if (pchRequest == NULL)
   {
       printf( "\nERROR - Pipe Server Failure:\n");
       printf( "   InstanceThread got an unexpected NULL heap allocation.\n");
       printf( "   InstanceThread exitting.\n");
       if (pchReply != NULL) HeapFree(hHeap, 0, pchReply);
       return (DWORD)-1;
   }

   if (pchReply == NULL)
   {
       printf( "\nERROR - Pipe Server Failure:\n");
       printf( "   InstanceThread got an unexpected NULL heap allocation.\n");
       printf( "   InstanceThread exitting.\n");
       if (pchRequest != NULL) HeapFree(hHeap, 0, pchRequest);
       return (DWORD)-1;
   }

   // Print verbose messages. In production code, this should be for debugging only.
   printf("InstanceThread created, receiving and processing messages.\n");

// The thread's parameter is a handle to a pipe object instance. 
 
   hPipe = (HANDLE) lpvParam; 

// Loop until done reading
   while (1) 
   { 
   // Read client requests from the pipe. This simplistic code only allows messages
   // up to BUFSIZE characters in length.
      fSuccess = ReadFile( 
         hPipe,        // handle to pipe 
         pchRequest,    // buffer to receive data 
         BUFSIZE*sizeof(TCHAR), // size of buffer 
         &cbBytesRead, // number of bytes read 
         NULL);        // not overlapped I/O 

      if (!fSuccess || cbBytesRead == 0)
      {   
          if (GetLastError() == ERROR_BROKEN_PIPE)
          {
              _tprintf(TEXT("InstanceThread: client disconnected.\n")); 
          }
          else
          {
              _tprintf(TEXT("InstanceThread ReadFile failed, GLE=%d.\n"), GetLastError()); 
          }
          break;
      }

   // Process the incoming message.
      GetAnswerToRequest(pchRequest, pchReply, &cbReplyBytes); 
 
   // Write the reply to the pipe. 
      fSuccess = WriteFile( 
         hPipe,        // handle to pipe 
         pchReply,     // buffer to write from 
         cbReplyBytes, // number of bytes to write 
         &cbWritten,   // number of bytes written 
         NULL);        // not overlapped I/O 

      if (!fSuccess || cbReplyBytes != cbWritten)
      {   
          _tprintf(TEXT("InstanceThread WriteFile failed, GLE=%d.\n"), GetLastError()); 
          break;
      }
  }

// Flush the pipe to allow the client to read the pipe's contents 
// before disconnecting. Then disconnect the pipe, and close the 
// handle to this pipe instance. 
 
   FlushFileBuffers(hPipe); 
   DisconnectNamedPipe(hPipe); 
   CloseHandle(hPipe); 

   HeapFree(hHeap, 0, pchRequest);
   HeapFree(hHeap, 0, pchReply);

   printf("InstanceThread exiting.\n");
   return 1;
}

VOID GetAnswerToRequest( LPTSTR pchRequest, 
                         LPTSTR pchReply, 
                         LPDWORD pchBytes )
// This routine is a simple function to print the client request to the console
// and populate the reply buffer with a default data string. This is where you
// would put the actual client request processing code that runs in the context
// of an instance thread. Keep in mind the main thread will continue to wait for
// and receive other client connections while the instance thread is working.
{
    _tprintf( TEXT("Client Request String:\"%s\"\n"), pchRequest );

    // Check the outgoing message to make sure it's not too long for the buffer.
    if (FAILED(StringCchCopy( pchReply, BUFSIZE, TEXT("default answer from server") )))
    {
        *pchBytes = 0;
        pchReply[0] = 0;
        printf("StringCchCopy failed, no outgoing message.\n");
        return;
    }
    *pchBytes = (lstrlen(pchReply)+1)*sizeof(TCHAR);
}

3.2.2 客户端

  • 命名管道客户机,步骤如下:
    1)调用API函数WaitNamedPipe,等候一个命名管道实例可供使用。
    2)调用API函数CreateFile,打开命名管道实例并建立连接。
    3)调用API函数WriteFile和ReadFile,分别向服务器发送数据和从中接收数据。
    4)调用API函数CloseHandle,关闭打开的命名管道会话。
#include <windows.h> 
#include <stdio.h>
#include <conio.h>
#include <tchar.h>

#define BUFSIZE 512
 
int _tmain(int argc, TCHAR *argv[]) 
{ 
   HANDLE hPipe; 
   LPTSTR lpvMessage=TEXT("Default message from client."); 
   TCHAR  chBuf[BUFSIZE]; 
   BOOL   fSuccess = FALSE; 
   DWORD  cbRead, cbToWrite, cbWritten, dwMode; 
   LPTSTR lpszPipename = TEXT("\\\\.\\pipe\\mynamedpipe"); 

   if( argc > 1 )
      lpvMessage = argv[1];
 
// Try to open a named pipe; wait for it, if necessary. 
 
   while (1) 
   { 
      hPipe = CreateFile( 
         lpszPipename,   // pipe name 
         GENERIC_READ |  // read and write access 
         GENERIC_WRITE, 
         0,              // no sharing 
         NULL,           // default security attributes
         OPEN_EXISTING,  // opens existing pipe 
         0,              // default attributes 
         NULL);          // no template file 
 
   // Break if the pipe handle is valid. 
 
      if (hPipe != INVALID_HANDLE_VALUE) 
         break; 
 
      // Exit if an error other than ERROR_PIPE_BUSY occurs. 
 
      if (GetLastError() != ERROR_PIPE_BUSY) 
      {
         _tprintf( TEXT("Could not open pipe. GLE=%d\n"), GetLastError() ); 
         return -1;
      }
 
      // All pipe instances are busy, so wait for 20 seconds. 
 
      if ( ! WaitNamedPipe(lpszPipename, 20000)) 
      { 
         printf("Could not open pipe: 20 second wait timed out."); 
         return -1;
      } 
   } 
 
// The pipe connected; change to message-read mode. 
 
   dwMode = PIPE_READMODE_MESSAGE; 
   fSuccess = SetNamedPipeHandleState( 
      hPipe,    // pipe handle 
      &dwMode,  // new pipe mode 
      NULL,     // don't set maximum bytes 
      NULL);    // don't set maximum time 
   if ( ! fSuccess) 
   {
      _tprintf( TEXT("SetNamedPipeHandleState failed. GLE=%d\n"), GetLastError() ); 
      return -1;
   }
 
// Send a message to the pipe server. 
 
   cbToWrite = (lstrlen(lpvMessage)+1)*sizeof(TCHAR);
   _tprintf( TEXT("Sending %d byte message: \"%s\"\n"), cbToWrite, lpvMessage); 

   fSuccess = WriteFile( 
      hPipe,                  // pipe handle 
      lpvMessage,             // message 
      cbToWrite,              // message length 
      &cbWritten,             // bytes written 
      NULL);                  // not overlapped 

   if ( ! fSuccess) 
   {
      _tprintf( TEXT("WriteFile to pipe failed. GLE=%d\n"), GetLastError() ); 
      return -1;
   }

   printf("\nMessage sent to server, receiving reply as follows:\n");
 
   do 
   { 
   // Read from the pipe. 
 
      fSuccess = ReadFile( 
         hPipe,    // pipe handle 
         chBuf,    // buffer to receive reply 
         BUFSIZE*sizeof(TCHAR),  // size of buffer 
         &cbRead,  // number of bytes read 
         NULL);    // not overlapped 
 
      if ( ! fSuccess && GetLastError() != ERROR_MORE_DATA )
         break; 
 
      _tprintf( TEXT("\"%s\"\n"), chBuf ); 
   } while ( ! fSuccess);  // repeat loop if ERROR_MORE_DATA 

   if ( ! fSuccess)
   {
      _tprintf( TEXT("ReadFile from pipe failed. GLE=%d\n"), GetLastError() );
      return -1;
   }

   printf("\n<End of message, press ENTER to terminate connection and exit>");
   _getch();
 
   CloseHandle(hPipe); 
 
   return 0; 
}

3.3 测试示例

3.3.1 服务端


#include <iostream>
#include <windows.h>
#include <ctime>
using namespace std;

#define pStrPipeName L"\\\\.\\pipe\\MyNamePipe"

int main()
{
    printf("命名管道: 服务器\n");
    printf("创建命名管道并等待连接\n");
    HANDLE hPipe = CreateNamedPipe(pStrPipeName, PIPE_ACCESS_DUPLEX,
                                   PIPE_TYPE_MESSAGE | PIPE_READMODE_MESSAGE | PIPE_WAIT,
                                   PIPE_UNLIMITED_INSTANCES, 0, 0, NMPWAIT_WAIT_FOREVER, 0);

    //等待连接。
    if (ConnectNamedPipe(hPipe, NULL) != NULL)
    {
        printf("连接成功,开始接收数据\n");

        const int BUFFER_MAX_LEN = 256;
        char szBuffer[BUFFER_MAX_LEN];
        memset(szBuffer, 0, BUFFER_MAX_LEN);
        DWORD dwLen = 0;

        //接收客户端发送的数据
        //读取管道中的内容(管道是一种特殊的文件)
        ReadFile(hPipe, szBuffer, BUFFER_MAX_LEN, &dwLen, NULL); 
        printf("接收到数据长度为: %d字节\n", dwLen);
        printf("具体数据内容如下: %s\n", szBuffer);

        //确认已收到数据,并发送消息给客户端
        printf("向客户端发送已经收到标志\n");
        //服务器发送的消息
        strcpy_s(szBuffer, "服务器已经收到\n"); 
        WriteFile(hPipe, szBuffer, strlen(szBuffer) + 1, &dwLen, NULL);
    }

    //关闭管道
    DisconnectNamedPipe(hPipe);
    CloseHandle(hPipe); 
    return 0;
}

3.3.2 客户端

#include <iostream>
#include <windows.h>
#include <ctime>
#include <conio.h>

using namespace std;
#define BUFSIZE 5
#define pStrPipeName L"\\\\.\\pipe\\MyNamePipe"

int main()
{
    printf("命名管道: 客户端\n");
    printf("按任意键以开始连接命名管道\n");
    _getch();
    printf("开始等待命名管道\n");

    if (WaitNamedPipe(pStrPipeName, NMPWAIT_WAIT_FOREVER) == FALSE)
    {
        printf("Error: 连接命名管道失败\n");
        return 0;
    }

    printf("打开命名管道\n");
    HANDLE hPipe = CreateFile(pStrPipeName, GENERIC_READ | GENERIC_WRITE, 0,
                              NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);

    printf("向服务端发送数据\n");
    const int BUFFER_MAX_LEN = 256;
    char szBuffer[BUFFER_MAX_LEN];
    memset(szBuffer, 0, BUFFER_MAX_LEN);
    DWORD dwLen = 0;

    //向服务端发送数据
    sprintf_s(szBuffer, "进程%d说\"%s\"", GetCurrentProcessId(), "Hello World!");
    WriteFile(hPipe, szBuffer, strlen(szBuffer) + 1, &dwLen, NULL);
    printf("数据写入完毕共%d字节\n", dwLen);

    //接收服务端发回的数据
    //读取管道中的内容(管道是一种特殊的文件)
    ReadFile(hPipe, szBuffer, BUFFER_MAX_LEN, &dwLen, NULL); 
    printf("接收服务端发来的确认信息长度为: %d字节\n", dwLen);
    printf("具体数据内容如下: %s\n", szBuffer);

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

4、Linux下的管道操作

管道:一个命令的输出可以通过管道做为另一个命令的输入。

在这里插入图片描述

“|”是管道命令操作符,简称管道符。利用Linux所提供的管道符“|”将两个命令隔开,管道符左边命令的输出就会作为管道符右边命令的输入。连续使用管道意味着第一个命令的输出会作为 第二个命令的输入,第二个命令的输出又会作为第三个命令的输入,依此类推。
在这里插入图片描述

管道实现的源代码在fs/pipe.c中,在pipe.c中有很多函数,其中有两个函数比较重要,即管道读函数pipe_read()和管道写函数pipe_wrtie()。管道写函数通过将字节复制到 VFS 索引节点指向的物理内存而写入数据,而管道读函数则通过复制物理内存中的字节而读出数据。当然,内核必须利用一定的机制同步对管道的访问,为此,内核使用了锁、等待队列和信号。
在这里插入图片描述

当写进程向管道中写入时,它利用标准的库函数write(),系统根据库函数传递的文件描述符,可找到该文件的 file 结构。file 结构中指定了用来进行写操作的函数(即写入函数)地址,于是,内核调用该函数完成写操作。写入函数在向内存中写入数据之前,必须首先检查 VFS 索引节点中的信息,同时满足如下条件时,才能进行实际的内存复制工作.

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

4.1 pipe 官方例子

  • pipe1.c
#include <stdio.h>    /* printf, fgets                  */
#include <stdlib.h>   /* exit                           */
#include <string.h>   /* strlen                         */
#include <unistd.h>   /* fork, pipe, read, write, close */
#include <sys/wait.h> /* wait                           */

int main(void) 
{
  int pid;
  char buffer[1024];
  int fd[2];
  
  pipe(fd); /* fd[0] is for read, fd[1] is for write */
  
  pid = fork();
  
  if (pid == 0) /* child */
  {
    int count;
    close(fd[0]); /* close unused end (read), child will write */
    
      /* prompt user for input */
    printf("input: ");
    fgets(buffer, sizeof(buffer), stdin);
    printf("child: message is %s", buffer);
    
      /* write to the pipe (include NUL terminator!) */
    count = write(fd[1], buffer, strlen(buffer) + 1);
    printf("child: wrote %i bytes\n", count);
    
    exit(0); 
  }
  else /* parent */
  {
    int count;
    close(fd[1]); /* close unused end (write), parent will read */
    
      /* read from the pipe */
    count = read(fd[0], buffer, sizeof(buffer));
    printf("parent: message is %s", buffer);
    printf("parent: read %i bytes\n", count);
    
    wait(NULL);   /* reap the child */
  }
  
  return 0;
}
#include <stdio.h>    /* printf, fgets            */
#include <stdlib.h>   /* exit                     */
#include <string.h>   /* strlen                   */
#include <ctype.h>    /* isalpha, toupper         */
#include <unistd.h>   /* pipe, read, write, close */
#include <sys/wait.h> /* wait                     */

void revcase(char *buffer)
{
  int i;
  int len = strlen(buffer);
  for (i = 0; i < len; i++)
  {
    if (isupper(buffer[i]))
      buffer[i] = tolower(buffer[i]);
    else if (islower(buffer[i]))
      buffer[i] = toupper(buffer[i]);
  }
}

int main(void) 
{
  int pid;

  /* setup stuff */
  
  pid = fork();
  
  if (pid == 0) /* child */
  {
  
    /* DO STUFF */  
  
    exit(0); 
  }
  else /* parent */
  {
    /* DO STUFF */
      
    wait(NULL);  
  }
  
  return 0;
}

4.2 pipe 测试例子

  • 例子1:

#include <stdio.h>
#include <unistd.h>

int main()
{
   int filedes[2];
   char buf[64];
   pipe(filedes);
   
   if (fork() == 0)
   {
     sprintf(buf, "%s", "hello pipe!");
     write(filedes[1], buf, sizeof(buf));
   }
   else
   {
     read(filedes[0], buf, sizeof(buf));
     printf("%s\n", buf);
   }
   
   return 0 ;
}
  • 例子2:
#include<stdio.h>
#include<unistd.h>

int main()
{
	// 这里的fd是文件描述符的数组,用于创建管道做准备的
	int n,fd[2]; 
	pid_t pid;
	char line[100];
	
	//创建管道
	if(pipe(fd)<0) 
	   printf("pipe create error/n");
	   
	//利用fork()创建新进程
	if((pid=fork())<0)              
	    printf("fork error/n");
	else if(pid>0){  //这里是父进程,先关闭管道的读出端,然后在管道的写端写入“hello world"
	    close(fd[0]);
	    write(fd[1],"hello word/n",11);
	}
	else{//这里是子进程,先关闭管道的写入端,然后在管道的读出端读出数据
	    close(fd[1]);                 
	    n= read(fd[0],line,100);
	    write(STDOUT_FILENO,line,n);
	}
	
	exit(0);
}
  • 例子3:
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

#include <sys/types.h>
#include <wait.h>
#include <string.h>

int main() {
	int filedes[2];
	char buf[10];
	int status;
	
	pipe(filedes);
	
	pid_t pid, wpid;
	pid = fork();
	
	if(pid == -1){ //创建进程失败
        printf("tfork is failed!\n");
    }  
	else if (pid == 0) { //子进程
		close(filedes[0]);
		
		for(int i = 0; i < 10; i++) {
			sprintf(buf, "%d=%s, %d", i, "ab", getpid());
			printf("write: %s\n", buf);
			write(filedes[1], buf, sizeof(buf));
		}
		
		sleep(2);
		//usleep(1000);微秒
		//delay(250);4毫秒
		
		for(int i = 10; i < 15; i++) {
			sprintf(buf, "%d=%s", i, "ab");
			printf("write: %s\n", buf);
			write(filedes[1], buf, sizeof(buf));
			sleep(1);
		}
		
		sleep(2);
		
		for(int i = 10; i < 15; i++) {
			sprintf(buf, "%d=%s", i, "ab");
			printf("write: %s\n", buf);
			write(filedes[1], buf, sizeof(buf));
		}
		
		close(filedes[1]);
		exit(5);
	} 
	else { //父进程
		close(filedes[1]);
		
		for(int i = 0; i < 20; i++) {
			read(filedes[0], buf, sizeof(buf));
			printf("read: %s, %d\n", buf, pid);
		}
		
		wpid=wait(&status);
		//wpid=wait(0);
		printf("结束的进程号:%d, %d\n", wpid, status);
		
		close(filedes[0]);
		
	}
	return 0;
}

4.3 popen

在这里插入图片描述

Using popen for pipes

/* compile with -D_BSD_SOURCE if using -ansi */

#include <stdio.h> /* popen, perror, fprintf, pclose, fgets */

#define BUFSIZE 100

int main(void)
{
  FILE *inpipe, *outpipe;
  char buffer[BUFSIZE];

    /* read pipe from ls (i.e. open ls program for reading) */
  inpipe = popen("ls /usr/bin", "r");
  if (!inpipe)
  {
    perror("popen read:");
    return 1;
  }

    /* write pipe to sort (i.e. open sort program for writing) */
  outpipe = popen("sort -r", "w");
  if (!outpipe)
  {
    perror("popen write:");
    return 2;
  }

    /* read from ls and write to sort (reversed) */
    /* it's this: ls /usr/bin | sort -r          */
  while(fgets(buffer, BUFSIZE, inpipe))
    fprintf(outpipe, "%s", buffer);

    /* clean up */
  pclose(inpipe);
  pclose(outpipe);

  return 0;
}
/* compile with -D_BSD_SOURCE if using -ansi */

#include <stdio.h> /* popen, perror, printf, pclose, fgets */

#define BUFSIZE 100

int main(void)
{
  FILE *inpipe;
  char buffer[BUFSIZE];

    /* read pipe from gcc */
  inpipe = popen("gcc foo.c", "r");
  if (!inpipe)
  {
    perror("popen read:");
    return 1;
  }

    /* Read from compiler and output to screen */
  while(fgets(buffer, BUFSIZE, inpipe))
    printf("%s", buffer);

    /* clean up */
  pclose(inpipe);

  return 0;
}

在这里插入图片描述

5、Windows的进程操作

5.1 CreateProcess Example

  • CreateProcess.cpp:
#include <iostream>
#include <windows.h>

int main(void) 
{
  STARTUPINFO start_info;
  PROCESS _INFORMATION proc_info;
  
  DWORD pid = GetCurrentProcessId();
  std::cout << "parent pid = " << pid << std::endl;

    // allocate memory and set to 0
  ZeroMemory(&start_info, sizeof(STARTUPINFO));
  ZeroMemory(&proc_info, sizeof(PROCESS_INFORMATION));
  
  std::cout << "creating child process" << std::endl;
  const char *program = "c:\\windows\\system32\\notepad.exe";
  BOOL err = CreateProcess(program,     // program to run
                           0,           // command line
                           0,           // security attributes
                           0,           // thread attributes
                           FALSE,       // don't inherit handles
                           0,           // creation flags (none)
                           0,           // use parent's environment
                           0,           // use parent's directory
                           &start_info, // start up info
                           &proc_info   // process info
                          );
  
  if (!err)
  {
    std::cout << "Error creating process" << std::endl;
    return -1;
  }

  std::cout << "waiting for child to terminate" << std::endl;
  WaitForSingleObject(proc_info.hProcess, INFINITE);
  std::cout << "parent terminating" << std::endl;

  CloseHandle(proc_info.hProcess);
  CloseHandle(proc_info.hThread);

  return 0;
}

5.2 Creating Multiple Processes Example

#include <stdio.h>
#include <windows.h>

int main(void) 
{
  const int COUNT = 2;

  HANDLE proc[COUNT], thread[COUNT];
  const char *programs[] = {"c:\\windows\\system32\\notepad.exe",
                             "c:\\windows\\system32\\mspaint.exe",
                            };

  for (int i = 0; i < COUNT; ++i) 
  {
    STARTUPINFO si;
    PROCESS_INFORMATION pi;

    ZeroMemory(&si, sizeof(si));
    ZeroMemory(&pi, sizeof(pi));

    CreateProcess(programs[i], 0, 0, 0, FALSE, 0, 0, 0, &si, &pi);

    proc[i] = pi.hProcess;
    thread[i] = pi.hThread;
  }

  WaitForMultipleObjects(COUNT, proc, TRUE, INFINITE);

  for (int i = 0; i < COUNT; ++i) 
  {
    printf("Process: %i, Thread: %i ended.\n", proc[i], thread[i]);
    CloseHandle(proc[i]);
    CloseHandle(thread[i]);
  }
  return 0;
}

结语

如果您觉得该方法或代码有一点点用处,可以给作者点个赞,或打赏杯咖啡;╮( ̄▽ ̄)╭
如果您感觉方法或代码不咋地//(ㄒoㄒ)//,就在评论处留言,作者继续改进;o_O???
如果您需要相关功能的代码定制化开发,可以留言私信作者;(✿◡‿◡)
感谢各位大佬童鞋们的支持!( ´ ▽´ )ノ ( ´ ▽´)っ!!!


  • 12
    点赞
  • 59
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
在 Windows 中,可以使用命名管道(Named Pipes)实现进程间通信。以下是一些基本步骤: 1. 创建命名管道 使用 CreateNamedPipe 函数创建一个命名管道。该函数需要指定管道名称、管道的读写模式、管道的最大实例数等参数。 2. 等待客户端连接 使用 ConnectNamedPipe 函数等待客户端的连接。该函数会一直阻塞,直到有客户端连接成功。 3. 接收客户端数据 使用 ReadFile 函数从管道中读取客户端发送的数据。 4. 发送数据给客户端 使用 WriteFile 函数向管道中写入数据,以便客户端读取。 5. 断开连接 使用 DisconnectNamedPipe 函数断开与客户端的连接。如果需要与多个客户端通信,则返回第 2 步。 6. 关闭管道 使用 CloseHandle 函数关闭命名管道的句柄。 注意事项: - 在创建管道时,需要指定管道名称,该名称在系统中必须是唯一的。 - 管道支持同步和异步方式进行读写操作,可以根据具体需求选择使用哪种方式。 - 管道的读写操作是阻塞式的,也可以使用 overlapped 结构体实现异步操作。 下面是一个简单的代码示例,演示如何使用命名管道实现进程间通信: ``` #include <windows.h> #include <stdio.h> #define PIPE_NAME "\\\\.\\pipe\\MyPipe" int main() { HANDLE hPipe; char buffer[1024]; DWORD dwRead; // 创建命名管道 hPipe = CreateNamedPipe(PIPE_NAME, PIPE_ACCESS_DUPLEX, PIPE_TYPE_BYTE | PIPE_READMODE_BYTE | PIPE_WAIT, PIPE_UNLIMITED_INSTANCES, 0, 0, 0, NULL); if (hPipe == INVALID_HANDLE_VALUE) { printf("CreateNamedPipe failed! Error code: %d\n", GetLastError()); return 1; } // 等待客户端连接 if (!ConnectNamedPipe(hPipe, NULL)) { printf("ConnectNamedPipe failed! Error code: %d\n", GetLastError()); return 1; } // 接收客户端数据 if (!ReadFile(hPipe, buffer, sizeof(buffer), &dwRead, NULL)) { printf("ReadFile failed! Error code: %d\n", GetLastError()); return 1; } printf("Received data from client: %s\n", buffer); // 发送数据给客户端 if (!WriteFile(hPipe, "Hello, client!", 15, NULL, NULL)) { printf("WriteFile failed! Error code: %d\n", GetLastError()); return 1; } // 断开连接 DisconnectNamedPipe(hPipe); // 关闭管道 CloseHandle(hPipe); return 0; } ```

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值