管道的通信

匿名管道
   管道(Pipe)是一种具有两个端点的通信通道:有一端句柄的进程可以和有另一端句柄的进程通信。管道可以是单向-一端是只读的,另一端点是只写的;也可以是双向的一管道的两端点既可读也可写。
  匿名管道(Anonymous Pipe)是在父进程和子进程之间,或同一父进程的两个子进程之间传输数据的无名字的单向管道。通常由父进程创建管道,然后由要通信的子进程继承通道的读端点句柄或写端点句柄,然后实现通信。父进程还可以建立两个或更多个继承匿名管道读和写句柄的子进程。这些子进程可以使用管道直接通信,不需要通过父进程。
  匿名管道是单机上实现子进程标准I/O重定向的有效方法,它不能在网上使用,也不能用于两个不相关的进程之间。
命名管道
   命名管道(Named Pipe)是服务器进程和一个或多个客户进程之间通信的单向或双向管道。不同于匿名管道的是命名管道可以在不相关的进程之间和不同计算机之间使用,服务器建立命名管道时给它指定一个名字,任何进程都可以通过该名字打开管道的另一端,根据给定的权限和服务器进程通信。
  命名管道提供了相对简单的编程接口,使通过网络传输数据并不比同一计算机上两进程之间通信更困难,不过如果要同时和多个进程通信它就力不从心了。

匿名管道的使用

匿名管道主要用于本地父进程和子进程之间的通信,

在父进程中的话,首先是要创建一个匿名管道,

在创建匿名管道成功后,可以获取到对这个匿名管道的读写句柄,

然后父进程就可以向这个匿名管道中写入数据和读取数据了,

但是如果要实现的是父子进程通信的话,那么还必须在父进程中创建一个子进程,

同时,这个子进程必须能够继承和使用父进程的一些公开的句柄,

为什么呢?

因为在子进程中必须要使用父进程创建的匿名管道的读写句柄,

通过这个匿名管道才能实现父子进程的通信,所以必须继承父进程的公开句柄。

同时在创建子进程的时候,

必须将子进程的标准输入句柄设置为父进程中创建匿名管道时得到的读管道句柄,

将子进程的标准输出句柄设置为父进程中创建匿名管道时得到的写管道句柄。

然后在子进程就可以读写匿名管道了。

匿名管道的创建

BOOL WINAPI CreatePipe(
          __out   PHANDLE hReadPipe,
          __out   PHANDLE hWritePipe,
          __in    LPSECURITY_ATTRIBUTES lpPipeAttributes,
          __in    DWORD nSize );

参数 hReadPipe 为输出参数,该句柄代表管道的读取句柄。

参数 hWritePipe 为输出参数,该句柄代表管道的写入句柄。

参数 lpPipeAttributes 为一个输入参数,指向一个 SECURITY_ATTRIBUTES 的结构体指针,

其检测返回的句柄是否能够被子进程继承,如果此参数为 NULL ,则表明句柄不能被继承,

在匿名管道中,由于匿名管道要在父子进程之间进行通信,

而子进程如果想要获得匿名管道的读写句柄,则其只能通过从父进程继承获得,

当一个子进程从其父进程处继承了匿名管道的读写句柄以后,

子进程和父进程之间就可以通过这个匿名管道的读写句柄进行通信了。

所以在这里必须构建一个 SECURITY_ATTRIBUTES 的结构体,

并且该结构体的第三个结构成员变量 bInheritHandle 参数必须设置为 TRUE 

从而让子进程可以继承父进程所创建的匿名管道的读写句柄。

typedef struct _SECURITY_ATTRIBUTES {
 
    DWORD nLength;
 
    LPVOID lpSecurityDescriptor;
 
    BOOL bInheritHandle;
 
} SECURITY_ATTRIBUTES, *LPSECURITY_ATTRIBUTES;

参数 nSize 用来指定缓冲区的大小,

如果此参数设置为 ,则表明系统将使用默认的缓冲区大小。一般将该参数设置为 即可。

子进程的创建

BOOL  CreateProcess( 
        LPCWSTR pszImageName,  LPCWSTR pszCmdLine, 
        LPSECURITY_ATTRIBUTES psaProcess, 
        LPSECURITY_ATTRIBUTES psaThread, 
        BOOL fInheritHandles,  DWORD fdwCreate, 
        LPVOID pvEnvironment,  LPWSTR pszCurDir, 
        LPSTARTUPINFOW psiStartInfo, 
        LPPROCESS_INFORMATION pProcInfo );

