HTTP服务器上断点下载文件

本文详细介绍如何使用HTTP协议从服务器下载文件,包括构建HTTP请求头、实现断点续传等功能,并提供了一个C++类的实现示例。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

从HTTP服务器上下载一个文件有很多方法,“热心”的微软提供了 WinInet 类,用起来也很方便。当然,我们也可以自己实现这些功能,通过格式化请求头很容易就能实现断点续传和检查更新等等功能 。
  1. 连接主机
  2. 格式化请求头
  3. 设置接收,发送超时
      

要想从服务器下载文件,首先要向服务器发送一个请求。HTTP 请求头由若干行字符串组成。下面结合实例说说 HTTP 请求头的格式。假设要下载 http://www.sina.com.cn/index.html 这个网页 ,那么请求头的写法如下:

第1行:方法,请求的内容,HTTP协议的版本
下载一般可以用GET方法,请求的内容是“/index.html”,HTTP协议的版本是指浏览器支持的版本,对于下载软件来说无所谓,所以用1.1版 “HTTP/1.1”;
“GET /index.html HTTP/1.1”

第2行:主机名,格式为“Host:主机”
在这个例子中是:“Host:www.sina.com.cn”

第3行:接受的数据类型,下载软件当然要接收所有的数据类型,所以:
“Accept:*/*”

第4行:指定浏览器的类型
有些服务器会根据客户服务器种类的不同会增加或减少一些内容,在这个例子中可以这样写:

“User-Agent:Mozilla/4.0 (compatible; MSIE 5.00; Windows 98)”

第5行:连接设置
设定为一直保持连接:“Connection:Keep-Alive”

第6行:若要实现断点续传则要指定从什么位置起接收数据,格式如下:

“Range: bytes=起始位置 - 终止位置”

比如要读前500个字节可以这样写:“Range: bytes=0 - 499”;从第 1000 个字节起开始下载:

“Range: bytes=999 -”

最后,别忘了加上一行空行,表示请求头结束。整个请求头如下:

GET /index.html HTTP/1.1
Host:www.sina.com.cn
Accept:*/*
User-Agent:Mozilla/4.0 (compatible; MSIE 5.00; Windows 98)
Connection:Keep-Alive
下面用例子看看如何进行断点的下载吧






//DownloadFile.h:interfacefortheCDownloadFileclass.
//
//
////////////////////////////////////////////////////////////////////

#
if!defined(AFX_DOWNLOADFILE_H__E9A59779_BEF9_4A78_8D0E_ED8C9498E07C__INCLUDED_)
#defineAFX_DOWNLOADFILE_H__E9A59779_BEF9_4A78_8D0E_ED8C9498E07C__INCLUDED_

#
if_MSC_VER>1000
#pragmaonce
#endif
//_MSC_VER>1000

#defineNOTIFY_MSG_WPARAM_GENDOWNFILEID
0x01

#defineNOTIFY_MSG_LOW_WPARAM_FULLSIZE
0x10
#defineNOTIFY_MSG_LOW_WPARAM_CURRENTSIZE
0x20
#defineNOTIFY_MSG_LOW_WPARAM_DOWNSIZE
0x30
#defineNOTIFY_MSG_LOW_WPARAM_DOWNSPEED
0x40

classCDownloadFile
{
public:
BOOLOpenRedirectHttpURL(CString
&strOldLocation,CInternetSession&cSession);
BOOLDownLoadFile(LPCTSTRlpFileURL,LPCTSTRlpSaveFile);
CDownloadFile();
virtual
~CDownloadFile();
LPCTSTRGetSavedFileName(){
returnm_strSaveToFile;}
LPCTSTRGetDownURL(){
returnm_strFileURL;}

public:
WORDGenFileID();
voidRegisterNotifyWindow(DWORDdwThreadID,HWNDhWnd,DWORDdwMsg);
BOOLGetUNCFile();
boolm_bForceReload;
DWORDm_TimeOut;
WORDm_wFileID;

protected:
DWORDm_dwThreadID;
voidPostNotifyMessage(WPARAMwParam,LPARAMlParam);
DWORDm_dwMsgID;
HWNDm_hNotify;
BOOLGetFtpFile(CInternetSession
&cSession);
BOOLGetHttpFile(CInternetSession
&cSession);
CStringm_strTmpFileName;
CStringm_strFileURL;
CStringm_strSaveToFile;
CStringm_rawHeaders;
floatm_transferRate;
DWORDm_infoStatusCode;
};

