《Windows核心编程》---Windows服务

  Windows服务(Services),是一些运行在WindowsNTWindows2000Windows XP等操作系统下用户环境以外的程序。它不同于一般的可执行程序,不需要系统登录便可以运行,以完成某些特定的功能。服务提供了管理能力,可以将后台程序转换成服务,然后就可以用命令或者在系统启动用户登录之前启动,并且也可以暂停、恢复和终止。服务信息在注册表中维护。

为了能够在系统中正确运行,在创建一个服务时必须接受一些特殊的规则,最重要的一点是:必须在目标系统中安装并且注册该Service。此外,基于用户界面的Service是没有多大意义的,当然Service可以有用户界面,不过由于每一个Service都是在自己的Windows Station中创建的,所以用户界面相关的API调用无法进行。

一个Windows服务,至少应该包括两个程序:一个是服务本体程序,一个是服务控制程序。服务本体程序一般是一个DOS控制台程序,而控制程序则是一个普通的Win32应用程序,用于对服务进行启动和停止等操作。

Windows服务在服务控制管理器(SCM)的控制下运行,把一个控制台程序转换成一个Windows服务,需要三个主要的步骤来将程序置于SCM控制下:

1)创建一个新的main()入口点以在SCM中注册服务,它提供逻辑服务入口点和名称;

2)转换旧的main()入口点函数为ServiceMain(),它注册服务控制处理器并向SCM通知其状态;

3)编写服务控制处理器函数以响应SCM命令。

 

服务控制管理器(Service Control Manager

SCM的可执行文件镜像是“/Winnt/System32/Services.exe”,Winlogon进程早在系统引导之前就启动它了。SCM运行后,扫描以下注册表键下的内容:

HKEY_LOCAL_MACHINE/System/CurrentControlSet/Services

为它遇到的每个子键在服务数据库中创建一个入口。服务入口包含所有服务相关的参数,也包含了追踪服务状态的域。如果服务或者驱动标识为自动启动,SCM将启动它们,并侦测启动过程中的错误。I/O管理器将在任何用户模式进程执行前加载标识为引导期间启动和系统启动期间启动的驱动程序。

HKEY_LOCAL_MACHINE/System/CurrentControlSet/Services下面有很多子键,一个子键名表示一个服务的内部名称,其下的键值项对应所有与此服务相关的参数。

安装服务所需的最低数量的参数如下:

DisplayName---用户接口程序使用的服务名称。如果没有指定名称,服务注册表键名将作为它的名称。

ErrorControl---如果SCM启动服务时驱动出错,这个值指定SCM的行为。取值如下:

1SERVICE_ERROR_IGNORE(0)I/O管理器忽略驱动返回的错误,但是仍然继续启动操作,不做任何记录;

2SERVICE_ERROR_NORMAL(1)---如果驱动加载或者初始化失败,系统将给用户显示一个警告框,并将错误记录到系统日志中。

ImagePath---指定驱动镜像文件的完整路径。

Start---指定何时启动服务,常用取值如下:

         1SERVICE_BOOT_START(0)---在系统引导期间加载;

         2SERVICE_AUTO_START(1)---在系统启动期间启动

         3SERVICE_DEMAND_START(2)---SCM根据用户的要求显式加载

Type---指定服务类型。若为内核驱动,则设为SERVICE_KERNEL_DRIVER(1)

 

 

服务本体程序设计:

在设计服务程序时必须满足特定函数调用的流程。首先调用main(),然后调用StartServiceCtrlDispatcher()把向ServiceMain()的指针传递给SCMSCM可以通过该指针启动服务,ServiceMain()产生服务状态句柄并注册Handler()。所以服务主体程序一般由main()ServiceMain()Handler()3部分组成。

1、服务控制函数Handler()

服务控制函数Handler()中包含一个switch语句,它用于分发由SCM发送的5个控制通知事件即:SERVICE_CONTROL_STOP, SERVICE_CONTROL_PAUSE, SERVICE_CONTROL_CONTINUE, SERVICE_CONTROL_INTERROGATE, SERVICE_CONTROL_SHUTDOWN,用户分别对这些通知事件进行相应处理,然后将处理后的新服务的最新状态消息发送给SCM

//分发从SCM获得的控制通知事件,并对控制通知事件进行处理

void WINAPI Handler(DWORD Opcod)

{

       switch(Opcod)

       {

              case SERVICE_CONTROL_STOP: //处理停止服务事件

                     //Usercode();       //用户加入自己的代码

                     ss.dwWin32ExitCode = 0;     //设置服务的出错代码

                     ss.dwCurrentState = SERVICE_STOPPED;

                     ss.dwCheckPoint = 0;

                     ss.dwWaitHint = 0;

                     SetServiceStatus(ssh, &ss); //必须随时更新数据库中Service的状态

                     break;

              case SERVICE_CONTROL_INTERROGATE:

                     SetServiceStatus(ssh, &ss); //更新数据库中的Service状态

                     break;

              ......

       }

}

2ServiceMain()

ServiceMain()Service的真正入口点,必须在main()中进行正确的定义。它的作用是:

1)创建状态句柄serviceStatusHandle和服务控制函数Handler

