整合营销服务商

电脑端+手机端+微信端=数据同步管理

免费咨询热线:

真正的 C++ 杀手不是 Rust

C++ 已经死了 80%?”本文作者已经使用 C++ 18 年了,他在体验了数十门编程语言后,他指出,尽管 C++ 在过去几十年中一直是程序员最常用的编程语言之一,但它存在一些问题,如不安全、效率低、浪费程序员的精力等。因此,文章探讨了一些可能会取代 C++ 的语言和技术,包括 Spiral、Numba 和 ForwardCom 等,并分别对它们进行了详细的介绍。

原文链接:https://wordsandbuttons.online/the_real_cpp_killers.html

以下为译文:

我是 C++ 粉,已经用 C++ 写了 18 年代码,而在这 18 年里,我一直在努力摆脱 C++。

一切始于 2005 年末的一个三维空间模拟引擎。该引擎具备了当时 C++ 所有的特性,三星指针、八层依赖关系,以及无处不在的 C 风格的宏。还有一些汇编代码片段,Stepanov 风格的迭代器,以及 Alexandrescu 风格的元编码。总之是应有尽有。那么,为什么呢?

因为这款引擎前后历时 8 年的时间,经手了 5 个不同的团队。每个团队都把自己喜欢的时髦技术带到了项目中,用时髦的包装方式包裹旧代码,但真正为引擎本身添加的价值却很少。

起初,我认真地尝试理解每一处小细节,但在碰了一鼻子灰之后,我放弃了。我还是老老实实完成任务,改 bug 吧。不能说我的工作效率很高,只能说很勉强,不至于被解雇。但后来我的老板问我:“你想把部分汇编代码改成 GLSG 吗?”虽然我并不了解GLSL是什么,但我觉得总不至于还不如 C++ 吧,于是我答应了。结果确实不至于还不如 C++。

后来,大部分的时间里我仍在用 C++ 写代码,但每当有人问我:“你想不想尝试一些非 C++ 的工作?”我就会说:“当然!”然后我就会去做。我写过 C89、MASM32、C#、PHP、Delphi、ActionScript、JavaScript、Erlang、Python、Haskell、D、Rust,以及令人闻风丧胆的脚本语言 InstallShield。我甚至还写过 VisualBasic、bash,以及几种不能公开谈论的专有语言。我甚至编写过自己的语言,我写了一个简单的 Lisp 风格解释器,帮助游戏设计师自动加载资源,然后去度假了。回来后发现他们用这个解释器编写了整个游戏场景,所以在接下来的一段时间里我们必须支持这个解释器。

在过去的 17 年里,我一直在努力摆脱 C++,但每次尝试过新技术后,总是会回到 C++。尽管如此,我仍然认为使用 C++ 编写程序是一个坏习惯。这门语言并不安全,效率也达不到人们的期望,而且程序员需要在与软件制作毫无关系的工作上浪费大量精力。你知道在 MSVC 中 uint16_t(50000) + uint16_t(50000) == -1794967296 吗?你知道为什么吗?你的看法与我不谋而合。

我认为,作为一名长期使用 C++ 的程序员,我有责任劝诫年轻一代程序员不要将 C++ 作为自己的专攻语言,就像有不良嗜好的人有责任劝诫不要重蹈覆辙。

那么,为什么我无法放弃 C++ 呢?问题出在哪里?问题在于,所有的编程语言,尤其是那些所谓的“C++ 杀手”,真正带来的优势都未能超越 C++。这些新语言大多会从一定程度上约束程序员。这本身没什么问题,毕竟当年晶体管密度每 18 个月翻一番,而程序员的数量每 5 年才翻一番,糟糕的程序员写不出优秀的代码也并不是什么大问题。

如今,我们生活在 21 世纪。经验丰富的程序员数量超过了历史任何时期,而且我们更需要高效的软件。

上个世纪,编写软件很简单。你有一个想法,然后将其包装成 UI,再作为桌面系统软件产品出售就可以了。运行速度太慢?没人在乎!18 个月内,台式机的速度就会翻倍。重要的是进入市场,打开销路,而且还没有 bug。当然,如果编译器能防止程序员犯错就更好了,因为 bug 不但不会产生收益,而且你还要付钱给程序员改 bug。

