蚊章汇 - Marty Kalin
type: channel
link: https://linuxcn.bwsl.wang/author/1150/
description: Marty Kalin
items: 从 Rust 调用 C 库函数
items: 实例讲解代码之内存安全与效率
items: 用 C 语言理解 Linux 软件库
items: 一个用 Java 实现的超轻量级 RESTful Web 服务示例
items: 如何使用 OpenSSL：哈希值、数字签名等
items: Java 中的数据流和函数式编程
items: OpenSSL 入门：密码学基础知识
items: 如何使用 Protobuf 做数据交换
items: Linux 下的进程间通信：套接字和信号
items: Linux 下的进程间通信：使用管道和消息队列

从 Rust 调用 C 库函数
type: item
date: 2022-12-16T19:01:55+08:00
description: Rust FFI 和 bindgen 工具是为 Rust 调用 C 库而设计的。Rust 很容易与 C 语言对话，从而与任何其它可以与 C 语言对话的语言对话。为什么要从 Rust 调用 C 函数？简短的答案就是软件库。冗长的答案则触及到 C 在众多编程语言中的地位，特别是相对 Rust 而言。C、C++，还有 Rust 都是系统语言，这意味着程序员可以访问机器层面的数据类型与操作。在这三个系统语言中，C 依然占据主导地位。现代操作系统的内核主要是用 C 来写的，其余部分依靠汇编语言补充。在标准系统函数库中，输入与输出、数字处理、加密计算、安全、网络、国际化、字符串处理、内存管理等等，大多都是用 C 来写的。这些函数库所代表的是一个庞大的基础设施，支撑着用其他语言写出来的应用。Rust 发展至今也有着可观的函数库，但是 C 的函数库 —— 自 1970 年代就已存在，迄今还在蓬勃发展 —— 是一种无法被忽视的资源。最后一点是，C 依然还是编程语言中的 通用语：大部分语言都可以与 C 交流，透过 C，语言之间可以互相交流。两个概念证明的例子Rust 支持 FFI（ 外部函数接口 Foreign Function Interface ）用以调用 C 函数。任何 FFI 所需要面临的问题是调用方语言是否涵盖了被调用语言的数据类型。例如，ctypes 是 Python 调用 C 的 FFI，但是 Python 并没有包括 C 所支持的无符号整数类型。结果就是，ctypes 必须寻求解决方案。相比之下，Rust 包含了所有 C 中的原始（即，机器层面）类型。比如说，Rust 中的 i32 类对应 C 中的 int 类。C 特别声明了 char 类必须是一个字节大小，而其他类型，比如 int，必须至少是这个大小（LCTT 译注：原文处有评论指出 int 大小依照 C 标准应至少为 2 字节）；然而如今所有合理的 C 编译器都支持四字节的 int，以及八字节的 double（Rust 中则是 f64 类），以此类推。针对 C 的 FFI 所面临的另一个挑战是：FFI 是否能够处理 C 的裸指针，包括指向被看作是字符串的数组指针。C 没有字符串类型，它通过结合字符组和一个非打印终止符（大名鼎鼎的 空终止符）来实现字符串。相比之下，Rust 有两个字符串类型：String 和 &str （字符串切片）。问题是，Rust FFI 是否能将 C 字符串转化成 Rust 字符串——答案是 肯定的。出于对效率的追求，结构体指针在 C 中也很常见。一个 C 结构体在作为一个函数的参数或者返回值的时候，其默认行为是传递值（即，逐字节复制）。C 结构体，如同它在 Rust 中的对应部分一样，可以包含数组和嵌套其他结构体，所以其大小是不定的。结构体在两种语言中的最佳用法是传递或返回引用，也就是说，传递或返回结构体的地址而不是结构体本身的副本。Rust FFI 再一次成功处理了 C 的结构体指针，其在 C 函数库中十分普遍。第一段代码案例专注于调用相对简单的 C 库函数，比如 abs（绝对值）和 sqrt（平方根）。这些函数使用非指针标量参数并返回一个非指针标量值。第二段代码案例则涉及了字符串和结构体指针，在这里会介绍工具 bindgen，其通过 C 接口（头文件）生成 Rust 代码，比如 math.h 以及 time.h。C 头文件声明了 C 函数的调用语法，并定义了会被调用的结构体。两段代码都能在 我的主页上 找到。调用相对简单的 C 函数第一段代码案例有四处 Rust 对标准数学库内的 C 函数的调用：两处分别调用了 abs（绝对值）和 pow（幂），两处重复调用了 sqrt（平方根）。这个程序可以直接用 rustc 编译器进行构建，或者使用更方便的命令 cargo build：use std::os::raw::c_int; // 32位 use std::os::raw::c_double; // 64位 // 从标准库 libc 中引入三个函数。 // 此处是 Rust 对三个 C 函数的声明： extern "C" { fn abs(num: c_int) -> c_int; fn sqrt(num: c_double) -> c_double; fn pow(num: c_double, power: c_double) -> c_double; } fn main() { let x: i32 = -123; println!("\n{x}的绝对值是: {}.", unsafe { abs(x) }); let n: f64 = 9.0; let p: f64 = 3.0; println!("\n{n}的{p}次方是: {}.", unsa…