2)向SCM发送服务启动状态信息并创建服务所在的线程;

3)创建终止事件句柄以控制服务的停止。

ServiceMain()的两个参数是由StartService()传递过来的,在ServiceMain()中应该立即调用RegisterServiceCtrlHandler()注册一个Handler去处理控制程序或者控制面板对Service的控制要求:

void WINAPI ServiceMain(DWORD dwArgc, LPTSTR *lpszArgv)

{

       //调用RegisterServiceCtrlHandler()获得服务状态句柄并注册生成Handler

       ssh = RegisterServiceCtrlHandler(SERVICE_NAME, Handler);

      

       //提供单个服务还是多个服务

       ss.dwServiceType = SERVICE_WIN32_OWN_PROCESS | SERVICE_WIN32_SHARE_PROCESS;

       //等用户程序完成后在当前运行状态设为SERVICE_RUNNING

       ss.dwCurrentState = SERVICE_START_PENDING;

       //目前能接受的是停止命令

       ss.dwControlAccepted = SERVICE_ACCEPT_STOP;

       //服务的出错代码

       ss.dwWin32ExitCode = NO_ERROR;

       //置服务在启动/关闭/运行操作中反映操作进度

       ss.dwCheckPoint = 0;

       //置服务在启动/关闭/运行操作时将持续的时间

       ss.dwWaitHint = 0;

      

       //必须随时更新数据库中的Service的状态信息

       SetServiceStatus(ssh, &ss);

      

       //Usercode() here

      

       ss.dwServiceType = SERVICE_WIN32_OWN_PROCESS | SERVICE_INTERACTIVE_PROCESS;

       ss.dwCurrentState = SERVICE_RUNNING;

       ss.dwControlsAccepted = SERVICE_ACCEPT_STOP;

       ss.dwWin32ExitCode = NO_ERROR;

       ss.dwCheckPoint = 0;

       ss.dwWaitHint = 0;

       SetServiceStatus(ssh, &ss);

      

       //UserCode() here

}

 

3main()

Main()是服务的主线程,先填充服务线程入口表SERVICE_TABLE_ENTRY,当SCM开始一个服务时,它总是等待这个服务去调用StartServiceCtrlDispatcher()函数。Main()作为这个进程的主线程应该在程序开始后尽快调用StartServiceCtrlDispatcher(),该函数被调用后并不立即返回,它把本Service的主线程连接到SCM,从而让SCM通过这个连接发送开始、停止、暂停和继续等控制命令给主线程。StartServiceCtrlDispatcher()在整个Service结束时才返回。

下面是主线程的部分实现代码:

#include <windows.h>

SERVICE_STATUS_HANDLE ssh;   //定义于SCM通讯的服务状态句柄²

char *SERVICE_NAME = "ACEService";      //定义Service线程的名字

SERVICE_STATUS ss; //定义服务状态标志

int main(int argc, char *argv[])

{

       SERVICE_TABLE_ENTRY ste[2];    //填充SERVICE_TABLE_ENTRY结构

       ste[0].lpServiceName = SERVICE_NAME;  //线程的名字

       ste[0].lpServiceProc = ServiceMain;      //线程入口地址

       ste[1].lpServiceName = NULL;       //最后必须是NULL

       ste[1].lpServiceProc = NULL;

       StartServiceCtrlDispatcher(ste); //将新的ServiceServiceMain的指针

                                   //传递给SCM,实现新的ServiceSCM中的注册

       return 0;      

}

 

