COM初学(二) 编写和使用一个简单的COM

网上教程很多,COM开发一般都是直接使用ALT的模板来开发COM,这里我们因为是初学入门,所以我们通过自己来写一个简单的COM,学习COM是个什么东西,实际上我的理解是,COM就是类似DLL,甚至是EXE,只是说通过微软的COM规范来进行调用而已。
1. 打开VS2015, 直接Win32-->Win32项目-->DLL (空项目即可),点击完成, 这样创建出来啥都没有。

2. 添加源文件
(1) 创建接口类:  
COM前面说了,它是一个对象(这里我们就理解它是一个DLL的进程内对象), COM提供给外面用户用的都是以 接口的形式提供,所以我们要先定义该 接口类(也就是你这个COM,能给用户提供什么功能):
这个一般都是放在头文件里,IHelloWorld.h  ---命名时候,都是以大写的I开头,后面跟上名称。
操作: 右键工程的头文件,新建头文件,IHelloWorld.h 
------------------------------------代码分割线-----------------------------------
#ifndef IHELLOCOM_h__
#define IHELLOCOM_h__

#include <Windows.h>
#include <Unknwn.h>
#include <ObjBase.h>

/*这个是GUID,表示我们这个接口类的全球唯一的标识码,
  用户就是通过这个标识码,让系统的COM库来找到我们注册的DLL信息之类的,
  然后系统再调用到工厂类的实现的, 关于工厂类,后面再进行解释*/
extern "C" const GUID IID_IHelloCom;
/*定义我们自己的接口类,这个接口类一定要继承IUnknown, 这个是COM的规定
  至于原因,可以详细去了解下,在博客中的《COM参考资料》中随便找几个博客都有解释*/
class IHelloCom : public IUnknown
{
/*注意,这个要定义virtual,因为我们最终还有个子类来实现这个接口中的这些方法*/
public:
/* 对外接口一: 设置一个字符串, 测试用于存储*/
virtual HRESULT STDMETHODCALLTYPE SetSaveString(IN char* pcString) = 0;
/* 对外接口二:获取一定长度的字符串内容, 测试用于存储有没有生效*/
virtual HRESULT STDMETHODCALLTYPE GetSaveString(OUT char* pcString, long lGetLen) = 0;
/* 对外接口三:将我们存储的字符串用MessageBox提示出来*/
virtual HRESULT STDMETHODCALLTYPE MessageBoxHelloCom() = 0;
};
#endif
------------------------------------代码分割线-----------------------------------
(2)  创建组件类:  
COM前面的是提供给外面的接口,而这个是具体的实现的类的定义,有各种完整的构造和析构等。
右键工程的头文件,新建头文件,HelloWorld.h    ---这个是真正实现的,不是对外的接口了,直接去掉I来命名就可以了
------------------------------------代码分割线-----------------------------------
#ifndef HELLOCOM_h__
#define HELLOCOM_h__

#include "IHelloCom.h"

/*字符串存储的内存大小*/
#define BUFFERLEN 256

/*这个是我们的组件具体的实现的类,还记得上面说的吗,要继承前面的定义的接口类
  看看区别吧!前面的接口类只给出了三个接口,但是这个组件类,包含了具体的一些成员变量,
  以及构造和析构函数,这个已经像是正常的类的实现了吧!
  所以,这个才是真正的内部的实现, 先看代码,稍后再解释部分原理*/