而如今情况大不相同了。你有一个想法,然后将其包装到 Docke 容器中,并在云中运行。如今想获取收入,你的软件就必须为用户解决问题。即使一款产品只做一件事,但只要做的正确,就能获得报酬。你不必为了销售新版本的产品而不断扩充功能。相反,如果你的代码发挥不了真正的作用,买单的就是你自己。云账单就能真实地反映出你的程序是否真的起作用。

因此,在新的环境下,你需要的功能更少,但所有的功能都需要更出色的性能。

在这个前提下你就会发现,所有的“C++ 杀手”,甚至是我由衷喜欢和尊敬的 Rust、Julia 和 D,也没有解决 21 世纪的问题。它们仍然停留在上个世纪。虽然这些语言可以帮助你编写更多功能,而且 bug 更少,但当你需要从租用的硬件中压榨出最后一点 FLOPS 时,它们就没有太大用处了。

因此,这些语言只不过是比 C++ 更具竞争优势,或者说彼此之间可以竞争。但大多数编程语言,例如 Rust、Julia 和 Cland,甚至共享同一个后端。所有赛车手都坐在同一辆车上,何谈谁能赢得比赛呢?

那么,究竟哪些技术比 C++ 或者传统的预先编译器更有优势呢?

C++的头号杀手:Spiral

在讨论 Spiral 之前,让我先来考考你。你觉得以下哪个版本的代码运行速度更快?版本1:标准的 C++ 正弦函数;版本2:由4个多项式模型组成的正弦函数?

下一个问题。以下哪个版本的代码运行速度更快?版本1:使用短路逻辑运算;版本2:将逻辑表达式转换为算术表达式?

第三个问题,以下哪个版本的三元组排序更快?版本1:带有分支的交换排序;版本2:无分支的索引排序?

如果你果断地回答了以上所有问题,甚至没有思考或上网搜索,那么只能说你被自己的直觉骗了。你没有发现陷阱吗?在没有上下文的情况下,这些问题都没有确定的答案。

  1. 如果使用 clang 11 和 -O2 -march=native 构建,在英特尔Core i7-9700F 上运行,多项式模型比标准正弦快 3 倍。但如果使用 NVCC 和 --use-fast-math 构建,在GeForce GTX 1050 Ti Mobile 上运行,标准正弦比多项式模型快10 倍。
  2. 在 i7 上,如果将短路逻辑替换为向量化算术,可以将代码的运行速度提高一倍。但在 ARMv7 上,使用 clang 和-O2,标准逻辑比微优化快 25%。
  3. 对于索引排序与交换排序,在英特尔上,索引排序比交换排序快 3 倍;而在 GeForce 上,交换排序比索引排序快 3 倍。

因此,我们喜爱的微优化都有可能将代码的运行提升3倍,也有可能导致速度下降90%。这完全取决于上下文。如果编译器能为我们选择最佳替代方案,那该多好,例如,当我们切换构建目标时,索引排序会神奇地变成交换排序。但可惜编译器做不到。

  1. 即使我们允许编译器将正弦函数换成多项式模型,用牺牲精度的代价换取速度,它也不清楚我们的目标精度。在 C++ 中,我们无法表达:“此函数允许有误差”。我们只有--use-fast-math之类的编译器标志,而且只在翻译单元的范围内。
  2. 在第二个示例中,编译器不知道我们的值仅限于 0 或 1,而且也不可能提出可以实施的优化。虽然我们可以通过布尔类型来暗示,但这又是另一个问题了。
  3. 在第三个示例中,两段代码完全不同,编译器无法将二者视为等效代码。代码描写了太多细节。如果只有 std::sort,就可以给编译器更多自由选择算法的空间。但它不会选择索引排序或交换排序,因为这两种算法处理大型数组的效率都很低,而 std::sort 适合通用可迭代容器。

此处就不得不提到 Spiral 了。该语言是卡内基梅隆大学和苏黎世联邦理工学院的联合项目。简单来说,信号处理专家厌倦了每出现一种新硬件就需要手动重写他们喜欢的算法,因此编写了一个可自动完成这项工作的程序。该程序接受算法的高级描述和硬件架构的详细描述,并优化代码,直到在指定的硬件上实现最高效的算法。

与 Fortran 等语言不同,Spiral 真正解决了数学意义上的优化问题。它将运行时定义为目标函数,并在受硬件架构限制的可变因素空间内寻找全局最优实现。编译器永远无法真正实现这种优化。