实例讲解代码之内存安全与效率
type: item
date: 2021-10-03T18:12:45+08:00
description: 了解有关内存安全和效率的更多信息。C 是一种高级语言，同时具有“ 接近金属 close-to-the-metal ”（LCTT 译注：即“接近人类思维方式”的反义词）的特性，这使得它有时看起来更像是一种可移植的汇编语言，而不像 Java 或 Python 这样的兄弟语言。内存管理作为上述特性之一，涵盖了正在执行的程序对内存的安全和高效使用。本文通过 C 语言代码示例，以及现代 C 语言编译器生成的汇编语言代码段，详细介绍了内存安全性和效率。尽管代码示例是用 C 语言编写的，但安全高效的内存管理指南对于 C++ 是同样适用的。这两种语言在很多细节上有所不同（例如，C++ 具有 C 所缺乏的面向对象特性和泛型），但在内存管理方面面临的挑战是一样的。执行中程序的内存概述对于正在执行的程序（又名 进程 process ），内存被划分为三个区域： 栈 stack 、 堆 heap 和 静态区 static area 。下文会给出每个区域的概述，以及完整的代码示例。作为通用 CPU 寄存器的替补，栈 为代码块（例如函数或循环体）中的局部变量提供暂存器存储。传递给函数的参数在此上下文中也视作局部变量。看一下下面这个简短的示例：void some_func(int a, int b) { int n; ... } 通过 a 和 b 传递的参数以及局部变量 n 的存储会在栈中，除非编译器可以找到通用寄存器。编译器倾向于优先将通用寄存器用作暂存器，因为 CPU 对这些寄存器的访问速度很快（一个时钟周期）。然而，这些寄存器在台式机、笔记本电脑和手持机器的标准架构上很少（大约 16 个）。在只有汇编语言程序员才能看到的实施层面，栈被组织为具有 push（插入）和 pop（删除）操作的 LIFO（后进先出）列表。 top 指针可以作为偏移的基地址；这样，除了 top 之外的栈位置也变得可访问了。例如，表达式 top+16 指向堆栈的 top 指针上方 16 个字节的位置，表达式 top-16 指向 top 指针下方 16 个字节的位置。因此，可以通过 top 指针访问实现了暂存器存储的栈的位置。在标准的 ARM 或 Intel 架构中，栈从高内存地址增长到低内存地址；因此，减小某进程的 top 就是增大其栈规模。使用栈结构就意味着轻松高效地使用内存。编译器（而非程序员）会编写管理栈的代码，管理过程通过分配和释放所需的暂存器存储来实现；程序员声明函数参数和局部变量，将实现过程交给编译器。此外，完全相同的栈存储可以在连续的函数调用和代码块（如循环）中重复使用。精心设计的模块化代码会将栈存储作为暂存器的首选内存选项，同时优化编译器要尽可能使用通用寄存器而不是栈。堆 提供的存储是通过程序员代码显式分配的，堆分配的语法因语言而异。在 C 中，成功调用库函数 malloc（或其变体 calloc 等）会分配指定数量的字节（在 C++ 和 Java 等语言中，new 运算符具有相同的用途）。编程语言在如何释放堆分配的存储方面有着巨大的差异：在 Java、Go、Lisp 和 Python 等语言中，程序员不会显式释放动态分配的堆存储。例如，下面这个 Java 语句为一个字符串分配了堆存储，并将这个堆存储的地址存储在变量 greeting 中：String greeting = new String("Hello, world!"); Java 有一个垃圾回收器，它是一个运行时实用程序，如果进程无法再访问自己分配的堆存储，回收器可以使其自动释放。因此，Java 堆释放是通过垃圾收集器自动进行的。在上面的示例中，垃圾收集器将在变量 greeting 超出作用域后，释放字符串的堆存储。Rust 编译器会编写堆释放代码。这是 Rust 在不依赖垃圾回收器的情况下，使堆释放实现自动化的开创性努力，但这也会带来运行时复杂性和开销。向 Rust 的努力致敬！在 C（和 C++）中，堆释放是程序员的任务。程序员调用 malloc 分配堆存储，然后负责相应地调用库函数 free 来释放该存储空间（在 C++ 中，new 运算符分配堆存储，而 delete 和 delete[] 运算符释放此类存储）。下面是一个 C 语言代码示例：char* greeting = malloc(14); /* 14 heap bytes */ strcpy(greeting, "Hello, world!"); /* copy greeting into bytes */ puts(greeting); /* print greeting */ free(greeting); /* free malloced bytes */ C 语言避免了垃圾回收器的成本和复杂性，但也不过是让程序员承担了堆释放的任务。…