#endif
//!defined(AFX_DOWNLOADFILE_H__E9A59779_BEF9_4A78_8D0E_ED8C9498E07C__INCLUDED_)


//DownloadFile.cpp:implementationoftheCDownloadFileclass.
//
//
////////////////////////////////////////////////////////////////////

#include
"stdafx.h"
#include
"DownloadFile.h"

#pragmacomment(lib,
"Wininet.lib")

#include
"shlwapi.h"
#pragmacomment(lib,
"shlwapi.lib")

#ifdef_DEBUG
#undefTHIS_FILE
static
charTHIS_FILE[]=__FILE__;
#define
newDEBUG_NEW
#endif

#defineBUFFER_SIZE
4095

constTCHARszHeaders[]
=_T("Accept:*/*\r\nUser-Agent:Mozilla/4.0(compatible;MSIE5.00;Windows98)\r\n");

//////////////////////////////////////////////////////////////////////
//
Construction/Destruction
//
////////////////////////////////////////////////////////////////////

CDownloadFile::CDownloadFile()
{
m_TimeOut
=0;
m_bForceReload
=true;
m_dwThreadID
=0;
m_hNotify
=NULL;
m_dwMsgID
=0;
m_wFileID
=0;
}

CDownloadFile::
~CDownloadFile()
{

}

BOOLCDownloadFile::DownLoadFile(LPCTSTRlpFileURL,LPCTSTRlpSaveFile)
{
BOOLbRet
=FALSE;
if(!::PathIsURL(lpFileURL))
{
returnbRet;
}
m_strSaveToFile
=lpSaveFile;
m_strFileURL
=lpFileURL;
m_strTmpFileName
=lpSaveFile;
m_strTmpFileName
+=_T(".df!");
CStringstrServer,strObject;INTERNET_PORTnPort;
CStringstrAgentCaption
=_T("UpdateDownload");
strAgentCaption
+=::PathFindFileName(lpSaveFile);
DWORDdwFlags
=0;
InternetGetConnectedState(
&dwFlags,0);
CInternetSessionsession(strAgentCaption,
1,
(dwFlags
&INTERNET_CONNECTION_PROXY)==INTERNET_CONNECTION_PROXY?INTERNET_OPEN_TYPE_PRECONFIG:INTERNET_OPEN_TYPE_PRECONFIG_WITH_NO_AUTOPROXY,
NULL,NULL,
0);
AfxParseURL(m_strFileURL,dwFlags,strServer,strObject,nPort);

if(m_TimeOut!=0)
session.SetOption(INTERNET_OPTION_DATA_RECEIVE_TIMEOUT,m_TimeOut);
if(!m_wFileID)
m_wFileID
=GenFileID();
PostNotifyMessage(NOTIFY_MSG_WPARAM_GENDOWNFILEID,m_wFileID);

try
{
if(dwFlags==AFX_INET_SERVICE_HTTP)
{
bRet
=GetHttpFile(session);
}
elseif(dwFlags==AFX_INET_SERVICE_FTP)
{
bRet
=GetFtpFile(session);
}
elseif(dwFlags==AFX_INET_SERVICE_FILE)
{
if(UrlIsFileUrl(m_strFileURL))
bRet
=GetUNCFile();
}
else
{
;
}
}
catch(CException*pEx)
{
TCHARszErrorMsg[MAX_PATH]
={0};
pEx
->GetErrorMessage(szErrorMsg,MAX_PATH);
TRACE(_T(
"Exception:%s\n"),szErrorMsg);
pEx
->Delete();
}

session.Close();
m_wFileID
=0;
if(bRet)
{
if(!::MoveFileEx(m_strTmpFileName,m_strSaveToFile,MOVEFILE_REPLACE_EXISTING))
{
Sleep(
1000);
::MoveFileEx(m_strTmpFileName,m_strSaveToFile,MOVEFILE_REPLACE_EXISTING);
}
}
returnbRet;
}