新的main()函数由SCM来调用,负责用SCM来注册服务,以及启动服务控制调度程序。这需要使用一个或者多个逻辑服务的名称和入口点来调用函数StartServiceCtrlDispatcher

BOOL StartServiceCtrlDispatcher(

       LPSERVICE_TABLE_ENTRY lpServiceStartTable);

参数lpServiceStartTableSERVICE_TABLE_ENTRY条目数组的地址,而每个条目时逻辑服务名称和入口点,数组的结尾由一对NULL条目来指示。

如果注册成功则返回TRUE,如果服务已经在运行或者更新注册表(HKEY_LOCAL_MACHINE/SYSTEM/CurrentControlSet/Services)时有问题,就会出现错误。调用StartServiceCtrlDispatcher服务进程的主线程把线程连接到SCM。而SCM用调用线程作为服务控制调度线程来注册服务。SCM不会返回到调用线程,直到所有的服务都终止了。

另一种类型的带有单个逻辑服务的服务主程序如下:

#include <...>

void WINAPI ServiceMain(DWORD argc, LPTSTR argv[]);

static LPTSTR ServiceName = _T("AceCommandLineService");

 

VOID _tmain(int argc, LPTSTR argv[])

{

       SERVICE_TABLE_ENTRY DispatchTable[] =

       {

              {ServiceName, ServiceMain},

              {NULL, NULL}

       };

 

       if(!StartServiceCtrlDispatcher(DispatchTable))

              //ServiceMain() will not run until started by the SCM

              //Return here only when all services have terminated.

              ReportError(_T("Failed"), 1, TRUE);

       return;

}

 

SCM调用的服务控制处理器必须能够控制相关的逻辑服务,每个逻辑服务必须使用RegisterServiceCtrlHandlerEx函数立即注册处理器。函数的调用应该在ServiceMain()的开头处,而且不需再次调用。SCM在接收到服务控制请求后调用处理器:

SERVICE_STATUS_HANDLE RegisterServiceCtrlHandleEx(

       LPCTSTR lpServiceName,

       LPHADNLE_FUNCTION_EX lpHandlerProc,

       LPVOID lpContext);

参数lpServiceName—是用户在服务表条目中提供的逻辑服务名称;

lpHandlerProc—是扩展处理器函数的地址;

lpContext—是传递给控制处理器的用户自定义数据,允许单个控制处理器使用相同的处理器来区分多个服务。

 

现在处理器已被注册,接着使用SetServiceStatus来把服务状态设置成SERVICE_START_PENDING。服务控制处理器每个调用时必须设置状态,即使状态没有改变:

BOOL SetServiceStatus(

       SERVICE_STATUS_HANDKE hServiceStatus,

       LPSERVICE_STATUS lpServiceStatus);

参数hServiceStatus—是由RegisterServiceCtrlHandlerEx返回的SERVICE_STATUS_HANDLE。因此,RegisterServiceCtrlHandlerEx调用必须在SetServiceStatus调用之前。

lpServiceStatus—指向SERVICE_STATUS结构,描述了服务属性、状态和特性。

 

SERVICE_STATUS结构定义如下:

typedef struct _SERVICE_STATUS

{

       DWORD dwServiceType;       //服务类型,可能是SERVICE_KERNEL_DRIVER

       DWORD dwCurrentState;       //服务当前状态

       DWORD dwControlAccepted;      //服务能够接收的控制代码

       DWORD dwWin32ExitCode;       //出错代码,当服务启动或停止时它使用该参数报告一个错误

       DWORD dwServiceSpecificExitCode;   //当错误发生时,服务返回的服务相关的错误代码

       DWORD dwCheckPoint; //当服务启动时,每完成一步它就增加这个值

       DWORD dwWaitHint;    //未决的启动、停止等操作花费的时间

}SERVICE_STATUS, *LPSERVICE_STATUS;

参数dwWin32ExitCode是逻辑服务的线程退出码。而服务在运行或者正常终止时必须将其设为NO_ERROR