用 C 语言理解 Linux 软件库
type: item
date: 2021-05-23T00:53:46+08:00
description: 软件库是重复使用代码的一种简单而合理的方式。软件库是一种是一直以来长期存在的、简单合理的复用代码的方式。这篇文章解释了如何从头开始构建库并使得其可用。尽管这两个示例库都以 Linux 为例，但创建、发布和使用这些库的步骤也可以应用于其它类 Unix 系统。这些示例库使用 C 语言编写，非常适合该任务。Linux 内核大部分由 C 语言和少量汇编语言编写（Windows 和 Linux 的表亲如 macOS 也是如此）。用于输入/输出、网络、字符串处理、数学、安全、数据编码等的标准系统库等主要由 C 语言编写。所以使用 C 语言编写库就是使用 Linux 的原生语言来编写。除此之外，C 语言的性能也在一众高级语言中鹤立鸡群。还有两个来访问这些库的示例 客户程序 client （一个使用 C，另一个使用 Python）。毫无疑问可以使用 C 语言客户程序来访问 C 语言编写的库，但是 Python 客户程序示例说明了一个由 C 语言编写的库也可以服务于其他编程语言。静态库和动态库对比Linux 系统存在两种类型库：静态库（也被称为归档库）：在编译过程中的链接阶段，静态库会被编译进程序（例如 C 或 Rust）中。每个客户程序都有属于自己的一份库的拷贝。静态库有一个显而易见的缺点 —— 当库需要进行一定改动时（例如修复一个 bug），静态库必须重新链接一次。接下来要介绍的动态库避免了这一缺点。动态库（也被称为共享库）：动态库首先会在程序编译中的链接阶段被标记，但是客户程序和库代码在运行之前仍然没有联系，且库代码不会进入到客户程序中。系统的动态加载器会把一个共享库和正在运行的客户程序进行连接，无论该客户程序是由静态编译语言（如 C）编写，还是由动态解释语言（如 Python）编写。因此，动态库不需要麻烦客户程序便可以进行更新。最后，多个客户程序可以共享同一个动态库的单一副本。通常来说，动态库优于静态库，尽管其复杂性较高而性能较低。下面是两种类型的库如何创建和发布：库的源代码会被编译成一个或多个目标模块，目标模块是二进制文件，可以被包含在库中并且链接到可执行的二进制中。目标模块会会被打包成一个文件。对于静态库，标准的文件拓展名是 .a 意为“ 归档 archive ”；对于动态库，标准的文件拓展名是 .so 意为“ 共享目标 shared object ”。对于这两个相同功能的示例库，分别发布为 libprimes.a （静态库）和 libshprimes.so （动态库）。两种库的文件名都使用前缀 lib 进行标识。库文件被复制到标准目录下，使得客户程序可以轻松地访问到库。无论是静态库还是动态库，典型的位置是 /usr/lib 或者 /usr/local/lib，当然其他位置也是可以的。构建和发布每种库的具体步骤会在下面详细介绍。首先我将介绍两种库里涉及到的 C 函数。示例库函数这两个示例库都是由五个相同的 C 函数构建而成的，其中四个函数可供客户程序使用。第五个函数是其他四个函数的一个工具函数，它显示了 C 语言怎么隐藏信息。每个函数的源代码都很短，可以将这些函数放在单个源文件中，尽管也可以放在多个源文件中（如四个公布的函数都有一个文件）。这些库函数是基本的处理函数，以多种方式来处理质数。所有的函数接收无符号（即非负）整数值作为参数：is_prime 函数测试其单个参数是否为质数。are_coprimes 函数检查了其两个参数的 最大公约数 greatest common divisor （gcd）是否为 1，即是否为互质数。prime_factors：函数列出其参数的质因数。glodbach：函数接收一个大于等于 4 的偶数，列出其可以分解为两个质数的和。它也许存在多个符合条件的数对。该函数是以 18 世纪数学家 克里斯蒂安·哥德巴赫 Christian Goldbach 命名的，他的猜想是任意一个大于 2 的偶数可以分解为两个质数之和，这依旧是数论里最古老的未被解决的问题。工具函数 gcd 留在已部署的库文件中，但是在没有包含这个函数的文件无法访问此函数。因此，一个使用库的客户程序无法调用 gcd 函数。仔细观察 C 函数可以明白这一点。更多关于 C 函数的内容每个在 C 语言中的函数都有一个存储类，它决定了函数的范围。对于函数，有两种选择。函数默认的存储类是 extern，它给了函数一个全局域。一个客户程序可以调用在示例库中用 extern 修饰的任意函数。下面是一个带有显式 extern 声明的 are_coprimes 函数定义：extern unsigned are_coprimes(unsigned n1, unsigned n2) { ... } 存储类 static 将一个函数的的范围限制到函数被定义的文件中。在示…