BOOLCDownloadFile::GetHttpFile(CInternetSession
&cSession)
{
BOOLbRet
=FALSE;
CFilem_TmpFile;
CFileExceptionfileException;

if(!m_TmpFile.Open(m_strTmpFileName,
CFile::modeCreate
|CFile::modeNoTruncate|CFile::modeReadWrite
|CFile::shareDenyWrite|CFile::typeBinary,
&fileException))
{
TRACE(_T(
"OpenFilefailed:%d\n"),fileException.m_cause);
returnbRet;
}
CStringstrRangeQuest;

if(m_TmpFile.GetLength()>0)
{
PostNotifyMessage(MAKEWPARAM(NOTIFY_MSG_LOW_WPARAM_CURRENTSIZE,m_wFileID),m_TmpFile.GetLength());
m_TmpFile.SeekToEnd();
strRangeQuest.Format(_T(
"%sRange:bytes=%d-\r\n"),szHeaders,m_TmpFile.GetLength());
}
else
strRangeQuest
=szHeaders;

DWORDdwCount
=0;
CHttpFile
*pFile=NULL;
CStringstrTmpURL
=m_strFileURL;
try
{
DWORDdwFlags
=INTERNET_FLAG_TRANSFER_BINARY
|INTERNET_FLAG_DONT_CACHE
|INTERNET_FLAG_PRAGMA_NOCACHE
;
if(m_bForceReload){
dwFlags
|=INTERNET_FLAG_RELOAD;
}
//HereFindURLFileRedirect.
//
OpenRedirectHttpURL(strTmpURL,cSession);
pFile=(CHttpFile*)cSession.OpenURL(strTmpURL,1,dwFlags,strRangeQuest,-1);
}
catch(CInternetException*e)
{
TCHARszCause[MAX_PATH]
={0};
e
->GetErrorMessage(szCause,MAX_PATH);
e
->Delete();
deletepFile;
pFile
=NULL;
returnbRet;
}

COleDateTimestartTime
=COleDateTime::GetCurrentTime();
DWORDdwHttpFileSize
=0;
if(pFile)
{
BYTEbuffer[BUFFER_SIZE
+1]={0};
try{
UINTnRead
=0;
pFile
->QueryInfo(HTTP_QUERY_CONTENT_LENGTH|HTTP_QUERY_FLAG_NUMBER,dwHttpFileSize);
PostNotifyMessage(MAKEWPARAM(NOTIFY_MSG_LOW_WPARAM_FULLSIZE,m_wFileID),dwHttpFileSize);
TRACE(_T(
"TotoalLengthis%d\n"),dwHttpFileSize);
dwCount
=0;
do
{
nRead
=pFile->Read(buffer,BUFFER_SIZE);
if(nRead>0)
{
buffer[nRead]
=0;
m_TmpFile.Write(buffer,nRead);

COleDateTimeSpanelapsed
=COleDateTime::GetCurrentTime()-startTime;
doubledSecs=elapsed.GetTotalSeconds();
PostNotifyMessage(MAKEWPARAM(NOTIFY_MSG_LOW_WPARAM_DOWNSIZE,m_wFileID),dwCount);
if(dSecs>0.0)
{
dwCount
+=nRead;
m_transferRate
=(float)(dwCount/1024.0/dSecs);
TRACE(
"Read%dbytes(%0.1fKb/s)\n",dwCount,m_transferRate);
}
else
{
TRACE(
"Read%dbytes\n",dwCount);
m_transferRate
=(float)(dwCount/1024.0);
}
PostNotifyMessage(MAKEWPARAM(NOTIFY_MSG_LOW_WPARAM_DOWNSPEED,m_wFileID),(LPARAM)m_transferRate);
}
}
while(nRead>0);
bRet
=TRUE;
}
catch(CFileException*e)
{
TCHARszCause[MAX_PATH]
={0};
e
->GetErrorMessage(szCause,MAX_PATH);
TRACE(
"ErrorMsg:%s\n",szCause);
e
->Delete();
deletepFile;
m_TmpFile.Close();
returnFALSE;
}
pFile
->QueryInfoStatusCode(m_infoStatusCode);
pFile
->QueryInfo(HTTP_QUERY_RAW_HEADERS,m_rawHeaders);
pFile
->Close();
m_TmpFile.Close();
deletepFile;
}

returnbRet;
}