class HelloCom :public IHelloCom
{
public:
HelloCom();
~HelloCom();
public:
/*下面是三个IUnknown的必须要实现的函数,COM规范中必须要有这三个的实现*/
HRESULT STDMETHODCALLTYPE QueryInterface(REFIID iid, LPVOID *ppv); /*这个是用户创建出COM对象时候,查询接口类的方法用的*/
ULONG   STDMETHODCALLTYPE AddRef(); /*对象被引用的计数增加*/
ULONG   STDMETHODCALLTYPE Release(); /*对象被引用的计数减少*/
/*COM的释放都是由系统的COM库来控制的,用户只是能用接口,但是对象不能被删除,因为不能保证还由其他人在用这个COM*/

/*前面提到的IHelloCom接口类提供的对外实现的三个方法!*/
HRESULT STDMETHODCALLTYPE SetSaveString(IN char* pcString);
HRESULT STDMETHODCALLTYPE GetSaveString(OUT char* pcString, long lGetLen);
HRESULT STDMETHODCALLTYPE MessageBoxHelloCom();

protected:
ULONG m_Ref; /*引用计数*/
char acBuffer[BUFFERLEN]; /*字符串存储的内存*/
};
#endif
------------------------------------代码分割线-----------------------------------
(3)  创建工厂类:  
一下子看到这个,有些读者可能会有点蒙,这个工厂类是啥?为啥不继续讲前面定义的具体接口类的实现呢?而是突然跳到了什么工厂类呢?
前面只是简单的说了下COM对外呈现的一种形式,即类似于DLL以及提供的一个接口定义头文件,给外部的用户使用。那么当我们开始进入到怎么来实现COM的时候,这个时候就需要明白COM组件,在我们的windows系统中到底是怎么实现的,是如何运行的?关于这些,建议先多找些资料看看,基本流程原理如下:
1. 客户端程序,也即使用这个COM组件的用户,会先调用一次CoCreateInstance()( 需要传递 组件对象类的CLSID以及所要 接口的IID , 这两个东西其实就是COM组件的在该系统被识别的身份证ID)
2. 系统的 COM库会读取 COM组件(COM为DLL为例,很多资料,会将该COM组件的使用者,称之为客户端,将COM本身提供能力的组件,称之为COM服务器)的CLSID, 在注册表HKEY_CLASSES_ROOT\CLSID的键值下查找该 组件的CLSID,这个键值包含了COM组件的注册信息(包含路径等等)
3. 系统的COM库找到该COM组件的DLL注册表信息后,会读取出该DLL的全路径,并将该DLL加载到客户端的进程空间中
4. 系统的COM库会调用该COM组件的DllGetClassObject()函数,为所请求的COM组件对象类请求 类工厂。(这个什么意思呢?就是说系统的COM库,一旦发现调用DllGetClassObject()时,它传递客户端请求的CLSID, 此时COM规范定义,COM服务一定要提供一个类,用于该COM对象的创建,这样就可以返回一个具体的对象给客户端用户使用了,而这个创建出COM对象的类,就叫 类工厂。所以说,类工厂,其实可以说是一个固定格式的类,里面的方法都是符合COM规范的,是一个模板,不懂也没关系,只要按步实现就可以,反正最重要的就是要提供能够创建出对象的一个类。)
5.  COM库在类工厂中调用CreateInstance()方法,来创建出一个该客户端请求的COM对象,这里COM对象就生成了,你就可以操作这个COM对象了。CreateInstance()方法返回的就是一个接口指针,可以使用前面定义的接口功能了。
(PS: 可能有些不是很准确,但是尽量已经粗浅的方式来说明了)
再跳跃回来,从前面说的,创建 对外接口类,创建 接口实现类,再到 类工厂,其实都最开始从IUnknown类继承的,不过类工厂还有个IClassFactory接口类,我们自己实现的类工厂是从IClassFactory继承的( 而IClassFactory接口类是继承IUnknow接口类的), 我们只要实现IClassFactory接口类中的功能就可以了,最关键的就是CreateInstance(),对吧?
先看看类工厂定义的头文件: CFactory.h
------------------------------------代码分割线-----------------------------------
#ifndef CFactory_h__
#define CFactory_h__
#include <Unknwn.h>

/*类工厂实现类的定义*/
class CFactory : public IClassFactory
{
public:
CFactory();
~CFactory();

/*IUnknown继承的,一定要实现这三个,CFactory类工厂也不例外*/
HRESULT STDMETHODCALLTYPE QueryInterface(const IID& iid, void **ppv);
ULONG STDMETHODCALLTYPE AddRef();
ULONG STDMETHODCALLTYPE Release();

/*IClassFactory类工厂定义的,我们自己要实现的两个成员方法*/
HRESULT STDMETHODCALLTYPE CreateInstance(IUnknown *, const IID& iid, void **ppv); /*创建对象实例*/
HRESULT STDMETHODCALLTYPE LockServer(BOOL bIsLocck); /*增加或减少COM服务器的引用计数*/

protected:
ULONG m_Ref;
};
 ------------------------------------代码分割线-----------------------------------