一个用 Java 实现的超轻量级 RESTful Web 服务示例
type: item
date: 2020-08-27T15:19:00+08:00
description: 通过管理一套图书的完整代码示例，来探索轻量级的 RESTful 服务。Web 服务，以这样或那样的形式，已经存在了近二十年。比如，XML-RPC 服务出现在 90 年代后期，紧接着是用 SOAP 分支编写的服务。在 XML-RPC 和 SOAP 这两个开拓者之后出现后不久，REST 架构风格的服务在大约 20 年前也出现了。REST 风格（以下简称 Restful）服务现在主导了流行的网站，比如 eBay、Facebook 和 Twitter。尽管分布式计算的 Web 服务有很多替代品（如 Web 套接字、微服务和远程过程调用的新框架），但基于 Restful 的 Web 服务依然具有吸引力，原因如下：Restful 服务建立在现有的基础设施和协议上，特别是 Web 服务器和 HTTP/HTTPS 协议。一个拥有基于 HTML 的网站的组织可以很容易地为客户添加 Web 服务，这些客户对数据和底层功能更感兴趣，而不是对 HTML 的表现形式感兴趣。比如，亚马逊就率先通过网站和 Web 服务（基于 SOAP 或 Restful）提供相同的信息和功能。Restful 服务将 HTTP 当作 API，因此避免了复杂的软件分层，这种分层是基于 SOAP 的 Web 服务的明显特征。比如，Restful API 支持通过 HTTP 命令（POST-GET-PUT-DELETE）进行标准的 CRUD（增加-读取-更新-删除）操作；通过 HTTP 状态码可以知道请求是否成功或者为什么失败。Restful Web 服务可以根据需要变得简单或复杂。Restful 是一种风格，实际上是一种非常灵活的风格，而不是一套关于如何设计和构造服务的规定。（伴随而来的缺点是，可能很难确定哪些服务不能算作 Restful 服务。）作为使用者或者客户端，Restful Web 服务与语言和平台无关。客户端发送 HTTP(S) 请求，并以适合现代数据交换的格式（如 JSON）接收文本响应。几乎每一种通用编程语言都至少对 HTTP/HTTPS 有足够的（通常是强大的）支持，这意味着 Web 服务的客户端可以用这些语言来编写。这篇文章将通过一段完整的 Java 代码示例来探讨轻量级的 Restful 服务。基于 Restful 的“小说” Web 服务基于 Restful 的“小说” web 服务包含三个程序员定义的类：Novel 类代表一个小说，只有三个属性：机器生成的 ID、作者和标题。属性可以根据实际情况进行扩展，但我还是想让这个例子看上去更简单一些。Novels 类包含了用于各种任务的工具类：将一个 Novel 或者它们的列表的纯文本编码转换成 XML 或者 JSON；支持在小说集合上进行 CRUD 操作；以及从存储在文件中的数据初始化集合。Novels 类在 Novel 实例和 servlet 之间起中介作用。NovelsServlet 类是从 HttpServlet 中继承的，HttpServlet 是一段健壮且灵活的代码，自 90 年代末的早期企业级 Java 就已经存在了。对于客户端的 CRUD 请求，servlet 可以当作 HTTP 的端点。 servlet 代码主要用于处理客户端的请求和生成相应的响应，而将复杂的细节留给 Novels 类中的工具类进行处理。一些 Java 框架，比如 Jersey（JAX-RS）和 Restlet，就是为 Restful 服务设计的。尽管如此，HttpServlet 本身为完成这些服务提供了轻量、灵活、强大且充分测试过的 API。我会通过下面的“小说”例子来说明。部署“小说” Web 服务当然，部署“小说” Web 服务需要一个 Web 服务器。我的选择是 Tomcat，但是如果该服务托管在 Jetty 或者甚至是 Java 应用服务器上，那么这个服务应该至少可以工作（著名的最后一句话！）。在我的网站上有总结了如何安装 Tomcat 的 README 文件和代码。还有一个附带文档的 Apache Ant 脚本，可以用来构建“小说”服务（或者任何其他服务或网站），并且将它部署在 Tomcat 或相同的服务。Tomcat 可以从它的官网上下载。当你在本地安装后，将 TOMCAT_HOME 设置为安装目录。有两个子目录值得关注：TOMCAT_HOME/bin 目录包含了类 Unix 系统（startup.sh 和 shutdown.sh）和 Windows（startup.bat 和 shutdown.bat） 的启动和停止脚本。Tomcat 作为 Java 应用程序运行。Web 服务器的 servlet 容器叫做 Catalina。（在 Jetty 中，Web 服务器和容器的名字一样。）当 Tomcat 启动后，在浏…

如何使用 OpenSSL：哈希值、数字签名等
type: item
date: 2020-08-13T23:03:00+08:00
description: 通过 OpenSSL 深入了解密码学的细节：哈希值、数字签名、数字证书等。本系列的第一篇文章通过 OpenSSL 库和命令行实用程序介绍了哈希、加密/解密、数字签名和数字证书。这第二篇文章将对细节进行深入探讨。让我们从计算中无处不在的哈希开始，并考虑是什么使哈希函数具备密码学意义。密码学哈希OpenSSL 源代码的下载页面包含了一个带有最新版本的表格。每个版本都有两个 哈希值 hash ：160 位 SHA1 和 256 位 SHA256。这些值可以用来验证下载的文件是否与存储库中的原始文件相匹配：下载者在本地重新计算下载文件的哈希值，然后将结果与原始文件进行比较。现代系统有计算这种哈希值的实用程序。例如，Linux 有 md5sum 和 sha256sum。OpenSSL 本身也提供了类似的命令行实用程序。哈希值被用于计算的许多领域。例如，比特币区块链使用 SHA256 哈希值作为区块标识符。挖比特币就是生成一个低于指定阈值的 SHA256 哈希值，也就是至少有 N 个前导零的哈希值。（N 的值可以上升或下降，这取决于特定时间的挖矿生产力）。作为一个兴趣点，如今的矿机是为并行生成 SHA256 哈希值而设计的硬件集群。在 2018 年的一个高峰期，全球的比特币矿工每秒产生约 7500 万个 太哈希值 terahash —— 这真是一个不可思议的数字。网络协议也使用哈希值（在这里通常叫做“ 校验和 checksum ”）来支持消息的完整性；也就是说，保证收到的消息与发送的消息是一样的。消息发送者计算消息的校验和，并将结果与消息一起发送。当消息到达时，接收方重新计算校验和。如果发送的校验和与重新计算的校验和不一致，那么消息在传输过程中可能出现了一些问题，或者发送的校验和出现了问题，或者两者都出现了问题。在这种情况下，应该重新发送消息和它的校验和，或者至少应该触发一个错误情况。（如 UDP 这样的低级网络协议不会理会校验和。）哈希的其他例子大家都很熟悉。比如一个网站，要求用户用密码进行验证，用户在浏览器中输入密码，然后，他们通过 HTTPS 连接到服务器，密码从浏览器加密发送到服务器。一旦密码到达服务器，就会被解密，然后进行数据库表的查询。在这个查询表中应该存储什么？存储密码本身是有风险的。风险要小得多的方式是存储一个由密码生成的哈希值，也许在计算哈希值之前“加一些 盐 salt （额外的位）改善口味”。你的密码可能会被发送到 Web 服务器上，但网站可以向你保证，密码不会存储在那里。哈希值还出现在安全的各个领域。例如， 基于哈希值的消息认证码 hash-based message authentication code （HMAC）使用一个哈希值和一个秘密的 加密密钥 cryptographic key 来认证通过网络发送的消息。HMAC 码轻量级且易于在程序中使用，在 Web 服务中很受欢迎。一个 X509 数字证书包括一个称为 指纹 fingerprint 的哈希值，它可以方便证书验证。一个存放于内存中的 可信存储 truststore 可以实现为一个以这种指纹为键的查找表 —— 作为一个支持恒定查找时间的 哈希映射 hash map 。来自传入的证书的指纹可以与可信存储中的密钥进行比较，以确定是否匹配。 密码学哈希函数 cryptographic hash function 应该具有什么特殊属性？它应该是 单向 one-way 的，这意味着很难被逆转。一个加密哈希函数应该是比较容易计算的，但是计算它的反函数（将哈希值映射回输入位串的函数）在计算上应该是困难的。下面是一个描述，用 chf 作为加密哈希函数，我的密码 foobar 作为样本输入。 +---+ foobar—>|chf|—>hash value ## 简单直接 +--–+ 相比之下，逆向操作是不可行的： +-----------+ hash value—>|chf inverse|—>foobar ## 棘手困难 +-----------+ 例如，回忆一下 SHA256 哈希函数。对于一个任意长度为 N > 0 的输入位串，这个函数会生成一个 256 位的固定长度的哈希值；因此，这个哈希值甚至不会反映出输入位串的长度 N，更不用说字符串中每个位的值了。顺便说一下，SHA256 不容易受到 长度扩展攻击 length extension attack 。唯一有效的逆向工程方法是通过蛮力搜索将计算出的 SHA256 哈希值逆向返回到输入位串，这意味着需要尝试所有可能的输入位串，直到找到与目标哈希值匹配的位串。这样的搜索在 SHA256 这样一个完善的加密哈希函数上是不可行的。现在，最后一个回顾的知识点是 有序 in order 。加密哈希值是统计学上的唯一，而不是无…