当服务启动或者停止时,dwServiceSpecificExitCode可以用来指示错误,但是该值会被忽略;除非dwWin32ExitCode被设置成ERROR_SERVICE_SPECIFIC_ERROR

dwCheckPoint应该由服务周期性地增加,以报告含初始化和关闭的所有步骤的过程。如果服务没有启动、停止、暂停或者继续挂起,则该值是非法的,为0

dwWaitHint是用dwCheckPoint变量的增加值或者dwCurrentState的改变值分别调用SetServiceStatus之间所花费的时间毫秒值;

服务类型dwServiceType取值如下:

SERVICE_WIN32_OWN_PROCESS    表示Windows服务利用自己的资源在自己的进程中运行

SERVICE_WIN32_SHARE_PROCESS       表示Windows服务于其他服务共享进程以便多个服务可以共享资源、环境变量等

SERVICE_KERNEL_DRIVER           表示Windows设备驱动器

SERVICE_FILE_SYSTEM_DRIVER 指定Windows文件系统驱动器

SERVICE_INTERACTIVE_PROCESS      表示某Windows服务进程可以通过桌面与用户交互

 

参数dwCurrentState表示当前服务的状态:

SERVICE_STOPPED        服务没有运行

SERVICE_START_PENDING 服务正在启动中,但是还没有准备好响应请求

SERVICE_STOP_PENDING   服务正在停止中,但是还没有完成关闭

SERVICE_RUNNING     服务在运行

SERVICE_CONTINUE_PENDING        在服务处于暂停状态后服务继续处于挂起状态

SERVICE_PAUSE_PENDING          服务暂停处于挂起状态,但是服务没有完全处于暂停状态

SERVICE_PAUSED 服务被暂停

 

参数dwControlsAccept指定了服务接受并由服务控制处理器处理的控制代码:

SERVICE_ACCEPT_STOP      启用SERVICE_CONTROL_STOP

SERVICE_ACCEPT_PAUSE_CONTINUE           启用SERVICE_CONTROL_PAUSESERVICE_CONTROL_CONTINUE

SERVICE_ACCEPT_SHUTDOWN                当发生系统关闭时通知服务,这使得系统可以向服务发送SERVICE_CONTROL_SHUTDOWN

SERVICE_ACCEPT_PARAMCHANGE     该启动参数可以无需重启就可改变,通知是SERVICE_CONTROL_PARAMCHANGE

 

服务控制处理器,即RegisterServiceCtrlHandlerEx中指定的回调函数具有如下形式:

DWORD WINAPI HandlerEx(

       DWORD dwControl,

       DWORD dwEventType,

       LPVOID lpEventData,

       LPVOID lpContext);

参数dwControl表示由SCM发送的应该处理的实际控制信号;它有14中可能值,如SERVICE_CONTROL_STOPSERVICE_CONTROL_PAUSE...

参数lpContext是处理器被注册时传递给RegisterServiceCtrlHandlerEx的用户自定义数据。

 

下面是编写服务的例子:

#include <...>

#define UPDATE_TIME 1000

 

VOID LogEvent(LPCTSTR, DWORD, BOOL);

void WINAPI ServiceMain(DWORD argc, LPTSTR argv[]);

VOID WINAPI ServerCtrlHandlerEx(DWORD, DWORD, LPVOID, LPVOID);

void UpdateStatus(int ,int);     //Calls SetServiceStatus

int ServiceSpecific(int, LPTSTR *);         //Former main program

volatile static BOOL ShutDown = FALSE;

volatile static BOOL PauseFlag = FALSE;

static SERVICE_STATUS hServStatus;

static SERVICE_STATUS_HANDLE hSStat;         //Handle to set status

static LPTSTR ServiceName = _T("AceCommandLineService");

static LPTSTR LogFileName = _T("CommandLineServiceLog.txt");

 

//Main routing that starts the service control dispatcher

VOID _tmain(int argc, LPTSTR argv[])

{

       SERVICE_TABLE_ENTRY DispatchTable[] =

       {

              {ServiceName, ServiceMain},

              {NULL, NULL}

       };

       StartServiceCtrlDispatcher(DispatchTable);

       return 0;

}

 

//ServiceMain entry point, called when the service is created

void WINAPI ServiceMain(DWORD argc, LPTSTR argv[])