BOOLCDownloadFile::OpenRedirectHttpURL(CString
&strOldLocation,CInternetSession&cSession)
{
BOOLbRet
=FALSE;
CHttpFile
*pFile=NULL;
CHttpConnection
*pServer=NULL;
CStringstrServerName,strObject;
INTERNET_PORTnPort
=0;
DWORDdwServiceType
=0;

if(!AfxParseURL(strOldLocation,dwServiceType,strServerName,strObject,nPort)||
dwServiceType
!=INTERNET_SERVICE_HTTP)
{
TRACE(_T(
"NotAHttpQuest!\n"));
returnbRet;
}

pServer
=cSession.GetHttpConnection(strServerName,nPort);

pFile
=pServer->OpenRequest(CHttpConnection::HTTP_VERB_GET,
strObject,NULL,
1,NULL,NULL,
INTERNET_FLAG_EXISTING_CONNECT
|INTERNET_FLAG_NO_AUTO_REDIRECT);
pFile
->AddRequestHeaders(szHeaders);
pFile
->SendRequest();

DWORDdwRet;
pFile
->QueryInfoStatusCode(dwRet);

//ifaccesswasdenied,prompttheuserforthepassword

if(dwRet==HTTP_STATUS_DENIED)
{
DWORDdwPrompt;
dwPrompt
=pFile->ErrorDlg(NULL,ERROR_INTERNET_INCORRECT_PASSWORD,
FLAGS_ERROR_UI_FLAGS_GENERATE_DATA
|FLAGS_ERROR_UI_FLAGS_CHANGE_OPTIONS,NULL);

//iftheusercancelledthedialog,bailout

if(dwPrompt!=ERROR_INTERNET_FORCE_RETRY)
{
TRACE(_T(
"Accessdenied:Invalidpassword\n"));
//closeuptheredirectedsite

pFile
->Close();
deletepFile;
pServer
->Close();
deletepServer;

returnbRet;
}

pFile
->SendRequest();
pFile
->QueryInfoStatusCode(dwRet);
}

//wereweredirected?
//theseresponsestatuscodescomefromWININET.H

if(dwRet==HTTP_STATUS_MOVED||
dwRet
==HTTP_STATUS_REDIRECT||
dwRet
==HTTP_STATUS_REDIRECT_METHOD)
{
CStringstrNewLocation;
pFile
->QueryInfo(HTTP_QUERY_RAW_HEADERS_CRLF,strNewLocation);
intnPlace=strNewLocation.Find(_T("Location:"));
if(nPlace==-1)
{
TRACE(_T(
"Error:Siteredirectswithnonewlocation\n"));
//closeuptheredirectedsite

pFile
->Close();
deletepFile;
pServer
->Close();
deletepServer;
returnbRet;
}
strNewLocation
=strNewLocation.Mid(nPlace+10);
nPlace
=strNewLocation.Find('\n');
if(nPlace>0)
strNewLocation
=strNewLocation.Left(nPlace);
strOldLocation
=strNewLocation;
}
if(dwRet==HTTP_STATUS_OK)
{
bRet
=TRUE;
}
//closeuptheredirectedsite
pFile->Close();
deletepFile;
pServer
->Close();
deletepServer;

returnbRet;
}