Java 中的数据流和函数式编程
type: item
date: 2020-02-06T08:25:00+08:00
description: 学习如何使用 Java 8 中的流 API 和函数式编程结构。当 Java SE 8（又名核心 Java 8）在 2014 年被推出时，它引入了一些更改，从根本上影响了用它进行的编程。这些更改中有两个紧密相连的部分：流 API 和函数式编程构造。本文使用代码示例，从基础到高级特性，介绍每个部分并说明它们之间的相互作用。基础特性流 API 是在数据序列中迭代元素的简洁而高级的方法。包 java.util.stream 和 java.util.function 包含了用于流 API 和相关函数式编程构造的新库。当然，代码示例胜过千言万语。下面的代码段用大约 2,000 个随机整数值填充了一个 List：Random rand = new Random2(); List<Integer> list = new ArrayList<Integer>(); // 空 list for (int i = 0; i < 2048; i++) list.add(rand.nextInt()); // 填充它另外用一个 for 循环可用于遍历填充列表，以将偶数值收集到另一个列表中。流 API 提供了一种更简洁的方法来执行此操作：List <Integer> evens = list .stream() // 流化 list .filter(n -> (n & 0x1) == 0) // 过滤出奇数值 .collect(Collectors.toList()); // 收集偶数值这个例子有三个来自流 API 的函数：stream 函数可以将集合转换为流，而流是一个每次可访问一个值的传送带。流化是惰性的（因此也是高效的），因为值是根据需要产生的，而不是一次性产生的。filter 函数确定哪些流的值（如果有的话）通过了处理管道中的下一个阶段，即 collect 阶段。filter 函数是 高阶的 higher-order ，因为它的参数是一个函数 —— 在这个例子中是一个 lambda 表达式，它是一个未命名的函数，并且是 Java 新的函数式编程结构的核心。lambda 语法与传统的 Java 完全不同：n -> (n & 0x1) == 0箭头（一个减号后面紧跟着一个大于号）将左边的参数列表与右边的函数体分隔开。参数 n 虽未明确类型，但也可以明确。在任何情况下，编译器都会发现 n 是个 Integer。如果有多个参数，这些参数将被括在括号中，并用逗号分隔。在本例中，函数体检查一个整数的最低位（最右）是否为零，这用来表示偶数。过滤器应返回一个布尔值。尽管可以，但该函数的主体中没有显式的 return。如果主体没有显式的 return，则主体的最后一个表达式即是返回值。在这个例子中，主体按照 lambda 编程的思想编写，由一个简单的布尔表达式 (n & 0x1) == 0 组成。collect 函数将偶数值收集到引用为 evens 的列表中。如下例所示，collect 函数是线程安全的，因此，即使在多个线程之间共享了过滤操作，该函数也可以正常工作。方便的功能和轻松实现多线程在生产环境中，数据流的源可能是文件或网络连接。为了学习流 API, Java 提供了诸如 IntStream 这样的类型，它可以用各种类型的元素生成流。这里有一个 IntStream 的例子：IntStream // 整型流 .range(1, 2048) // 生成此范围内的整型流 .parallel() // 为多个线程分区数据 .filter(i -> ((i & 0x1) > 0)) // 奇偶校验 - 只允许奇数通过 .forEach(System.out::println); // 打印每个值IntStream 类型包括一个 range 函数，该函数在指定的范围内生成一个整数值流，在本例中，以 1 为增量，从 1 递增到 2048。parallel 函数自动划分该工作到多个线程中，在各个线程中进行过滤和打印。（线程数通常与主机系统上的 CPU 数量匹配。）函数 forEach 参数是一个方法引用，在本例中是对封装在 System.out 中的 println 方法的引用，方法输出类型为 PrintStream。方法和构造器引用的语法将在稍后讨论。由于具有多线程，因此整数值整体上以任意顺序打印，但在给定线程中是按顺序打印的。例如，如果线程 T1 打印 409 和 411，那么 T1 将按照顺序 409-411 打印，但是其它某个线程可能会预先打印 2045。parallel 调用后面的线程是并发执行的，因此它们的输出顺序是不确定的。map/reduce 模式map/reduce 模式在处理大型数据集方面变得很流行。一个 map/reduce 宏操作由两个微操作构成。首…

