☰
MFC文件上传实战:用WinINet构造multipart/form-data并稳定传输大文件
2026/10/14 1:35:03 网站建设 项目流程

前阵子有朋友在群里问,MFC程序要给服务器传文件,照着网上搜的代码抄下来,要么报错要么界面卡死。我一听大概就猜到原因了:多半是直接拿CString拼URL当POST发,要么压根没按multipart/form-data组织请求体。这个需求表面看就是一个HTTP POST上传文件,实际踩下去才知道坑不少——请求体格式、字符编码、超时、大文件内存、UI线程阻塞,任何一环没处理好都会出问题。这篇文章就把我在老项目里从"能发"到"能稳定发大文件"的完整过程写出来,给正在MFC里做上传的朋友一份可以照着改的作业。

1. 上传之前先搞清楚:HTTP POST上传文件的真实格式

1.1 multipart/form-data到底长什么样

很多人一上来就搜"MFC http post上传文件",然后找到一个答案就往里套,结果代码抄下来还是不行。问题往往不在代码本身,而在你根本没搞清楚POST出去的数据是什么结构。

HTTP POST上传文件,绝大多数后端框架解析的是multipart/form-data格式。这个格式不是随手拼个URL、塞个二进制就能用的。

举一个最简单的例子,只有单个文件字段的上传请求,在报文层面长这样:

POST /api/upload HTTP/1.1 Host: my-upload-test-server.com Content-Type: multipart/form-data; boundary=----MyBoundary7MA4YWxkTrZu0gW Content-Length: 1365 ------MyBoundary7MA4YWxkTrZu0gW Content-Disposition: form-data; name="file"; filename="readme.txt" Content-Type: application/octet-stream hello, this is file content ------MyBoundary7MA4YWxkTrZu0gW--

你真正要上传的二进制数据,其实被夹在两段"文本头"中间。前面是Content-Disposition,声明这是一个名为file的字段、文件名是什么;后面是结束边界。服务端就是靠这个boundary字符串来切分数据的。

1.2 服务端是按"边界"切数据的,不是按文件大小

我见过有人把整个文件读进内存,然后直接往HttpSendRequest里塞,不构造任何边界。结果服务端要么收到空文件,要么直接报"请求格式不正确"。

原因很简单:服务端解析multipart时,会把整个请求体按boundary拆成多个part,每个part里有自己的Content-Disposition头。它只认边界字符串,不认文件大小,也不认你心里想的"我就传一个文件"。

所以,哪怕你只是传一个小文本文件,也必须在body里把这段"障碍跑"完整跑完:

  1. 开头的边界线--boundary\r\n
  2. 每个字段的Content-Disposition头
  3. 空行\r\n
  4. 字段内容
  5. 下一个边界线,循环
  6. 收尾的--boundary--\r\n

这里最容易翻车的是最后一行。很多新手写完了就忘记给结束边界收尾,服务端解析到一半发现没有结束标志,直接超时或者返回400。

1.3 为什么不能直接塞二进制进URL或body

也有人偷懒,想用"文件名放URL参数,文件内容直接POST"的方式。这种方式在某些内部接口里确实能跑,但只要后端用了Spring MVC这类框架来处理文件上传,人家就只认multipart/form-data。你把文件裸着放body里,后端解析不到文件字段,接口直接抛异常。

结论:MFC这边要做的,就是老老实实按multipart协议去拼请求体,然后把拼好的数据通过HTTP POST发出去。下面所有代码都围绕这一件事展开。

2. WinINet选型与MFC工程配置:决定成败的先行步骤

2.1 为什么我不推荐CInternetSession封装

MFC自己带了CInternetSession、CHttpConnection、CHttpFile这套HTTP封装。刚接触的人容易觉得这套东西够用,但真做文件上传时,你会发现自己被封装得很别扭:

  • CHttpFile能拿到请求和响应,可它的SendRequest设计目标是普通表单提交,想塞multipart大文件并不顺手。
  • MFC封装把很多底层参数都藏起来了,你想控制超时、分块写入、证书行为,反而要去GetHandle()拿底层句柄再调用WinINet函数——既然都这样了,那还不如直接用WinINet。