{

       DWROD i;

       DWORD Context = 1;

       //Set the current directory and open a log file

       //appending to an existing file

             

       //Set all server status data members

       hServStatus.dwServiceType = SERVICE_WIN32_OWN_PROCESS;

       hServStatus.dwCurrentState = SERVICE_START_PENDING;

       hServStatus.dwControlAccepted = SERVICE_ACCEPT_STOP |

              SERVICE_ACCEPT_SHUTDOWN | SERVICE_ACCEPT_PAUSE_CONTINUE;

       hServStatus.dwWin32ExitCode = ERROR_SERVICE_SPECIFIC_ERROR;

       hServStatus.dwServiceSpecificExitCode = 0;

       hServStatus.dwCheckPoint = 0;

       hServStatus.dwWaitHint = 2*CS_TIMEOUT;

      

       hSStat = RegisterServiceCtrlHandlerEx(ServiceName,

              ServiceCtrlHandler, &Context);

             

       SetServiceStatus(hSStat, &hServStatus);

      

       //Start service-specific work;generic work is complete

       if(ServiceSpecific(argc, argv) != 0)

       {

              hServStatus.dwCurrentState = SERVICE_STOPPED;

              hServStatus.dwServiceSpecificExitCode = 1;

              //Server initialization failed

              SetServiceStatus(hSStat, &hServStatus);

              return;  

       }

      

       //We will only return here when the ServiceSpecific function

       //completes, indicating system shutdown

       UpdateStatus(SERVICE_STOPPED, 0);

       return;

}

 

//Set a new service status and checkpoint

//either specific value or increment

void UpdateStatus(int NewStatus, int Check)

{

       if(Check < 0)

              hServStatus.dwCheckPoint++;

       else

              hServStatus.dwCheckPoint = Check;

       if(NewStatus >= 0)

              hServStatus.dwCurrentState = newStatus;

      

       SetServiceStatus(hSStat, &hServStatus);

       return;

}

 

//Control handler function,invoked by the SCM to run

//in the same thread as the main program

VOID WINAPI ServerCtrlHandlerEx(DWORD Control, DWORD EventType,

       LPVOID lpEventData, LPVOID lpContext)

{

       switch(Control)

       {

              case SERVICE_CONTROL_SHUTDOWN:

              case SERVICE_CONTROL_STOP:

                     ShutDown = TRUE;

                     UpdateStatus(SERVICE_STOP_PENDING, -1);

                     break;

              case SERVICE_CONTROL_PAUSE:

                     PauseFlag = TRUE;

                     break;

              case SERVICE_CONTROL_CONTINUE:

                     PauseFlag = FALSE;

                     break;

              case SERVICE_CONTROL_INTERROGATE:

                     break;

              default:

                     if(Control > 127 && Control < 256)//User defined

                            break;   

       }     

       UpdateStatus(-1, -1);  //Increment checkpoint

       return;

}

 

//This is the service-specific function,or "main",and is

//called from the more generic ServiceMain.

int ServiceSpecific(int argc, LPTSTR argv[])

{

       UpdateStatus(-1, -1);

       //---Initialize system---

       //Be sure to update the checkpoint periodically

      

       return 0;      

}

 

 

服务控制程序设计

控制程序一般是一个普通的Win32应用程序,用于对服务进行启动和停止等操作。

首先的打开SCM,获取句柄然后允许创建服务:

SC_HANDLE OpenSCManager(

       LPCTSTR lpMachineName, //NULL表示SCM在本地系统,也可以访问网络上的SCM

       LPCTSTR lpDatabaseName,    //通常为NULL

       DWORD dwDesiredAccess);   

 

注册服务使用函数CreateService