编译器不会寻找真正的最优解。它只不过是根据程序员所教的启发式规则来优化代码。实质上,编译器并不是一个寻找最优解的机器,更像一个汇编程序员。一个好的编译器就像一个好的汇编程序员,仅此而已。

Spiral是一个研究项目,范围和预算都很有限。但最后展现的结果却很惊人。在快速傅里叶变换中,他们的解决方案明显优于 MKL 和 FFTW 的实现,他们的代码速度约快了 2 倍,即使在英特尔上也是如此。

为了突显如此宏大的成就,需要说明一下,MKL 是英特尔自己的数学内核库(Math Kernel Library,简称MKL),因此他们非常了解如何充分利用自家的硬件。而WWTF(Fastest Fourier Transform in the West,西部最快傅里叶变换)是一种高度专业化的库,由最了解该算法的人编写。二者都是各自领域的冠军,而 Spiral 的速度能够达到二者两倍,这实在太不可思议了。

等到 Spiral 使用的优化技术最终成熟并商业化,不仅仅是 C++,包括 Rust、Julia,甚至 Fortran 都将面临前所未有的竞争压力。既然能使用高级算法描述语言编写2倍速的代码,谁还会使用C++呢?

C++ 杀手之二:Numba

相信你很熟悉这门优秀的编程语言。几十年来,大多数程序员来说最熟悉的语言一直是 C。在 TIOBE 指数中,C语言一直名列第一,其他类似 C 的语言占据了前十名。然而,两年前,一件前所未闻的事情发生了,C 语言第一名的地位不保。

取而代之的语言是Python。90年代,没有人看好Python,因为它不过是众多脚本语言中的一个。

有人会说:“Python很慢”,但这种说法很荒谬,就像说手风琴或平底锅很慢一样,语言本身没有快慢之分。就像手风琴的速度取决于演奏者一样,语言的快慢取决于编译器的速度。

可能还会有人说:“Python不是一种编译语言”,这个说法也不严谨。Python 编译器有很多,其中一个最被看好的编译器也算是Python脚本。我来解释一下。

我曾经有一个项目,是一个3D打印模拟,最初是用Python编写的,后来“为了性能”改用C++重写,后来又移植到 GPU 上,当然这些都是在我进入项目之前发生的事儿。后来,我花了几个月的时间将构建迁移到 Linux,优化了 Tesla M60 的 GPU 代码,因为这是当时AWS中最便宜的GPU。之后,我又在 C++/CU 代码中验证了所有变更,以便与原来的Python代码相结合。除了设计几何算法之外,所有的工作都是由我完成的。

在一切正常运行后,Bremen 的一名兼职学生打电话给我问道:“听说你很擅长使用多种技术,能帮我在 GPU 上运行一个算法吗?”“当然可以!”我给他讲了CUDA、CMake、Linux 构建、测试以及优化等等,大约花了一个小时。他很有礼貌地听完了我的介绍,最后说:“很有意思,但我想问一个非常具体的问题。我有一个函数,我在函数的定义前面加了@cuda.jit,Python就无法编译内核了,还提示了一些关于数组的错误。你知道这里面有什么问题吗?”

我不知道。后来,他花了一天时间自己搞清楚了。原因是,Numba 无法处理原生的Python列表,只接受 NumPy 数组中的数据。他找到了问题所在,并在 GPU 上运行了算法。使用的是Python。他没有遇到我花费了几个月心思解决的任何“问题”。想在 Linux 上运行代码?没问题,直接在Linux运行即可。想针对目标平台优化代码?也不是问题。Numba 会替你优化在平台上运行的代码,因为它不会预先编译代码,而是在部署时按需编译。

很厉害,对不对?然而,对我来说并不是。我花费了几个月的时间,使用C++解决 Numba 中不会出现的问题,而那位Bremen的兼职学生完成相同的工作只花费了几天的时间。如果不是因为那是他第一次使用Numba,可能只需要几个小时。说到底,Numba是什么?它是一种什么样的魔法?

没有魔法。Python 的装饰器将每一段代码都转换成了抽象语法树,因此你可以随意处理。Numba是一个 Python 库,可使用任何后端、为任何支持的平台编译抽象语法树。如果你想将Python 代码编译成以高度并行的方式在 CPU 核心上运行,只需告诉 Numba 编译即可。如果你希望在GPU上运行代码,同样只需提出请求即可。

