实现HTTP协议Get、Post和文件上传功能——使用WinHttp接口实现

        在《使用WinHttp接口实现HTTP协议Get、Post和文件上传功能》一文中,我已经比较详细地讲解了如何使用WinHttp接口实现各种协议。在最近的代码梳理中,我觉得Post和文件上传模块可以得到简化,于是几乎重写了这两个功能的代码。因为Get、Post和文件上传功能的基础(父)类基本没有改动,函数调用的流程也基本没有变化,所以本文我将重点讲解修改点。(转载请指明出于breaksoftware的csdn博客)

        首先我修改了接口的字符集。之前我都是使用UNICODE作为接口参数类型,其中一个原因是Windows提倡UNICODE编码,其次是因为WinHttp接口只提供了UNICODE接口函数。而我在本次修改中,将字符集改成UTF8。因为在网络传输方便,UTF8格式才是主流。于是为了使用WinHttp接口,我提供了一个A版本的转换层——工程中WinhttpA.h。

        其次,我增强了Post接口。《使用WinHttp接口实现HTTP协议Get、Post和文件上传功能》的读者和我讨论了很多Post协议,让我感觉非常有必要重视起该功能。本文我们将着重讲解Post的实现和测试。

        再次,我将Post的实现和文件上传功能的实现合二为一。因为两者代码非常相似,其实在原理方面也是很相似的。

        最后,我使用前一篇博文中介绍的IMemFileOperation接口,重新定义了Post和文件上传功能的参数定义。因为IMemFileOperation的特性,我们可以上传文件,或者上传一片内存值,或者上传文件中的内容,而这些操作是相同的。

        Get请求没什么好说的了,我们主要关注Post和文件上传。

        一般情况下,我们遇到的是“我们需要向http://www.xxx.com:8080/yyyy/zzz地址Post数据”。其中的“数据”是我们问题的重点。可能很多人认为Post请求就是将所有参数都Post到服务器,其实不然。打个比方,比如我们要求对http://www.xxxx.com/post?a=b&c=d地址Post一个数据e=f,我们并不是将"a=b&c=d&e=f"Post到服务器,而只是"e=f"Post过去,"a=b&c=d"还是按Get的方式发送。于是我对上一版的设计做了改良,废掉了ParseParams函数,简化了设计,但是要求用户传进来的URL中不包含需要Post过去的数据——需要Post的数据通过SetPostParam方法传递进来。我们想把重点发到这种发送分离的实现上:

	if ( !WinHttpCrackUrlA_( m_strUrl, strHost, strPath, strExt, nPort ) ) {
		break;
	}

	m_hSession = WinHttpOpenA( m_strAgent.empty() ? NULL : m_strAgent.c_str(), WINHTTP_ACCESS_TYPE_DEFAULT_PROXY, WINHTTP_NO_PROXY_NAME, WINHTTP_NO_PROXY_BYPASS, 0 ); 
	if ( NULL == m_hSession ) {
		break;
	}

	if ( FALSE == WinHttpSetTimeouts(m_hSession, m_nResolveTimeout, m_nConnectTimeout, m_nSendTimeout, m_nSendTimeout) ) {
		break;
	}

	m_hConnect = WinHttpConnectA( m_hSession, strHost.c_str(), nPort, 0 );
	if ( NULL == m_hConnect ) {
		break;
	}

	m_strRequestData = strPath + strExt;

        主要关注最后一行,我将URL路径和URL参数放到m_strRequestData里。之后