我的结论是:MFC工程做HTTP上传,直接用WinINet底层API,别绕一圈去用CInternetSession。这样代码可读性差一点,但可控性强的不是一点半点。

2.2 WinINet还是WinHTTP:一个老项目的选择

Windows上做HTTP请求,原生两条路:WinINet和WinHTTP。我见过很多人在论坛里问,MFC到底应该用哪个。

对比项WinINetWinHTTP
和IE共用底层设置是否
服务端场景支持较弱强
并发性能一般更好
证书控制可用InternetSetOption更灵活
大文件分块上传HttpSendRequestExWinHttpWriteData
MFC历史代码兼容最常见较少

对于MFC老项目、桌面工具、内部系统,我优先选WinINet。原因很实际:一是历史代码多,二是WinINet会自动读取当前系统的代理设置,走INTERNET_OPEN_TYPE_PRECONFIG很省事。如果是在Windows服务里做大量并发上传,才需要考虑WinHTTP或者C++ REST SDK。本文代码以WinINet为主。

2.3 编译配置必须检查两处:字符集和链接库

动手写代码前,先检查工程配置。

第一件事是字符集。MFC工程最常见的配置是Unicode,也就是CString实际上存的是宽字符。HTTP协议本身只认字节,所以后面我们做中文字段名、文件名时,必须做宽字符到UTF-8的转换。这一步逃不掉。

第二件事是链接库。WinINet的导入库是wininet.lib,在代码顶部加:

#pragma comment(lib, "wininet.lib")

或者到项目属性的"链接器-输入-附加依赖项"里填上wininet.lib。不链接的话,InternetOpen这类函数会直接报链接错误,而且是那种让你排查半天找不到原因的LNK2019。

3. 手工构造multipart请求体:边界、字段与文件块

3.1 boundary的生成规则,别再用固定字符串

boundary是multipart请求体里最关键的分隔标识。服务端会拿它来切分字段,所以这个字符串不能出现在文件内容里,否则解析就会乱。

我见过有人写死一个boundary = "----WebKitFormBoundary",这在文件内容恰好包含这个字符串时,会让服务端解析错乱。正确做法是每次上传生成一个足够随机的boundary。

我常用的办法是把系统时间加随机数拼起来:

SYSTEMTIME st; GetLocalTime(&st); char szSeed[128] = {0}; sprintf_s(szSeed, "%04d%02d%02d%02d%02d%02d%03d%03d", st.wYear, st.wMonth, st.wDay, st.wHour, st.wMinute, st.wSecond, st.wMilliseconds, rand() % 1000); std::string boundary = "----MFCUpload" + std::string(szSeed);

这个boundary足够长,文件内容里撞上的概率极低。生成之后,整个请求体、请求头里都用同一份。

3.2 一个完整的表单body由三段组成

以"带一个token字段、一个文件字段"的body为例,它实际由三段拼起来:

  1. 前缀部分:token字段 + 文件头
  2. 文件内容:读出来的二进制字节
  3. 后缀部分:结束边界的\r\n--boundary--\r\n

拼出来的效果是:

------MFCUpload20250101123000123 Content-Disposition: form-data; name="token" abcdef123456 ------MFCUpload20250101123000123 Content-Disposition: form-data; name="file"; filename="readme.txt" Content-Type: application/octet-stream 这里开始是文件原文 ------MFCUpload20250101123000123--

我把"文件头"和"文件尾"分开计算,这样后面做大文件分块上传时,不需要把文件整个读进内存,只需要先发送前缀,再循环读文件写入,最后发送后缀。

3.3 中文文件名必须转UTF-8,否则服务端解析乱码

MFC工程的宽字符CString,在发HTTP请求时要转成UTF-8字节。我有一个简单的工具函数:

std::string Utf8FromWide(const std::wstring& wstr) { if (wstr.empty()) return std::string(); int nLen = ::WideCharToMultiByte( CP_UTF8, 0, wstr.c_str(), (int)wstr.size(), NULL, 0, NULL, NULL); std::string strResult(nLen, '\0'); ::WideCharToMultiByte( CP_UTF8, 0, wstr.c_str(), (int)wstr.size(), &strResult[0], nLen, NULL, NULL); return strResult; }

比如文件名"测试报告.txt",CString里是宽字符,直接放进Content-Disposition头里发出去,后端拿到是乱码甚至直接报错。先转成UTF-8,后端才能正确识别。请求头里的普通ASCII字符串,比如Content-Type: application/octet-stream,保持ASCII就行,不用转。

3.4 文件名里的陷阱:引号、换行与控制字符

这是经验之谈。Content-Disposition里的filename="..."是带引号的字符串,如果文件名里带着英文双引号、回车换行,会把整个HTTP头结构破坏掉。

我处理文件上传参数的时候,会做一层过滤:把文件名里的"、\r、\n这些字符替换成下划线或者空格,再放进header里。虽然这会导致后端拿到的文件名略有变化,但至少请求不会失败。做多层系统对接时,文件名怪异导致的偶发失败,排查起来非常痛苦,所以宁可源头过滤。

4. 小文件版:一次HttpSendRequest发出去

4.1 调用链路:InternetOpen到InternetReadFile

如果只是传几MB以内的小文件,最直接的一条链路是:

InternetOpen -> InternetConnect -> HttpOpenRequest -> 拼好body -> HttpSendRequest -> HttpEndRequest(或直接读响应) -> InternetReadFile

HttpSendRequest可以一次性把头部和body都发出去,逻辑最简单,适合快速验证。

4.2 核心代码:完整的小文件上传函数

假设文件不大,我直接读进std::vector<BYTE>,然后拼body发送:

#include <afxwin.h> #include <wininet.h> #include <vector> #include <string> #pragma comment(lib, "wininet.lib") BOOL UploadSmallFile( LPCTSTR lpszServer, INTERNET_PORT nPort, LPCTSTR lpszUrlPath, LPCTSTR lpszFilePath, LPCTSTR lpszFieldName, LPCTSTR lpszToken, CString& strErrMsg) { HINTERNET hInternet = NULL; HINTERNET hConnect = NULL; HINTERNET hRequest = NULL; do { // 1. 打开WinINet会话,自动读取系统代理设置 hInternet = InternetOpen(_T("MyFileUploader/1.0"), INTERNET_OPEN_TYPE_PRECONFIG, NULL, NULL, 0); if (!hInternet) { strErrMsg = _T("InternetOpen失败"); break; } // 2. 连接服务器 hConnect = InternetConnect(hInternet, lpszServer, nPort, NULL, NULL, INTERNET_SERVICE_HTTP, 0, 0); if (!hConnect) { strErrMsg = _T("InternetConnect失败"); break; } // 3. 读取小文件到内存 CFile file; if (!file.Open(lpszFilePath, CFile::modeRead | CFile::typeBinary)) { strErrMsg = _T("打开文件失败"); break; } ULONGLONG ullFileSize = file.GetLength(); std::vector<BYTE> vecData((size_t)ullFileSize); file.Read(vecData.data(), (UINT)ullFileSize); file.Close(); // 4. 生成boundary SYSTEMTIME st; GetLocalTime(&st); char szSeed[128] = {0}; sprintf_s(szSeed, "%04d%02d%02d%02d%02d%02d%03d", st.wYear, st.wMonth, st.wDay, st.wHour, st.wMinute, st.wSecond, st.wMilliseconds); std::string boundary = "----MFCUpload" + std::string(szSeed); // 5. 拼请求体 std::string body; if (lpszToken && _tcslen(lpszToken) > 0) { body += "--" + boundary + "\r\n"; body += "Content-Disposition: form-data; name=\"token\"\r\n\r\n"; body += Utf8FromWide(lpszToken); // token可能含中文 body += "\r\n"; } // 文件块头 std::string strFileName = Utf8FromWide( (LPCWSTR)PathFindFileNameW(lpszFilePath)); std::string strFieldName = Utf8FromWide(lpszFieldName); body += "--" + boundary + "\r\n"; body += "Content-Disposition: form-data; name=\"" + strFieldName + "\"; filename=\"" + strFileName + "\"\r\n"; body += "Content-Type: application/octet-stream\r\n\r\n"; size_t nHeaderLen = body.size(); body.append((const char*)vecData.data(), vecData.size()); body += "\r\n--" + boundary + "--\r\n"; // 6. 打开POST请求 hRequest = HttpOpenRequest(hConnect, _T("POST"), lpszUrlPath, NULL, NULL, NULL, 0, 0); if (!hRequest) { strErrMsg = _T("HttpOpenRequest失败"); break; } // 7. 组装请求头 std::string strHeaders; strHeaders += "Content-Type: multipart/form-data; boundary=" + boundary + "\r\n"; strHeaders += "Content-Length: " + std::to_string(body.size()) + "\r\n"; // 8. 发送 BOOL bSend = HttpSendRequest(hRequest, strHeaders.c_str(), (DWORD)strHeaders.size(), (LPVOID)body.data(), (DWORD)body.size()); if (!bSend) { strErrMsg = _T("HttpSendRequest失败"); break; } // 9. 读取响应(诊断用) char szBuf[1024] = {0}; DWORD dwRead = 0; BOOL bRead = InternetReadFile(hRequest, szBuf, sizeof(szBuf), &dwRead); if (!bRead) { strErrMsg = _T("读取响应失败"); break; } strErrMsg = _T("上传成功"); return TRUE; } while (FALSE); if (hRequest) InternetCloseHandle(hRequest); if (hConnect) InternetCloseHandle(hConnect); if (hInternet) InternetCloseHandle(hInternet); return FALSE; }

这段代码里我用到了PathFindFileNameW,需要包含shlwapi.h并链接shlwapi.lib。如果不想引入,也可以自己在字符串里从最后一个\\或/往后找。

4.3 为什么必须丢进工作线程

很多人第一次写完,直接在按钮点击事件里调用UploadSmallFile,然后发现界面卡住,几秒到几十秒不等。这是因为WinINet的同步调用在等待网络响应时,会阻塞当前线程。MFC的主线程就是UI线程,UI线程一阻塞,整个窗口就"死了"。

正确的姿势是借用工作线程,典型做法:

UINT UploadThreadProc(LPVOID pParam) { UploadParam* pParamData = (UploadParam*)pParam; CString strErr; BOOL bOk = UploadSmallFile( pParamData->lpszServer, pParamData->nPort, pParamData->lpszUrlPath, pParamData->lpszFilePath, pParamData->lpszFieldName, pParamData->lpszToken, strErr); ::PostMessage(pParamData->hWnd, WM_UPLOAD_FINISHED, (WPARAM)bOk, (LPARAM)0); delete pParamData; return 0; }

线程结束前用PostMessage通知主线程,把结果带回UI。这里的关键点是:上传过程中任何UI操作都要PostMessage,不能SendMessage。因为SendMessage会等待UI线程处理,而UI线程此时可能正在等待上传线程,形成死锁。

5. 大文件版:HttpSendRequestEx流式上传

5.1 为什么body一次性拼完在三五百兆文件面前很脆弱

小文件版最大的问题,是把整个文件都读进内存,然后拼成一个大字符串。文件一旦上到三五百兆,内存立刻告急,32位进程甚至直接崩溃。另一个问题是HttpSendRequest本质要求body连续内存,拼body时的多次字符串拷贝,会让内存占用翻好几倍。

大文件上传的正确做法,是流式写入:先算出整个请求体的总长度,告诉WinINet,然后分块把文件内容通过InternetWriteFile写进去。这样内存占用固定,不管文件多大,内存里始终只保留一块缓冲区。

5.2 HttpSendRequestEx + InternetWriteFile工作流程

先看API调用流程:

HttpOpenRequest -> 构造INTERNET_BUFFERS,设置头部和总长度 -> HttpSendRequestEx -> 循环: 读文件块 -> InternetWriteFile写文件块 -> 计算进度 -> 写结束边界 -> HttpEndRequest -> InternetReadFile读响应

HttpSendRequestEx是准备阶段,告诉WinINet"我要发一个总长多少字节的请求"。InternetWriteFile才是真正往请求管道里写数据的函数,可以调用多次。最后用HttpEndRequest结束发送并读取响应。

5.3 Content-Length与dwBufferTotal:别写重复头

这是我踩过的坑。一开始我在头里手动写了Content-Length,然后又给INTERNET_BUFFERS.dwBufferTotal填了总长度,结果抓包发现报文里出现了两个Content-Length,服务端直接返回400。

WinINet的行为是:当INTERNET_BUFFERS.dwBufferTotal被设置为总字节数时,它会自动生成Content-Length头。所以正确做法是二选一。

我的习惯是只在INTERNET_BUFFERS.dwBufferTotal里设置总长度,不在自定义头里写Content-Length。自定义头里只保留Content-Type。

5.4 完整的大文件上传函数骨架

直接上一段可用的大文件上传函数核心:

BOOL UploadLargeFileByWinInet( LPCTSTR lpszServer, INTERNET_PORT nPort, LPCTSTR lpszUrlPath, LPCTSTR lpszFilePath, LPCTSTR lpszFieldName, LPCTSTR lpszToken, CString& strErrMsg, HANDLE hCancelEvent = NULL) { HINTERNET hInternet = NULL; HINTERNET hConnect = NULL; HINTERNET hRequest = NULL; BOOL bOk = FALSE; do { CFile file; if (!file.Open(lpszFilePath, CFile::modeRead | CFile::typeBinary)) { strErrMsg = _T("打开文件失败"); break; } ULONGLONG ullFileSize = file.GetLength(); // 生成boundary SYSTEMTIME st; GetLocalTime(&st); char szSeed[128] = {0}; sprintf_s(szSeed, "%04d%02d%02d%02d%02d%02d%03d", st.wYear, st.wMonth, st.wDay, st.wHour, st.wMinute, st.wSecond, st.wMilliseconds); std::string boundary = "----MFCUpload" + std::string(szSeed); // 前缀:token + 文件块头 std::string prefix; if (lpszToken && _tcslen(lpszToken) > 0) { prefix += "--" + boundary + "\r\n"; prefix += "Content-Disposition: form-data; name=\"token\"\r\n\r\n"; prefix += Utf8FromWide(lpszToken); prefix += "\r\n"; } std::string strFileName = Utf8FromWide( (LPCWSTR)PathFindFileNameW(lpszFilePath)); std::string strFieldName = Utf8FromWide(lpszFieldName); prefix += "--" + boundary + "\r\n"; prefix += "Content-Disposition: form-data; name=\"" + strFieldName + "\"; filename=\"" + strFileName + "\"\r\n"; prefix += "Content-Type: application/octet-stream\r\n\r\n"; // 后缀:结束边界 std::string suffix = "\r\n--" + boundary + "--\r\n"; // 总长度 ULONGLONG ullTotal = prefix.size() + ullFileSize + suffix.size(); if (ullTotal > 0xFFFFFFFF) { strErrMsg = _T("WinINet接口的单次请求不能超过4GB"); break; } // 打开连接 hInternet = InternetOpen(_T("MyFileUploader/1.0"), INTERNET_OPEN_TYPE_PRECONFIG, NULL, NULL, 0); if (!hInternet) { strErrMsg = _T("InternetOpen失败"); break; } hConnect = InternetConnect(hInternet, lpszServer, nPort, NULL, NULL, INTERNET_SERVICE_HTTP, 0, 0); if (!hConnect) { strErrMsg = _T("InternetConnect失败"); break; } hRequest = HttpOpenRequest(hConnect, _T("POST"), lpszUrlPath, NULL, NULL, NULL, 0, 0); if (!hRequest) { strErrMsg = _T("HttpOpenRequest失败"); break; } // 组装头部,注意这里不手动加Content-Length std::string strHeaders; strHeaders += "Content-Type: multipart/form-data; boundary=" + boundary + "\r\n"; // 准备缓冲结构 INTERNET_BUFFERS BufferIn = {0}; BufferIn.dwStructSize = sizeof(BufferIn); BufferIn.lpcszHeader = strHeaders.c_str(); BufferIn.dwHeadersLength = (DWORD)strHeaders.size(); BufferIn.dwBufferTotal = (DWORD)ullTotal; if (!HttpSendRequestEx(hRequest, &BufferIn, NULL, 0, 0)) { strErrMsg = _T("HttpSendRequestEx失败"); break; } // 写前缀 DWORD dwWritten = 0; if (!InternetWriteFile(hRequest, prefix.c_str(), (DWORD)prefix.size(), &dwWritten)) { strErrMsg = _T("写入前缀失败"); break; } // 循环写文件内容 const DWORD dwChunkSize = 32 * 1024; BYTE byChunk[dwChunkSize]; ULONGLONG ullSent = prefix.size(); BOOL bCancel = FALSE; while (ullSent < ullTotal - suffix.size()) { if (hCancelEvent && WaitForSingleObject(hCancelEvent, 0) == WAIT_OBJECT_0) { bCancel = TRUE; strErrMsg = _T("用户取消上传"); break; } UINT nRead = file.Read(byChunk, dwChunkSize); if (nRead == 0) { strErrMsg = _T("读取文件失败"); break; } DWORD dwWrite = 0; if (!InternetWriteFile(hRequest, byChunk, nRead, &dwWrite)) { strErrMsg = _T("写文件块失败"); break; } ullSent += dwWrite; // 这里可以PostMessage给主窗口,报告 ullSent * 100 / ullTotal // 注意控制消息频率,比如每隔200ms报一次 } if (bCancel || ullSent != ullTotal - suffix.size()) { break; } // 写后缀 if (!InternetWriteFile(hRequest, suffix.c_str(), (DWORD)suffix.size(), &dwWritten)) { strErrMsg = _T("写入结束边界失败"); break; } // 结束请求 if (!HttpEndRequest(hRequest, NULL, 0, 0)) { strErrMsg = _T("HttpEndRequest失败"); break; } // 读取响应 char szBuf[1024] = {0}; DWORD dwRead = 0; while (InternetReadFile(hRequest, szBuf, sizeof(szBuf), &dwRead) && dwRead > 0) { dwRead = 0; } bOk = TRUE; strErrMsg = _T("上传成功"); } while (FALSE); if (hRequest) InternetCloseHandle(hRequest); if (hConnect) InternetCloseHandle(hConnect); if (hInternet) InternetCloseHandle(hInternet); return bOk; }

这个版本我实测过,内存占用始终稳定在几十KB。文件有多大都不怕,因为那32KB缓冲区是反复使用的。注意循环结束的判断条件:当已发送字节数等于总长度 - 后缀长度时,说明文件部分已经发完,接下来写后缀。

5.5 进度上报与取消机制

进度上报,我在循环里通过PostMessage发给主窗口:

int nPercent = (int)(ullSent * 100 / ullTotal); ::PostMessage(hWnd, WM_UPLOAD_PROGRESS, nPercent, 0);

有个优化建议:不要每个32KB都PostMessage,否则高带宽下消息会刷爆UI线程。我一般加一个节流判断,比如每200毫秒才发一次消息,进度条尽量平滑。网络情况复杂时,按时间节流比按块数节流更稳。

取消机制就利用hCancelEvent。用户点"取消"时,主线程SetEvent,上传线程循环里检测到事件就跳出。注意取消后不要再去HttpEndRequest,直接InternetCloseHandle(hRequest)收尾即可。我见过有人在取消后强制调HttpEndRequest,结果服务端又收到一个半截请求,反而出乱子。

6. 实测中避开的坑:超时、HTTPS证书、抓包定位

6.1 超时不是设一次就完事

默认的WinINet超时对上传来说太宽松,一旦服务器响应慢,界面就干等着。我习惯在拿到hRequest后设置三个超时:

DWORD dwTimeout = 30000; InternetSetOption(hRequest, INTERNET_OPTION_CONNECT_TIMEOUT, &dwTimeout, sizeof(dwTimeout)); InternetSetOption(hRequest, INTERNET_OPTION_SEND_TIMEOUT, &dwTimeout, sizeof(dwTimeout)); InternetSetOption(hRequest, INTERNET_OPTION_RECEIVE_TIMEOUT, &dwTimeout, sizeof(dwTimeout));

CONNECT_TIMEOUT控制TCP连接建立的等待时间,SEND_TIMEOUT控制发送数据的写超时,RECEIVE_TIMEOUT控制读取响应的等待时间。大文件上传时,SEND_TIMEOUT尤其关键,如果不设置,慢速网络下一个大文件可能中途被WinINet认为超时而断开。注意超时设置要在HttpOpenRequest之后,HttpSendRequestEx之前。

6.2 HTTPS证书校验失败的常见错误码与处理边界

如果上传地址是HTTPS,HttpOpenRequest需要加上INTERNET_FLAG_SECURE。证书校验失败时,常见的错误码是12037(证书过期)和12038(证书域名不匹配)。

开发测试阶段,我确实会临时关闭证书校验,方便连本地测试环境:

DWORD dwFlags = SECURITY_FLAG_IGNORE_UNKNOWN_CA | SECURITY_FLAG_IGNORE_CERT_DATE_INVALID | SECURITY_FLAG_IGNORE_CERT_CN_INVALID; InternetSetOption(hRequest, INTERNET_OPTION_SECURITY_FLAGS, &dwFlags, sizeof(dwFlags));

但必须强调:这只适合内网开发测试,生产环境绝不能这么干。证书的作用是防中间人篡改,你把它关掉,相当于裸奔。正式对接时,请确保服务器证书合法、域名匹配,客户端不要绕开校验。这种安全底线问题,千万别图省事。

6.3 用抓包工具确认边界符和服务端解析结果

上传接口联调不顺利时,我最先怀疑的不是代码逻辑,而是请求体格式。怀疑头没用,直接抓包看最实在。抓包工具我常用的是本地代理工具,把目标端口指过去,就能看到完整的multipart/form-data请求。

看请求头时重点核对三点:

  1. Content-Type里必须有boundary=xxx,且和body里实际用的边界完全一致。
  2. 头与body之间必须有空行。
  3. body末尾必须有--boundary--\r\n作为完整结束。

这三点全对了,服务端还不能解析,那就去看服务端那边是不是要求字段名不同。比如我遇到过坑,服务端要求字段叫upload_file,我传的是file,对接方解析不到。这种问题不看抓包报文,纯靠猜,三天都不一定查得出来。

最后再分享几点个人体会

这个需求折腾下来,我的感受很深。第一,MFC里做HTTP上传,核心不在于"调什么API",而在于把multipart协议搞懂。协议清楚了,WinINet、WinHTTP都是表达方式而已。第二,千万不要在主线程里做网络请求,MFC程序的所有卡顿,一半以上都是网络操作直接写在UI线程里导致的。第三,代码里一定要留好错误码和抓包方法,大文件上传这种东西,一次失败几分钟,没有日志和报文回放,排查效率会很低。

如果你正在做一个MFC上传工具,建议先跑通小文件版,确认抓包里报文格式没问题,再换成大文件流式版本。这套思路不止适用于文件上传,后面遇到类似需要手工构造HTTP body的场景,比如调用第三方开放平台接口、上传JSON配置,同样能复用。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询