Numba是一个Python编译器,可以淘汰C++。然而,从理论上来说,Numba并没有超越C++,因为二者使用的是同一个后端。Numba的GPU编程使用了CUDA,CPU编程使用了LLVM。实际上,由于它不需要针对每种新的架构提前重建,因此能够更好地适应每种新硬件及其潜在的优化。

当然,如果Numba能像Spiral那样具有显著的性能优势会更好。但Spiral更像是一个研究项目,最终可能会淘汰C++,但前提是足够幸运才行。Numba与Python的结合可以立即判C++死刑。如果可以使用Python编程,而且能拥有C++的性能,谁还会写C++代码呢?

C++ 杀手之三:ForwardCom

下面,我们再玩一个游戏。我给你三段代码,你猜猜哪一段(也有可能是多段)是用汇编语言编写的。

第一段代码:

第二段代码:

第三段代码:

如果你猜到这三个例子都是汇编,那么恭喜你!

第一个例子是用 MASM32 编写的。这是一个带有“if”和“while”的宏汇编器,用于编写原生Windows 应用程序。注意,不是以前有人这么写,而是至今仍在采用这种写法。微软一直在积极维护Windows 与 Win32 API 的向后兼容性,因此所有以前编写的 MASM32 程序都可以在现代 PC 上正常运行。

很讽刺的是,C 语言的发明是为了降低将 UNIX 从PDP-7 转换成 PDP-11 的难度。C语言的设计初衷就是成为一种便携式汇编语言,能够在 70 年代硬件架构的寒武纪爆发中生存下来。但在 21 世纪,硬件架构的演变如此缓慢,我在 20 年前用 MASM32 写的程序如今仍然能完美运行,但我不敢确定去年用 CMake 3.21 构建的 C++ 应用程序今时今日能否用 CMake 3.25 构建。

第二段代码是 WebAssembly,这门技术甚至不是一个宏汇编器,没有“if”和“while”,更像是人类可读的浏览器机器码。从概念上来说,可以是任何浏览器。

WebAssembly代码根本不依赖于硬件架构。它提供的机器是抽象的、虚拟的、通用的,随你怎么称呼它。如果你能阅读这段文字,说明你的物理机器上已经有一个能运行WebAssembly的硬件架构了。

最有趣的是第三段代码。这是 ForwardCom:一款由著名的 C++ 和汇编优化手册作者 Agner Fog 提出的汇编器。与 Web Assembly 一样,这不仅仅是一个汇编器,而且旨在实现向后以及向前兼容性的通用指令集。因此得名。ForwardCom 的全称是an open forward-compatible instruction set architecture(一款开放式向前兼容指令集架构)。换句话说,它不仅是一个汇编器的提议,而且也是一份和平条约提议。

我们知道最常见的计算机架构系列 x64、ARM 和 RISC-V 都有不同的指令集。但没有人知道为什么要保持这种状态。所有现代处理器,除了最简单的一些之外,运行的都不是你提供的代码,而是将你的输入转换为微码。因此,不仅M1芯片提供英特尔的向后兼容层,每个处理器本质上都为自己的早期版本提供了向后兼容层。

那么,为什么架构设计者未能就类似的向前兼容层达成统一意见呢?无外乎各个公司之间的竞争野心。但如果处理器制造商最终决定建立一个共同的指令集,而不是为每个竞争对手实现一个新的兼容层,ForwardCom就能够让汇编重回主流。这种向前兼容层可以治愈每个汇编程序员最大的心理创伤:“如今我为这个特定的架构编写一次性代码,不出一年就会被淘汰?”

有了向前兼容层,这些代码就永远不会过时。这就是关键所在。

此外,汇编编程还受到了另一种错误观念的限制,人们普遍认为汇编代码太难写,因此不实用。Fog 的提议也解决了这个问题。如果人们认为写汇编代码太难,而写 C 不难,那么我们就把汇编变成C语言。这不是问题。现代汇编语言没有必要延续50年代祖宗的模样。

上面你看到的三个汇编示例都不像“传统”的汇编,而且也不应该还是老样子。

ForwardCom是一种汇编,可用于编写永远不会过时的最佳代码,并且不需要学习“传统”的汇编。从现实的角度来看率,ForwardCom是未来的 C。不是 C++。

C++ 什么时候终消亡?

我们生活在一个后现代世界。与世长辞的不是技术,而是人。就像拉丁语从未真正消失一样,COBOL、Algol 68 和 Ada 也一样,C++ 注定要永远介于生死参半的状态。C++ 永远不会真正消失,它只会被更新更强大的新技术所取代。