OpenSSL 入门：密码学基础知识
type: item
date: 2020-01-23T22:23:44+08:00
description: 想要入门密码学的基础知识，尤其是有关 OpenSSL 的入门知识吗？继续阅读。本文是使用 OpenSSL 的密码学基础知识的两篇文章中的第一篇，OpenSSL 是在 Linux 和其他系统上流行的生产级库和工具包。（要安装 OpenSSL 的最新版本，请参阅这里。）OpenSSL 实用程序可在命令行使用，程序也可以调用 OpenSSL 库中的函数。本文的示例程序使用的是 C 语言，即 OpenSSL 库的源语言。本系列的两篇文章涵盖了加密哈希、数字签名、加密和解密以及数字证书。你可以从我的网站的 ZIP 文件中找到这些代码和命令行示例。让我们首先回顾一下 OpenSSL 名称中的 SSL。OpenSSL 简史 安全套接字层 Secure Socket Layer （SSL）是 Netscape 在 1995 年发布的一种加密协议。该协议层可以位于 HTTP 之上，从而为 HTTPS 提供了 S： 安全 secure 。SSL 协议提供了各种安全服务，其中包括两项在 HTTPS 中至关重要的服务： 对等身份验证 Peer authentication （也称为相互质询）：连接的每一边都对另一边的身份进行身份验证。如果 Alice 和 Bob 要通过 SSL 交换消息，则每个人首先验证彼此的身份。 机密性 Confidentiality ：发送者在通过通道发送消息之前先对其进行加密。然后，接收者解密每个接收到的消息。此过程可保护网络对话。即使窃听者 Eve 截获了从 Alice 到 Bob 的加密消息（即中间人攻击），Eve 会发现他无法在计算上解密此消息。反过来，这两个关键 SSL 服务与其他不太受关注的服务相关联。例如，SSL 支持消息完整性，从而确保接收到的消息与发送的消息相同。此功能是通过哈希函数实现的，哈希函数也随 OpenSSL 工具箱一起提供。SSL 有多个版本（例如 SSLv2 和 SSLv3），并且在 1999 年出现了一个基于 SSLv3 的类似协议 传输层安全性 Transport Layer Security （TLS）。TLSv1 和 SSLv3 相似，但不足以相互配合工作。不过，通常将 SSL/TLS 称为同一协议。例如，即使正在使用的是 TLS（而非 SSL），OpenSSL 函数也经常在名称中包含 SSL。此外，调用 OpenSSL 命令行实用程序以 openssl 开始。除了 man 页面之外，OpenSSL 的文档是零零散散的，鉴于 OpenSSL 工具包很大，这些页面很难以查找使用。命令行和代码示例可以将主要主题集中起来。让我们从一个熟悉的示例开始（使用 HTTPS 访问网站），然后使用该示例来选出我们感兴趣的加密部分进行讲述。一个 HTTPS 客户端此处显示的 client 程序通过 HTTPS 连接到 Google：/* compilation: gcc -o client client.c -lssl -lcrypto */ #include <stdio.h> #include <stdlib.h> #include <openssl/bio.h> /* BasicInput/Output streams */ #include <openssl/err.h> /* errors */ #include <openssl/ssl.h> /* core library */ #define BuffSize 1024 void report_and_exit(const char* msg) { perror(msg); ERR_print_errors_fp(stderr); exit(-1); } void init_ssl() { SSL_load_error_strings(); SSL_library_init(); } void cleanup(SSL_CTX* ctx, BIO* bio) { SSL_CTX_free(ctx); BIO_free_all(bio); } void secure_connect(const char* hostname) { char name[BuffSize]; char request[BuffSize]; char response[BuffSize]; const SSL_METHOD* method = TLSv1_2_client_method(); if (NULL == method) report_and_exit("TLSv1_2_client_method..."); SSL_CTX* ctx = SSL_CTX_new(method); if (NULL == ctx) report_and_exit("SSL_C…