参数 pszImageName 是一个指向 NULL 终止的字符串,用来指定可执行程序的名称。

参数 pszCmdLine 用来指定传递给新进程的命令行字符串,一般做法是在 pszImageName 中传递可执行文件的名称,

在 pszCmdLine 中传递命令行参数。

参数 psaProcess 即代表当 CreateProcess 函数创建进程时,需要给进程对象设置一个安全性。

参数 psaThread 代表当 CreateProcess 函数创建新进程后,需要给该进程的主线程对象设置一个安全性。

参数 fInheritHandles 用来指定父进程随后创建的子进程是否能够继承父进程的对象句柄,

如果该参数设置为 TRUE ,则父进程的每一个可继承的打开句柄都将被子进程所继承,

继承的句柄与原始的句柄拥有同样的访问权。

在匿名管道的使用中,因为子进程需要使用父进程中创建的匿名管道的读写句柄,

所以应该将这个参数设置为 TRUE ,从而可以让子进程继承父进程创建的匿名管道的读写句柄。

参数 fdwCreate 用来指定控件优先级类和进程创建的附加标记。

如果只是为了启动子进程,则并不需要设置它创建的标记,可以将此参数设置为 0

对于这个参数的具体取值列表可以参考 MSDN 。

参数 pvEnvironment 代表指向环境块的指针,

如果该参数设置为 NULL ,则默认将使用父进程的环境。通常给该参数传递 NULL

参数 pszCurDir 用来指定子进程当前的路径,

这个字符串必须是一个完整的路径名,其包括驱动器的标识符,

如果此参数设置为 NULL ,那么新的子进程将与父进程拥有相同的驱动器和目录。

参数 psiStartInfo 指向一个 StartUpInfo 的结构体的指针,用来指定新进程的主窗口如何显示。

typedef struct _STARTUPINFOA {
    DWORD cb;
    LPSTR lpReserved;
    LPSTR lpDesktop;
    LPSTR lpTitle;
    DWORD dwX;
    DWORD dwY;
    DWORD dwXSize;
    DWORD dwYSize;
    DWORD dwXCountChars;
    DWORD dwYCountChars;
    DWORD dwFillAttribute;
    DWORD dwFlags;
    WORD wShowWindow;
    WORD cbReserved2;
    LPBYTE lpReserved2;
    HANDLE hStdInput;
    HANDLE hStdOutput;
    HANDLE hStdError;
} STARTUPINFOA, *LPSTARTUPINFOA;

对于 dwFlags 参数来说,如果其设置为 STARTF_USESTDHANDLES 

则将会使用该 STARTUPINFO 结构体中的 hStdInput , hStdOutput , hStdError 成员,

来设置新创建的进程的标准输入,标准输出,标准错误句柄。

参数 pProcInfo 为一个输出参数,

指向一个 PROCESS_INFORMATION 结构体的指针,用来接收关于新进程的标识信息。

typedef struct _PROCESS_INFORMATION 
{             
    HANDLE hProcess;             
    HANDLE hThread;             
    DWORD dwProcessId;              
    DWORD dwThreadId; 
 
}PROCESS_INFORMATION;

其中 hProcess 和 hThread 分别用来标识新创建的进程句柄和新创建的进程的主线程句柄。

dwProcessId 和 dwThreadId 分别是全局进程标识符和全局线程标识符。

前者可以用来标识一个进程,后者用来标识一个线程。

