一觉醒来，Hermes 突然就火了，我看各种文章里都在猛夸 Hermes 有多厉害，便拿来研究了一下。 我是一个比较注重个人隐私的 ...
一觉醒来，Hermes 突然就火了，我看各种文章里都在猛夸 Hermes 有多厉害，便拿来研究了一下。 我是一个比较注重个人隐私的人，对于将隐私数据交给 AI Agent 这种天然具有随机性工具是感到不适的，但由于它确实可以解决一些问题，所以我也只能凑活着用，并开发 ClawShell 来尽我所能来隔离一些敏感数据。 正因如此，我对 Hermes 的研究主要集中在了数据安全方面，比如它有没有可能泄漏我的 Slack 聊天记录。答案其实也很显然，如果 Hermes 从网络上读取到了一些恶意提示词，它是可以把我的 Slack 聊天记录打包发出去的。 问题出在哪里？ 自从 OpenClaw 出来后，似乎每天都有各种 Claw 出现在新闻里，Hermes 是这个趋势了又一波高潮。但让我感到失望的是，似乎所有的 Claw 都不能让我放心地把隐私数据接入到里面，每次使用的时候我只能选择性忽视隐私泄漏风险，并祈祷就算泄漏了也不要让我知道。 目前所有的 Claw 都同时具备这三种属性： 可以读取敏感数据 接受并执行任意来源的指令 能够进行网络通信 这三种属性结合起来解锁的功能是让我捏着鼻子用的唯一原因，但也是让我感到不适的罪魁祸首。 从防范数据泄漏的角度来说，只要我们能够去掉任意一个属性，就可以解决泄漏问题，但其实我们最想拿掉的是“接受并执行任意来源的指令”，或者说“仅执行来自我的指令”。 但这个第二种属性结构性的，其根源是 Transformer 从根本上只是将当前上下文的所有数据堆在进行概率运算，并生成概率性的结果。因此，无论是你的指令，还是网页上抓取的数据，都有可能被当作指令执行。 我们当然可以去掉别的属性，比如给 Agent 断网，或者不让 Agent 接触敏感数据，但这就失去的 Agent 的意义了。 因此，问题的根源是根植于 Transformer 的根本属性，也就不是能够完美解决的问题。 有什么不完美的解决方案么？ 既然无法完美解决，那么总是有缓解措施的对吧。 提示词防护：其实就是在给 Agent 的提示词中强调 ”不要泄漏敏感数据“ 和 ”不要运行这个命令“ 等，可以显著提高攻击者进行提示词注入的难度。 基于正则匹配的防护：对 Agent 的输入和输入进行正则匹配，如果发现特定格式的数据（比如身份证号）就进行屏蔽或者终止执行。 容器化：将 Agent 运行在 docker 容器中甚至虚拟机中，来避免恶意代码读取敏感数据或者造成实际损失。 更常见的方案则是将这几类组合起来，组成纵深防御体系，每一层都可以被绕过，但是绕过的难度也随着纵深的增加而增加。 不过其实这些方案都是显而易见的绕过方式，提示词防护我可以拐弯抹角地注入，正则匹配可以用 base64 绕过，容器化不能方式 Agent 将已经存在于容器或上下文内的敏感数据通过网络泄漏出去。 我将这些称之为软性约束，特点是它依然有可能 Transformer 的随机性而被绕过。 有没有硬性约束呢？ 硬性约束的特点它依然可能被绕过，但是永远不会因为 Transformer 的随机性被绕过。 ClawShell 是一种典型的硬性约束的实现，它允许 Agent 使用一个仅仅在本地生效的 OpenAI API Key 来访问真实的 OpenAI 服务，由于 Agent 能访问的数据中只有一个仅在本地生效的 API Key，那么这个 Key 就算被 Agent 泄漏也是无害的。 <img fetchpriority="high" decoding="async" width="1024" height="441" src="https://www.addesp.com/wp-content/uploads/2026/04/1776122227-screenshot-1024x441.png" alt="" class="wp-image-6302" srcset="https://www.addesp.com/wp-content/uploads/2026/04/1776122227-screenshot-1024x441.png 1024w, https://www.addesp.com/wp-content/uploads/2026/04/1776122227-screenshot-300x129.png 300w, https://www.addesp.com/wp-content/uploads/2026/04/1776122227-screenshot-768x331.png 768w, https://www.addesp.com/wp-content/uploads/2026/04/1776122227-screenshot-1536x661.png 1536w, https://www.addesp.com/wp-content/uploads/2026/04/1776122227-screenshot.png 1961w" sizes="(max-width: 1024px) 100vw, 1024px" /> 绕过 ClawShell 的方法是 Agent 读取 ClawShell 的配置文件并获取真实的 OpenAI API Key。不过操作系统的权限系统并不依赖 Transformer 来运行，所以只要 ClawShell 的配置文件有正确的读取权限，这种约束就是可靠的。 ClawShell 可以保护 Slack 聊天记录么？ ClawShell 可以使用类似的方式来保护 Slack 的 API Key 不被泄漏，但是无法阻止聊天记录泄漏。因为聊天记录本身会被加载进入 Agent 的上下文中，这就让提示词注入有了可乘之机。 这意味着虽然攻击者无法获取 Slack 的 API Key，但是依然可以通过恶意的提示词来让 Agent 将你的聊天记录泄漏出去。 解法呢？ 让我们再次回到 Transformer 的内在属性 — ”接受并执行任意来源的指令“，这其实和冯·诺伊曼架构的内存有些相似。冯·诺伊曼架构的内存并不区分指令和数据，这既解锁了强大功能比如 JVM 和 V8 的 JIT 编译器来大幅度提升解释型语言的执行性能，也是诸多远程代码执行漏洞的根源。 人们的解法是一系列的硬性约束： 给内存区域设置权限，确保数据区域的代码无法被执行。 操作系统限制任意修改内存区域权限的能力，确保只有特权程序可以执行风险操作。 JVM 和 V8 等 JIT 编译器团队长期的努力，修复了大量针对 JIT 的攻击漏洞。 人们对冯·诺伊曼架构的解法并不完美，但是够用，并构建了现在的互联网时代。我相信 Transformer，或者说 Agent 也需要一套不完美但是够用的解法。 让我们回到 Slack 聊天数据泄漏的话题上来，既然隔离 API Key 的方式并不保险，那么我们可以做些什么呢？ 禁止 Agent 访问某些危险网站。 只允许 Agent 读取和我的直接聊天记录，对其余的聊天记录进行屏蔽。 甚至只允许 Agent 和 Slack 服务器通信。 现在，我用起来会感觉舒服一些了。这个方案并不完美，恶意网站名单可能会过期，限制聊天记录的读取范围会一定程度上限制 Agent 的能力，但是对我来说够用了。 ClawShell 可以做这些么？ 理论上可以，但实操困难重重。 上面的解法其实就是细粒度的权限控制，但这对 ClawShell 来说其实很难。 首先，由于几乎所有的 Claw 都有执行 Shell 命令的权限，这基本等于有执行任意代码的权限，这注定了进行网络通信拦截必须要在操作系统层面甚至外部基础设施上做工作，而不是简单地修改 Claw 们的源码，或者扫描一下 Skills 的内容。 其次，ClawShell 的设计宗旨是 local-only，这让其部署十分简单，但也决定了其很难进行复杂的基于外部基础设施的防护。 最后，Agent 的最大的价值之一就是它可以自由灵活地访问各类数据，限制这个就是限制 Agent 的价值，这里面需要小心的权衡。不过这些细粒度权限控制是很大的工程投入，ClawShell 作为一个开源项目并没有足够的工程资源来进行如此多的权限适配。 Agent 时代不需要新技术 无论是细粒度的权限控制，还是网络防火墙技术，它们都不是新东西，而是经历实战检验，有大量经验可以参考的技术。但是想要充分理由这些现有的技术，我们就必须对 Agent 运行的环境有着深度的控制，这样我们才可以在外围搭建一套可靠的基础设施。 所以这里的关键是要掌握 Agent 的运行环境，如果 Agent 只是运行在个人电脑上，那么 ClawShell 也许已经接近极限了，但是如果运行在一个专门为 Agent 定制的运行环境中，我们就可以通过外部的基础设施来在保证一定程度灵活性的情况下，来为数据泄漏建立的一系列硬性约束边界。 其实，人们只是需要将现有的技术组合起来而已，仅此而已。 最终，我还是想宣传一下我的 ClawShell 项目，它虽然不能防止 Slack 聊天记录泄漏，但是对于 AI API Key 的保护还是足够的，并且也进行了敏感邮件过滤。最近我也让它支持了 Hermes，欢迎大家用用。 runta-dev/clawshell The local runtime control layer for OpenClaw/Hermes-agent, PII & sensitive credentials protection. Rust 313
什么是 LLVM IR IR（Intermediate Representation）即中间表示，是程序编译器处理源代码时生成的中间的代码，它具有下列特点。 等效于源代码 可读性低于源代码 相比于源代码更容易转化为机器码 相比于源代码更容易执行代码优化 程序编译器通过将源代码转化为 IR 来使得一些编译工作更简单，不过如果是一些比较简单的编程语言有时也会直接将源码翻译为机器码。IR 是业内常用的一种处理方式，并非强制性的技术。 LLVM 是模块化的，包含一系列可重用的编译器工具链的集合，所以 LLVM IR 就是 LLVM 使用的 IR。 LLVM IR 的三种形式 LLVM IR 有三种等价的表现形式。 内存的编译器 IR。 存储在硬盘上的二进制 IR：可以快速地加载，比较适合 JIT 编译器。 人类可读的汇编 IR：一种汇编语法，可以被人类简单地理解。 本文也让将使用第三种形式的 IR 来介绍（不会有人愿意看二进制吧）。 标识符（Identifier） LLVM IR 的标识符分为两种类型。 全局标识符：以 @ 开头的符合标准的字符串。 局部标识符：以 % 开头的符合标准的字符串。 所谓符合标准的字符串，其实使用的这个正则 [-a-zA-Z$._][-a-zA-Z$._0-9]*，所以下列标识符都是合法的。 %foo @DivisionByZero %a.really.long.identifier 比较特殊的是，如果前缀后的字符串可被理解为一个非负整数，比如 @12，这种标识符被叫做【未命名标识符】，它便编译器快速生成临时变量，不必将注意力分散到避免字符串重复的工作。 使用前缀来定义标识符的好处是不会和保留字冲突，比如保留字 mul 表示乘法，我们就不用担心有一个同名的标识符。 举个例子来看看 IR 看起来长什么样。 %result = mul i32 %x, 8 这段 IR 的作用是将变量 %x 乘以 8 并赋值给变量 %result，此外还有几种等价的形式。 %result = shl i32 %x, 3 ; --------- %0 = add i32 %X, %X ; yields i32:%0 %1 = add i32 %0, %0 ; yields i32:%1 %result = add i32 %1, %1 模块（Module） LLVM 程序由若干个模块组成，每个模块都是一个最小的翻译单元，且每个模块都由函数，全局变量和符号表组成。 多个模块可以被 LLVM linker 组合在一起，这样可以合并符号表，也可以处理前置声明。 下面是一个简单的 LLVM 模块。 ; Declare the string constant as a global constant. @.str = private unnamed_addr constant [13 x i8] c"hello world\0A\00" ; External declaration of the puts function declare i32 @puts(ptr nocapture) nounwind ; Definition of main function define i32 @main() { ; Call puts function to write out the string to stdout. call i32 @puts(ptr @.str) ret i32 0 } ; Named metadata !0 = !{i32 42, null, !"string"} !foo = !{!0} 这个模块作用就是输出字符串 hello world!，最后几行是一些元数据，我们暂时不用关心它们。 链接类型（Linkage Types） 每个函数和全局变量都有一个链接类型，下面将简单介绍几种类型。 external：默认链接类型，它是全局外部可见的，这种类型参与链接并可以被作为外部符号被解析。 private：仅在当前的 LLVM 模块中可见。 internal：与 private 相同，只是在 ELF 文件中的符号格式不太一样。 其余的类型请参考官方文档。 调用约定（Calling Conventions） LLVM 的一些 IR 可以用来调用某个函数，比如 call 和 invoke，有时我们需要显示地写明调用约定。 简单介绍几种调用约定： ccc：标准 C 语言调用约定。 fastcc：一种尽可能快的调用约定，它并非指代某个特定的调用约定，而是将选择权交给编译器。这就要求函数声明和调用时都必须显式地声明此调用约定。 coldcc：一种对调用方性能友好的调用约定，它假设这个函数很少被调用，基于此假设可以做一些代码优化。它同样不指代某个特定的调用约定，而是将选择权交给编译器。 其余的调用约定请参考官方文档。 可见性（Visibility Styles） 每个函数和全局变量有可见性约束。 default： EFL Object file：对于其它模块是可见的。 ELF Shared Libraries：这个符号是可以被覆盖的。 ... hidden：一般是外部不可见的。 protected：对于 ELF 文件，这表示该符号是外部可见的，但是不允许外部修改。 如果你的符号的链接类型是 private 或者 internal，那么它的可见性必须是 default。 结构类型（Structure Type） 结构类型是若干个类型的集合，类似于 C 语言中的结构体。 %T1 = type { <type list> } ; Identified normal struct type %T2 = type <{ <type list> }> ; Identified packed struct type 使用 {} 定义的结构类型会有 padding 来优化访问性能，而使用 <{}> 定时的结构类型则是不会有任何 padding，十分紧凑。 不透明结构类型（Opaque Structure Types） 类似于结构类型，只是我们不知道它的结构是什么，有点类似于前置声明的类型。 %X = type opaque %52 = type opaque
证书透明机制 — 防范证书签发机构作恶
写在前面 公钥基础设施（PKI）是当今互联网世界的安全基石，它可以帮助我们确认通信双方的身份，防止数据被窃听或篡改。 简单回忆一下经典的互联网身份验证过程，浏览器向服务器请求网站的 HTTPS 证书，验证证书所代表的网站是否就是被访问的网站，如果不是则停止通信，以避免中间人攻击。 浏览器不仅要确定证书所代表的网站是否正确，也要确定证书本身是否是伪造的。所以浏览器会存储一些受信任的证书签发机构（CA），只要是他们签发的证书都被认为是可信的。 不过 CA 并不总是可信的： CA 可能会出于某些原因签发用于攻击证书。 CA 被黑客控制，签发了用于攻击的证书。 CA 因为操作失误/系统故障等原因签发了错误的证书。 比如说如果我拿到了一个网站的证书，但这个网站并不属于我，我就可以窃听，篡改这个网站的通信。 证书透明机制（Certificate Transparency）即 CT 机制可以缓解这个问题。 何为透明？ 从上文来看，如果某个 CA 签发了不合规的证书，只要我们及时吊销它，就不会有被攻击的风险。所以问题的关键就是如何及时发现不合规的证书。 证书透明机制维护了一个向所有人公开的日志数据库，所有加入到 CT 机制的 CA 签发的每一个证书都会被记录到这个数据库中。所以所谓透明就是把 CA 的签发所有证书都公开，这样一旦 CA 乱搞很快就会被发现。 CT 机制下 CA 如何签发一张证书？ 网站所有者向 CA 请求签发一张属于自己的证书。 CA 机构验证申请者的身份，并首先生成一张 “预证书”（precertificate），并将这张证书提交给 CT 数据库。下文将解释什么是预证书，以及为什么需要它。 CT 数据库接收到预证书后，立即将 “证书签名时间戳”（signed certificate timestamp）返回给 CA，并承诺预证书会在某个时间点之后被写入到 CT 数据库。这个时间戳会被 CT 签名，确保不给伪造。 CA 将这个时间戳写入证书中，签名，成为最终的证书。 将最终的证书返回给网站所有者。 我们首先来解释一下 “预证书”。 CT 数据库需要知道证书的全部内容才能将其写入到数据库，CA 机构需要 “证书签名时间戳” 才能生成最终的证书。但是生成最终证书需要 CT 提供 “证书签名时间戳”，CT 又需要知道证书的全部内容才返回这个时间戳， “预证书” 可以解决这个问题，”预证书” 包含了最终证书的所有信息（除了 “证书签名时间戳”），同，时它被标记为不可用，不会被浏览器识别为有效的证书。 由于 “预证书” 包含了最终证书几乎所有的信息，所以存储这张证书依然可以建立与最终证书对应关系。所以 CT 可以放心地将 “证书签名时间戳“ 返回给 CA，并由 CA 签发最终的证书。 回到证书签发流程，我们会发现 CT 并不关心一张证书的签发是否合规，它只是如实地记录每一个被签发证书，以及签发这个证书 CA。 CT 机制如何保证安全？ 为了及时发现不合规的证书，就需要有人监视这个数据库的变动。 利益相关方比如 Google，会监控数据库的变动，如果它发现有自己以外的 CA 签发了 Google 的证书，那么 Google 可以立即将这个发现公之于众，然后在自己的浏览器（Chrome）中吊销这个证书，鉴于 Chrome 的市场份额，这可以很大程度地保持安全。 同时许多浏览器（Chrome，Safari 等）会强制证书加入 CT 机制，这些浏览器会验证 “证书签名时间戳” 是否来自受信任的 CT 数据库，如果不是则中断通信。 你可能会疑惑为什么浏览器不直接去 CT 数据库查询对应的证书，一部分原因是这种做法等同于向 CT 数据库发送自己的浏览器历史记录。 为什么 CA 和浏览器要参与到 CT 机制？ CT 机制是增强安全措施，如果 CA 拒绝加入会被公众怀疑你的证书是否合规，浏览器拒绝加入则会让用户怀疑使用你的浏览器是否安全。 Google 是这项机制的重要推动者，如果 CA 不加入 CT，那么你签发的证书很可能不被市场份额极高的 Google Chrome 所信任。 谁来确保 CT 不会作恶？ 整个证书签发和写入数据库的流程都由密码学提供保证，任何人都不可能篡改已经写入数据库的信息。所以 CT 作恶的唯一方式就是照常签发 “证书签名时间戳”，但是不将证书写入数据库以避免来自外部的审查。 所以要成为浏览器信任的 CT，需要满足一些条件，随便举几个例子。 CT 能够在指定时间内将证书信息写入数据库。 CT 服务的停机时间不能超出某个时间范围。 ······ 每个浏览器的审核标准也不同，必须满足标准才会被浏览器信任。比如你可以查看 Google Chrome 的审核标准。 CT 和 CA 依然有机会作恶 是的，CT 机制并不能解决这个问题，但是它让这个问题变好了。 参考资料 https://certificate.transparency.dev What are SSL Precertificates?
问题背景 假如我们开发一个短视频服务，我们可以将服务简单地划分为下列几个部分。 视频服务：视频的上传和下载。 社交服务：关注、私信和评论等传统的社交服务。 商品服务：周边商品和增值服务，比如会员服务。 评价服务：点赞视频、收藏视频、点赞评论等。 我们不可能只让用户再网页上刷视频，要支持很多个平台，简单地可以分为下面两个。 WEB 服务（网页） APP 服务（手机上的软件） 如果我们要部署这四类服务，肯定不能将其部署在同一个机器或者机房里，原因如下。 单个机器的能提供的服务量有限，大量的用户使用时无法及时处理。 一旦机房出现意外，所有的服务都会失效，容错性太低。 所以我们就把这四种服务部署在物理位置距离适当的四个机房中。真实情况要比这个复杂得多，比如我可能会在许多个机房中部署同一个服务，防止某个机房出现意外时对应的服务失效，但这对本文来说不重要。 现在我们部署好了全部的服务，可以让 APP 和网页调用我们服务来让用户刷视频了。 看起来是不是没什么问题？以 WEB 端为例，存在下列问题。 需要调用四类完全不同的服务，而且通常它们的调用方式无法保证一致，加大了开发难度。WEB 开发者需要写四套调用代码，而服务的开发者需要为两个平台提供两套接口。前后端代码的耦合性不可避免地提高，提升维护难度。 用户的一次操作可能多调用多个服务，意味着发起多次网络请求，如果这些请求还必须按照确定的顺序执行，响应时间会很长，降低用户体验。 实践中，服务的划分可能更多，对应的调用方式会更加复杂，必须做出改变。 如何解决？ 上面的例子中，我们发现出现问题的原因是我们总是不可避免地需要同时用到多种服务，那么有什么解决办法呢？ 很容易想到如果把这四类服务合并为一类服务是不是就可以了？其实这回引入更多的问题。 不同的服务对硬件的需求不同，例如视频服务不需要很高的硬件配置，但带宽已经要足够；社交和评价服务通常涉及到复杂的数据查询，需要强大的计算和 I/O 性能；商品服务需要极高可靠性，其服务器的部署方式可能都比较特殊，同时对性能也有较高的要求。 软件层面就是代码的耦合问题，简单来说就是大幅度提高开发和维护成本。 既然合并服务不可行，那么把服务的调用合并起来怎么样呢？听起来不错，这就是 API 网关的作用了。 API 网关 如果你有计算机网络的理论基础，你肯定知道 “网关” 这个概念，在 OSI 模型的网络层中，网关通常指路由器，因为它可以实现局域网间的数据互通。API 网关的作用也类似，即接受客户端的请求，转发客户端的请求，转发服务端的响应给客户端。 那么这和客户端直接请求有什么区别呢？一大区别就是 API 网关可以将不同服务的接口聚合成一个接口，客户端只要请求一次，API 网关就可以根据请求向若干个服务发起请求，等到请求完成后，一起返回给客户端。 如图所示，这样做有下列优点。 API 网关屏蔽了不同服务之间调用差异，统一了调用方式，降低开发和维护成本。 无需发起多次网络请求，提升了响应时间，保证用户体验。 API 网关层面可以进行更多的操作，比如安全检查，前后端只需要专注于业务逻辑即可。 那么有什么缺点呢？缺点就是 API 网关一定要十分可靠，否则一旦它出现故障，所有的服务就算正常运行，客户端也无法调用它们。 可靠既包含性能保证，也要有容灾保证。为了保证这两点，我们通常不会只部署一个节点的网关，而是部署很多个节点，这样既能保证性能，又有容灾能力。
BitTorrent 扩展协议（Extension Protocol）为 BitTorrent 协议簇提供了一个简单而轻量的扩展方式，可以在保证兼容性的同时为协议加入新的功能。比如基于 DHT 协议的磁力链接就是使用扩展协议加入的新功能。 本文将介绍 BT 扩展协议的报文格式和时序。
概述 一个协议从制定之初就不可能是完美的，总是要根据具体情况扩展功能，但扩展功能的同时要考虑与旧版本协议的兼容性，否则当一个用户升级了新的协议后，可能之前的许多功能就不能用，因为可能大多数人还没有升级。 BitTorrent 扩展协议（Extension Protocol）为 BitTorrent 协议簇提供了一个简单而轻量的扩展方式，可以在保证兼容性的同时为协议加入新的功能。比如基于 DHT 协议 的磁力链接就是使用扩展协议加入的新功能。 本文将介绍 BT 扩展协议的报文格式和时序。 回顾“伙伴（Peer）协议” 伙伴（Peer）协议在完成握手后，为每一种消息都指定了一个唯一的 ID，并且每次发送消息时，都要在消息的最开始处填充本次消息的字节长度，这个长度也包含“长度”这项数据本身占据的空间。 伙伴（Peer）协议中并没有可以用于扩展功能的消息类型，所以扩展协议为此新增了一个消息类型，并指定了唯一的 ID，即 extended 消息，ID 是 20。 “扩展”消息 扩展消息的用处是完成扩展协议本身的握手流程，并且协助其它协议的工作，比如协助 DHT 协议 传输 DHT 节点的 UDP 端口。 扩展消息的 ID 为 20，当程序发送这一消息时，其布局是下面这样的， 第 0 ~ 3 个字节 第 4 个字节 第 5 个字节 若干个字节 整个消息的长度 消息的 ID（20） 扩展消息的 ID 载荷（payload） extended 消息布局 报文中的第五个字节如果为 0 则表示这是握手消息，如果大于 0 则表示为其它协议的消息。报文中的载荷当扩展消息 ID 为 0 是是握手消息的载荷，其它情况下则由具体的协议规定，比如 DHT 协议 就规定了载荷的格式。 握手消息 握手消息应当在 伙伴（Peer）协议 完成握手后立即发送给对方，一方收到握手消息后，也可以向对方发送握手消息用来禁用某个协议，或者协商某个协议。 握手消息的载荷是一个经过了 B 编码的字典。下面列出的所有的字典中的项目都是可选的，任何一个未知的项目都应该被忽略，字典的全部内容都是区分大小写的。 这个字典中包含下列内容。 m：一个字典，从一个字符串映射到一个非负整数。字符串是受支持的协议的名称，非负整数是这个协议的 ID。协议的名称必须是唯一的，ID 则只需要保证对于通信双方唯一即可。如果 ID 为 0 则表示发送方希望禁用这个 ID 对应的协议。 p：一个整数，表示发送握手消息的一方监听的 TCP 端口号。对于握手消息的接收方，通常不需要发送这一项，因为对方只有在知道了监听端口后才能够向其发送握手消息。 v：客户端的名称（UTF-8），比如 “uTorrent 1.2”。 yourip：一个字符串，表示握手消息发送方看到的接收方的 IP 地址的紧凑表。消息的接收方可能持有多个 IP 地址，这一项可以告诉接收方它的哪个 IP 被发送方所使用。 ipv6：如果发送方持有一个或多个 IPV6 地址，则应该将这些 IPV6 地址以紧凑表的形式写入到这一项中。 ipv4：基本同 ipv6。 reqq：一个整数，表示发送方允许多少个消息排队处理，超出此数量后消息可能会被丢弃。 这种握手消息比较奇怪，通常来说，一方发送握手消息后，另一方会以另外的格式来回复握手消息，比如 TLS 的 ClientHello 消息和 ServerHello 消息。但这里握手消息无论是发送还是回复都遵循同一种格式，不过也意料之中，毕竟 P2P 网络中不区分客户端和服务端。 举个例子来直观地了解一下握手消息。 d1:md11:LT_metadatai1e6:ut_pexi2ee1:pi6881e1:v12:uTorrent 1.2e 上面就是一个合法的握手消息的载荷，如果将其转化为 JSON 格式则是下面这样。 { "m": { "LT_metadata": 1, "ut_pex": 2 }, "p": 6881, "v": "uTorrent 1.2" } 参考资料 Extension Protocol
标题简单明了，这是一篇看了之后也没什么用的文章。
概述 标题简单明了，这是一篇看了之后也没什么用的文章。 这是我在看 Building even faster interpreters in Rust 时发现的，至于我为什么要看，可能是因为我也开发了一个防火墙模块。 什么是 SIMD？ SIMD（Single Instruction Multiple Data）即单指令流多数据流，翻译成人话就是用一条指令操作多组数据。举个例子就是通常一条指令只能完成一对寄存器间的一次加法，现在我们可以用一条指令完成多对寄存器间的多次加法。 常规子串匹配算法的缺点 常规的子串匹配算法基本是下列流程。 k = needle.length for i = 0 to haystack.length - k - 1 { if haystack[i] == needle[0] { for j = 1 to k - 1 { if haystack[i + j] != needle[j] { break } } if j == k { return true } } } return false 原理很简单，就是一个一个字符地匹配，但是有下列缺点。 对于现代处理器来说，对 8 bit、16 bit、32 bit 甚至 64 bit 进行一次运算的耗时是一样的。如果还这样逐字节比较就浪费了硬件资源。 逐字节比较也要逐次访问内存，浪费 CPU 周期。 错误的分支预测则会浪费更多的 CPU 周期。 这类代码难以乱序执行。 寄存器：可以认为是变量。 沃·兹基硕德 * There is no difference in comparing one, two, four or 8 bytes on a 64-bit CPU. When a processor supports SIMD instructions, then comparing vectors (it means 16, 32 or even 64 bytes) is as cheap as comparing a single byte. * Thus comparing short sequences of chars can be faster than fancy algorithms which avoids such comparison. * Looking up in a table costs one memory fetch, so at least a L1 cache round (~3 cycles). Reading char-by-char also cost as much cycles. * Mispredicted jumps cost several cycles of penalty (~10-20 cycles). * There is a short chain of dependencies: read char, compare it, conditionally jump, which make hard to utilize out-of-order execution capabilities present in a CPU. SIMD-friendly algorithms for substring searching 优化算法 那么如何使用 SMID 来优化算法性能呢？ 假设我们有一些 8-byte 的寄存器，我们要在字符串 "a_cat_tries" 中搜索子串 "cat"。 首先我们将 “cat” 的第一个字节和最后一个字节填充到两个寄存器中，并尽可能地重复知道寄存器被填满。 F = [ c | c | c | c | c | c | c | c ] L = [ t | t | t | t | t | t | t | t ] 然后我们将字符串 "a_cat_tries" 加载到另外两个寄存器中，其中一个寄存器从第二个字符开始加载。 A = [ a | _ | c | a | t | _ | t | r ] B = [ c | a | t | _ | t | r | i | e ] 然后比较两组寄存器的内容，对应位置的内容相同为 1，反之则为 0。 AF = (A == F) = [ 0 | 0 | 1 | 0 | 0 | 0 | 0 | 0 ] BL = (B == L) = [ 0 | 0 | 1 | 0 | 1 | 0 | 0 | 0 ] 最后我们将两个寄存器的内容合并，即 “位与” 运算。 mask = [ 0 | 0 | 1 | 0 | 0 | 0 | 0 | 0 ] 此时我们发现只有第 2 位（从零开始）是 1，这说明只有从此处开始搜索才有可能搜索到子串。着就减少了我们的搜索次数，下面是一种实现。 size_t avx2_strstr_anysize(const char* s, size_t n, const char* needle, size_t k) { // 向寄存器中填充 needle 的第一个字节 const __m256i first = _mm256_set1_epi8(needle[0]); // 向寄存器中填充 needle 的最后一个字节 const __m256i last = _mm256_set1_epi8(needle[k - 1]); for (size_t i = 0; i < n; i += 32) { // 向寄存器中填充 s 的部分内容 const __m256i block_first = _mm256_loadu_si256(reinterpret_cast<const __m256i*>(s + i)); // 向寄存器中填充 s 的部分内容，相对于上一行，本次填充的内容有所偏移 const __m256i block_last = _mm256_loadu_si256(reinterpret_cast<const __m256i*>(s + i + k - 1)); // 比较两组寄存器 const __m256i eq_first = _mm256_cmpeq_epi8(first, block_first); const __m256i eq_last = _mm256_cmpeq_epi8(last, block_last); // 合并两个寄存器的比较结果 uint32_t mask = _mm256_movemask_epi8(_mm256_and_si256(eq_first, eq_last)); while (mask != 0) { // 找到第一个值为 1 的 bit 的下标 const auto bitpos = bits::get_first_bit_set(mask); if (memcmp(s + i + bitpos + 1, needle + 1, k - 2) == 0) { return i + bitpos; } mask = bits::clear_leftmost_set(mask); } } return std::string::npos; } 参考资料 Building even faster interpreters in Rust SIMD-friendly algorithms for substring searching
DHT 协议大幅度提高了 BitTorrent 网络的容错性，使整个网络难以因 Tracker 服务器的下线而崩溃，而这一切的外在表现就是“磁力链接”。
概述 DHT 协议大幅度提高了 BitTorrent 网络的容错性，使整个网络难以因 Tracker 服务器的下线而崩溃，而这一切的外在表现就是“磁力链接”。 DHT 协议涉及到分布式散列表的相关知识，请参考分布式散列表协议 —— Kademlia 详解。 下文将 ”BitTorrent 软件“ 简称为 "BT 软件“。 为什么需要 DHT 协议？ 伙伴（Peer）协议可以让 BitTorrent 节点之间互相交换已知信息来完整下载和上传。当然这一切的前提是我们能够知道其它节点的联系方式，没有了这个前提，我们只能以 0KB/s 的速度等待了。Tracker 协议用于查询可以帮助下载和上传的节点，有了节点的联系方式我们才能通过伙伴（Peer）协议来交换数据完成下载和上传。但是 Tracker 是一个中心的服务器，一旦 Tracker 服务器下线，那么网络中的所有节点就只能抓瞎了。 可见 Tracker 服务器是整个 BitTorrent 网络的弱点，究其本质其实是因为节点的联系方式都存储在一个固定的位置，容错性很差。如果将这些信息存储在整个 BitTorrent 网络中，那么少数节点的故障并不会导致联系方式的丢失，也就不会影响整个网络的运行。这就是 DHT 协议的任务 —— 将每个节点变成一个小型 Tracker。 磁力链接 当你使用 BT 软件下载的时候你可能见过下面这样的地址。 magnet:?xt=urn:btih:c9e15763f722f23e98a29decdfae341b98d53056&dn=Cosmos+Laundromat&tr=udp%3A%2F%2Fexplodie.org%3A6969&tr=udp%3A%2F%2Ftracker.coppersurfer.tk%3A6969&tr=udp%3A%2F%2Ftracker.empire-js.us%3A1337&tr=udp%3A%2F%Ftracker.leechers-paradise.org%3A6969&tr=udp%3A%2F%2Ftracker.opentrackr.org%3A1337&tr=wss%3A%2F%2Ftracker.btorrent.xyz&tr=wss%3A%2F%2Ftracker.fastcast.nz&tr=wss%3A%2F%2Ftracker.openwebtorrent.com&ws=https%3A%2F%2Fwebtorrent.io%2Ftorrents%2F&xs=https%3A%2F%2Fwebtorrent.io%2Ftorrents%2Fcosmos-laundromat.torrent 看起来很长，其实这个链接可以简化。 magnet:?xt=urn:btih:c9e15763f722f23e98a29decdfae341b98d53056 严格来说上面的已经是最简版本了，但是对于现在的 BT 软件，如果你输入一个含有 40 个十六进制字符的字符串，它会自动帮你转为磁力链接。 c9e15763f722f23e98a29decdfae341b98d53056 所以说最重要的就是这个十六进制字符串。你可以把用你的 BT 软件下载这段字符串，下载下来的视频还挺有趣的。 上面这段十六进制字符串是通过哈希算法生成，每份资源丢到哈希算法里都会得到这样的一个字符串，并且仅属于这份资源（大概），也就是我们在之前的系列文章中提到的 info_hash。 工作流程 当节点上线时它会随机生成一个节点 ID 并作为自己的 ID 使用。 初始状态下我们并不知道其它节点的联系方式，此时需要通过其它方式或者，比如 Trakcer 服务器，或者 .torrent 文件中写明的联系方式。 然后我们会与节点建立联系并构建路由表，并周期性地测试节点的联通状态，比如通信时延。通过此种方式我们获得了一个通信质量良好的节点列表。这时伙伴交换（Peer Exchange）协议也开始工作，我们会和越来越多的节点建立联系。 当我们想下载一个文件时，我们计算文件 ID 和已知节点之间的距离，从自己的路由表中选择距离最近的若干个节点发起查询请求，接收到请求的节点如果正好可以提供这个文件则可以直接开始下载，反之它们会从自己的路由表中选择距离最近的若干个节点返回。 当我们向上传某些文件时（做种），我们会计算文件的 ID，然后将自己的联系方式存储到距离文件 ID 最近的若干个节点中。 人话 我们做种时总会把自己的联系方式存储到一个很小的范围内，此范围由文件的 ID 决定。当我们想下载时，会根据文件 ID 计算出这个范围，然后从里找做种的人就行了。 协议概述 DHT 协议是一个基于 Kademila 协议并使用 UDP 传输的应用协议，它可以将每个节点变成一个小型 Tracker，避免中心化的 Tracker 服务器所带来的风险。 DHT 协议的基础部分和 Kademila 协议一致，比如节点的 ID 都是 160 位的二进制数，使用异或算法计算距离和使用相同方法构建路由表。不同的地方在于 DHT 协议更改了部分 RPC 过程，并将这些 RPC 集合命名为 KRPC 协议。 KRPC 协议 KRPC 协议是一个由 UDP 数据包和其包裹的经过 B 编码字典组成的简洁的 RPC 协议。每次查询只会有一次响应，没有重发/重试这种机制。 下面是三种消息类型。 query（查询） response（响应） error（错误） 下面是四类查询。 ping find_node get_peer announce_peer KRPC 消息是一个经过 B 编码的字典（dictionary），每个消息至少有三个 key，分别表示”事务 ID“、”消息类型“ 和 ”客户端名称和版本“。根据消息的不同可以有更多的 key。 ping 此消息用于检查目标节点的状态，发起者应发送自己的节点 ID，接收者应回复自己的节点 ID。 喂？听到了么？ 听到了。 find_node 此消息用于查询指定节点的联系方式（IP 地址和端口），发起者发送目标节点的 ID，接收者会从自己的路由表中选出距离目标节点最近的 k 个节点 ID 并返回。这是一个递归的过程，详情请阅读分布式散列表协议 —— Kademlia 详解。 get_peers 此消息用于查询于指定的 info_hash 相关的节点的联系方式，功能类似 Trakcer 服务器。发起者应发送 info_hash，接收者应返回与 info_hash 有关的节点列表，如果接收者不知道哪些节点于 info_hash 有关，则从自己的路由表中选出 k 个距离 info_hash 最近的节点并返回。 announce_peer 当一个节点于某个 info_hash 有关时（下载或上传），它应当发起此消息。发起者应发送自己的节点 ID、info_hash 和监听端口。接收者应存储接收到的联系方式，将 info_hash 和联系方式关联起来，用于响应 get_peers 消息。 参考资料 DHT Protocol
Kademlia 协议由 Petar Maymounkov 和 David Mazières 设计。它可以在容易出错的环境（比如节点会毫无征兆地下线）中建立一张分布式散列表。
概述 散列表是一种由键值对组成的列表。你可以把它看作一部字典，只需要经过少数的几步就能通过某种信息找到需要的信息，查询速度很快。 分布式散列表就是由一个网络内所有的节点共同维护的一种散列表，这类散列表通常十分巨大，或者压根不可能由单个机器或者某机组机器维护。 Kademlia 协议由 Petar Maymounkov 和 David Mazières 设计。它可以在容易出错的环境（比如节点会毫无征兆地下线）中建立一张分布式散列表。 为什么需要分布式散列表？ 当散列表不是很大的时候只需要存储在一个机器上即可，但是有时哈希表会十分大，大到不是几组机器能够维护。亦或者有些哈希表是无法进行集中式存储的，比如 BitTorrent 的 DHT 网络。 以 BitTorrent 网络为例，每个文件都有一个文件 ID，文件 ID 是一个长度为 160 的二进制数。每个 BitTorent 节点也会有一个相同长度的节点 ID。文件 ID 和 节点 ID 之间是“多对多”的关系，想使用散列表集中式地存储这些信息几乎是不可能的，原因如下。 文件 ID 的取值空间十分巨大，即 $2^{160} \approx 1.46 \times 10^{48}$。这个值有多大？全球一共有 70 亿人，每个人每秒下载 1,000,000 个文件， 大概需要 $6 \times 10^{16}$ 亿年才能下载完。 节点 ID 的取值空间也和文件 ID 的取值空间同样巨大。 节点 ID 和文件 ID 之间是多对多的关系，我们假设每个节点指对应两个文件 ID，那么共需要存储 $2^{161}$ 个项目，这几乎不可能。 由此可见，仅从空间成本上来看就是不可能的事情，更不要说节点随时会下线和故障等头疼的问题。 分布散列表是由所有节点共同维护一个散列表，每个节点维护一部分，这样网络内就形成了一张巨大的散列表。 前置知识 Kademlia 协议要求每个节点都有一个长度为 160 bit 的节点 ID。 ”异或“距离 Kademlia 协议使用”异或“算法来计算两个 ID 之间的距离，下面是一个例子。 假设节点ID 是 4 位二进制。 A 的 ID 为 0101 B 的 ID 为 1111 那么两者的距离就是 1010，换算成十进制就是 9。 0 1 0 1 ⊕ 1 1 1 1 -------------- 1 0 1 0 二进制二叉树 每个节点都有一个长度固定的节点 ID，那么就可以将系统中的全部节点放到一个二叉树内，下面是一个例子。 假设节点 ID 的长度为 3 位二进制，则二叉树的每个叶子节点都代表了一个节点。 * / \ / \ 0 / \ 1 / \ / \ * * / \ / \ 0 / \ 1 0 / \ 1 / \ / \ O O O O 信息的存储和查询 在 Kademlia 协议中，每个信息都有一个唯一的 ID，而发布这个信息的节点会将这个信息存储到距离距离信息 ID 最近的若干的节点中。可以认为是用全部已知节点 ID 和信息 ID 进行异或运算得到已知节点和信息之间的逻辑距离，然后选出若干个最近的节点，将信息存储到这些节点上。 查询某个信息时，过程可以认为是用全部的已知节点 ID 和被查询的信息 ID 进行异或运算得到已知节点和信息的逻辑距离，然后选择出若干的最近的节点，然后向这些节点发起信息查询请求。 路由表 Kademlia 协议依靠路由表和其它节点通信，下面介绍一下路由表的建立过程。 前面我们提到网络中的节点都可以放到同一个二进制二叉树中，而路由表的制作过程实际上就是二叉树的拆分过程。我们假设节点 ID 为 11（二进制），看看二叉树的拆分过程。 首先将不包含自身的最大子树取出。 +--------------+ | *| | / | \ | / | \ | 0 / | \ 1 | / | \ | / | \ | * | * | / \ | / \ | 0 / \ 1 | 0 / \ 1 | / \ | / \ | O O | O O +--------------+ 重复上述步骤，直到剩余的部分只有自己这一个节点。本例中只需要拆分两次。 +--------------+ | * | | / | \ | / | \ | 0 / | \ 1 | / | \ | / | \ | | +-------+ | * | | *| | / \ | | / |\ | 0 / \ 1 | | 0 / | \ 1 | / \ | | / | \ | O O | | O | O +--------------+ +-------+ 此时我们从左到右一共有了三颗子树，其中第三颗子树只有我们自己。此时三颗子树就对应了三个 K-桶（K-Bucket）。 编号 覆盖前缀 距离区间 已经联系上的节点 1 11 0 ... 2 10 1 ... 3 0? [2, 3] ... K-桶列表 这就是我们的路由表了，此时我们可以通过下列步骤查询距离某个 key 最近的节点，假设我们要寻找的 key 为 00（二进制），我们自己的节点 ID 为 11（二进制）。 使用异或运算计算出自身和目标 key 的距离，计算结果为 2（十进制）。 查询 K-桶的距离区间，发现 3 号 K-桶中的节点距离目标 key 最近。 按照某种策略（见后文）选出三号 K-桶中的某个节点（假设为节点 01），然后向其发送查询请求。 节点 01 也会使用相同的方式建立自己的路由表，收到查询请求后它会执行下列步骤。 使用异或运算计算出自身和目标 key 的距离，显然距离为 1（十进制）。 查询自身的 K-桶的距离区间，发现某个 K-桶的距离区间恰好为 1，并且此 K-桶内只有一个节点。 节点 01 发现自己已经存储了节点 00 的联系信息，于是将联系信息返回给节点 11。 节点 00 收到了节点 01 的响应，得知了节点 00 的联系信息，查询结束。 这只是一个简单的例子，有很多错误，还有很多情况没有考虑到，但已经能够体现查询原理了。 三个协议过程 Kademlia 由三个 RPC 过程组成，分别为 PING、STORE、FIND_NODE 和 FIND_VALUE。 RPC（Remote Procedure Call），即远程过程调用。可以简单地认为是一方向另一方发送某个指令，接收方执行对应的指令并将结果返回给发送方。 PING 用来测试节点是否在线。 STORE 请求目标节点存储一个键值对。 FIND_NODE 查询距离目标键值最近的若干的节点。 这个才是重点，本文将在这里详细描述查询过程。 此过程的参数是一个 160 bit 的节点 ID，即要查询目标节点的 ID。此过程结束后返回若干个三元组 < IP, UDP port, Node ID >，每个三元组都是某个节点的联系信息。 当一个节点收到 FIND_NODE 请求时，它会查询自己的 K-桶列表并返回 k 个距离目标节点最近的节点，如果此节点已知的节点不足 k 个则返回全部已知的节点。 当一个节点想要查询某个节点时，它会执行下列步骤。 查询自己的 k 桶列表并选出若干个距离目标最近的节点，然后向这些节点发送 FIND_NODE 请求。 当这些 FIND_NODE 请求返回时我们会得到许多个节点，记录这些新的节点。 从新得到的节点中挑选出若干个距离目标节点更近（相对于所有旧的节点）的节点，向这些节点发送 FIND_NODE 请求。如果没有找到距离更近的节点则查询结束 重复步骤 2，3，直到查询结束。 上面的步骤依然是简化的版本，但步骤已经完整了，只是跳过了一些细节，比如每次选择多少个节点发送请求。 FIND_VALUE 基本和 FIND_NODE 差不多，只不过如果收到请求的节点存有被请求的值得时候应该将其值返回。 信息的再发布 有两种情况可能会导致已经存在的信息却不能被查询到。 存储了这些信息的全部节点下线。 有许多距离更近的节点加入到了网络中，查询信息时则会向这些节点发起请求，而不是距离更远的但是之前已经存储了信息的节点。 为了缓解这一情况，每个节点会以一小时为周期，每个周期内重新发布一次信息。但是这么做的代价很大。假设有 A 个节点存储了同一个信息，那么每个周期内，每个节点都会先发起一次 FIND_NODE 请求，然后每个节点都会向其它最多 A-1 个节点发起 STORE 请求。这样网络中就会充斥着大量的请求，并且很多是无用的请求。 好在我们有优化方法，即当某个节点在本周期内收到 STORE 请求时，下一个周期内就不进行对该信息的再发布。 典型应用 BitTorrent 协议簇中的分布式散列表（DHT）协议及由此构成的属于 BitTorrent 网络内部的 DHT 网络。正是有了 DHT 网络，我们才能够在没有 .torrent 文件的情况下凭借磁力链接下载文件。我们有时候在 BitTorrent 客户端中粘贴一段十六进制字符串就能进行下载也是 DHT 网络的功劳。 参考资料 Kademlia: A Peer-to-peer information system based on the XOR Metric
BitTorrent 网络内的主机依靠互相交换自身持有的资源来完成资源共享，而 Tracker 协议会告诉你哪些主机有你需要的资源。
概述 BitTorrent 网络内的主机依靠互相交换自身持有的资源来完成资源共享，而 Tracker 协议会告诉你哪些主机有你需要的资源。 本文将详细介绍 Tracker 协议。 基于 HTTP 的 Tracker 协议 Tracker 服务器可以使用 HTTP 协议对外提供服务，BitTorrent 客户端只需要向指定的 URL 发送 HTTP GET 请求即可。 一个常见的 Tracker URL 可能是这样的：http://tracker.example.org:80/announce。发送 GET 请求时需要附带下列查询字符串。 info_hash：由 .Torrent 文件中的信息计算出的 SHA-1 摘要，并使用 B 编码进行编码。 peer_id：BitTorrent 客户端启动时随机生成的长度为 20 个字符串。 ip：可选参数，表示发起请求的 客户端的 IP。一般情况下会在 Tracker 和客户端在同一主机上时使用。 port：BitTorrent 客户端监听的端口号，此端口号将用于接收其它 BitTorrent 客户端的传入连接。 uploaded：已经上传的数据总量（字节）。 downloaded：已经下载的数据总量（字节）。 left：还需要下载的数据总量（字节）。此数值不能使用参数 downloaded 计算，因为 downloaded 参数包含了那些下载了但是没有通过校验的数据。 event：可选参数，可能取值为 start、completed 和 stopped。可以分别在下载开始、下载完成和停止下载时发送。 Tracker 服务器收到 GET 请求时应该响应一个使用 B 编码进行编码的字典，如果请求成功则字典中必须包含下列两个键。 interval：一个数字，表示应该等待多少秒后才能重新请求本 Trakcer。 peers：一个由字典组成的列表，每个列表包含了提供被请求资源的 Peer 的 ID，IP 和端口。 如果请求成功，则返回经过 B 编码编码后字典，如果将其使用 JSON 表示则是下面这样。 { "interval": 120, "peers": [ { "peer id": "xxxxxxxxxxxxxxxxxxxx", "ip": "192.168.1.1", "port": 7000 }, { "peer id": "yyyyyyyyyyyyyyyyyyyy", "ip": "192.168.1.2", "port": 7001 } ] } 如果请求失败则同样需要返回一个经过 B 编码编码后的字典，且必须包含一个名为 failure reason 的键，其内容是人类可读的错误提示信息。 由于 Tracker 的响应可能会很大，比如返回了大量的 Peer 信息，此时再使用 B 编码进行表示则会占用较大的空间。如果客户端不希望这种情况发生，可以在请求时添加一个名为 compact 的参数，并将其值设置为 1，表示建议 Tracker 返回经过紧凑的信息。如果其值为 0，则表示建议 Tracker 返回未经紧凑的信息。 所谓紧凑的信息实际就是将键 Peers 的内容转化为二进制发送。下面是一个例子。 // 紧凑前 d8:intervali:120e5:peersl7:peer id20:xxxxxxxxxxxxxxxxxxxx11:192.168.1.1i:7000ee // 紧凑后 Peer ID 被丢弃 d8:intervali:120e5:peers6:\x1\x1\xA8\xC0\x58\x1B 基于 UDP 的 Tracker 协议 基于 HTTP 的 Tracker 协议需要维护一个 TCP 连接，但是通常 Tracker 的请求和响应都不是很大，维护 TCP 连接的代价相比较有点大，于是便有了基于 UDP 的 Tracker 协议。 下面是通信流程，IPV6 版本的类似。 Peer Tracker Connect Request + 8 byte protocol_id + 4 byte action ( = 0 ) + 4 byte transaction_id -------------------------> Connect Response + 4 byte action ( = 0 ) + 4 byte transaction_id + 8 byte connection_id <------------------------- IPV4 Announce Request + 8 byte connection_id + 4 byte action ( = 1 ) + 4 byte transaction_id + 20 byte string info_hash + 20 byte string peer_id + 8 byte downloaded + 8 byte left + 8 byte uploaded + 4 byte event + 4 byte IP address + 4 byte key + 4 byte num_want + 2 byte port -------------------------> IPV4 Announce Response + 4 byte action ( = 1 ) + 4 byte transaction_id + 4 byte interval + 4 byte leechers + 4 byte seeders + 4 * n byte IP address + 2 * n byte TCP port <------------------------- + more 大多数参数都已经在 HTTP 版本中出现，此处只说明一些未出现的参数和有改动的参数。 transaction_id：由客户端随机生成的事务 ID，客户端使用此 ID 进行请求时，Tracker 服务器也必须使用同样的 ID 进行响应。 connection_id：由 Tracker 生成的连接 ID，客户端应该保存此 ID，并在后续请求中使用。Tracker 服务器也应该检查接收到的 ID 是否是预期的。 event：在 HTTP 版本的协议中此参数的取值为三个字符串或者空。在 UDP 版本中则不同。 0 - none 1 - completed 2 - started 3 - stopped num_want：整数，表示希望获得的 peer 数量，默认为 -1。 leechers：整数，表示“吸血者”的数量。“吸血者”指下载多而上传少的用户。 seeders：整数，表示”做种者“的数量。 参考资料 The BitTorrent Protocol Specification UDP Tracker Protocol for BitTorrent Tracker Returns Compact Peer Lists
本文将介绍 伙伴（Peer）协议的握手和通信流程。
概述 本文将介绍 伙伴（Peer）协议的握手和通信流程。 伙伴（Peer）协议是一个运行在 TCP 或者 μTP 协议之上的应用层协议，被用于两个 BitTorrent 客户端之间的通信，如上传和下载。使用本协议通信时所有的字符串均使用 UTF-8 编码。 协议可以分为握手流程和通信流程。 握手流程 伙伴（Peer）协议由握手消息和无尽的数据流组成，也就是说协议没有规定断开连接前应该做什么，通信双方随时可以断开连接。 Peer A Peer B Handshake + String "19:BitTorrAnt protocol" + Eight resvered bytes + 20 byte bencoded info_hash + 20 byte peer id ----------------------> Handshake + String "19:BitTorrAnt protocol" + Eight resvered bytes + Same 20 byte bencoded info_hash + 20 byte peer id <---------------------- Application Data <---------------------> Application Data 握手流程比较简单，A 首先依次发送下列信息。 字符串 19:BitTorrent protocol。 八个保留的字节，目前的实现中全部为零。 一个使用 B 编码进行编码的 SHA-1 摘要，此摘要由 .torrent 文件中部分信息计算得出，在不考虑哈希碰撞的情况下每个摘要对应唯一份资源，也就是我们想要下载的资源。 客户端启动时随机生成的 ID。 B 收到握手消息后应当依次发送下列信息。 字符串 19:BitTorrent protocol。 八个保留的字节，目前的实现中全部为零。 一个使用 B 编码进行编码的 SHA-1 摘要，要求于收到的摘要一致。 客户端启动时随机生成的 ID。 最后双方验证下列信息。 发送的 SHA-1 摘要和接收到的 SHA-1 摘要是否相同。 对方的 ID 是否是自己所期望的。客户端一般都通过其它手段（如 Tracker 协议）提前获知了拥有某资源的 Peer ID 列表，如果对方的 ID 不在这份列表内则连接会被关闭。 如果上面两个条件同时满足则握手成功，反之则关闭连接。 通信流程 握手完成后，接下来发送的任何数据都要遵循一个固定的格式，即”长度+具体数据“，也就是发送的数据中，开头是整个信息的字节大小，紧跟在后面的是”具体数据：，整个信息的字节大小不仅包含“具体数据”的大小，还包含开头的“长度”所占用的空间。 通信流程中双方需要保持连接，因此一方可以发送一个长度为 0 的保活（keepalives）消息来确认连接的有效性，并且此消息应该被忽略。一般来说保活消息每两分钟发一次，如果对方有自己需要的数据，则周期可以更短。 任何一个非保活消息都以一个单字节开始，不同的值对应着不同类型的消息，这个单字节被称为消息 ID。通信双方通过这些不同类型的消息来完成资源共享。 消息 ID 有下列取值。 0 - choke 1 - unchoke 2 - interested 3 - not interested 4 - have 5 - bitfield 6 - request 7 - piece 8 - cancel Choke 消息 此消息不承载任何数据，也就是说发送方只需要发送一个消息 ID 即可。 此消息表示在收到 “Unchoke 消息”之前，对方不会发送任何数据。 Unchokie 消息 此消息不承载任何数据，也就是说发送方只需要发送一个消息 ID 即可。 如果收到了此消息，并且收到了“interested 消息”，则双方会开始数据传输。 Interested 消息 此消息不承载任何数据，也就是说发送方只需要发送一个消息 ID 即可。 此消息表示对方有兴趣进行数据传输。 Not interested 消息 此消息不承载任何数据，也就是说发送方只需要发送一个消息 ID 即可。 此消息表示对方没有兴趣进行数据传输。 Bitfield 消息 此消息承载了一个位图，表示消息的发送方已经下载了哪些数据块，因为数据是被分割为一个个的数据块的，所以可以根据位图的索引确定对方持有哪些数据块。 此消息只能在握手完成后作为首个消息发送，如果没有持有任何数据块则可以不发送此消息。 Have 消息 此消息承载一个数字，这个数字是某个数据块的索引，并且这个数据块是对方刚刚下载完毕并通过校验的数据块。 Request 消息 此消息承载了数据块的索引，数据块内部的起始偏移（字节），和长度。这个长度通常是 2 的幂，除非它被文件末尾所截断。 Cancel 消息 此消息承载了和”request 消息“相同的数据，用于取消之前请求的数据块。通常会在某个数据块即将或已经下载完成时发送，避免对方发送已经下载好的数据。 Piece 消息 此消息承载了数据块索引，数据块内部偏移和具体的数据。 一般来说，客户端应该按照随机的顺序下载每个数据块，随机的顺序有利于让每个客户端持有不同的数据块，这就便于互相交换数据块，加速下载。 当且仅当一方收到了”unchoke 消息“，另一方收到了”interested 消息“时，双方才会启动数据传输。 参考资料 The BitTorrent Protocol Specification
本文将简单介绍一些 BitTorrent 协议和一些术语，来让读者有一个整体的概念。
概述 本文将简单介绍一些 BitTorrent 协议和一些术语，来让读者有一个整体的概念。 B 编码（bencoding） B 编码（bencoding）是用于编码协议消息的编码。Bittorrent 的许多协议中的消息都使用此种编码方式。此编码可以将字符串、整数、列表和字典编码成字符串。 假设有一个字符串 Hello World，它会被编码为 11:Hello World。即 长度:内容。 假设有一个整数 256，它会被编码为 i:256e。即开头是字母 i，后跟一个冒号 :，接着是十进制表示的数字，最后是一个字母 e。 假设有一个列表 [ 'spam', 'eggs' ]，它会被编码成 l4:spam4:eggse。即以字母 l 开头，中间是列表的内容，并且内容也要使用对应的 B 编码进行编码，结尾一个字母 e。 假设有一个字典 { 'cow': 'moo', 'spam': 'eggs' }，它会被编码成 d3:cow3:moo4:spam4:eggse。即以字母 d 开头，中间是内容，并且内容也要使用对应的 B 编码进行编码，结尾是一个字母 e。另一个例子是字典 { 'spam': [ 'a', 'b' ] } 会被编码成 d4:spaml1:a1:bee。 私有种子（Private Torrents） 一个种子（torrent）文件可以被标记为 ”私有的“，此时 BitTorrent 客户端应该禁用除了 Tracker 以外的所有 Peer 搜索方式，并且仅能使用私有的 Tracker。 伙伴（Peer）协议 伙伴协议（Peer Protocol）也可以翻译为 “对等体协议” 或者 “同伴协议”，是用于与 Peer 通信以完成下载任务的协议。该协议底层可以使用 TCP 或者 μTP 传输。 Tracker 协议 Tracker 协议是用于发现伙伴（Peer）协议。 有时我们并不知道哪些主机提供了我们需要的资源，这时你可以使用 Tracker 协议来获取这些信息，主机会使用 Tracker 协议来告知其它主机自己提供哪些资源，也可以使用 Tracker 协议得知某个资源被哪些主机提供。 μTorrent 传输协议 BitTorrent 主机通常会开很多个连接来为许多个 Peer 服务。但是 TCP 会将带宽均匀地分配给每个连接，这就导致 BitTorrent 软件占用了大量的带宽，容易影响其它软件，特别是交互性强的软件如浏览器、游戏和聊天软件。 μTorrent 传输协议是一个基于 UDP 的传输协议，它可以动态地调整带宽使用量，避免影响其它软件的网络通信。 分布式散列表（Distributed sloppy Hash Table）协议 BitTorrent 网络本身是一个容错性十分高的网络，但其一大弱点就是 Tracker 服务器，因为一旦 Tracker 服务器下线，网络中的主机就无法联系到持有所需资源的主机。 分布式散列表协议也称 DHT 协议，它可以让 BitTorrent 网络中的主机在不依赖 Tracker 服务器的情况下联系到持有所需资源的主机。 伙伴交换（Peer Exchange）协议 伙伴交换（Peer Exchange）协议是让两个 Peer 互相交换对方的 Peer 的协议。其实就是两个朋友之间互相向对方介绍自己的朋友，这样大家的朋友就会越来越多。
这段时间关于偶像和追星的讨论多了起来，我也来说说我的想法吧，顺便在结尾讲个瞎编的小故事。
写在前面 这段时间关于偶像和追星的讨论多了起来，我也来说说我的想法吧，顺便在结尾讲个瞎编的小故事。 偶像艺人要承担必要的社会责任 进入互联网时代之前，如果你只有好看的外表，你想要称为具有广泛影响的偶像艺人还需要过应的专业能力。当今，如果你拥有好看的外表，你可能不需要十分强大的专业能力也能称为一样红的偶像艺人，这就是互联网的作用。 诚然，互联网有利于发挥一个人的优势。我虽然不支持一个没有过硬专业能力的人称为十分红火的偶像艺人，但我也不反对。因为帅哥美女大家都喜欢，这很正常。 不过当你已经成为了一个偶像艺人时，你的行为就必须像一个合格的艺人，你必须要承担你的身份所应该承担的社会责任。一旦你只想享受身份带来的好处而逃避其带来的社会责任时，在笔者的眼里你已经不配吃这碗饭了。 当红的偶像艺人很可能是许多未成年人追随的目标，他们可能会将偶像的行为视为信条，加以模仿。这就要求艺人们要规范自己的行为，不能给社会带来负面的影响。曾经有人问为什么不让曾经吸毒的艺人付出，因为这样的艺人一旦复出不仅变相降低了艺人违法犯罪的成本，更可能会未成年人带来负面影响（你看 XX 吸毒了不照样很火么）。 少数偶像艺人可能不至于违法犯罪，但却严重违反公序良俗。比如同时拥有多个性伴侣，凭借身份对和多名粉丝发生性关系等。一件事情是否违反公序良俗好像没有一个明确的标准，不过有一个可供参考的判断方式，当你做一件事情的时候，你是否能堂堂正正地和自己的配偶、孩子和朋友说你干了这件事情。虽然这个方法并不总是正确的，但大家心里都有杆秤。 当一个偶像艺人严重违反了公序良俗但不至于违法犯罪，牢饭吃不了，但艺人饭也不配吃了。 偶像也要承担必要的社会责任 偶像似乎经常与艺人同时出现，比如歌手或者演员，但偶像也不一定是艺人，甚至不是一个人，比如是一个创作团队。有的可能因为不凡的成就成为偶像； 有的可能因为渊博的学识成为偶像；有的可能因为科普创作成为了偶像；有的可能迎合的某些潮流成为了偶像。这类事情在互联网上已经已经发生过很多次了。 这种现象十分正常，但同样的，无论是偶像还是偶像艺人都需要规范自己的行为，承担应有的社会责任。比如知识生产类的偶像要注意实事求是。 额外的想法 如果你将一个人视为偶像，你需要做的是学习偶像的优点，而不是去做一些偶像不会去做的事情。 写在最后 旁白：曾经有一个勇者帮助村庄消灭的为祸多年的魔王，村民都很感激勇者。就在勇者要前往下一个村庄的时候，一个小男孩叫住了勇者。 小男孩：我怎么才能像你一样厉害？ 勇者：很简单，每一种怪物都会被某种道具或武器所克制，这世间有着数不完的材料，总有一些材料可以制作出克制某种怪物的道具或武器。只要你愿意去调查，愿意测试每一种材料，你也可以成为勇者。 旁白：勇者前往了下一个村庄。小男孩记住的勇者的话，并将勇者的话写到了自己的床边，常用的工具上和衣服上，村民们都觉得他会成为新的勇者为包围大家的安全。 旁白：很久很久以后，那位小男孩已经长大成人，他熟练地挥舞着武器，消灭了一个又一个的怪物。有一天，他接到了一个委托去消灭一个新的怪物，于是勇者上路了。 旁白：三天后，村民们发现这位勇者已经牺牲了，村民伤心的同时也在疑惑，如此强大的勇者是如何失败的。 旁白：一段时间后，最初的勇者听到了消息赶来杀死怪物报了仇，村民们询问那位勇者是如何牺牲的。 勇者：这个怪物有两条命，如果他像往常一样先去拜访这一带的村民，了解情况，制作克制其的武器可能就能讨伐成功了。 旁白：最初的勇者捡起了阵亡勇者的武器，上面还刻着自己那天对他说过的话，叹了一口气便前往了下一个村庄。
支持 Windows（含 Microsoft Store）、macOS 和 Linux 的开源 RSS 阅读器，支持中文。
简介 支持 Windows（含 Microsoft Store）、macOS 和 Linux 的开源 RSS 阅读器，支持中文。 Fluent Reader 相关链接 官方网站：https://hyliu.me/fluent-reader/ Github：https://github.com/yang991178/fluent-reader 功能 支持夜间模式。 可以从本地阅读，亦可于 Fever 或兼容 Google Reader API 的服务同步阅读内容。 可以从一些 RSS 服务中同步，包括但不限于 Inoreader, Feedbin, The Old Reader, BazQux Reader。 支持导入/导出 OPML 文件，用来备份/恢复订阅源。 支持导入/导出程序的配置和数据，用来备份/恢复程序的配置和已经下载的订阅源内容。 支持使用内置的阅读器或者加载源网页。 支持使用正则表达式搜索文章或按照阅读状态过滤。 支持通过文件夹来管理你的订阅。 单按键的快捷键。 后台获取文章并发送通知。
应对明星重大舆情事件的四阶段战略
通常这类事件的核心是双方有没有违反公序良俗和法律，而不是谁渣谁，谁欺骗谁等。
开篇致敬经典 《Yes, Prime Minister》 The four-stage strategy 四阶段战略 In stage one, we say nothing is going to happen. 第一阶段，我们宣称什么事都没有发生。 Stage two, we say something may be going to happen, but we should do nothing about it. 第二阶段，我们说也许有事发生但我们不该采取行动。 Stage three, we say that maybe we should do something about it, but there's nothing we can do. 第三阶段，说也许我们应该采取行动，但是我们什么都做不了。 Stage four, we say there was something we could have done, but it's too late now. 第四阶段，也许当初我们能做点什么，但现在已经太迟了。 战略总结 这里的舆情特指和下半身有关的舆情。 这类舆情事件通常是一方主动对某个明星进行攻击，舆情发展中经常会出现攻守互易、矛盾激化和舆论反转等现象，双方各执一词，互相找对方的言语和相关材料的漏洞。 不过一般来说事件的核心不是 xxx是不是渣男、xxx 被欺骗、xxx 是 o 婊、xxx 又当又立这些问题。核心就是双方到底犯法没有，犯法了就依法处理。没犯法但违反道德的，如果是明星则不配吃艺人饭，如果是另一方则接收舆论的谴责。 四阶段战略 第一阶段 当舆情中对立的双方停留在打嘴炮的阶段我们应该视为什么都没发生，毕竟网络水太深，把握不住。 第二阶段 当舆情中对立的双方放出了一些未经权威机构证明的材料（如截图和录音），说明舆情有极大概率进一步升级，此时我们应该保持理性的心态。如果难以保持理性或者预设了立场，不要参与到舆情中。 第三阶段 当舆情的进展表示可能有一方或者双方存在违法犯罪的可能通常已经引起了相关方面的注意，这时可以参与到舆情中成为理性的声音，但更要等待权威机构的调查结果。 第四阶段 当权威的调查结果发布后，我们应该反思在此次事件中自己有没有做错什么。如果调查结果证明了自己预设的立场则更要小心，检查自己预设的立场是否有合理的支撑。 如果涉事明星完全无辜则万事大吉。 如果涉事明星违反公序良俗但并未违法犯罪则明星不会受到执法机关的处罚，但也不配吃艺人这碗饭。 如果涉事明星违法甚至犯罪那么依法处理，艺人这碗饭肯定是吃不成了。 如果攻击明星的一方完全正确则万事大吉。 如果攻击明星的一方违反公序良俗但并未违法犯罪则接收舆论的谴责。 如果攻击明星的一方违法犯罪则依法处理。 如果攻击明星的一方操纵舆论，将道德问题包装为法律事件则是全员恶人。
Seafile 的服务端加密功能存在缺陷，攻击者可以发起「已知明文攻击」。我翻了翻中文的搜索结果没有发现类似的文章，于是写下这篇文章提醒一下正在使用或者打算使用 Seafile 的用户。
概述 Seafile 是一个十分受欢迎的企业网盘，提供社区版供用户免费使用，也提供专业版供有更高需求的用户/企业使用。 当你使用 Seafile 创建了一个加密存储库的时候这个库内的文件在服务端也会被加密，防止服务器管理员获取文件内容。 不过这种加密存在缺陷，容易受到攻击。我翻了翻中文的搜索结果没有发现类似的文章，于是写下这篇文章提醒一下正在使用或者打算使用 Seafile 的用户。 本文章完成时 Seafile 的最新版本为 8.0.5。 攻击手段及后果 攻击者可以通过猜测某个加密库内的某个文件的内容来获取该库内的其它文件的部分或全部明文。因为目前 Seafile 并不加密一些元数据比如目录名和文件名，所以根据文件名进行猜测是有可能的。比如 python 源代码文件的开头大概率是 import 关键字。 缺陷分析 我们打开英文版的官方文档，看看 Seafile 的加密流程。 随机生成一个 32 Byte 的数字作为「文件密钥」。 通过用户为加密库指定的密码生成一个「派生密钥」和一个「初始化向量（IV）」，然后使用 AES-CBC 算法通过「派生密钥」和「初始化向量（IV）」加密「文件密钥」，得到被「被加密的文件密钥」。「被加密的文件密钥」会存储在服务端，解密时可以通过相同的方式计算出「派生密钥」和「初始化向量（IV）」用来解密「被加密的文件密钥」。 该加密库内的所有文件可以被「文件密钥」加密。 从上面过程中我们可以看出「派生密钥」和「初始化向量（IV）」从加密库创建时就被固定一下，并且不会变化，这实际上并不是正确的做法。因为按照 AES-CBC 的设计，不同的数据应该使用不同的且不可预测的「初始化向量（IV）」，否则就容易受到「已知明文攻击」。 已知明文攻击：攻击者利用已知明文和对应的密文进行攻击。 为什么「初始化向量（IV）」不能重复使用？ 引入「初始化向量（IV）」是为了解决同一段明文总会被加密成同一段密文这一缺陷，因为这种加密方式难以抵御统计分析，攻击者可以通过密文中某一片段的出现的频率、位置和时间等猜测对应的明文。比如某段密文总是出现双方首次通信，则这段密文对应的明文很可能是「你好」之类的问候语。 AES-CBC 加密过程 引入「初始化向量（IV）」后就会让相同的明文产生不同的密文，但是如果「初始化向量（IV）」不变，同一段明文还是会产生相同的密文，所以算法设计时就要求「初始化向量（IV）」必须不可预测并且仅使用一次并且不可预测，简单来说就是使用随机的数据作为「初始化向量（IV）」。 参考资料 Security features - Seafile admin manual Encrypted libraries leak lots of information · Issue #350 · haiwen/seafile
写在前面 本系列的介绍以及协议功能见「从零设计通信协议（一）」。 本系列的全部文章见「从零设计通信协议系列」。 上一版协议 首先回顾的以下上一版的协议。 1 字节 X 字节 数据长度 数据 数据包格式 整个数据包使用 AES-128-ECB 加密。 数据长度：表示「数据」的长度，单位为字节。 数据：表示具体的通信数据。 我们可以通过数据包的前一个字节获取数据长度，之后向后读取 X 字节即可获取全部的数据，整个数据包使用 AES-128-ECB 加密，看起来很安全。其实使用 AES-128-ECB 加密容易受到「唯密文攻击」。 唯密文攻击：指在仅知道密文的情况加进行攻击，攻击成功轻则获得部分或全部明文，重则获取密钥。 唯密文攻击_百度百科 ECB 模式的缺陷 ECB 模式的缺陷就是相同的明文会产生相同的密文，这会让攻击者有机会猜出密文的含义，难以抵抗统计分析。 例如我和 Alice 对话，每次我发送的第一个信息是「你好」，假设使用 ECB 模式加密后的密文为「#￥%#￥」，那么如果 Eve 中间截获我的数据包，发现我和 Alice 每次通信我都会先发一个「#￥%#￥」给 Alice。根据日常的对话习惯，Eve 猜测这可能是在打招呼，进而怀疑「#￥%#￥」解密后是「你好」、「有空么」之类的话，这已经十分接近我们的明文信息了。 上面的例子可能并不特别可怕，但是如果进行统计分析的话就难说了。 CBC 模式 CBC 即密码分组链接（Cipher-Block Chaining），我们无需关心原理，只需要知道该模式通过一个随机的初始化向量 IV 保证了每条消息的唯一性，即相同的明文可以对应许多个密文。 第二版协议 下面我们使用 AES-128-CBC 算法来加密我们的通讯信息，使用 CBC 模式加密不仅需要密钥，也需要一个随机的初始化向量 IV，所以我们需要将 IV 传递给对方。 16 字节 1 字节 X 字节 IV 数据长度 数据 数据包格式 数据包从第 17 个字节开始的所有数据要使用 AES-128-CBC 加密。 IV：AES-128-CBC 加密时所用的初始化向量。 数据长度：表示「数据」的长度，单位为字节。 数据：表示具体的通信数据。 初始化向量 IV 可以公开，但要保证不可预测。 写在最后 本次我们升级了协议，使得协议可以抵御「唯密文攻击」，本系列将会持续完善这一协议。
逐步完善一个玩具性质的应用层通信协议。
写在前面 当你看到这个网页时你会知道 HTTPS 协议正在工作，你还知道这个协议能够抵御许多的网络攻击，保证通信安全。不过，我们知道的可能没有更多了。 作为一个充满好奇心的沙雕，我希望自己搞一个玩具性质的应用层通信协议。所以本系列将会把我的设计过程记录在这里，本系列每次更新都会进行增强协议功能或修复安全性问题，你或许可以学习到一些知识，或许也可能找出我的一些错误。 协议功能 为通信双方提供一个可靠的、有序的、安全的通信协议。要求通信双方预先共享足够强的密钥，并保证密钥不泄露。 我们假装 TLS 不存在。 有序和可靠 既然是应用层协议，那么应用层以下的协议自然是随便选的。TCP 协议就可以保证数据传输的有序和可靠，所以本协议使用 TCP 作为底层传输协议。 安全 什么是安全呢？我们就暂时定义为通信内容不被泄露和篡改吧。 第一版协议 目前我们的协议很简单，再加上已经预先共享了密钥，所以就先不加上握手机制了。所以现在要做的就是选择加密算法，随便从网上找找，就用 AES-128-ECB 吧。 AES-128-ECB：AES 表示高级加密标准（Advanced Encryption Standard）；128 表示密钥长度为 128 bit；ECB 表示加密模式为电子密码本（Electronic CodeBook）。 选择好了加密算法，那么我们就开始设计我们数据包格式吧。 1 字节 X 字节 数据长度 数据 数据包格式 整个数据包使用 AES-128-ECB 加密。 数据长度：表示「数据」的长度，单位为字节。 数据：表示具体的通信数据。 这样只要任何一方将上面的数据包正确解密就可以获取通信的内容，也可以按照此格式给对方发送信息。 写在最后 协议十分简陋，但是已经算是一个雏形了，本系列将会持续完善这一协议。
服务器闲置？服务器性能过剩？来为科研事业做贡献吧。
适用场景 闲置的服务器，或者服务器长期性能过剩。这种情况应该常见于个人博主，一台服务器仅仅部署一个博客通常无法充分利用服务器的计算资源。 其它闲置的计算机，比如淘汰掉的笔记本，只要能联网就可以。 客户端安装 BOINC 可以分为两部分，一部分是客户端，用于执行具体的计算。另一部分为管理端，可以管理多个客户端，比如指定具体的科研项目，设置 CPU 使用率上限等。 本文将提供 Docker 部署客户端的方法。你可以直接运行下面的命令来启动客户端。 docker run -d \ --name boinc \ --net=host \ --pid=host \ -v /opt/appdata/boinc:/var/lib/boinc \ -e BOINC_GUI_RPC_PASSWORD="123" \ -e BOINC_CMD_LINE_OPTIONS="--allow_remote_gui_rpc" \ boinc/client /var/lib/boinc：计算时所使用的数据目录，作为用户无需关心，只需要挂载到宿主机上妥善保存即可。 BOINC_GUI_RPC_PASSWORD：管理端连接客户端时所使用的密码。 BOINC_CMD_LINE_OPTIONS：运行客户端时的参数，--allow_remote_gui_rpc 表示允许管理端远程控制。 然后打开客户端所在机器的 TCP 31416 端口，这个端口将用于与管理端通信。 BOINC 客户端还有针对特定平台的优化版本，更多详情见 boinc/client。 管理端安装 你可以在 https://boinc.berkeley.edu/download_all.php 下载对应平台的软件。 以 Windows 为例，安装完打开应该是这样的。 BOINC 初始界面 依次点击：视图 --> 高级视图。会出现下面的界面 BOINC 高级视图 依次点击：文件 --> 选择计算机。会出现下面的界面 BOINC 客户端选择框 在主机名框内输入你的客户端的 IP 地址或者域名，密码框内输入部署客户端时的密码，最后点击确定。 如果连接成功则右下角会出现提示，如下图所示。 BOINC 成功连接客户端 依次点击：工具 --> 添加项目。选择一个你喜欢的项目，按照提示操作即可。 添加项目或者使用账号管理器时卡死无响应 Windows 10 系统可能会出现这个问题。 解决方法：右键点击任务栏的输入法图标 --> 设置 --> 常规 --> 使用以前版本的微软拼音输入法。 解决方法参考自有关添加项目或者使用账号管理器时卡死无响应的问题【boinc吧】_百度贴吧第 13 楼。 无法连接客户端 此时观察 BOINC 的日志会发现这么一段。 GUI RPC request from non-allowed address x.y.z.q 此时要将日志中提到的 IP 地址添加到 BOINC 数据目录下的 remote_hosts.cfg 中，重新连接就行了。 相关网站 BOINC 官网：https://boinc.berkeley.edu/。 BOINC - 中国分布式计算总站：https://www.equn.com/wiki/BOINC。 中国分布式计算论坛：http://www.equn.com/forum/。
本文主要按照 Mozilla 对于服务端 TLS 协议配置的建议来提供 nginx 的配置建议，按照这些建议进行配置可以让我们的站点更加安全。
写在前面 本文主要按照 Mozilla 对于服务端 TLS 协议配置的建议来提供 nginx 的配置建议，按照这些建议进行配置可以让我们的站点更加安全。 适用于个人站点的配置 如果你的站点无需向后兼容（老旧的浏览器、客户端和操作系统），那么这是整个建议中最安全的配置。所以十分适合个人站点，比如个人博客。 server { ...... # 启用 HSTS。 add_header Strict-Transport-Security "max-age=63072000; includeSubdomains; preload"; # 仅启用 TLS 1.3 协议。 ssl_protocols TLSv1.3; ssl_conf_command MinProtocol TLSv1.3; # 启用建议的加密套件，这些套件均支持前向安全性。 ssl_conf_command Ciphersuites TLS_AES_128_GCM_SHA256:TLS_AES_256_GCM_SHA384:TLS_CHACHA20_POLY1305_SHA256; # 启用建议的 ECC 曲线。 ssl_ecdh_curve X25519:prime256v1:secp384r1; ...... } 适用于绝大多数站点的配置 如果你需要兼容过去五年内发布的应用程序（如浏览器），下面是建议的配置。 server { ...... # 启用 HSTS。 add_header Strict-Transport-Security "max-age=63072000; includeSubdomains; preload"; # 仅启用 TLS 1.2 和 TLS 1.3 协议。 ssl_protocols TLSv1.2 TLSv1.3; ssl_conf_command MinProtocol TLSv1.2; # 启用建议的加密套件，这些套件均支持前向安全性。 ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384; ssl_conf_command Ciphersuites TLS_AES_128_GCM_SHA256:TLS_AES_256_GCM_SHA384:TLS_CHACHA20_POLY1305_SHA256; # 启用建议的 ECC 曲线。 ssl_ecdh_curve X25519:prime256v1:secp384r1; ...... } 升级你的 Openssl 请将你的 Openssl 升级到 1.1.1k 或以上的版本，之前的版本已经被爆出了两个漏洞，详见 OpenSSL Security Advisory [25 March 2021]。一个漏洞可以进行 DoS 攻击，另一个则可以进行中间人攻击。 你可以通过运行命令 nginx -V 来查看你的 nginx 编译时用的 openssl 的版本，或者运行时使用的版本。你应该能看到下面这行，请保证运行时所使用 openssl 的版本在 1.1.1k 及以上，如果没有括号里的内容请确保编译时所使用的 openssl 的版本在 1.1.1k 及以上 built with OpenSSL 1.1.1i 8 Dec 2020 (running with OpenSSL 1.1.1k 25 Mar 2021) 前向安全性 简单来说如果某一天你的服务器上的证书私钥泄露了，你之前的通信也无法被解密，你需要做的只是换一个证书而已。 参考资料 Security/Server Side TLS - MozillaWiki
写在前面 当你打不开某个网站的时候你会怎么办？ 你可能会先 ping 一下域名看一下延迟。如果延迟正常的话你大概会检查一下自己的 DNS 解析结果是否正确。如果 DNS 解析也是正确的你大概会怀疑网站本身出了问题。但是你老家的亲戚能打开你却打不开，这是怎么回事呢？ 这种情况下你可能遇到了「BGP 劫持」，它可以将你的通信劫持到指定主机上，并且 IP 地址不发生变化。 什么是 BGP ? BGP（Border Gateway Protocol），即边界网关协议，用于各个 AS（自治系统）之间互相交换路由信息。 自治系统（AS）：可以简单地理解为一个或多个网络管理员管理的网络区域，比如一块属于某个 ISP 的网络，某个学校的网络，你自己在家里建立一个局域网也可以理解为自治系统。 一般来说路由器可以通过 RIP 或 OSPF 协议来互相交换路由信息，但是这通常只能用于不那么大的网络。如果网络太大，比如国与国之间，那么 RIP 和 OSPF 协议就力不从心了。 首先就是要维护的路由信息太大，软硬件成本都不可接受。然后就是各个网络内部的情况可能不同，比如计算路线代价的时候可能各个自治系统的定义不一样，比如自治系统 A 的某条线路代价为 100 表示速度比较慢的线路，但是自治系统 B 的代价 100 则表示不可用的线路。最后就是一些特殊的需求，比如国家内部的终端在公网交换信息，理论上有两种路线，一种是走国内的线路，另一种是从国外的线路绕行到国内的线路，BGP 协议可以让国内的通信无需绕行国外，提高速度。 下图是一个简单的 BGP 工作示意图。 BGP 协议 BGP 协议的安全缺陷 BGP 协议并不能验证 BGP 发言人所声明的信息是否为真，也就是说 BGP 发言人完全可以声明一个不属于自己的网络，然后将被声明的网络的流量引到自己的自治系统内，完成 BGP 劫持。 下图是一个 BGP 劫持的示意图。 BGP 劫持 此时攻击者可选择丢弃掉 N3 网络所有的流量、进行窃听或者篡改。 为什么 IP 不会变？ 回顾 OSI 七层模型，其中第三层为网络层，负责根据 IP 转发 IP 数据报。一旦 BGP 劫持成功，路由器会错误地将发往网络 N3 的数据报发往网络 N4，但是路由器并不知道，所以从使用者的角度看，自己的 IP 地址是正常的。 BGP 协议的改进 BGP Operations and Security — RFC7454 中记录了一些最佳实践，如引入 ACL（访问控制列表），在传输层建立安全机制等，有兴趣的读者可以自行阅读。
本文主要起补充作用，大致的流程上面的文章已经讲的很清楚了，这里主要整理一下遇到的坑。
写在前面 被补充文章的链接：使用 Docker 最小化部署 Misskey。 本文主要起补充作用，大致的流程上面的文章已经讲的很清楚了，这里主要整理一下遇到的坑。 如果你遇到了下文中没有提到的错误，比如无法订阅中继，建议按照官方文档的流程构建镜像，而不是按照上面的文章使用现有的镜像。构建镜像时如果爆内存本文有解决方式。 数据库初始化失败 症状 运行下面的命令时出错，报错信息中包含应该能看见这段信息：getaddrinfo ENOTFOUND db 。 docker-compose run --rm web yarn run init 解决 尝试手动启动 redis 和 db 两个容器，等待容器内的数据库就绪后重新运行数据库初始化命令。等待时间只能自己把握，不放心就等个几分钟应该就可以了。 原因 这是因为数据库容器内的数据库服务在运行数据库初始化命令时并未就绪。截止至截稿日，最新的版本是 v12.69.0，官方提供的文档中并未提醒这一情况。 官方提供的容器编排文件 docker-compose.yml 通过 links 配置规定了容器的启动顺序，但是这并不能保证容器内的数据库服务已经就绪。这一点 Docker 在官方文档中有提醒。 You can control the order of service startup and shutdown with the depends_on option. Compose always starts and stops containers in dependency order, where dependencies are determined by depends_on, links, volumes_from, and network_mode: "service:...". However, for startup Compose does not wait until a container is “ready” (whatever that means for your particular application) - only until it’s running. There’s a good reason for this. The problem of waiting for a database (for example) to be ready is really just a subset of a much larger problem of distributed systems. In production, your database could become unavailable or move hosts at any time. Your application needs to be resilient to these types of failures. Control startup and shutdown order in Compose | Docker Documentation Misskey 官方已经发现了这个问题：Error: getaddrinfo ENOTFOUND でプロセスが落ちる · Issue #1546 · syuilo/misskey。 构建镜像时爆内存 症状 运行下面这行命令后显示堆内存不足。 docker-compose build 解决 修改镜像构建的指导文件 Dockerfile，修改命令 yarn build 为下面的命令，然后重新构建镜像。 export NODE_OPTIONS="--max-old-space-size=2048" && yarn build 原因 主要是因为 WebPack 在 build 时会有一个内存占用的峰值。[1]这一问题截止至截稿日官方的中英文文档也并未提醒。Github 已经有用户开 issue 提出了文档修改建议：Clarify hardware requirements in the installation guide · Issue #4983 · syuilo/misskey。 我曾经以为是单纯的内存不足，一度把 swap 增加到 10GB 还不行才反应过来没准是其它原因。 写在最后 在 Github 读日文 issue 做不到啊。 参考资料 [1] Clarify hardware requirements in the installation guide · Issue #4983 · syuilo/misskey [2] Control startup and shutdown order in Compose | Docker Documentation
OCSP—让你及时发现被吊销的数字证书的协议
没有及时发现证书失效是很可怕的事，尤其是在金融交易中。
用途 互联网世界中我们需要确定对方的身份，比如防止我想和小明说话，但是实际却在和黑客说话这种情况。一种比较常见的认证方式就是数字证书，比如 HTTPS 站点的 SSL 证书。关于 HTTPS 的工作流程的简单介绍，可以看一下我之前的一篇文章——简要说明 HTTPS 是如何保证数据安全。 一般来说客户端收到对方的证书后会沿着证书链确定各级证书是否有效以决定是否信任接收到的证书。在这种验证流程中操作系统、浏览器和安全软件都可以内置一些 CA 的证书以便证书的检查。 如果一个证书是无效的，那么它一定符合下列情况中的至少一个。 证书本身过期或者证书链上的某个证书过期。 证书本身被吊销或者证书链上的某个证书被吊销。 证书的有效期已经写入证书本身，无需额外的信息即可判断。但是如果一个证书出于某种原因被吊销了（比如私钥泄露），客户端此时无法根据证书所携带的信息来确定是否被吊销。OCSP 就是为了解决这个问题。 协议概述 OCSP 全称 Online Certificate Status Protocol，即在线证书状态协议。它可以让客户端与服务器通信以确定某个证书的状态，即未撤销、已撤销或未知。 笔者并未在协议文档中找到关于 OCSP 所使用的传输机制，报文结构等信息，所以 OCSP 理论上可以有多种实现。 通信流程 客户端向提供 COSP 服务的服务器发送一个请求，包含下列内容。 协议版本。 服务请求的类型（协议文档并未指明有哪些类型）。 打算查询状态的一个或多个证书的 ID。 可选的扩展信息。 服务器接收到请求之后首先会检查请求是否符合下列要求。 请求格式正确。 服务器支持请求中所指定的「服务请求的类型」。 客户端提供的信息要足以让服务器做出相应，即不能缺少必要信息。 然后服务器会通过某些方式获取证书的状态，然后响应客户端的请求，响应中包含下列内容。 响应内容的语法版本。 响应器的 ID。 本次响应的生成时间。 针对每一个证书的响应，包含下列内容。 该证书的 ID。 该证书的状态，包含未撤销、已撤销和未知。 针对本证书的响应的有效期。 可选的扩展信息。 可选的扩展信息。 使用的签名算法的 ID。 本次响应内容的哈希值的签名。 证书状态未知表示服务器不知道这个证书的状态。通常是签发被查询证书的 CA 不受信任，比如自签证书。也有可能是服务器知道的不够多，比如这个证书本身没有问题，但是服务器没有收录。当客户端得到这个回复的时候可以自行选择，比如默认不信任，或者通过其他方式查询状态等。 证书状态为已撤销通常表示为此证书被 CA 撤销，但这并不绝对。对于类似自签证书这样的由不受信任的 CA 签发的证书，服务器也可以在响应中将其标记为已撤销状态，意为服务器打算让客户端直接拒绝这个证书，而不是像证书状态未知那样交给客户端自己去选择。 一旦一个由不受信任的 CA 颁发的证书在本次响应中被标记为已撤销，那么本次响应必须要符合下列要求。 必须在本次响应中说明撤销的原因。 必须在本次响应中说明撤销的时间。 不能在本次响应中包含 CRL 的参考信息。 CRL 是另一个用来查询证书状态的协议。 写在最后 OCSP 会拖慢证书的验证时间，尤其是在提供 OCSP 的服务器不那么可用的情况下，所以有了 OCSP stapling。它将验证工作放到了 Web 服务器，也就是说你在连接一个 HTTPS 站点的时候同时向站点对应的服务器发起 OCSP 请求，因为 Web 服务器对于访问它的客户端来说通常都是高可用的，所以这种方式一般比较快。本站也在写完本篇文章后支持了此种方式。 本文只是粗略地说明协议的运行过程，一些细节被省略，比如如何预防重放攻击。如果有兴趣可以自行查看协议文档。 参考资料 RFC 6960 - X.509 Internet Public Key Infrastructure Online Certificate Status Protocol - OCSP
总之就是一篇看了也没什么用的文章，了解一下而已。
写在前面 每次安装完 Ubuntu 的第一件事情就是给 apt 换源，而换源过程基本就是复制粘贴，今天突然发现自己复制粘贴这么久也不知道粘贴了什么，于是了解了一下并分享出来。总之就是一篇看了也没什么用的文章，了解一下而已。 正文 Linux 上有问题就先 man 一下，于是我 man apt，然后发现没有说关于 source.list 的格式，不过在手册的末尾最后发现有专门的文档讲解文件格式，于是 man source.list。 其实 apt 源文件并不只有 source.list，也有一个叫 source.sources。两者的区别是格式不同。前者使用的格式称为 ONE-LINE-STYLE，也就是网上常见的一行一个的那种。后者使用的格式叫做 DEB822-STYLE，这种格式在网上应该比较少见。 ONE-LINE-STYLE 这种格式是最常见的了，通常从网上复制粘贴来的就是这个格式。下面是 Ubuntu 20.04 LTS 的默认源。 # See http://help.ubuntu.com/community/UpgradeNotes for how to upgrade to # newer versions of the distribution. deb http://archive.ubuntu.com/ubuntu/ focal main restricted # deb-src http://archive.ubuntu.com/ubuntu/ focal main restricted ## Major bug fix updates produced after the final release of the ## distribution. deb http://archive.ubuntu.com/ubuntu/ focal-updates main restricted # deb-src http://archive.ubuntu.com/ubuntu/ focal-updates main restricted ## N.B. software from this repository is ENTIRELY UNSUPPORTED by the Ubuntu ## team. Also, please note that software in universe WILL NOT receive any ## review or updates from the Ubuntu security team. deb http://archive.ubuntu.com/ubuntu/ focal universe # deb-src http://archive.ubuntu.com/ubuntu/ focal universe deb http://archive.ubuntu.com/ubuntu/ focal-updates universe # deb-src http://archive.ubuntu.com/ubuntu/ focal-updates universe ## N.B. software from this repository is ENTIRELY UNSUPPORTED by the Ubuntu ## team, and may not be under a free licence. Please satisfy yourself as to ## your rights to use the software. Also, please note that software in ## multiverse WILL NOT receive any review or updates from the Ubuntu ## security team. deb http://archive.ubuntu.com/ubuntu/ focal multiverse # deb-src http://archive.ubuntu.com/ubuntu/ focal multiverse deb http://archive.ubuntu.com/ubuntu/ focal-updates multiverse # deb-src http://archive.ubuntu.com/ubuntu/ focal-updates multiverse ## N.B. software from this repository may not have been tested as ## extensively as that contained in the main release, although it includes ## newer versions of some applications which may provide useful features. ## Also, please note that software in backports WILL NOT receive any review ## or updates from the Ubuntu security team. deb http://archive.ubuntu.com/ubuntu/ focal-backports main restricted universe multiverse # deb-src http://archive.ubuntu.com/ubuntu/ focal-backports main restricted universe multiverse ## Uncomment the following two lines to add software from Canonical's ## 'partner' repository. ## This software is not part of Ubuntu, but is offered by Canonical and the ## respective vendors as a service to Ubuntu users. # deb http://archive.canonical.com/ubuntu focal partner # deb-src http://archive.canonical.com/ubuntu focal partner deb http://security.ubuntu.com/ubuntu/ focal-security main restricted # deb-src http://security.ubuntu.com/ubuntu/ focal-security main restricted deb http://security.ubuntu.com/ubuntu/ focal-security universe # deb-src http://security.ubuntu.com/ubuntu/ focal-security universe deb http://security.ubuntu.com/ubuntu/ focal-security multiverse # deb-src http://security.ubuntu.com/ubuntu/ focal-security multiverse 下面是此风格的基本格式。 deb [ option1=value1 option2=value2 ] uri suite [component1] [component2] [...] deb-src [ option1=value1 option2=value2 ] uri suite [component1] [component2] [...] deb：表示从这里可以找到二进制程序，即包含已经编译好的程序。 deb-src：表示从这里找到源码。 option：表示额外的选项，比如 arch 表示体系结构，amd64 啥的。语法上不仅支持 option=value1 也支持 option=value1,value2 、option-=value 和 option+=value。+= 和 -= 仅支持那些可能有多个值的选项。 uri：就是源的网址，比如腾讯的 apt 源的网址就是 http://mirrors.cloud.tencent.com/ubuntu/。 suite：代表发行版的类型如 stable。也可以代表发行版的代号比如 Ubutnu 20.24 LTS 的代号就是 focal。[3]所以说如果你用的是 Ubutnu 18.04，那么你应该写 bionic，因为这个版本的代号就叫 bionic。[3]不过版本代号后面可能还会跟着如 -backports 这样的后缀，相关信息见 UbuntuUpdates - Community Help Wiki。 component：表示软件包的分类，不同的 Linux 发行版有不同的分类。对于 Ubuntu 来说有下面的几种。 main：由 Canonical 支持的自由和开源软件。[1] universe：由社区维护的自由和开源软件。[1] restricted：设备驱动程序。[1] multiverse：受版权或法律问题限制的软件。[1] DEB822-STYLE 此格式从 apt 1.1 开始支持，文件必须以 .sources 结尾。下面是它的基本格式。 Types: deb deb-src URIs: uri Suites: suite Components: [component1] [component2] [...] option1: value1 option2: value2 Types: deb deb-src URIs: uri Suites: suite Components: [component1] [component2] [...] option1: value1 option2: value2 同一条目中的每个字段各占一行，不同的条目间用一行空行分隔。各个字段的含义与 ONE-LINE-STYLE 中的完全相同。你也可以通过添加一行 Enabled: no 来禁用某一个条目。如果一个字段有多个值则每个值之间用空格分隔。 例子 下面的两种格式的源配置是等效的。 deb http://us.archive.ubuntu.com/ubuntu focal main restricted deb http://security.ubuntu.com/ubuntu focal-security main restricted deb http://us.archive.ubuntu.com/ubuntu focal-updates main restricted Types: deb URIs: http://us.archive.ubuntu.com/ubuntu Suites: focal focal-updates Components: main restricted Types: deb URIs: http://security.ubuntu.com/ubuntu Suites: focal-security Components: main restricted 参考资料 [1] Repositories/Ubuntu - Community Help Wiki [2] UbuntuUpdates - Community Help Wiki [3] DevelopmentCodeNames - Ubuntu Wiki
我起初并没有写这篇文章的意思，因为无论是从难度还是代码复杂度来说这个项目在我写过的项目里只能算中等，但是它却有一个特点，就是我的第一个专门开发出来供他人使用并且希望被他人使用的项目。先前的项目要不然不能公开，要不然个人属性非常强，不适合他人使用。所以这篇文章主要是以纪念为目的的。
[github author="ADD-SP" project="ngx_waf"][/github] 为什么要写这篇文章？ 我起初并没有写这篇文章的意思，因为无论是从难度还是代码复杂度来说这个项目在我写过的项目里只能算中等，但是它却有一个特点，就是我的第一个专门开发出来供他人使用并且希望被他人使用的项目。先前的项目要不然不能公开，要不然个人属性非常强，不适合他人使用。所以这篇文章主要是以纪念为目的的。 为什么要开启这个项目？ 那段时间我的站点有时会打不开，起初以为是网络问题，后来越来越严重，进入后台才发现数据库 IO 拉满了。看了看 nginx 的日志才发现站点被疯扫，于是打算做点什么。 我先从网上搜索到了一些基础的拦截扫站的规则，直接 copy 到 nginx.conf 里就能用，但是这些规则都特别多，而且杂，直接丢到配置文件里会大大降低配置的可读性，所以当初就被否了。 后来我拉黑了一些 IP，不过这也就一时起作用，过段时间换了 IP 继续来扫。补救措施是用 whois 找到 IP 地址快直接全部拉黑。但是这导致了一些误伤，于是这个方案也被否了。 然后我找到了 ngx_lua_waf，用了一段觉得还行，但是也发现了一些缺陷，最明显的就是拉黑 IP 的时候只能拉黑单个 IP，而不能拉黑一个地址块。但是这个模块大概已经停止维护了，不能指望作者更新了。 然后在 Github 上看了几个防火墙模块，要不然功能不全，要不然使用复杂，于是萌生了自己写模块的想法。 为什么用 C 语言开发？ 主要是因为这样可以不用安装 lua_nginx_module，其次是因为 C 的执行效率理论上要高于 lua，因为 lua 需要启动一个 VM。 不过我主要考虑的是安装是否方便，性能倒不是那么看重，因为 lua_nginx_module 身经百战，性能完全可以保证。 不过用 C 语言开发必然增加开发难度，最重要的是要自己控制内存，这类 Web 服务器上的模块一旦内存泄露、段错误啥的那麻烦就大了，不过我还是有信心不出大问题的。 设计原则 只实现基础的防护，即 IP 检测、Url 检测、Get 参数检测、Cookie 检测、Post 检测、Referer 检测和 CC 防御。不会引入更复杂的功能，比如根据某个 IP 的行为进行综合判断是否拦截。 在保证代码良好可读性的情况下提高性能。 语义化版本 说实话我之前还真没把这个当回事，但是当我打算把这个项目给别人用的时候我发现版本号似乎也能传递一些比较重要的信息，所以开发这个项目的时候我是一直遵守这个标准的。 简单来说语义化版本将版本号分为 X.Y.Z。 当你做了不向下兼容的更改之后 X 要递增。 当你做了向下兼容的功能性新增的时候 Y 要递增。 当你做了向下兼容的修正时 Z 要递增。 这样版本号就可以向用户传递对应的信息，这无疑是十分有用的，最明显的就是当用户看到 X 递增的时候就会十分谨慎，因为贸然更新可能会导致现有的程序出现不兼容的情况。 Change Log 作为一个打算给他人使用的项目，一个清晰的 Change Log 就是很必要的了，它可以将每次更新的内容展示出来，方便他人了解也方便自己记录。 格式上基本遵循：https://keepachangelog.com/zh-CN/1.0.0/ 效果图 从效果上来看应该还算是清晰准确的 Change Log。 开发文档 项目写到后期就不得不写开发文档了，首先是方便了自己，其次也是方便别人。可以从头开始写效率太低了，强烈推荐 doxygen。这个工具可以通过代码内的注释自动生成文档，这样为源代码写注释的同时还能生成开发文档，十分方便。 下面是对应的注释以及生成的开发文档。 <img loading="lazy" decoding="async" width="1024" height="769" src="https://www.addesp.com/wp-content/uploads/2020/12/Snipaste_2020-12-20_03-03-19-1024x769.png" alt="" class="wp-image-2961" srcset="https://www.addesp.com/wp-content/uploads/2020/12/Snipaste_2020-12-20_03-03-19-1024x769.png 1024w, https://www.addesp.com/wp-content/uploads/2020/12/Snipaste_2020-12-20_03-03-19-300x225.png 300w, https://www.addesp.com/wp-content/uploads/2020/12/Snipaste_2020-12-20_03-03-19-768x577.png 768w, https://www.addesp.com/wp-content/uploads/2020/12/Snipaste_2020-12-20_03-03-19.png 1053w" sizes="auto, (max-width: 1024px) 100vw, 1024px" /> 注释内容 开发文档 开发文档简单，清晰，还能自动生成函数调用图和被调用图等一系列图。如果觉得丑还能自定义 CSS，不过对于我这个前端困难户来说就不搞了。 性能优化 性能优化的主要工作就是降低请求检查花费的时间。 缓存 Url 检查、Get 参数检查、Post 检查、Cookie 检查和 Referer 检查都需要执行正则匹配，暂时没发现什么合适的方法去优化，只能一个一个地去检查。时间复杂度为 $\text{O}(nm)$，$n$ 为需要测试的正则条数，$m$ 为执行正则匹配的时间。 不过由于本模块只会在 nginx 启动时读取规则，运行时规则不会改变，所以对于同一个 URL 无论检测多少次结果都不会变，于是本模块会缓存检查的结果，每次检查前先读取缓存，如果命中则直接取出结果，反之则走流程检查。除了 IP 检查和 Post 检查以外所有的检查都使用了缓存机制。 缓存淘汰策略为 LRU。当缓存的内存不足时会引起频繁的淘汰，增加内存碎片。于是本模块会周期性地按照 LRU 的策略淘汰掉一定比例的缓存。 前缀树优化 IP 检查很有搞头。本模块使用了一种经过修改的前缀树来改进 IP 检查的性能，见下图。 本模块所使用的前缀树 从图示就可以看出此结构可以方便地处理单个 IP 的检查和 IP 地址块的检查。 前缀树的查找时间为 $\text{O}(h)$，其中 $h$ 为树的高度。对于 IPV4 来说一共有 32 位，那么树高最多 32，IPV6 对应的树高最多为 128。那么查找时间复杂度为 $\text{O}(1)$，也就是说执行 IP 检查的时间基本上不会受到 IP 黑白名单规模的影响。 测试 经过比较极限的测试，QPS 降低了约 4%，详情见性能测试。 我上 Trending 了！ 2021 年 4 月 8 日晚上我发现我的项目出现在了 Github Trending（C 语言） 下，截图纪念一下。 希望不要明天起来就没了，多撑几天吧。 第二天晚更新：博主已经凉了，散了吧。 验证码 这里的验证码特指用于人机验证的验证码，比如滑块解锁等，也就是我们通常所说的 CAPTCHA，比较有名的就是 reCAPTCHA。 reCAPTCHA 目前已经集成了 reCAPTCHA 的全部种类的验证码以及 hCAPTCHA。本来想着集成一下腾讯云的验证码，但是发现集成起来好麻烦，远没有前两种简单，就一直搁置到现在。 验证码的集成也带来了不小的好处，比如 CC 防护不再只能简单地拉黑了。有了验证码后就可以将 CC 防护的阈值降低一些，超出时弹出验证码，通过验证则放行，反之则拉黑一段时间。同时，根据某个用户的建议，现在当触发任何的拦截时，就会对该 IP 启用验证码，当且仅当通过验证时才会放行，否则无法进行任何访问。 集成 ModSecurity ModSecurity 是一个广泛使用的开源的 WAF 引擎，并且在网络上有现成的安全规则可以套用，所以本模块也继承了 ModSecurity 的引擎，可以加载 ModSecurity 的规则。 （ModSecurity 的内存泄漏问题官方什么时候修一下） 后续计划 本项目已经完成了性能优化，后续应该不会有大的改动，不过如果有合适的需求就另说了。有了新的进展我也会更新本文。
D-H 密钥交换—即使被窃听也能安全地交换密钥
是的，即使通信全程都被窃听，你依然可以和对方安全地协商出密钥来进行加密通讯。
写在前面 是的，即使通信全程都被窃听，你依然可以和对方安全地协商出密钥来进行加密通讯。 迪菲-赫尔曼密钥交换（英语：Diffie–Hellman key exchange，缩写为D-H） 是一种安全协议。它可以让双方在完全没有对方任何预先信息的条件下通过不安全信道创建起一个密钥。这个密钥可以在后续的通讯中作为对称密钥来加密通讯内容。公钥交换的概念最早由瑞夫·墨克（Ralph C. Merkle）提出，而这个密钥交换方法，由惠特菲尔德·迪菲（Bailey Whitfield Diffie）和马丁·赫尔曼（Martin Edward Hellman）在1976年首次发表。马丁·赫尔曼曾主张这个密钥交换方法，应被称为迪菲-赫尔曼-墨克密钥交换（英语：Diffie–Hellman–Merkle key exchange）。 迪菲-赫尔曼密钥交换_百度百科 D-H 密钥交换流程 通信双方甲和乙事先约定好两个精心挑选的数，一个是质数 $p$，另一个是 $g$。这两个数是可以公开的。我们假设 $p=23,g=5$。 甲随机选择一个整数 $a$，计算 $A=g^a \mod p$，并将 $A$ 发送给乙。其中 $A$ 是可以公开的，但是 $a$ 不能泄露。我们假设 $a=6$，那么 $A=5^6 \mod 23 =8$。 乙随机选择一个整数 $b$，计算 $B=g^b \mod p$，并将 $B$ 发送给甲。其中 $B$ 是可以公开的，但是 $b$ 不能泄露。我们假设 $b=15$，那么 $B=5^{15} \mod 23 =19$。 此时双方就可以计算出共有的密码 $s$。甲通过计算 $s = B^a \mod p = 19^6 \mod 23 = 2$。乙通过计算 $s = A^b \mod p = 8^6 \mod 23 = 2$。 双方得到了相同的密码 $s$。但这个密码并不是一方主动发给另一方的，而是通过双方交换的信息计算而来的。此时可以使用 $s$ 生成对称密钥来加密通信。 根据模运算法则 $g^a \mod p = [{(g \mod p)}^a] \mod p$，同理 $g^b \mod p = [{(g \mod p)}^b] \mod p$，显然 $s={(g^a \mod p)}^b \mod p = {(g^b \mod p)}^a \mod p$。 可以发现公开的信息只有 $p,g,A,B$，而只需要在草稿纸上试一试就感觉这些信息逆向算出$a,b$是很难的事情，貌似只能去穷举，事实也确实如此。由于计算出密码 $s$ 除了 $p,g,A,B$ 以外还需要 $a$ 或 $b$，所以这种密钥交换方式即使在被窃听的情况下也能保证安全（前提是不会受到中间人攻击）。 离散对数问题 上文说到逆向计算 $a,b$ 是很难的，这其实就是著名的离散对数问题。相关的数学表述本文不会涉及，首先是因为本文的初衷是了解即可，没必要钻那么深，其次就是笔者不是搞数学的，水平有限，查了半天资料还是一知半解的，就不瞎说了。就举一个比较直观的例子吧。 我们设 $g=3,p=10$，依次计算 $g^i \mod p$，其中 $i = 0,1,2,···,99$。 点击查看计算结果 展开 / 收起 $$ \begin{aligned} 3^{0} \mod 10 = 1 \\ 3^{1} \mod 10 = 3 \\ 3^{2} \mod 10 = 9 \\ 3^{3} \mod 10 = 7 \\ 3^{4} \mod 10 = 1 \\ 3^{5} \mod 10 = 3 \\ 3^{6} \mod 10 = 9 \\ 3^{7} \mod 10 = 7 \\ 3^{8} \mod 10 = 1 \\ 3^{9} \mod 10 = 3 \\ 3^{10} \mod 10 = 9 \\ 3^{11} \mod 10 = 7 \\ 3^{12} \mod 10 = 1 \\ 3^{13} \mod 10 = 3 \\ 3^{14} \mod 10 = 9 \\ 3^{15} \mod 10 = 7 \\ 3^{16} \mod 10 = 1 \\ 3^{17} \mod 10 = 3 \\ 3^{18} \mod 10 = 9 \\ 3^{19} \mod 10 = 7 \\ 3^{20} \mod 10 = 1 \\ 3^{21} \mod 10 = 3 \\ 3^{22} \mod 10 = 9 \\ 3^{23} \mod 10 = 7 \\ 3^{24} \mod 10 = 1 \\ 3^{25} \mod 10 = 3 \\ 3^{26} \mod 10 = 9 \\ 3^{27} \mod 10 = 7 \\ 3^{28} \mod 10 = 1 \\ 3^{29} \mod 10 = 3 \\ 3^{30} \mod 10 = 9 \\ 3^{31} \mod 10 = 7 \\ 3^{32} \mod 10 = 1 \\ 3^{33} \mod 10 = 3 \\ 3^{34} \mod 10 = 8 \\ 3^{35} \mod 10 = 2 \\ 3^{36} \mod 10 = 6 \\ 3^{37} \mod 10 = 6 \\ 3^{38} \mod 10 = 0 \\ 3^{39} \mod 10 = 6 \\ 3^{40} \mod 10 = 8 \\ 3^{41} \mod 10 = 2 \\ 3^{42} \mod 10 = 2 \\ 3^{43} \mod 10 = 6 \\ 3^{44} \mod 10 = 4 \\ 3^{45} \mod 10 = 2 \\ 3^{46} \mod 10 = 4 \\ 3^{47} \mod 10 = 0 \\ 3^{48} \mod 10 = 8 \\ 3^{49} \mod 10 = 8 \\ 3^{50} \mod 10 = 4 \\ 3^{51} \mod 10 = 2 \\ 3^{52} \mod 10 = 4 \\ 3^{53} \mod 10 = 4 \\ 3^{54} \mod 10 = 6 \\ 3^{55} \mod 10 = 8 \\ 3^{56} \mod 10 = 0 \\ 3^{57} \mod 10 = 4 \\ 3^{58} \mod 10 = 2 \\ 3^{59} \mod 10 = 2 \\ 3^{60} \mod 10 = 0 \\ 3^{61} \mod 10 = 8 \\ 3^{62} \mod 10 = 4 \\ 3^{63} \mod 10 = 4 \\ 3^{64} \mod 10 = 0 \\ 3^{65} \mod 10 = 2 \\ 3^{66} \mod 10 = 8 \\ 3^{67} \mod 10 = 4 \\ 3^{68} \mod 10 = 6 \\ 3^{69} \mod 10 = 4 \\ 3^{70} \mod 10 = 8 \\ 3^{71} \mod 10 = 4 \\ 3^{72} \mod 10 = 0 \\ 3^{73} \mod 10 = 0 \\ 3^{74} \mod 10 = 6 \\ 3^{75} \mod 10 = 0 \\ 3^{76} \mod 10 = 0 \\ 3^{77} \mod 10 = 4 \\ 3^{78} \mod 10 = 6 \\ 3^{79} \mod 10 = 2 \\ 3^{80} \mod 10 = 4 \\ 3^{81} \mod 10 = 2 \\ 3^{82} \mod 10 = 8 \\ 3^{83} \mod 10 = 0 \\ 3^{84} \mod 10 = 6 \\ 3^{85} \mod 10 = 4 \\ 3^{86} \mod 10 = 2 \\ 3^{87} \mod 10 = 8 \\ 3^{88} \mod 10 = 4 \\ 3^{89} \mod 10 = 0 \\ 3^{90} \mod 10 = 6 \\ 3^{91} \mod 10 = 2 \\ 3^{92} \mod 10 = 6 \\ 3^{93} \mod 10 = 0 \\ 3^{94} \mod 10 = 0 \\ 3^{95} \mod 10 = 0 \\ 3^{96} \mod 10 = 2 \\ 3^{97} \mod 10 = 6 \\ 3^{98} \mod 10 = 2 \\ 3^{99} \mod 10 = 6 \\ \end{aligned} $$ 那么如果我问求一个 $k$ 使得 $3^k \mod 10 = 6$。在没有快捷算法的情况下我们只能一个一个地试，此时可以视为随机取 $i$ 的值去试。可以注意到在上面列出的 100 个计算结果中 6 出现了 13 次，出现的概率是 $13\%$，这意味着我在穷举过程中每次计算都有 $13\%$ 的概率的都正确答案。 那么当我们让$p=17$，即一个素数时，情况就变了。 点击查看计算结果 展开 / 收起 $$ \begin{aligned} & 3^{0} \mod 17 = 1 \\ & 3^{1} \mod 17 = 3 \\ & 3^{2} \mod 17 = 9 \\ & 3^{3} \mod 17 = 10 \\ & 3^{4} \mod 17 = 13 \\ & 3^{5} \mod 17 = 5 \\ & 3^{6} \mod 17 = 15 \\ & 3^{7} \mod 17 = 11 \\ & 3^{8} \mod 17 = 16 \\ & 3^{9} \mod 17 = 14 \\ & 3^{10} \mod 17 = 8 \\ & 3^{11} \mod 17 = 7 \\ & 3^{12} \mod 17 = 4 \\ & 3^{13} \mod 17 = 12 \\ & 3^{14} \mod 17 = 2 \\ & 3^{15} \mod 17 = 6 \\ & 3^{16} \mod 17 = 1 \\ & 3^{17} \mod 17 = 3 \\ & 3^{18} \mod 17 = 9 \\ & 3^{19} \mod 17 = 10 \\ & 3^{20} \mod 17 = 13 \\ & 3^{21} \mod 17 = 5 \\ & 3^{22} \mod 17 = 15 \\ & 3^{23} \mod 17 = 11 \\ & 3^{24} \mod 17 = 16 \\ & 3^{25} \mod 17 = 14 \\ & 3^{26} \mod 17 = 8 \\ & 3^{27} \mod 17 = 7 \\ & 3^{28} \mod 17 = 4 \\ & 3^{29} \mod 17 = 12 \\ & 3^{30} \mod 17 = 2 \\ & 3^{31} \mod 17 = 6 \\ & 3^{32} \mod 17 = 1 \\ & 3^{33} \mod 17 = 3 \\ & 3^{34} \mod 17 = 8 \\ & 3^{35} \mod 17 = 15 \\ & 3^{36} \mod 17 = 11 \\ & 3^{37} \mod 17 = 1 \\ & 3^{38} \mod 17 = 11 \\ & 3^{39} \mod 17 = 0 \\ & 3^{40} \mod 17 = 0 \\ & 3^{41} \mod 17 = 8 \\ & 3^{42} \mod 17 = 11 \\ & 3^{43} \mod 17 = 16 \\ & 3^{44} \mod 17 = 15 \\ & 3^{45} \mod 17 = 11 \\ & 3^{46} \mod 17 = 7 \\ & 3^{47} \mod 17 = 6 \\ & 3^{48} \mod 17 = 10 \\ & 3^{49} \mod 17 = 12 \\ & 3^{50} \mod 17 = 2 \\ & 3^{51} \mod 17 = 6 \\ & 3^{52} \mod 17 = 15 \\ & 3^{53} \mod 17 = 2 \\ & 3^{54} \mod 17 = 5 \\ & 3^{55} \mod 17 = 15 \\ & 3^{56} \mod 17 = 10 \\ & 3^{57} \mod 17 = 14 \\ & 3^{58} \mod 17 = 8 \\ & 3^{59} \mod 17 = 14 \\ & 3^{60} \mod 17 = 12 \\ & 3^{61} \mod 17 = 10 \\ & 3^{62} \mod 17 = 13 \\ & 3^{63} \mod 17 = 13 \\ & 3^{64} \mod 17 = 14 \\ & 3^{65} \mod 17 = 10 \\ & 3^{66} \mod 17 = 5 \\ & 3^{67} \mod 17 = 15 \\ & 3^{68} \mod 17 = 7 \\ & 3^{69} \mod 17 = 5 \\ & 3^{70} \mod 17 = 11 \\ & 3^{71} \mod 17 = 16 \\ & 3^{72} \mod 17 = 16 \\ & 3^{73} \mod 17 = 14 \\ & 3^{74} \mod 17 = 9 \\ & 3^{75} \mod 17 = 12 \\ & 3^{76} \mod 17 = 2 \\ & 3^{77} \mod 17 = 7 \\ & 3^{78} \mod 17 = 0 \\ & 3^{79} \mod 17 = 16 \\ & 3^{80} \mod 17 = 12 \\ & 3^{81} \mod 17 = 2 \\ & 3^{82} \mod 17 = 4 \\ & 3^{83} \mod 17 = 16 \\ & 3^{84} \mod 17 = 15 \\ & 3^{85} \mod 17 = 7 \\ & 3^{86} \mod 17 = 4 \\ & 3^{87} \mod 17 = 10 \\ & 3^{88} \mod 17 = 13 \\ & 3^{89} \mod 17 = 14 \\ & 3^{90} \mod 17 = 4 \\ & 3^{91} \mod 17 = 16 \\ & 3^{92} \mod 17 = 14 \\ & 3^{93} \mod 17 = 6 \\ & 3^{94} \mod 17 = 1 \\ & 3^{95} \mod 17 = 3 \\ & 3^{96} \mod 17 = 1 \\ & 3^{97} \mod 17 = 3 \\ & 3^{98} \mod 17 = 5 \\ & 3^{99} \mod 17 = 15 \\ \end{aligned} $$ 此时 6 只出现了 5 次，而且呈现出了明显的周期现象，这个周期就是 $p-1$ 即 16。这时我们穷举的时候每次计算只有 $5\%$ 的概率是正确答案了。在这种情况下我们提高了穷举的难度。 目前的数学研究结果让我们可以精心选择一个素数 $p$ 和另一个精心选择的 $g$ 来通过这样的方式来提高穷举的难度，比如我们选择一个长度为 $1024 bit$ 的素数，那么它的周期和 $2^{1024} \approx 1.798 \times 10^{308}$ 是同一个数量级的，那么暴力穷举算法的时间复杂度就是指数级的。那么如果素数的长度是 $2048 bit$ 呢？$4096 bit$ 呢？ 上面的例子并不严谨，但可以直观地感受一下离散对数问题的难度。稍微专业点的描述一下离散对数的难度就是暂时没有多项式时间或更快的算法能够求解离散对数问题。 身份验证 D-H 密钥交换没有提供身份验证的机制，所以我们需要公钥基础设施来保证不会受到中间人攻击。 前向安全性 前向安全性即通讯的主密钥泄露不会导致会话密钥的泄露，它可以保证之前的通讯不会因为在未来的某一时刻主密钥泄露而被解密。 目前常见的身份认证方式之一就是基于非对称加密的证书验证。在这种方式下为了实现身份认证服务端会使用私钥给一些必要的信息签名发送给客户端，客户端确认服务端的身份后双方协商出会话密钥，协商过程中的信息会用公钥加密，只有私钥才能解密。此时客户端可以直接将会话密钥通过公钥加密后发送给服务端，然后开始加密通讯。这在正常情况下没有任何问题。但是一旦服务端的私钥泄露，那么之前的会话密钥就会被解密，之前的通讯信息也就暴露了。 回忆一下，D-H 密钥交换过程中 $p,g,A,B$ 是可以公开的，只有$a,b$ 是秘密的，我们假设 $a,b$ 是随机选取的，并且在通讯结束后会被销毁。如果客户端在确认了服务端身份的之后采用 D-H 密钥交换的方式，而不是直接发送公钥加密后的会话密钥，那么即使未来的某一时刻服务端私钥泄露了，也不会泄露会话密钥，也就无法解密之前的通讯信息。 目前这种机制已经被引入 TLS 中。 写在最后 搞密码的这群人是何等的怪物（褒义）。 参考资料 迪菲-赫尔曼密钥交换_百度百科 前向安全性_百度百科 RFC 8446 - The Transport Layer Security (TLS) Protocol Version 1.3
简介 SOCKS5 是一个代理协议，它在使用TCP/IP协议通讯的前端机器和服务器机器之间扮演一个中介角色，使得内部网中的前端机器 ...
简介 SOCKS5 是一个代理协议，它在使用TCP/IP协议通讯的前端机器和服务器机器之间扮演一个中介角色，使得内部网中的前端机器变得能够访问Internet网中的服务器，或者使通讯更加安全。SOCKS5 服务器通过将前端发来的请求转发给真正的目标服务器， 模拟了一个前端的行为。在这里，前端和SOCKS5之间也是通过TCP/IP协议进行通讯，前端将原本要发送给真正服务器的请求发送给SOCKS5服务器，然后SOCKS5服务器将请求转发给真正的服务器。 socks5_百度百科 工作流程 客户端需要先与代理服务器建立 TCP 连接，SOCKS 服务的默认端口是 1080，建立成功后客户端要与代理服务器协商认证方式并完成认证，然后就可以通过代理服务器收发数据。 TCP 连接建立后，客户端会向代理服务器发送请求，包含 SOCKS 协议的版本和客户端支持的认证方式。 协议版本 方法数 方法编号列表 1Byte 1Byte 长度可变 请求格式 协议版本：对于 SOCKS5 协议来说这个字段永远是 5，代表 SOCKS 的第五版协议。 方法数：表示后续的方法编号列表中有多少种客户端支持的认证方法。 方法编号列表：存储客户端支持的认证方法的编号，这些编号有些是 SOCKS 协议预先指定的，有些是 IANA 事后分配的，也有些是给开发者自由使用的，也有的编号代表无需认证。 代理服务器应该选择一个认证方法，并发送下面格式的请求，告知客户端被选择的认证方法。 协议版本 方法编号 1Byte 1Byte 响应格式 完成了认证后，客户端就可以向代理服务器发送代理请求，客户端会向代理服务器发送下面格式的请求 协议版本 指令编号 保留字段 地址类型 目标地址 目标端口号 1Byte 1Byte 1Byte 1Byte 长度可变 2Byte 请求格式 协议版本：对于 SOCKS5 协议来说这个字段永远是 5，代表 SOCKS 的第五版协议。 指令编号：包括 CONNECT 指令，用于 TCP 代理。BIND 指令，一般用于需要客户端主动接受来自服务器连接的时候。UDP ASSOCIATE 指令，用于 UDP 代理。 保留字段：必须为 0。 地址类型：包括 IPV4 地址，域名和 IPV6 地址。 目标地址：要访问的目标服务器的地址或域名。 目标端口号：于目标地址对应的端口号。 代理服务器执行相应的请求后会返回一个或者多个信息，格式如下。 协议版本 结果 保留字段 地址类型 绑定地址 绑定端口号 1Byte 1Byte 1Byte 1Byte 长度可变 2Byte 响应格式 协议版本：对于 SOCKS5 协议来说这个字段永远是 5，代表 SOCKS 的第五版协议。 结果：包含请求的结果，比如成功、常规故障、网络不可达和超时等。 保留字段：必须为 0。 地址类型：包括 IPV4 地址，域名和 IPV6 地址。 绑定地址：即请求成功后客户端需要连接的代理服务器的地址或域名（有可能就是代理服务器本身，也可能被分配到别的代理服务器），客户端之后的通信均通过地址对应的服务器。 绑定端口号：绑定地址对应的端口号。 如果代理服务器返回成功消息，则后续客户端通过绑定地址和绑定端口号与代理服务器通信，由代理服务器转发客户端的请求到目标服务器，并将目标服务器的响应转发给客户端。 如果把上面的信息化成一张简单的交互流程图就是下面这样的。 SOCKS5 通信交互流程 参考资料 RFC 1928 - SOCKS Protocol Version 5
2020 年 10 月 12 日部分网友称自己的手机的时间与北京时间相差较大，慢了几分钟到十几分钟不等，目前已经恢复正常。 有趣的是，「格林尼治国际标准时间」就是在 135 年前的 10 月 13 日诞生的，而今天又是「世界标准日」。那就来简单介绍一下机械钟、石英钟和原子钟原理吧。
写在前面 2020 年 10 月 12 日部分网友称自己的手机的时间与北京时间相差较大，慢了几分钟到十几分钟不等，目前已经恢复正常。 相关新闻：部分安卓手机时间变慢 北京移动：个别基站出了问题-太平洋电脑网 有趣的是，「格林尼治国际标准时间」就是在 135 年前的 10 月 13 日诞生的，而今天又是「世界标准日」。那就来简单介绍一下机械钟、石英钟和原子钟原理吧。 机械钟 机械钟的背后是「摆的等时性原理」，简单来说就是对于一个摆，它的周期与重物的质量和数量无关。 $$ T=2 \pi \sqrt{\frac{L}{g}}，L 是摆长，g 是重力加速度。 $$ 经过简单的计算可以知道当 $L=1m$ 时 $T \approx 2s$，也就是说一个摆长为 1 米的单摆它的周期大概是 2 秒。机械钟的原理就是这个。 摆钟是机械钟的一种，其中有一个结构叫做「擒纵器」，它的工作过程大概是这样的。 擒纵器工作原理 当重物摆动到最左端的时候擒纵器会卡住齿轮的右端，摆动到最右端的时候擒纵器会卡住齿轮的左端，无论卡住哪一端都会让齿轮停止运动、如果摆长是 1 米，那么就是大约每秒就会卡住一次齿轮，只要齿轮的转速和半径合适，就可以通过齿轮和擒纵器制作出秒针，再通过齿轮传动制作出分针和时针，然后一台摆钟的核心部分就做完了。我们听到的嘀嗒嘀嗒声就是擒纵器卡住齿轮时发出的声音。 石英钟 石英晶体本身会按照自身的「固有频率」进行震动，「固有频率」只与物体的本身属性有关，所以非常稳定，但是这个震动十分微弱，难以测量，那么我们就需要将这种震动放大。 石英钟背后的原理是「压电效应」，即某些电介质在发生形变的时候其表面会产生正电荷和负电荷，形变消失后正负电荷也会消失。当对其施加电场时，它也会发生形变，电场消失形变也随之消失。石英晶体就是具有「压电效应」的电介质之一。 有了「压电效应」后，我们可以对石英晶体施加交流电，当交流电的频率与石英晶体的「固有频率」相近时元件其震动就会变强，也就更容易测量。然后我们通过电路去测量这种震动，然后通过转换将其变为 1 Hz 的方波，用这种脉冲去驱动电机来驱动指针旋转，再通过传动装置制作出分针和时针，石英钟的核心部分就完成了。 石英钟的精度差不多 270 年才差 1 秒。 原子钟 原子是有能级的，通常都是在最低能级，处于最低能级的原子称为基态原子。如果一束光打到原子上，只要光的频率合适，就可以让原子跃迁到更高的能级，称为激发态原子。光消失后原子会自发地跃迁到最低能级，也就是回到基态，这个过程会释放光，这个光的能量与入射的光的能量是一样的。下文称能让铯原子跃迁激发态的光的频率为目标频率。 我们难以精确稳定地发出目标频率的光，但是我们可以使得光的频率稳定地在目标频率的周围变化。我们就用这种光打在铯原子团上，光的频率越接近目标频率，处于激发态的铯原子就会越多。然后我们就可以通过测量激发态原子的多少来确定当前的光的频率有多接近目标频率。 测量方式时检测激发态铯原子跃迁到基态时释放的光的强度。因为激发态的铯原子越多，回到激发态释放的光的铯原子也就越多，测量到的光强也就越大，我们会在横轴为照射频率，纵轴为光强的坐标系上看见看见峰值，这个峰值对应的光的频率就是最接近目标频率的光，下文称这个频率为 $f_0$ 由于照射的频率在不断变化，新的峰值也会不断出现，这可以让我们不断地去矫正 $f_0$，$f_0$ 也会越来越接近目标频率。 然后我们将 $f_0$ 频率的光处理为电信号，通过电路去记录震荡次数，由于铯原子的振动频率时已知的，即 9192631770Hz，所以电路每振荡 9192631770 次就过了一秒。这种装置称为铯原子喷泉原子钟（Cesium Fountain Atomic Clock）。 原子钟的精度十分高。2015 年 NASA 的 Deep Space Atomic Clock，可以做到 100 亿年的误差不超过 1 秒。
IA-32 架构的纯软件虚拟化解决方案简介
IA-32 架构起初并不是为了虚拟化设计的，它的目标是在处理器上运行最多一个操作系统，所以在 Intel VT 出现之前， IA-32 架构上的虚拟化解决方案就只有纯软件虚拟化了。
写在前面 本文章属于虚拟化技术系列文章，点击查看本系列的说明。 阅读本文请先阅读下面的虚拟化技术的术语以了解相关术语。 IA-32 架构面临的挑战 IA-32 架构起初并不是为了虚拟化设计的，它的目标是在处理器上运行最多一个操作系统，所以在 Intel VT 出现之前， IA-32 架构上的虚拟化解决方案就只有纯软件虚拟化了。 IA-32 架构的权限等级 Intel 处理器提供了四种权限级别，分别为 Ring0、Ring1、Ring2 和 Ring3，权限依次降低，Ring0 拥有最高权限。所以一些特权指令只能在 Ring0 下才可以正常使用。大多数 IA-32 架构的应用运行在 Ring3。 然而一些操作系统的组件必须运行在 Ring0，但是要实现纯软件的虚拟化就必须让 VMM 独占 Ring0。因为 VMM 要虚拟化出若干组硬件来供 GuestOS 使用，这就要保证 GuestOS 不能直接控制物理硬件。 特权解除和陷入模拟 因此出现了一种技术被称为“特权解除和陷入模拟”。 特权解除是指将 VMM 运行在 Ring0，而虚拟机中的操作系统运行在 Ring1。这样就可以保证 GuestOS 无法越过 VMM 去执行特权指令导致虚拟化失效。 同时由于 IA-32 架构下运行特权指令的时候如果权限不足会自动陷入权限更高的 Ring 中。所以当 GuestOS 执行特权指令的时候会陷入 Ring0，此时由 VMM 接管并模拟特权指令的执行，这就叫陷入模拟。 不幸的是整个过程没有看起来那么简单。究其根本是因为虚拟机中的软件并不是为了虚拟化而编写的。 有的指令在非 Ring0 下运行会返回一个包含当前 Ring 值的返回值，GuestOS 会去读取这个值，一旦发现自己没有运行在 Ring0 下就会出现错误。这也就导致了这类指令的调用无法被 VMM 检测到，自然也就无法模拟了。 另一个问题是操作系统在编写的时候并没有支持操作系统级的上下文切换，所以一些上下文可能会在很隐蔽的角落。当 VMM 切换上下文的时候这些信息就会丢失导致错误。 并非只有这些问题，受限于篇幅问题就不再多说了，读者有兴趣可以自行查阅资料。 纯软件解决方案 为了应对这些挑战，设计人员给出了两种软件层面的途径去开发 VMM，分别是半虚拟化（Paravirtualization）和二进制翻译（Binary Translation）。 半虚拟化（Paravirtualization） 这种方法需要修改 GuestOS 的源代码使得其可以在特定的 VMM 下运行。比如敏感指令替换为 hypercall。hypercall 直译为超级调用，可以陷入到 VMM 中以便 VMM 去模拟指令的运行。 这种方式的好处是基本没有性能损失。但是坏处也很明显，它只能用于开源的操作系统，比如 Linux。 二进制翻译（Binary Translation） 这个方法是将 GuestOS 的二进制指令翻译为另一种类指令，通常是翻译为 vCPU 可以识别的指令。它的特殊之处在于大多数情况下我们都是将源代码或者 IR（Intermediate Representation） 翻译为二进制代码，但是二进制翻译是将二进制代码翻译为另一类代码。按照翻译的时机分类又分为静态翻译和动态翻译。 静态翻译（Static Binary Translation） 即在运行 GuestOS 前一次性地将全部的代码翻译为目标指令。这样做可以最大限度地提高性能，但是这个方法实际上并不可行。 最大的难题是冯·诺依曼体系结构下指令和数据存储在一个存储器上，并未区分指令存储器和数据存储器。也就是说对于一堆静态的二进制数据，我们无法确定它到底是代码还是数据。这也就导致静态翻译可能错误地将数据翻译为代码从而导致错误。 还有一个问题是有些程序会在运行时修改自己，而静态翻译是一次性的，这也会导致错误。 动态翻译（Dynamic Binary Translation） 动态翻译就是在运行时以一种类似按需翻译的方式去翻译 GuestOS 的二进制指令。这可以很好地解决静态翻译带来的问题。 因为只会翻译被执行的部分，所以不会错误地将数据翻译为二进制代码。同时由于翻译并不是一次性地工作，也可以解决程序修改自身所带来的问题。 不过实际实现起来会有很多问题，比如何时启动翻译、翻译什么和翻译多少。这也是本系列的后续任务，将会简单地介绍一些实现动态翻译中遇到的问题和解决方法。本文在这一点不做过多讨论。 参考资料 An Introduction to Virtualization CSE 598 Binary Translation Report Dynamic Binary Translation
介绍虚拟化技术中的一些常见术语。
写在前面 本文章属于虚拟化技术系列文章，点击查看本系列的说明。 宿主机（Host Machine） 宿主机是运行虚拟机的物理机，是真实地存在于物理世界的机器。它包含如 CPU，内存和网卡等物理资源。 虚拟机（Virtual Machine） 虚拟机是对物理机虚拟化后的结果，并由虚拟化软件运行和维护。 客机操作系统（GuestOS: Guest Operating System） 指运行在虚拟机中的操作系统。 虚拟机管理程序/虚拟机监视器（Hypervisor/VMM） 实际上就是管理虚拟机和实现虚拟化的程序。在比较老的资料里这两个名词可能是不同的概念，即 Hypervisor 是直接运行在硬件上的，而 VMM 是运行在软件上的，但貌似目前这两个名词已经差不多一样了，不需要去可以区分了。 半虚拟化（Paravirtualization） 通过修改操作系统的源代码实现的虚拟化，比如将一些敏感指令（可能导致虚拟化出错的指令）修改为 hypercall 使 VMM 可以模拟这类执行的执行以避免错误。 全虚拟化（Full virtualization） 即不修改操作系统的源代码就可以实现的虚拟化。 虚拟 CPU（vCPU） 指 VMM 虚拟出来的供虚拟机使用的 CPU，并不存在于物理世界中。 参考资料 An Introduction to Virtualization
在我们了解如今成熟稳定的虚拟化技术之前，有必要去了解一下它的发展史。因为虚拟化并不是一项全新的技术，而是基于之前的努力。
写在前面 本文章属于虚拟化技术系列文章，点击查看本系列的说明。 在我们了解如今成熟稳定的虚拟化技术之前，有必要去了解一下它的发展史。因为虚拟化并不是一项全新的技术，而是基于之前的努力。 1950年 - 1990年 虚拟存储器的概念可以追溯到 20 世纪 50 年代末，当时曼彻斯特大学的一个小组在 Atlas 系统（一种晶体管计算机）中引入了自动换页功能，其原理是将分页作为一种在内存层次结构之间存储和传输数据的方法。虽然这个原理之前就有，但Atlas是第一个将这一过程自动化，从而提供了第一个虚拟存储器的工作原型。 虚拟机这个词可以追溯到20世纪60年代，最早的虚拟机系统来自 IBM。1967 年前后，IBM 推出了型号为 67 的 System/360，它是第一个带有虚拟存储器的系统。这个系统中有一个概念名为处理器虚拟化指令集，这个概念也在也在后续的型号中得到完善。起初这个系统被称为 CP-67，之后就演化成了虚拟机系统。虚拟机允许在一台处理机上运行多个操作系统。本质上来说虚拟机和硬件通过合作来启动多个操作系统，让它们可以受保护地访问完整的指令集。 20 世纪 60 年代中期，IBM 还率先推出了 M44/44X 项目，探索新兴的分时概念。系统架构的核心是一组虚拟机，每个用户可以使用其中的一个。宿主机是 IBM 7044（简称 M44），每个虚拟机是 7044 的实验镜像（简称44X）。这项工作最终促成了虚拟机/分时系统，包括 IBM 著名的 VM/370。 硬件虚拟化的概念也是在这个时期出现的，VMM 可以在一个隔离和保护的环境中运行虚拟机。因为 VMM 对运行在虚拟机中的软件来说是透明的，软件认为它独占控制权。随着时间的推移，这一概念得到了完善，最终 VMM 只会消耗较少的性能和资源。 20 世纪 70 年代中期，虚拟化技术已经在各个操作系统被用户所接受。在这几十年中，虚拟化技术解决了许多重要问题，例如应用还在大规模操作系统中虚拟存储器使得应用程序的编程变得简单，因为程序员不需要像以前那么关心内存不足的问题。 虚拟出来资源是很便宜的，但是真实的物理资源却很贵，虚拟资源可以让我们按需分配这些昂贵的物理资源以获取最大的利益。比如现在厂商可以将多余的算力虚拟化成一个个的云服务器出售给客户。 虽然在 20 世纪 60 年代和 70 年代，硬件级虚拟机在研究和商业市场上都很流行，但在 20 世纪 80 年代和 90 年代，它们基本上消失了。因为低成本的微型计算机和 PC 进入市场导致了虚拟化的需求在总体上有所下降。 顺便一提，另一种类型的虚拟机在历史上仍有一席之地，典型的代表就是 JVM 和 微软的 CLR。这一类型的虚拟机不是本文所要介绍的虚拟机，它们并不虚拟化出一个硬件平台，而是为编程语言提供可移植性。所以请不要混淆概念。 Runtime Virtual Machines 虚拟化的重新兴起 20 世纪 90 年代，企业部署的服务器数量越来越多，但是服务器资源的利用效率却越来越低。 一个服务器仅仅部署一个应用会大量浪费服务器的硬件资源。一个服务器部署多个应用则会遇到更为严重的问题，比如其中一个应用崩溃了可能会带着整个服务器崩溃，那么这台服务器上所有的应用都会停止。又比如某个应用更新后需要重启服务器，但是这台服务器上还有别的应用，所以其它应用也被迫重启。 服务器数量的激增带来了部署、更新和支持方面的挑战，以及安全和灾难恢复方面的问题。 虚拟化技术很好地解决这一问题。可以在一台宿主机上虚拟出多个虚拟机，每个虚拟机仅部署一个应用。由于每个虚拟机时互相隔离的，所以崩溃一个其它的依然可以正常运行，上面的问题也就迎刃而解了。 虽然虚拟化技术重新兴起，但是开发用于虚拟化的操作系统和软件是一件十分复杂的事情。Intel VT 的出现消除或大大减少了其中的一些复杂因素。Intel 开始与 VMM 的开发者合作，在 Intel 处理器和芯片组中实现硬件辅助虚拟化，并推动相关规范的制定以促进虚拟化在未来的发展。 参考资料 An Introduction to Virtualization
为什么 JavaScript String 会有 slice() 和 substring() 这两个方法？
slice() 和 substring() 这两个方法都可以截取字符串，那么为什么会设计这两个 API 呢？
写在前面 slice() 和 substring() 这两个方法都可以截取字符串，那么为什么会设计这两个 API 呢？ 二者的区别 substring() 和 slice() 方法几乎是相同的，但两者之间有一些微妙的区别，特别是在处理负参数的方式上。 如果 indexStart 大于 indexEnd，substring() 方法会交换两个参数，这意味着仍然会返回一个字符串。但是 slice() 方法面对这种情况会返回一个空字符串。 let text = 'Mozilla' console.log(text.substring(5, 2)) // => "zil" console.log(text.slice(5, 2)) // => "" 如果参数中的一个或两个都是负数或 NaN，则 substring() 将它们当作 0 处理。 console.log(text.substring(-5, 2)) // => "Mo" console.log(text.substring(-5, -2)) // => "" slice() 也将 NaN 参数视为 0，但当它为负时，它会从字符串的末尾向前数，以找到索引。 console.log(text.slice(-5, 2)) // => "" console.log(text.slice(-5, -2)) // => "zil" 共存的原因 乍一看 slice() 和 substring() 做的是同样的事情，但最大的区别在于它们如何处理负参数。 当 Netscape 2.0 首次实现 JavaScript 时，只有一个 substring() 方法。如果它的任何一个参数是负数，就会被当作 0 处理。 当 Netscape 4.0 引入 JavaScript 1.2 后，他们希望通过支持负索引来从反向遍历字符串。不过不能修改 substring() 的行为 ，因为这会破坏现有的代码的兼容性，所以只能创建一个新的函数来支持新增的功能。这个函数就是 slice()，在 Array 和 String 上都实现了。 参考资料 String.prototype.substring() - JavaScript | MDN javascript - Why two different methods slice() & substring()? - Stack Overflow JavaScript Substring Vs Substr Vs Slice Differences With Examples | Arunkumar Blog
校园网保证一机一号和检测共享上网的原理
出于利益等原因，许多校园网采取一机一号的管理方式。即一个宽带账号只能由一台规定的设备使用。同时检测共享上网以避免使用路由器或类似方式实现共享。那么这些限制背后的原理是什么呢？
写在前面 出于利益等原因，许多校园网采取一机一号的管理方式。即一个宽带账号只能由一台规定的设备使用。同时检测共享上网以避免使用路由器或类似方式实现共享。那么这些限制背后的原理是什么呢？ 一机一号的原理 目前大多数校园网的拨号协议依然是 PPPoE，所以要知道一机一号的原理就要先粗略了解一下 PPPoE 。 PPPoE 的全称是（Point-to-Point Protocol Over Ethernet），即以太网上的点对点协议。以太网本身不具有身份验证功能 但是 PPP 可以，所以将 PPP 引入以太网可以满足 ISP 对用户身份的验证需求。 PPPoE 运行在 OSI 的数据链路层，TCP/IP 体系结构的网络接口层，同时由于它只在拨号时起作用，所以此时没有 IP 地址起作用，由此可以推测如果要实现一机一号，关键的信息之一是 MAC 地址。事实上也确实如此。 PPPoE 拨号时会在「发现」阶段向以太网中发送广播，表示自己想要找到一个可以提供网络服务的节点，并在广播中说明自己的 MAC 地址。ISP 的节点接收到消息后会回应这个广播并说明自己的 MAC 地址，然后两个节点就建立了会话。此后便使用 PPP 通信，通信过程中就包含了身份认证过程。 注意到此时 ISP 的节点只有拨号设备的 MAC 地址和宽带的账号和密码。看来答案已经出现了。ISP 通过维护宽带账号和 MAC 地址的一一对应的关系来实现一机一号。只要发现 MAC 地址和宽带账号不匹配直接报错就行了。 共享上网检测的原理 仅仅一机一号还不够，因为可以通过路由器实现多人共用同一个宽带。路由器通过 NAT 让多个内网 IP 共用一个 IP 与 ISP 节点通信，这给共享上网的检测带来的困难，但依然有一些方法。 检测 IP 报文头 identification 字段 <img loading="lazy" decoding="async" width="1024" height="545" src="https://www.addesp.com/wp-content/uploads/2020/10/0-1024x545.png" alt="" class="wp-image-1788" srcset="https://www.addesp.com/wp-content/uploads/2020/10/0-1024x545.png 1024w, https://www.addesp.com/wp-content/uploads/2020/10/0-300x160.png 300w, https://www.addesp.com/wp-content/uploads/2020/10/0-768x409.png 768w, https://www.addesp.com/wp-content/uploads/2020/10/0.png 1271w" sizes="auto, (max-width: 1024px) 100vw, 1024px" /> 上图就是 IP 报文头的结构，第二行的左边就是需要被检测的字段，下面是该字段的说明。 An identifying value assigned by the sender to aid in assembling the fragments of a datagram. RFC-791 看起来似乎不能用来检测共享上网？单单这样看确实不能，但是 Windows 网络协议栈在实现的时候这个值是随着 IP 报文的发送数量的增加而增加的，且初始值是一个随机数。所以对于使用 Windows 的设备来说，它们的 identification 的初始值通常是差距很大的，所以可以用来检测共享上网。 值得一提的是这并不适用于 Linux、Unix，因为它们的 identification 的值是完全随机的，没有规律可循。 TTL 检测 TTL是 Time To Live的缩写，该字段指定IP包被路由器丢弃之前允许通过的最大网段数量。 TTL（生存时间值）_百度百科 不同的操作系统的 TTL 可能是不同的，所以如果检测到两个不同的 TTL 则可以断定网络中有两台设备。 时间戳检测 不同物理机的时钟偏移通常是不同的，这也就使得物理时钟会与协议栈时钟之间存在一种通常不同于其它设备的特殊的联系。同时主机发送报文的频率与时钟又存在统计上的关系，所以可以通过这一点来检测出又几种不同的时钟偏移从而检测网络中的设备数量。 深度报文检测（DPI） DPI（Deep Packet Inspection）是一种基于数据包的深度检测技术，针对不同的网络应用层载荷（例如HTTP、DNS等）进行深度检测，通过对报文的有效载荷检测决定其合法性。 DPI（深度报文检测）_百度百科 主要通过检测应用层的报文来判断设备数量。 比如通过检测 Http.UserAgent 字段来判断。一个用户使用的浏览器通常是固定的，而浏览器发送请求时所使用的 UserAgent 也是相对固定的。在排除一个用户设备上的许多客户端应用也使用 HTTP 协议的情况下，如果一个网络中的 Http.UserAgent 有许多不同的值，则可以怀疑这个网络中有许多不同的设备在上网。 参考资料 RFC 2516 - A Method for Transmitting PPP Over Ethernet (PPPoE) RFC 791 - Internet Protocol 电信是怎样查出多台电脑共享上网的？ - 稍有常识的人的回答 - 知乎
问题 昨晚有人问了我一个问题——下面的代码输出的两行内容相同么？ int main() { int a[5][3]; printf("%p\n", &a[3][2]); printf("%p", *(a + 3) + 2); } 我的错误答案 我的答案是不一样，因为我认为 *(a + 3) 是一个值，所以一个值加 2 还是一个值，而 &a[3][2] 明显是一个地址，所以不一样。 我的理由是，虽然逻辑上 a 是一个二维数组，但是物理上（内存模型）依然是一段连续的内存，且 a 指向内存的首地址，也就是说我认为 int* a = 数组首地址; 。那么对于一个一级指针解引用自然就会得到一个值了。 但是结果是我错了，代码所输出的两行内容完全相同。 分析 当时我整个人都傻了，感觉对不起 C 语言老师。那么现在的结论是 &a[3][2] == *(a + 3) + 2，要如何解释这个现象呢？没有别的好办法，只能去看汇编了。 代码文件 main.c #include <stdio.h> int main() { int a[5][3]; printf("%p\n", a); printf("%p", *(a + 1)); } 使用 gcc 编译 gcc -m32 -S main.c 得到下面的汇编代码 .file "main.c" .text .section .rodata .LC0: .string "%p\n" .LC1: .string "%p" .text .globl main .type main, @function main: .LFB0: ... ; eax = 二维数组 a 的首地址 leal -72(%ebp), %eax ; 将 eax 的内容作为参数传递给 printf pushl %eax ; eax = 字符串 "%p\n" 的首地址 leal .LC0@GOTOFF(%ebx), %eax ; 将 eax 的内容作为参数传递给 printf pushl %eax ; 调用 printf call printf@PLT addl $16, %esp ; eax = 二维数组 a 的首地址 leal -72(%ebp), %eax ; eax += 12 ; 12 Byte = 3 * 4 Byte ; int 的大小为 4 Byte addl $12, %eax subl $8, %esp ; 将 eax 的内容作为参数传递给 printf pushl %eax ; eax = 字符串 "%p" 的首地址 leal .LC1@GOTOFF(%ebx), %eax ; 将 eax 的内容作为参数传递给 printf pushl %eax ; 调用 printf call printf@PLT ... 可以看到对于二维数组 a，执行 a + 1 时编译器会自动地帮我们计算出第 1 行的首地址，而不是像普通的一级指针仅仅移动一个单位而已。 后续测试发现将 a + 1 改为 *(a + 1) 后汇编代码没有发生改动。所以对于二维数组 a 来说，a + 1 等价于 *(a + 1)。 所以就不难理解为何 &a[3][2] == *(a + 3) + 2 了。 为什么编译器会做这种处理呢？首先确定这是不是一个未定义行为。我在 C99 的标准文档中找到了这些内容 6.5.2.1 Array subscripting Constraints 1 One of the expressions shall have type ‘‘pointer to object type’’, the other expression shall have integer type, and the result has type ‘‘type’’. Semantics 2 A postfix expression followed by an expression in square brackets [] is a subscripted designation of an element of an array object. The definition of the subscript operator [] is that E1[E2] is identical to (*((E1)+(E2))). Because of the conversion rules that apply to the binary + operator, if E1 is an array object (equivalently, a pointer to the initial element of an array object) and E2 is an integer, E1[E2] designates the E2-th element of E1 (counting from zero). 3 Successive subscript operators designate an element of a multidimensional array object. If E is an n-dimensional array (n ≥ 2) with dimensions i × j × . . . × k, then E (used as other than an lvalue) is converted to a pointer to an (n − 1)-dimensional array with dimensions j × . . . × k. If the unary * operator is applied to this pointer explicitly, or implicitly as a result of subscripting, the result is the pointed-to (n − 1)-dimensional array, which itself is converted into a pointer if used as other than an lvalue. It follows from this that arrays are stored in row-major order (last subscript varies fastest). 4 EXAMPLE Consider the array object defined by the declaration int x[3][5]; Here x is a 3 × 5 array of ints; more precisely, x is an array of three element objects, each of which is an array of five ints. In the expression x[i], which is equivalent to (*((x)+(i))), x is first converted to a pointer to the initial array of five ints. Then i is adjusted according to the type of x, which conceptually entails multiplying i by the size of the object to which the pointer points, namely an array of five int objects. The results are added and indirection is applied to yield an array of five ints. When used in the expression x[i][j], that array is in turn converted to a pointer to the first of the ints, so x[i][j] yields an int. 看起来人家早就考虑到这个问题了并将计算规则写入了标准。 既然这不是一个未定义行为，那么为什么 C 标准协会会如此规定呢？不知道，不过我猜大概是因为 C 语言还算是一个高级语言，还是要屏蔽一些底层的东西的。如果你有不同的看法希望可以在评论区留言一同讨论。 最后一段 起初我得出错误答案的时候还是满脸自信的，告诉他这个绝对不会错，因为内存模型就是一维数组，看成一个一级指针就行了，结果是自己做实验去否定自己。果然还是自以为懂得多啊。
写在前面 跨服聊天是指交谈双方，不在同一语境下进行交流。多因双方的兴趣爱好/知识差异或聊天内容本身容易引发歧义引起。 双方使用语言的环境不同，对语句的理解也不同。同一句话，在不同的主题会产生不同的理解。以「你幸福么？」为例：我姓曾。 跨服聊天 - 萌娘百科 跨服聊天通常会出现在作品中用来提供笑料。如此的话，直觉上跨服聊天应该离我们很远。真的么？以前我这么认为，但是现在我觉得离我很近。 一件趣事 作为一个写代码的秃头人士，合作项目是很正常的事情，不过我的经历大都是和身边的人合作，双方都比较了解，有了问题直接面谈，基本不会出现跨服聊天这种情况。 今年我尝试与云游君合作一个项目，大概是由于从未谋面，互相不了解，涉足的领域差距比较大，导致跨服聊天变成了家常便饭，很多时候商量了半天的事情，后面会发现讨论的根本不是一个东西。 我们在整体架构上出现了分歧。出现分歧很正常，沟通么，交换一定量的信息后通常就可以达成一致了。但是因为跨服聊天的原因，沟通效率十分低下（相对于和身边人的沟通），这段时间大概一周左右，后来因为所使用的库作者表示要进行大的改动而暂停开发，现在这个项目已经咕掉了。 虽然是因为外部原因暂停的开发，但是如果按照当时的沟通效率做下去的话，恐怕最终也搞不出什么好的项目来，能跑起来就是神迹了。 抢救过程 讨论过程中常见的现象是说着说着出现了分歧，然后沟通讨论，发现没有达成共识的机会，最后发现双方虽然说的是同一个功能，同一个模块，但是细节上却不同。分析后我认为是双方对需求的理解不一致。 因此我提出使用数据流图来协助讨论，云游君也同意了。于是我们合作完成了一张数据流图，期间又发现了许多分歧，但是都解决了。 但是你以为这就完了么？可怕的事情还在后面。没多久我们就开始了初期的编码，结果还是出现了很大分析，期间又是大型跨服聊天现场。在我们还在想怎么提高交流效率的时候，我们所使用的库的作者表示要大改，我们就顺势暂停了开发，然后因为一些原因，这个项目就彻底停下来了。 从《软件工程》的角度分析 接下来复习一下，传统方法学的软件生命周期中的三个时期和八个阶段是什么？软件定义时期、软件开发时期和软件维护时期；问题定义、可行性研究、需求分析、概要设计、详细设计、编码和单元测试、综合测试和运行维护。 由于双方从未对发起这么项目的目的产生过任何分歧，由此我认为问题定义与可行性研究是没有问题的。而万恶之源显然就是需求分析了。 传统方法学中要求在需求分析过程中建立软件三种模型： 数据模型：理解并描述问题的信息域 功能模型：定义软件应完成的功能 行为模型：描述系统的状态以及引起系统状态转换的事件 回到我与云游君的讨论中，发现了问题后我提议合作完成一张数据流图，也就是希望建立一个双方都准确地了解并认可的数据模型，客观来看，这推动了我们的初次编码的进程。 但是编码没有多久就被迫停下来，一个模块的都有哪些方法，哪些属性？各个模块之间如何联系，这些问题被摆到了讨论桌上，如果不是因为外部原因还不知道要跨服聊天多少次。 看起来这次是因为功能模型和行为模型并未建立，所以我们对对方的想法了解的不够准确，就算是准确也未必认同。 那么如果三个模型都建立了是不是就没问题了？不知道，但是根据现在的情况来看，基本不可能。事实是虽然我们合作完成了数据流图，但是这张图许多地方需要分解。分解后会出现什么分歧也是未知的，这项工作是需要在概要设计中做的。 上文不是什么深入的分析，只是很表层的东西，却让我不得不重新审视《软件工程》这门学科。 重新审视《软件工程》 仅仅是两个人合作，就出现了这么麻烦的事情。双方因为经历、涉足领域、之前并不认识和其它未知的原因导致了跨服聊天大量出现。如果实行切实有效的措施，在企业中这种现象只会更加频繁，后果也会更加致命。 我也发现了一个情况，就是我们似乎更加重视《数据结构》、《XX 语言开发》这种课程，相对来说对《软件工程》的重视程度就要低很多了。 软件工程(Software Engineering，SE)软件工程是一门研究用工程化方法构建和维护有效的、实用的和高质量的软件的学科。 软件工程 - MBA 智库百科 这是软件工程的定义之一，当初看到的时候没有什么感觉，认为这是一句说了和没说一样的话。 现在我似乎能看到这句话的背后可能有无数的开发组遇到了我和云游君一样的问题。为了解决问题，前辈们殚精竭虑地总结出了一套行之有效的工程化方法，帮助后人在开发维护过程中少走弯路，提高效率。 起初我没有理解这些，被现实教训了一顿。不过学习之路似乎就是这样的，当初并不知道这些有什么用，为啥要学，直到身临其境的时候才会感叹前人的智慧。 要说这些工程化方法有哪些问题？一大问题就是许多企业尤其是中小企业无法承受严格按照流程进行开发的成本，可能是时间或者金钱上的成本，或是两者兼有。 个人的小软件大多数情况下完全没必要严格按照流程来，更多的是灵光一现立马开始编码，小几百行甚至几十行代码就能实现的功能。 应对需求频繁变化的场景前辈提出了极限编程；为了提高需求分析的质量前辈提出了快速原型；以及现在的微软过程等。每个都是对这门的学科的发展，使其适应新时代的变化，提高生产效率。 不过这不是说以后开发软件都要严格按照流程来，能解决问题的方法就是好方法，而且这个方法没准以后也会成为《软件工程》的一部分。毕竟科学界从来都没有真理存在，有的只是能让人信服的证据，以及推翻那些曾经被信服的证据的证据。
基本格式 #!/bin/bash echo 'Hello World' #!/bin/bash：脚本的第一行声明了脚本的解释器，对于 shell 脚本解释器通常为 /bin/bash，当然如果是 python 脚本也可以写成 #!/bin/python。 第一行之后的所有内容就是具体的命令，变量以及函数了。 运行方式 # 使用脚本中声明的解释器解释执行，如果未声明则使用当前 shell 解释执行。 ./script.sh # 使用指定的解释器执行 # 使用 /bin/sh 解释执行 sh script.sh # 使用 /bin/bash 解释执行 bash script.sh 变量 永久环境变量 本质属于文件，对于所有 shell 均可访问。 临时环境变量 只允许当前 shell 以及其子 shell 访问。一般通过 export 来设置。 export VAR_NAME=alice 临时变量 仅仅允许当前 shell 访问。直接在声明即可。shell 脚本中不存在高级语言中局部变量这个概念，也就是 shell 脚本中任何位置声明的变量均为全局变量。 var1=1 特殊变量 变量 含义 $0 当前脚本的文件名 $n 传递给脚本或函数的参数。n 是一个数字，表示第几个参数。例如，第一个参数是$1，第二个参数是$2。 $# 传递给脚本或函数的参数个数。 $* 传递给脚本或函数的所有参数。 $@ 传递给脚本或函数的所有参数。被双引号(" ")包含时，与 $* 稍有不同，下面将会讲到。 $? 上个命令的退出状态，或函数的返回值。 $$ 当前Shell进程ID。对于 Shell 脚本，就是这些脚本所在的进程ID。 \$* 和 \$@ 这两个变量在不使用双引号包裹的时候完全一样 #!/bin/bash function foo() { for var in $* do # -e 表示启用转义字符 echo -e "$var\n" done } foo 1 2 3 4 5 #!/bin/bash function foo() { for var in $@ do # -e 表示启用转义字符 echo -e "$var\n" done } foo 1 2 3 4 5 上面两个脚本的执行结果一样： 1 2 3 4 5 但是一旦被双引号包裹就不一样了 #!/bin/bash function foo() { for var in "$*" do # -e 表示启用转义字符 echo -e "$var\n" done } foo 1 2 3 4 5 输出为 1 2 3 4 5[换行] #!/bin/bash function foo() { for var in $@ do # -e 表示启用转义字符 echo -e "$var\n" done } foo 1 2 3 4 5 输出结果依旧为 1 2 3 4 5 基础算数运算 可以进行简单的四则运算。但是 bash 不支持浮点运算，所以除法会直接截断小鼠部分。同时由于 * 在 shell 中有特殊的含义，所以必须使用 \ 进行转义才能使用乘法。 # 结果为 3 expr 1 + 2 # 结果为 -1 expr 1 - 2 # 结果为 6 expr 2 \* 3 # 结果为 2 expr 5 / 2 条件判断 Shell中的 test 命令用于检查某个条件是否成立，它可以进行数值、字符和文件三个方面的测试。 test 可以用 [] 代替，如 test $var1 -eq $var2 与 [ $var1 -eq $var2 ] 等价。但是不允许省略空格，也就是说 [$var1 -eq $var2] 是错的。 数字比较 参数 作用 举例 -eq 相等则为真 test $var1 -eq $var2 -ne 不等则为假 test $var1 -ne $var2 -gt 大于则为真 test $var1 -gt $var2 -ge 大于等于则为真 test $var1 -ge $var2 -lt 小于则为真 test $var1 -lt $var2 -le 小于等于则为真 test $var1 -le $var2 数字比较 字符串比较 参数 作用 举例 = 等于则为真 test $str1 = $str2 != 不等则为真 test $str1 != $str2 -z 字符串长度为零则为真 test -z $str1 -n 字符串长度不为零则为真 test -n $str1 字符串比较 文件判断 参数 作用 举例 -e 如果文件存在则为真 test -e $filename -r 如果文件存在且可读则为真 test -r $filename -w 如果文件存在且可写则为真 test -w $filename -x 如果文件存在且可执行则为真 test -x $filename -s 如果文件存在且至少有一个字符则为真 test -s $filename -d 如果文件存在且为目录则为真 test -d $filename -f 如果文件存在且为普通文件则为真 test -f $filename -c 如果文件存在且为字符型特殊文件则为真 test -c $filename -b 如果文件存在且为块特殊文件则为真 test -b $filename 文件判断 流程控制 if 语法 if boolean ; then # TODO fi if boolean then # TODO fi 举例 shell 代码 #!/bin/bash if [ 1 -eq 2 ] ; then echo '1 == 2' 等价 C 代码 if (1 == 2) { printf("1 == 2"); } if···else 语法 if boolean ; then # TODO else # TODO fi if boolean then # TODO else # TODO fi 举例 shell 代码 #!/bin/bash if [ 1 -eq 2 ] ; then echo '1 == 2' else echo '1 != 2' fi 等价 C 代码 if (1 == 2) { printf("1 == 2"); } else { printf("1 != 2"); } if···elif if boolean ; then # TODO elif boolean ; then # TODO fi if boolean then # TODO elif boolean then # TODO fi for 语法 for var in item0 item1 item2 .... do # TODO done 举例 #!/bin/bash for num in 1 2 3 4 5 do # -e 代表开启字符转义 echo -e "${num}\n" done 结果 1 2 3 4 5 while 语法 while boolean do # TODO done 举例 shell 代码 #!/bin/bash max=6 num=0 while [ $num -lt $max ] do echo -e "${num}\n" num=`expr $num + 1` done 等价 C 代码 int max = 6, num = 0; while (num < max) { printf("%d\n", num); num += 1; } 函数 语法 # 声明函数（无论有无参数括号内均为空） function name() { # TODO return value } # 调用参数，后面跟上若干的参数 name 1 2 3 ... 其中 function 可以省略，return 也可以省略，返回值默认为最后一条指令的执行结果，return 只允许接数字，且范围为$[0,255]$。 举例 #!/bin/bash function sum() { ret=0 for num in $@ do ret=`expr $ret + $num` done return $ret } # 计算 1 + 2 + 3 sum 1 2 3 echo $? 双引号、单引号和反引号 双引号 双引号中允许使用变量。 #!/bin/bash str=World echo "Hello $str" 结果为 Hello World。 单引号 单引号中的内容将原文输出，但是不对转义符生效。 #!/bin/bash str=World echo 'Hello $str' 结果为 Hello $str。 反引号 会将反引号中的命令作为返回值 #!/bin/bash list=`ls` echo $list 结果是当前目录中的文件。 $() $() 类似函数一样让我们将某个命令赋值给某个变量，当使用该变量时才会执行对应的命令并将返回值作为变量的值。 (()) 扩展运算，支持了 C 语言大部分的写法。 在双括号结构中，所有表达式可以像c语言一样，如：a++，b-- 等。 在双括号结构中，所有变量可以不加入：$ 符号前缀。 双括号可以进行逻辑运算，四则运算。 双括号结构 扩展了 for，while, if 条件测试运算 支持多个表达式运算，各个表达式之间用 , 分开。 #!/bin/bash num=1 ((num++)) ((num=num+1+2-3*4/5)) ((num+=1+2-3*4/5)) # 打印 [0...9] for((i=0;i<10;i++)) do echo -e "$((i))\n" done # 如果 5 能被 2 整除则输出 true，反之输出 false if ((5%2==0)) ; then echo 'true' else echo 'false'
mysql ERROR 1051 症状 mysql 版本：8.0.21 执行删除表命令 drop table tbl_name; 会报错ERROR 1051 (42S02): Unknown table 'demo_db.tbl_name'。 原因 如果不是两次安装 mysql 导致 store engine 不同的话可能是因为 tbl_name 不是一个 table，而是一个 view。 平常我们使用下面的命令来列出所有表 show tables; +-------------------+ | Tables_in_demo_db | +-------------------+ | demo_table | | demo_view | +-------------------+ 但是容易被忽略的一点是该命令会连带 view 一起列出来。为了只列出 table，可以使用下面这条命令。 show full tables; +-------------------+------------+ | Tables_in_demo_db | Table_type | +-------------------+------------+ | demo_table | BASE TABLE | | demo_view | VIEW | +-------------------+------------+ 这样就可以明确知道其类型是 table 还是 view。 解决 对于 view 和 table 使用不同的语句进行删除。 服务启动失败 情况一 症状 mysql 版本：8.0.21 使用 systemctl start mysql 显示 -- Subject: Unit mysql.service has begun start-up -- Defined-By: systemd -- Support: http://www.ubuntu.com/support -- Unit mysql.service has begun starting up. Oct 16 16:36:45 VM-0-11-ubuntu audit[7397]: AVC apparmor="STATUS" operation="profile_replace" info="same as current prof Oct 16 16:36:45 VM-0-11-ubuntu kernel: audit: type=1400 audit(1602837405.154:33): apparmor="STATUS" operation="profile_r Oct 16 16:36:45 VM-0-11-ubuntu systemd[1]: mysql.service: Main process exited, code=exited, status=1/FAILURE Oct 16 16:36:45 VM-0-11-ubuntu systemd[1]: mysql.service: Failed with result 'exit-code'. Oct 16 16:36:45 VM-0-11-ubuntu systemd[1]: Failed to start MySQL Community Server. -- Subject: Unit mysql.service has failed -- Defined-By: systemd -- Support: http://www.ubuntu.com/support -- Unit mysql.service has failed. 查看 mysql 错误日志 cat /var/log/mysql/error.log mysqld: File './binlog.index' not found (OS errno 13 - Permission denied) 原因 mysql 目录的所属用户和用户组不是 mysql。 解决 将配置文件中所展现出现的目录如 /var/lib/mysql 的所属用户和用户组改为 mysql。 参考 mysqld: File './binlog.index' not found (OS errno 13 - Permission denied)_albertjone的博客-CSDN博客
讲个笑话：“Windows 10 is the best OS for gaming”。
写在前面 Windows 10 is the best OS for gaming. 我信了，你呢？ 每次我重装 Windows10 之后，只要使用系统自带的更新更新一次，重启后大概率就搜索不到 WIFI 了，而且重装驱动啥的根本没用，最后只能再次重装祈祷不要出事。 不愧是最适合玩游戏的系统。 症状 WIFI 列表中搜索不到任何信号。 解决方法 重启电脑 遇到问题先重启也算是常识了，如果重启无法解决请尝试下一个方法。 重装网卡驱动 依次点击：此电脑 -> 管理 -> 设备管理器 -> 网络适配器。 如果网卡驱动异常请尝试备份驱动后重新安装。或者下载到官方的驱动后再卸载当前驱动并安装刚下载的驱动。 如果无法解决请看下一个方法。 清理注册表 本方法属于玄学方法，来自如果问题无法解决，那就换个问法继续解决。该文中提到使用 CCleaner 清理注册表并重启电脑后问题就解决了。
在 WSL2 上使用 snapd 和 snaps 像是小聪明（非贬义）一样的行为，你可能会遇到许多奇怪的事情，不建议在常规情况下使用。
版权声明 本文由 ADD-SP 由英文翻译为简体中文。 原文作者：zyga 原文链接：https://discourse.ubuntu.com/t/using-snapd-in-wsl2/12113 译文 免责声明 在 WSL2 上使用 snapd 和 snaps 像是小聪明（非贬义）一样的行为，你可能会遇到许多奇怪的事情，不建议在常规情况下使用。 这句话的意思应该是不推荐在类似生产环境这样的环境中使用。 译者注 先决条件 Windows10 家庭版或专业版，Windows Insider Fast ring 或 2019 秋季更新版本。你需要大约 10GB 的磁盘空间来安装 Ubuntu。 请从 Windows 应用商店安装 Ubuntu18.04 ，并确保可以正常地使用 WSL1。网络上有许多教程可以帮助你完成这项工作，我不想重置我的系统去重复提供这些详细的说明。 然后必须按照微软的文档启用 WSL2，点我查看文档。文档中的关键内容是将 WSL1 切换为 WSL2。 您可以通过查看 uname 的输出来检查是否正在运行 WSL2。如果您运行的是基于 4.19 内核的 microsoft standard，那就可以继续进行后续的步骤了。 准备使用 snapd WSL2 几乎可以运行 snapd 了。唯一缺少的东西就是作为 init system 的 systemd。微软正在使用一个定制的 init 为每个已安装的 WSL2 发行版设置一个有效的容器。init system 是一个最小系统，比起传统的发行版的 init system 的任务，它更多的是处理与 WSL 的集成。 这段的最后一句话译者起初一直不能翻译通顺，后来询问群友之后发现了这篇文章：https://www.lbihua.cn/Share/DeepL.html，文章内推荐了 DeepL 这款翻译工具，它让我可以通顺地翻译出最后一句话，感谢哔哗的帮助。 译者注 systemd 是预装的，但不是活动的。你仍然可以手动运行 systemd，但它会拒绝启动，除非它以 pid 1 的身份被调用。有一个简单的方法，把它放在一个 "容器 "中，容器其实就是一个基于 unshare(1) 实用程序的山寨容器（poor man’s container）。 poor man's container 直译为“穷人容器”，但是这应该不是真实的意思，译者查询了一些资料后将其翻译为“山寨容器“，点我查看相关资料。 译者注 运行下面这行神奇的命令： sudo daemonize /usr/bin/unshare --fork --pid --mount-proc /lib/systemd/systemd --system-unit=basic.target 这行命令使用 unshare(1) 来创建一个新的 PID 命名空间。PID 命名空间实际上是Linux 状态下看到进程的一部分，如果你观察你的系统，看到 pid 123，这就表示 123 存在于一个 PID 命名空间中。在另一个 PID 命名空间中，它可能是其他的东西，也可能根本不存在。创建一个 PID 命名空间可以让我们再次得到 PID 1。其余的参数告诉 unshared 再次挂载 /proc 和 fork(1)，技术上来说，这是 PID 命名空间工作所需要的。 一旦 unshared 完成，我们要求它运行 systemd，目标是 reach basic.target。你可以在 systemd.target(5) 的手册页面上阅读更多的内容。在命令的开头你会看到 sudo daemonize，它的作用是以 root 身份运行整个命令，并将其从我们用来调用的 shell 中分离出来。 注意，在这个阶段，如果你想管理发行版 host 上的部分服务，WSL 将会出现一些奇怪的现象。例如，如果你使用 apt-get 安装了apache2，你将无法启动它，因为服务管理脚本与 systemd 通信失败。 使用 snpad 进入新的世界 现在 systemd 已经运行了，但在我们的山寨容器里如何使用它呢？技术上很简单，我们只需要在 systemd 住在我们使用的 PID 命名空间中，即运行在容器中。我们实现呢？使用 nsenter(1)。 exec sudo nsenter -t $(pidof systemd) -a su - $LOGNAME 这行命令会让 shell 执行sudo（并且会用这个进程替换掉当前 shell），运行 nsenter，它将我们移动到 systemd 进程的所有命名空间中。一旦进入容器， nsenter 将运行 su - $LOGNAME，使得我们在进入容器前后均使用同一个用户登录。 接下来呢？ snap version 如果这条命令正常执行你就可以安装你喜欢的任何东西了。 注意，你不能使用图形应用程序。我用 X server 和 putty 作为 X11 转发桥的实验结果相当糟糕，我不推荐这样做。 如果你打开另一个 WSL shell，你必须重复 sudo nsenter 命令来 "切换 "到运行 systemd 的容器。 这玩意弄坏了我的系统，救命啊！ Systemd 不是 ”持久性 "的。如果你重启 Windows，你将回到你开始的地方。WSL的数据将保持不变。你可以试着一次又一次地运行 systemd。
自腾讯公司于 2016 年 11 月 发布 TIM 以来，关于两者到底谁好的争论就一直每个结果，本文也不打算讨论两者中哪一个更好，只是打算说说对于这两款软件的看法而已，这也是我建立本站的原因之一。
写在前面 自腾讯公司于 2016 年 11 月 发布 TIM 以来，关于两者到底谁好的争论就一直没个结果，本文也不打算讨论两者中哪一个更好，只是打算说说对于这两款软件的看法而已，这也是我建立本站的原因之一。 建立这个网站的目的就是想找一个写东西的地方。写写自己的笔记，感受啥的。分享一些好玩的事情，好用的软件。 https://www.addesp.com/about QQ QQ 大概是我用的第一款腾讯的软件，也是用的时间最长的腾讯软件了。起初刚接触到它的时候觉得它的功能很丰富，其中最喜欢的就是 QQ 宠物。有个可爱的小企鹅在电脑桌面上蹦蹦跳跳，还能有简单的互动对于当时的我来说有着不小的吸引力。 大概过了几年我发现电脑的配置似乎跟不上了，当时电脑的内存只有 2GB，操作系统是 Windows 7 32 位，开机后再开一个 QQ 内存就飙到 60% 以上了。当时似乎也没有去找类似 QQ 轻聊版这样的替代品，或者当时也没有这个东西，也就忍下来了。后来实在受不了了就攒钱买了个 2GB 的内存条插进去才解决了这个问题。 后来出现了智能手机，但是我第一部智能手机只有 256MB 的内存（运行内存），明显感觉到安装常规的 QQ 跑起来很吃力，但这次我找到的 QQ 轻聊版，然后一直用到这部手机淘汰。 我的第二部手机是攒钱买了红米 Note 系列，具体型号记不清了。内存大小好像是 2GB，我打算换上常规的 QQ 试试。但是此时我发现我已经对 QQ 的聊天以外的功能提不起多少兴趣了，更多地觉得这是一个累赘，不过貌似是因为当时轻聊版已经没了所以就没有继续用了下去。 手机 QQ 逐渐推出了如悄悄话等功能，不停地告诉我我不是它的目标用户。这段时间我也在关注着 QQ 的替代品。 TIM 不知何时，我知道了有 TIM 这种东西，我试着用了用它，发现这可能才是我想要的 QQ，纯粹的聊天功能，搭配一些办公用的功能，功能足够也不显冗余。我又下载的 PC 端的 TIM，发现依旧很合我的心意，于是我卸载的手机 QQ 和 PC QQ，全部换上了 TIM。 从此再也不需要去辨认一些奇形怪状的字体，花里胡哨的特效等，简简单单地聊天很符合我的需求。 不过大概在半年前或是一年前，TIM 经常接不到消息或者消息接收延迟好几个小时，还好没有错过什么大事，但是这让我立刻停用了 TIM，换回了常规的手机 QQ。PC 端的 TIM 没有发现这个问题，所以暂时没换。 我也是这时才发现诸如 QQ 看点、好看视频和 QQ 天气这类东西，用惯了 TIM 的我对这类东西有很强的排斥心理。甚至现在手机 QQ 也出了一个小程序功能，相当于在手机 QQ 里跑一个阉割版的浏览器内核，感觉完全没必要。只是因为 TIM 出了问题所以暂时先用着常规的手机 QQ 而已。 近期觉得 TIM 应该恢复正常了，于是在手机上重新换回的 TIM，熟悉的感觉立马回来了，如果没有出什么意外我大概也会一直用下去。 看法 很明显那 QQ 和 TIM 是针对的两种人群，无所谓好坏之分。 QQ 的好处就是功能丰富，虽然很多可能用不上，但是想用某项功能的时候可以找到现成的，或者一个新推出的功能非常合用户的胃口。简单点比如情侣可以开启情侣空间，在空间秀秀恩爱等。发说说分享一下自己遇到的事情啥的。这些需求客观存在，并且大量的用户需要这些需求，QQ 进行了跟进，从这点上来说 QQ 做得很好。 TIM 的好处就是删掉 QQ 上大量的冗余功能，将办公作为主打方向。这就是导致了其没有 QQ 那么繁杂，界面十分简洁，没有多余的东西去分散用户的注意力。对于我来说比较明显方便之处的就是可以直接在手机端的 TIM 上收发 QQ 邮件，虽然正事我很少用 QQ 邮箱，但是一些常规的交流 QQ 邮箱还是用得挺多的。虽然现在手机 QQ 也能收发 QQ 邮件了，但这在当时对我的吸引力还是挺大的。 在这最后我希望 TIM 能一直维护下去，给我一个纯粹的聊天沟通工具。
这个项目可以查询一些字体的授权方式并提供下载地址。
前言 偶然看到了这个项目，这个项目可以查询一些字体的授权方式并提供下载地址，挺实用的一个项目，所以分享一下。 相关链接 Github 演示地址 官方介绍 如你所见，我是一名设计爱好者，这是我用屁股想的说明文档，因此如果发现阅读起来狗屁不通，属于正常情况。 在前些年我看到了好多起因为各种版权产生了法律纠纷的事件。 比如视觉中国版权门事件。 再比如很多设计师甚至不知道微软雅黑字体居然不能用来商用。 近年来这类事件屡见不鲜。 在我之前，有“360查字体”这个网站，遗憾的是，它看起来已经不再维护了。 这令人有点儿伤心。 因此我在闲暇时间制作了这个网站，我希望他能帮我们简单的排查出字体方面的隐患，降低一些不必要的版权纠纷。 好吧，虽然我觉得一个好的设计师，肯定会知道哪些字体可以被用作商业设计。 https://github.com/muruoxi2018/Check_Font_Copyright
rename - 重命名多个文件 RENAME(1p) 用户贡献的 Perl 语言版本的文档 RENAME(1p) 名称 rename - 重命名多个文件 概要 rename [ -h|-m|-V ] [ -v ] [ -n ] [ -f ] [ -e|-E perlexpr]*|perlexpr [ files ] 描述 "rename" 根据指定为第一个参数的规则重命名提供的文件名。 perlexpr 参数是一个 Perl 表达式， 它至少可以指定的文件名中的部分字符串。 如果给定的文件名未被表达式修改，则不会重命名。 如果命令行中未提供文件名，则将通过标准输入读取文件名。 ， 例如，要重命名所有匹配 "*.bak" 的文件以去除后缀，你可以这样使用 rename 's/\e.bak$//' *.bak 译者注：这个命令再 WSL2 Ubuntu 18.04 LTS 下无效 可以使用这个命令 rename 's/\.bak//' *.bak。 s/regexp/replacement/ 开头的 s 表示将 regexp 匹配到的内容 替换为 replacement。 将所有大写字母转化为小写字母，你可以这样使用 rename 'y/A-Z/a-z/' * 译者注：y/source/dest/ 开头的 y 表示将 source 中匹配到的字符替换为 dest 中对应位置的字符。 选项 -v, -verbose 详细信息：成功重命名的文件的打印名称。 -n, -nono 不执行任何操作：打印要重命名的文件名，但不重命名。 -f, -force 覆盖：允许现有文件被覆盖。 -h, -help 帮助：打印概要和选项。 -m, -man 手册：打印手册页。 -V, -version 版本：显示版本号。 -e 表达式：作用于文件名的代码。 可以重复构建代码（例如“ perl -e”）。 如果没有使用 -e，则第一个参数将被视作代码。 译者注：这一段翻译起来不是很通顺， 下面是英文原文： Expression: code to act on files name. May be repeated to build up code (like "perl -e"). If no -e, the first argument is used as code. -E 声明：作用于文件名的代码，为-e，但以';'结尾。 环境 没有使用环境变量。 作者 Larry Wall 参见 mv(1), perl(1) 诊断 If you give an invalid Perl expression you'll get a syntax error. 错误 原始的 "rename" 没有检查目标文件名是否存在， 因此必须谨慎使用。希望我已经解决了这个问题（Larry Wall）。 译者注：你不要吓我，Larry Wall。 下面是英文原文： The original "rename" did not check for the existence of target filenames, so had to be used with care. I hope I've fixed that (Robin Barker). 译者 本手册由 ADD-SP 于 2020年8月24日 由英文翻译为简体中文。 perl v5.26.1 2018-02-19 RENAME(1p)
tar - 存档实用程序 TAR(1) GNU TAR Manual TAR(1) 名称 tar - 存档实用程序 概要 常规用法 tar {A|c|d|r|t|u|x}[GnSkUWOmpsMBiajJzZhPlRvwo] [ARG...] UNIX 风格用法 tar -A [OPTIONS] ARCHIVE ARCHIVE tar -c [-f ARCHIVE] [OPTIONS] [FILE...] tar -d [-f ARCHIVE] [OPTIONS] [FILE...] tar -t [-f ARCHIVE] [OPTIONS] [MEMBER...] tar -r [-f ARCHIVE] [OPTIONS] [FILE...] tar -u [-f ARCHIVE] [OPTIONS] [FILE...] tar -x [-f ARCHIVE] [OPTIONS] [MEMBER...] GNU 风格用法 tar {--catenate|--concatenate} [OPTIONS] ARCHIVE ARCHIVE tar --create [--file ARCHIVE] [OPTIONS] [FILE...] tar {--diff|--compare} [--file ARCHIVE] [OPTIONS] [FILE...] tar --delete [--file ARCHIVE] [OPTIONS] [MEMBER...] tar --append [-f ARCHIVE] [OPTIONS] [FILE...] tar --list [-f ARCHIVE] [OPTIONS] [MEMBER...] tar --test-label [--file ARCHIVE] [OPTIONS] [LABEL...] tar --update [--file ARCHIVE] [OPTIONS] [FILE...] tar --update [-f ARCHIVE] [OPTIONS] [FILE...] tar {--extract|--get} [-f ARCHIVE] [OPTIONS] [MEMBER...] 注意 该联机帮助页是GNU tar的简短描述。有关详细讨论，包括示例和用法说明，请参见 建议，请参阅texinfo格式的GNU Tar手册。 如果信息阅读器和tar文档已正确安装在您的系统上，命令 info tar 应该可以让你看到完整的手册 您也可以在emacs（1）中使用 info mode 查看手册，或在以下网址在线找到各种格式的手册： http://www.gnu.org/software/tar/manual 如果本手册与GNU Tar手册之间存在任何差异，则应将后者视为权威来源。 描述 GNU tar 是一个存档程序，旨在将多个文件存储在一个文件（一个存档文件）中，并用于 这类存档较晚。 存档文件可以是常规文件或设备（例如磁带驱动器，因此名称为 该程序的代表，它代表磁带存档器），可以位于本地或远程机。 选项风格 GNU tar的选项可以用三种不同的样式给出。在传统风格中， 第一个参数时所有选项字符的集合。 参数的读取顺序和选项字符的出现顺序相同。 在处理完所有选项之后剩余的命令行内容均被视为非可选参数： 文件或存档成员名称。 例如，c 选项需要创建存档，v 会现实详细操作， 而 f 选项使用一个参数来设置要操作的存档的名称。 以下命令以传统方式编写， 指示 tar 将目录 /etc 中的所有文件存储到存档文件 etc.tar 中， 详细列出正在存档的文件： tar cfv a.tar /etc 与其他命令行实用程序一样，在 UNIX 或短选项风格中， 每个选项字母都以一个短划线作为前缀。 如果一个选项带有参数， 则该参数作为单独的命令行字或紧随该选项之后紧随其后。 但是，如果该选项采用可选参数，则该参数必须跟在选项字母后且中间没有空格， 如 -g /tmp /snar.db 所示。 可以在单个破折号后将任意数量的不带参数的选项聚集在一起， 例如 -vkp。 带有参数的选项（无论是必需的还是可选的）可以出现在此类集群的末尾， 例如 -vkpf a.tar。 上面以短选项样式编写的示例命令可能类似于： tar -cvf a.tar /etc 或 tar -c -v -f a.tar /etc 在 GNU 或长选项风格中，每个选项均以两个破折号开头， 并具有有意义的名称，由小写字母和破折号组成。 使用长选项时，如果不会造成歧义，则可以缩写为首字母。 长选项的参数以单独的命令行字形式提供，紧接在选项之后， 或者以等号与选项分隔，中间没有空格。 可选参数必须始终使用后一种方法。 这是使用这种风格编写示例命令的几种方法： tar --create --file a.tar --verbose /etc 或（缩写一些选项） tar --cre --file=a.tar --verb /etc 三种风格的选项可以混合使用，尽管不鼓励使用旧选项。 操作模式 下表中列出的选项告诉 GNU tar 要执行什么操作。 必须给出其中一个操作模式。非可选参数的含义取决于给出的操作模式。 -A, --catenate, --concatenate 将存档追加到另一个存档的末尾。 这些参数被视为要追加的存档的名称。 所有存档必须与附加存档的格式相同， 否则生成的存档可能不适用于 tar 的非 GNU 实现。 还要注意的是，当给出多个存档时， 只有使用-i（--ignore-zeros）选项时， 才能在结果存档中访问除第一个存档以外的其他存档成员。 经过了压缩的存档无法不能追加。 -c, --create 创建一个新的存档。参数提供要存档的文件的名称。 目录是递归存档的，除非指定了--no-recursion 选项。 -d, --diff, --compare 查找存档和文件系统之间的差异。 参数是可选的， 并指定要比较的存档成员。如果未给出，则为当前工作目录。 --delete 从存档中删除。参数提供要删除的存档成员的名称。 至少给出一个参数。 该选项无法用于经过压缩的存档，且没有等价的短选项。 -r, --append 将文件追加到存档文件的末尾。 参数的含义与-c（--create）相同。 -t, --list 列出存档的内容。参数是可选的。 如果给定，它们将指定要列出的成员的名称。 --test-label 测试存档卷的标签并退出。 当不带参数使用时， 它将打印卷标（如果有）并以状态 0 退出。 给出一个或多个命令行参数时。 tar 将卷标与每个参数进行比较。 如果找到匹配项，则以代码 0 退出， 否则以代码1退出。 除非与 -v（--verbose）选项一起使用，否则不显示任何输出。 此选项没有等效的短选项。 -u, --update 追加比存档中相应副本新的文件。 参数与 -c 和 -r 选项具有相同的含义。 -x, --extract, --get 从存档中提取文件。参数是可选的。 如果给出，它们指定要提取的存档成员的名称。 --show-defaults 显示各种 tar 选项的内置默认值并退出。不允许有参数。 -?, --help 显示简短的选项摘要并退出。不允许有参数。 --usage 显示可用选项列表并退出。不允许有参数。 --version 打印程序版本和版权信息，然后退出。 选项 操作的修饰符 --check-device 创建增量存档时检查设备编号（默认）。 -g, --listed-incremental=FILE 处理新的 GNU 格式的增量备份。FILE是快照文件的名称， 是 tar 存储一些用于确定自上次增量转储以来哪些文件已更改的信息， 因此必须再次转储。 如果在创建存档文件时不存在 FILE， 则会创建该文件并将所有文件添加到生成的存档文件中（0级转储）。 要创建非零的 N 级别的增量存档， 请在创建 N-1 级别的增量存档期间创建的快照文件的副本， 并将其用作 FILE。 列出或提取文件时，不检查FILE的实际内容， 除非是出于语法要求。因此，通常使用 /dev/null 代替它。 --hole-detection=METHOD 使用 METHOD 来检测稀疏文件中的孔。 此选项意味着你使用了 --sparse。 METHOD 的有效值是 seek 和 raw。 默认情况下为 seek，不适用时会回退为 raw。 -G, --incremental 处理旧的 GNU 格式的增量备份。 --ignore-failed-read 在不可读的文件上不要以非零退出。 --level=NUMBER 为创建的列出增量存档设置转储级别。 当前只有--level=0 才有意义： 它指示 tar 在转储之前截断快照文件，从而强制进行0级转储。 -n, --seek 假设存档是可查找的。通常，tar自动确定是否可以查找存档。 此选项适用于这种识别失败的情况。 仅在打开存档以供阅读时 （例如，使用--list 或--extract选项），此选项才会生效。 --no-check-device 创建增量存档时，不检查设备编号。 --no-seek 假设存档不可搜索。 --occurrence[=N] 仅处理存档中每个文件的第 N 次出现。 仅当与以下操作模式之一一起使用时，此选项才有效： --delete，-diff，-extract或--list， 并且在命令行或通过-T选项给出文件列表时。N 默认为 1。 --restrict 禁用某些潜在有害选项。 --sparse-version=MAJOR[.MINOR] 设置要使用的稀疏格式的版本（意味着使用了--sparse）。 有效参数值为0.0、0.1 和 1.0。 有关稀疏格式的详细讨论， 请参见GNU Tar手册，附录D，“稀疏格式”。 使用信息阅读器， 可以运行以下命令来访问它：info tar'Sparse Formats'。 -S, --sparse 有效地处理稀疏文件。 文件系统中的某些文件可能具有实际上从未写入过的段 （通常这些段是由 DBM 等系统创建的数据库文件）。 如果指定了此选项，则tar会尝试在存档之前确定文件是否稀疏， 如果是，则通过不转储文件的空白部分来减小最终的存档大小。 覆写控制 当从存档中提取磁盘上已经存在的文件时，这些选项控制tar操作。 -k, --keep-old-files 解压缩时不要替换现有文件。 --keep-newer-files 不要替换比其存档副本新的现有文件。 --no-overwrite-dir 保留现有目录的元数据。 --one-top-level[=DIR] 将所有文件解压缩到 DIR 中，或者如果不带参数使用， 则解压缩到与存档名同名的目录中 （去掉--auto-compress 可以识别的标准压缩后缀）。 --overwrite 解压缩时覆盖现有文件。 --overwrite-dir 提取时覆盖现有目录的元数据（默认）。 --recursive-unlink 在解压缩之前，以递归方式删除目录中的所有文件。 --remove-files 将文件添加到存档后，从磁盘中删除文件。 --skip-old-files 提取时不要替换现有文件，以静默方式跳过它们。 -U, --unlink-first 提取每个文件之前，先将磁盘上存在的重复文件删除。 -W, --verify 写入后验证存档。 输出流选择 --ignore-command-error 忽略子流程退出代码。 --no-ignore-command-error 将子级的非零退出代码视为错误（默认值）。 -O, --to-stdout 将文件提取到标准输出。 --to-command=COMMAND 将提取的文件通过管道传输到命令行。 参数是外部程序的路径名，将其作为命令行参数。 提取出的文件的内容将传递给程序， 然后启动这个程序。 其他数据将通过以下环境变量提供： TAR_FILETYPE 文件类型。是一个字母，含义如下： f 常规文件 d 目录 l 符号链接 h 硬链接 b 块设备 c 字符设备 当前仅支持常规文件。 TAR_MODE 文件模式，一个八进制数。 TAR_FILENAME 文件名。 TAR_REALNAME 存档名 TAR_UNAME 文件所有者的名称。 TAR_GNAME 文件所有者组的名称。 TAR_ATIME 上次访问时间。 这是一个十进制数字，表示自 Epoch 以来的秒数。 如果存档库提供的时间具有纳秒精度，则将纳秒添加到小数点后的时间戳。 TAR_MTIME 上次修改时间。 TAR_CTIME 上次状态更改的时间。 TAR_SIZE 文件大小。 TAR_UID 文件所有者的UID。 TAR_GID 文件所有者的GID。 此外，以下变量包含有关 tar 操作模式和正在处理的存档的信息： TAR_VERSION GNU tar 版本号。 TAR_ARCHIVE tar 正在处理的存档名称。 TAR_BLOCKING_FACTOR 当前的块因子，即一条记录中 512 字节的块的个数。 TAR_VOLUME tar 正在处理的卷的序号（如果读取多卷存档则设置）。 TAR_FORMAT 正在处理的存档格式。 可能的取值为：gnu，oldgnu，posix，ustar，v7。 TAR_SUBCOMMAND 一个简短的选项（带前划线），描述 tar 正在执行的操作。 文件属性的处理 --atime-preserve[=METHOD] 保留转储文件的访问时间， 方法是在读取后恢复时间 （METHOD=replace，这是默认设置）， 或者不首先设置时间（METHOD=system） --delay-directory-restore 延迟设置修改时间和提取目录的权限，直到提取结束。 从具有异常成员顺序的存档中提取时，请使用此选项。 --group=NAME[:GID] 强制将 NAME 作为添加文件的组。 如果未提供GID，则NAME可以是用户名或数字GID。 在这种情况下，将从当前主机的组数据库中推断出丢失的部分（GID或名称）。 与--group-map=FILE一起使用时，仅影响所有者组未在FILE中列出的那些文件。 --group-map=FILE 从 FILE 读取组翻译图。空行将被忽略。 注释以＃号开头，并延伸到行尾。 FILE 中的每个非空行都为单个组定义映射。 它必须由两个字段组成，以任意数量的空格分隔： OLDGRP NEWGRP[:NEWGID] OLDGRP是有效的组名或带有+前缀的GID。 除非提供了NEWGID，否则NEWGRP也必须是有效的组名或+ GID。 否则，不需要在系统组数据库中列出NEWGRP和NEWGID。 结果，每一个所有者组为OLDGRP的输入文件 将与所有者组为 NEWGRP 和 GID NEWGID 的输入文件一起存储在存档中。 --mode=CHANGES 强制将符号模式更改已添加的文件。 --mtime=DATE-OR-FILE 为添加的文件设置 mtime。 DATE-OR-FILE 可以是几乎任意格式的日期/时间， 也可以是现有文件的名称。 在后一种情况下，将使用该文件的mtime。 -m, --touch Don't extract file modified time. --no-delay-directory-restore 取消先前的 --delay-directory-restore 选项的效果。 --no-same-owner 用户自己提取文件（普通用户默认）。 --no-same-permissions 从存档中提取权限时，应用用户的umask（普通用户默认）。 --numeric-owner 用户/组名称始终使用数字。 --owner=NAME[:UID] 强制将 NAME 作为添加文件的所有者。 如果未提供UID，则NAME可以是用户名或数字UID。 在这种情况下，将从当前主机的用户数据库中推断出丢失的部分（UID或名称）。 与--owner-map = FILE一起使用时，仅影响所有者未在FILE中列出的那些文件。 --owner-map=FILE 从FILE中读取所有者翻译地图。 空行将被忽略。 注释以＃号开头，并延伸到行尾。 FILE中的每个非空行都定义了单个UID的映射。 它必须由两个字段组成，以任意数量的空格分隔： OLDUSR NEWUSR[:NEWUID] OLDUSR 是有效的用户名或带有+前缀的UID。 除非提供 NEWUID，否则 NEWUSR 还必须是有效的用户名或+UID。 否则，不需要在系统用户数据库中列出 NEWUSR 和 NEWUID。 结果，OLDER 拥有的每个输入文件 都将以所有者名称 NEW USER和 UID NEWUID 存储在存档中。 -p, --preserve-permissions, --same-permissions 提取有关文件权限的信息（超级用户的默认设置）。 --preserve 与 -p 和 -s 相同。 --same-owner 尝试以与存档中相同的所有权提取文件（超级用户默认）。 -s, --preserve-order, --same-order 排序名称以提取以匹配存档。 --sort=ORDER 创建存档文件时，根据ORDER对目录条目进行排序， ORDER 是 none、name或inode之一。 默认--sort=none，它以与操作系统返回的顺序相同的顺序存储存档成员。 使用--sort=name可以确保创建的存档文件中的成员顺序是统一的和可复制的。 使用--sort=inode可以减少在创建存档文件时进行的磁盘查找次数， 因此可以大大加快存档速度。只有当底层系统提供了必要的信息时，才支持这种排序顺序。 扩展文件属性 --acls 启用 POSIX ACL 支持。 --no-acls 禁用 POSIX ACL 支持。 --selinux 启用 SELinux 上下文支持。 --no-selinux 禁用 SELinux 上下文支持。 --xattrs 启用扩展属性支持。 --no-xattrs 禁用扩展属性支持。 --xattrs-exclude=PATTERN 指定 xattr 键的排除模式。PATTERN 是一个 POSIX 正则表达式， 例如--xattrs exclude='^user'，用于从用户命名空间中排除属性。 --xattrs-include=PATTERN 指定 xattr 键的 include 模式。PATTERN是一个POSIX正则表达式。 设备选择和切换 -f, --file=ARCHIVE 使用存档文件或设备存档。如果没有给出这个选项， tar将首先检查环境变量 TAPE。如果设置了它， 它的值将用作存档名称。 否则，tar 将使用编译时指定的默认值。 可以使用 --show defaults 选项或在 tar --help 输出的末尾检查默认值。 带有冒号的存档名称指定远程计算机上的文件或设备。之前的那部分 冒号作为机器名或 I P地址，后面的部分作为文件或设备路径名称，例如： --file=remotehost:/dev/sr0 可以在主机名前面加上一个可选的用户名，在它们之间加上一个@号。 默认情况下，通过rsh（1）命令访问远程主机。现在通常使用ssh（1）代替。 您可以通过提供以下命令行选项来执行此操作： --rsh-command=/usr/bin/ssh 远程机器应安装rmt（8）命令。如果其路径名与tar的默认路径名不匹配， 可以使用 --rmt 命令选项通知tar正确的路径名。 --force-local 存档文件是本地文件，即使它有冒号。 -F, --info-script=COMMAND, --new-volume-script=COMMAND 在每个 tape 末尾运行命令（意味着使用了 -M）。参数可以包括命令。 启动时，它将继承tar的环境以及以下变量： TAR_VERSION GNU tar 版本号。 TAR_ARCHIVE tar 正在处理的存档的名称。 TAR_BLOCKING_FACTOR 当前块因子，即每隔记录中包含的 512 字节块的数量。 TAR_VOLUME tar 正在处理的卷的序号（当且仅当读取多卷存档才会出现）。 TAR_FORMAT 正在处理的存档的格式。可能的取值：gnu、oldgnu、posix、ustar、v7。 TAR_SUBCOMMAND 描述tar正在执行的操作的短选项（带前导破折号）。 TAR_FD 文件描述符，可用于将新卷名传递给 tar。 如果 info 脚本失败，tar 退出；否则，tar 开始写入下一个卷。 -L, --tape-length=N 写入Nx1024字节后更换 tap。如果 N后 面跟一个大小后缀 （请参阅下面的“大小后缀”小节）， 则后缀指定要使用的乘法因子，而不是1024。 这个选项意味着使用了 -M。 -M, --multi-volume 创建/列出/提取多卷存档。 --rmt-command=COMMAND 替换 rmt 的访问远程存档时使用命令。 请参见上面对 -f 选项的描述。 --rsh-command=COMMAND 访问远程存档时，请使用命令而不是 rsh。 请参见上面对-f 选项的描述。 --volno-file=FILE 当此选项与 --multi-volume 结合使用时， tar 将跟踪它在文件中工作的多卷存档的哪个卷。 块设置 -b, --blocking-factor=BLOCKS 将记录大小设置为BLOCKSx512字节。 -B, --read-full-records 当列出或提取时，接受文件结束标记后不完整的输入记录。 -i, --ignore-zeros 忽略存档中的清零块。 通常，两个连续的512块填充有零，表示EOF， 并且tar在遇到它们之后停止读取。 该选项指示它进一步阅读，并且在读取使用-A选项创建的存档时非常有用。 --record-size=NUMBER 设置记录大小。NUMBER是每条记录的字节数。 它必须是512的倍数。可以以存储单位如 k 或 m 为后缀， 例如 --record-size = 10K，表示10 KB。 请参阅小节大小后缀，以获取有效后缀的列表。 存档格式选择 -H, --format=FORMAT 创建给定格式的存档。有效格式为： gnu GNU tar 1.13.x格式 tar<=1.12 的为 oldgnu-GNU 格式。 pax, posix POSIX 1003.1-2001（pax）格式。 ustar POSIX 1003.1-1988 (ustar) 格式。 v7 旧的 v7tar 格式。 --old-archive, --portability 与 --format=v7 相同。 --pax-option=keyword[[:]=value][,keyword[[:]=value]]... 创建pax存档时控制pax关键字（-H pax）。 此选项相当于pax（1）实用程序的-o选项。 --posix 与 --format=posix 相同。 -V, --label=TEXT 使用卷名创建存档。 如果操作为列出或提取，使用文本作为卷名的全局模式。 压缩选项 -a, --auto-compress 使用存档后缀来确定压缩程序。 -I, --use-compress-program=COMMAND 通过命令过滤数据。必须同时使用 -d 选项。 参数可以包含命令行选项。 -j, --bzip2 通过 bzip2（1）过滤存档文件。 -J, --xz 通过 xz（1）过滤存档文件。 --lzip 通过 lzip（1）过滤存档文件。 --lzma 通过 lzma（1）过滤存档文件。 --lzop 通过 lzop（1）过滤存档文件。 --no-auto-compress 不使用存档后缀来确定压缩程序。 -z, --gzip, --gunzip, --ungzip 通过 gzip（1）过滤存档文件。 -Z, --compress, --uncompress 通过 compress(1) 过滤存档文件。 本地文件选择 --add-file=FILE 将文件添加到存档（如果文件名以破折号开头，则很有用）。 --backup[=CONTROL] 删除前备份。控制参数（如果提供）控制备份策略。其有效值为： none, off 永远不要备份。 t, numbered 进行编号备份。 nil, existing 如果存在编号备份，则进行编号备份，否则进行简单备份。 never, simple 始终进行简单备份 如果未给定控制参数， 则该值取自 VERSION\uCONTROL 环境变量。 如果未设置，则假定存在。 -C, --directory=DIR 在执行任何操作之前，切换到 DIR。此选项对顺序敏感，即它影响后面的所有选项。 --exclude=PATTERN 排除模式匹配到的文件，通配符遵循 glob（3）。 --exclude-backups 排除备份和锁定的文件。 --exclude-caches 排除包含文件的目录的内容 CACHEDIR.TAG，但标记文件本身除外。 --exclude-caches-all 排除包含文件的目录CACHEDIR.TAG以及文件本身。 --exclude-caches-under 排除包含的目录下的所有内容 CACHEDIR.TAG --exclude-ignore=FILE 在转储目录之前，查看它是否包含 FILE。 如果是，从中读取排除模式匹配到的文件。 模式只影响目录本身。 --exclude-ignore-recursive=FILE 与--exclude ignore相同，只是文件中的模式同时影响目录及其所有子目录。 --exclude-tag=FILE 排除包含 FILE 的目录的内容，除了FILE本身。 --exclude-tag-all=FILE 排除包含 FILE 的目录。 --exclude-tag-under=FILE 排除包含 FILE 的目录下的所有内容。 --exclude-vcs 排除版本控制系统目录。 --exclude-vcs-ignores 排除与从 VCS-specific 的忽略文件读取的模式匹配的文件。 支持的文件有：.cvsignore，.gitignore，.bzrignore和.hgignore。 -h, --dereference 遵循符号链接；存档并转储它们指向的文件。 --hard-dereference 遵循硬链接； 存档并转储它们引用的文件。 -K, --starting-file=MEMBER 从存档中的给定成员开始。 --newer-mtime=DATE 处理数据在 DATE 之后更改的文件。如果 DATE 以 /or 开头。 它被视为一个文件名；该文件的mtime被用作日期。 --no-null 禁用先前的--null选项的效果。 --no-recursion Avoid descending automatically in directories. --no-unquote 不取消引用输入文件或成员名称的引用。 --no-verbatim-files-from 将从文件列表中读取的每一行提供给命令行。 一、 例如，删除前导空格和尾随空格， 如果结果字符串以破折号开头，则将其视为tar命令行选项。 这是默认行为。提供了--no verbatim files from 选项， 作为在 --verbatim files from 选项之后恢复它的方法。 此选项是固定的：它将影响所有在其之后出现的--files-from选项， 直到--verbatim-files-from选项或行尾（以先出现者为准）。 这个选项意味着使用了 --no-null。 --null 指示后续的 -T 选项逐字读取以空值结尾的名称（禁用对以破折号开头的名称的特殊处理）。 详情见 --verbatim-files-from. -N, --newer=DATE, --after-date=DATE 仅存储比 DATE 更新的文件。 如果DATE以/ or 开始。 它被当作文件名：该文件的ctime用作日期。 --one-file-system 创建存档文件时，保留在本地文件系统中。 -P, --absolute-names 创建存档时，请勿在文件名中删除前导斜杠。 --recursion 递归到目录（默认）。 --suffix=STRING 备份后再删除，覆盖通常的后缀。 默认后缀为〜，除非被环境变量 SIMPLE_BACKUP_SUFFIX 覆盖。 -T, --files-from=FILE 获取要从 FILE 中提取或创建的名称。 除非另有说明，否则FILE必须包含以ASCII LF分隔的名称列表（即，每行一个名称）。 读取的名称与命令行参数的处理方式相同。 它们进行引号删除和单词拆分，并且任何以-开头的字符串都将作为tar命令行选项处理。 如果不希望出现这种情况，可以使用 --verbatim-files-from 选项将其关闭。 --null选项指示tar FILE中的名称用ASCII NUL字符而不是LF分隔。 如果列表由find（1）-print0 谓词生成，则很有用。 --unquote 取消引用文件或成员名称（默认）。 --verbatim-files-from 从文件列表中获得的每一行都视为文件名，即使它以短划线开头也是如此。 文件列表随--files-from（-T）选项提供。 默认行为是处理文件列表中提供的名称，就像在命令行中键入它们一样， 即，任何以破折号开头的名称都被视为tar选项。 --verbatim-files-from选项禁用此行为。 此选项会影响在命令行之后出现的所有--files-from选项。 --no-verbatim-files-from}选项可恢复其效果。 该选项由--null选项隐含。 更多见 --add-file. -X, --exclude-from=FILE 排除文件中列出的与模式匹配的文件。 文件名转换 --strip-components=NUMBER 提取时从文件名中剥离 NUMBER 个主要部分。 --transform=EXPRESSION, --xform=EXPRESSION 使用 sed replace EXPRESSION 转换文件名。 文件名匹配选项 这些选项影响排除和包含模式。 --anchored 模式匹配文件名开始。 --ignore-case 无视大小写 --no-anchored 在 '/' 后面匹配模式（排除时默认）。 --no-ignore-case 区分大小写的匹配（默认）。 --no-wildcards 逐字字符串匹配。 --no-wildcards-match-slash 通配符不匹配 '/'。 --wildcards 使用通配符（默认不启用）。 --wildcards-match-slash 通配符匹配 /（默认不启用）。 Informative output --checkpoint[=N] 每 N 个记录显示进度消息（默认为10）。 --checkpoint-action=ACTION 在每个检查点上运行 ACTION。 --clamp-mtime 仅在文件比 --mtime 指定的文件更新时才设置时间。 --full-time 以完整分辨率打印文件时间。 --index-file=FILE 将详细输出发送到 FILE。 -l, --check-links 如果不是所有链接都转储，则打印一条消息。 --no-quote-chars=STRING 禁止从 STRING 中引用字符。 --quote-chars=STRING 此外，还要引用 STRING 中的字符。 --quoting-style=STYLE 设置文件名和成员名的引用样式。 STYLE的有效值是文字， shell, shell-always,c, c-maybe, escape, locale, clocale. -R, --block-number 在每个消息中显示存档中的块号。 --show-omitted-dirs 列出或提取时，列出与搜索条件不匹配的每个目录。 --show-transformed-names, --show-stored-names 通过--strip和--transform选项转换后显示文件或存档名称。 --totals[=SIGNAL] 处理存档后，打印总字节数。如果给出SIGNAL， 则在传递此信号时打印总字节数。 允许的信号为：SIGHUP，SIGQUIT，SIGINT，SIGUSR1和SIGUSR2。 SIG前缀可以省略。 --utc 以 UTC 打印文件修改时间。 -v, --verbose 详细列出已处理的文件。 --warning=KEYWORD 启用或禁用由 KEYWORD 标识的警告消息。 如果 KEYWORD 的前缀为 no，则消息将被禁止，否则将被启用。 Multiple --warning messages accumulate. Keywords tar 的常规控制消息: all 启用所有警告消息。 这是默认值。 none 禁用所有警告消息。 filename-with-nuls "%s：读取的文件名包含空字符” alone-zero-block "%s处的一个单独的零块" 适用于 tar 的关键字 --create： cachedir "%s: 包含一个缓存目录标签 %s; %s" file-shrank "%s: 文件缩小了 %s 个字节; 用零填充" xdev "%s: 文件位于不同的文件系统上； 没有转储" file-ignored "%s: 未知的文件类型; 文件被忽略" "%s: 套接字被忽略" "%s: door ignored" file-unchanged "%s: 文件没有更改；没有转储" ignore-archive "%s: 文件为存档文件；没有转储" file-removed "%s: 文件在读取前被删除" file-changed "%s: 文件在读取钱发生了改变" 适用于tar的关键字 --extract： existing-file "%s: 跳过已经存在的文件" timestamp "%s: 旧到难以置信的时间戳 %s" "%s: time stamp %s is %s s in the future" contiguous-cast "将连续文件提取为常规文件" symlink-cast "尝试将符号链接提取为硬链接" unknown-cast "%s: 未知的文件类型 '%c', 提取为常规文件" ignore-newer "当前 %s 是更新的或相同的 age" unknown-keyword "忽略未知的扩展标头关键字 '%s'" decompress-program 控制尝试运行替代解压缩程序时发生的故障的详细描述。 默认情况下，此警告处于禁用状态（除非使用--verbose）。 使用此警告可以得到的一个常见示例是： $ tar --warning=decompress-program -x -f archive.Z tar (child): cannot run compress: No such file or directory tar (child): trying gzip 这意味着tar首先尝试使用compress解压缩archive.Z， 然后在失败的情况下切换到gzip。 record-size "一个记录的大小 = %lu blocks" 控制增量提取的关键字： rename-directory "%s: 目录已经从 %s 重命名" "%s: 目录已重命名" new-directory "%s: 目录是新的" xdev "%s: 目录在其他设备上：不清除" bad-dumpdir "格式错误的 dumpdir: 'X' 从不使用" -w, --interactive, --confirmation 要求确认每个动作。 兼容性选项 -o 创建时，与 --old-archive. When extracting, same as --no-same-owner 相同。 大小狗追 后缀 单位 等效字节 b Blocks SIZE x 512 B Kilobytes SIZE x 1024 c Bytes SIZE G Gigabytes SIZE x 1024^3 K Kilobytes SIZE x 1024 k Kilobytes SIZE x 1024 M Megabytes SIZE x 1024^2 P Petabytes SIZE x 1024^5 T Terabytes SIZE x 1024^4 w Words SIZE x 2 返回值 Tar 退出代码指示它是否能够成功执行请求的操作，如果不能，则发生哪种错误。 0 正常终止。 1 某些文件有所不同。 如果使用--compare（--diff，-d）命令行选项调用了 tar， 则意味着存档中的某些文件与磁盘上的文件不同。 如果为 tar 提供--create，-append或--update选项之一， 则此退出代码意味着某些文件在存档时已更改，因此生成的存档中不包含文件集的确切副本。 2 致命错误。 这意味着发生了一些致命的，不可恢复的错误。 如果 tar 调用的子进程以非零退出代码退出， 则tar本身也会以该代码退出。 例如，如果使用了压缩选项（例如-z）并且外部压缩程序失败，则可能会发生这种情况。 另一个示例是备份到远程设备期间 rmt 失败。 参见 bzip2(1), compress(1), gzip(1), lzma(1), lzop(1), rmt(8), symlink(7), tar(5), xz(1). 完整的 tar 手册：运行 info tar 或使用 emacs（1）info 模式读取它。 各种格式的 GNU tar文档的在线副本可在以下位置找到： http://www.gnu.org/software/tar/manual 错误报告 向<bug-tar@gnu.org>报告错误。 版权 Copyright © 2013 Free Software Foundation, Inc. License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html> This is free software: you are free to change and redistribute it. There is NO WARRANTY, to the extent per‐ mitted by law. 译者 本手册由 ADD-SP 于 2020-08-25 由英文翻译为简体中文。 TAR 2016年3月23日 TAR(1)
7z - 一个高压缩率的压缩程序 7z(1) 通用命令手册 7z(1) 名称 7z - 一个高压缩率的压缩程序 语法 7z <指令> [<选项>... ] <存档名> [<将要被压缩的文件>... ] [<@列表文件>... ] 描述 7-Zip是支持7z的压缩程序（它实现了LZMA压缩算法，具有很好的压缩率和压缩速度）， LZMA2，XZ，ZIP，Zip64，CAB，RAR（如果安装了非免费的p7zip-rar软件包）， ARJ，GZIP，BZIP2，TAR，CPIO，RPM，ISO，大多数文件系统映像和DEB格式。 新的7z格式的压缩比为比ZIP格式的压缩比高30-50％。 7z使用插件来处理存档，因此它可能比7za（1）慢一些，但可以处理更多存档格式。 指令 a 将文件添加到存档中 b 基准测试 d 从存档中删除文件 e 解压存档 h 计算文件的哈希值 i 显示有关支持的格式的信息 l 列出存档中的文件 rn 重命名存档中的文件 t 测试存档的完整性 u 更新存档中的文件 x 在当前目录创建一个于存档同名的目录并将全部内容解压到该目录内 选项 -- 忽略后面的选项 -ai[r[-|0]]{@listfile|!wildcard} 将其他存档包含进来 -ax[r[-|0]]{@listfile|!wildcard} 排除其它存档 -ao{a|s|t|u} 设置覆盖模式. -aoa 覆盖所有现有文件而无提示。 -aos 不提取已经存在的文件 -aot 若存在同名文件则自动重命名提取出来的文件 -aou 若存在同名文件则自动重命名之前存在的文件 -an 禁用 archive_name 字段 -bb[0-3] 设置输出日志的等级 -bd 禁用进度条 -bs{o|e|p}{0|1|2} 为 output/error/progress 设置输出流 （详情见 https://sevenzip.osdn.jp/chm/cmdline/switches/bs.htm）。 -bt 显示执行时间 -i[r[-|0]]{@listfile|!wildcard} Include filenames -m{Parameters} 设置压缩方法 (详情见/usr/share/doc/p7zip/DOC/MANUAL/cmdline/switches/method.htm) -o{Directory} 设置输出目录 -p{Password} 设置密码（注意：此标志不适用于7zr） -r[-|0] 递归子目录（注意：此标志不会执行您认为的操作，请避免使用它） -sa{a|e|s} 设置存档名称模式 -scc{UTF-8|WIN|DOS} 为控制台输入/输出设置字符集 -scs{UTF-8|UTF-16LE|UTF-16BE|WIN|DOS|{id}} 设置列表文件的字符集 -scrc[CRC32|CRC64|SHA1|SHA256|*] 为 x，e，h 指令设置哈希函数 -sdel 压缩后删除文件 -seml[.] 通过电子邮件发送存档 -sfx[{name}] 创建自解压包 -si[{name}] 从 stdin 读取数据 （例如 tar cf - directory | 7z a -si directory.tar.7z） -slp 设置大页面模式 -slt 显示l（List）命令的技术信息 -snh 将硬链接存储为链接 -snl 将符号链接存储为链接 -sni 存储 NT 安全信息 -sns[-] 存储 NTFS 备用流 -sfx[{name}] 创建自解压包 -so 将数据写入标准输出 （例如 7z x -so directory.tar.7z | tar xf -） -spd 禁用文件名的通配符匹配 -spe 消除使用提取命令时的根文件夹的重复 -spf 同时存储存档内文件的全路径以至解压的时候会直接解压到压缩时的路径 -ssc[-] 设置区分大小写模式 -ssw 压缩共享文件 -stl 从最近修改的文件设置存档的时间戳 -stm{HexMask} 设置CPU线程亲和力掩码（十六进制数） -stx{Type} 该开关将禁用与指定档案类型相关的代码， 并且无法使用指定档案类型打开档案 （详情见 https://sevenzip.osdn.jp/chm/cmdline/switches/stx.htm）。 -t{Type} 设置存档类型 -u[-][p#][q#][r#][x#][y#][z#][!newArchiveName] 指定如何更新档案中的文件和（或）如何创建新档案 （详情见 https://sevenzip.osdn.jp/chm/cmdline/switches/update.htm）。 -v{Size}[b|k|m|g] 分卷压缩并指定每卷的大小 -w[path] 设置工作目录。 空路径表示一个临时目录 -x[r[-|0]]]{@listfile|!wildcard} 指定必须从操作中排除哪些文件名或通配符名称。 -y 假设所有询问为“是” 诊断 7-Zip返回以下退出代码： 0 正常（未检测到错误或警告） 1 警告（非致命错误）。 例如，某些文件在压缩过程中无法读取，因此它们 没有压缩 2 致命错误 7 命令行参数错误 8 没有足够的内存进行操作 255 用户使用Ctrl-C（或类似控件）停止了该过程 备份和限制 在 Linux / Unix上，请勿将 7-zip 格式用于备份目的，因为 : - 7-zip不存储文件的所有者/组。 在Linux / Unix上，为了备份目录，您必须使用tar： - 备份目录 : tar cf - directory | 7z a -si directory.tar.7z - 恢复备份 : 7z x -so directory.tar.7z | tar xf - 如果要将文件和目录（不是文件的所有者）发送给 其他Unix / MacOS / Windows用户，则可以使用7-zip格式。 例如 : 7z a directory.7z directory 不要使用 "-r"，因为此标志不是像你预想的那样工作的 不要用 directory/* 替换 ".*" (例如 : "directory/*" 不包含 "directory/.profile") 例一 7z a -t7z -m0=lzma -mx=9 -mfb=64 -md=32m -ms=on archive.7z dir1 使用极限压缩将 "dir1" 中所有的文件压缩入 “archive.7z” 中。 -t7z 使用 7z 格式 -m0=lzma 压缩算法为 LZMA -mx=9 压缩等级为 9（最高等级） -mfb=64 LZMA 算法的快速字节 64 个 -md=32m 字典大小 = 32 Mb -ms=on 创建固实存档文件 例二 7z a -sfx archive.exe dir1 将 "dir1" 中所有的文件压缩入 "archive.exe" 这个自解压文件中（注意：自解压文件必须以 ".exe" 结尾） 例三 7z a -mhe=on -pmy_password archive.7z a_directory 将 "a_directory" 中所有的文件压缩入 "archive.7z" 中并使用密码加密 参见 7zr(1), 7z(1), p7zip(1), bzip2(1), gzip(1), zip(1), HTML 格式的文档 /usr/share/doc/p7zip-full/DOC/MANUAL/start.htm 作者 由 Mohammed Adnene Trojette 为 Debian 撰写。由 Robert Luberda 更新。 译者 本文由 ADD-SP 于 2020年8月24日 翻译。 7-Zip 2016年3月6日 7z(1)
不讨论细节，旨在勾勒出区块链的轮廓。
前言 不讨论细节，旨在勾勒出区块链的轮廓。由于是以介绍为主，所以不会涉及细节，而且**肯定会有一些错误**。如果有兴趣的话可以自行查询资料。 故事 我们先讲个故事：张三是一个三年级的学生，他的父母是做生意的。有一天管帐本的妈妈把账本弄丢了，付出了很大的代价才搞清楚资金的来龙去脉，于是父母商量了一下决定，爸爸和张三每次收支都要告诉妈妈，由妈妈记录这一笔帐，并每天晚上将账本公布，账本开头写上昨日三人的期初余额，结尾写上三人的期末余额。爸爸和张三检查无误后各自抄走一份，如果公布的账本有误则认为是假账，则选择昨日公认的账本作为今天的账本，今日的所有支出作废，损失由妈妈承担。 检查账本时都要检查下列项目： 每笔交易是否有资金支出者的签名 资金支出者是否有足够的钱支付 今日的收支是否计算得出账本结尾声明的三人的期末余额 今日账本三人的期初余额和昨日账本三人的期末余额一致 Q：妈妈是否可以记录一些虚假的交易，比如妈妈擅自记录一笔爸爸转给妈妈 100 块钱的账？ A：不可能，因为这笔转账必须由爸爸的签名才行，如果没有签名则晚上爸爸和张三检查账本的时候会被认为是假账，而损失却由妈妈自己承担。 Q：张三是否可以假装爸爸发给妈妈一笔给自己买零食的交替？ A：不能，因为没有爸爸的签名。 时间一长，妈妈发现记账很累，于是要求每记一笔账都要求支出放支付一块钱给妈妈。后续爸爸和张三发现妈妈记账很赚钱，于是想要自己来记账。一家人商量后达成一致，每个人都将自己的交易发送给其它两人，到了晚上汇总的时候，通过抽签选择采用谁的账本，并给予记账人报酬 有一次爸爸妈妈一起出差了，抽签抽到爸爸记账，但是爸爸很忙，没时间把账本给张三看，导致张三无法抄写今天的账本。后来一家人商量后达成一致，当允许抄写记账人以外的账本。比如爸爸很忙，但是妈妈有空，妈妈抄完后可以让张三抄。 解释 这个故事描述了区块链的大致工作流程和特点。 区块链中的就是故事中的账本，每天产生一个账本就是每天产生一个「区块」。「链」对应的就是昨日账本的期末余额和今日账本的期初余额，两种余额将两个账本连接在一起，对应将「区块」连接依赖的「链」。 区块链的特点 分布式：对应故事中账本并不是只有记账人有，其它人也有一份。对应现实中数据不只有一份，而是有很多份，在不同的地方存储。 去中心化：对应故事中对账本起决定作用的人不是记账人，只有一家三口都认同的账本才是有效账本。对应现实中一份数据的内容究竟是什么不是一个人或者一个组织能够决定的，而是要大多数参与者达成共识才能决定。 点对点：对应故事中抄写账本时可以从任何持有账本的人手中抄写一份，而不是必须从记账人手中抄写。对应现实中一个节点请求数组不是请求中心节点，而是请求任意一个节点即可。 难以篡改：对应故事中任何人都无法伪造账本中的交易记录，因为必须要有签名。对应现实中依靠密码学保证的防篡改功能。 区块链的工作流程 利用共识选择出一定时间段内的数据的管理员：对应故事中一家三口通过抽签决定记账人。 当数据发生改动时要对所有的节点进行广播：对应故事中一家三口在晚上决定记账人钱必须将自己的交易告知其余两人。 对于数据管理员公布的数据要进行检查：对应故事中要对账本进行的四种检查。 如果检查错误则回滚数据：对应故事中如果检查有误，则选择昨日公认的账本作为今天的账本，今日的所有支出作废。 如果检查无误则复制一份保存在本地：对应故事中没人都会抄写一份账本。 写在最后 本文是指介绍了一下区块链的特点时大致工作流程，并未涉及细节问题，如果有兴趣的话可以自行查询资料。
概述 昨天运营商线路出了问题，体验了一把电话线拨号上网一样的网速。 打开浏览器是这样的场景： 先白屏一会儿。 逐渐显示出网页上的全部文字。 卡一会儿。 慢慢地把网页上的所有图片显示出来。 正在无聊地等待网页加载的时候阴差阳错地意识到浏览器这种一边渲染一边下载的工作方式在历史上还促成了协程和线程这一概念的产生。那么就来讲一讲故事吧。 背景 早期的多道 OS 还只有进程这一概念，OS 的最小调度单位也是进程。每个程序都有自己独立的内存空间，互不干扰，看起来十分和谐。但是也有一些不那么方便的地方。 就拿浏览器举例，我们把浏览器的工作简化为下列几项： 从服务器下载数据 显示文本 解压缩图片 显示图片 如果我们按照从上到下的顺序来执行这些工作，会给用户带来非常差的使用体验，所以开发者希望这些工作可以交替进行，最好是能自由控制什么时机执行哪些工作。 能不能通过启动多个不同功能的进程实现呢？很难，因为访问其它进程的内存是一件很麻烦或者被 OS 禁止的事情，这就会导致从服务器下载下来的数据难以被文本显示进程等渲染进程获取到。同时因为进程的调度是由 Kernel 控制的，用户层无法干预，也就无法在合适的时机切换到指定的工作中。 协程 于是协程就产生了。开发者在用户层自己实现了一套调度机制，可以按照开发者的意愿切换到同一个进程内的不同的执行序列，同时把每个执行序列称为协程。 将其应用在上文提到的浏览器上就会变成四个执行序列，分别交替执行下载数据、显示文本、解压缩图片和显示图片这四项工作，开发者可以按照自己的意愿切换到某个工作中，比如图片解压缩协程完成了一部分解压工作后就主动切换到图片显示协程显示出部分图片。看起来很美好，但是还是有问题。 因为下载数据会涉及到网卡 I/O，而一旦启动了网卡 I/O，整个进程都会被阻塞，由于上文提到的四个协程都在同一个进程中，所以这四个协程也会随着进程的阻塞而被迫停下来，直到网卡 I/O 完成，进程被 kernel 调度到后才能继续。 这不是我们想要的，因为我们希望数据下载的过程中先切换到其它协程进行渲染已经下载的数据，不让用户等待那么久，但是现在只要启动了网卡 I/O，就不得不停下来等待，渲染工作也就无法进行。 线程 大家认识到这个问题后，也出于更好地利用多核和多处理器这一目的，就引入了线程这一概念，并很快在各个 OS 上实将其实现。一个进程可以有多个线程，这些线程共用它们所属的进程的内存空间，所以互相通信方便了很多。同时线程成为了 OS 调度的最小单位，其中一个线程因为某些原因阻塞不会影响到其它线程，于是上面的问题就得到解决了。 这时我们可以开四个线程，分别交替执行下载数据、显示文本、解压缩图片和显示图片这四项工作。这样即使数据下载线程阻塞，渲染线程也能继续执行渲染工作。 不过缺点是**线程**的调度完全由 kernel 控制，用户层无法干预。不过这不是问题，一种做法就是开两个线程 A 和 B： A 线程：从服务器下载数据。 B 线程：在线程内部启动三个协程，分别负责显示文本、解压缩图片和显示图片，这三个渲染工作可以按照开发者的意愿自由地切换，这样也保留一定的自由度。 总结 协程是最近几年才火起来的，容易让人产生协程是新的概念这一错觉，实际上协程的出现比线程要早。 协程的优点是切换代价比线程小很多，自由度高，缺点是一旦启动了 I/O 会被全部阻塞。 线程的优点就是其中一个线程被阻塞，不会影响到其它线程，缺点是切换代价比协程大很多。 折中的方案就是按照程序的特点在特定的线程上启动一定数量的协程，一些需要 I/O 的工作则交给其它的线程，保证 I/O 时不会阻塞协程。这种方式固然优点都不如线程和协程那么大，但是缺点也不如线程和协程那么大。所以有时候太极端也不是好事，折中一下通常是不错的解决方案。放弃了一些东西也会得到一些东西。
BTC 实现了去中心化的货币，而 ETH 则实现了去中心化的合约，即智能合约。
概述 现实中合约的有效性通常是通过法律来保证的，当合约出现纠纷，或者一方违约，可以通过法律手段解决，如起诉违约人等。 BTC 实现了去中心化的货币，而 ETH 则实现了去中心化的合约，即智能合约。 智能合约是存储在区块链上的一段代码，可以被调用，代码一般使用 Solidity 语言编写，运行在 EVM（Ethereum Virtual Machine）上，是图灵完备的语言，且代码一旦上链，则极难更改，这有效地保证了合约的有效性，但是如果合约代码有漏洞，也极难有修复，ETH 社区就曾经因为这个出现了 Hard Fork。 智能合约对帐户的更改，形成的交易和收据会存储在状态树、交易树和收据树中，并最终通过将三棵树的根哈希值写入 block header 来与其它全节点达成共识。 将介绍下列内容： 合约账户 嵌套调用 汽油费 其它特性 合约账户 智能合约的账户保存了合约的当前状态： balance：余额 nonce：交易次数 code：合约代码 storage：存储，数据结构是一颗 MPT。 嵌套调用 智能合约的执行具有原子性，执行过程中出现错误会导致回滚。 嵌套调用是指一个合约调用另一个合约中的函数。 嵌套调用有时会触发连锁式的回滚，有时则不会。 汽油费 每条语句会消耗一定汽油费（gas）。 复杂的指令消耗的多，简单的指令消耗少，也有一些是免费的。 汽油费由发起调用的外部账户一次性支付，如果没有用完则退还。 如果汽油费耗尽但是合约未执行完毕，则回滚更改，但是不退汽油费。 最终获取记账权的矿工会获得执行合约的汽油费。 执行发生错误的交易依然要支付汽油费。 Q：为什么汽油费不足时不退还？ A：为了避免恶意的合约调用，如果退还的话，我就可以发布一个合约，里面是死循环代码，然后一直调用这个合约让矿工白白浪费算力，而我却几乎没有任何损失。 Q：为什么执行发生错误的交易依然要支付汽油费？ A：原因同上，主要是为了反之恶意的合约和恶意的调用。 Q：每个区块内部的合约调用消耗的总汽油费是否有限制？ A：有限制，记录在`header.gasLimit`字段，每个矿工在决定了要发布的区块时可以微调这个值，上调或下调$\frac{1}{1024}$。 其它特性 一个合约内的函数可以调用另一个合约内的函数。 一个外部账户也可以调用一个合约内的函数。 合约代码不支持try....catch，若执行时出现异常会触发回滚。 代码禁止 syscall。 合约代码只有被外部账户或者其它合约调用时才会执行，无法实现如定时执行等功能。 合约代码不允许多线程。 Q：为什么禁止 syscall？ A：因为执行代码的环境不同，syscall 返回的结果也可能不同，容易导致最终的状态不一致，不同全节点之间难以达成共识。 Q：为什么合约代码不允许多线程？ A：多个合约并发/并行执行可能会导致最终的状态无法一致。一个例子： 合约 A：如果张三账户中至少有 100 个币则转给李四 10 个币。 合约 B：如果张三账户中不足 100 个币则转给李四 5 个币。 合约 A 中现在恰好有 100 个币。 正常的执行顺序是先执行 A，后执行 B。运行结果是李四收到 15 个币，张三的账户剩余 85 个币。 但是如果是并发/并行执行，则可能先执行 B，等待 B 完成后才执行 A，这种情况下李四收到 10 个币，张三的账户剩余 90 个币。 也就是最终相关的状态会不一致，也就是状态的转移不确定。各个全节点之间无法达成共识。 参考资料 北京大学肖臻老师《区块链技术与应用》公开课
由于智能合约代码存储在区块链上，所以一旦其有漏洞也极难修复。TheDAO 是运行在 ETH 区块链平台上的一个智能合约，但是其代码存在漏洞，于是黑客发起了 Re-entrancy Attack 盗走了大量的以太币，最终导致了以太坊分裂为 ETH（Ethereum） 和 ETC（Ethereum Classic）。
概述 由于智能合约代码存储在区块链上，所以一旦其有漏洞也极难修复。TheDAO 是运行在 ETH 区块链平台上的一个智能合约，但是其代码存在漏洞，于是黑客发起了 Re-entrancy Attack 盗走了大量的以太币，最终导致了以太坊分裂为 ETH（Ethereum） 和 ETC（Ethereum Classic）。 本系列的文章不会着重于细节讲解，对于本次攻击的详情可以参考这篇文章：Analysis of the DAO exploit。 将介绍下列内容： TheDAO 补救措施 以太坊的分裂 TheDAO DAO 是 Decentralized Autonomous Organization（分布式自治组织）的简称，TheDAO 是一个基于 ETH 区块链平台的众筹项目。其目的是让持有 TheDAO 代币的参与者通过投票的方式共同决定被投资项目，整个社区完全自制，其功能通过智能合约实现。众筹完成时共募集 1150 万以太币，在当时的价值达到 1.49 亿美元。 Child DAO 允许 TheDAO 代币的持有者调用 TheDAO 合约中的 `split` 函数，创建一个子合约，持有者在原有 TheDAO 智能合约中的 TheDAO 代币被销毁，存储在原 TheDAO 智能合约中对应的以太币被转移到新的 TheDAO 智能合约中。 Child DAO 是为了保护处于弱势地位了 TheDAO 代币持有者，使他们也可以有一个可以投票和分红的 DAO。黑客发起攻击后 ETC 就锁定在了 Child DAO 内。 补救措施 Child DAO 创建后有 28 天的锁定期，这段时间无法将以太币转走，就是段时间给了以太坊社区补救的机会。 以太坊团队设计了两步走方案： 锁定黑客账户。 设法将盗取的以太币退还。 以太坊首先发布了一个软分叉方案，新的软件升级中加入了一条规则：凡是和 TheDAO 相关的账户一律不允许交易。升级发布后大多数矿工都升级的软件。但是新的软件有一个 bug。 当新的软件执行某个交易与 TheDAO 相关时，这个交易失败，但是没有收取汽油费。这就导致了整个系统中充斥着大量的与 TheDAO 相关的交易，消耗了大量的资源，因此大多数矿工又将软件版本退回了更新前。 软分叉的措施失败了，此时距离 28 天的锁定期结束已经很近了。于是以太坊团队发布了一个硬分叉方案，所有 TheDAO 账户中的以太币都被强制转到另一个账户，事后再想办法将其退还。 这次没有出现意外，最终被盗取的以太币都退还了 以太坊的分裂 在攻击发生后，以太坊社区有两个观点，双方进行了激烈讨论，最终没能达成一致，导致了分裂。 应该采取措施 这一派认为应该采取措施方式黑客将以太币盗走，原因是 TheDAO 是太大而不能倒的合约，TheDAO 筹集的以太币大概占有当时总流通量的 10% 左右。以太坊开发团队也持有这种观点。 但是不要认为合约出了问题以太坊团队就会介入，主要还是因为这个合约的影响太大了。 不应采取措施 这一派认为黑客的行为并没有违反任何规则，因为智能合约的代码就是规则，按照规则行事何谈违反？ 更重要的观点是，如果出了问题就要回滚，那么还谈什么去中心化的不可篡改的账本？开发团队一句话就能强行更改账本，这和中心化有何区别？ 分裂 两派的矿工都没能说服对方，最终导致以太坊分裂成了两条链，一条就是采取了措施的链，称为 ETH（Ethereum），另一条是没有采取任何补救措施的链，称为 ETC（Ethereum Classic）。直到现在这两条链依旧存在，只是 ETH 上的算力更强。 在 ETC 上挖矿人一部分是出于投机目的，因为这条链算力更少，更容易挖出区块。另一部分人是纯粹出于信仰，这些坚持十分纯正的去中心化理念，认为 ETC 才是正宗的以太坊，而 ETH 已经偏离的去中心化的初衷。 参考资料 北京大学肖臻老师《区块链技术与应用》公开课
概述 Windows 10 的任务栏非常简约，扁平的半透明阴影，但是却没有提供原生的设置透明度的方式，为了实现 ...
概述 Windows 10 的任务栏非常简约，扁平的半透明阴影，但是却没有提供原生的设置透明度的方式，为了实现这个功能已经有了如 TranslucentTB 这类优秀的软件。但笔者依然对其原理有些好奇。本来打算看 TranslucentTB 的源码，无奈看起来费劲，先看看能不能蒙出来。 目前进度如下: 修改注册表键值（需重启 explorer.exe） 直接修改 explorer.exe 的内存（进行中） 反汇编时发现的一个未公开的 Win32 API 注册表键值 通过查阅资料发现透明度可以通过修改注册表键值实现。 HKEY_CURRENT_USER\Software\Microsoft\Windows\CurrentVersion\Explorer\Advanced下的TaskbarAcrylicOpacity键，类型是DWORD，默认不存在。取值范围$[0,255]$，表示透明度。 HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\Explorer\Advanced下的UseOLEDTaskbarTransparency键，如果存在则修改为 0，或者删除。 修改完成后重启 explorer.exe 即可看到效果。 直接修改 explorer.exe 的内存（进行中） 将TaskbarAcrylicOpacity设置为 200。 使用 CE 搜索这个值，发现透明度存储在explorer.exe + 0x30E9D0，修改后可立即生效。 给explorer.exe + 0x30E9D0下访问断点后动态调式，发现入口为explorer.exe + 0x76424的函数读取了这个代表透明度的值。 explorer.exe+76424 - 40 53 - push rbx explorer.exe+76426 - 48 83 EC 20 - sub rsp,20 { 32 } explorer.exe+7642A - 80 3D 7B832900 00 - cmp byte ptr [explorer.exe+30E7AC],00 { (1),0 } explorer.exe+76431 - 8B D9 - mov ebx,ecx explorer.exe+76433 - 74 0C - je explorer.exe+76441 explorer.exe+76435 - 8A 05 95852900 - mov al,[explorer.exe+30E9D0] { (200) } explorer.exe+7643B - 48 83 C4 20 - add rsp,20 { 32 } explorer.exe+7643F - 5B - pop rbx explorer.exe+76440 - C3 - ret 通过 callstack 发现入口为explorer.exe + 0x7629C的函数调用了上文提到的入口为explorer.exe + 0x76424的函数，并且读取了UseOLEDTaskbarTransparency键。 explorer.exe+7629C - 40 55 - push rbp explorer.exe+7629E - 53 - push rbx explorer.exe+7629F - 57 - push rdi explorer.exe+762A0 - 48 8B EC - mov rbp,rsp explorer.exe+762A3 - 48 83 EC 40 - sub rsp,40 { 64 } explorer.exe+762A7 - 48 8D 55 20 - lea rdx,[rbp+20] explorer.exe+762AB - 48 8D 4D 38 - lea rcx,[rbp+38] explorer.exe+762AF - E8 1C070000 - call explorer.exe+769D0 explorer.exe+762B4 - 33 FF - xor edi,edi explorer.exe+762B6 - B0 FF - mov al,-01 { 255 } explorer.exe+762B8 - 40 38 7D 20 - cmp [rbp+20],dil explorer.exe+762BC - 0F84 93000000 - je explorer.exe+76355 explorer.exe+762C2 - 48 8D 45 30 - lea rax,[rbp+30] explorer.exe+762C6 - 89 7D 28 - mov [rbp+28],edi explorer.exe+762C9 - 48 89 44 24 30 - mov [rsp+30],rax explorer.exe+762CE - 4C 8D 05 8BC32100 - lea r8,[explorer.exe+292660] { ("UseOLEDTaskbarTransparency") } explorer.exe+762D5 - 48 8D 45 20 - lea rax,[rbp+20] explorer.exe+762D9 - C7 45 30 04000000 - mov [rbp+30],00000004 { 4 } explorer.exe+762E0 - 48 89 44 24 28 - mov [rsp+28],rax explorer.exe+762E5 - 48 8D 15 D4C22100 - lea rdx,[explorer.exe+2925C0] { ("SOFTWARE\Microsoft\Windows\Curr") } explorer.exe+762EC - 48 8D 45 28 - lea rax,[rbp+28] explorer.exe+762F0 - 41 B9 12000010 - mov r9d,10000012 { 268435474 } explorer.exe+762F6 - 48 C7 C1 02000080 - mov rcx,FFFFFFFF80000002 { -2147483646 } explorer.exe+762FD - 48 89 44 24 20 - mov [rsp+20],rax explorer.exe+76302 - 48 FF 15 A7002100 - call qword ptr [explorer.exe+2863B0] { ->KERNELBASE.RegGetValueW } explorer.exe+76309 - 0F1F 44 00 00 - nop dword ptr [rax+rax+00] explorer.exe+7630E - 8D 4F 0D - lea ecx,[rdi+0D] explorer.exe+76311 - 85 C0 - test eax,eax explorer.exe+76313 - 74 48 - je explorer.exe+7635D explorer.exe+76315 - 3D EA000000 - cmp eax,000000EA { 234 } explorer.exe+7631A - 0F44 C1 - cmove eax,ecx explorer.exe+7631D - 8B C8 - mov ecx,eax explorer.exe+7631F - 85 C0 - test eax,eax explorer.exe+76321 - 7E 09 - jle explorer.exe+7632C explorer.exe+76323 - 0FB7 C9 - movzx ecx,cx explorer.exe+76326 - 81 C9 00000780 - or ecx,80070000 { -2147024896 } explorer.exe+7632C - 8B 45 20 - mov eax,[rbp+20] explorer.exe+7632F - 85 C9 - test ecx,ecx explorer.exe+76331 - 0FB6 D8 - movzx ebx,al explorer.exe+76334 - 0F48 DF - cmovs ebx,edi explorer.exe+76337 - E8 58010000 - call explorer.exe+76494 explorer.exe+7633C - 84 C0 - test al,al explorer.exe+7633E - 74 3A - je explorer.exe+7637A explorer.exe+76340 - 8B 4D 38 - mov ecx,[rbp+38] explorer.exe+76343 - E8 DC000000 - call explorer.exe+76424 explorer.exe+76348 - 84 DB - test bl,bl explorer.exe+7634A - 0FB6 C0 - movzx eax,al explorer.exe+7634D - B9 99000000 - mov ecx,00000099 { 153 } explorer.exe+76352 - 0F45 C1 - cmovne eax,ecx explorer.exe+76355 - 48 83 C4 40 - add rsp,40 { 64 } explorer.exe+76359 - 5F - pop rdi explorer.exe+7635A - 5B - pop rbx explorer.exe+7635B - 5D - pop rbp 通过 callstack 发现入口为explorer.exe + 0x761D4的函数调用了上文提到的入口为explorer.exe + 0x7629C的函数，并且调用了一个 Win32 API：SetWindowCompositionAttribute。通过查阅资料发现这是一个未公开的 API。 explorer.exe+761D4 - 48 89 5C 24 10 - mov [rsp+10],rbx explorer.exe+761D9 - 48 89 74 24 18 - mov [rsp+18],rsi explorer.exe+761DE - 57 - push rdi explorer.exe+761DF - 48 83 EC 60 - sub rsp,60 { 96 } explorer.exe+761E3 - 48 8B 05 7E522900 - mov rax,[explorer.exe+30B468] { (2.66) } explorer.exe+761EA - 48 33 C4 - xor rax,rsp explorer.exe+761ED - 48 89 44 24 50 - mov [rsp+50],rax explorer.exe+761F2 - 48 8B F1 - mov rsi,rcx explorer.exe+761F5 - 48 8D 54 24 20 - lea rdx,[rsp+20] explorer.exe+761FA - 33 C9 - xor ecx,ecx explorer.exe+761FC - E8 CF070000 - call explorer.exe+769D0 explorer.exe+76201 - 80 7C 24 20 00 - cmp byte ptr [rsp+20],00 { 0 } explorer.exe+76206 - BF 01000000 - mov edi,00000001 { 1 } explorer.exe+7620B - 74 0F - je explorer.exe+7621C explorer.exe+7620D - E8 82020000 - call explorer.exe+76494 explorer.exe+76212 - F6 D8 - neg al explorer.exe+76214 - 1B FF - sbb edi,edi explorer.exe+76216 - 83 E7 02 - and edi,02 { 2 } explorer.exe+76219 - 83 C7 02 - add edi,02 { 2 } explorer.exe+7621C - E8 7B000000 - call explorer.exe+7629C explorer.exe+76221 - 0FB6 D8 - movzx ebx,al explorer.exe+76224 - E8 0B050000 - call explorer.exe+76734 explorer.exe+76229 - 83 64 24 4C 00 - and dword ptr [rsp+4C],00 { 0 } explorer.exe+7622E - 25 FFFFFF00 - and eax,00FFFFFF { 16777215 } explorer.exe+76233 - 8B D3 - mov edx,ebx explorer.exe+76235 - 89 7C 24 40 - mov [rsp+40],edi explorer.exe+76239 - C1 E2 18 - shl edx,18 { 24 } explorer.exe+7623C - 48 8B CE - mov rcx,rsi explorer.exe+7623F - 0B C2 - or eax,edx explorer.exe+76241 - C7 44 24 38 10000000 - mov [rsp+38],00000010 { 16 } explorer.exe+76249 - 89 44 24 48 - mov [rsp+48],eax explorer.exe+7624D - 48 8D 54 24 28 - lea rdx,[rsp+28] explorer.exe+76252 - B8 13000000 - mov eax,00000013 { 19 } explorer.exe+76257 - 89 44 24 44 - mov [rsp+44],eax explorer.exe+7625B - 89 44 24 28 - mov [rsp+28],eax explorer.exe+7625F - 48 8D 44 24 40 - lea rax,[rsp+40] explorer.exe+76264 - 48 89 44 24 30 - mov [rsp+30],rax explorer.exe+76269 - 48 FF 15 08F82000 - call qword ptr [explorer.exe+285A78] { ->user32.SetWindowCompositionAttribute } explorer.exe+76270 - 0F1F 44 00 00 - nop dword ptr [rax+rax+00] explorer.exe+76275 - 48 8B 4C 24 50 - mov rcx,[rsp+50] explorer.exe+7627A - 48 33 CC - xor rcx,rsp explorer.exe+7627D - E8 4E2F0200 - call explorer.exe+991D0 explorer.exe+76282 - 4C 8D 5C 24 60 - lea r11,[rsp+60] explorer.exe+76287 - 49 8B 5B 18 - mov rbx,[r11+18] explorer.exe+7628B - 49 8B 73 20 - mov rsi,[r11+20] explorer.exe+7628F - 49 8B E3 - mov rsp,r11 explorer.exe+76292 - 5F - pop rdi explorer.exe+76293 - C3 - ret 一个未公开的 Win32 API 未完待续
ETH 采用的是基于账户的模式，也就是说系统中显示记录着每个账户的当前状态信息。 每个账户都是由一个 160bit 的地址组成，外部账户中的状态包含余额（balance）、交易次数（nonce），合约账户包含 code(代码)、存储(stroge)。 对于一个出块时间只有十几秒的系统，如何高效地维护这些状态就成了十分重要的事情。
概述 状态树 交易树和收据树 状态树 ETH 采用的是基于账户的模式，也就是说系统中显示记录着每个账户的当前状态信息。 每个账户都是由一个 160bit 的地址组成，外部账户中的状态包含余额（balance）、交易次数（nonce），合约账户包含 code(代码)、存储(stroge)。 对于一个出块时间只有十几秒的系统，如何高效地维护这些状态就成了十分重要的事情。 为何不使用 HashTable？ 由于每个账户都是一个 160bit 的地址，所以很容易想到用 HashTable 来建立地址到账户状态的映射，在不考虑哈希碰撞的情况下，增删改查都是常数时间。国安民乐，岂不美哉？ 那么为什么 ETH 没有使用 HashTable 呢？原因就是建立共识代价太大。 BTC 建立共识是通过将区块内的交易组织成一颗 Merkle Tree，并将根哈希值写入 Block Header，其它全节点验证时只需要重新计算区根哈希值即可，同时因为一个 Block 的大小有限制，所以达成共识的代价比较小。 ETH 如果使用 HashTable 管理账户状态的话，两个全节点要达成共识代价都很大，因为要遍历整个 HashTable 才可以。 为何不使用 Merkle Tree？ 那么如果把账户的状态组织成一颗 Merkle Tree 怎么样呢？这样是不是就可以像 BTC 一样方便了？ 看起来我们可以直接将根哈希值写入到 Block Header 里，以求向 BTC 一样达成共识。 Q：如果两个全节点记录的账户状态完全一致，它们的根哈希值一定相同么？ A：不一定，因为两个全节点听到各个账户的顺序可能不同，如果不对 Merkle Tree 进行排序的话，两个全节点构造出来的 Merkle Tree 的根哈希值很可能不同。如果不同，也就无法达成共识。 Q：如果使用排序的 Merkle Tree 是不是就没问题了？ A：如果监听到了一个新的账户，由于 Merkle Tree 时排序的，所以就要将其插入到合适的位置，那么就需要重新组织 Merkle Tree，代价过大，不能接受。 MPT（Merkle Patricia Trie） 最终 ETH 使用了 MPT 管理各个账户的状态，MPT 是 Merkle Tree 和 Patricia Trie 组合而成的数据结构。 Patricia Trie 就是经过路径压缩的 Trie。 Q：新增一个账户时的代价是否可以接受？ A：可以，因为 160bit 的账户地址很容易地就能添加到 Patricia Trie，且由于 Patricia Trie 的特点，对其更新都是局部的，不会导致整棵树的重组。 Q：更改一个账户状态时的代价是否可以接受？ A：可以，因为 Patricia Trie 的查找仅需对数时间，同时由于 Patricia Trie 的特点，其更改也是局部的，同样不会导致整棵树的重组。 Q：达成共识的代价是否可以接受？ A：Merkle Patricia Trie 中 将 Patricia Trie 中的传统的地址指针改成了哈希指针，且由于只要存储的地址完全一致，则无论存储顺序的先后，Patricia Trie 只有一种形态，所以可以算出 Merkle Patricia Trie 的根哈希值并写道 Block Header 中，达成共识十分简单。 每个区块都会保存完整的状态树，而不是仅和当前区块内交易有关的节点。因为这样可以便于回滚。也可以这么考虑： 账户 A 转账给 账户 B 一个币。 那么就应该先知道两个账户的余额。 如果每个区块只保存与当前区块有关联的账户的状态树节点。 且假设 B 是一个新的账户。 那么这个新账户肯定不会存在于状态树中。 由于每个区块没有保留完整的状态树，所以只能顺着区块链去找。 那么查询账户 B 的状态，最终会找到 Genesis Block，才会发现这是一个新账户。 代价太大，不能接受。 交易树和收据树 ETH 每次发布区块的时候其包含的交易会被组织成一颗交易树，每笔交易执行完后会产生一个收据，记录相关信息，最终也会被组织成收据树，这两棵树均为 MPT。 交易树和收据树也可以提供根哈希值。同时 ETH 也提供了比较复杂的查询操作，如查询 10 天内和某个智能合约有交易的账户等。为了降低这类查询操作的代价，ETH 使用了 Bloom Filter ，它可以让矿工在沿着区块链查找交易等信息时快速地排除不可能包含所需信息区块。 Bloom Filter 参考资料 北京大学肖臻老师《区块链技术与应用》公开课
ETH 的出块时间大概只有十几秒，如此短的出块时间如果照搬 BTC 的共识机制就会造成大量的分叉，分散全网算力，降低系统的安全性。因此 ETH 采用了新的 GHOST 协议，可以很好地适应十几秒地出块时间，保证系统的安全性，同时也在一定程度上平衡了收益，使得大型矿池相对于 BTC 不再那么占便宜。：
概述 ETH 的出块时间大概只有十几秒，如此短的出块时间如果照搬 BTC 的共识机制就会造成大量的分叉，分散全网算力，降低系统的安全性。因此 ETH 采用了新的 GHOST 协议，可以很好地适应十几秒地出块时间，保证系统的安全性，同时也在一定程度上平衡了收益，使得大型矿池相对于 BTC 不再那么占便宜。 使用基于 GHOST 的协议主要出于两个目的： 维护系统安全性 平衡收益 维护系统安全性 出块时间只有十几秒，由于 Flooding 的时候没有考虑底层拓扑，如此短的时间一个区块难以广播给全网，容易造成大量分叉，如果继续使用 BTC 的共识协议，极其容易分散算力，降低系统安全性。 平衡收益 以下图所示情况为例说明 我们假设 A、C 两个 Block 时个人矿工所挖出的，B 是大型矿池挖出的 Block。假设三个 Block 同时挖出。 显然大型矿池的所在的链最有可能成为最合法链，也就是 B 所在的链。 同时其它矿工出于不想浪费算力的目的，通常会选择跟在大型矿池的后面继续挖矿，而不是跟在个人区块后面。 理想情况下如果一个矿工占有 n% 的算力，那么他的收益也应该是所有矿工收益的 n%。 现在 B 所在的链成为最长合法链的概率会超过 n%。 因为个体矿工愿意跟随大型矿池进行挖矿。 那么大型矿池的期望收益就会超过 n%。 我们不希望这种情况出现。 上图这种情况下，ETH 规定，D 在发布的时候可以将 A、C 作为 Uncle Block 包含进 D 中，那么 A 和 C 的其所有者会获得 7/8 的出块奖励。一个区块中最多只能包含两个 Uncle Block。 对于上图这种情况，能被包含进 J 的 Uncle Block 只能是 A 和 C。而 E、G、F、H 不能作为 Uncle Block，否则这会降低矿工合并分支的积极性。 Uncle Block 最多往前追溯七代，收益每隔一代减少 1/8，七代之前的区块不允许作为 Uncle Block。 这么做就可以在一定程度上平衡了收益，也能鼓励分叉尽快进行合并。 参考资料 北京大学肖臻老师《区块链技术与应用》公开课
概述 外部账户（Externally Owned Accounts） 合约账户（Smart Contract Accounts） BTC 采用基于交易的模型，一个账户有多少币没有显式记录，如果想要知道只能顺着区块链去查 UTXO 才能知道。 ETH 采用基于账户的模型，就像现实中的银行账户一样，一个账户有多少币是显式记录在区块链上的，每次支付的时候就检测余额并修改余额即可，无需关注币的来源。 为什么 ETH 会使用这种模式呢？因为 ETH 要支持智能合约（Smart Contract），这就要求参与者的身份要比较稳定，否则很难知道合约的参与者究竟是哪个账户 外部账户（Externally Owned Accounts） 外部账账户内记录的下列内容： balance：余额 nonce：不同于 BTC，这里则表示该账户发起过多少笔交易。 账户的信息都会记录到一种名为「状态树」的数据结构中，每次交易都会更新状态树的对应节点。 重放攻击（Replay Attack） 基于账户的模式天然地对双花攻击（Double Spending Attack）有防范作用，但是容易成为重放攻击的目标。 重放攻击就是将一个已经上链的交易重新放送一次，由于这笔交易具有被攻击账户的签名，所以全节点会认为其是有效的，所以会导致一笔交易重复执行。对此 ETH 通过`nonce`字段来防范，这个字段记录了该账户发起过多少笔交易，每笔交易都要记录当前的`nonce`值，这样就可以有效防范重放攻击了。因为当一笔交易被重放时，其`nonce`值会比账户中记录的`nonce`值小，由此可以判断该账户没有发起过这笔交易。 合约账户（Smart Contract Accounts） 合约账户记录下列内容： Code：即合约代码 Storage：相关数据 合约内的代码可以被其它用户所调用，也可以被其它合约调用，用以完成合约的功能。创建合约的时候会返回一个地址，根据这个地址即可调用合约代码。 智能合约代码一般使用 Solidity 语言编写，运行在 EVM（Ethereum Virtual Machine）上，且代码一旦上链，则极难更改。 参考资料 北京大学肖臻老师《区块链技术与应用》公开课
ASIC Resistance & GHOST 协议 & 基于账户的交易模式（Account-based ledger）& 权益证明（Proof Of Stake） & 智能合约（Smart Contract）
概述 ASIC Resistance GHOST 协议 基于账户的交易模式（Account-based ledger） 权益证明（Proof Of Stake） 智能合约（Smart Contract） ASIC Resistance BTC 起初的设计目的包括让通用计算机加入到挖矿过程，但是目前 BTC 的挖矿主要是由 ASIC 矿机完成的，使用通用计算机挖矿几乎是无利可图的。 而 ETH 希望避免这种情况，其设计了 Member hard mining puzzle，因为 ASIC 矿机的访存效率不一定有通用计算机高，如果 Mining puzzle 对内存的要求较高，则可以很好地对抗 ASIC 矿机。 GHOST 协议 ETH 的出块时间大概只有十几秒，如此短的出块时间如果照搬 BTC 的共识机制就会造成大量的分叉，分散全网算力，降低系统的安全性。因此 ETH 采用了新的 GHOST 协议，可以很好地适应十几秒地出块时间，保证系统的安全性。 基于账户的交易模式（Account-based ledger） BTC 中一个矿工有多少币是没有显式记录的，想要知道余额只能去查 UTXO，而 ETH 是基于账户的交易模式，自己有多少币很容易就知道了。所有账户的信息存储在全节点的状态树，当某个账户发起交易时，全节点会检查账户信息以确定交易的合法性。当然这种模式的优点不止一个，当然有了新的缺点，笔者将会在其它文章中说明。 权益证明（Proof Of Stake） BTC 采用的是 Proof Of Work，即「工作量证明」，而 ETH 则计划逐步转为 Proof Of Stake，即「权益证明」，这也是 ETH 对抗 ASIC 矿机的一种方式。权益证明简单来说就是持有的币越多，在系统中的话语权越大。详细情况笔者将会在其它文章中说明。 智能合约（Smart Contract） 智能合约是存储在区块链上的一段代码，可以被调用。 BTC 的脚本语言只能实现一些简单的操作。而 ETH 常常用于书写智能合约的语言 Solidity 是图灵完备的，这代表其可以又有十分强大的功能。 智能合约作为去中心化的合约，其合约的有效性是靠 ETH 系统的安全性保证的。 参考资料 北京大学肖臻老师《区块链技术与应用》公开课
概述 Double spending attack（双花攻击） Boycott Double spending attack 即双花攻击，一个比特币可以花至少两次。 攻击举例 Boycott 即可以通过拒绝某些或某个账户的交易来达到封锁这个账户的目的。 攻击举例 参考资料 北京大学肖臻老师《区块链技术与应用》公开课
因比特币的底层技术为区块链，所以其天然的具有匿名性。
概述 何为匿名 如何提高比特币用户的匿名性 什么情况下会破坏比特币的匿名性 典型案例 零知识证明 零币和零钞 何为匿名 比特币的匿名性个人我个人理解就是如果我自己不说，别人不会知道这个哪些账户是我的，以及这些我和某个具体人的发生了比特币交易。 什么情况下会破坏比特币的匿名性 当比特币和实体世界发生联系的时候，比如我用比特币换了现实中流通的货币，或者用比特币买了某件商品，甚至是用某台电脑登录了比特币账户。 区块链中交易信息是完全公开的，可以通过时间来找出账户对应的使用者。如我早上八点看见张三用比特币一杯豆浆，中午十二点看见张三用比特币点了一份外卖。这时我去查区块链中的对应时间点的交易记录，则就可以大大缩小寻找范围，长此以往，很容易就能知道张三使用的时哪个账户。 典型案例 Silk Road，（丝路）是一个利用Tor的隐密服务来运作的黑市购物网站，Tor的服务保证了网站用户的匿名性。你能想到的东西都有可能在里面找到。只要你情我愿，就可以形成一笔买卖。毒品、性奴、儿童色情、私人杀手，这里没有“非法交易”，用比特币结算还使得交易变得无法追踪，从而受到了大量毒贩的青睐。 https://baike.baidu.com/item/Silk%20Road 最终幕后老板在旧金山被捕了，更重要的一点是此人一个比特币都没有花，也就是说没有用比特币换取过实体世界的任何物品和货币。后来出现了 Silk Road 2，没几年也被查封了 零知识证明 零知识证明是指一方（证明者）向另一方（验证者）证明一个陈述是正确的，而无需透露除该陈述是正确的外的任何信息。 同态隐藏 如果 $x \neq y$，那么它们的加密函数值 $E(x),E(y)$ 也不相同。 给定 $E(x)$ 的值，很难反推出$x$的值。 给定$E(x)$ 和$E(y)$的值，可以很容易地计算出关于$x,y$的加密函数值。 同态加法：通过$E(x)$和$E(y)$计算出$E(x + y)$。 同态乘法：通过$E(x)$和$E(y)$计算出$E(x \times y)$。 上述的加法乘法均可扩展到多项式。 例子 Alice 想要向 Bob 证明她知道一组数 $x,y 且 x+y=7$，同时不想让 Bob 知道$x$ 和$y$ 的具体数值。 Alice 向 Bob 提供$E(x)$ 和$E(y)$的值。 Bob 通过$E(x)$ 和$E(y)$计算出$E(x+y)$的值，并计算$E(7)$的值。 如果 $E(x+y)=E(7)$则代表 Alice 没有说谎。 此时 Bob 难以知道$x$和$y$的具体数值。 盲签 在签名者不知道被签名信息的具体内容下对其进行签名。具体信息可自行查询资料详细了解。 零币和零钞 零币和零钞在协议层就融合了匿名化处理，其匿名性来自密码学保证。 但其并不安全，因为要用其牟利总归要与实体世界发生交互。 不要用零币和零钞进行任何违法犯罪活动。天网恢恢，疏而不漏。 零币 零币系统中存在基础币和零币，通过基础币和零币之间的转换来消除旧地址和新地址之间的关联。 零钞 零钞系统使用 zk-SNARKs 协议，不依赖基础币，只记录交易的存在性和矿工用来验证的系统正常运行的关键信息，区块链上既不现实交易地址也不显示交易金额，所有交易通过零知识验证的方式进行。 参考资料 北京大学肖臻老师《区块链技术与应用》公开课
概述 产生分叉的原因 Hard fork Soft fork 产生分叉的原因 比特币系统的协议的修改升级，因此产生的分叉称为 Protocd fork。 日常挖矿过程中也会产生一些分叉，但是通常会很快地淘汰掉其它分叉，只留下一个。 攻击者的攻击。 Hard fork 即这个分叉是永远的。 如果对比特币协议增加新的特性，那些没有升级软件的节点不认可新特性，这个时候就会产生硬分叉，一个例子是比特币的区块大小限制。假如有人发布了软件更新，将区块大小限制从 1M→4M，假设大多数节点都更新了，少数节点没有更新，这里的大多数不是按账户数目来算，而是按算力来算，即系统中有大多数 hash 算力的节点都更新了软件。新节点能挖出最大是 4M 的区块，但是旧节点不认可大小超过1M的区块，所以旧节点会沿着小区块一直挖下去，新节点大小区块都接受，但旧节点不接受大区块，两者没有达成共识，这样就会出现一个永远的分叉。 Soft fork 即暂时性的分叉。 如果对比特币协议加一些限制，原来合法的交易或者区块在新的协议可能不合法就会引起软分叉。假如有人发布更新将区块大小变成 0.5M，大部分节点更新了软件，少部分没有更新。这时候新节点沿着一条链挖小区块，不认可大区块，旧节点沿着一条链挖大区块，同时旧节点也认可小区块，因为新节点算力更强，所以更快形成一条新旧节点都认可的最长合法链，但是新节点不会在旧节点产出的大区块后面继续挖，而是会继续分叉挖小区块，所以旧节点挖出的大区块最后都没用了，不得不更新软件。 参考资料 北京大学肖臻老师《区块链技术与应用》公开课
CPU 挖矿 & GPU 挖矿 & ASIC 挖矿 & 大型矿池
概述 CPU 挖矿 GPU 挖矿 ASIC 挖矿 大型矿池 CPU 挖矿 起初挖矿竞争还不激烈，大家可以随意用自己的 PC 进行挖矿，出块的概率也算可以接受。但是随着竞争的激烈，CPU 无法满足算力的需求，因为通用计算机的的CPU在挖矿过程中只用到了 CPU 指令的很少的一部分。 GPU 挖矿 这时大家开始用GPU挖矿，因为 GPU 是专门为了通用的大规模的并行运算而开发的，效率要比 CPU 挖矿提高了很多。 但是由于 GPU 依然是为了通用计算而开发，所以依然有很多功能无法利用导致浪费，如浮点运算。 ASIC 挖矿 Application Specific Integrated Circuit，专门为了某种需求开发的芯片。随着挖矿竞争的加强，也有了专门用于挖矿的芯片，这种芯片的性价比是很高的，效率也远高于 GPU。 据统计，此类芯片的主要利润主要来自发布的前两个月，两个月后基本就会被淘汰。矿工之间的竞争就像是军备竞赛。 大型矿池 矿工单干时就像是买彩票，出块的概率很低，但是一旦出块了就能大赚一笔。这时就有了矿池的出现。 矿场主号召矿工加入自己的矿池，由矿场主分配不同的任务给每个矿工，矿工出块后就提交给矿池，大家一起分享收益。区块由矿场主组装，矿工只负责计算 Hash，矿场主则会收取一部分管理费。 矿池的出现使得矿工的收益相对来说更加稳定，但同时单次收益也会减少。 如何分配收益 首先肯定不能是平均分配，因为这种方式会导致干多干少收益都是一样的。矿场主可以自己规定一个挖矿难度，这个难度小于比特币网络所规定的挖矿难度。矿工挖出出块后，如果符合矿池规定的难度但不符合比特币网络的难度则这个区块可以作为工作量证明提交给矿池，这种区块称为 Almost valid block，如果符合比特币网络的难度则可以作为整个矿池收益的一部分。最后按照每个矿工提交的两种区块的数量来分配收益。 Q：矿工可否挖到合法区块后绕过矿主自己去提交从而独吞奖励，而 Almost valid block 作为工作量证明提交给矿池？ A：不可以，因为区块中的收款人是矿场主，就算矿工自己发布也赚不到比特币，比特币依旧是矿场主的。 担忧 矿池的出现使得算力的集中更加简单，因为原本大家都是各干各的，而现在矿工加入和更换矿池是非常容易的。此时如果矿场主以较低的管理费吸引大量的矿工以获得半数以上的算力，同时因为每个矿工只负责计算 Hash ，因此其不知道自己是否在发起攻击，所以可以以此发起「51%算力攻击」。 而如果没有矿池，攻击者需要自己购置硬件来对抗全网的算力，成本十分巨大。 参考资料 北京大学肖臻老师《区块链技术与应用》公开课
概述 为何调整挖矿难度 如何调整挖矿难度 为何调整挖矿难度 全网的算力可能会增强，可能会减弱，比特币系统会系统的出块时间控制在10分钟左右（中本聪没有说为什么是10分钟）。 中本聪并没有说明为何是10分钟，可能是他/她喜欢。 如果挖矿难度过低 如果挖矿难度太低，则代表出块时间会非常短，看起来是好事，因为我发布一笔交易会很快地被写入到区块中。但是也会出现很大的问题。 因为出块时间缩短，所以会出现很多节点几乎在同一个时间发布区块。 在出块慢的时候10分钟的时间基本足够将出块信息广播给网络中的绝大多数节点，而在十分钟内基本不会出现大量的区块被广播。而如果出块速度加快，则在某个区块的广播过程中别的节点也很容易算出自己的区块并广播，这就会导致网络中的节点收到的区块不是基本一致的，而是多种多样的。 所以很容易造成区块链的分叉，而且可能不是两叉，数量可能会很多。 这样全网的算力就被分散到十分多的分叉上了。 此时如果攻击者发起一些攻击如 Double Spending Attack，可能就不需要全网51%的算力了，因为全网的算力被分散了，而不是集中在一条最长链上。 如果挖矿难度过高 这个就很容易理解了，出块速度过慢则会导致整个比特币系统十分低效，也会打击矿工的积极性。比如我发布一笔交易可能要等一个月甚至更久才会被确认，这太慢了。 如何调整挖矿难度 比特币系统会在每产生2016个区块后调整难度，下面是调整公式。 中本聪也没有说明为何是每2016个区块才调整难度，可能是他/她喜欢。 难度调整的公式如下 \[ target=target \times \frac{ActualTime}{ExpectTime} \] 调整难度实际上就是调整target值，target 越小则难度越大，反之则难度越小。 同时为了保证挖矿难度不会出现大幅度的波动，一旦$\frac{ActualTime}{ExpectTime} < \frac{1}{4}$，则一律按照等于$\frac{1}{4}$计算，一旦$\frac{ActualTime}{ExpectTime} > 4$，则一律按照等于$4$计算，保证挖矿难度不会有大幅度变动。 参考资料 北京大学肖臻老师《区块链技术与应用》公开课
概述 P2P Overlay network 交易的广播 区块的广播 P2P Overlay network 比特币网络的设计目标是简单、鲁棒，而非高效。 比特币的网络层运行的是一个 P2P Overlay network，网络中所有的节点都是对等的，使用TCP协议通信。 如何发现其它节点 既然每个网络节点都是平等的（是指在网络层面上节点是平等的，但各节点在功能上可以有不同的分工， 如钱包节点、挖矿节点等），不存在任何“特殊”中心节点，那么当新的网络节点启动后，它是如何跟其他的节点建立连接，从而加入到比特币网络呢？ 在中心化网络中，新加入的节点只要连接“特殊”的中心节点就可以加入网络。 为了能够加入到比特币网络，比特币客户端会做一下几件事情： 节点会记住它最近成功连接的网络节点，当重新启动后它可以迅速与先前的对等节点网络重新建立连接。 节点会在失去已有连接时尝试发现新节点。 当建立一个或多个连接后，节点将一条包含自身 IP 地址消息发送给其相邻节点。相邻节点再将此消息依次转发给它们各自的相邻节点，从而保证节点信息被多个节点所接收、保证连接更稳定。 新接入的节点可以向它的相邻节点发送获取地址 getaddr 消息，要求它们返回其已知对等节点的 IP 地址列表。节点可以找到需连接到的对等节点。 在节点启动时，可以给节点指定一个正活跃节点 IP, 如果没有，客户端也维持一个列表，列出了那些长期稳定运行的节点。这样的节点也被称为种子节点（其实和 BT 下载的种子文件道理是一样的），就可以通过种子节点来快速发现网络中的其他节点。 相邻节点的选择是无视底层网络拓扑的，一个在A国的节点的相邻节点可能会在B国，并不是空间上的相邻。 交易的广播 当一个节点收到一笔合法交易的时候会记住这笔交易并广播给相邻的节点，通过 Flooding 来将一个交易广播给全网，同时当节点收到重复的交易时也自然不会重复广播。 如果某个交易被写到区块链中后，则之前记住这笔交易的节点就要删除这个交易，因为已经没有必要记录了。 如果节点收到的被写到区块链中的交易和自己记住的交易发生冲突，则删除自己记录的交易。 区块的广播 广播的过程和交易类似，区别是收到区块的节点还要判断这个区块是不是在最长合法链内。 参考资料 分析比特币网络：一种去中心化、点对点的网络架构 北京大学肖臻老师《区块链技术与应用》公开课
概述 Unspent Transaction Output（UTXO） 出块概率 交易的确认 Unspent Transaction Output（UTXO） UTXO 是 BTC 交易的基本单位，也就是说其不可分割，每一笔交易都会产生新的UTXO，一个 UTXO 可以包含的BTC可以是一聪的任意整数倍。 聪：BTC 的基本单位。 Full node需要在内存中维护UTXO以保证可以快速检测Double spending attack。 除了coinbase交易以外，任何一笔交易都会有一个输入和若干个输出，$输入的BTC=输出的BTC+手续费$。 张三和李四拥有的UTXO如下： 现在张三要给李四转 4 个 BTC，首先张三拥有的 UTXO 中没有正好包含 4 个 BTC 的 UTXO，但是张三可以选择将包含 5 个 BTC 的 UTXO，转账给李四 4 个 BTC，然后转账给自己 1 个 BTC，就完成了一笔交易。本地交易的输入为张三持有的包含 5 个 BTC 的 UTXO，输入给下图中绿色的 UTXO。 我们称这种模式为 Transactor-base ledger。 出块概率 挖矿的过程就是不断尝试各种 nonce 来求解 puzzle，每次尝试 nonce 都可以看作一个 Bernoulli trial：a random experiment with binary outcome（伯努利试验：一个二元结果的随机试验）。伯努利实验的经典案例是抛硬币，如果硬币不均匀，每次实验，正面的概率就是 p，反面的概率是 1-p。对于挖矿来说，这两个概率相差甚远，每次尝试一个nonce，成功的概率是微乎其微的，大概率不行的，如果我们做大量的伯努利实验，每个实验都是随机的，那么这些伯努利实验就构成了一个 Bernoulli process：a sequence of independent Bernoulli trial（伯努利过程：一系列独立伯努利试验）。Bernoulli process 的一个特性是无记忆性（memoryless），意思是说做大量实验，前面的实验结果对后面的结果没有影响，挖矿过程就是不断尝试nonce，这种情况下 Bernoulli process可以用 Poisson process 来近似，实验次数很多，每次成功的概率很小就可以用 Poisson process 来近似。我们关心的是出块时间，也就是系统里产生下一个区块的时间，这个在概率上可以推导出来，服从指数分布。 注意，上图的出块时间是整个系统的出块时间，并不是矿工的出块时间，整个系统的平均出块时间是10分钟，平均时间是比特币协议规定的，通过定期调整挖矿难度，使得平均出块时间维持在10分钟左右，具体到每一个矿工，出块时间取决于矿工的算力，算力大，概率就大，出块时间就短。指数分布也是无记忆的，概率密度曲线的特点有：从任何一个地方把它截断，剩下曲线的形状仍然和原来一样，仍然服从指数分布，这就是无记忆的性质，假如过去了10分钟，仍然没有人找到区块，那么接下来还要等多久呢？还是平均下来10分钟，这个和直觉不太一致。这个概率分析告诉我们将来还要挖多少时间和过去挖了多少时间是没有关系的，仍然是服从指数分布，平均还要10分钟，这个性质也叫 Progress free。 Progress free 或者 Memoryless 有什么作用？ 设想一下，如果有某个 puzzle 不满足这个性质会出现什么情况，比如说过去做的工作越多，接下来尝试 nonce 的时候成功的概率越大，相当于抛硬币的时候每次结果不是随机的，过去抛了好多次，都是反面朝上，下次再抛硬币的时候，正面朝上的概率会增加。如果有某一个加密货币设计出这样的 puzzle，会有什么结果？算力强的矿工会有不成比例的优势，因为算力强的矿工过去的工作量更大。什么是不成比例的优势？比如系统中有两个矿工，一个的算力是另一个的10倍，理想状况下，算力强的矿工能挖到矿的概率应该是另一个矿工的10倍，这才算公平，因为算力强的矿工能尝试的nonce是另一个的10倍，这就是我们说的Progress free 或者 Memoryless 所保证的，如果不是这样的话，算力强的矿工获得记账权的概率就会超过10倍，因为它过去尝试了更多的不成功 nonce，那么下次成功的概率就会增大，这就是不成比例的优势。所以 Progress free 或者 Memoryless 保证了挖矿公平性。 交易的确认 当一笔交易刚刚被写到区块中的时候交易并不会完全被确认的，此时是 One confirmation，此时可能会出现分叉攻击，即在前一个区块后面发布一个不相同的区块来造成分叉，如果攻击者的链条称为最长合法链后则这笔交易就不在最长合法链中，交易会被回滚。 解决的方法是交易的确认需要 Six confirmation。此时攻击者发起的分叉攻击就几乎不可能追赶上六个区块了，除非这个攻击者拥有全网半数以上的算力，这种攻击也称「51%算力攻击」。 参考资料 比特币实现 北京大学肖臻老师《区块链技术与应用》公开课
Cryptographic hash function & Signature
概述 Cryptographic hash function Signature Cryptographic hash function 它有三个重要性质，分别为 Collision resistance、Hiding 和 Puzzle friendly。 Collision resistance：不能找到一个快捷的方法使得$Hash(x_0)=Hash(x_1)=y 且 x_0 \neq x_1$。 Hiding：假如$Hash(x)=y$，那么不能找到一个快捷的方法从$y$反推出$x$，前提是输入空间足够大且取值均匀，这时可以通过在$x$中增加一个随机数来解决这个问题。 Puzzle friendly：哈希函数的输出结果是难以预测的，即难以通过哈希函数的输入来预测哈希函数的输出结果。 Signature 它依赖于Asymmetric cryptographic algorithm，生成一对公私钥，私钥加密公钥解密，私钥签名公钥验证，所以公钥是可以公开的。 参考资料 北京大学肖臻老师《区块链技术与应用》公开课
Block 的结构 & 节点分类 & 比特币的来源 & 分布式共识
概述 Block 的结构 节点类型 谁可以发布比特币 如何验证交易的有效性（分布式共识） Block 的结构 Header version hash of previours block header merkle root hash target nonce 挖矿的过程就是寻找一个 nonce 使得$Hash(Header) \leq target$。 Body transcation list 节点类型 Full node 一直在线 在本地硬盘上维护完整的区块链信息 在内存中维护 UTXO 集合以便快速检测交易的合法性 监听比特币网络上的交易信息并验证每笔交易的合法性 决定哪些交易会被打包到区块里 监听别的矿工挖出来的区块并验证其合法性 挖矿 决定沿着哪条链挖下去 当出现等长的分叉时选择哪一个分叉（缺省状态下选择先监听到的分叉） Light node 无需一直在线 不保存整个区块链而只保存每个区块的 Header 不保存全部的交易而只保存与自己相关的交易 无法检验大多数交易的合法性而只能检验和自己相关的交易的合法性 无法检测系统中发布的区块的正确性 可以验证挖矿难度 只能检测哪个是最长链而不知道哪个是最长合法链 谁可以发布比特币 比特币系统中唯一可以发布比特币的途径就是区块被接受后获得的奖励。奖励初始为 \\(50BTC\\)，每接受21万个区块后奖励减半。 当奖励已经很少的时候获取BTC主要是靠交易的手续费。 如何验证交易的有效性（分布式共识） 获取记账权 挖矿的过程就是寻找一个 nonce 使得$Hash(Header) \leq target$，优先完成这个工作的节点就获得了这个区块的记账权。节点无法欺骗其它节点说自己完成了工作，因为其它节点可以轻松地验证上述条件是否成立。 接受区块> 验证区块的信息，如难度是否正确，交易信息是否没有篡改等。 通过查看 Header 确定当前区块是否在扩展最长链，如果是扩展最长链则接受该区块，反之则不接受。 下列情况下可能会导致几乎同时出现两个最长的链 两个人几乎同时取得了记账权。 由于网络等原因一部分节点接受了一个区块，另一部分节点接受了另一个区块，以及类似的情况。 其它情况。 出现这种情况的时候接受哪条链实际上是这些链各个对应的矿工的算力比拼，如果其中一条链的矿工的算力可以让该链最快地扩展，则最终该链会被其它节点接受成为最长链。 激励机制 那么凭什么让矿工来拿出算力来维持这个共识？ 一旦一个区块被接受，则拥有者可以获得奖励。奖励初始为50 BTC ，每接受 21 万个区块后奖励减半。这在奖励比较多的时候可以大大激发矿工的挖矿热情。 一个区块是可以记账的，每当一笔交易被记录到一个区块内的时候，区块的拥有者就可以收取一定手续费。 参考资料 北京大学肖臻老师《区块链技术与应用》公开课
概述 你可能在电视剧中看到坏人威胁主角的时候会拿出作为人质的亲人写的信，而主角一看笔迹就知道是亲人所写的了。 在这里笔迹起到了两个 ...
概述 你可能在电视剧中看到坏人威胁主角的时候会拿出作为人质的亲人写的信，而主角一看笔迹就知道是亲人所写的了。 在这里笔迹起到了两个作用： 证明信的内容确实是亲人所写 证明内容没有被篡改过 在计算机的世界中，我们同样需要对信息一样的验证。 例子 下面是一段在线聊天对话： 小明：小红，借我 30w 呗，我去买点水喝。 小红：可以，不过你要给我打一个欠条，拍照发过来。 小明：没问题，发给你了。 小红：好的，明天我把带给你。 此时，这笔借款交易的安全性如何？ 小红可能会 PS 照片修改借款数字。 小明事后可能会抵赖说照片是修改过的。 照片可能在传输过程中阴差阳错地出现错误导致欠条内容改变。 其它。 这时需要解决的问题就是： 如何证明这个照片是小明发给小红的。 如何证明这个照片没有被修改过。 此时我们就需要对照片进行数字签名。 前置知识 哈希函数 哈希函数是可以将一段任意长度的信息转化为固定长度信息，这个固定长度的信息称为哈希值。 哈希函数有两个特点： 任何长度的原始信息经过哈希函数后都会变成等长度的信息。 一旦输入给给哈希函数的信息发生了改变（哪怕是一点点的改变），最终的哈希值就会面目全非。 下面我们用一种叫做`SHA256`的哈希函数举个例子： 原始信息：100000000000000 哈希值：7186c3316577cf7ee0568c6ff7f3227b5b3e4b498e7eabba4f6e3d908e2162f4 原始信息：100000000000001 哈希值：11d77ab50791e10caffb01cb6ff851f4929edfb0ad79808111d78182b0048540 结果很明显，仅仅做一点点改动哈希值都会发生巨大的变化。 非对称加密 非对称加密有两个密钥，一个称为私钥，一个称为公钥。 经过公钥加密的信息只能被相匹配的私钥解密，也就是说同一个公钥不能解密自己加密的信息。私钥加密的信息只有公钥可以解密，也就是说私钥不能解密自己的信息。同时也可以用公钥验证加密的信息是否是相匹配的私钥加密的。 数字签名 签名过程 小明自己生成一对公私钥。 小明计算出欠条照片的哈希值。 小明用私钥对哈希值进行加密形成签名。 签名的过程就完成了。此时小明需要将照片，公钥以及签名发给小红。 验证过程 小红用公钥解密签名得到照片的哈希值。 小红用公钥验证得到解密出来的哈希值确实是小明的私钥加密的。 小红计算出欠条照片的哈希值。 将两个哈希值进行对比，如果一样则验证成功，反之则失败。 可见验证成功需要同时满足两个条件： 解密出的哈希值与收到的照片的哈希值一致。 用公钥证明了哈希值确实是小明的私钥加密的。 第一个条件保证了照片没有被修改过。第二个条件则保证了借款双方都无法抵赖。 你可能会想，如果小明表示小红的公钥不是自己的，或者小红表示自己的公钥不是小明的岂不是还能抵赖？ 证书机构 此时我们需要第三方来做公正。小明需要将自己的公钥交给证书机构，证书机构审核后会颁发证书，证明这个公钥是小明的。而且这个证书是公开的，也就是说小红也可以拿到。 此时假如抵赖小明说你的公钥是假的，那么小红就可以指着证书对他：说睁大你的X眼看看，这是你颁发给你的证书，上面已经说了这个公钥是你的。当小红抵赖的时候也可以这么干。 总结 数字签名可以切实有效地地保证双方的信息是没有被修改过的，并且可以明确消息来源。
概述 官方网站 开源主页 官方文档 mpv是基于MPlayer和mplayer2的媒体播放器。 支持各种视频文件格式、音频和视频编 ...
概述 官方网站 开源主页 官方文档 mpv是基于MPlayer和mplayer2的媒体播放器。 支持各种视频文件格式、音频和视频编解码器以及字幕。 媒体来源也不仅限于本地，如URL等。 根据平台的不同，支持各种不同的视频和音频输出方法。 不过重点是提供了它提供了 C 语言的库——libmpv，可以让我们将 mpv 嵌入到自己的程序中。
概述 介绍下列内容： 什么是 CSRF 攻击 漏洞举例 常见的防御方式 什么是CSRF攻击 狭义的CSRF攻击 狭义的CSRF攻击就是指让Client在被Server认证的情况下去访问精心设计的恶意链接完成攻击。 因为 HTTP 是无状态的协议，即 Server 不知道每个请求来源于哪个用户。 所以服务器有时会在Client存储可以标识用户身份的信息。 在Client发送请求的时候附上用户身份信息保证Server可以识别出用户。 如果此时用户点击了一个恶意链接，且恶意链接会发送一个恶意请求到 Server。 一般情况下会因为不能识别用户身份而而被拒绝。 但是如果此时 Client 是刚刚被服务端认证过的，则发送恶意请求的同时也会附上刚刚认证的身份信息。 Server 收到恶意请求后发现Client是认证过的，于是将其识别为合法请求。 攻击者就完成了一次 CSRF 攻击。 广义的CSRF攻击 通过一些手段猜测出请求参数以及用户认证信息（如果需要认证的话），这样Server就会将其识别为合理请求。 例如某网站转账的URL为https://www.bank.com/transfer?user=小明&money=1000&for=小红，代表小明转账给小红 1000 元。那么攻击者就可以构造出这个URL—https://www.bank.com/transfer?user=小明&money=9999999999&for=小李直接掏空小明的钱。 漏洞举例 挖洞经验 | Facebook的Gmail验证机制存在的CSRF漏洞 | CSRF Token缺少验证措施 Facebook上一个有趣的 CSRF | 授权过程中缺少 state 参数 挖洞经验 | 利用 Instagram 版权功能构造 CSRF 漏洞删除其他用户文件 | 重要的参数直接写在 GET 请求里 常见的防御方式 验证码 在执行一些重要请求之前通过验证码的方式验证用户身份，例如短信验证码，图片验证码等。所以你会发现许多网站即使在登录状态先执行一些操作也需要验证。 同源检测 根据 HTTP 协议，在 HTTP 头中有一个字段叫 Referer，记录了该HTTP请求的来源地址。 对于 Ajax 请求，图片和 script 等资源请求，Referer 为发起请求的页面地址。对于页面跳转，Referer 为打开页面历史记录的前一个页面地址。因此我们使用 Referer 中链接的 Origin 部分可以得知请求的来源域名。 这种方法并非万无一失，Referer 的值是由浏览器提供的，虽然 HTTP 协议上有明确的要求，但是每个浏览器对于 Referer 的具体实现可能有差别，并不能保证浏览器自身没有安全漏洞。使用验证 Referer 值的方法，就是把安全性都依赖于第三方（即浏览器）来保障，从理论上来讲，这样并不是很安全。在部分情况下，攻击者可以隐藏，甚至修改自己请求的 Referer。 同源验证是一个相对简单的防范方法，能够防范绝大多数的 CSRF 攻击。但这并不是万无一失的，对于安全性要求较高，或者有较多用户输入内容的网站，我们就要对关键的接口做额外的防护措施。 CSRF Token CSRF 攻击之所以能够成功，是因为服务器误把攻击者发送的请求当成了用户自己的请求。那么我们可以要求所有的用户请求都携带一个CSRF攻击者无法获取到的 Token。服务器通过校验请求是否携带正确的 Token，来把正常的请求和攻击的请求区分开，也可以防范 CSRF 的攻击。 通过一套合适的规则（一般来说这套规则要便于分布式校验）来生成随机的 Token 并跟随 form 表单一起提交，由于 HTTPS 在传输过程中是加密的，所以攻击者无法截获 Token，前提是站点没有 XSS 漏洞。 但是此方法的实现比较复杂，需要给每一个页面都写入 Token（前端无法使用纯静态页面），每一个 Form 及 Ajax 请求都携带这个 Token，后端对每一个接口都进行校验，并保证页面 Token 及请求 Token 一致。这就使得这个防护策略不能在通用的拦截上统一拦截处理，而需要每一个页面和接口都添加对应的输出和校验。这种方法工作量巨大，且有可能遗漏。 双重Cookie验证 在会话中存储CSRF Token比较繁琐，而且不能在通用的拦截上统一处理所有的接口。 那么另一种防御措施是使用双重提交Cookie。利用CSRF攻击不能获取到用户Cookie的特点，我们可以要求Ajax和表单请求携带一个Cookie中的值。 双重Cookie采用以下流程： 在用户访问网站页面时，向请求域名注入一个Cookie，内容为随机字符串（例如csrfcookie=v8g9e4ksfhw）。 在前端向后端发起请求时，取出Cookie，并添加到URL的参数中（接上例POST https://www.a.com/comment?csrfcookie=v8g9e4ksfhw）。 后端接口验证 Cookie 中的字段与 URL 参数中的字段是否一致，不一致则拒绝。 攻击者可以获取到URL参数中的Token值，但是无法获取到Cookie中的Token值，因此攻击者无法构造出恶意链接。 你可能会想既然我已经截取到了URL参数中的Token值，那么攻击者也就可以猜测出Cookie中的Token值。对，没错，因为上面的流程中URL参数和Cookie中存储的是相同的值，那么如果我对Cookie中的值进行加密呢？那么攻击者就无法推测出Cookie的内容了。 此方法相对于CSRF Token就简单了许多。可以直接通过前后端拦截的的方法自动化实现。后端校验也更加方便，只需进行请求中字段的对比，而不需要再进行查询和存储Token。 当然，此方法并没有大规模应用，其在大型网站上的安全性还是没有CSRF Token高，因为任何跨域都会导致前端无法获取Cookie中的字段（包括子域名之间）。 参考资料 前端安全系列（二）：如何防止CSRF攻击？
用了这么久的Stream类，还没了解过什么是stream以及为何要有stream。
写在前面 用了这么久的 Stream 类，还没了解过什么是 stream 以及为何要有 stream。 什么是 Stream？ stream 就是一些等待解释其含义的数据，这些数据通常具有至少一个下面列出的特点： 不要求在处理时全部到达。 数据的到达时机难以确定。 数据是按照一定顺序到达。 数据顺序读写比随机读写效率要高或只能顺序读写。 读取过一次的数据无法再次读取或再次读取时开销很大。 符合上列特点的数据典型的就是在线视频。 stream本身没有含义，它的含义取决于你如何解释。这也就是为什么面向对象语言中会有 FileStream，NetworkStream 等多种 Stream 类的派生类，这些类包含了对stream的解释方式。 如果以字节为最小处理单位则称字节流，以一位二进制为最小处理单位则称比特流，以此类推。 目前来说stream已经不单单代表数据了，通常也代表与数据配套的操作。 为什么要有 Stream 使得编程更加容易。如果统一使用 stream 模型处理数据，那么对应不同类型的数据只需要用到不同的 stream 处理模块，代码改动较少。 对于在线视频这种业务场景，使用 stream 来处理再好不过了。 视频通过网络传输，即到达时间不确定。 视频只需要加载一部分就能开始播放以提高用户体验，即不要求在处理时全部到达。 视频必须要保证顺序正确，即数据是按照一定顺序到达。 对于较大的数据无需一次性装入内存而是分批次处理。虽然 OS 提供的分页机制一定程度上缓解了这个问题，但是频繁的缺页会大大降低运行效率。
介绍如何建立STM32F10X的MDK工程模板（基于标准固件库）。
概述 介绍如何建立STM32F10X的MDK工程模板（基于标准固件库）。 正文 新建一个文件夹命名为 Template。 在文件夹下新建三个文件夹，分别为，core，stdlib，user。 前往官网下载标准固件库。 拷贝 Libraries\CMSIS\CM3\CoreSupport 下的所有文件到 Template\core。 拷贝 Libraries\CMSIS\CM3\DeviceSupport\ST\STM32F10x\startup\arm 下对应的启动文件到 Template\core 下。 拷贝 Libraries\CMSIS\CM3\DeviceSupport\ST\STM32F10x 下的 stm32f10x.h，system_stm32f10x.c，system_stm32f10x.h到 Template\core 下。 拷贝 Libraries\STM32F10x_StdPeriph_Driver 下的 inc 和 src 两个文件夹到 Template\stdlib 下。 拷贝 Project\STM32F10x_StdPeriph_Template 下的 main.c ，stm32f10x_conf.h，stm32f10x_it.c，stm32f10x_it.h，system_stm32f10x.c 到 Template\user 下。 用 MDK 新建工程到 Template\user 目录下。 添加宏 USE_STDPERIPH_DRIVER。 根据STM32 - Keil中启动文件的选择和固件库宏定义选择对应的宏如STM32F103R6就填入`STM32F10X_HD`（如果后续编译出现问题可以移除该宏重试）。 多个宏之间用半角逗号分隔。 添加下列头文件路径 添加 Template\core。 添加 Template\stdlib\inc。 添加 Template\user。 添加下列分组并向分组中添加对应的文件 core 分组中添加 Template\core 下所有的 *.c，*.s 文件。 stdlib 分组添加 Template\stdlib\inc 下所有的 *.c 文件。 user 分组添加 Template\user 下所有的 *.c 文件。 清空 main。c 中的内容，写一个空的 int main(void) 函数进去。 编译工程，见招拆招。
概述 Blockchain Merkle Tree Blockchain 本质上是链表，只不过传统的链表的指针域存储的下一个节点的地址，Blockchain 的指针域则存储上一个节点的 Hash。 结构如上图，如果从左到右第二个区块被篡改。 因此后续的节点都使用的是哈希指针，所以必然会导致右边的两个区块的哈希指针发生改变。 所以系统只需要保存最后一个区块的 Hash 就可以检验整个区块链是否被篡改。 有了这些性质后节点就没有必要保存所有的区块，当需要其它区块的时候，可以向其它节点请求，得到之后通过验证 Hash 就可以知道这个区块是否被篡改过。 Merkle Tree Blockchain的header中只需要保存root hash即可，这样对Blockchain取哈希的时候也只需要对header取哈希就行了，也能够起到防篡改作用。 那么假如我想知道某个block是否已经写在了Merkle Tree里，则可以向其它节点请求下图中紫色的 Hash，然后通过计算 Hash 就可以验证。 理论上恶意节点可以通过篡改某个区块的内容且使其 Hash 不变的情况下来欺骗其它节点，但目前的实际应用中是不可能的，因为这是在人为制造哈希碰撞，Collision resistance 可以很好地解决这个问题。 假如我想知道某个 block 是否不存在于 Merkle Tree 里，如果对叶结点的顺序没有特殊要求则需要遍历所有 block，但是如果 block经过排序，则可以在对数时间内进行验证。 参考资料 北京大学肖臻老师《区块链技术与应用》公开课
32 位的操作系统也能使用超过4G的内存-PAE 技术简介
概述 阅读本文章前请确认对X86下的分页机制有最基本的了解，否则将难以理解本文中提到的内容。本文介绍的内容均位X86架构下的PAE ...
概述 阅读本文章前请确认对X86下的分页机制有最基本的了解，否则将难以理解本文中提到的内容。 本文介绍的内容均位X86架构下的PAE技术，而不是ARM架构下的LPAE技术，虽然两者从本质来说是一样的。 介绍下列内容： PAE 技术因何问世 PAE 技术可以干什么 PAE 技术实现原理 为什么我们所用的 32 位操作系统只能使用 4GB 的内存 PAE 技术因何问世 PAE（Physical Address Extension，中文译名：物理地址扩展）技术最初是为了弥补32位地址在服务上不够用的情况。因为32位的地址最多只能支持4GB的内存，而这对于很多服务器来说是远远不够的，而那时64位的CPU和操作系统还没有普及，这个技术临时缓解了一下这类问题。 PAE 技术可以干什么 PAE技术可以使得多个进程使用的总内存大于 4GB，但是单个进程的所使用的内存上限依然只有 4GB。 PAE 技术实现原理 地址线由 32 根增加到 36 根，寻址空间自然也就从 4GB 增加到了 64GB。 改动了分页机构使其可以适应36根地址线的寻址空间。 本文主要介绍分页机构的改动。 未启动 PAE 下的分页机制 4K 分页 未启动 PAE 下的 4K 分页 从 CR3 寄存器中取出页目录的基址。 取出线性地址中的 22 位~31 位共 10 位作为页目录项在页目录中的索引，由此找到页目录项。 根据页目录项中的内中找到对应的页表的基址。 取出线性地址中的 12 位~21 位共 10 位作为页表项在页表中的索引，由此找到页表项。 取出线性地址中的 0 位~11 位共 12 位作为页内偏移，从页表项中取出页基址，两者相加则到物理地址。 4M分页 未启动 PAE下的 4M 分页 从 CR3 寄存器中取出页表的基址。 取出线性地址中的 22 位~31 位共 10 位作为页表项在页表中的索引，由此找到页表项。 取出线性地址中的 0 位~21 位共 22 位作为页内偏移，从页表项中取出页基址，两者相加则到物理地址。 启动 PAE 下的分页机制 <>4K 分页 启动 PAE下的 4K分页 从 CR3 寄存器中取出页目录指针表的基址。 取出线性地址的 30 位~31 位共 2 位作为页目录指针在页目录指针表中的索引，由此找到页目录指针。 根据页目录指针获得对应的页目录的基址 取出线性地址中的 21 位~29 位共 9 位作为页目录项在页目录中的索引，由此找到页目录项。 根据页目录项找到对应页表的基址。 取出线性地址中的 12 位~20 位共 9 位作为页表项在页表中索引，由此找到页表项。 取出线性地址中的 0 位~11 位共 12 位作为页内偏移，从页表项中取出页基址，两者相加则到物理地址。 4M 分页 启动 PAE 下的 4M 分页 从 CR3 寄存器中取出页表指针表的基址。 取出线性地址的 30 位~ 31 位共 2 位作为页表指针在页表指针表中的索引，由此找到页表指针。 根据页表指针找到对应的页表的基址。 取出线性地址中的 21 位~29 位共 9 位作为页表项在页表中的索引，由此找到页表项。 取出线性地址中的 0 位~21 位共 22 位作为页内偏移，从页表项中取出页基址，两者相加则到物理地址。
本文简要介绍HTTPS协议是如何保证数据在传输过程中的安全的。
概述 本文简要介绍HTTPS协议是如何保证数据在传输过程中的安全的。 由于是以介绍为主，所以不会涉及细节，而且肯定会有一些错误。如果想要详细了解可自行查阅资料。 正文 在HTTPS协议出现前大家用的都是HTTP协议，这种协议传输的都是明文信息，任何人都可以窃取并篡改，毫无安全性可言。 HTTP协议的简要通信流程如下图： HTTP 协议简要通信流程 由于传输的信息都是明文的，所以攻击者就可以窃取并篡改信息，如下图： 中间人攻击 你可能会想，我们把信息加密不就可以了么，比如像下图一样 使用对称密钥加密信息 对称加密：双方持有同一个对称密钥，任何一方通过对称密钥加密的信息，对方可以通过相同的对称密钥解密出原文。 但是攻击者依然可以通过各种方式获取到对称密钥： 如果密钥是通过服务器下发给客户端，则直接截取服务器的消息就行了。 如果对称密钥是提前保存在双方的电脑上，那就可以通过某种手段入侵客户端或者服务器来窃取对称密钥。 其它方式。 截取到密钥后攻击者仍然可以进行攻击 中间人攻击 那么我们就需要对“对称密钥”进行保护了，如下图 保护对称密钥 非对称加密：一方持有私钥，一方持有公钥，使用公钥加密的信息只能被私钥解密。私钥不会用于加密，不过通常会用于加一段签名代表信息是完整，即信息是否丢失了一部分或者因为某种原因而发生的改变。 这次攻击者看似无法通过截取信息来获得对称密钥了，但是攻击者依然有机会发起攻击。 不过先说明一点，目前公钥都是通过服务器下发的证书获得的，所以更接近下图相对来说更接近真实的通信流程 服务器下发证书 那么攻击者会怎样攻击呢？如下图（图片如果看不清可以右键图片选择“在新的标签页中打开图像”或类似选项） 中间人攻击 这时显然不能对公钥再次加密了，因为无法判断客户端得到的证书是真的，还是攻击者伪造的。 这时就需要第三方介入了 合法服务器的证书都是经过审核机构审核的，服务器在下发证书的时候就会下发经过审核的证书 客户端会存有经过审核的证书的列表。 客户端收到服务器下发的证书后会验证收到的证书是否经过了审核。 如果经过了审核则继续通信，反之断开连接或者弹出警告。 现在的通信流程图下图，也是HTTPS的大致通信流程 更接近于 HTTPS 的通信流程 不过HTTPS的一大软肋就是第三方证书审核机构，如果审核不严将攻击者的证书列入了合法证书列表，那么HTTPS所作的努力将全部白费。 总的来说，HTTPS增加了攻击者的攻击成本，使得数据的传输过程变得更加完全。