如何使用 Protobuf 做数据交换
type: item
date: 2019-11-22T15:59:06+08:00
description: 在以不同语言编写并在不同平台上运行的应用程序之间交换数据时，Protobuf 编码可提高效率。 协议缓冲区 Protocol Buffers （Protobufs）像 XML 和 JSON 一样，可以让用不同语言编写并在不同平台上运行的应用程序交换数据。例如，用 Go 编写的发送程序可以在 Protobuf 中对以 Go 表示的销售订单数据进行编码，然后用 Java 编写的接收方可以对它进行解码，以获取所接收订单数据的 Java 表示方式。这是在网络连接上的结构示意图：Go 销售订单 —> Pbuf 编码 —> 网络 —> Pbuf 界面 —> Java 销售订单与 XML 和 JSON 相比，Protobuf 编码是二进制而不是文本，这会使调试复杂化。但是，正如本文中的代码示例所确认的那样，Protobuf 编码在大小上比 XML 或 JSON 编码要有效得多。Protobuf 以另一种方式提供了这种有效性。在实现级别，Protobuf 和其他编码系统对结构化数据进行 序列化 serialize 和 反序列化 deserialize 。序列化将特定语言的数据结构转换为字节流，反序列化是将字节流转换回特定语言的数据结构的逆运算。序列化和反序列化可能成为数据交换的瓶颈，因为这些操作会占用大量 CPU。高效的序列化和反序列化是 Protobuf 的另一个设计目标。最近的编码技术，例如 Protobuf 和 FlatBuffers，源自 1990 年代初期的 DCE/RPC（ 分布式计算环境/远程过程调用 Distributed Computing Environment/Remote Procedure Call ）计划。与 DCE/RPC 一样，Protobuf 在数据交换中为 IDL（接口定义语言）和编码层做出了贡献。本文将着眼于这两层，然后提供 Go 和 Java 中的代码示例以充实 Protobuf 的细节，并表明 Protobuf 是易于使用的。Protobuf 作为一个 IDL 和编码层像 Protobuf 一样，DCE/RPC 被设计为与语言和平台无关。适当的库和实用程序允许任何语言和平台用于 DCE/RPC 领域。此外，DCE/RPC 体系结构非常优雅。IDL 文档是一侧的远程过程与另一侧的调用者之间的协定。Protobuf 也是以 IDL 文档为中心的。IDL 文档是文本，在 DCE/RPC 中，使用基本 C 语法以及元数据的语法扩展（方括号）和一些新的关键字，例如 interface。这是一个例子：[uuid (2d6ead46-05e3-11ca-7dd1-426909beabcd), version(1.0)] interface echo { const long int ECHO_SIZE = 512; void echo( [in] handle_t h, [in, string] idl_char from_client[ ], [out, string] idl_char from_service[ECHO_SIZE] ); }该 IDL 文档声明了一个名为 echo 的过程，该过程带有三个参数：类型为 handle_t（实现指针）和 idl_char（ASCII 字符数组）的 [in] 参数被传递给远程过程，而 [out] 参数（也是一个字符串）从该过程中传回。在此示例中，echo 过程不会显式返回值（echo 左侧的 void），但也可以返回值。返回值，以及一个或多个 [out] 参数，允许远程过程任意返回许多值。下一节将介绍 Protobuf IDL，它的语法不同，但同样用作数据交换中的协定。DCE/RPC 和 Protobuf 中的 IDL 文档是创建用于交换数据的基础结构代码的实用程序的输入：IDL 文档 —> DCE/PRC 或 Protobuf 实用程序 —> 数据交换的支持代码作为相对简单的文本，IDL 是同样便于人类阅读的关于数据交换细节的文档（特别是交换的数据项的数量和每个项的数据类型）。Protobuf 可用于现代 RPC 系统，例如 gRPC；但是 Protobuf 本身仅提供 IDL 层和编码层，用于从发送者传递到接收者的消息。与原本的 DCE/RPC 一样，Protobuf 编码是二进制的，但效率更高。目前，XML 和 JSON 编码仍在通过 Web 服务等技术进行的数据交换中占主导地位，这些技术利用 Web 服务器、传输协议（例如 TCP、HTTP）以及标准库和实用程序等原有的基础设施来处理 XML 和 JSON 文档。 此外，各种类型的数据库系统可以存储 XML 和 JSON 文档，甚至旧式关系型系统也可以轻松生成查询结果的 XML 编码。现在，每种通用编程语言都具…

Linux 下的进程间通信：套接字和信号
type: item
date: 2019-06-03T07:44:52+08:00
description: 学习在 Linux 中进程是如何与其他进程进行同步的。本篇是 Linux 下进程间通信（IPC）系列的第三篇同时也是最后一篇文章。第一篇文章聚焦在通过共享存储（文件和共享内存段）来进行 IPC，第二篇文章则通过管道（无名的或者命名的）及消息队列来达到相同的目的。这篇文章将目光从高处（套接字）然后到低处（信号）来关注 IPC。代码示例将用力地充实下面的解释细节。套接字正如管道有两种类型（命名和无名）一样，套接字也有两种类型。IPC 套接字（即 Unix 套接字）给予进程在相同设备（主机）上基于通道的通信能力；而网络套接字给予进程运行在不同主机的能力，因此也带来了网络通信的能力。网络套接字需要底层协议的支持，例如 TCP（传输控制协议）或 UDP（用户数据报协议）。与之相反，IPC 套接字依赖于本地系统内核的支持来进行通信；特别的，IPC 通信使用一个本地的文件作为套接字地址。尽管这两种套接字的实现有所不同，但在本质上，IPC 套接字和网络套接字的 API 是一致的。接下来的例子将包含网络套接字的内容，但示例服务器和客户端程序可以在相同的机器上运行，因为服务器使用了 localhost（127.0.0.1）这个网络地址，该地址表示的是本地机器上的本地机器地址。套接字以流的形式（下面将会讨论到）被配置为双向的，并且其控制遵循 C/S（客户端/服务器端）模式：客户端通过尝试连接一个服务器来初始化对话，而服务器端将尝试接受该连接。假如万事顺利，来自客户端的请求和来自服务器端的响应将通过管道进行传输，直到其中任意一方关闭该通道，从而断开这个连接。一个迭代服务器（只适用于开发）将一直和连接它的客户端打交道：从最开始服务第一个客户端，然后到这个连接关闭，然后服务第二个客户端，循环往复。这种方式的一个缺点是处理一个特定的客户端可能会挂起，使得其他的客户端一直在后面等待。生产级别的服务器将是并发的，通常使用了多进程或者多线程的混合。例如，我台式机上的 Nginx 网络服务器有一个 4 个 工人 worker 的进程池，它们可以并发地处理客户端的请求。在下面的代码示例中，我们将使用迭代服务器，使得我们将要处理的问题保持在一个很小的规模，只关注基本的 API，而不去关心并发的问题。最后，随着各种 POSIX 改进的出现，套接字 API 随着时间的推移而发生了显著的变化。当前针对服务器端和客户端的示例代码特意写的比较简单，但是它着重强调了基于流的套接字中连接的双方。下面是关于流控制的一个总结，其中服务器端在一个终端中开启，而客户端在另一个不同的终端中开启：服务器端等待客户端的连接，对于给定的一个成功连接，它就读取来自客户端的数据。为了强调是双方的会话，服务器端会对接收自客户端的数据做回应。这些数据都是 ASCII 字符代码，它们组成了一些书的标题。客户端将书的标题写给服务器端的进程，并从服务器端的回应中读取到相同的标题。然后客户端和服务器端都在屏幕上打印出标题。下面是服务器端的输出，客户端的输出也和它完全一样：Listening on port 9876 for clients... War and Peace Pride and Prejudice The Sound and the Fury示例 1. 使用套接字的客户端程序#include <string.h> #include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <sys/types.h> #include <sys/socket.h> #include <netinet/tcp.h> #include <arpa/inet.h> #include "sock.h" void report(const char* msg, int terminate) { perror(msg); if (terminate) exit(-1); /* failure */ } int main() { int fd = socket(AF_INET, /* network versus AF_LOCAL */ SOCK_STREAM, /* reliable, bidirectional: TCP */ 0); /* system picks underlying protocol */ if (fd < 0) report("socket", 1); /* terminate */ /* bind the server's local address in memory */ struct sockaddr_in saddr; memset(&saddr, 0, sizeof(saddr)); /* clear the byt…