示例代码:
父进程:

 1  void  CParentView::OnPipeCreate() 
 2  {
 3       //  TODO: Add your command handler code here
 4      SECURITY_ATTRIBUTES sa;
 5 
 6      sa.bInheritHandle  =  TRUE;
 7      sa.lpSecurityDescriptor  =  NULL;
 8      sa.nLength  =   sizeof (SECURITY_ATTRIBUTES);
 9 
10       if ( ! CreatePipe( & hRead, & hWrite, & sa, 0 ))
11      {
12          MessageBox( " 创建匿名管道失败! " );
13           return ;
14      }
15 
16      STARTUPINFO sui;
17      PROCESS_INFORMATION pi;
18 
19      ZeroMemory( & sui, sizeof (STARTUPINFO));
20 
21      sui.cb  =   sizeof (STARTUPINFO);
22      sui.dwFlags  =  STARTF_USESTDHANDLES;
23      sui.hStdInput  =  hRead;
24      sui.hStdOutput  =  hWrite;
25      sui.hStdError  =  GetStdHandle(STD_ERROR_HANDLE);
26 
27       if ( ! CreateProcess( " ..\\Child\\Debug\\Child.exe " ,NULL,NULL,NULL,TRUE, 0 ,NULL,NULL, & sui, & pi))
28      {
29          CloseHandle(hRead);
30          CloseHandle(hWrite);
31          hRead  =  NULL;
32          hWrite  =  NULL;
33          MessageBox( " 创建子进程失败! " );
34           return ;
35      }
36       else
37      {
38          CloseHandle(pi.hProcess);
39          CloseHandle(pi.hThread);
40      }
43  }
44 
45  void  CParentView::OnPipeRead() 
46  {
47       //  TODO: Add your command handler code here
48       char  buf[ 100 ];
49      DWORD dwRead;
50       if ( ! ReadFile(hRead,buf, 100 , & dwRead,NULL))
51      {
52          MessageBox( " 读取数据失败! " );
53           return ;
54      }
55      MessageBox(buf);    
56  }
57 
58  void  CParentView::OnPipeWrite() 
59  {
60       //  TODO: Add your command handler code here
61       char  buf[]  =   " http:\\www.hit.edu.cn " ;
62      DWORD dwWrite;
63       if ( ! WriteFile(hWrite,buf,strlen(buf) + 1 , & dwWrite,NULL))
64      {
65          MessageBox( " 写入数据失败! " );
66           return ;
67      }
68  }

子进程:
 1  void  CChildView::OnPipeRead() 
 2  {
 3       //  TODO: Add your command handler code here
 4       char  buf[ 100 ];
 5      DWORD dwRead;
 6       if ( ! ReadFile(hRead,buf, 100 , & dwRead,NULL))
 7      {
 8          MessageBox( " 读取数据失败! " );
 9           return ;
10      }
11      MessageBox(buf);    
12  }
13 
14  void  CChildView::OnPipeWrite() 
15  {
16       //  TODO: Add your command handler code here
17       char  buf[]  =   " 匿名管道测试程序 " ;
18      DWORD dwWrite;
19       if ( ! WriteFile(hWrite,buf,strlen(buf) + 1 , & dwWrite,NULL))
20      {
21          MessageBox( " 写入数据失败! " );
22           return ;
23      }
24  }
25 
26  void  CChildView::OnInitialUpdate() 
27  {
28      CView::OnInitialUpdate();
29      
30       //  TODO: Add your specialized code here and/or call the base class
31      hRead  =  GetStdHandle(STD_INPUT_HANDLE);
32      hWrite  =  GetStdHandle(STD_OUTPUT_HANDLE);    
33  }

命名管道的实现:    
1.命名管道ServerClient间通信的实现流程    
(1)建立连接:服务端通过函数CreateNamedPipe创建一个命名管道的实例并返回用于 今后操作的句柄,或为已存在的管道创建新的实例。如果在已定义超时值变为零以前,有 一个实例管道可以使用,则创建成功并返回管道句柄,并用以侦听来自客户端的连接请求, 该功能通过ConnectNamedPipe函数实现。 

      另一方面,客户端通过函数WaitNamedPipe使服务进程等待来自客户的实例连接,如 果在超时值变为零以前,有一个管道可以为连接使用,则WaitNamedPipe将返回True,并 通过调用CreateFileCallNamedPipe来呼叫对服务端的连接。此时服务端将接受客户端 的连接请求,成功建立连接,服务端ConnectNamedPipe返回True,客户端CreateFile将返 回一指向管道文件的句柄。

      从时序上讲,首先是客户端通过WaitNamedPipe使服务端的CreateFile在限时时间内 创建实例成功,然后双方通过ConnectNamedPipeCreateFile成功连接,并返回用以通信 的文件句柄,此时双方即可进行通信。    