严格来说,不是“将来会被取代”,而是“正在被取代”。我的职业生涯源自 C++,而如今在使用 Python 写代码。我编写方程式,SymPy 帮我求解,然后将解决方案转换为 C++。然后,我将这段代码粘贴到 C++ 库中,甚至都无需调整格式,因为 clang-tidy 会自动完成。静态分析器会检查命名空间是否混乱,动态分析器会检查内存泄漏。CI/CD 负责跨平台编译。性能分析器让我了解代码实际的运行情况,反汇编器可以解释为什么。

如果我用 C++ 之外的技术代替 C++,那么 80% 的工作不会有变化。对于我的大多数工作来说,C++ 根本无关紧要。这是否意味着,对于我来说,C++ 已经死了 80%?

icpick是一款非常实用的屏幕截图工具,软件为用户提供了多种截取屏幕的方式,如全屏、活动窗口、窗口空间、滚动窗口、矩形区域、固定区域、任意形状等等,用户们可以根据自己的需求进行选择使用,满足了不同用户的使用需求,同时还提供了众多的实用工具供用户使用,比如取色器、放大镜、标尺、调色板、坐标轴、量角器、白板等等,在截图之后就可以直接使用这些实用工具制作你需要的素材图片了,轻松就可以帮你制作出最优质的图片,并且还可以对图片进行添加各种的效果,因此软件内置了一个强大的编辑器,用户可以进行添加文本、阴影、水印等一系列操作,非常的方便实用。总之使用picpick截图软件可以轻松帮你抓取到全屏幕或是局部的画面,可以随意进行修改,而且操作也很简单,是一款非常优秀的截图工具。picpick下载安装-picpick截图软件下载 v5.1.9中文免费版 - 多多软件站

软件功能

1、截获任何截图
截获屏幕截图、活动窗口的截图、桌面滚动窗口的截图和任何特定区域的截图等等。
2、编辑你的图片
注释并标记您的图片:您可以使用内置图片编辑器中的文本、箭头、形状和更多功能,并且该编辑器还带有最新的Ribbon风格菜单。
3、增强效果
为你的图片添加各种效果:阴影、框架、水印、马赛克、运动模糊和亮度控制等等。
4、分享到任何地方
通过网络、邮件、ftp、Dropbox、Google Drive、SkyDrive、Box、Evernote、Facebook、Twitter和其它更多方式来保存、分享或发送你的照片。
5、平面设计附件
各种平面设计附件包括颜色选择器、颜色调色板、像素标尺、量角器、瞄准线、放大镜和白板。
6、自定义设置
软件带有各种高级的设置,您可以自定义快捷键、文件命名、图片质量和许多其它的功能。

picpick怎么修改快捷键?

1、点击左上角的文件,然后在弹出窗口中选择程序选项;


2、在打开的窗口左侧有一项是快捷键,点击打开快捷键窗口;


3、在快捷键窗口中根据自己的喜好,分别设置需要的快捷键。设置完成后记得点击底部的确定按钮进行保存。

picpick怎么滚动截图?

1、首先打开软件,然后点击滚动窗口;


2、这时候,可以看到窗口的外围有个绿色的框框,不要乱动鼠标;


3、用鼠标点击右侧的滚动条,然后不要动鼠标,键盘和电脑;


4、picpick自己截屏;


5、当滚动条滚动到最下方的时候,截屏结束,picpick自动保存图片,打开图片编辑界面;


6、可以看一下,由于电脑自运行比较慢的问题,有的地方截取效果不好,这需要自己修改。

软件特色

1、友好的用户界面,提供Windows 7的 Ribbon 样式;
2、拥有基本的编辑绘图、形状、指示箭头、线条、文本等功能;
3、同时还支持模糊,锐化,色调,对比度,亮度,色彩平衡,像素化,旋转,翻转,边框等效果;
4、支持共享截图至 FTP 、Web 、E-Mail、Facebook、Twitter 等社交网络;
5、屏幕截图:支持全屏、活动窗口、滚动窗口 、窗口控制、区域、固定区域、手绘、重复捕捉;
6、Ribbon界面图像编辑器:箭头、线条等绘图工具。模糊、锐化、像素化、旋转、翻转,框架等特效;
7、拾色器和调色板:支持RGB、HTML、C++、Delphi等代码类型,Photoshop风格转换,保存颜色;
8、屏幕放大镜、量角器、屏幕坐标计算功能,为你的演示文稿把屏幕当作白板自由绘画。