BOOLCDownloadFile::GetFtpFile(CInternetSession
&cSession)
{
BOOLbRet
=FALSE;
CFilem_TmpFile;
CFileExceptionfileException;

if(!m_TmpFile.Open(m_strTmpFileName,
CFile::modeCreate
|CFile::modeNoTruncate|CFile::modeReadWrite
|CFile::shareDenyWrite|CFile::typeBinary,
&fileException))
{
TRACE(_T(
"OpenFilefailed:%d\n"),fileException.m_cause);
returnbRet;
}

DWORDdwCount
=0;
CFtpConnection
*pFtpConn=NULL;
CInternetFile
*pFile=NULL;
try
{
CStringstrServerName,strObject,strUserName,strPassword;
INTERNET_PORTnPort
=0;
DWORDdwServiceType
=0;
CStringstrRestPointCommand;

if(!AfxParseURLEx(m_strFileURL,dwServiceType,strServerName,strObject,nPort,
strUserName,strPassword)
||
dwServiceType
!=INTERNET_SERVICE_FTP)
{
TRACE(_T(
"NotAFtpQuest!\n"));
}
//CFtpConnectionERROR_INTERNET_NO_DIRECT_ACCESSCInternetSession
if(strUserName.IsEmpty())
pFtpConn
=cSession.GetFtpConnection(strServerName,NULL,NULL,nPort,m_bForceReload);
else
pFtpConn
=cSession.GetFtpConnection(strServerName,strUserName,strPassword,nPort,m_bForceReload);
if(m_TmpFile.GetLength())
{
PostNotifyMessage(MAKEWPARAM(NOTIFY_MSG_LOW_WPARAM_CURRENTSIZE,m_wFileID),m_TmpFile.GetLength());
m_TmpFile.SeekToEnd();
strRestPointCommand.Format(_T(
"REST%d"),m_TmpFile.GetLength());
//strRestPointCommand.Format(_T("ls"));
if(!FtpCommand((*pFtpConn),FALSE,FTP_TRANSFER_TYPE_ASCII,
strRestPointCommand,
0,0))
{
TRACE(_T(
"FtpCommandfailed,error:%d\n"),GetLastError());
m_TmpFile.SeekToBegin();
}
}
if(pFtpConn)
{
pFile
=pFtpConn->OpenFile(strObject);
}
}
catch(CInternetException*e)
{
TCHARszCause[MAX_PATH]
={0};
e
->GetErrorMessage(szCause,MAX_PATH);
e
->Delete();
deletepFile;
deletepFtpConn;
returnbRet;
}

COleDateTimestartTime
=COleDateTime::GetCurrentTime();
DWORDdwFtpFileSize
=0;
if(pFile)
{
BYTEbuffer[BUFFER_SIZE
+1]={0};
try{
UINTnRead
=0;
dwFtpFileSize
=FtpGetFileSize((*pFile),0);
PostNotifyMessage(MAKEWPARAM(NOTIFY_MSG_LOW_WPARAM_FULLSIZE,m_wFileID),dwFtpFileSize);
TRACE(_T(
"TotoalLengthis%d\n"),dwFtpFileSize);
dwCount
=0;
do
{
nRead
=pFile->Read(buffer,BUFFER_SIZE);
if(nRead>0)
{
buffer[nRead]
=0;
m_TmpFile.Write(buffer,nRead);

COleDateTimeSpanelapsed
=COleDateTime::GetCurrentTime()-startTime;
doubledSecs=elapsed.GetTotalSeconds();
PostNotifyMessage(MAKEWPARAM(NOTIFY_MSG_LOW_WPARAM_DOWNSIZE,m_wFileID),dwCount);
if(dSecs>0.0)
{
dwCount
+=nRead;
m_transferRate
=(float)(dwCount/1024.0/dSecs);
TRACE(
"Read%dbytes(%0.1fKb/s)\n",dwCount,m_transferRate);
}
else
{
TRACE(
"Read%dbytes\n",dwCount);
m_transferRate
=(float)(dwCount/1024.0);
}
PostNotifyMessage(MAKEWPARAM(NOTIFY_MSG_LOW_WPARAM_DOWNSPEED,m_wFileID),(LPARAM)m_transferRate);
}
}
while(nRead>0);
bRet
=TRUE;
}
catch(CFileException*e)
{
TCHARszCause[MAX_PATH]
={0};
e
->GetErrorMessage(szCause,MAX_PATH);
TRACE(
"ErrorMsg:%s\n",szCause);
e
->Delete();
deletepFile;
pFtpConn
->Close();
deletepFtpConn;
m_TmpFile.Close();
returnFALSE;
}
pFile
->Close();
deletepFile;
m_TmpFile.Close();
pFtpConn
->Close();
deletepFtpConn;
}
returnbRet;
}