(2)通信实现:建立连接之后,客户端与服务器端即可通过ReadFileWriteFile 利用得到的管道文件句柄,彼此间进行信息交换。   
(3)连接终止:当客户端与服务端的通信结束,或由于某种原因一方需要断开时,客 户端应调用CloseFile,而服务端应接着调用DisconnectNamedPipe。当然服务端亦可通 过单方面调用DisconnectNamedPipe终止连接。最后应调用函数CloseHandle来关闭该管道。

2.命名管道服务器端和客户端代码实现


服务端:
 1  void  CTestServiceDlg::OnBnClickedBtncon()
 2  {
 3       //  TODO: 在此添加控件通知处理程序代码
 4      m_strPipeName  =  _T( " \\\\.\\Pipe\\mypipe " );
 5      m_hPipe  =  CreateNamedPipeW(
 6          m_strPipeName, // 管道名称
 7          PIPE_ACCESS_DUPLEX,
 8          PIPE_TYPE_MESSAGE |      // 消息流类型的管道
 9          PIPE_READMODE_BYTE |      // 消息流读模式
10          PIPE_WAIT,             // 阻塞模式
11          PIPE_UNLIMITED_INSTANCES,     // 允许最大管道实例个数
12          BUF_SIZE, // 输出缓冲
13          BUF_SIZE, // 输入缓冲
14           0 ,         // 客户端超时时间
15          NULL     // 默认的安全属性
16          );
17       if (m_hPipe  ==  INVALID_HANDLE_VALUE)
18      {
19          AfxMessageBox(_T( " 创建命名管道失败! " ));
20          PostQuitMessage( - 8 );
21           return ;
22      }
23       if  (ConnectNamedPipe(m_hPipe,NULL))
24      {
25          SetDlgItemText(IDC_LblRead, "" );
26          GetDlgItem(IDC_BtnRead) -> EnableWindow(TRUE);
27          GetDlgItem(IDC_BtnSend) -> EnableWindow(TRUE);
28          AfxMessageBox( " 连接成功! " );
29      }
30       else
31      {
32          CloseHandle(m_hPipe);
33          m_hPipe  =  NULL;
34          AfxMessageBox( " 连接失败! " );
35          PostQuitMessage( - 8 );
36           return ;
37      }
38  }
39 
40  void  CTestServiceDlg::OnBnClickedBtnread()
41  {
42      TCHAR buff[BUF_SIZE];
43      DWORD dwRead;
44       if  ( ! ReadFile(m_hPipe,buff,BUF_SIZE, & dwRead,NULL))
45      {
46          CString errMsg;
47          errMsg.Format( " 读取发生错误:%d " ,GetLastError());
48          AfxMessageBox(errMsg);
49           return ;
50      }
51       else
52      {
53          buff[dwRead]  =   ' \0 ' ; // 结束符
54          CString strRead(buff);
55          SetDlgItemText(IDC_LblRead, strRead);
56      }
57  }
58 
59  void  CTestServiceDlg::OnBnClickedBtnsend()
60  {
61       //  TODO: 在此添加控件通知处理程序代码
62      UpdateData(TRUE);
63      DWORD dwWrite;
64       bool  rt  =  WriteFile(m_hPipe,
65          m_TxtSend,(m_TxtSend.GetLength() + 1 ) * sizeof (TCHAR),
66           & dwWrite,NULL);
67       if ( ! rt)
68      {
69          CString errMsg;
70          errMsg.Format( " 写入文件失败,错误信息%d " ,::GetLastError());
71          AfxMessageBox(errMsg);
72           return ;
73      }
74       else
75      {
76          SetDlgItemText(IDC_LblRead, " 发送成功! " );
77          m_TxtSend  =  L "" ;
78          UpdateData(FALSE);
79      }
80  }

