实现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
相关文章:

第一篇文章,做个纪念
第一篇文章,做个纪念,这个blog好吗?拭目以待!转载于:https://blog.51cto.com/197536/88241

Maven工程引入jar包(转)
Maven项目引入jar包的方法,希望能帮助有需要的朋友们 法一.手动导入:项目右键—>Build Path—>Configure Build Path—>选中Libraries—>点击Add External Jars—>选中已事先下好的Jar包导入即可。 法二.通过pom.xml文件的Dependencies标…

实现HTTP协议Get、Post和文件上传功能——使用libcurl接口实现
之前我们已经详细介绍了WinHttp接口如何实现Http的相关功能。本文我将主要讲解如何使用libcurl库去实现相关功能。(转载请指明出于breaksoftware的csdn博客) libcurl在http://curl.haxx.se/libcurl/有详细的介绍,有兴趣的朋友可以去读下。本文…
32岁程序员,补偿N+2:“谢谢裁我,让我翻倍!” 网友:榜样!
2019年的冬天,“冷”的有些频繁。12月19日,《马蜂窝被曝裁员40% UGC模式变现难?》爆火,据悉马蜂窝将裁员40%,交易中心成了“重灾区”,赔偿N2,留下的除搜索推荐、内容中心等核心部门外࿰…

山有木兮木有枝,心悦君兮君不知
《越人歌》今夕何夕兮,搴舟中流。 今日何日兮,得与王子同舟 蒙羞被好兮,不訾诟耻 心几烦而不绝兮,得知王子 山有木兮木有枝,心悦君兮君不知。本是《夜宴》中的,"山有木兮木有枝,心悦君兮君…

浅析电商、社区、游戏常用的 MySQL 架构
一般、或者必须是这样、MySQL 架构一定要结合业务来分析、设计、优化 所以不管是那种架构、根据业务要求组合成符合需求的即是最好的、不能泛泛而谈 同时、也必须注意数据的安全(如ipsec,ssh,vpn传输) 常见的架构都是进行业务切…
基于Co-Attention和Co-Excitation的少样本目标检测 | NeurIPS 2019
「免费学习 60 节公开课:投票页面,点击讲师头像」作者 | VincentLee来源 | 晓飞的算法工程笔记(ID: gh_084c810bc839)导读:论文提出CoAE少样本目标检测算法,该算法使用non-local block来提取目标图片与查询…
服务器架设笔记——搭建用户注册和验证功能
之前介绍的Apache Httpd相关内容,都是些零散的知识点。而实际运用中,我们要根据不同的业务,将这些知识点连接起来以形成各种组合,来满足我们的需求。(转载请指明出于breaksoftware的csdn博客) 本文我将以用…

项目管理过程中应注意的问题
软件项目从角色分工方面可以划分为研发、开发和实施三类,每个类型的项目有各自的管理过程。下面笔者就公司实施类项目的经历,从项目经理的角度谈一谈实施类项目管理过程中应该注意的一些问题,希望大家共勉。确定项目概况俗话说:“…
原创jQuery移动设备弹出框插件——msgalert.js
最近开发经常会用到顶部弹出框,虽然有现成的(bootstrap等),但是都很臃肿,对于有些时候移动端活动页面有点大材小用。所以今晚花了20分钟写了一个通用的插件,我将其命名为msgalert.js。因为定位是jQuery插件,…
AbutionGraph:构建以知识图谱为核心的下一代数据中台
「免费学习 60 节公开课:投票页面,点击讲师头像」作者 | 图特摩斯科技创始人闭雨哲出品 | AI科技大本营(ID:rgznai100)前言图特摩斯科技(Thutmose)基于自研的图形数据库AbutionGraph(实时多维数…

服务器架设笔记——多模块和全局数据
随着项目工程的发展,多模块设计和性能优化是在所难免的。本文我将基于一些现实中可能遇到的需求,讲解如何在Apache的Httpd插件体系中实现这些功能。(转载请指明出于breaksoftware的csdn博客) 之前我碰到两个需求: 需要…

JSP学习笔记(七):使用JavaBean
bean.java publicclassB1 { publicString getString() { return"content"; } }page.jsp <%B1 b1 newB1(); out.print(b1.getString());%>

折返(Reentrancy)VS线程安全(Thread safety)
在Wiki上,折返例如,下面的定义(接) In computing, a computer program or subroutine is called reentrant if it can be interrupted in the middle of its execution and then safely called again ("re-entered") be…
服务器架设笔记——httpd插件支持mysql字符集选择
mysql数据库默认的字符集是latin1。默认情况下,我们编译的httpd插件是可以正常读取该类型的数据库,并且不会出现乱码。但是,如果我们的数据库变成其他格式,比如UTF8,那么默认读取出来的数据就是乱码,且无论…
只需3行代码自动生成高性能模型,支持4项任务,亚马逊发布开源库AutoGluon
作者 | KYLE WIGGERS编译 | AI科技大本营(ID:rgznai100)构建涉及图像、文本和表格数据集的机器学习应用并不容易。它需要特征工程或使用数据领域知识来创建使AI算法起作用的特征,还需要进行大量数据预处理,以确保训练模型时不会出…

在客户端(IE中)无法登录Citrix MetaFrame server的原因
当在IE中登录服务器时如果出现这面的错误提示: ERROR: The Citrix MetaFrame servers cannot process your request at this time. The Citrix XML Service object was not found. [404 Not Found] 我的原因是IE使用了8080端口,而我的Citrix XML servic…

虚拟化--015 配置VMware View Event database失败:
015 配置VMware View Event database失败:参考链接http://url.cn/VTq4zN 转载于:https://blog.51cto.com/williamliuwen/1686536
以金山界面库(openkui)为例思考和分析界面库的设计和实现——代码结构(完)
三年前,准备将金山界面库做一个全面的剖析。后来由于种种原因,这个系列被中断而一直没有更新。时过境迁,现在在windows上从事开发的人员越来越少,关注这块的技术的朋友也很少了。本以为这系列也随着技术的没落而不再被人所关注&am…
一包烟钱买到电动剃须刀,小米有品告诉你什么叫性价比
男人身上长得最快的是什么?答案是胡须。一名健康男性的胡须每天都要生长超过 0.4mm,比咱们头发的生长速度还快,这也是男人隔三差五就要剃须的原因之一。男人的一生是与胡子战斗一生,也是被剃须刀拖累的一生。出差办事儿࿰…

Label控件属性AssociatedControlID
可以使用Label控件来标注一个HTML表单字段。Label控件拥有属性AssociatedControlID,可以设置此属性来指向表示表单字段的ASP.NET控件。 例如,代码清单2-3中的页面含有一个简单的表单,表单包含两个字段用于输入名和姓。Label控件用于标注这两个TextBox控件。 Code<% Page Lan…

2015_8_21作业——有自翻译有复制他人的英语太差
date作用:打印或设置系统日期和时间格式:date [OPTION]...[FORMAT]date [u|--utc|--universal] [MMDDhhmm[[CC]YY][.ss]]|是或 多选一的选项 ...代表同类内容可多次选项:注意短选项后不可加号-d,--dateSTRING显示时间字符串但不是立即:如date…
WMI技术介绍和应用——接收事件
时隔两三年,再次更新WMI系列博文。好在功能在三年前就已经实现了,现在只要补充些实例即可。 之前介绍的基本都是查询静态数据,而本文将要介绍非常有意思的事件接收功能。(转载请指明出于breaksoftware的csdn博客) 监控…

WML标签速查手册
WML标签速查手册 结构相关标签语法及属性<wml><wml xml:lang"lang" > content</wml> WML元素的共有属性主要有3个,即id、class和xml:lang属性。WML的所有元素都有两个核心属性,即标识(id)和类(class)属性。它们主要用…
Python 三十大实践、建议和技巧
所有参与投票的 CSDN 用户都参加抽奖活动群内公布奖项,还有更多福利赠送作者 | Erik-Jan van Baaren译者 | 凯隐编辑 | Jane出品 | AI科技大本营(ID:rgznai100)【导读】2020年,你又立了什么新的 Flag?新一年…

点击通知栏后打开Activity,并传参
为什么80%的码农都做不了架构师?>>> Reciver中intent new Intent(context, MessageDetailsaActivity_.class);intent.putExtra("freshMessageId", String.valueOf(push.getObid()));intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Inten…

WMI技术介绍和应用——执行方法
在之前的博文中,我们主要介绍了如何使用WMI查询信息和接收事件。本文将介绍WMI的另一种用法——执行方法。(转载请指明出于breaksoftware的csdn博客) 这块的内容在msdn中有详细的介绍,如果想看原版的可以参阅《Example: Calling a…

动态指定超链接参数的几种方法(Passing a JavaScript variable into href of )
情景:有些环境下我们需要根据页面中JavaScript变量的值来确定某个(某些)超链接的参数,如“http://www.bla.com/test.aspx?var1”中,究竟var1等于多少,要根据JavaScript变量来判定 方法一:很简单…

知乎「致知计划之科学季」颁奖,创作者分享80万元奖金
1月11日,「致知计划之科学季」颁奖典礼在北京798艺术中心举行,近500名创作者到场参加。 为了激励创作者,推动优质内容持续产出,知乎的「致知计划」从四个方面对创作者进行了扶持。一是流量扶持,让专业、优质的内容获得…

Inno Setup制作应用程序安装包
我最近写了一个MFC应用程序,想发给其他的小伙伴玩一玩,直接发了个exe文件过去,结果发现小伙伴那边打不开。原来这个exe文件虽然是MFC静态编译的,但是还依赖了其他几个.dll文件,需要把这几个dll文件和exe文件放在同一个…