BOOLCDownloadFile::GetUNCFile()
{
BOOLbRet
=FALSE;
CFilem_TmpFile,m_SrcFile;
CFileExceptionfileException;
CStringstrOldLocation
=m_strFileURL;
if(!m_TmpFile.Open(m_strTmpFileName,
CFile::modeCreate
|CFile::modeNoTruncate|CFile::modeReadWrite
|CFile::shareDenyWrite|CFile::typeBinary,
&fileException))
{
TRACE(_T(
"OpenFilefailed:%d\n"),fileException.m_cause);
returnbRet;
}
strOldLocation.TrimLeft();
strOldLocation.TrimRight();
if(StrCmpNI(strOldLocation,_T("file:/"),6)==0)
{
strOldLocation
=strOldLocation.Mid(8);
strOldLocation.Replace(_T('
/'),_T('\\'));
}

if(!m_SrcFile.Open(strOldLocation,
CFile::modeRead
|CFile::shareDenyWrite|CFile::typeBinary,
&fileException))
{
TRACE(_T(
"OpenFilefailed:%d\n"),fileException.m_cause);
returnbRet;
}
COleDateTimestartTime
=COleDateTime::GetCurrentTime();
DWORDdwCount
=0;
try{
if(m_TmpFile.GetLength())
{
PostNotifyMessage(MAKEWPARAM(NOTIFY_MSG_LOW_WPARAM_CURRENTSIZE,m_wFileID),m_TmpFile.GetLength());
m_TmpFile.SeekToEnd();
m_SrcFile.Seek(m_TmpFile.GetLength(),CFile::begin);
}
BYTEbuffer[BUFFER_SIZE
+1]={0};
UINTnRead
=0;
PostNotifyMessage(MAKEWPARAM(NOTIFY_MSG_LOW_WPARAM_FULLSIZE,m_wFileID),m_SrcFile.GetLength());
TRACE(_T(
"TotoalLengthis%d,leftis%d\n"),m_SrcFile.GetLength(),m_SrcFile.GetLength()-m_TmpFile.GetLength());
dwCount
=0;
do
{
nRead
=m_SrcFile.Read(buffer,BUFFER_SIZE);
if(nRead>0)
{
buffer[nRead]
=0;
m_TmpFile.Write(buffer,nRead);

COleDateTimeSpanelapsed
=COleDateTime::GetCurrentTime()-startTime;
doubledSecs=elapsed.GetTotalSeconds();
PostNotifyMessage(MAKEWPARAM(NOTIFY_MSG_LOW_WPARAM_DOWNSIZE,m_wFileID),dwCount);
if(dSecs>0.0)
{
dwCount
+=nRead;
m_transferRate
=(float)(dwCount/1024.0/dSecs);
TRACE(
"Read%dbytes(%0.1fKb/s)\n",dwCount,m_transferRate);
}
else
{
TRACE(
"Read%dbytes\n",dwCount);
m_transferRate
=(float)(dwCount/1024.0);
}
PostNotifyMessage(MAKEWPARAM(NOTIFY_MSG_LOW_WPARAM_DOWNSPEED,m_wFileID),(LPARAM)m_transferRate);
}
}
while(nRead>0);
bRet
=TRUE;
}
catch(CFileException*e)
{
TCHARszCause[MAX_PATH]
={0};
e
->GetErrorMessage(szCause,MAX_PATH);
TRACE(
"ErrorMsg:%s\n",szCause);
e
->Delete();
m_TmpFile.Close();
returnFALSE;
}
m_TmpFile.Close();
returnbRet;
}

voidCDownloadFile::RegisterNotifyWindow(DWORDdwThreadID,HWNDhWnd,DWORDdwMsg)
{
m_dwThreadID
=dwThreadID;
m_hNotify
=hWnd;
m_dwMsgID
=dwMsg;
}

voidCDownloadFile::PostNotifyMessage(WPARAMwParam,LPARAMlParam)
{
if(m_hNotify)
{
::PostMessage(m_hNotify,m_dwMsgID,wParam,lParam);
}
if(m_dwThreadID)
{
::PostThreadMessage(m_dwThreadID,m_dwMsgID,wParam,lParam);
}
}

WORDCDownloadFile::GenFileID()
{
srand(GetTickCount());
returnrand()&0xFFFF;
}
UINTThreadDownSingleFile(LPVOIDpParam)
{
CDownloadFilem_DownFile;
UINTuRet
=0;
if(lpDownParam)
{
m_DownFile.m_wFileID
=m_DownFile.GenFileID();
//这里注册通知窗口和消息
//m_DownFile.RegisterNotifyWindow
if(m_DownFile.DownLoadFile(m_Msg.lpFileSrc,m_Msg.lpFileDst))
{
uRet
=1;
}
}
returnuRet;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值