VOID CHttpRequestByWinHttp::TransmiteDataToServerByPost()
{
	BOOL bSuc = FALSE;
	do {
		m_hRequest = WinHttpOpenRequestA(m_hConnect, "Post",
				m_strRequestData.c_str(), NULL, WINHTTP_NO_REFERER, WINHTTP_DEFAULT_ACCEPT_TYPES, 0);
		if ( NULL == m_hRequest ) {
			break;
		}

        这样,我们便将不需要Post的数据发送了过去。

        现在我们再探讨下需要Post过去的数据。首先我们需要明确下数据的来源:

  • 内存中的数据
  • 文件中的数据

        不管数据来源于何处,它都可以成为待Post过去的数据或者待上传的文件的内容。于是我们借用上一篇博文中的IMemFileOperation接口,定义Post的数据的格式。

typedef struct _FMParam_ {
    std::string strkey;
    ToolsInterface::LPIMemFileOperation value;
    bool postasfile;
    struct FileInfo {
        char szfilename[128];
    };

    struct MemInfo{
        bool bMulti;
    };

    union {
        FileInfo fileinfo;
        MemInfo meminfo;
    };
}FMParam, *PFMParam;

typedef std::vector<FMParam> FMParams;
typedef FMParams::iterator FMParamsIter;
typedef FMParams::const_iterator FMParamsCIter;

        不管是Post数据还是要上传文件,协议中都需要key的存在。strkey是数据的key。value字段只是一个指针,它是指向一个文件还是内存。已经没有关系了,因为之后我们将使用统一的接口去访问它。postasfile字段是标志该参数是否以文件内容的形式Post上去。这儿需要特别说明下,postasfile和value是内存还是文件是没有关系的。因为value只是指向了数据内容,至于内容上传到服务器是作为文件的内容还是只是普通Post的数据值是由postasfile决定的。如果postasfile为真,则FileInfo将被利用到。因为它标记了内容上传到服务器后,服务器上保存的文件名。如果postasfile为假,则我们需要考虑下数据是作为普通数据post,还是作为MultiPart数据Post。这个就取决于MemInfo中的字段了。至于什么是MultiPart类型,可以简单参考《使用WinHttp接口实现HTTP协议Get、Post和文件上传功能》后半部分关于文件上传的讨论。

        对于待上传的数据,之前设计改框架时,框架提供了GetData方法,让继承类提供数据。因为数据存在延续性,所以导致继承类的书写很麻烦——需要记录已经上传了哪些数据。这个版本我将这个设计做了修改,基类暴露一个发送方法,让继承类在需要的时候调用基类的方法,从而不需要基类记录过程的状态。于是以前一大坨代码被简化到如下几行:

DWORD dwUserDataLength = GetUserDataSize();
if ( FALSE == WinHttpSendRequest( m_hRequest, WINHTTP_NO_ADDITIONAL_HEADERS, 0, WINHTTP_NO_REQUEST_DATA, 0, dwUserDataLength, 0)) {
	break;
}

DWORD dwSendUserDataLength = SendUserData();
bSuc = (dwUserDataLength == dwSendUserDataLength) ? TRUE : FALSE;

        通过GetUserDataSize我们将获得待Post过去的数据的大小。然后调用SendUserData发送数据,返回发送了的数据的大小。通过对比两者大小得知是否整个操作是否成功。

       现在我们再看下发送数据的具体实现,首先我们看下一些固定要写死的字段的申明

#define BOUNDARYPART "--MULTI-PARTS-FORM-DATA-BOUNDARY"

#define PARTRETURN  "\r\n"
#define PARTDISPFD  "Content-Disposition:form-data;"
#define PARTNAME    "name"
#define PARTEQUATE  "="
#define PARTQUOTES  "\""
#define PARTSPLIT   "&"
#define PARTSEMICOLON   ";"
#define PARTFILENAME    "filename"
#define PARTTYPEOCT "Content-Type:application/octet-stream"

        读过《使用WinHttp接口实现HTTP协议Get、Post和文件上传功能》的朋友应该记得其中有很多繁杂的数据格式化。之前我们讲过,我们需要先获得待Post的数据大小,再发送数据。这意味着繁杂的数据格式化需要做两次。如果以后需要对其中某个发送数据格式化做修改,那么相应的计算数据长度的方法也要做修改。这是非常不利于维护的。于是,我将两者合为一个函数,通过参数判断是需要计算还是需要发送。这样以后修改发送数据时,只要修改一处,降低了维护的成本和难度。

    DWORD CHttpTransByPost::SendUserData() {
        return SendOrCalcData();
    }

    DWORD CHttpTransByPost::GetUserDataSize() {
        return SendOrCalcData(FALSE);
    }

    DWORD CHttpTransByPost::SendOrCalcData( BOOL bSend /*= TRUE*/ ) {
        DWORD dwsize = 0;
        for (FMParamsCIter it = m_PostParam.begin(); it != m_PostParam.end(); it++) {
            dwsize += SendData(*it, bSend);
        }
        if (!m_strBlockEnd.empty()) {
            dwsize += DataToServer(m_strBlockEnd.c_str(), m_strBlockEnd.length(), bSend);
        }   
        return dwsize;
    }

        在SendOrCalcData的最后,我们判断m_strBlockEnd是否为空,如果不为空,则我们将BlockEnd格式化数据发送过去,告诉服务器MultiPart数据发送结束。如果为空,则代表此次发送数据不需要按MultiPart形式发送。至于是否需要MultiPart,以及其各种格式化则是在下面的代码中判断

BOOL CHttpTransByPost::ModifyRequestHeader( HINTERNET hRequest ) {
        bool bMulti = false;
        for (FMParamsCIter it = m_PostParam.begin(); it != m_PostParam.end(); it++) {
            if (it->postasfile) {
                bMulti = true;
                break;
            }
            else {
                bMulti = it->meminfo.bMulti;
                if (bMulti) {
                    break;
                }
            }
        }

		if (bMulti) {
			m_strBlockStart = "--";
			m_strBlockStart += BOUNDARYPART;
			m_strBlockStart += "\r\n";

			m_strBlockEnd =  "\r\n--";
			m_strBlockEnd += BOUNDARYPART;
			m_strBlockEnd +=  "--\r\n";

			m_strNewHeader = "Content-Type: multipart/form-data; boundary=";
			m_strNewHeader += BOUNDARYPART;
			m_strNewHeader += "\r\n";
		}
		else {
			m_strNewHeader = "Content-Type:application/x-www-form-urlencoded";
			m_strNewHeader += "\r\n";
		}

		::WinHttpAddRequestHeadersA(hRequest, m_strNewHeader.c_str(), 
			m_strNewHeader.length(), WINHTTP_ADDREQ_FLAG_ADD | WINHTTP_ADDREQ_FLAG_REPLACE) ;
		return AddUserRequestHeader(hRequest);
	}

        最后我们将注意力集中到发送(计算)数据的函数SendData上。

    DWORD CHttpTransByPost::SendData(const FMParam& postparam, BOOL bSend /*= TRUE*/ ) {
        DWORD dwsize = 0;
        postparam.value->MFSeek(0, SEEK_SET);
        if (postparam.postasfile) {
            dwsize = SendFileData(postparam, bSend);
        }
        else {
            dwsize = SendMemData(postparam, bSend);
        }
        return dwsize;
    }

        首先,我们使用MFSeek将文件(内存)的指针置到起始处。然后再通过postasfile决定是按文件的形式发送还是按内存的形式发送。

    DWORD CHttpTransByPost::SendFileData(const FMParam& postparam, BOOL bSend) {
        DWORD dwsize = 0;
        dwsize += DataToServer(PARTRETURN, strlen(PARTRETURN), bSend);
        dwsize += DataToServer(m_strBlockStart.c_str(), m_strBlockStart.length(), bSend);
        dwsize += DataToServer(PARTDISPFD, strlen(PARTDISPFD), bSend);
        dwsize += DataToServer(PARTNAME, strlen(PARTNAME), bSend);
        dwsize += DataToServer(PARTEQUATE, strlen(PARTEQUATE), bSend);
        dwsize += DataToServer(PARTQUOTES, strlen(PARTQUOTES), bSend);
        dwsize += DataToServer(postparam.strkey.c_str(), postparam.strkey.length(), bSend);
        dwsize += DataToServer(PARTQUOTES, strlen(PARTQUOTES), bSend);
        dwsize += DataToServer(PARTSEMICOLON, strlen(PARTSEMICOLON), bSend);
        dwsize += DataToServer(PARTFILENAME, strlen(PARTFILENAME), bSend);
        dwsize += DataToServer(PARTEQUATE, strlen(PARTEQUATE), bSend);
        dwsize += DataToServer(PARTQUOTES, strlen(PARTQUOTES), bSend);
        dwsize += DataToServer(postparam.fileinfo.szfilename, strlen(postparam.fileinfo.szfilename), bSend);
        dwsize += DataToServer(PARTQUOTES, strlen(PARTQUOTES), bSend);
        dwsize += DataToServer(PARTRETURN, strlen(PARTRETURN), bSend);
        dwsize += DataToServer(PARTTYPEOCT, strlen(PARTTYPEOCT), bSend);
        dwsize += DataToServer(PARTRETURN, strlen(PARTRETURN), bSend);
        dwsize += DataToServer(PARTRETURN, strlen(PARTRETURN), bSend);
        while(!postparam.value->MFEof()) {
            char buffer[1024] = {0};
            size_t size = postparam.value->MFRead(buffer, 1, 1024);
            dwsize += DataToServer(buffer, size, bSend);
        }
        return dwsize;
    }

        以文件内容形式发送的代码如上。我们关注下最后几行,MFRead读取内容,然后发送(计算)数据。

    DWORD CHttpTransByPost::SendMemData(const FMParam& postparam, BOOL bSend) {
        DWORD dwsize = 0;
        if (postparam.meminfo.bMulti) {
            dwsize += DataToServer(PARTRETURN, strlen(PARTRETURN), bSend);
            dwsize += DataToServer(m_strBlockStart.c_str(), m_strBlockStart.length(), bSend);
            dwsize += DataToServer(PARTDISPFD, strlen(PARTDISPFD), bSend);
            dwsize += DataToServer(PARTNAME, strlen(PARTNAME), bSend);
            dwsize += DataToServer(PARTEQUATE, strlen(PARTEQUATE), bSend);
            dwsize += DataToServer(PARTQUOTES, strlen(PARTQUOTES), bSend);
            dwsize += DataToServer(postparam.strkey.c_str(), postparam.strkey.length(), bSend);
            dwsize += DataToServer(PARTQUOTES, strlen(PARTQUOTES), bSend);
            dwsize += DataToServer(PARTRETURN, strlen(PARTRETURN), bSend);
	        while(!postparam.value->MFEof()) {
		        char buffer[1024] = {0};
		        size_t size = postparam.value->MFRead(buffer, 1, 1024);
                dwsize += DataToServer(buffer, size, bSend);
	        }
        }
        else {
            dwsize += DataToServer(PARTSPLIT, strlen(PARTSPLIT), bSend);
            dwsize += DataToServer(postparam.strkey.c_str(), postparam.strkey.length(), bSend);
            dwsize += DataToServer(PARTEQUATE, strlen(PARTEQUATE), bSend);
            while(!postparam.value->MFEof()) {
                char buffer[1024] = {0};
                size_t size = postparam.value->MFRead(buffer, 1, 1024);
                dwsize += DataToServer(buffer, size, bSend);
            }
        }

        return dwsize;
    }

        以上是发送普通Post数据的方法。其中分为是否需要以MultiiPart形式发送,还是以普通形式发送。MultiPart形式之前已经说过,而普通Post数据形式则是无约束的。我将该数据时拼装成Name1=Value1&Name2=Value2的形式发送的。

        对于MultiParg类型的Post,我们使用WireShark截取发送包

        发送普通Post数据的WireShark截包为

        最后我们看下使用的例子

    HttpRequestFM::CHttpTransByPost* p = new HttpRequestFM::CHttpTransByPost();
    ToolsInterface::IMemFileOperation* pMemOp = new MemFileOperation::CMemOperation();
	
    p->SetOperation(pMemOp);
    p->SetProcessCallBack(ProcssCallback);
    p->SetUrl(BIGFILEURL);
    
    FMParams params;

    FMParam param1;
    param1.postasfile = false;
    param1.strkey = "key1";
    param1.meminfo.bMulti = false;
    MemFileOperation::CMemOperation mem1("value1", strlen("value1"));
    param1.value = &mem1;
    params.push_back(param1);

    FMParam param2;
    param2.postasfile = false;
    param2.strkey = "key2";
    param2.meminfo.bMulti = true;
    //sprintf_s(param2.fileinfo.szfilename, sizeof(param2.fileinfo.szfilename), "2.bin");
    MemFileOperation::CFileOperation file2("F:/2.bin");
    param2.value = &file2;
    params.push_back(param2);
    
    FMParam param3;
    param3.strkey = "key3";
    //param3.meminfo.bMulti = true;
    param3.postasfile = true;
    sprintf_s(param3.fileinfo.szfilename, sizeof(param3.fileinfo.szfilename), "3.bin");
    MemFileOperation::CFileOperation file3("F:/1.bin");
    param3.value = &file3;
    params.push_back(param3);

    p->SetPostParam(params);
    p->Start();

        param1是以普通Post数据格式传输的参数;param2的value是从F:/2.bin文件中读取的,但是其只是MultiPart形式上传的数据,而非上传文件。param3是要求上传文件F:/1.bin文件到服务器上为3.bin。

        通过不同的组合,我们可以同时上传多个文件。比如我们将上例中的param2做稍微的修改,即可以将其对应的文件上传至服务器,实现同时上传多个文件的功能。

        工程源码链接:http://pan.baidu.com/s/1i3eUnMt 密码:hfro

  • 6
    点赞
  • 14
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 19
    评论
WinHTTP提供以下功能WinHttpAddRequestHeaders 向HTTP请求句柄添加一个或多个HTTP请求标头。 WinHttpCheckPlatform 确定WinHTTP是否支持当前平台。 WinHttpCloseHandle 关闭单个 HINTERNET句柄。 WinHttpConnect 指定HTTP请求的初始目标服务器。 WinHttpCrackUrl 将URL分为其组成部分,例如主机名和路径。 WinHttpCreateProxyResolver 创建WinHttpGetProxyForUrlEx使用的句柄。 WinHttpCreateUrl 从组件部分创建URL,例如主机名和路径。 WinHttpDetectAutoProxyConfigUrl 查找代理自动配置(PAC)文件的URL。此功能报告PAC文件的URL,但不下载该文件。 WinHttpFreeProxyResult 释放从以前的调用WinHttpGetProxyResult检索的数据。 WinHttpGetDefaultProxyConfiguration 从注册表中检索默认的WinHTTP代理配置。 WinHTTPGetIEProxyConfigForCurrentUser 获取当前用户的Internet Explorer(IE)代理配置。 WinHttpGetProxyForUrl 检索指定URL的代理信息。 WinHttpGetProxyForUrlEx 检索指定URL的代理信息。 WinHttpGetProxyResult 检索到调用的结果WinHttpGetProxyForUrlEx。 WinHttpOpen 初始化应用程序对WinHTTP功能使用WinHttpOpenRequest 创建HTTP请求句柄。 WinHttpQueryAuthSchemes 返回服务器支持的授权方案。 WinHttpQueryDataAvailable 返回可立即与读取数据的字节数 WinHttpReadData。 WinHttpQueryHeaders 检索与HTTP请求相关联的头信息。 WinHttpQueryOption 在指定的句柄上查询Internet选项。 WinHttpReadData 从WinHttpOpenRequest函数打开的句柄中读取数据 。 WinHttpReceiveResponse 结束由WinHttpSendRequest启动的HTTP请求 。 WinHttpResetAutoProxy 重置自动代理。 WinHttpSendRequest 将指定的请求发送到HTTP服务器。 WinHttpSetCredentials 将所需的授权凭证传递给服务器。 WinHttpSetDefaultProxyConfiguration 在注册表中设置默认的WinHTTP代理配置。 WinHttpSetOption 设置Internet选项。 WinHttpSetStatusCallback 设置WinHTTP可以在操作过程中进行调用的回调函数。 WinHttpSetTimeouts 设置涉及HTTP事务的各种超时。 WinHttpTimeFromSystemTime 根据HTTP版本1.0规范格式化日期和时间。 WinHttpTimeToSystemTime 获取HTTP时间/日期字符串并将其转换为 SYSTEMTIME结构。 WinHttpWriteData 将请求数据写入HTTP服务器。 WinHttpWebSocketClose 关闭WebSocket连接。 WinHttpWebSocketCompleteUpgrade 完成由WinHttpSendRequest启动的WebSocket握手。 WinHttpWebSocketQueryCloseStatus 获取服务器发送的关闭状态。 WinHttpWebSocketReceive 从WebSocket连接接收数据。 WinHttpWebSocketSend 通过WebSocket连接发送数据。 WinHttpWebSocketShutdown 向WebSocket连接发送关闭框架
可以使用WinHTTP API来实现通过HTTPPOST方式上传文件并获取返回值。以下是一个简单的示例代码: ```c++ #include <Windows.h> #include <WinHttp.h> #include <iostream> #pragma comment(lib, "winhttp.lib") int main() { // 创建WinHTTP会话句柄 HINTERNET hSession = WinHttpOpen(L"WinHTTP Example/1.0", WINHTTP_ACCESS_TYPE_DEFAULT_PROXY, WINHTTP_NO_PROXY_NAME, WINHTTP_NO_PROXY_BYPASS, 0); if (!hSession) { std::cerr << "Failed to create WinHTTP session.\n"; return 1; } // 创建WinHTTP连接句柄 HINTERNET hConnect = WinHttpConnect(hSession, L"example.com", INTERNET_DEFAULT_HTTP_PORT, 0); if (!hConnect) { std::cerr << "Failed to create WinHTTP connection.\n"; WinHttpCloseHandle(hSession); return 1; } // 创建WinHTTP请求句柄 HINTERNET hRequest = WinHttpOpenRequest(hConnect, L"POST", L"/upload", NULL, WINHTTP_NO_REFERER, NULL, 0); if (!hRequest) { std::cerr << "Failed to create WinHTTP request.\n"; WinHttpCloseHandle(hConnect); WinHttpCloseHandle(hSession); return 1; } // 设置请求头 LPCWSTR szHeaders = L"Content-Type: multipart/form-data; boundary=----WebKitFormBoundary7MA4YWxkTrZu0gW"; if (!WinHttpAddRequestHeaders(hRequest, szHeaders, wcslen(szHeaders), WINHTTP_ADDREQ_FLAG_REPLACE)) { std::cerr << "Failed to set request headers.\n"; WinHttpCloseHandle(hRequest); WinHttpCloseHandle(hConnect); WinHttpCloseHandle(hSession); return 1; } // 读取要上传的文件 std::ifstream file("example.txt", std::ios::binary); if (!file) { std::cerr << "Failed to open file.\n"; WinHttpCloseHandle(hRequest); WinHttpCloseHandle(hConnect); WinHttpCloseHandle(hSession); return 1; } // 获取文件长度 file.seekg(0, std::ios::end); int fileSize = file.tellg(); file.seekg(0, std::ios::beg); // 构建POST数据 const char* szBoundary = "----WebKitFormBoundary7MA4YWxkTrZu0gW"; std::vector<char> postData; postData.reserve(fileSize + 1024); postData.insert(postData.end(), szBoundary, szBoundary + strlen(szBoundary)); postData.insert(postData.end(), "\r\nContent-Disposition: form-data; name=\"file\"; filename=\"example.txt\"\r\nContent-Type: application/octet-stream\r\n\r\n", "\r\nContent-Disposition: form-data; name=\"file\"; filename=\"example.txt\"\r\nContent-Type: application/octet-stream\r\n\r\n" + 1024); std::copy(std::istreambuf_iterator<char>(file), std::istreambuf_iterator<char>(), std::back_inserter(postData)); postData.insert(postData.end(), "\r\n", "\r\n" + 2); postData.insert(postData.end(), szBoundary, szBoundary + strlen(szBoundary)); postData.insert(postData.end(), "--\r\n", "--\r\n" + 4); // 发送请求 if (!WinHttpSendRequest(hRequest, WINHTTP_NO_ADDITIONAL_HEADERS, 0, &postData[0], postData.size(), postData.size(), 0)) { std::cerr << "Failed to send WinHTTP request.\n"; WinHttpCloseHandle(hRequest); WinHttpCloseHandle(hConnect); WinHttpCloseHandle(hSession); return 1; } // 接收响应 if (!WinHttpReceiveResponse(hRequest, NULL)) { std::cerr << "Failed to receive WinHTTP response.\n"; WinHttpCloseHandle(hRequest); WinHttpCloseHandle(hConnect); WinHttpCloseHandle(hSession); return 1; } // 读取响应数据 std::vector<char> responseData(1024); DWORD dwBytesRead = 0; while (WinHttpReadData(hRequest, &responseData[0], responseData.size(), &dwBytesRead)) { std::cout.write(&responseData[0], dwBytesRead); } // 关闭请求句柄、连接句柄和会话句柄 WinHttpCloseHandle(hRequest); WinHttpCloseHandle(hConnect); WinHttpCloseHandle(hSession); return 0; } ``` 上述代码以POST方式上传名为`example.txt`的文件到`example.com`的`/upload`路径,并输出响应数据。请根据实际情况修改代码中的URL、请求头、POST数据和文件名等参数。
评论 19
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

breaksoftware

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值