客户端:
 1  void  CTestClientDlg::OnBnClickedBtnconnect()
 2  {
 3       //  TODO: 在此添加控件通知处理程序代码
 4       // 查询是否有管道实例可用
 5      m_strPipeName  =  _T( " \\\\.\\Pipe\\mypipe " );
 6       if ( ! WaitNamedPipe(m_strPipeName,NMPWAIT_USE_DEFAULT_WAIT))
 7      {
 8          AfxMessageBox(_T( " 连接到服务器端失败! " ));
 9           return ;
10      }
11      hPipe  =  CreateFileW(
12          m_strPipeName,     // 管道名称
13          GENERIC_READ |      // 以可读写模式打开
14          GENERIC_WRITE,
15           0 ,                 // 不支持共享
16          NULL,             // 默认安全属性
17          OPEN_EXISTING,     // 只打开已存在的管道
18           0 ,
19          NULL);
20       if (hPipe  !=  INVALID_HANDLE_VALUE)
21      {
22          GetDlgItem(IDC_BtnConnect) -> EnableWindow(FALSE);
23          GetDlgItem(IDC_BtnRec) -> EnableWindow(TRUE);
24          GetDlgItem(IDC_BtnSend) -> EnableWindow(TRUE);
25           /// GetDlgItem(idc)
26      }
27       if (GetLastError() !=  ERROR_PIPE_BUSY) // 如果连接不是忙碌
28      {
29          AfxMessageBox(L " 连接到服务端成功! " );
30           return ;
31      }
32  }
33 
34  void  CTestClientDlg::OnBnClickedBtnsend()
35  {
36       //  TODO: 在此添加控件通知处理程序代码
37      UpdateData(TRUE);
38      DWORD dwWrite;
39       if  ( ! WriteFile(hPipe,m_Send,(m_strPipeName.GetLength() + 1 ) * sizeof (TCHAR), & dwWrite,NULL))
40      {
41          CString errMsg;
42          errMsg.Format(L " 发送失败:%d " ,GetLastError());
43           return ;
44      }
45      m_Rec  =  L " 发送成功! " ;
46      UpdateData(TRUE);
47  }
48 
49  void  CTestClientDlg::OnBnClickedBtnrec()
50  {
51       //  TODO: 在此添加控件通知处理程序代码
52      TCHAR buff[BUF_SIZE];
53      DWORD dwRead;
54       if ( ! ReadFile(hPipe,buff,BUF_SIZE, & dwRead,NULL))
55      {
56          CString errMsg;
57          errMsg.Format(L " 读取失败,错误信息%d " ,GetLastError());
58           return ;
59      }    
60       else
61      {
62          buff[dwRead]  =  _T( ' \0 ' );
63          CStringW strRead(buff);
64          m_Rec  =  strRead;
65          UpdateData(FALSE);
66      }
67  }

命名管道程序设计的注意事项    
1.如果命名管道客户端已打开,函数将会强迫关闭管道,用DisconnectNamedPipe关闭 的管道,其客户端还必须用CloseHandle来关闭最后的管道。    
2. ReadFileWriteFilehFile句柄是由CreateFileConnectNamedPipe返回得到。   
3.一个已被某客户端连接的管道句柄在被另一客户通过ConnectNamedPipe建立连接之前,服务端必须用DisconnectNamedPipe函数对已存在的连接进行强行拆离。服务端拆离管道会造成管道中数据的丢失,用FlushFileBuffers函数可以保证数据不被丢失。
4.命名管道服务端可以通过新创建的管道句柄或已被连接过其他客户的管道句 柄来使用ConnectNamedPipe函数,但在连接新的客户端之前,服务端必须用函数 DisconnectNamedPipe切断之前的客户句柄,否则ConnectNamedPipe 将会返回False    
5.阻塞模式,这种模式仅对字节传输管道"操作有效,并且要求客户端与服务端不 在同一机器上。如果用这种模式,则只有当函数通过网络向远端计算机管道缓冲器写数 据成功时,才能有效返回。如果不用这种模式,系统会运行缺省方式以提高网络的工作效率。    
6.用户必须用FILE—CREATE—PIPE—INSTANCE 来访问命名管道对象。新的命名管 道建立后,来自安全参数的访问控制列表定义了访问该命名管道的权限。所有命名管道 实例必须使用统一的管道传输方式、管道模式等参数。客户端未启动,管道服务端不能 执行阻塞读操作,否则会发生空等的阻塞状态。当最后的命名管道实例的最后一个句柄 被关闭时,就应该删除该命名管道。

