达到HTTP合约Get、Post和文件上传功能——采用WinHttp介面
于《采用WinHttp实现HTTP协议Get、Post和文件上传功能》一文中,我已经比較具体地解说了怎样使用WinHttp接口实现各种协议。
在近期的代码梳理中,我认为Post和文件上传模块能够得到简化,于是差点儿重写了这两个功能的代码。由于Get、Post和文件上传功能的基础(父)类基本没有修改,函数调用的流程也基本没有变化,所以本文我将重点解说修改点。
首先我改动了接口的字符集。之前我都是使用UNICODE作为接口參数类型,当中一个原因是Windows提倡UNICODE编码,其次是由于WinHttp接口仅仅提供了UNICODE接口函数。而我在本次改动中,将字符集改成UTF8。由于在网络传输方便,UTF8格式才是主流。
于是为了使用WinHttp接口,我提供了一个A版本号的转换层——project中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到server,事实上不然。打个比方。比方我们要求对http://www.xxxx.com/post?
a=b&c=d地址Post一个数据e=f,我们并非将"a=b&c=d&e=f"Post到server,而仅仅是"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仅仅是指向了数据内容,至于内容上传到server是作为文件的内容还是仅仅是普通Post的数据值是由postasfile决定的。假设postasfile为真。则FileInfo将被利用到。由于它标记了内容上传到server后,server上保存的文件名称。
假设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格式化数据发送过去,告诉serverMultiPart数据发送结束。假设为空,则代表此次发送数据不须要按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截包为
watermark/2/text/aHR0cDovL2Jsb2cuY3Nkbi5uZXQvYnJlYWtzb2Z0d2FyZQ==/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70/gravity/Center" align="middle" alt="">
最后我们看下使用的样例
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文件到server上为3.bin。
通过不同的组合。我们能够同一时候上传多个文件。比方我们将上例中的param2做略微变化,那就是将文件上传到其各自的能力server。上传多个文件在同一时间实现功能。
版权声明:本文博主原创文章,博客,未经同意不得转载。
达到HTTP合约Get、Post和文件上传功能——采用WinHttp介面的更多相关文章
- iOS 的 Safari 文件上传功能详解
iOS 6 给 Safari 浏览器带来的另外一个功能是文件上传,终于 Safari 终于支持 input 输入框的文件类型了,并且还支持 HTML媒体捕获(HTML Media Capture). ...
- [php基础]PHP.INI配置:文件上传功能配置教程
昨天分享了在PHP网站开发中如何在php.ini中配置实现session功能的PHP教程,今天继续分享在利用PHP实现文件上传功能时几点关键php.ini的配置. 说到在php.ini中的文件上传的配 ...
- 学习ASP.NET Core Razor 编程系列十五——文件上传功能(三)
学习ASP.NET Core Razor 编程系列目录 学习ASP.NET Core Razor 编程系列一 学习ASP.NET Core Razor 编程系列二——添加一个实体 学习ASP.NET ...
- 学习ASP.NET Core Razor 编程系列十四——文件上传功能(二)
学习ASP.NET Core Razor 编程系列目录 学习ASP.NET Core Razor 编程系列一 学习ASP.NET Core Razor 编程系列二——添加一个实体 学习ASP.NET ...
- django之 文件上传功能(缺陷:无法改存放目录)
百度云盘:django之 文件上传功能(缺陷:无法改存放目录)
- 文件/大文件上传功能实现(JS+PHP)全过程
文件/大文件上传功能实现(JS+PHP) 参考博文:掘金-橙红年代 前端大文件上传 路漫漫 其修远 PHP + JS 实现大文件分割上传 本文是学习文件上传后的学习总结文章,从无到有实现文件上传功能, ...
- 使用element的upload组件实现一个完整的文件上传功能(上)
说到标题就有点心塞了,前段时间项目上需要实现一个文件上传的功能,然后就咔咔的去用了element的upload组件,不用不知道一用吓一跳哇. 在使用的过程中遇到了很多让意想不到的问题,后来也因为时间问 ...
- 使用element的upload组件实现一个完整的文件上传功能(下)
本篇文章是<使用element的upload组件实现一个完整的文件上传功能(上)>的续篇. 话不多说,接着上一篇直接开始 一.功能完善—保存表格中每一列的文件列表状态 1.思路 保存表格中 ...
- SpringBoot实现文件上传功能详解
目录 利用SpirngBoot实现文件上传功能 零.本篇要点 一.SpringBoot对文件处理相关自动配置 二.处理上传文件MultipartFile接口 三.SpringBoot+Thymelea ...
随机推荐
- JAVA 计算地球上任意两点(经纬度)距离
/** * 计算地球上任意两点(经纬度)距离 * * @param long1 * 第一点经度 * @param lat1 * 第一点纬度 * @param long2 * 第二点经度 * @para ...
- Cordova CLI源码分析(三)——初始化
本部分主要涉及以下三个文件 1 cli.js 2 cordova.js 3 events.js 通过前一篇package.json的分析,可以知道,当命令行执行cordova相关命令时,首先调用mai ...
- vb.net WPF webbrowser window.close 关闭后不触发 WindowClosing 事件 WNDPROC解决方式
vb.net WPF webbrowser window.close 关闭后不触发 WindowClosing 事件 WNDPROC解决方式 #Region "WPF 当浏览器窗体关闭 ...
- NPOI 创建Excel,数据读取与写入
<1> using System; using System.Collections.Generic; using System.Linq; using System.Web; using ...
- 【翻译】Why JavaScript Is and Will Continue to Be the First Choice of Programmers
花费2半小时,那么最终会被翻译.假设有问题,请提出,毕竟,自己的6不超过级别. 附加链接 Why JavaScript Is and Will Continue to Be the First Cho ...
- SAP ABAP计划 SY-REPID与SY-CPROG差异
首先,它的两个解释 sy-repid is the name of the current program. "当前程序的程序名 ...
- PHP --字符串编码转换(自动识别原编码)
/** * 对数据进行编码转换 * @param array/string $data 数组 * @param string $output 转换后的编码 */ function array_icon ...
- Cnblogs API
http://wcf.open.cnblogs.com/news/help http://wcf.open.cnblogs.com/blog/help/ 不支持用户登陆
- Cocos2d-x学习笔记(五岁以下儿童) 精灵两种方式播放动画
这几天在看控件类,临时没有想好实际运用的方向.单纯的创建网上已经有非常多这方面的样例,我就不写了.接下来是学习精灵类.精灵类若是单独学习也是非常easy.于是我加了一些有关动画方面的知识点与精灵 ...
- Android 混淆proguard的实现(图文)
1. 在Eclipse中的project编译执行后,在文件夹bin以下有生成一些文件,当中classes.dex是未经过混淆生成的.而我们要混淆的话,就要又一次生成一个混淆过的classes.dex ...