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

article/2025/10/29 9:13:57

        在《使用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


http://chatgpt.dhexx.cn/article/Y1Yt71JE.shtml

相关文章

使用WinINet和WinHTTP实现Http访问

Http访问有两种方式&#xff0c;GET和POST&#xff0c;就编程来说GET方式相对简单点&#xff0c;它不用向服务器提交数据&#xff0c;在这个例程中我使用POST方式&#xff0c;提交数据value1与value2&#xff0c;并从服务器得到他们的和&#xff08;value1 value2&#xff09;…

WinHttp.WinHttpRequest.5.1

Set oHttp CreateObject ( " WinHttp.WinHttpRequest.5.1 " )oHttp.Option( 6 ) 0 禁止自动Redirect&#xff0c;最关键的 oHttp.SetTimeouts 9999999 , 9999999 , 9999999 , 9999999 设置超时&#xff5e;和ServerXMLHTTP组件一样 oHttp.Open " GET &q…

Tinyhttpd for Windows

TinyHTTPd forWindows 前言 TinyHTTPd是一个开源的简易学习型的HTTP服务器&#xff0c;项目主页在&#xff1a;http://tinyhttpd.sourceforge.net/&#xff0c;源代码下载&#xff1a;https://sourceforge.net/projects/tinyhttpd/&#xff0c;因为是学习型的代码,已经有好多年…

windows安装http测试服务

祝您身体健康&#xff0c;前程似锦&#xff0c;小弟期待文章对您有帮助&#xff0c;也期待您的打赏: 目录 1. 下载 HttpMockServer工具 2. windows10 安装jdk1.8 3. jdk按照上面配置完环境变量后打开cmd&#xff0c;运行 4. 准备测试的响应数据 text.txt,内容如下: 5. 会弹…

C++ winhttp 实现文件下载器

本篇内容讲述 C winHttp 实现下载器的简单 demo&#xff0c;使用了 WinHttpOpen、WinHttpConnect、WinHttpOpenRequest、WinHttpSendRequest、WinHttpReceiveResponse、WinHttpQueryDataAvailable、WinHttpReadData、WinHttpCloseHandle 等函数。 版权声明&#xff1a;本文为C…

WinHTTP教程

最近有些忙&#xff0c;也没更新BLOG&#xff0c;这几天在捣鼓一个小玩意要用到WinHTTP API&#xff0c;发现资料很少&#xff0c;而且大都是些MFC封装的例子&#xff0c;看得我是一个头几个大。就把自己关于WinHTTP的学习总结了一下&#xff0c;仅供参考&#xff0c;各人理解可…

WinHttp c++ 介绍及应用

一、HTTP协议介绍 http协议的底层协议是TCP协议。TCP协议是基于数据流的传输方式。其又叫做“超文本传输协议”&#xff0c;为什么呢&#xff0c;因为它是将超文本标记语言(HTML)文档从Web服务器传送到客户端的浏览器&#xff0c;通过因特网传送万维网文档的数据传送协议。 1…

WinHTTP

记录WinHTTP学习过程 一、什么是WinHTTP&#xff1f; WinHTTP的全称是Microsoft Windows HTTP Services&#xff0c; 它提供给开发者一个HTTP客户端应用程序接口(API)&#xff0c;通过这种API借助HTTP协议给其他的HTTP服务器发送请求。 二、WinHTTP访问流程 如上图&#xff0c;…

Java递归算法计算5的阶乘

递归 package com.etime.test019; //计算5的阶乘&#xff1b; public class Test15 {public static void main(String[] args) {//调用test1方法&#xff0c;且只调用一次int i test1(5);System.out.println(i);}//定义一个int类型返回值的静态方法public static int test1(i…

Java算法递归与递推

Java算法----递归与递推 递推实现递推思想递归实现递归思想递归实现递推思想递推实现递归思想四种方法的特点思维拓展 问题&#xff1a;给你一个整数n&#xff0c;如果n是奇数&#xff0c;就进行运算nn*31&#xff0c;如果n是偶数&#xff0c;就进行运算nn/2&#xff0c;直到n等…

Java开发 | 数据结构和算法之——递归算法

著名的Pascal之父——Nicklaus Wirth(沃斯)让他获得图灵奖的一句话就是他提出的著名公式:“程序=数据结构+算法”,这个公式对计算机科学的影响类似于爱因斯坦的质能方程在物理界的影响。 因此可以看出来数据结构和算法在我们开发程序中有多么的重要了,下面我们来简单认识…

java中递归算法的理解

Coding多了&#xff0c;递归算法是非常常见的&#xff0c;最近我一直在做树形结构的封装&#xff0c;所以更加的离不开递归算法。所以今天就简单说一下这个递归算法&#xff0c;用java实现一个非常经典的递归实例。 递归算法&#xff0c;其实说白了&#xff0c;就是程序的自身调…

【递归】java递归算法及替代方法

文章目录 菜单树递归&#xff08;树根往子节点递归&#xff09;需求&#xff1a; 取所有level小于2的节点 ( 返回结果为普通list格式) 为list格式的数据设置children&#xff08;非递归&#xff09;需求&#xff1a; 数据库查出来的原始list 如果有children就设置 使用循环代替…

Java递归

1.递归的概念&#xff1a; 一个方法在执行过程中调用自身, 就称为 "递归". 示例&#xff1a; //递归求n的阶乘 public static void main(String[] args) {int n 5;int ret factor(n);System.out.println("ret " ret); }public static int factor(int…

java递归算法(一)——详解以及几个经典示例

什么是递归 递归就是一个程序或函数在其中定义或说明有之间或者间接调用自身的一种方法&#xff0c;它通常把一个大型复杂的问题层层转化为一个原问题相似的规模较小的问题来求解&#xff0c;递归策略只需要少量的程序就可以描述出解题过程所需要的多次重复计算&#xff0c;大…

Java 递归算法

一、概述&#xff1a; Java递归&#xff1a;简单说就是函数自身直接或间接调用函数的本身。 二、应用场景&#xff1a; 若&#xff1a;一个功能在被重复使用&#xff0c;并每次使用时&#xff0c;参与运算的结果和上一次调用有关&#xff0c;这时就可以使用递归来解决这个问题…

递归与递归算法实例(java实现)

一、递归介绍 递归算法&#xff08;英语&#xff1a;recursion algorithm&#xff09;在计算机科学中是指一种通过重复将问题分解为同类的子问题而解决问题的方法。绝大 多数编程语言支持函数的自调用&#xff0c;在这些语言中函数可以通过调用自身来进行递归。 定义&#xff1…

Java的递归算法

递归算法设计的基本思想是&#xff1a;对于一个复杂的问题&#xff0c;把原问题分解为若干个相对简单类同的子问题&#xff0c;继续下去直到子问题简单到能够直接求解&#xff0c;也就是说到了递推的出口&#xff0c;这样原问题就有递推得解。 关键要抓住的是&#xff1a; &…

java递归算法实现

Coding多了&#xff0c;递归算法是非常常见的&#xff0c;最近我一直在做树形结构的封装&#xff0c;所以更加的离不开递归算法。所以今天就简单说一下这个递归算法&#xff0c;用java实现一个非常经典的递归实例。 递归算法&#xff0c;其实说白了&#xff0c;就是程序的自身调…

【Java】递归算法

文章目录 什么是递归&#xff1f;递归求阶乘递归求解斐波那契数列猴子吃桃问题 什么是递归&#xff1f; 程序 调用自身 的编程技巧成为 递归&#xff08;recursion&#xff09;。 递归算法是一种直接或间接调用、定义自身的函数或方法的算法&#xff0c;也就是调用自身。 递归…