<think>好的,我现在需要回答用户关于管道通信的实现原理和示例代码的问题。首先,用户提到了匿名管道,所以我要先回顾一下相关知识。 根据引用[1],匿名管道主要用于具有亲缘关系的进程间通信,比如父子进程。它的工作原理涉及到创建管道,这会生成两个文件描述符,一个用于读,一个用于写。数据流动是单向的,所以需要确定好方向。然后,通过fork创建子进程,子进程继承父进程的文件描述符,这样双方就可以通过读写管道通信了。 接下来,用户需要示例代码。引用[1]提到使用MFC实现,但用户可能也接受其他语言的例子,比如C语言,因为这是更常见的管道实现方式。在Linux环境下,可以使用pipe系统调用。需要包含头文件unistd.h。然后创建管道,fork进程,父进程关闭读端,写入数据,子进程关闭写端,读取数据。要注意正确关闭不需要的文件描述符,避免资源泄漏。 另外,引用[2]提到了Linux下的匿名管道实现示例,可能涉及端口四种情况的探究,比如读端关闭、写端关闭等情况下的行为。这部分可能在示例代码中需要说明,比如当一端关闭时,另一端的操作会有什么结果,例如读端关闭后继续写入会导致SIGPIPE信号。 最后,用户可能想知道管道的局限性,比如只能单向通信,只能用于有亲缘关系的进程,这些都需要在回答中提及。同时,生成相关问题的时候,可以考虑管道的其他类型,比如命名管道,或者与其他IPC方式的比较,以及具体应用场景。 现在需要确保LaTeX格式正确,行内公式用$...$,独立公式用$$...$$,但在这个问题中可能涉及代码较多,数学公式较少。检查是否有需要用到数学的地方,比如缓冲区大小计算,可能用变量如$buffer$。代码部分需要用代码块包裹,正确显示语法。 总结步骤: 1. 解释匿名管道的工作原理,包括创建、单向性、亲缘关系。 2. 提供C语言的示例代码,展示pipe、fork、读写操作。 3. 分析示例中的关键步骤,如文件描述符的关闭,错误处理。 4. 讨论匿名管道的局限性和应用场景。 5. 生成相关问题,如命名管道、其他IPC方式、性能分析等。 需要确保引用正确,引用[1]和[2]的内容要自然地融入回答中,比如在提到进程间通信时引用[2],在实现原理部分引用[1]。同时,代码部分要正确无误,避免语法错误,比如正确使用pipe返回的文件描述符,父子进程的正确处理。</think>### 管道通信的实现原理与示例 #### 一、工作原理 匿名管道(Anonymous Pipe)是一种**单向通信机制**,专为具有**亲缘关系**的进程(如父子进程)设计[^1]。其核心原理如下: 1. **创建管道**:通过系统调用(如`pipe()`)生成两个文件描述符(`fd[0]`为读端,`fd[1]`为写端)。 2. **单向数据流**:数据只能从写端流向读端,且遵循**先进先出(FIFO)**原则。 3. **进程继承**:父进程调用`fork()`后,子进程继承管道的文件描述符,实现跨进程通信。 #### 二、C语言实现示例 以下为Linux环境下的匿名管道示例代码: ```c #include <unistd.h> #include <stdio.h> #include <string.h> int main() { int fd[2]; pid_t pid; char buffer[100]; // 1. 创建管道 if (pipe(fd) == -1) { perror("pipe error"); return 1; } // 2. 创建子进程 pid = fork(); if (pid < 0) { perror("fork error"); return 1; } if (pid > 0) { // 父进程(写端) close(fd[0]); // 关闭读端 const char *msg = "Hello from parent!"; write(fd[1], msg, strlen(msg) + 1); close(fd[1]); // 写入完成后关闭写端 } else { // 子进程(读端) close(fd[1]); // 关闭写端 read(fd[0], buffer, sizeof(buffer)); printf("Child received: %s\n", buffer); close(fd[0]); // 读取完成后关闭读端 } return 0; } ``` #### 三、关键点分析 1. **文件描述符管理**:父/子进程需显式关闭未使用的端口,避免资源泄漏[^2]。 2. **阻塞行为**: - 读端未关闭时,若管道为空,`read()`会阻塞。 - 写端未关闭时,若管道满,`write()`会阻塞。 3. **异常处理**: - 当读端已关闭时,继续写入会触发`SIGPIPE`信号(默认终止进程)。 - 当写端关闭后,`read()`返回0,表示数据流结束。 #### 四、局限性 1. **单向通信**:若需双向通信,需创建两个管道。 2. **亲缘关系限制**:仅限父子进程或兄弟进程间使用。 3. **生命周期短**:随进程终止而销毁。 ---
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值