SC_HANDLE CreateService(

       SC_HANDLE hSCManager,     //OpenSCManager获得的SC_HANDLE

       LPCTSTR lpServiceName, //指定要安装的服务的名称。

//此字符串对应服务注册处中一个子键的名称

       LPCTSTR lpDisplayName,       //用户界面程序使用的标识该服务的名称

                                   //它对应服务注册处lpServiceName子键下DisplayName的键值

       DWORD dwDesiredAccess,     //访问权限

       DWORD dwServiceType, //服务类型,对应服务注册处lpServiceName子键下Type的键值

       DWORD dwStartType,    //指定何时启动此服务。如果打算通过命令启动,可以传递

//SERVICE_DEMAND_START,如果打算让此服务在系统引导

//时自动启动,传递SERVICE_AUTO_START,它对应

    //注册表中的Start键值

       DWORD dwErrorControl,      //指定驱动启动失败这个错误的严重性。

//SERVICE_ERROR_IGNORE表示忽略所有错误;

//SERVICE_ERROR_NORMAL表示记录下可能出现的错误。

//此参数对应注册表中的ErrorControl键值

       LPCTSTR lpBinaryPathName, //驱动二进制文件(.sys文件)的路径,

//对应注册表中的ImagePath键值

       LPCTSTR lpLoadOrderGroup, //指定此服务所属的加载顺序组的名字

       LPDWORD lpdwTagId,   //指定一个在lpLoadOrderGroup组中唯一的标签

       LPCTSTR lpDependencies,       //指定一组此服务依靠的服务的名字

       LPCTSTR lpServiceStartName,       //指定此服务应该运行在哪一个账户下

       LPCTSTR lpPassword);     //指定lpServiceStartName账户对应的密码

注册后新的服务位于注册表项中:

HKEY_LOCAL_MACHINE/SYSTEM/CurrentControlSet/Services

 

服务创建后并不立即执行,我们通过指定CreateService函数获取的句柄,以及服务主函数的argcargv命令行参数,可以启动ServiceMain()函数:

BOOL StartService(

       SC_HANDLE hService,      //指定打开的服务,这是OpenServiceCreateService返回的句柄

       DWORD argc,    //对设备驱动来说,此参数永远是NULL

       LPTSTR argv[]);//对驱动服务来说,此参数应该设为NULL

 

通过告知SCM使用指定控制选项来调用服务控制处理器,即可控制服务:

BOOL ControlService(

       SC_HANDLE hService,      //服务句柄

       DWORD dwControlCode,      //向服务发送的控制代码

//要停止服务,应发送SERVICE_CONTROL_STOP

       LPSERVICE_STATUS lpServStat);   //返回服务的状态

 

使用以下方式可以获得SERVICE_STATUS数据结构中服务的当前状态值:

BOOL QueryServiceStatus(

       SC_HANDLE hService,      //要查询服务的句柄

       LPSERVICE_STATUS lpServiceStatus);   //用于返回状态信息

 

实例代码如下:

void ACECreate()       //创建服务

{

       SC_HANDLE scm;      //打开指定服务

       SC_HANDEL svc;      

       //打开SCM数据库并返回句柄

       scm = OpenSCManager(NULL, NULL, SC_MANAGER_CREATE_SERVICE);

       if(scm != NULL)

       {

              svc = CreateService(scm, "aceservice", "ace's Windows Service",

                            SERVICE_ALL_ACCESS,

                            SERVICE_WIN32_OWN_PROCESS | SERVICE_INTERACTIVE_PROCESS,

                            SERVICE_AUTO_START, SERVICE_ERROR_IGNORE,

                            "C://ServiceFileName.exe",      //Service本体程序路径

                            NULL, NULL, NULL, NULL, NULL);

              if(svc != NULL)

                     CloseServiceHandle(svc);  //关闭新服务句柄

              CloseServiceHandle(scm); //关闭SCM数据库句柄

       }

}

 

void ACEDelete() //删除服务

{

       SC_HANDLE scm;

       SC_HANDLE svc;

       SERVICE_STATUS ServiceStatus;

       //指定服务控制管理器建立连接并打开服务管理数据库

       scm = OpenSCManager(NULL, NULL, SC_MANAGER_CONNECT);

       if(scm != NULL)

       {

              QueryServiceStatus(svc, &ServiceStatus);      //返回服务状态,保存到状态结构变量中

              if(ServiceStatus.dwCurrentState == SERVICE_RUNNING)

              {

                     //删除之前先得停止服务

                     ControlService(svc, SERVICE_CONTROL_STOP, &ServiceStatus);      

                     DeleteService(svc);     //向服务发送控制代码

                     CloseServiceHandle(svc); //删除Service后,最后再调用CloseServiceHandle()

                                          //以便立即从数据库中移走此条目

              }                   

              CloseServiceHandle(scm);

       }     

}

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值