(4) 实现工厂类:  
看了上面的头文件定义,我们可以直接贴我们类工厂的实现文件了,CFactory.cpp
 ------------------------------------代码分割线-----------------------------------
#include "CFactory.h"
#include "HelloCom.h"
/*在COM规范的接口DllCanUnloadNow()中需要判断计数,是否还有人引用*/
extern ULONG g_LockNumber; /*引用计数*/
extern ULONG g_HelloNumber;

/*这两个是构造和析构,没什么好讲的,就是新建类和释放类的时候会调用的*/
CFactory::CFactory()
{
m_Ref = 0;
}
CFactory::~CFactory()
{

}

/*在类工厂中的查询接口,实现的就是通过IID查询到相应的创建好的对象实例, 
  类工厂的查询接口,不是给用户用的,类工厂的对象是系统的COM库创建的,我们就按照下面来提供指针出去即可*/
HRESULT CFactory::QueryInterface(const IID& iid, void **ppv)
{
/*提供IUnknown的this指针*/
if (iid == IID_IUnknown)
{
*ppv = (IUnknown*)this;
((IUnknown*)(*ppv))->AddRef();
}
/*提供系统的COM库创建的IClassFactory的this指针*/
else if (iid == IID_IClassFactory)
{
*ppv = (IClassFactory*)this;
((IClassFactory*)(*ppv))->AddRef();
}
else {
*ppv = NULL;
return E_NOINTERFACE;
}
return S_OK;
}

ULONG CFactory::AddRef()
{
m_Ref++;
return m_Ref;
}

ULONG CFactory::Release()
{
m_Ref--;
if (m_Ref == 0)
{
delete this;
return 0;
}
return m_Ref;
}

/*这个就是用户调用CreateInstance的实现了,通过IID,我们可以创建HelloCom的实例对象了
  */
HRESULT CFactory::CreateInstance(IUnknown *pUnknownOuter, const IID& iid, void **ppv)
{

HRESULT hr;
HelloCom *pObj; /*看,就在这里,创建出了我们自己的COM对象了*/

*ppv = NULL;
hr = E_OUTOFMEMORY;

if (NULL != pUnknownOuter)
return CLASS_E_NOAGGREGATION;
pObj = new HelloCom();
if (NULL == pObj)
return hr;
hr = pObj->QueryInterface(iid, ppv); /* 这个是HelloCom的Unknown的查询接口实现,要返回自己的This指针
从这里就可以看到,为什么COM规范要规定所有继承IUnknown的接口,都要实现查询接口了,可以前面去看下HelloCom.cpp中实现的QueryInterface()接口,是不是有返回This指针*/
if (hr != S_OK)
{
g_HelloNumber--; /*这个要注意,这个是HelloCom对象的引用计数*/
delete pObj;
}
return S_OK;
}

HRESULT CFactory::LockServer(BOOL bIsLock)
{
if (bIsLock)
g_LockNumber++;
else
g_LockNumber--;
return S_OK;
}
 ------------------------------------代码分割线-----------------------------------

(5) 新建 def模块定义文件:  
标准的COM有下面这四个函数,这个是标准的COM规范用法, 用户需要这么几个函数才可使用COM。
 HelloCom.def
------------------------------------代码分割线-----------------------------------
LIBRARY "HelloCom"
EXPORTS
    ; Explicit exports can go here
DllGetClassObject PRIVATE
DllRegisterServer PRIVATE
DllUnregisterServer PRIVATE
DllCanUnloadNow  PRIVATE
 ------------------------------------代码分割线-----------------------------------


(6)  实现HelloCom类:  
讲了工厂类之后,我们就可以来实现HelloCom的具体类了,我们只要理解了类工厂的实现,那么讲解自己的HelloCom的时候,就比较容易理解了.
 ------------------------------------代码分割线-----------------------------------
#include "CFactory.h"
#include "HelloCom.h"


ULONG g_LockNumber = 0; /*工厂类用的计数, 在退出时,进行计数检查,是否还有别的对象调用*/
ULONG g_HelloNumber = 0; /*自己的COM的计数,HelloCOM的计数检查*/


/*COM组件都是在系统的注册表中GUID形式被识别的,系统的COM库就是看注册表来确定查找,相关的COM组件的*/
// {913AAE18-1D57-4868-AF2F-B47D32163E8F}
extern "C" const GUID CLSID_HelloCom=
{ 0x913aae18, 0x1d57, 0x4868,{ 0xaf, 0x2f, 0xb4, 0x7d, 0x32, 0x16, 0x3e, 0x8f } };

// {416DC65F-48E2-436a-BA34-FC00AC3DA598}
extern "C" const GUID IID_IHelloCom =
{ 0x416dc65f, 0x48e2, 0x436a,{ 0xba, 0x34, 0xfc, 0x0, 0xac, 0x3d, 0xa5, 0x98 } };


HMODULE g_hModule = 0;

/*本COM是作为DLL的,进程内的形式被用户使用的,编译为DLL形式,有DLLMain,但是啥都没做*/
BOOL APIENTRY DllMain(HMODULE hModule,
DWORD  ul_reason_for_call,
LPVOID lpReserved
)
{
g_hModule = hModule;
return TRUE;
}

/*退出计数检查接口,是否还有别的对象使用*/
extern "C" HRESULT STDMETHODCALLTYPE DllCanUnloadNow()
{
if ((g_LockNumber == 0) && (g_HelloNumber == 0))
return S_OK;
else
return S_FALSE;
}

/*组件的自动注册,就是写入注册表,这个接口提供给外面的用户使用,外部用户要先注册一下,然后再使用COM*/
extern "C" HRESULT STDMETHODCALLTYPE DllRegisterServer()
{
char szModule[1024];
DWORD dwResult = ::GetModuleFileNameA((HMODULE)g_hModule, szModule, 1024);
if (dwResult == 0)
return -1;

/*读取出自己的DLL路径,然后注册到系统的注册表中*/
return RegisterServer(CLSID_HelloCom,
szModule,
"HelloCom.Object",
"HelloCom StringBufTest Component",
NULL);
}

/*相应的去注册的接口*/
extern "C" HRESULT STDMETHODCALLTYPE DllUnregisterServer()
{
return UnregisterServer(CLSID_HelloCom,
"STRING.Object", NULL);
}

/*获取实例对象接口,外部用户直接通过这个接口就可以获取到自己的HelloCom对象的指针了
  前面说了很多的类工厂这些,最终就是为了这个服务的,我们再复习下,用户调用的流程:
  用户一开始要调用CoCreateInstance(CLSID_HelloCom),让系统的COM库,去注册表找HelloCom组件,
  系统找到HelloCom组件,我们就可以调用下面这个DllGetClassObject,来创建一个你这个COM的类工厂,
  然后再通过类工厂对象的CreateInstance()来创建HelloCom对象,
  然后通过类工厂的QueryInstance()获取到这个HelloCom对象的This指针了, 看下面代码就可以明白了*/
extern "C" HRESULT STDMETHODCALLTYPE DllGetClassObject(__in REFCLSID rclsid, __in REFIID riid, __deref_out LPVOID FAR* ppv)
{
if (rclsid == CLSID_HelloCom)
{
CFactory *pFactory = new CFactory(); /*创建类工厂对象*/
if (pFactory == NULL)
return E_OUTOFMEMORY;
HRESULT hr = pFactory->QueryInterface(riid, ppv); /*这个用法是,先创建出类工厂对象,再通过类工厂对象的CreateInstance()创建HelloCom对象。*/
return hr;
}
else
{
return CLASS_E_CLASSNOTAVAILABLE;
}
}

/*再通过这个查询,获取到HelloCom的对象指针*/
HRESULT  HelloCom::QueryInterface(REFIID iid, LPVOID *ppv)
{
if (iid == IID_IUnknown)
{
*ppv = (IUnknown*)this;
((IUnknown*)(*ppv))->AddRef();
}
else if (iid == IID_IHelloCom)
{
*ppv = (IClassFactory*)this;
((IClassFactory*)(*ppv))->AddRef();
}
else
{
*ppv = NULL;
return E_NOINTERFACE;
}
return S_OK;
}


/*下面就是HelloCom这个类的实现了, 注意: 有三个IUnknown接口都要实现的*/
HelloCom::HelloCom()
{
m_Ref = 0;
}

HelloCom::~HelloCom()
{

}

ULONG  HelloCom::AddRef()
{
m_Ref++;
return m_Ref;
}

ULONG  HelloCom::Release()
{
m_Ref--;
if (m_Ref == 0)
{
delete this;
return 0;
}
return m_Ref;

}

HRESULT  HelloCom::GetSaveString(char* pcString, long lGetLen)
{
long i;
i = strlen(pcString);
--lGetLen;
if (i > lGetLen) i = lGetLen;

CopyMemory(acBuffer, pcString, i);
acBuffer[i] = '\0';

return(0);
}

HRESULT  HelloCom::SetSaveString(IN char* pcString)
{
DWORD i;
i = strlen(pcString);
if (i > BUFFERLEN-1) i = BUFFERLEN-1;

CopyMemory(acBuffer, pcString, i);
acBuffer[i] = '\0';

return(0);
}

HRESULT HelloCom::MessageBoxHelloCom()
{
MessageBoxA(NULL, "Hello, I am a HellCom MessageBox!", "Message", MB_OK);
return S_OK;
}
 ------------------------------------代码分割线-----------------------------------
(7)  最后注册表的 操作:  
这个是注册表的操作, register.cpp
 ------------------------------------代码分割线-----------------------------------
HRESULT RegisterServer(const CLSID& clsid,         // Class ID
const char *szFileName,     // DLL module handle
const char* szProgID,       //   IDs
const char* szDescription,  // Description String
const char* szVerIndProgID) // optional

{
// Convert the CLSID into a char.
char szCLSID[CLSID_STRING_SIZE];
CLSIDtoString(clsid, szCLSID, sizeof(szCLSID));

// Build the key CLSID\\{...}
char szKey[64];
strcpy_s(szKey, "CLSID\\");
strcat_s(szKey, szCLSID);

// Add the CLSID to the registry.
SetKeyAndValue(szKey, NULL, szDescription);

// Add the server filename subkey under the CLSID key.
SetKeyAndValue(szKey, "InprocServer32", szFileName);

// Add the ProgID subkey under the CLSID key.
if (szProgID != NULL) {
SetKeyAndValue(szKey, "ProgID", szProgID);
SetKeyAndValue(szProgID, "CLSID", szCLSID);
}

if (szVerIndProgID) {
// Add the version-independent ProgID subkey under CLSID key.
SetKeyAndValue(szKey, "VersionIndependentProgID",
szVerIndProgID);

// Add the version-independent ProgID subkey under HKEY_CLASSES_ROOT.
SetKeyAndValue(szVerIndProgID, NULL, szDescription);
SetKeyAndValue(szVerIndProgID, "CLSID", szCLSID);
SetKeyAndValue(szVerIndProgID, "CurVer", szProgID);

// Add the versioned ProgID subkey under HKEY_CLASSES_ROOT.
SetKeyAndValue(szProgID, NULL, szDescription);
SetKeyAndValue(szProgID, "CLSID", szCLSID);
}

return S_OK;
}

//
// Remove the component from the registry.
//
HRESULT UnregisterServer(const CLSID& clsid,      // Class ID
const char* szProgID,       //   IDs
const char* szVerIndProgID) // Programmatic
{
// Convert the CLSID into a char.
char szCLSID[CLSID_STRING_SIZE];
CLSIDtoString(clsid, szCLSID, sizeof(szCLSID));

// Build the key CLSID\\{...}
char szKey[64];
strcpy_s(szKey, "CLSID\\");
strcat_s(szKey, szCLSID);

// Delete the CLSID Key - CLSID\{...}
LONG lResult = DeleteKey(HKEY_CLASSES_ROOT, szKey);

// Delete the version-independent ProgID Key.
if (szVerIndProgID != NULL)
lResult = DeleteKey(HKEY_CLASSES_ROOT, szVerIndProgID);

// Delete the ProgID key.
if (szProgID != NULL)
lResult = DeleteKey(HKEY_CLASSES_ROOT, szProgID);

return S_OK;
}

///
//
// Internal helper functions
//

// Convert a CLSID to a char string.
void CLSIDtoString(const CLSID& clsid,
char* szCLSID,
int length)
{
assert(length >= CLSID_STRING_SIZE);
// Get CLSID
LPOLESTR wszCLSID = NULL;
HRESULT hr = StringFromCLSID(clsid, &wszCLSID);
assert(SUCCEEDED(hr));

// Covert from wide characters to non-wide.
wcstombs(szCLSID, wszCLSID, length);

// Free memory.
CoTaskMemFree(wszCLSID);
}

//
// Delete a key and all of its descendents.
//
LONG DeleteKey(HKEY hKeyParent,           // Parent of key to delete
const char* lpszKeyChild)  // Key to delete
{
// Open the child.
HKEY hKeyChild;
LONG lRes = RegOpenKeyEx(hKeyParent, lpszKeyChild, 0,
KEY_ALL_ACCESS, &hKeyChild);
if (lRes != ERROR_SUCCESS)
{
return lRes;
}

// Enumerate all of the decendents of this child.
FILETIME time;
char szBuffer[256];
DWORD dwSize = 256;
while (RegEnumKeyEx(hKeyChild, 0, szBuffer, &dwSize, NULL,
NULL, NULL, &time) == S_OK)
{
// Delete the decendents of this child.
lRes = DeleteKey(hKeyChild, szBuffer);
if (lRes != ERROR_SUCCESS)
{
// Cleanup before exiting.
RegCloseKey(hKeyChild);
return lRes;
}
dwSize = 256;
}

// Close the child.
RegCloseKey(hKeyChild);

// Delete this child.
return RegDeleteKey(hKeyParent, lpszKeyChild);
}

//
// Create a key and set its value.
//
BOOL SetKeyAndValue(const char* szKey,
const char* szSubkey,
const char* szValue)
{
HKEY hKey;
char szKeyBuf[1024];

// Copy keyname into buffer.
strcpy(szKeyBuf, szKey);

// Add subkey name to buffer.
if (szSubkey != NULL)
{
strcat(szKeyBuf, "\\");
strcat(szKeyBuf, szSubkey);
}

// Create and open key and subkey.
long lResult = RegCreateKeyEx(HKEY_CLASSES_ROOT,
szKeyBuf,
0, NULL, REG_OPTION_NON_VOLATILE,
KEY_ALL_ACCESS, NULL,
&hKey, NULL);
if (lResult != ERROR_SUCCESS)
{
return FALSE;
}

// Set the Value.
if (szValue != NULL)
{
RegSetValueEx(hKey, NULL, 0, REG_SZ,
(BYTE *)szValue,
strlen(szValue) + 1);
}

RegCloseKey(hKey);
return TRUE;
}
 ------------------------------------代码分割线-----------------------------------
(8) 客户端的实现 操作:  

#include "IHelloCom.h"     
#include <Windows.h>     
#include <iostream>     

using namespace std;

// {913AAE18-1D57-4868-AF2F-B47D32163E8F}     
extern "C" const GUID CLSID_HelloCom =
{ 0x913aae18, 0x1d57, 0x4868,{ 0xaf, 0x2f, 0xb4, 0x7d, 0x32, 0x16, 0x3e, 0x8f } };

// {416DC65F-48E2-436a-BA34-FC00AC3DA598}     
extern "C" const GUID IID_IHelloCom =
{ 0x416dc65f, 0x48e2, 0x436a,{ 0xba, 0x34, 0xfc, 0x0, 0xac, 0x3d, 0xa5, 0x98 } };

int main()
{
IHelloCom *pIHello;
IUnknown *pIUk = NULL;
HRESULT hr;
char chGetChar[80];
CLSID rclsid;

if (CoInitialize(NULL) != S_OK)
{
cout << "initialize Failed" << endl;
return -1;
}

hr = CLSIDFromProgID(OLESTR("HelloCom.Object"), &rclsid);
if (hr != S_OK)
{
cout << "Can't find the dictionary CLSID!" << endl;
return -2;
}

hr = CoCreateInstance(rclsid, NULL, CLSCTX_INPROC_SERVER, IID_IUnknown, (LPVOID*)&pIUk);
if (hr != S_OK)
{
cout << "Create object failed!" << endl;
return -2;
}

hr = pIUk->QueryInterface(IID_IHelloCom, (LPVOID*)&pIHello);
if (hr != S_OK) {
pIUk->Release();
printf("QueryInterface IString failed!\n");
return -3;
}
pIHello->SetSaveString("11111111");
pIHello->GetSaveString(chGetChar, sizeof(chGetChar));
cout << chGetChar << endl;
pIHello->MessageBoxHelloCom();
CoUninitialize();
return 0;
}
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值