Linux 下的进程间通信：使用管道和消息队列
type: item
date: 2019-05-12T21:43:00+08:00
description: 学习在 Linux 中进程是如何与其他进程进行同步的。本篇是 Linux 下进程间通信（IPC）系列的第二篇文章。第一篇文章 聚焦于通过共享文件和共享内存段这样的共享存储来进行 IPC。这篇文件的重点将转向管道，它是连接需要通信的进程之间的通道。管道拥有一个写端用于写入字节数据，还有一个读端用于按照先入先出的顺序读入这些字节数据。而这些字节数据可能代表任何东西：数字、员工记录、数字电影等等。管道有两种类型，命名管道和无名管道，都可以交互式的在命令行或程序中使用它们；相关的例子在下面展示。这篇文章也将介绍内存队列，尽管它们有些过时了，但它们不应该受这样的待遇。在本系列的第一篇文章中的示例代码承认了在 IPC 中可能受到竞争条件（不管是基于文件的还是基于内存的）的威胁。自然地我们也会考虑基于管道的 IPC 的安全并发问题，这个也将在本文中提及。针对管道和内存队列的例子将会使用 POSIX 推荐使用的 API，POSIX 的一个核心目标就是线程安全。请查看一些 mq\_open 函数的 man 页，这个函数属于内存队列的 API。这个 man 页中有关 特性 的章节带有一个小表格：接口特性值mq_open()线程安全MT-Safe上面的 MT-Safe（MT 指的是 多线程 multi-threaded ）意味着 mq_open 函数是线程安全的，进而暗示是进程安全的：一个进程的执行和它的一个线程执行的过程类似，假如竞争条件不会发生在处于相同进程的线程中，那么这样的条件也不会发生在处于不同进程的线程中。MT-Safe 特性保证了调用 mq_open 时不会出现竞争条件。一般来说，基于通道的 IPC 是并发安全的，尽管在下面例子中会出现一个有关警告的注意事项。无名管道首先让我们通过一个特意构造的命令行例子来展示无名管道是如何工作的。在所有的现代系统中，符号 | 在命令行中都代表一个无名管道。假设我们的命令行提示符为 %，接下来考虑下面的命令：## 写入方在 | 左边，读取方在右边 % sleep 5 | echo "Hello, world!" sleep 和 echo 程序以不同的进程执行，无名管道允许它们进行通信。但是上面的例子被特意设计为没有通信发生。问候语 “Hello, world!” 出现在屏幕中，然后过了 5 秒后，命令行返回，暗示 sleep 和 echo 进程都已经结束了。这期间发生了什么呢？在命令行中的竖线 | 的语法中，左边的进程（sleep）是写入方，右边的进程（echo）为读取方。默认情况下，读取方将会阻塞，直到从通道中能够读取到字节数据，而写入方在写完它的字节数据后，将发送 流已终止 end-of-stream 的标志。（即便写入方过早终止了，一个流已终止的标志还是会发给读取方。）无名管道将保持到写入方和读取方都停止的那个时刻。在上面的例子中，sleep 进程并没有向通道写入任何的字节数据，但在 5 秒后就终止了，这时将向通道发送一个流已终止的标志。与此同时，echo 进程立即向标准输出（屏幕）写入问候语，因为这个进程并不从通道中读入任何字节，所以它并没有等待。一旦 sleep 和 echo 进程都终止了，不会再用作通信的无名管道将会消失然后返回命令行提示符。下面这个更加实用的示例将使用两个无名管道。我们假定文件 test.dat 的内容如下：this is the way the world ends下面的命令：% cat test.dat | sort | uniq会将 cat（ 连接 concatenate 的缩写）进程的输出通过管道传给 sort 进程以生成排序后的输出，然后将排序后的输出通过管道传给 uniq 进程以消除重复的记录（在本例中，会将两次出现的 “the” 缩减为一个）：ends is the this way world下面展示的情景展示的是一个带有两个进程的程序通过一个无名管道通信来进行通信。示例 1. 两个进程通过一个无名管道来进行通信#include <sys/wait.h> /* wait */ #include <stdio.h> #include <stdlib.h> /* exit functions */ #include <unistd.h> /* read, write, pipe, _exit */ #include <string.h> #define ReadEnd 0 #define WriteEnd 1 void report_and_exit(const char* msg) { perror(msg); exit(-1); /** failure **/ } int main() { int pipeFDs[2]; /* two file descr…