PicPick截图之后怎么批量保存图片?

1、打开PicPick,并使用PicPick截图多张图片。此时图片处于未保存状态;


2、然后我们点击PicPick左上角的文件;


3、接下来我们点击如下图所示的保存(或者另存为);


4、然后我们看到并点击“全部保存”;


5、在弹出的保存窗口中,点击三个点的图标,找到你的图片要保存的位置;


6、之后点击确定,即可完成批量保存。

载大文件时,断点续传是很有必要的,特别是网速度慢且不稳定的情况下,很难保证不出意外,一旦意外中断,又要从头下载,会很让人抓狂。断点续传就能很好解决意外中断情况,再次下载时不需要从头下载,从上次中断处继续下载即可,这样下载几G或十几G大小的一个文件都没问题。本文介绍利用miniframe开源Web框架分别在lazarus、delphi下实现文件HTTP下载断点续传的功能。

本文Demo还实现了批量下载文件,同步服务器上的文件到客户端的功能。文件断点续传原理:分块下载,下载后客户端逐一合并,同时保存已下载的位置,当意外中断再次下载时从保存的位置开始下载即可。这其中还要保证,中断后再次下载时服务器上相应的文件如果更新了,还得重新下载,不然下载到的文件是错了。说明:以下代码lazarus或delphi环境下都能使用。全部源码及Demo请到miniframe开源web框架下载: https://www.wyeditor.com/miniframe/或https://github.com/dajingshan/miniframe。

服务器端代码

文件下载断点续传服务器端很简单,只要提供客户端要求下载的开始位置和指定大小的块即可。

以下是服务器获取文件信息和下载一个文件一块的代码:

<%@//Script头、过程和函数定义
program codes;
%>
 
<%!//声明变量
var
  i,lp: integer;
  FileName, RelativePath, FromPath, ErrStr: string;
  json: TminiJson;
  FS: TFileStream;
  
function GetOneDirFileInfo(Json: TminiJson; Path: string): string;
var
  Status: Integer;
  SearchRec: TSearchRec;
  json_sub: TminiJson;
begin
  Path := PathWithSlash(Path);
  SearchRec := TSearchRec.Create;
  Status := FindFirst(Path + '*.*', faAnyFile, SearchRec);
  try
    while Status = 0 do
    begin 
      if SearchRec.Attr and faDirectory = faDirectory then
      begin
        if (SearchRec.name <> '.') and (SearchRec.name <> '..') then
          GetOneDirFileInfo(Json, Path + SearchRec.Name + '\');
      end else
      begin
        FileName := Path + SearchRec.Name;
        try
          if FileExists(FileName) then
          begin 
            json_sub := Pub.GetJson;  
            json_sub.SO; //初始化 或 json.Init;    
            json_sub.S['filename'] := SearchRec.name;
            json_sub.S['RelativePath'] := GetDeliBack(FileName, FromPath);
            json_sub.S['FileTime'] := FileGetFileTimeA(FileName);
            json_sub.I['size'] := SearchRec.Size;
            json.A['list'] := json_sub;
          end;
        except
          //print(ExceptionParam)
        end;//}
      end; 
      Status := FindNext(SearchRec);
    end;
  finally
    FindClose(SearchRec);
    SearchRec.Free;
  end;//*) 
end;
%>
<%
begin
  FromPath := 'D:\code\delphi\sign\发行文件'; //下载源目录
  
  json := Pub.GetJson; //这样创建json对象不需要自己释放,系统自动管理
  json.SO; //初始化 或 json.Init;
  
  // 验证是否登录代码
  {if not Request.IsLogin('Logined') then
  begin 
    json.S['retcode'] := '300';
    json.S['retmsg'] := '你还没有登录(no logined)!'; 
    print(json.AsJson(true));
    exit; 
  end;//} 
  
  json.S['retcode'] := '200';
  json.S['retmsg'] := '成功!';
  if Request.V('opr') = '1' then
  begin //获取服务上指定目录的文件信息
    GetOneDirFileInfo(Json, FromPath);
  end else
  if Request.V('opr') = '2' then
  begin //下载指定文件给定大小的块 
    FromPath := PathWithSlash(FromPath);   
    RelativePath := Request.V('fn');
    FileName := FromPath + RelativePath;
    Fs := Pub.GetFS(FileName, fmShareDenyWrite, ErrStr);
    if trim(ErrStr) <> '' then 
    begin
      json.S['retcode'] := '300';
      json.S['retmsg'] := ErrStr;
      print(json.AsJson(true));  
      exit;
    end;
    Fs.Position := StrToInt(Request.V('pos'));
    Response.ContentStream := TMemoryStream.Create; //注意不能用 Pub.GetMs,这是因为Pub.GetMs创建的对象在动态脚本运行完就释放了
    Response.ContentStream.CopyFrom(Fs, StrToInt(Request.V('size')));
    //返回流数据
    Response.ContentType := 'application/octet-stream';   
  end;
  print(json.AsJson(true));
end;
%>

客户端代码

客户端收到块后,进行合并。全部块下载完成后,还要把新下载的文件的文件修改为与服务器上的文件相同。以下是客户端实现的主代码:

procedure TMainForm.UpgradeBlock_Run(var ThreadRetInfo: TThreadRetInfo);
const
  BlockSize = 1024*1024; //1M
var
  HTML, ToPath, RelativePath, FN, Tmp, TmpFileName, FailFiles, SuccFiles, Newfn, TmpToPath: string;
  Json, TmpJson: TminiJson;
  lp, I, Number, HadUpSize, AllSize, AllBlockCount, MySize, MyNumber: Int64;
  Flag: boolean;
  SL, SLDate, SLSize, SLTmp: TStringlist;
  MS: TMemoryStream;
  Fs: TFileStream;
  procedure HintMsg(Msg: string);
  begin
    FMyMsg := Msg; // '正在获取文件列表。。。';
    ThreadRetInfo.Self.Synchronize(ThreadRetInfo.Self, MyUpdateface); //为什么不直接用匿名,因为laz不支持
  end;
begin
  ToPath := 'D:\superhtml'; //如果是当前程序更新  ExtractFilePath(ParamStr(0))
 
  ThreadRetInfo.Ok := false;
 
  HintMsg('正在获取文件列表。。。');
  if not HttpPost('/接口/同步文件到客户端.html?opr=1',
      '', ThreadRetInfo.ErrStr, ThreadRetInfo.HTML) then exit;
  if Pos('{', ThreadRetInfo.HTML) <> 1 then
  begin
    ThreadRetInfo.ErrStr :='请先检查脚本源码是否配置正确!';
    exit;
  end;
  ToPath := Pub.PathWithSlash(ToPath);
 
  Json := TminiJson.Create;
  SL := TStringlist.Create;
  SLDate := TStringlist.Create;
  SLSize := TStringlist.Create;
  SLTmp := TStringlist.Create;
  try
    Json.LoadFromString(ThreadRetInfo.HTML);
    if json.S['retcode'] = '200' then
    begin
      TmpJson := json.A['list'];
      for lp := 0 to TmpJson.length - 1 do
      begin
        HintMsg(lp.ToString + '/' + TmpJson.length.ToString + '正在检查文件:' + RelativePath);
        RelativePath := TmpJson[lp].S['RelativePath'];
        if trim(RelativePath) = '' then Continue;
        Flag := FileExists(ToPath + RelativePath);
        if Flag then
        begin
          if (PubFile.FileGetFileTimeA(ToPath + RelativePath) = TmpJson[lp].S['FileTime']) and
             (PubFile.FileGetFileSize(ToPath + RelativePath) = TmpJson[lp].I['Size']) then
          else
            Flag := false;
        end;
        if not Flag then //此文件需要更新
        begin
          SL.Add(RelativePath);
          SLDate.Add(TmpJson[lp].S['FileTime']);
          SLSize.Add(TmpJson[lp].S['Size']);
        end;
      end;
 
      //开始下载
      FailFiles := '';
      SuccFiles := '';
      HintMsg('需要更新的文件共有' + IntToStr(SL.Count) + '个。。。');
      for lp := 0 to SL.Count - 1 do
      begin
        RelativePath := SL[lp];
        if RelativePath[1] = '\' then RelativePath := Copy(RelativePath, 2, MaxInt);
        FN := ToPath + RelativePath;
 
        //先计算要分几个包,以处理进度
        Number := 0;
        HadUpSize := 0;
        AllSize := StrToInt64(SLSize[lp]);
        AllBlockCount := 0;
        while true do
        begin
          AllBlockCount := AllBlockCount + 1;
          if AllSize - HadUpSize >= BlockSize then
             MySize := BlockSize
          else
             MySize := AllSize - HadUpSize;
          HadUpSize := HadUpSize + MySize;
          if HadUpSize >= AllSize then
            break;
        end;
 
        //开始分块下载
        Number := 0;
        HadUpSize := 0;
        //AllSize := Fs.Size;
        //TmpToPath := PubFile.FileGetTemporaryPath;
        Newfn := '@_' + PubPWD.GetMd5(SLDate[lp] + SLSize[lp]) + ExtractFileName(FN);  //Pub.GetClientUniqueCode;
 
        if FileExists(ToPath + Newfn) and (FileExists(FN)) then
        begin
          SLTmp.LoadFromFile(ToPath + Newfn);
          MyNumber := StrToInt64(trim(SLTmp.Text));
          Fs := TFileStream.Create(FN, fmOpenWrite);
        end else
        begin
          MyNumber := 0;
          Fs := TFileStream.Create(FN, fmCreate);
        end;
        try
          while true do
          begin
            HintMsg('正在下载文件[' + Pub.GetDeliBack(RelativePath, '@@') + ']第[' + IntToStr(Number + 1) + '/' + IntToStr(AllBlockCount) + ']个包。。。');
 
            if AllSize - HadUpSize >= BlockSize then
               MySize := BlockSize
            else
               MySize := AllSize - HadUpSize;
            Number := Number + 1;
            if (MyNumber = 0) or (Number >= MyNumber) or (HadUpSize + MySize >= AllSize) then
            begin
              for I := 1 to 2 do //意外出错重试一次
              begin
                if not HttpPost('/接口/同步文件到客户端.html?opr=2fn=' + UrlEncode(RelativePath) +
                  'pos=' + UrlEncode(IntToStr(HadUpSize)) + 'size=' + UrlEncode(IntToStr(MySize)),
                  '', ThreadRetInfo.ErrStr, ThreadRetInfo.HTML, MS) then
                begin
                  if I = 2 then
                  begin
                    ThreadRetInfo.ErrStr := Json.S['retmsg'];
                    exit;
                  end else
                    Continue;
                end;
                if Pos('{', ThreadRetInfo.HTML) < 1 then
                begin
                  if I = 2 then
                  begin
                    ThreadRetInfo.ErrStr := Json.S['retmsg'];
                    exit;
                  end else
                    Continue;
                end;
 
                Json.LoadFromString(ThreadRetInfo.HTML);
                if json.S['retcode'] <> '200' then
                begin
                  if I = 2 then
                  begin
                    ThreadRetInfo.ErrStr := Json.S['retmsg'];
                    exit;
                  end else
                    Continue;
                end;
                break;
              end;
 
              if MS = nil then
              begin
                ThreadRetInfo.ErrStr := '没能下载到文件[' + RelativePath + ']!' + json.S['retmsg'];
                exit;
              end else
              begin
                Fs.Position := HadUpSize;
                MS.Position := 0;
                Fs.CopyFrom(MS, MS.Size);
                MS.Free;
                MS := nil;
                SLTmp.Text := Number.ToString;
                try
                  SLTmp.SaveToFile(ToPath + Newfn);
                except
                end;
              end;
            end;
            HadUpSize := HadUpSize + MySize;
 
            if HadUpSize >= AllSize then
            begin //全部下载完成
              Fs.Free;
              Fs := nil;
              Sleep(10);
              PubFile.FileChangeFileDate(Fn, SLDate[lp]);
              DeleteFile(ToPath + Newfn);
              SuccFiles := SuccFiles + #13#10 + RelativePath;
              break;
            end;
          end;
        finally
          if Fs <> nil then
            Fs.Free;
        end;
      end;
      ThreadRetInfo.HTML := '';
      if trim(SuccFiles) <> '' then
        ThreadRetInfo.HTML := '本次更新了以下文件:'#13#10 + SuccFiles;
      //if trim(FailFiles) <> '' then
        //ThreadRetInfo.HTML := trim(ThreadRetInfo.HTML + #13#10'以下文件更新失败:'#13#10 + FailFiles);
    end;
  finally
    SLTmp.Free;
    SLSize.Free;
    SL.Free;
    Json.Free;
    SLDate.Free;
  end;
  ThreadRetInfo.Ok := true;
end;

以下是Demo运行界面: