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}次方是: {}.", unsafe { pow(n, p) }); let mut y: f64 = 64.0; println!("\n{y}的平方根是: {}.", unsafe { sqrt(y) }); y = -3.14; println!("\n{y}的平方根是: {}.", unsafe { sqrt(y) }); //** NaN = NotaNumber（不是数字） } 顶部的两个 use 声明是 Rust 的数据类型 c_int 和 c_double，对应 C 类型里的 int 和 double。Rust 标准模块 std::os::raw 定义了 14 个类似的类型以确保跟 C 的兼容性。模块 std::ffi 中有 14 个同样的类型定义，以及对字符串的支持。位于 main 函数上的 extern "C" 区域声明了 3 个 C 库函数，这些函数会在 main 函数内被调用。每次调用都使用了标准的 C 函数名，但每次调用都必须发生在一个 unsafe 区域内。正如每个新接触 Rust 的程序员所发现的那样，Rust 编译器极度强制内存安全。其他语言（特别是 C 和 C++）作不出相同的保证。unsafe 区域其实是说：Rust 对外部调用中可能存在的不安全行为不负责。第一个程序输出为：-123的绝对值是: 123. 9的3次方是: 729. 64的平方根是: 8. -3.14的平方根是: NaN. 输出的最后一行的 NaN 表示 不是数字 Not a Number ：C 库函数 sqrt 期待一个非负值作为参数，这使得参数 -3.14 生成了 NaN 作为返回值。调用涉及指针的 C 函数C 库函数为了提高效率，经常在安全、网络、字符串处理、内存管理，以及其他领域中使用指针。例如，库函数 asctime（ASCII 字符串形式的时间）期待一个结构体指针作为其参数。Rust 调用类似 asctime 的 C 函数就会比调用 sqrt 要更加棘手一些，后者既没有牵扯到指针，也不涉及到结构体。函数 asctime 调用的 C 结构体类型为 struct tm。一个指向此结构体的指针会作为参数被传递给库函数 mktime（时间作为值）。此结构体会将时间拆分成诸如年、月、小时之类的单位。此结构体的 字段 field 类型为 time_t，是 int（32位）和 long（64 位）的别名。两个库函数将这些破碎的时间片段组合成了一个单一值：asctime 返回一个以字符串表示的时间，而 mktime 返回一个 time_t 值表示自 “ 纪元 Epoch 以来所经历的秒数，这是一个系统的时钟和时间戳的相对时间。典型的纪元设置为 1900 年或 1970 年，1 月 1 日 0 时 0 分 0 秒。（LCTT 校注：Unix、Linux 乃至于如今所有主要的计算机和网络的时间纪元均采用 1970 年为起点。）以下的 C 程序调用了 asctime 和 mktime，并使用了其他库函数 strftime 来将 mktime 的返回值转化成一个格式化的字符串。这个程序可被视作 Rust 对应版本的预热：#include <stdio.h> #include <time.h> int main () { struct tm sometime; /* 时间被打破细分 */ char buffer[80]; int utc; sometime.tm_sec = 1; sometime.tm_min = 1; sometime.tm_hour = 1; sometime.tm_mday = 1; sometime.tm_mon = 1; sometime.tm_year = 1; /*LCTT 校注：注意，相对于 1900 年的年数*/ sometime.tm_hour = 1; sometime.tm_wday = 1; sometime.tm_yday = 1; printf("日期与时间: %s\n", asctime(&sometime)); utc = mktime(&sometime); if( utc < 0 ) { fprintf(stderr, "错误: mktime 无法生成时间\n"); } else { printf("返回的整数值: %d\n", utc); strftime(buffer, sizeof(buffer), "%c", &sometime); printf("更加可读的版本: %s\n", buffer); } return 0; } 程序输出为：日期与时间: Fri Feb 1 01:01:01 1901 返回的整数值: 2120218157 更加可读的版本: Fri Feb 1 01:01:01 1901 （LCTT 译注：如果你尝试在自己电脑上运行这段代码，然后得到了一行关于 mktime 的错误信息，然后又在网上随便找了个在线 C 编译器，复制代码然后得到了跟这里的结果有区别但是没有错误的结果，不要慌，我的电脑上也是这样的。导致本地机器上 mktime 失败的原因是作者没有设置 tm_isdst，这个是用来标记夏令时的标志。tm_isdst 大于零则夏令时生效中，等于零则不生效，小于零标记未知。加入 sometime.tm_isdst = 0 或 = -1 后应该就能得到跟在线编译器大致一样的结果。不同的地方在于结果第一行我得到的是 Mon Feb ...，这个与作者代码中 sometime.tm_wday = 1 对应，这里应该是作者写错了；第二行我和作者和网上得到的数字都不一样，这大概是合理的，因为这与机器的纪元有关；第三行我跟作者的结果是一样的，1901 年 2 月 1 日也确实是周五，这是因为 mktime 其实会修正时间参数中不合理的地方。至于夏令时具体是如何影响 mktime 这个问题，我能查到的只有 mktime 的计算受时区影响，更底层的原因我也不知道了。）总的来说，Rust 在调用库函数 asctime 和 mktime 时，必须处理以下两个问题：将裸指针作为唯一参数传递给每个库函数。把从 asctime 返回的 C 字符串转化为 Rust 字符串。Rust 调用 asctime 和 mktime工具 bindgen 会根据类似 math.h 和 time.h 之类的 C 头文件生成 Rust 支持的代码。下面这个简化版的 time.h 就可以用来做例子，简化版与原版主要有两个不同：内置类型 int 被用来取代别名类型 time_t。工具 bindgen 可以处理 time_t 类，但是会生成一些烦人的警告，因为 time_t 不符合 Rust 的命名规范：time_t 以下划线区分 time 和 t；Rust 更偏好驼峰式命名方法，比如 TimeT。出于同样的原因，这里选择 StructTM 作为 struct tm 的别名。以下是一份简化版的头文件，mktime 和 asctime 在文件底部：typedef struct tm { int tm_sec; /* 秒 */ int tm_min; /* 分钟 */ int tm_hour; /* 小时 */ int tm_mday; /* 日 */ int tm_mon; /* 月 */ int tm_year; /* 年 */ int tm_wday; /* 星期 */ int tm_yday; /* 一年中的第几天 */ int tm_isdst; /* 夏令时 */ } StructTM; extern int mktime(StructTM*); extern char* asctime(StructTM*); bindgen 安装好后，mytime.h 作为以上提到的头文件，以下命令（% 是命令行提示符）可以生成所需的 Rust 代码并将其保存到文件 mytime.rs：% bindgen mytime.h > mytime.rs 以下是 mytime.rs 中的重要部分：/* automatically generated by rust-bindgen 0.61.0 */ #[repr(C)] #[derive(Debug, Copy, Clone)] pub struct tm { pub tm_sec: ::std::os::raw::c_int, pub tm_min: ::std::os::raw::c_int, pub tm_hour: ::std::os::raw::c_int, pub tm_mday: ::std::os::raw::c_int, pub tm_mon: ::std::os::raw::c_int, pub tm_year: ::std::os::raw::c_int, pub tm_wday: ::std::os::raw::c_int, pub tm_yday: ::std::os::raw::c_int, pub tm_isdst: ::std::os::raw::c_int, } pub type StructTM = tm; extern "C" { pub fn mktime(arg1: *mut StructTM) -> ::std::os::raw::c_int; } extern "C" { pub fn asctime(arg1: *mut StructTM) -> *mut ::std::os::raw::c_char; } #[test] fn bindgen_test_layout_tm() { const UNINIT: ::std::mem::MaybeUninit<tm> = ::std::mem::MaybeUninit::uninit(); let ptr = UNINIT.as_ptr(); assert_eq!( ::std::mem::size_of::<tm>(), 36usize, concat!("Size of: ", stringify!(tm)) ); ... Rust 结构体 struct tm，跟原本在 C 中的一样，包含了 9 个 4 字节的整型字段。这些字段名称在 C 和 Rust 中是一样的。extern "C" 区域声明了库函数 astime 和 mktime 分别需要只一个参数，一个指向可变实例 StructTM 的裸指针。（库函数可能会通过指针改变作为参数传递的结构体。）#[test] 属性下的其余代码是用来测试 Rust 版的时间结构体的布局。通过命令 cargo test 可以进行这些测试。问题在于，C 没有规定编译器应该如何对结构体中的字段进行布局。比如说，C 的 struct tm 以字段 tm_sec 开头用以表示秒；但是 C 不需要编译版本遵循这个排序。不管怎样，Rust 测试应该会成功，而 Rust 对库函数的调用也应如预期般工作。设置好第二个案例并开始运行从 bindgen 生成的代码不包含 main 函数，所以是一个天然的模块。以下是一个 main 函数初始化了 StructTM 并调用了 asctime 和 mktime：mod mytime; use mytime::*; use std::ffi::CStr; fn main() { let mut sometime = StructTM { tm_year: 1, tm_mon: 1, tm_mday: 1, tm_hour: 1, tm_min: 1, tm_sec: 1, tm_isdst: -1, tm_wday: 1, tm_yday: 1 }; unsafe { let c_ptr = &mut sometime; // 裸指针 // 调用，转化，并拥有 // 返回的 C 字符串 let char_ptr = asctime(c_ptr); let c_str = CStr::from_ptr(char_ptr); println!("{:#?}", c_str.to_str()); let utc = mktime(c_ptr); println!("{}", utc); } } 这段 Rust 代码可以被编译（直接用 rustc 或使用 cargo）并运行。输出为：Ok( "Mon Feb 1 01:01:01 1901\n", ) 2120218157 对 C 函数 asctime 和 mktime 的调用必须再一次被放在 unsafe 区域内，因为 Rust 编译器无法对这些外部函数的潜在内存安全风险负责。此处声明一下，asctime 和 mktime 并没有安全风险。调用的两个函数的参数是裸指针 ptr，其指向结构体 sometime （在 栈 stack 中）的地址。asctime 是两个函数中调用起来更棘手的那个，因为这个函数返回的是一个指向 C char 的指针，如果函数返回 Mon 那么指针就指向 M。但是 Rust 编译器并不知道 C 字符串 （char 的空终止数组）的储存位置。是内存里的静态空间？还是 堆 heap ？asctime 函数内用来储存时间的文字表达的数组实际上是在内存的静态空间里。无论如何，C 到 Rust 字符串转化需要两个步骤来避免编译错误：调用 Cstr::from_ptr(char_ptr) 来将 C 字符串转化为 Rust 字符串并返回一个引用储存在变量 c_str 中。对 c_str.to_str() 的调用确保了 c_str 是所有者。Rust 代码不会增加从 mktime 返回的整型值的易读性，这一部分留作课外作业给感兴趣的人去探究。Rust 模板 chrono::format 也有一个 strftime 函数，它可以被当作 C 的同名函数来使用，两者都是获取时间的文字表达。使用 FFI 和 bindgen 调用 CRust FFI 和工具 bindgen 都能够出色地协助 Rust 调用 C 库，无论是标准库还是第三方库。Rust 可以轻松地与 C 交流，并透过 C 与其他语言交流。对于调用像 sqrt 一样简单的库函数，Rust FFI 表现直截了当，这是因为 Rust 的原始数据类型覆盖了它们在 C 中的对应部分。对于更为复杂的交流 —— 特别是 Rust 调用像 asctime 和 mktime 一样，会涉及到结构体和指针的 C 库函数 —— bindgen 工具是优秀的帮手。这个工具会生成支持代码以及所需要的测试。当然，Rust 编译器无法假设 C 代码对内存安全的考虑会符合 Rust 的标准；因此，Rust 必须在 unsafe 区域内调用 C。via: https://opensource.com/article/22/11/rust-calls-c-library-functions作者：Marty Kalin 选题：lkxed 译者：yzuowei 校对：wxy本文由 LCTT 原创编译，Linux中国 荣誉推出
了解有关内存安全和效率的更多信息。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 语言函数）、字符串文字（例如“Hello, world!”）和全局变量提供存储空间：int n; /* global variable */ int main() { /* function */ char* msg = "No comment"; /* string literal */ ... } 该区域是静态的，因为它的大小从进程执行开始到结束都固定不变。由于静态区相当于进程固定大小的内存占用，因此经验法则是通过避免使用全局数组等方法来使该区域尽可能小。下文会结合代码示例对本节概述展开进一步讲解。栈存储想象一个有各种连续执行的任务的程序，任务包括了处理每隔几分钟通过网络下载并存储在本地文件中的数字数据。下面的 stack 程序简化了处理流程（仅是将奇数整数值转换为偶数），而将重点放在栈存储的好处上。#include <stdio.h> #include <stdlib.h> #define Infile "incoming.dat" #define Outfile "outgoing.dat" #define IntCount 128000 /* 128,000 */ void other_task1() { /*...*/ } void other_task2() { /*...*/ } void process_data(const char* infile, const char* outfile, const unsigned n) { int nums[n]; FILE* input = fopen(infile, "r"); if (NULL == infile) return; FILE* output = fopen(outfile, "w"); if (NULL == output) { fclose(input); return; } fread(nums, n, sizeof(int), input); /* read input data */ unsigned i; for (i = 0; i < n; i++) { if (1 == (nums[i] & 0x1)) /* odd parity? */ nums[i]--; /* make even */ } fclose(input); /* close input file */ fwrite(nums, n, sizeof(int), output); fclose(output); } int main() { process_data(Infile, Outfile, IntCount); /** now perform other tasks **/ other_task1(); /* automatically released stack storage available */ other_task2(); /* ditto */ return 0; } 底部的 main 函数首先调用 process_data 函数，该函数会创建一个基于栈的数组，其大小由参数 n 给定（当前示例中为 128,000）。因此，该数组占用 128000 * sizeof(int) 个字节，在标准设备上达到了 512,000 字节（int 在这些设备上是四个字节）。然后数据会被读入数组（使用库函数 fread），循环处理，并保存到本地文件 outgoing.dat（使用库函数 fwrite）。当 process_data 函数返回到其调用者 main 函数时，process_data 函数的大约 500MB 栈暂存器可供 stack 程序中的其他函数用作暂存器。在此示例中，main 函数接下来调用存根函数 other_task1 和 other_task2。这三个函数在 main 中依次调用，这意味着所有三个函数都可以使用相同的堆栈存储作为暂存器。因为编写栈管理代码的是编译器而不是程序员，所以这种方法对程序员来说既高效又容易。在 C 语言中，在块（例如函数或循环体）内定义的任何变量默认都有一个 auto 存储类，这意味着该变量是基于栈的。存储类 register 现在已经过时了，因为 C 编译器会主动尝试尽可能使用 CPU 寄存器。只有在块内定义的变量可能是 register，如果没有可用的 CPU 寄存器，编译器会将其更改为 auto。基于栈的编程可能是不错的首选方式，但这种风格确实有一些挑战性。下面的 badStack 程序说明了这点。#include <stdio.h>; const int* get_array(const unsigned n) { int arr[n]; /* stack-based array */ unsigned i; for (i = 0; i < n; i++) arr[i] = 1 + 1; return arr; /** ERROR **/ } int main() { const unsigned n = 16; const int* ptr = get_array(n); unsigned i; for (i = 0; i < n; i++) printf("%i ", ptr[i]); puts("\n"); return 0; } badStack 程序中的控制流程很简单。main 函数使用 16（LCTT 译注：原文为 128，应为作者笔误）作为参数调用函数 get_array，然后被调用函数会使用传入参数来创建对应大小的本地数组。get_array 函数会初始化数组并返回给 main 中的数组标识符 arr。 arr 是一个指针常量，保存数组的第一个 int 元素的地址。当然，本地数组 arr 可以在 get_array 函数中访问，但是一旦 get_array 返回，就不能合法访问该数组。尽管如此，main 函数会尝试使用函数 get_array 返回的堆栈地址 arr 来打印基于栈的数组。现代编译器会警告错误。例如，下面是来自 GNU 编译器的警告：badStack.c: In function 'get_array': badStack.c:9:10: warning: function returns address of local variable [-Wreturn-local-addr] return arr; /** ERROR **/ 一般规则是，如果使用栈存储实现局部变量，应该仅在该变量所在的代码块内，访问这块基于栈的存储（在本例中，数组指针 arr 和循环计数器 i 均为这样的局部变量）。因此，函数永远不应该返回指向基于栈存储的指针。堆存储接下来使用若干代码示例凸显在 C 语言中使用堆存储的优点。在第一个示例中，使用了最优方案分配、使用和释放堆存储。第二个示例（在下一节中）将堆存储嵌套在了其他堆存储中，这会使其释放操作变得复杂。#include <stdio.h> #include <stdlib.h> int* get_heap_array(unsigned n) { int* heap_nums = malloc(sizeof(int) * n); unsigned i; for (i = 0; i < n; i++) heap_nums[i] = i + 1; /* initialize the array */ /* stack storage for variables heap_nums and i released automatically when get_num_array returns */ return heap_nums; /* return (copy of) the pointer */ } int main() { unsigned n = 100, i; int* heap_nums = get_heap_array(n); /* save returned address */ if (NULL == heap_nums) /* malloc failed */ fprintf(stderr, "%s\n", "malloc(...) failed..."); else { for (i = 0; i < n; i++) printf("%i\n", heap_nums[i]); free(heap_nums); /* free the heap storage */ } return 0; } 上面的 heap 程序有两个函数： main 函数使用参数（示例中为 100）调用 get_heap_array 函数，参数用来指定数组应该有多少个 int 元素。因为堆分配可能会失败，main 函数会检查 get_heap_array 是否返回了 NULL；如果是，则表示失败。如果分配成功，main 将打印数组中的 int 值，然后立即调用库函数 free 来对堆存储解除分配。这就是最优的方案。get_heap_array 函数以下列语句开头，该语句值得仔细研究一下：int* heap_nums = malloc(sizeof(int) * n); /* heap allocation */ malloc 库函数及其变体函数针对字节进行操作；因此，malloc 的参数是 n 个 int 类型元素所需的字节数（sizeof(int) 在标准现代设备上是四个字节）。malloc 函数返回所分配字节段的首地址，如果失败则返回 NULL .如果成功调用 malloc，在现代台式机上其返回的地址大小为 64 位。在手持设备和早些时候的台式机上，该地址的大小可能是 32 位，或者甚至更小，具体取决于其年代。堆分配数组中的元素是 int 类型，这是一个四字节的有符号整数。这些堆分配的 int 的地址存储在基于栈的局部变量 heap_nums 中。可以参考下图： heap-based stack-based / \ +----+----+ +----+ heap-nums--->|int1|int2|...|intN| +----+----+ +----+ 一旦 get_heap_array 函数返回，指针变量 heap_nums 的栈存储将自动回收——但动态 int 数组的堆存储仍然存在，这就是 get_heap_array 函数返回这个地址（的副本）给 main 函数的原因：它现在负责在打印数组的整数后，通过调用库函数 free 显式释放堆存储：free(heap_nums); /* free the heap storage */ malloc 函数不会初始化堆分配的存储空间，因此里面是随机值。相比之下，其变体函数 calloc 会将分配的存储初始化为零。这两个函数都返回 NULL 来表示分配失败。在 heap 示例中，main 函数在调用 free 后会立即返回，正在执行的程序会终止，这会让系统回收所有已分配的堆存储。尽管如此，程序员应该养成在不再需要时立即显式释放堆存储的习惯。嵌套堆分配下一个代码示例会更棘手一些。C 语言有很多返回指向堆存储的指针的库函数。下面是一个常见的使用情景：1、C 程序调用一个库函数，该函数返回一个指向基于堆的存储的指针，而指向的存储通常是一个聚合体，如数组或结构体：SomeStructure* ptr = lib_function(); /* returns pointer to heap storage */ 2、 然后程序使用所分配的存储。3、 对于清理而言，问题是对 free 的简单调用是否会清理库函数分配的所有堆分配存储。例如，SomeStructure 实例可能有指向堆分配存储的字段。一个特别麻烦的情况是动态分配的结构体数组，每个结构体有一个指向又一层动态分配的存储的字段。下面的代码示例说明了这个问题，并重点关注了如何设计一个可以安全地为客户端提供堆分配存储的库。#include <stdio.h> #include <stdlib.h> typedef struct { unsigned id; unsigned len; float* heap_nums; } HeapStruct; unsigned structId = 1; HeapStruct* get_heap_struct(unsigned n) { /* Try to allocate a HeapStruct. */ HeapStruct* heap_struct = malloc(sizeof(HeapStruct)); if (NULL == heap_struct) /* failure? */ return NULL; /* if so, return NULL */ /* Try to allocate floating-point aggregate within HeapStruct. */ heap_struct->heap_nums = malloc(sizeof(float) * n); if (NULL == heap_struct->heap_nums) { /* failure? */ free(heap_struct); /* if so, first free the HeapStruct */ return NULL; /* then return NULL */ } /* Success: set fields */ heap_struct->id = structId++; heap_struct->len = n; return heap_struct; /* return pointer to allocated HeapStruct */ } void free_all(HeapStruct* heap_struct) { if (NULL == heap_struct) /* NULL pointer? */ return; /* if so, do nothing */ free(heap_struct->heap_nums); /* first free encapsulated aggregate */ free(heap_struct); /* then free containing structure */ } int main() { const unsigned n = 100; HeapStruct* hs = get_heap_struct(n); /* get structure with N floats */ /* Do some (meaningless) work for demo. */ unsigned i; for (i = 0; i < n; i++) hs->heap_nums[i] = 3.14 + (float) i; for (i = 0; i < n; i += 10) printf("%12f\n", hs->heap_nums[i]); free_all(hs); /* free dynamically allocated storage */ return 0; } 上面的 nestedHeap 程序示例以结构体 HeapStruct 为中心，结构体中又有名为 heap_nums 的指针字段：typedef struct { unsigned id; unsigned len; float* heap_nums; /** pointer **/ } HeapStruct; 函数 get_heap_struct 尝试为 HeapStruct 实例分配堆存储，这需要为字段 heap_nums 指向的若干个 float 变量分配堆存储。如果成功调用 get_heap_struct 函数，并将指向堆分配结构体的指针以 hs 命名，其结果可以描述如下：hs-->HeapStruct instance id len heap_nums-->N contiguous float elements 在 get_heap_struct 函数中，第一个堆分配过程很简单：HeapStruct* heap_struct = malloc(sizeof(HeapStruct)); if (NULL == heap_struct) /* failure? */ return NULL; /* if so, return NULL */ sizeof(HeapStruct) 包括了 heap_nums 字段的字节数（32 位机器上为 4，64 位机器上为 8），heap_nums 字段则是指向动态分配数组中的 float 元素的指针。那么，问题关键在于 malloc 为这个结构体传送了字节空间还是表示失败的 NULL；如果是 NULL，get_heap_struct 函数就也返回 NULL 以通知调用者堆分配失败。第二步尝试堆分配的过程更复杂，因为在这一步，HeapStruct 的堆存储已经分配好了：heap_struct->heap_nums = malloc(sizeof(float) * n); if (NULL == heap_struct->heap_nums) { /* failure? */ free(heap_struct); /* if so, first free the HeapStruct */ return NULL; /* and then return NULL */ } 传递给 get_heap_struct 函数的参数 n 指明动态分配的 heap_nums 数组中应该有多少个 float 元素。如果可以分配所需的若干个 float 元素，则该函数在返回 HeapStruct 的堆地址之前会设置结构的 id 和 len 字段。 但是，如果尝试分配失败，则需要两个步骤来实现最优方案：1、 必须释放 HeapStruct 的存储以避免内存泄漏。对于调用 get_heap_struct 的客户端函数而言，没有动态 heap_nums 数组的 HeapStruct 可能就是没用的；因此，HeapStruct 实例的字节空间应该显式释放，以便系统可以回收这些空间用于未来的堆分配。2、 返回 NULL 以标识失败。如果成功调用 get_heap_struct 函数，那么释放堆存储也很棘手，因为它涉及要以正确顺序进行的两次 free 操作。因此，该程序设计了一个 free_all 函数，而不是要求程序员再去手动实现两步释放操作。回顾一下，free_all 函数是这样的：void free_all(HeapStruct* heap_struct) { if (NULL == heap_struct) /* NULL pointer? */ return; /* if so, do nothing */ free(heap_struct->heap_nums); /* first free encapsulated aggregate */ free(heap_struct); /* then free containing structure */ } 检查完参数 heap_struct 不是 NULL 值后，函数首先释放 heap_nums 数组，这步要求 heap_struct 指针此时仍然是有效的。先释放 heap_struct 的做法是错误的。一旦 heap_nums 被释放，heap_struct 就可以释放了。如果 heap_struct 被释放，但 heap_nums 没有被释放，那么数组中的 float 元素就会泄漏：仍然分配了字节空间，但无法被访问到——因此一定要记得释放 heap_nums。存储泄漏将一直持续，直到 nestedHeap 程序退出，系统回收泄漏的字节时为止。关于 free 库函数的注意事项就是要有顺序。回想一下上面的调用示例：free(heap_struct->heap_nums); /* first free encapsulated aggregate */ free(heap_struct); /* then free containing structure */ 这些调用释放了分配的存储空间——但它们并 不是 将它们的操作参数设置为 NULL（free 函数会获取地址的副本作为参数；因此，将副本更改为 NULL 并不会改变原地址上的参数值）。例如，在成功调用 free 之后，指针 heap_struct 仍然持有一些堆分配字节的堆地址，但是现在使用这个地址将会产生错误，因为对 free 的调用使得系统有权回收然后重用这些分配过的字节。使用 NULL 参数调用 free 没有意义，但也没有什么坏处。而在非 NULL 的地址上重复调用 free 会导致不确定结果的错误：free(heap_struct); /* 1st call: ok */ free(heap_struct); /* 2nd call: ERROR */ 内存泄漏和堆碎片化“内存泄漏”是指动态分配的堆存储变得不再可访问。看一下相关的代码段：float* nums = malloc(sizeof(float) * 10); /* 10 floats */ nums[0] = 3.14f; /* and so on */ nums = malloc(sizeof(float) * 25); /* 25 new floats */ 假如第一个 malloc 成功，第二个 malloc 会再将 nums 指针重置为 NULL（分配失败情况下）或是新分配的 25 个 float 中第一个的地址。最初分配的 10 个 float 元素的堆存储仍然处于被分配状态，但此时已无法再对其访问，因为 nums 指针要么指向别处，要么是 NULL。结果就是造成了 40 个字节（sizeof(float) * 10）的泄漏。在第二次调用 malloc 之前，应该释放最初分配的存储空间：float* nums = malloc(sizeof(float) * 10); /* 10 floats */ nums[0] = 3.14f; /* and so on */ free(nums); /** good **/ nums = malloc(sizeof(float) * 25); /* no leakage */ 即使没有泄漏，堆也会随着时间的推移而碎片化，需要对系统进行碎片整理。例如，假设两个最大的堆块当前的大小分别为 200MB 和 100MB。然而，这两个堆块并不连续，进程 P 此时又需要分配 250MB 的连续堆存储。在进行分配之前，系统可能要对堆进行 碎片整理 以给 P 提供 250MB 连续存储空间。碎片整理很复杂，因此也很耗时。内存泄漏会创建处于已分配状态但不可访问的堆块，从而会加速碎片化。因此，释放不再需要的堆存储是程序员帮助减少碎片整理需求的一种方式。诊断内存泄漏的工具有很多工具可用于分析内存效率和安全性，其中我最喜欢的是 valgrind。为了说明该工具如何处理内存泄漏，这里给出 leaky 示例程序：#include <stdio.h> #include <stdlib.h> int* get_ints(unsigned n) { int* ptr = malloc(n * sizeof(int)); if (ptr != NULL) { unsigned i; for (i = 0; i < n; i++) ptr[i] = i + 1; } return ptr; } void print_ints(int* ptr, unsigned n) { unsigned i; for (i = 0; i < n; i++) printf("%3i\n", ptr[i]); } int main() { const unsigned n = 32; int* arr = get_ints(n); if (arr != NULL) print_ints(arr, n); /** heap storage not yet freed... **/ return 0; } main 函数调用了 get_ints 函数，后者会试着从堆中 malloc 32 个 4 字节的 int，然后初始化动态数组（如果 malloc 成功）。初始化成功后，main 函数会调用 print_ints函数。程序中并没有调用 free 来对应 malloc 操作；因此，内存泄漏了。如果安装了 valgrind 工具箱，下面的命令会检查 leaky 程序是否存在内存泄漏（% 是命令行提示符）：% valgrind --leak-check=full ./leaky 绝大部分输出都在下面给出了。左边的数字 207683 是正在执行的 leaky 程序的进程标识符。这份报告给出了泄漏发生位置的详细信息，本例中位置是在 main 函数所调用的 get_ints 函数中对 malloc 的调用处。==207683== HEAP SUMMARY: ==207683== in use at exit: 128 bytes in 1 blocks ==207683== total heap usage: 2 allocs, 1 frees, 1,152 bytes allocated ==207683== ==207683== 128 bytes in 1 blocks are definitely lost in loss record 1 of 1 ==207683== at 0x483B7F3: malloc (in /usr/lib/x86_64-linux-gnu/valgrind/vgpreload_memcheck-amd64-linux.so) ==207683== by 0x109186: get_ints (in /home/marty/gc/leaky) ==207683== by 0x109236: main (in /home/marty/gc/leaky) ==207683== ==207683== LEAK SUMMARY: ==207683== definitely lost: 128 bytes in 1 blocks ==207683== indirectly lost: 0 bytes in 0 blocks ==207683== possibly lost: 0 bytes in 0 blocks ==207683== still reachable: 0 bytes in 0 blocks ==207683== suppressed: 0 bytes in 0 blocks 如果把 main 函数改成在对 print_ints 的调用之后，再加上一个对 free 的调用，valgrind 就会对 leaky 程序给出一个干净的内存健康清单：==218462== All heap blocks were freed -- no leaks are possible 静态区存储在正统的 C 语言中，函数必须在所有块之外定义。这是一些 C 编译器支持的特性，杜绝了在另一个函数体内定义一个函数的可能。我举的例子都是在所有块之外定义的函数。这样的函数要么是 static ，即静态的，要么是 extern，即外部的，其中 extern 是默认值。C 语言中，以 static 或 extern 修饰的函数和变量驻留在内存中所谓的 静态区 中，因为在程序执行期间该区域大小是固定不变的。这两个存储类型的语法非常复杂，我们应该回顾一下。在回顾之后，会有一个完整的代码示例来生动展示语法细节。在所有块之外定义的函数或变量默认为 extern；因此，函数和变量要想存储类型为 static ，必须显式指定：/** file1.c: outside all blocks, five definitions **/ int foo(int n) { return n * 2; } /* extern by default */ static int bar(int n) { return n; } /* static */ extern int baz(int n) { return -n; } /* explicitly extern */ int num1; /* extern */ static int num2; /* static */ extern 和 static 的区别在于作用域：extern 修饰的函数或变量可以实现跨文件可见（需要声明）。相比之下，static 修饰的函数仅在 定义 该函数的文件中可见，而 static 修饰的变量仅在 定义 该变量的文件（或文件中的块）中可见：static int n1; /* scope is the file */ void func() { static int n2; /* scope is func's body */ ... } 如果在所有块之外定义了 static 变量，例如上面的 n1，该变量的作用域就是定义变量的文件。无论在何处定义 static 变量，变量的存储都在内存的静态区中。extern 函数或变量在给定文件中的所有块之外定义，但这样定义的函数或变量也可以在其他文件中声明。典型的做法是在头文件中 声明 这样的函数或变量，只要需要就可以包含进来。下面这些简短的例子阐述了这些棘手的问题。假设 extern 函数 foo 在 file1.c 中 定义，有无关键字 extern 效果都一样：/** file1.c **/ int foo(int n) { return n * 2; } /* definition has a body {...} */ 必须在其他文件（或其中的块）中使用显式的 extern 声明 此函数才能使其可见。以下是使 extern 函数 foo 在文件 file2.c 中可见的声明语句：/** file2.c: make function foo visible here **/ extern int foo(int); /* declaration (no body) */ 回想一下，函数声明没有用大括号括起来的主体，而函数定义会有这样的主体。为了便于查看，函数和变量声明通常会放在头文件中。准备好需要声明的源代码文件，然后就可以 #include 相关的头文件。下一节中的 staticProg 程序演示了这种方法。至于 extern 的变量，规则就变得更棘手了（很抱歉增加了难度！）。任何 extern 的对象——无论函数或变量——必须 定义 在所有块之外。此外，在所有块之外定义的变量默认为 extern：/** outside all blocks **/ int n; /* defaults to extern */ 但是，只有在变量的 定义 中显式初始化变量时，extern 才能在变量的 定义 中显式修饰（LCTT 译注：换言之，如果下列代码中的 int n1; 行前加上 extern，该行就由 定义 变成了 声明）：/** file1.c: outside all blocks **/ int n1; /* defaults to extern, initialized by compiler to zero */ extern int n2 = -1; /* ok, initialized explicitly */ int n3 = 9876; /* ok, extern by default and initialized explicitly */ 要使在 file1.c 中定义为 extern 的变量在另一个文件（例如 file2.c）中可见，该变量必须在 file2.c 中显式 声明 为 extern 并且不能初始化（初始化会将声明转换为定义）：/** file2.c **/ extern int n1; /* declaration of n1 defined in file1.c */ 为了避免与 extern 变量混淆，经验是在 声明 中显式使用 extern（必须），但不要在 定义 中使用（非必须且棘手）。对于函数，extern 在定义中是可选使用的，但在声明中是必须使用的。下一节中的 staticProg 示例会把这些点整合到一个完整的程序中。staticProg 示例staticProg 程序由三个文件组成：两个 C 语言源文件（static1.c 和 static2.c）以及一个头文件（static.h），头文件中包含两个声明：/** header file static.h **/ #define NumCount 100 /* macro */ extern int global_nums[NumCount]; /* array declaration */ extern void fill_array(); /* function declaration */ 两个声明中的 extern，一个用于数组，另一个用于函数，强调对象在别处（“外部”）定义：数组 global_nums 在文件 static1.c 中定义（没有显式的 extern），函数 fill_array 在文件 static2.c 中定义（也没有显式的 extern）。每个源文件都包含了头文件 static.h。static1.c 文件定义了两个驻留在内存静态区域中的数组（global_nums 和 more_nums）。第二个数组有 static 修饰，这将其作用域限制为定义数组的文件 (static1.c)。如前所述， extern 修饰的 global_nums 则可以实现在多个文件中可见。/** static1.c **/ #include <stdio.h> #include <stdlib.h> #include "static.h" /* declarations */ int global_nums[NumCount]; /* definition: extern (global) aggregate */ static int more_nums[NumCount]; /* definition: scope limited to this file */ int main() { fill_array(); /** defined in file static2.c **/ unsigned i; for (i = 0; i < NumCount; i++) more_nums[i] = i * -1; /* confirm initialization worked */ for (i = 0; i < NumCount; i += 10) printf("%4i\t%4i\n", global_nums[i], more_nums[i]); return 0; } 下面的 static2.c 文件中定义了 fill_array 函数，该函数由 main（在 static1.c 文件中）调用；fill_array 函数会给名为 global_nums 的 extern 数组中的元素赋值，该数组在文件 static1.c 中定义。使用两个文件的唯一目的是凸显 extern 变量或函数能够跨文件可见。/** static2.c **/ #include "static.h" /** declarations **/ void fill_array() { /** definition **/ unsigned i; for (i = 0; i < NumCount; i++) global_nums[i] = i + 2; } staticProg 程序可以用如下编译：% gcc -o staticProg static1.c static2.c 从汇编语言看更多细节现代 C 编译器能够处理 C 和汇编语言的任意组合。编译 C 源文件时，编译器首先将 C 代码翻译成汇编语言。这是对从上文 static1.c 文件生成的汇编语言进行保存的命令：% gcc -S static1.c 生成的文件就是 static1.s。这是文件顶部的一段代码，额外添加了行号以提高可读性： .file "static1.c" ## line 1 .text ## line 2 .comm global_nums,400,32 ## line 3 .local more_nums ## line 4 .comm more_nums,400,32 ## line 5 .section .rodata ## line 6 .LC0: ## line 7 .string "%4i\t%4i\n" ## line 8 .text ## line 9 .globl main ## line 10 .type main, @function ## line 11 main: ## line 12 ... 诸如 .file（第 1 行）之类的汇编语言指令以句点开头。顾名思义，指令会指导汇编程序将汇编语言翻译成机器代码。.rodata 指令（第 6 行）表示后面是只读对象，包括字符串常量 "%4i\t%4i\n"（第 8 行），main 函数（第 12 行）会使用此字符串常量来实现格式化输出。作为标签引入（通过末尾的冒号实现）的 main 函数（第 12 行），同样也是只读的。在汇编语言中，标签就是地址。标签 main:（第 12 行）标记了 main 函数代码开始的地址，标签 .LC0:（第 7 行）标记了格式化字符串开头所在的地址。global_nums（第 3 行）和 more_nums（第 4 行）数组的定义包含了两个数字：400 是每个数组中的总字节数，32 是每个数组（含 100 个 int 元素）中每个元素的比特数。（第 5 行中的 .comm 指令表示 common name，可以忽略。）两个数组定义的不同之处在于 more_nums 被标记为 .local（第 4 行），这意味着其作用域仅限于其所在文件 static1.s。相比之下，global_nums 数组就能在多个文件中实现可见，包括由 static1.c 和 static2.c 文件翻译成的汇编文件。最后，.text 指令在汇编代码段中出现了两次（第 2 行和第 9 行）。术语“text”表示“只读”，但也会涵盖一些读/写变量，例如两个数组中的元素。尽管本文展示的汇编语言是针对 Intel 架构的，但 Arm6 汇编也非常相似。对于这两种架构，.text 区域中的变量（本例中为两个数组中的元素）会自动初始化为零。总结C 语言中的内存高效和内存安全编程准则很容易说明，但可能会很难遵循，尤其是在调用设计不佳的库的时候。准则如下：尽可能使用栈存储，进而鼓励编译器将通用寄存器用作暂存器，实现优化。栈存储代表了高效的内存使用并促进了代码的整洁和模块化。永远不要返回指向基于栈的存储的指针。小心使用堆存储。C（和 C++）中的重难点是确保动态分配的存储尽快解除分配。良好的编程习惯和工具（如 valgrind）有助于攻关这些重难点。优先选用自身提供释放函数的库，例如 nestedHeap 代码示例中的 free_all 释放函数。谨慎使用静态存储，因为这种存储会自始至终地影响进程的内存占用。特别是尽量避免使用 extern 和 static 数组。本文 C 语言代码示例可在我的网站（https://condor.depaul.edu/mkalin）上找到。via: https://opensource.com/article/21/8/memory-programming-c作者：Marty Kalin 选题：lujun9972 译者：unigeorge 校对：wxy本文由 LCTT 原创编译，Linux中国 荣誉推出
软件库是重复使用代码的一种简单而合理的方式。软件库是一种是一直以来长期存在的、简单合理的复用代码的方式。这篇文章解释了如何从头开始构建库并使得其可用。尽管这两个示例库都以 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 将一个函数的的范围限制到函数被定义的文件中。在示例库中，工具函数 gcd 是静态的（static）：static unsigned gcd(unsigned n1, unsigned n2) { ... } 只有在 primes.c 文件中的函数可以调用 gcd，而只有 are_coprimes 函数会调用它。当静态库和动态库被构建和发布后，其他的程序可以调用外部的（extern）函数，如 are_coprimes ，但是不可以调用静态（static）函数 gcd。静态（static）存储类通过将函数范围限制在其他库函数内，进而实现了对库的客户程序隐藏 gcd 函数。在 primes.c 文件中除了 gcd 函数外，其他函数并没有指明存储类，默认将会设置为外部的（extern）。然而，在库中显式注明 extern 更加常见。C 语言区分了函数的 定义 definition 和 声明 declaration ，这对库来说很重要。接下来让我们开始了解定义。C 语言仅允许命名函数不允许匿名函数，并且每个函数需要定义以下内容：一个唯一的名字。一个程序不允许存在两个同名的函数。一个可以为空的参数列表。参数需要指明类型。一个返回值类型（例如：int 代表 32 位有符号整数），当没有返回值时设置为空类型（void）。用一对花括号包围起来的函数主体部分。在一个特制的示例中，函数主体部分可以为空。程序中的每个函数必须要被定义一次。下面是库函数 are_coprimes 的完整定义：extern unsigned are_coprimes(unsigned n1, unsigned n2) { /* 定义 */ return 1 == gcd(n1, n2); /* 最大公约数是否为 1? */ } 函数返回一个布尔值（0 代表假，1 代表真），取决于两个整数参数值的最大公约数是否为 1。工具函数 gcd 计算两个整数参数 n1 和 n2 的最大公约数。函数声明不同于定义，其不需要主体部分：extern unsigned are_coprimes(unsigned n1, unsigned n2); /* 声明 */ 声明在参数列表后用一个分号代表结束，它没有被花括号包围起来的主体部分。程序中的函数可以被多次声明。为什么需要声明？在 C 语言中，一个被调用的函数必须对其调用者可见。有多种方式可以提供这样的可见性，具体依赖于编译器如何实现。一个必然可行的方式就是当它们二者位于同一个文件中时，将被调用的函数定义在在它的调用者之前。void f() {...} /* f 定义在其被调用前 */ void g() { f(); } /* ok */ 当函数 f 被在调用前声明，此时函数 f 的定义可以移动到函数 g 的下方。void f(); /* 声明使得函数 f 对调用者可见 */ void g() { f(); } /* ok */ void f() {...} /* 相较于前一种方式，此方式显得更简洁 */ 但是当如果一个被调用的函数和调用它的函数不在同一个文件中时呢？因为前文提到一个函数在一个程序中需要被定义一次，那么如何使得让一个文件中被定义的函数在另一个文件中可见？这个问题会影响库，无论是静态库还是动态库。例如在这两个质数库中函数被定义在源文件 primes.c 中，每个库中都有该函数的二进制副本，但是这些定义的函数必须要对使用库的 C 程序可见，该 C 程序有其自身的源文件。函数声明可以帮助提供跨文件的可见性。对于上述的“质数”例子，它有一个名为 primes.h 的头文件，其声明了四个函数使得它们对使用库的 C 程序可见。/** 头文件 primes.h：函数声明 **/ extern unsigned is_prime(unsigned); extern void prime_factors(unsigned); extern unsigned are_coprimes(unsigned, unsigned); extern void goldbach(unsigned); 这些声明通过为每个函数指定其调用语法来作为接口。为了客户程序的便利性，头文件 primes.h 应该存储在 C 编译器查找路径下的目录中。典型的位置有 /usr/include 和 /usr/local/include。一个 C 语言客户程序应使用 #include 包含这个头文件，并尽可能将这条语句其程序源代码的首部（头文件将会被导入另一个源文件的“头”部）。C 语言头文件可以被导入其他语言（如 Rust 语言）中的 bindgen，使其它语言的客户程序可以访问 C 语言的库。总之，一个库函数只可以被定义一次，但可以在任何需要它的地方进行声明，任一使用 C 语言库的程序都需要该声明。头文件可以包含函数声明，但不能包含函数定义。如果头文件包含了函数定义，那么该文件可能会在一个 C 语言程序中被多次包含，从而破坏了一个函数在 C 语言程序中必须被精确定义一次的规则。库的源代码下面是两个库的源代码。这部分代码、头文件、以及两个示例客户程序都可以在 我的网页 上找到。#include <stdio.h> #include <math.h> extern unsigned is_prime(unsigned n) { if (n <= 3) return n > 1; /* 2 和 3 是质数 */ if (0 == (n % 2) || 0 == (n % 3)) return 0; /* 2 和 3 的倍数不会是质数 */ /* 检查 n 是否是其他 < n 的值的倍数 */ unsigned i; for (i = 5; (i * i) <= n; i += 6) if (0 == (n % i) || 0 == (n % (i + 2))) return 0; /* 不是质数 */ return 1; /* 一个不是 2 和 3 的质数 */ } extern void prime_factors(unsigned n) { /* 在数字 n 的质因数分解中列出所有 2 */ while (0 == (n % 2)) { printf("%i ", 2); n /= 2; } /* 数字 2 已经处理完成，下面处理奇数 */ unsigned i; for (i = 3; i <= sqrt(n); i += 2) { while (0 == (n % i)) { printf("%i ", i); n /= i; } } /* 还有其他质因数？*/ if (n > 2) printf("%i", n); } /* 工具函数：计算最大公约数 */ static unsigned gcd(unsigned n1, unsigned n2) { while (n1 != 0) { unsigned n3 = n1; n1 = n2 % n1; n2 = n3; } return n2; } extern unsigned are_coprimes(unsigned n1, unsigned n2) { return 1 == gcd(n1, n2); } extern void goldbach(unsigned n) { /* 输入错误 */ if ((n <= 2) || ((n & 0x01) > 0)) { printf("Number must be > 2 and even: %i is not.\n", n); return; } /* 两个简单的例子：4 和 6 */ if ((4 == n) || (6 == n)) { printf("%i = %i + %i\n", n, n / 2, n / 2); return; } /* 当 n > 8 时，存在多种可能性 */ unsigned i; for (i = 3; i < (n / 2); i++) { if (is_prime(i) && is_prime(n - i)) { printf("%i = %i + %i\n", n, i, n - i); /* 如果只需要一对，那么用 break 语句替换这句 */ } } } 库函数这些函数可以被库利用。两个库可以从相同的源代码中获得，同时头文件 primes.h 是两个库的 C 语言接口。构建库静态库和动态库在构建和发布的步骤上有一些细节的不同。静态库需要三个步骤，而动态库需要增加两个步骤即一共五个步骤。额外的步骤表明了动态库的动态方法具有更多的灵活性。让我们先从静态库开始。库的源文件 primes.c 被编译成一个目标模块。下面是命令，百分号 % 代表系统提示符，两个井字符 # 是我的注释。% gcc -c primes.c ## 步骤1（静态） 这一步生成目标模块是二进制文件 primes.o。-c 标志意味着只编译。下一步是使用 Linux 的 ar 命令将目标对象归档。% ar -cvq libprimes.a primes.o ## 步骤2（静态） -cvq 三个标识分别是“创建”、“详细的”、“快速添加”（以防新文件没有添加到归档中）的简称。回忆一下，前文提到过前缀 lib 是必须的，而库名是任意的。当然，库的文件名必须是唯一的，以避免冲突。归档已经准备好要被发布：% sudo cp libprimes.a /usr/local/lib ## 步骤3（静态） 现在静态库对接下来的客户程序是可见的，示例在后面。（包含 sudo 可以确保有访问权限将文件复制进 /usr/local/lib 目录中）动态库还需要一个或多个对象模块进行打包：% gcc primes.c -c -fpic ## 步骤1（动态） 增加的选项 -fpic 指示编译器生成与位置无关的代码，这意味着不需要将该二进制模块加载到一个固定的内存位置。在一个拥有多个动态库的系统中这种灵活性是至关重要的。生成的对象模块会略大于静态库生成的对象模块。下面是从对象模块创建单个库文件的命令：% gcc -shared -Wl,-soname,libshprimes.so -o libshprimes.so.1 primes.o ## 步骤2（动态） 选项 -shared 表明了该库是一个共享的（动态的）而不是静态的。-Wl 选项引入了一系列编译器选项，第一个便是设置动态库的 -soname，这是必须设置的。soname 首先指定了库的逻辑名字（libshprimes.so），接下来的 -o 选项指明了库的物理文件名字（libshprimes.so.1）。这样做的目的是为了保持逻辑名不变的同时允许物理名随着新版本而发生变化。在本例中，在物理文件名 libshprimes.so.1 中最后的 1 代表是第一个库的版本。尽管逻辑文件名和物理文件名可以是相同的，但是最佳做法是将它们命名为不同的名字。一个客户程序将会通过逻辑名（本例中为 libshprimes.so）来访问库，稍后我会进一步解释。接下来的一步是通过复制共享库到合适的目录下使得客户程序容易访问，例如 /usr/local/lib 目录：% sudo cp libshprimes.so.1 /usr/local/lib ## 步骤3（动态） 现在在共享库的逻辑名（libshprimes.so）和它的物理文件名（/usr/local/lib/libshprimes.so.1）之间设置一个符号链接。最简单的方式是将 /usr/local/lib 作为工作目录，在该目录下输入命令：% sudo ln --symbolic libshprimes.so.1 libshprimes.so ## 步骤4（动态） 逻辑名 libshprimes.so 不应该改变，但是符号链接的目标（libshrimes.so.1）可以根据需要进行更新，新的库实现可以是修复了 bug，提高性能等。最后一步（一个预防措施）是调用 ldconfig 工具来配置系统的动态加载器。这个配置保证了加载器能够找到新发布的库。% sudo ldconfig ## 步骤5（动态） 到现在，动态库已为包括下面的两个在内的示例客户程序准备就绪了。一个使用库的 C 程序这个示例 C 程序是一个测试程序，它的源代码以两条 #include 指令开始：#include <stdio.h> /* 标准输入/输出函数 */ #include <primes.h> /* 我的库函数 */ 文件名两边的尖括号表示可以在编译器的搜索路径中找到这些头文件（对于 primes.h 文件来说在 /usr/local/inlcude 目录下）。如果不包含 #include，编译器会抱怨缺少 is_prime 和 prime_factors 等函数的声明，它们在两个库中都有发布。顺便提一句，测试程序的源代码不需要更改即可测试两个库中的每一个库。相比之下，库的源文件（primes.c）使用 #include 指令打开以下头文件：#include <stdio.h> #include <math.h> math.h 头文件是必须的，因为库函数 prime_factors 会调用数学函数 sqrt，其在标准库 libm.so 中。作为参考，这是测试库程序的源代码：#include <stdio.h> #include <primes.h> int main() { /* 是质数 */ printf("\nis_prime\n"); unsigned i, count = 0, n = 1000; for (i = 1; i <= n; i++) { if (is_prime(i)) { count++; if (1 == (i % 100)) printf("Sample prime ending in 1: %i\n", i); } } printf("%i primes in range of 1 to a thousand.\n", count); /* prime_factors */ printf("\nprime_factors\n"); printf("prime factors of 12: "); prime_factors(12); printf("\n"); printf("prime factors of 13: "); prime_factors(13); printf("\n"); printf("prime factors of 876,512,779: "); prime_factors(876512779); printf("\n"); /* 是合数 */ printf("\nare_coprime\n"); printf("Are %i and %i coprime? %s\n", 21, 22, are_coprimes(21, 22) ? "yes" : "no"); printf("Are %i and %i coprime? %s\n", 21, 24, are_coprimes(21, 24) ? "yes" : "no"); /* 哥德巴赫 */ printf("\ngoldbach\n"); goldbach(11); /* error */ goldbach(4); /* small one */ goldbach(6); /* another */ for (i = 100; i <= 150; i += 2) goldbach(i); return 0; } 测试程序在编译 tester.c 文件到可执行文件时，难处理的部分时链接选项的顺序。回想前文中提到两个示例库都是用 lib 作为前缀开始，并且每一个都有一个常规的拓展后缀：.a 代表静态库 libprimes.a，.so 代表动态库 libshprimes.so。在链接规范中，前缀 lib 和拓展名被忽略了。链接标志以 -l （小写 L）开始，并且一条编译命令可能包含多个链接标志。下面是一个完整的测试程序的编译指令，使用动态库作为示例：% gcc -o tester tester.c -lshprimes -lm 第一个链接标志指定了库 libshprimes.so，第二个链接标志指定了标准数学库 libm.so。链接器是懒惰的，这意味着链接标志的顺序是需要考虑的。例如，调整上述实例中的链接顺序将会产生一个编译时错误：% gcc -o tester tester.c -lm -lshprimes ## 危险！ 链接 libm.so 库的标志先出现，但是这个库中没有函数被测试程序显式调用；因此，链接器不会链接到 math.so 库。调用 sqrt 库函数仅发生在 libshprimes.so 库中包含的 prime_factors 函数。编译测试程序返回的错误是：primes.c: undefined reference to 'sqrt' 因此，链接标志的顺序应该是通知链接器需要 sqrt 函数：% gcc -o tester tester.c -lshprimes -lm ## 首先链接 -lshprimes 链接器在 libshprimes.so 库中发现了对库函数 sqrt 的调用，所以接下来对数学库 libm.so做了合适的链接。链接还有一个更复杂的选项，它支持链接的标志顺序。然而在本例中，最简单的方式就是恰当地排列链接标志。下面是运行测试程序的部分输出结果：is_prime Sample prime ending in 1: 101 Sample prime ending in 1: 401 ... 168 primes in range of 1 to a thousand. prime_factors prime factors of 12: 2 2 3 prime factors of 13: 13 prime factors of 876,512,779: 211 4154089 are_coprime Are 21 and 22 coprime? yes Are 21 and 24 coprime? no goldbach Number must be > 2 and even: 11 is not. 4 = 2 + 2 6 = 3 + 3 ... 32 = 3 + 29 32 = 13 + 19 ... 100 = 3 + 97 100 = 11 + 89 ... 对于 goldbach 函数，即使一个相当小的偶数值（例如 18）也许存在多个一对质数之和的组合（在这种情况下，5+13 和 7+11）。因此这种多个质数对是使得尝试证明哥德巴赫猜想变得复杂的因素之一。封装使用库的 Python 程序与 C 不同，Python 不是一个静态编译语言，这意味着 Python 客户示例程序必须访问动态版本而非静态版本的 primes 库。为了能这样做，Python 中有众多的支持 外部语言接口 foreign function interface （FFI）的模块（标准的或第三方的），它们允许用一种语言编写的程序来调用另一种语言编写的函数。Python 中的 ctypes 是一个标准的、相对简单的允许 Python 代码调用 C 函数的 FFI。任何 FFI 都面临挑战，因为对接的语言不大可能会具有完全相同的数据类型。例如：primes 库使用 C 语言类型 unsigned int，而 Python 并不具有这种类型；因此 ctypes FFI 将 C 语言中的 unsigned int 类型映射为 Python 中的 int 类型。在 primes 库中发布的四个 extern C 函数中，有两个在具有显式 ctypes 配置的 Python 中会表现得更好。C 函数 prime_factors 和 goldbach 返回 void 而不是返回一个具体类型，但是 ctypes 默认会将 C 语言中的 void 替换为 Python 语言中的 int。当从 Python 代码中调用时，这两个 C 函数会从栈中返回一个随机整数值（因此，该值无任何意义）。然而，可以对 ctypes 进行配置，让这些函数返回 None （Python 中为 null 类型）。下面是对 prime_factors 函数的配置：primes.prime_factors.restype = None 可以用类似的语句处理 goldbach 函数。下面的交互示例（在 Python3 中）展示了在 Python 客户程序和 primes 库之间的接口是简单明了的。>>> from ctypes import cdll >>> primes = cdll.LoadLibrary("libshprimes.so") ## 逻辑名 >>> primes.is_prime(13) 1 >>> primes.is_prime(12) 0 >>> primes.are_coprimes(8, 24) 0 >>> primes.are_coprimes(8, 25) 1 >>> primes.prime_factors.restype = None >>> primes.goldbach.restype = None >>> primes.prime_factors(72) 2 2 2 3 3 >>> primes.goldbach(32) 32 = 3 + 29 32 = 13 + 19 在 primes 库中的函数只使用一个简单数据类型：unsigned int。如果这个 C 语言库使用复杂的类型如结构体，如果库函数传递和返回指向结构体的指针，那么比 ctypes 更强大的 FFI 更适合作为一个在 Python 语言和 C 语言之间的平滑接口。尽管如此，ctypes 示例展示了一个 Python 客户程序可以使用 C 语言编写的库。值得注意的是，用作科学计算的流行的 Numpy 库是用 C 语言编写的，然后在高级 Python API 中公开。简单的 primes 库和高级的 Numpy 库强调了 C 语言仍然是编程语言中的通用语言。几乎每一个语言都可以与 C 语言交互，同时通过 C 语言也可以和任何其他语言交互。Python 很容易和 C 语言交互，作为另外一个例子，当 Panama 项目 成为 Java Native Interface（JNI）一个替代品后，Java 语言和 C 语言交互也会变的很容易。via: https://opensource.com/article/21/2/linux-software-libraries作者：Marty Kalin 选题：lujun9972 译者：萌新阿岩 校对：wxy本文由 LCTT 原创编译，Linux中国 荣誉推出
通过管理一套图书的完整代码示例，来探索轻量级的 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 启动后，在浏览器中输入 http://localhost:8080/可以查看详细文档，包括示例。TOMCAT_HOME/webapps 目录是已部署的 Web 网站和服务的默认目录。部署网站或 Web 服务的直接方法是复制以 .war 结尾的 JAR 文件（也就是 WAR 文件）到 TOMCAT_HOME/webapps 或它的子目录下。然后 Tomcat 会将 WAR 文件解压到它自己的目录下。比如，Tomcat 会将 novels.war 文件解压到一个叫做 novels 的子目录下，并且保留 novels.war 文件。一个网站或 Web 服务可以通过删除 WAR 文件进行移除，也可以用一个新版 WAR 文件来覆盖已有文件进行更新。顺便说一下，调试网站或服务的第一步就是检查 Tomcat 已经正确解压 WAR 文件；如果没有的话，网站或服务就无法发布，因为代码或配置中有致命错误。因为 Tomcat 默认会监听 8080 端口上的 HTTP 请求，所以本机上的 URL 请求以 http://localhost:8080/ 开始。通过添加不带 .war 后缀的 WAR 文件名来访问由程序员部署的 WAR 文件：http://locahost:8080/novels/ 如果服务部署在 TOMCAT_HOME 下的一个子目录中（比如，myapps），这会在 URL 中反映出来：http://locahost:8080/myapps/novels/ 我会在靠近文章结尾处的测试部分提供这部分的更多细节。如前所述，我的主页上有一个包含 Ant 脚本的 ZIP 文件，这个文件可以编译并且部署网站或者服务。（这个 ZIP 文件中也包含一个 novels.war 的副本。）对于“小说”这个例子，命令的示例（% 是命令行提示符）如下：% ant -Dwar.name=novels deploy 这个命令首先会编译 Java 源代码，并且创建一个可部署的 novels.war 文件，然后将这个文件保存在当前目录中，再复制到 TOMCAT_HOME/webapps 目录中。如果一切顺利，GET 请求（使用浏览器或者命令行工具，比如 curl）可以用来做一个测试：% curl http://localhost:8080/novels/ 默认情况下，Tomcat 设置为 热部署 hot deploys ：Web 服务器不需要关闭就可以进行部署、更新或者移除一个 web 应用。“小说”服务的代码让我们回到“小说”这个例子，不过是在代码层面。考虑下面的 Novel 类：例 1：Novel 类package novels; import java.io.Serializable; public class Novel implements Serializable, Comparable<Novel> { static final long serialVersionUID = 1L; private String author; private String title; private int id; public Novel() { } public void setAuthor(final String author) { this.author = author; } public String getAuthor() { return this.author; } public void setTitle(final String title) { this.title = title; } public String getTitle() { return this.title; } public void setId(final int id) { this.id = id; } public int getId() { return this.id; } public int compareTo(final Novel other) { return this.id - other.id; } } 这个类实现了 Comparable 接口中的 compareTo 方法，因为 Novel 实例是存储在一个线程安全的无序 ConcurrentHashMap 中。在响应查看集合的请求时，“小说”服务会对从映射中提取的集合（一个 ArrayList）进行排序；compareTo 的实现通过 Novel 的 ID 将它按升序排序。Novels 类中包含多个实用工具函数：例 2：Novels 实用工具类package novels; import java.io.IOException; import java.io.File; import java.io.ByteArrayOutputStream; import java.io.InputStream; import java.io.InputStreamReader; import java.io.BufferedReader; import java.nio.file.Files; import java.util.stream.Stream; import java.util.concurrent.ConcurrentMap; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.atomic.AtomicInteger; import java.util.Collections; import java.beans.XMLEncoder; import javax.servlet.ServletContext; // not in JavaSE import org.json.JSONObject; import org.json.XML; public class Novels { private final String fileName = "/WEB-INF/data/novels.db"; private ConcurrentMap<Integer, Novel> novels; private ServletContext sctx; private AtomicInteger mapKey; public Novels() { novels = new ConcurrentHashMap<Integer, Novel>(); mapKey = new AtomicInteger(); } public void setServletContext(ServletContext sctx) { this.sctx = sctx; } public ServletContext getServletContext() { return this.sctx; } public ConcurrentMap<Integer, Novel> getConcurrentMap() { if (getServletContext() == null) return null; // not initialized if (novels.size() < 1) populate(); return this.novels; } public String toXml(Object obj) { // default encoding String xml = null; try { ByteArrayOutputStream out = new ByteArrayOutputStream(); XMLEncoder encoder = new XMLEncoder(out); encoder.writeObject(obj); encoder.close(); xml = out.toString(); } catch(Exception e) { } return xml; } public String toJson(String xml) { // option for requester try { JSONObject jobt = XML.toJSONObject(xml); return jobt.toString(3); // 3 is indentation level } catch(Exception e) { } return null; } public int addNovel(Novel novel) { int id = mapKey.incrementAndGet(); novel.setId(id); novels.put(id, novel); return id; } private void populate() { InputStream in = sctx.getResourceAsStream(this.fileName); // Convert novel.db string data into novels. if (in != null) { try { InputStreamReader isr = new InputStreamReader(in); BufferedReader reader = new BufferedReader(isr); String record = null; while ((record = reader.readLine()) != null) { String[] parts = record.split("!"); if (parts.length == 2) { Novel novel = new Novel(); novel.setAuthor(parts[0]); novel.setTitle(parts[1]); addNovel(novel); // sets the Id, adds to map } } in.close(); } catch (IOException e) { } } } } 最复杂的方法是 populate，这个方法从一个包含在 WAR 文件中的文本文件读取。这个文本文件包括了“小说”的初始集合。要打开此文件，populate 方法需要 ServletContext，这是一个 Java 映射类型，包含了关于嵌入在 servlet 容器中的 servlet 的所有关键信息。这个文本文件有包含了像下面这样的记录：Jane Austen!Persuasion 这一行被解析为两部分（作者和标题），由感叹号（!）分隔。然后这个方法创建一个 Novel 实例，设置作者和标题属性，并且将“小说”加到容器中，保存在内存中。Novels 类也有一些实用工具函数，可以将“小说”容器编码为 XML 或 JSON，取决于发出请求的人所要求的格式。默认是 XML 格式，但是也可以请求 JSON 格式。一个轻量级的 XML 转 JSON 包提供了 JSON。下面是关于编码的更多细节。例 3：NovelsServlet 类package novels; import java.util.concurrent.ConcurrentMap; import javax.servlet.ServletException; import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.util.Arrays; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.OutputStream; import java.io.BufferedReader; import java.io.InputStreamReader; import java.beans.XMLEncoder; import org.json.JSONObject; import org.json.XML; public class NovelsServlet extends HttpServlet { static final long serialVersionUID = 1L; private Novels novels; // back-end bean // Executed when servlet is first loaded into container. @Override public void init() { this.novels = new Novels(); novels.setServletContext(this.getServletContext()); } // GET /novels // GET /novels?id=1 @Override public void doGet(HttpServletRequest request, HttpServletResponse response) { String param = request.getParameter("id"); Integer key = (param == null) ? null : Integer.valueOf((param.trim())); // Check user preference for XML or JSON by inspecting // the HTTP headers for the Accept key. boolean json = false; String accept = request.getHeader("accept"); if (accept != null && accept.contains("json")) json = true; // If no query string, assume client wants the full list. if (key == null) { ConcurrentMap<Integer, Novel> map = novels.getConcurrentMap(); Object list = map.values().toArray(); Arrays.sort(list); String payload = novels.toXml(list); // defaults to Xml if (json) payload = novels.toJson(payload); // Json preferred? sendResponse(response, payload); } // Otherwise, return the specified Novel. else { Novel novel = novels.getConcurrentMap().get(key); if (novel == null) { // no such Novel String msg = key + " does not map to a novel.\n"; sendResponse(response, novels.toXml(msg)); } else { // requested Novel found if (json) sendResponse(response, novels.toJson(novels.toXml(novel))); else sendResponse(response, novels.toXml(novel)); } } } // POST /novels @Override public void doPost(HttpServletRequest request, HttpServletResponse response) { String author = request.getParameter("author"); String title = request.getParameter("title"); // Are the data to create a new novel present? if (author == null || title == null) throw new RuntimeException(Integer.toString(HttpServletResponse.SC_BAD_REQUEST)); // Create a novel. Novel n = new Novel(); n.setAuthor(author); n.setTitle(title); // Save the ID of the newly created Novel. int id = novels.addNovel(n); // Generate the confirmation message. String msg = "Novel " + id + " created.\n"; sendResponse(response, novels.toXml(msg)); } // PUT /novels @Override public void doPut(HttpServletRequest request, HttpServletResponse response) { /\* A workaround is necessary for a PUT request because Tomcat does not generate a workable parameter map for the PUT verb. \*/ String key = null; String rest = null; boolean author = false; /\* Let the hack begin. \*/ try { BufferedReader br = new BufferedReader(new InputStreamReader(request.getInputStream())); String data = br.readLine(); /\* To simplify the hack, assume that the PUT request has exactly two parameters: the id and either author or title. Assume, further, that the id comes first. From the client side, a hash character # separates the id and the author/title, e.g., id=33#title=War and Peace \*/ String[] args = data.split("#"); // id in args[0], rest in args[1] String[] parts1 = args[0].split("="); // id = parts1[1] key = parts1[1]; String[] parts2 = args[1].split("="); // parts2[0] is key if (parts2[0].contains("author")) author = true; rest = parts2[1]; } catch(Exception e) { throw new RuntimeException(Integer.toString(HttpServletResponse.SC_INTERNAL_SERVER_ERROR)); } // If no key, then the request is ill formed. if (key == null) throw new RuntimeException(Integer.toString(HttpServletResponse.SC_BAD_REQUEST)); // Look up the specified novel. Novel p = novels.getConcurrentMap().get(Integer.valueOf((key.trim()))); if (p == null) { // not found String msg = key + " does not map to a novel.\n"; sendResponse(response, novels.toXml(msg)); } else { // found if (rest == null) { throw new RuntimeException(Integer.toString(HttpServletResponse.SC_BAD_REQUEST)); } // Do the editing. else { if (author) p.setAuthor(rest); else p.setTitle(rest); String msg = "Novel " + key + " has been edited.\n"; sendResponse(response, novels.toXml(msg)); } } } // DELETE /novels?id=1 @Override public void doDelete(HttpServletRequest request, HttpServletResponse response) { String param = request.getParameter("id"); Integer key = (param == null) ? null : Integer.valueOf((param.trim())); // Only one Novel can be deleted at a time. if (key == null) throw new RuntimeException(Integer.toString(HttpServletResponse.SC_BAD_REQUEST)); try { novels.getConcurrentMap().remove(key); String msg = "Novel " + key + " removed.\n"; sendResponse(response, novels.toXml(msg)); } catch(Exception e) { throw new RuntimeException(Integer.toString(HttpServletResponse.SC_INTERNAL_SERVER_ERROR)); } } // Methods Not Allowed @Override public void doTrace(HttpServletRequest request, HttpServletResponse response) { throw new RuntimeException(Integer.toString(HttpServletResponse.SC_METHOD_NOT_ALLOWED)); } @Override public void doHead(HttpServletRequest request, HttpServletResponse response) { throw new RuntimeException(Integer.toString(HttpServletResponse.SC_METHOD_NOT_ALLOWED)); } @Override public void doOptions(HttpServletRequest request, HttpServletResponse response) { throw new RuntimeException(Integer.toString(HttpServletResponse.SC_METHOD_NOT_ALLOWED)); } // Send the response payload (Xml or Json) to the client. private void sendResponse(HttpServletResponse response, String payload) { try { OutputStream out = response.getOutputStream(); out.write(payload.getBytes()); out.flush(); } catch(Exception e) { throw new RuntimeException(Integer.toString(HttpServletResponse.SC_INTERNAL_SERVER_ERROR)); } } } 上面的 NovelsServlet 类继承了 HttpServlet 类，HttpServlet 类继承了 GenericServlet 类，后者实现了 Servlet 接口：NovelsServlet extends HttpServlet extends GenericServlet implements Servlet 从名字可以很清楚的看出来，HttpServlet 是为实现 HTTP(S) 上的 servlet 设计的。这个类提供了以标准 HTTP 请求动词（官方说法， 方法 methods ）命名的空方法：doPost （Post = 创建）doGet （Get = 读取）doPut （Put = 更新）doDelete （Delete = 删除）其他一些 HTTP 动词也会涉及到。HttpServlet 的子类，比如 NovelsServlet，会重载相关的 do 方法，并且保留其他方法为 空 no-ops 。NovelsServlet 重载了七个 do 方法。每个 HttpServlet 的 CRUD 方法都有两个相同的参数。下面以 doPost 为例：public void doPost(HttpServletRequest request, HttpServletResponse response) { request 参数是一个 HTTP 请求信息的映射，而 response 提供了一个返回给请求者的输出流。像 doPost 的方法，结构如下：读取 request 信息，采取任何适当的措施生成一个响应。如果该信息丢失或者损坏了，就会生成一个错误。使用提取的请求信息来执行适当的 CRUD 操作（在本例中，创建一个 Novel），然后使用 response 输出流为请求者编码一个适当的响应。在 doPost 例子中，响应就是已经成功生成一个新“小说”并且添加到容器中的一个确认。当响应被发送后，输出流就关闭了，同时也将连接关闭了。关于方法重载的更多内容HTTP 请求的格式相对比较简单。下面是一个非常熟悉的 HTTP 1.1 的格式，注释由双井号分隔：GET /novels ## start line Host: localhost:8080 ## header element Accept-type: text/plain ## ditto ... [body] ## POST and PUT only 第一行由 HTTP 动词（在本例中是 GET）和以名词（在本例中是 novels）命名目标资源的 URI 开始。报头中包含键-值对，用冒号分隔左面的键和右面的值。报头中的键 Host（大小写敏感）是必须的；主机名 localhost 是当前机器上的本地符号地址，8080 端口是 Tomcat web 服务器上等待 HTTP 请求的默认端口。（默认情况下，Tomcat 在 8443 端口上监听 HTTP 请求。）报头元素可以以任意顺序出现。在这个例子中，Accept-type 报头的值是 MIME 类型 text/plain。一些请求（特别是 POST 和 PUT）会有报文，而其他请求（特别是 GET 和 DELETE）没有。如果有报文（可能为空），以两个换行符将报头和报文分隔开；HTTP 报文包含一系列键-值对。对于无报文的请求，比如说查询字符串，报头元素就可以用来发送信息。下面是一个用 ID 2 对 /novels 资源的 GET 请求：GET /novels?id=2 通常来说，查询字符串以问号开始，并且包含一个键-值对，尽管这个键-值可能值为空。带有 getParameter 和 getParameterMap 等方法的 HttpServlet 很好地回避了有报文和没有报文的 HTTP 请求之前的差异。在“小说”例子中，getParameter 方法用来从 GET、POST 和 DELETE 方法中提取所需的信息。（处理 PUT请求需要更底层的代码，因为 Tomcat 没有提供可以解析 PUT 请求的参数映射。）下面展示了一段在 NovelsServlet中被重载的 doPost 方法：@Override public void doPost(HttpServletRequest request, HttpServletResponse response) { String author = request.getParameter("author"); String title = request.getParameter("title"); ... 对于没有报文的 DELETE 请求，过程基本是一样的：@Override public void doDelete(HttpServletRequest request, HttpServletResponse response) { String param = request.getParameter("id"); // id of novel to be removed ... doGet 方法需要区分 GET 请求的两种方式：一种是“获得所有”，而另一种是“获得某一个”。如果 GET 请求 URL 中包含一个键是一个 ID 的查询字符串，那么这个请求就被解析为“获得某一个”：http://localhost:8080/novels?id=2 ## GET specified 如果没有查询字符串，GET 请求就会被解析为“获得所有”：http://localhost:8080/novels ## GET all 一些值得注意的细节“小说”服务的设计反映了像 Tomcat 这样基于 Java 的 web 服务器是如何工作的。在启动时，Tomcat 构建一个线程池，从中提取请求处理程序，这种方法称为 “ 每个请求一个线程 one thread per request ” 模型。现在版本的 Tomcat 使用非阻塞 I/O 来提高个性能。“小说”服务是作为 NovelsServlet 类的单个实例来执行的，该实例也就维护了一个“小说”集合。相应的，也就会出现竞态条件，比如出现两个请求同时被处理：一个请求向集合中添加一个新“小说”。另一个请求获得集合中的所有“小说”。这样的结果是不确定的，取决与 读 和 写 的操作是以怎样的顺序进行操作的。为了避免这个问题，“小说”服务使用了线程安全的 ConcurrentMap。这个映射的关键是生成了一个线程安全的 AtomicInteger。下面是相关的代码片段：public class Novels { private ConcurrentMap<Integer, Novel> novels; private AtomicInteger mapKey; ... 默认情况下，对客户端请求的响应被编码为 XML。为了简单，“小说”程序使用了以前的 XMLEncoder 类；另一个包含更丰富功能的方式是使用 JAX-B 库。代码很简单：public String toXml(Object obj) { // default encoding String xml = null; try { ByteArrayOutputStream out = new ByteArrayOutputStream(); XMLEncoder encoder = new XMLEncoder(out); encoder.writeObject(obj); encoder.close(); xml = out.toString(); } catch(Exception e) { } return xml; } Object 参数要么是一个有序的“小说” ArraList（用以响应“ 获得所有 get all ”请求），要么是一个 Novel 实例（用以响应“ 获得一个 get one ”请求），又或者是一个 String（确认消息）。如果 HTTP 请求报头指定 JSON 作为所需要的类型，那么 XML 就被转化成 JSON。下面是 NovelsServlet 中的 doGet 方法中的检查：String accept = request.getHeader("accept"); // "accept" is case insensitive if (accept != null && accept.contains("json")) json = true; Novels类中包含了 toJson 方法，可以将 XML 转换成 JSON：public String toJson(String xml) { // option for requester try { JSONObject jobt = XML.toJSONObject(xml); return jobt.toString(3); // 3 is indentation level } catch(Exception e) { } return null; } NovelsServlet会对各种类型进行错误检查。比如，POST 请求应该包含新“小说”的作者和标题。如果有一个丢了，doPost 方法会抛出一个异常：if (author == null || title == null) throw new RuntimeException(Integer.toString(HttpServletResponse.SC_BAD_REQUEST)); SC_BAD_REQUEST 中的 SC 代表的是 状态码 status code ，BAD_REQUEST 的标准 HTTP 数值是 400。如果请求中的 HTTP 动词是 TRACE，会返回一个不同的状态码：public void doTrace(HttpServletRequest request, HttpServletResponse response) { throw new RuntimeException(Integer.toString(HttpServletResponse.SC_METHOD_NOT_ALLOWED)); } 测试“小说”服务用浏览器测试 web 服务会很不顺手。在 CRUD 动词中，现代浏览器只能生成 POST（创建）和 GET（读取）请求。甚至从浏览器发送一个 POST 请求都有点不好办，因为报文需要包含键-值对；这样的测试通常通过 HTML 表单完成。命令行工具，比如说 curl，是一个更好的选择，这个部分展示的一些 curl 命令，已经包含在我网站的 ZIP 文件中了。下面是一些测试样例，没有展示相应的输出结果：% curl localhost:8080/novels/ % curl localhost:8080/novels?id=1 % curl --header "Accept: application/json" localhost:8080/novels/ 第一条命令请求所有“小说”，默认是 XML 编码。第二条命令请求 ID 为 1 的“小说”，XML 编码。最后一条命令通过 application/json 添加了 Accept 报头元素，作为所需要的 MIME 类型。“ 获得一个 get one ”命令也可以用这个报头。这些请求用了 JSON 而不是 XML 编码作为响应。下面两条命令在集合中创建了一个新“小说”，并且确认添加了进去：% curl --request POST --data "author=Tolstoy&title=War and Peace" localhost:8080/novels/ % curl localhost:8080/novels?id=4 curl 中的 PUT 命令与 POST 命令相似，不同的地方是 PUT 的报文没有使用标准的语法。在 NovelsServlet 中关于 doPut 方法的文档中有详细的介绍，但是简单来说，Tomcat 不会对 PUT 请求生成合适的映射。下面是一个 PUT 命令和确认命令的的例子：% curl --request PUT --data "id=3#title=This is an UPDATE" localhost:8080/novels/ % curl localhost:8080/novels?id=3 第二个命令确认了集合已经更新。最后，DELETE 命令会正常运行：% curl --request DELETE localhost:8080/novels?id=2 % curl localhost:8080/novels/ 这个请求是删除 ID 为 2 的“小说”。第二个命令会显示剩余的“小说”。web.xml 配置文件尽管官方规定它是可选的，web.xml 配置文件是一个生产级别网站或服务的重要组成部分。这个配置文件可以配置独立于代码的路由、安全性，或者网站或服务的其他功能。“小说”服务的配置通过为该服务的请求分配一个 URL 模式来配置路由：<xml version = "1.0" encoding = "UTF-8"> <web-app> <servlet> <servlet-name>novels</servlet-name> <servlet-class>novels.NovelsServlet</servlet-class> </servlet> <servlet-mapping> <servlet-name>novels</servlet-name> <url-pattern>/*</url-pattern> </servlet-mapping> </web-app> servlet-name 元素为 servlet 全名（novels.NovelsServlet）提供了一个缩写（novels），然后这个名字在下面的 servlet-mapping 元素中使用。回想一下，一个已部署服务的 URL 会在端口号后面有 WAR 文件的文件名：http://localhost:8080/novels/ 端口号后斜杠后的 URI，是所请求资源的“路径”，在这个例子中，就是“小说”服务。因此，novels 出现在了第一个单斜杠后。在 web.xml 文件中，url-patter 被指定为 /*，代表 “以 /novels 为起始的任意路径”。假设 Tomcat 遇到了一个不存在的 URL，像这样：http://localhost:8080/novels/foobar/ web.xml 配置也会指定这个请求被分配到“小说” servlet 中，因为 /* 模式也包含 /foobar。因此，这个不存在的 URL 也会得到像上面合法路径的相同结果。生产级别的配置文件可能会包含安全相关的信息，包括 连接级别 wire-level 和 用户角色 users-roles 。即使在这种情况下，配置文件的大小也只会是这个例子中的两到三倍大。总结HttpServlet 是 Java web 技术的核心。像“小说”这样的网站或 web 服务继承了这个类，并且根据需求重载了相应的 do 动词方法。像 Jersay（JAX-RS）或 Restlet 这样的 Restful 框架通过提供定制的 servlet 完成了基本相同的功能，针对框架中的 web 应用程序的请求，这个 servlet 扮演着 HTTP(S) 终端 endpoint 的角色。当然，基于 servlet 的应用程序可以访问 web 应用程序中所需要的任何 Java 库。如果应用程序遵循 关注点分离 separation-of-concerns 原则，那么 servlet 代码仍然相当简单：代码会检查请求，如果存在缺陷，就会发出适当的错误；否则，代码会调用所需要的功能（比如，查询数据库，以特定格式为响应编码），然后向请求这发送响应。HttpServletRequest 和 HttpServletReponse 类型使得读取请求和编写响应变得简单。Java 的 API 可以从非常简单变得相当复杂。如果你需要用 Java 交付一些 Restful 服务的话，我的建议是在做其他事情之前先尝试一下简单的 HttpServlet。via: https://opensource.com/article/20/7/restful-services-java作者：Marty Kalin 选题：lujun9972 译者：Yufei-Yan 校对：wxy本文由 LCTT 原创编译，Linux中国 荣誉推出
通过 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 。加密哈希值是统计学上的唯一，而不是无条件的唯一，这意味着两个不同的输入位串产生相同的哈希值是不太可能的，但也不是不可能的 —— 这称之为 碰撞 collision 。生日问题提供了一个很好的反直觉的碰撞例子。对各种哈希算法的 抗碰撞性 collision resistance 有着广泛的研究。例如，MD5（128 位哈希值）在大约 2 21 次哈希之后，抗碰撞能力就会崩溃。对于 SHA1（160 位哈希值），大约在 2 61 次哈希后开始崩溃。对于 SHA256 的抗碰撞能力的剖析，目前还没有一个很好的估计。这个事实并不奇怪。SHA256 有 2 256 个不同的哈希值范围，这个数字的十进制表示法有 78 位之多！那么，SHA256 哈希会不会发生碰撞呢？当然可能，但可能性极小。在下面的命令行示例中，有两个输入文件被用作位串源：hashIn1.txt 和 hashIn2.txt。第一个文件包含 abc，第二个文件包含 1a2b3c。为了便于阅读，这些文件包含的是文本，但也可以使用二进制文件代替。在命令行（百分号 % 是提示符）使用 Linux sha256sum 实用程序对这两个文件进行处理产生以下哈希值（十六进制）:% sha256sum hashIn1.txt 9e83e05bbf9b5db17ac0deec3b7ce6cba983f6dc50531c7a919f28d5fb3696c3 hashIn1.txt % sha256sum hashIn2.txt 3eaac518777682bf4e8840dd012c0b104c2e16009083877675f00e995906ed13 hashIn2.txt OpenSSL 哈希对应的结果与预期相同：% openssl dgst -sha256 hashIn1.txt SHA256(hashIn1.txt)= 9e83e05bbf9b5db17ac0deec3b7ce6cba983f6dc50531c7a919f28d5fb3696c3 % openssl dgst -sha256 hashIn2.txt SHA256(hashIn2.txt)= 3eaac518777682bf4e8840dd012c0b104c2e16009083877675f00e995906ed13 这种对密码学哈希函数的研究，为我们仔细研究数字签名及其与密钥对的关系奠定了基础。数字签名顾名思义， 数字签字 digital signature 可以附在文件或其他一些电子 工件 artifact （如程序）上，以证明其真实性。因此，这种签名类似于纸质文件上的手写签名。验证数字签名就是要确认两件事：第一，被担保的工件在签名被附上后没有改变，因为它部分是基于文件的加密学哈希值。第二，签名属于一个人（例如 Alice），只有她才能获得一对密钥中的私钥。顺便说一下，对代码（源码或编译后的代码）进行数字签名已经成为程序员的普遍做法。让我们来了解一下数字签名是如何创建的。如前所述，没有公钥和私钥对就没有数字签名。当使用 OpenSSL 创建这些密钥时，有两个独立的命令：一个是创建私钥，另一个是从私钥中提取匹配的公钥。这些密钥对用 base64 编码，在这个过程中可以指定它们的大小。 私钥 private key 由数值组成，其中两个数值（一个 模数 modulus 和一个 指数 exponent ）组成了公钥。虽然私钥文件包含了 公钥 public key ，但提取出来的公钥并不会透露相应私钥的值。因此，生成的带有私钥的文件包含了完整的密钥对。将公钥提取到自己的文件中是很实用的，因为这两把钥匙有不同的用途，而这种提取方式也将私钥可能被意外公开的危险降到最低。接下来，这对密钥的私钥被用来生成目标工件（如电子邮件）的哈希值，从而创建签名。在另一端，接收者的系统使用这对密钥的公钥来验证附在工件上的签名。现在举个例子。首先，用 OpenSSL 生成一个 2048 位的 RSA 密钥对：openssl genpkey -out privkey.pem -algorithm rsa 2048 在这个例子中，我们可以舍去 -algorithm rsa 标志，因为 genpkey 默认为 RSA 类型。文件的名称（privkey.pem）是任意的，但是 隐私增强邮件 Privacy Enhanced Mail （PEM）扩展名 .pem 是默认 PEM 格式的惯用扩展名。（如果需要的话，OpenSSL 有命令可以在各种格式之间进行转换。）如果需要更大的密钥大小（例如 4096），那么最后一个参数 2048 可以改成 4096。这些大小总是二的幂。下面是产生的 privkey.pem 文件的一个片断，它是 base64 编码的：-----BEGIN PRIVATE KEY----- MIICdgIBADANBgkqhkiG9w0BAQEFAASCAmAwggJcAgEAAoGBANnlAh4jSKgcNj/Z JF4J4WdhkljP2R+TXVGuKVRtPkGAiLWE4BDbgsyKVLfs2EdjKL1U+/qtfhYsqhkK ... -----END PRIVATE KEY----- 接下来的命令就会从私钥中提取出这对密钥的公钥：openssl rsa -in privkey.pem -outform PEM -pubout -out pubkey.pem 由此产生的 pubkey.pem 文件很小，可以在这里完整地显示出来：-----BEGIN PUBLIC KEY----- MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDZ5QIeI0ioHDY/2SReCeFnYZJY z9kfk11RrilUbT5BgIi1hOAQ24LMilS37NhHYyi9VPv6rX4WLKoZCmkeYaWk/TR5 4nbH1E/AkniwRoXpeh5VncwWMuMsL5qPWGY8fuuTE27GhwqBiKQGBOmU+MYlZonO O0xnAKpAvysMy7G7qQIDAQAB -----END PUBLIC KEY----- 现在，有了密钥对，数字签名就很容易了 —— 在本例中，源文件 client.c 是要签名的工件：openssl dgst -sha256 -sign privkey.pem -out sign.sha256 client.c client.c 源文件的摘要是 SHA256，私钥在前面创建的 privkey.pem 文件中。由此产生的二进制签名文件是 sign.sha256，这是一个任意的名字。要得到这个文件的可读版本（比如 base64），后续命令是：openssl enc -base64 -in sign.sha256 -out sign.sha256.base64 文件 sign.sha256.base64 现在包含如下内容：h+e+3UPx++KKSlWKIk34fQ1g91XKHOGFRmjc0ZHPEyyjP6/lJ05SfjpAJxAPm075 VNfFwysvqRGmL0jkp/TTdwnDTwt756Ej4X3OwAVeYM7i5DCcjVsQf5+h7JycHKlM o/Jd3kUIWUkZ8+Lk0ZwzNzhKJu6LM5KWtL+MhJ2DpVc= 或者，可执行文件 client 也可以被签名，由此产生的 base64 编码签名将如预期的不同：VMVImPgVLKHxVBapJ8DgLNJUKb98GbXgehRPD8o0ImADhLqlEKVy0HKRm/51m9IX xRAN7DoL4Q3uuVmWWi749Vampong/uT5qjgVNTnRt9jON112fzchgEoMb8CHNsCT XIMdyaPtnJZdLALw6rwMM55MoLamSc6M/MV1OrJnk/g= 这一过程的最后一步是用公钥验证数字签名。作为验证的一个重要步骤，应重新计算用于签署工件（在本例中，是可执行的 client 程序）的哈希值，因为验证过程应表明工件在签署后是否发生了变化。有两个 OpenSSL 命令用于这个目的。第一条命令是对 base64 签名进行解码。openssl enc -base64 -d -in sign.sha256.base64 -out sign.sha256 第二条是核实签名：openssl dgst -sha256 -verify pubkey.pem -signature sign.sha256 client 第二条命令的输出，应该是这样的：Verified OK 为了了解验证失败时的情况，一个简短但有用的练习是将最后一个 OpenSSL 命令中的可执行的 client 文件替换为源文件 client.c，然后尝试验证。另一个练习是改变 client 程序，无论多么轻微，然后再试一次。数字证书 数字证书 digital certificate 汇集了到目前为止所分析的各个部分：哈希值、密钥对、数字签名和加密/解密。生产级证书的第一步是创建一个 证书签名请求 certificate signing request （CSR），然后将其发送给 证书颁发机构 certificate authority （CA）。在 OpenSSL 的例子中，要做到这一点，请运行：openssl req -out myserver.csr -new -newkey rsa:4096 -nodes -keyout myserverkey.pem 这个例子生成了一个 CSR 文档，并将该文档存储在文件 myserver.csr（base64 文本）中。这里的目的是：CSR 文档要求 CA 保证与指定域名相关联的身份，域名也就是 CA 所说的 通用名 common name （CN）。尽管可以使用现有的密钥对，但这个命令也会生成一个新的密钥对。请注意，在诸如 myserver.csr 和 myserverkey.pem 等名称中使用 server 暗示了数字证书的典型用途：作为与 www.google.com 等域名相关的 Web 服务器的身份担保。然而，无论数字证书如何使用，同样使用这个命令都会创建一个 CSR。它还会启动一个问题/回答的交互式会话，提示有关域名的相关信息，以便与请求者的数字证书相连接。这个交互式会话可以通过在命令中提供基本的信息，用反斜杠来续行一步完成。-subj 标志提供了所需的信息。% openssl req -new \ -newkey rsa:2048 -nodes -keyout privkeyDC.pem \ -out myserver.csr \ -subj "/C=US/ST=Illinois/L=Chicago/O=Faulty Consulting/OU=IT/CN=myserver.com" 产生的 CSR 文件在发送给 CA 之前可以进行检查和验证。这个过程可以创建具有所需格式（如 X509）、签名、有效期等的数字证书。openssl req -text -in myserver.csr -noout -verify 这是输出的一个片断：verify OK Certificate Request: Data: Version: 0 (0x0) Subject: C=US, ST=Illinois, L=Chicago, O=Faulty Consulting, OU=IT, CN=myserver.com Subject Public Key Info: Public Key Algorithm: rsaEncryption Public-Key: (2048 bit) Modulus: 00:ba:36:fb:57:17:65:bc:40:30:96:1b:6e:de:73: … Exponent: 65537 (0x10001) Attributes: a0:00 Signature Algorithm: sha256WithRSAEncryption … 自签证书在开发 HTTPS 网站的过程中，手头有一个不用经过 CA 流程的数字证书是很方便的。在 HTTPS 握手的认证阶段， 自签证书 self-signed certificate 就能满足要求，尽管任何现代浏览器都会警告说这样的证书毫无价值。继续这个例子，自签证书的 OpenSSL 命令（有效期为一年，使用 RSA 公钥）如下：openssl req -x509 -sha256 -nodes -days 365 -newkey rsa:4096 -keyout myserver.pem -out myserver.crt 下面的 OpenSSL 命令呈现了生成的证书的可读版本：openssl x509 -in myserver.crt -text -noout 这是自签证书的部分输出：Certificate: Data: Version: 3 (0x2) Serial Number: 13951598013130016090 (0xc19e087965a9055a) Signature Algorithm: sha256WithRSAEncryption Issuer: C=US, ST=Illinois, L=Chicago, O=Faulty Consulting, OU=IT, CN=myserver.com Validity Not Before: Apr 11 17:22:18 2019 GMT Not After : Apr 10 17:22:18 2020 GMT Subject: C=US, ST=Illinois, L=Chicago, O=Faulty Consulting, OU=IT, CN=myserver.com Subject Public Key Info: Public Key Algorithm: rsaEncryption Public-Key: (4096 bit) Modulus: 00:ba:36:fb:57:17:65:bc:40:30:96:1b:6e:de:73: ... Exponent: 65537 (0x10001) X509v3 extensions: X509v3 Subject Key Identifier: 3A:32:EF:3D:EB:DF:65:E5:A8:96:D7:D7:16:2C:1B:29:AF:46:C4:91 X509v3 Authority Key Identifier: keyid:3A:32:EF:3D:EB:DF:65:E5:A8:96:D7:D7:16:2C:1B:29:AF:46:C4:91 X509v3 Basic Constraints: CA:TRUE Signature Algorithm: sha256WithRSAEncryption 3a:eb:8d:09:53:3b:5c:2e:48:ed:14:ce:f9:20:01:4e:90:c9: ... 如前所述，RSA 私钥包含的值是用来生成公钥的。但是，给定的公钥不会泄露匹配的私钥。关于底层数学理论的介绍，见 https://simple.wikipedia.org/wiki/RSA_algorithm。数字证书与用于生成该证书的密钥对之间存在着重要的对应关系，即使证书只是自签的：数字证书包含构成公钥的指数和模数值。这些值是最初生成的 PEM 文件中密钥对的一部分，在本例中，是文件 myserver.pem。 指数 exponent 几乎总是 65,537（如本例中），所以可以忽略。密钥对的 模数 modulus 应该与数字证书的模数相匹配。模数是一个很大的值，为了便于阅读，可以进行哈希处理。下面是两个 OpenSSL 命令，它们检查相同的模数，从而确认数字证书是基于 PEM 文件中的密钥对。% openssl x509 -noout -modulus -in myserver.crt | openssl sha1 ## 证书中的模数 (stdin)= 364d21d5e53a59d482395b1885aa2c3a5d2e3769 % openssl rsa -noout -modulus -in myserver.pem | openssl sha1 ## 密钥中的模数 (stdin)= 364d21d5e53a59d482395b1885aa2c3a5d2e3769 所产生的哈希值匹配，从而确认数字证书是基于指定的密钥对。回到密钥分发问题上让我们回到第一部分末尾提出的一个问题：client 程序和 Google Web 服务器之间的 TLS 握手。握手协议有很多种，即使是用在 client 例子中的 Diffie-Hellman 版本也有不同的方式。尽管如此，client 例子遵循了一个共同的模式。首先，在 TLS 握手过程中，client 程序和 Web 服务器就 加密套件 cipher suite 达成一致，其中包括要使用的算法。在本例中，该套件是 ECDHE-RSA-AES128-GCM-SHA256。现在值得关注的两个要素是 RSA 密钥对算法和 AES128 块密码，用于在握手成功的情况下对消息进行加密和解密。关于加密/解密，这个过程有两种流派： 对称 symmetric 和 非对称 asymmetric 。在对称流派中，加密和解密使用的是相同的密钥，这首先就引出了 密钥分发问题 key distribution problem 。如何将密钥安全地分发给双方？在非对称流派中，一个密钥用于加密（在这种情况下，是 RSA 公钥），但另一个密钥用于解密（在这种情况下，是来自同一对密钥的 RSA 私钥）。client 程序拥有来认证证书的 Google Web 服务器的公钥，而 Web 服务器拥有来自同一对密钥的私钥。因此，client 程序可以向 Web 服务器发送加密信息，而 Web 服务器可以单独对该通信进行解密。在 TLS 的情况下，对称方式有两个显著的优势：在 client 程序与 Google Web 服务器之间的互动中，认证是单向的。Google Web 服务器向 client 程序发送三张证书，但 client 程序并没有向 Web 服务器发送证书，因此，Web 服务器没有来自客户端的公钥，无法加密发给客户端的消息。使用 AES128 的对称加密/解密比使用 RSA 密钥的非对称加密/解密快了近千倍。TLS 握手将两种加密/解密方式巧妙地结合在一起。在握手过程中，client 程序会生成随机位，即所谓的 预主密 pre-master secret （PMS）。然后，client 程序用服务器的公钥对 PMS 进行加密，并将加密后的 PMS 发送给服务器，服务器再用 RSA 密钥对的私钥对 PMS 信息进行解密： +-------------------+ encrypted PMS +--------------------+ client PMS--->|server’s public key|--------------->|server’s private key|--->server PMS +-------------------+ +--------------------+ 在这个过程结束时，client 程序和 Google Web 服务器现在拥有相同的 PMS 位。每一方都使用这些位生成一个 主密码 master secret ，并立即生成一个称为 会话密钥 session key 的对称加密/解密密钥。现在有两个不同但等价的会话密钥，连接的每一方都有一个。在 client 的例子中，会话密钥是 AES128 类的。一旦在 client 程序和 Google Web 服务器两边生成了会话密钥，每一边的会话密钥就会对双方的对话进行保密。如果任何一方（例如，client 程序）或另一方（在这种情况下，Google Web 服务器）要求重新开始握手，握手协议（如 Diffie-Hellman）允许整个 PMS 过程重复进行。总结在命令行上说明的 OpenSSL 操作也可以通过底层库的 API 完成。这两篇文章重点使用了这个实用程序，以保持例子的简短，并专注于加密主题。如果你对安全问题感兴趣，OpenSSL 是一个很好的开始地方，并值得深入研究。via: https://opensource.com/article/19/6/cryptography-basics-openssl-part-2作者：Marty Kalin 选题：lujun9972 译者：wxy 校对：wxy本文由 LCTT 原创编译，Linux中国 荣誉推出
学习如何使用 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 宏操作由两个微操作构成。首先，将数据分散（ 映射 mapped ）到各个工作程序中，然后将单独的结果收集在一起 —— 也可能收集统计起来成为一个值，即 归约 reduction 。归约可以采用不同的形式，如以下示例所示。下面 Number 类的实例用 EVEN 或 ODD 表示有奇偶校验的整数值：public class Number { enum Parity { EVEN, ODD } private int value; public Number(int n) { setValue(n); } public void setValue(int value) { this.value = value; } public int getValue() { return this.value; } public Parity getParity() { return ((value & 0x1) == 0) ? Parity.EVEN : Parity.ODD; } public void dump() { System.out.format("Value: %2d (parity: %s)\n", getValue(), (getParity() == Parity.ODD ? "odd" : "even")); } }下面的代码演示了用 Number 流进行 map/reduce 的情形，从而表明流 API 不仅可以处理 int 和 float 等基本类型，还可以处理程序员自定义的类类型。在下面的代码段中，使用了 parallelStream 而不是 stream 函数对随机整数值列表进行流化处理。与前面介绍的 parallel 函数一样，parallelStream 变体也可以自动执行多线程。final int howMany = 200; Random r = new Random(); Number[] nums = new Number[howMany]; for (int i = 0; i < howMany; i++) nums[i] = new Number(r.nextInt(100)); List<Number> listOfNums = Arrays.asList(nums); // 将数组转化为 list Integer sum4All = listOfNums .parallelStream() // 自动执行多线程 .mapToInt(Number::getValue) // 使用方法引用，而不是 lambda .sum(); // 将流值计算出和值 System.out.println("The sum of the randomly generated values is: " + sum4All);高阶的 mapToInt 函数可以接受一个 lambda 作为参数，但在本例中，它接受一个方法引用，即 Number::getValue。getValue 方法不需要参数，它返回给定的 Number 实例的 int 值。语法并不复杂：类名 Number 后跟一个双冒号和方法名。回想一下先前的例子 System.out::println，它在 System 类中的 static 属性 out 后面有一个双冒号。方法引用 Number::getValue 可以用下面的 lambda 表达式替换。参数 n 是流中的 Number 实例中的之一：mapToInt(n -> n.getValue())通常，lambda 表达式和方法引用是可互换的：如果像 mapToInt 这样的高阶函数可以采用一种形式作为参数，那么这个函数也可以采用另一种形式。这两个函数式编程结构具有相同的目的 —— 对作为参数传入的数据执行一些自定义操作。在两者之间进行选择通常是为了方便。例如，lambda 可以在没有封装类的情况下编写，而方法则不能。我的习惯是使用 lambda，除非已经有了适当的封装方法。当前示例末尾的 sum 函数通过结合来自 parallelStream 线程的部分和，以线程安全的方式进行归约。但是，程序员有责任确保在 parallelStream 调用引发的多线程过程中，程序员自己的函数调用（在本例中为 getValue）是线程安全的。最后一点值得强调。lambda 语法鼓励编写 纯函数 pure function ，即函数的返回值仅取决于传入的参数（如果有）；纯函数没有副作用，例如更新一个类中的 static 字段。因此，纯函数是线程安全的，并且如果传递给高阶函数的函数参数（例如 filter 和 map ）是纯函数，则流 API 效果最佳。对于更细粒度的控制，有另一个流 API 函数，名为 reduce，可用于对 Number 流中的值求和:Integer sum4AllHarder = listOfNums .parallelStream() // 多线程 .map(Number::getValue) // 每个 Number 的值 .reduce(0, (sofar, next) -> sofar + next); // 求和此版本的 reduce 函数带有两个参数，第二个参数是一个函数：第一个参数（在这种情况下为零）是特征值，该值用作求和操作的初始值，并且在求和过程中流结束时用作默认值。第二个参数是累加器，在本例中，这个 lambda 表达式有两个参数：第一个参数（sofar）是正在运行的和，第二个参数（next）是来自流的下一个值。运行的和以及下一个值相加，然后更新累加器。请记住，由于开始时调用了 parallelStream，因此 map 和 reduce 函数现在都在多线程上下文中执行。在到目前为止的示例中，流值被收集，然后被规约，但是，通常情况下，流 API 中的 Collectors 可以累积值，而不需要将它们规约到单个值。正如下一个代码段所示，收集活动可以生成任意丰富的数据结构。该示例使用与前面示例相同的 listOfNums：Map<Number.Parity, List<Number>> numMap = listOfNums .parallelStream() .collect(Collectors.groupingBy(Number::getParity)); List<Number> evens = numMap.get(Number.Parity.EVEN); List<Number> odds = numMap.get(Number.Parity.ODD);第一行中的 numMap 指的是一个 Map，它的键是一个 Number 奇偶校验位（ODD 或 EVEN），其值是一个具有指定奇偶校验位值的 Number 实例的 List。同样，通过 parallelStream 调用进行多线程处理，然后 collect 调用（以线程安全的方式）将部分结果组装到 numMap 引用的 Map 中。然后，在 numMap 上调用 get 方法两次，一次获取 evens，第二次获取 odds。实用函数 dumpList 再次使用来自流 API 的高阶 forEach 函数：private void dumpList(String msg, List<Number> list) { System.out.println("\n" + msg); list.stream().forEach(n -> n.dump()); // 或者使用 forEach(Number::dump) }这是示例运行中程序输出的一部分：The sum of the randomly generated values is: 3322 The sum again, using a different method: 3322 Evens: Value: 72 (parity: even) Value: 54 (parity: even) ... Value: 92 (parity: even) Odds: Value: 35 (parity: odd) Value: 37 (parity: odd) ... Value: 41 (parity: odd)用于代码简化的函数式结构函数式结构（如方法引用和 lambda 表达式）非常适合在流 API 中使用。这些构造代表了 Java 中对高阶函数的主要简化。即使在糟糕的过去，Java 也通过 Method 和 Constructor 类型在技术上支持高阶函数，这些类型的实例可以作为参数传递给其它函数。由于其复杂性，这些类型在生产级 Java 中很少使用。例如，调用 Method 需要对象引用（如果方法是非静态的）或至少一个类标识符（如果方法是静态的）。然后，被调用的 Method 的参数作为对象实例传递给它，如果没有发生多态（那会出现另一种复杂性！），则可能需要显式向下转换。相比之下，lambda 和方法引用很容易作为参数传递给其它函数。但是，新的函数式结构在流 API 之外具有其它用途。考虑一个 Java GUI 程序，该程序带有一个供用户按下的按钮，例如，按下以获取当前时间。按钮按下的事件处理程序可能编写如下：JButton updateCurrentTime = new JButton("Update current time"); updateCurrentTime.addActionListener(new ActionListener() { @Override public void actionPerformed(ActionEvent e) { currentTime.setText(new Date().toString()); } });这个简短的代码段很难解释。关注第二行，其中方法 addActionListener 的参数开始如下：new ActionListener() {这似乎是错误的，因为 ActionListener 是一个抽象接口，而抽象类型不能通过调用 new 实例化。但是，事实证明，还有其它一些实例被实例化了：一个实现此接口的未命名内部类。如果上面的代码封装在名为 OldJava 的类中，则该未命名的内部类将被编译为 OldJava$1.class。actionPerformed 方法在这个未命名的内部类中被重写。现在考虑使用新的函数式结构进行这个令人耳目一新的更改：updateCurrentTime.addActionListener(e -> currentTime.setText(new Date().toString()));lambda 表达式中的参数 e 是一个 ActionEvent 实例，而 lambda 的主体是对按钮上的 setText 的简单调用。函数式接口和函数组合到目前为止，使用的 lambda 已经写好了。但是，为了方便起见，我们可以像引用封装方法一样引用 lambda 表达式。以下一系列简短示例说明了这一点。考虑以下接口定义：@FunctionalInterface // 可选，通常省略 interface BinaryIntOp { abstract int compute(int arg1, int arg2); // abstract 声明可以被删除 }注释 @FunctionalInterface 适用于声明唯一抽象方法的任何接口；在本例中，这个抽象接口是 compute。一些标准接口，（例如具有唯一声明方法 run 的 Runnable 接口）同样符合这个要求。在此示例中，compute 是已声明的方法。该接口可用作引用声明中的目标类型：BinaryIntOp div = (arg1, arg2) -> arg1 / arg2; div.compute(12, 3); // 4包 java.util.function 提供各种函数式接口。以下是一些示例。下面的代码段介绍了参数化的 Predicate 函数式接口。在此示例中，带有参数 String 的 Predicate<String> 类型可以引用具有 String 参数的 lambda 表达式或诸如 isEmpty 之类的 String 方法。通常情况下，Predicate 是一个返回布尔值的函数。Predicate<String> pred = String::isEmpty; // String 方法的 predicate 声明 String[] strings = {"one", "two", "", "three", "four"}; Arrays.asList(strings) .stream() .filter(pred) // 过滤掉非空字符串 .forEach(System.out::println); // 只打印空字符串在字符串长度为零的情况下，isEmpty Predicate 判定结果为 true。 因此，只有空字符串才能进入管道的 forEach 阶段。下一段代码将演示如何将简单的 lambda 或方法引用组合成更丰富的 lambda 或方法引用。考虑这一系列对 IntUnaryOperator 类型的引用的赋值，它接受一个整型参数并返回一个整型值：IntUnaryOperator doubled = n -> n * 2; IntUnaryOperator tripled = n -> n * 3; IntUnaryOperator squared = n -> n * n;IntUnaryOperator 是一个 FunctionalInterface，其唯一声明的方法为 applyAsInt。现在可以单独使用或以各种组合形式使用这三个引用 doubled、tripled 和 squared：int arg = 5; doubled.applyAsInt(arg); // 10 tripled.applyAsInt(arg); // 15 squared.applyAsInt(arg); // 25以下是一些函数组合的样例：int arg = 5; doubled.compose(squared).applyAsInt(arg); // 5 求 2 次方后乘 2：50 tripled.compose(doubled).applyAsInt(arg); // 5 乘 2 后再乘 3：30 doubled.andThen(squared).applyAsInt(arg); // 5 乘 2 后求 2 次方：100 squared.andThen(tripled).applyAsInt(arg); // 5 求 2 次方后乘 3：75函数组合可以直接使用 lambda 表达式实现，但是引用使代码更简洁。构造器引用构造器引用是另一种函数式编程构造，而这些引用在比 lambda 和方法引用更微妙的上下文中非常有用。再一次重申，代码示例似乎是最好的解释方式。考虑这个 POJO 类：public class BedRocker { // 基岩的居民 private String name; public BedRocker(String name) { this.name = name; } public String getName() { return this.name; } public void dump() { System.out.println(getName()); } }该类只有一个构造函数，它需要一个 String 参数。给定一个名字数组，目标是生成一个 BedRocker 元素数组，每个名字代表一个元素。下面是使用了函数式结构的代码段：String[] names = {"Fred", "Wilma", "Peebles", "Dino", "Baby Puss"}; Stream<BedRocker> bedrockers = Arrays.asList(names).stream().map(BedRocker::new); BedRocker[] arrayBR = bedrockers.toArray(BedRocker[]::new); Arrays.asList(arrayBR).stream().forEach(BedRocker::dump);在较高的层次上，这个代码段将名字转换为 BedRocker 数组元素。具体来说，代码如下所示。Stream 接口（在包 java.util.stream 中）可以被参数化，而在本例中，生成了一个名为 bedrockers 的 BedRocker 流。Arrays.asList 实用程序再次用于流化一个数组 names，然后将流的每一项传递给 map 函数，该函数的参数现在是构造器引用 BedRocker::new。这个构造器引用通过在每次调用时生成和初始化一个 BedRocker 实例来充当一个对象工厂。在第二行执行之后，名为 bedrockers 的流由五项 BedRocker 组成。这个例子可以通过关注高阶 map 函数来进一步阐明。在通常情况下，一个映射将一个类型的值（例如，一个 int）转换为另一个相同类型的值（例如，一个整数的后继）:map(n -> n + 1) // 将 n 映射到其后继然而，在 BedRocker 这个例子中，转换更加戏剧化，因为一个类型的值（代表一个名字的 String）被映射到一个不同类型的值，在这个例子中，就是一个 BedRocker 实例，这个字符串就是它的名字。转换是通过一个构造器调用来完成的，它是由构造器引用来实现的：map(BedRocker::new) // 将 String 映射到 BedRocker传递给构造器的值是 names 数组中的其中一项。此代码示例的第二行还演示了一个你目前已经非常熟悉的转换：先将数组先转换成 List，然后再转换成 Stream：Stream<BedRocker> bedrockers = Arrays.asList(names).stream().map(BedRocker::new);第三行则是另一种方式 —— 流 bedrockers 通过使用数组构造器引用 BedRocker[]::new 调用 toArray 方法:BedRocker[ ] arrayBR = bedrockers.toArray(BedRocker[]::new);该构造器引用不会创建单个 BedRocker 实例，而是创建这些实例的整个数组：该构造器引用现在为 BedRocker[]:new，而不是 BedRocker::new。为了进行确认，将 arrayBR 转换为 List，再次对其进行流式处理，以便可以使用 forEach 来打印 BedRocker 的名字。Fred Wilma Peebles Dino Baby Puss该示例对数据结构的微妙转换仅用几行代码即可完成，从而突出了可以将 lambda，方法引用或构造器引用作为参数的各种高阶函数的功能。 柯里化 Currying 柯里化函数是指减少函数执行任何工作所需的显式参数的数量（通常减少到一个）。（该术语是为了纪念逻辑学家 Haskell Curry。）一般来说，函数的参数越少，调用起来就越容易，也更健壮。（回想一下一些需要半打左右参数的噩梦般的函数！）因此，应将柯里化视为简化函数调用的一种尝试。java.util.function 包中的接口类型适合于柯里化，如以下示例所示。引用的 IntBinaryOperator 接口类型是为函数接受两个整型参数，并返回一个整型值：IntBinaryOperator mult2 = (n1, n2) -> n1 * n2; mult2.applyAsInt(10, 20); // 200 mult2.applyAsInt(10, 30); // 300引用 mult2 强调了需要两个显式参数，在本例中是 10 和 20。前面介绍的 IntUnaryOperator 比 IntBinaryOperator 简单，因为前者只需要一个参数，而后者则需要两个参数。两者均返回整数值。因此，目标是将名为 mult2 的两个参数 IntBinraryOperator 柯里化成一个单一的 IntUnaryOperator 版本 curriedMult2。考虑 IntFunction<R> 类型。此类型的函数采用整型参数，并返回类型为 R 的结果，该结果可以是另一个函数 —— 更准确地说，是 IntBinaryOperator。让一个 lambda 返回另一个 lambda 很简单：arg1 -> (arg2 -> arg1 * arg2) // 括号可以省略完整的 lambda 以 arg1 开头，而该 lambda 的主体以及返回的值是另一个以 arg2 开头的 lambda。返回的 lambda 仅接受一个参数（arg2），但返回了两个数字的乘积（arg1 和 arg2）。下面的概述，再加上代码，应该可以更好地进行说明。以下是如何柯里化 mult2 的概述：类型为 IntFunction<IntUnaryOperator> 的 lambda 被写入并调用，其整型值为 10。返回的 IntUnaryOperator 缓存了值 10，因此变成了已柯里化版本的 mult2，在本例中为 curriedMult2。然后使用单个显式参数（例如，20）调用 curriedMult2 函数，该参数与缓存的参数（在本例中为 10）相乘以生成返回的乘积。。这是代码的详细信息：// 创建一个接受一个参数 n1 并返回一个单参数 n2 -> n1 * n2 的函数，该函数返回一个（n1 * n2 乘积的）整型数。 IntFunction<IntUnaryOperator> curriedMult2Maker = n1 -> (n2 -> n1 * n2);调用 curriedMult2Maker 生成所需的 IntUnaryOperator 函数：// 使用 curriedMult2Maker 获取已柯里化版本的 mult2。 // 参数 10 是上面的 lambda 的 n1。 IntUnaryOperator curriedMult2 = curriedMult2Maker2.apply(10);值 10 现在缓存在 curriedMult2 函数中，以便 curriedMult2 调用中的显式整型参数乘以 10：curriedMult2.applyAsInt(20); // 200 = 10 * 20 curriedMult2.applyAsInt(80); // 800 = 10 * 80缓存的值可以随意更改：curriedMult2 = curriedMult2Maker.apply(50); // 缓存 50 curriedMult2.applyAsInt(101); // 5050 = 101 * 50当然，可以通过这种方式创建多个已柯里化版本的 mult2，每个版本都有一个 IntUnaryOperator。柯里化充分利用了 lambda 的强大功能：可以很容易地编写 lambda 表达式来返回需要的任何类型的值，包括另一个 lambda。总结Java 仍然是基于类的面向对象的编程语言。但是，借助流 API 及其支持的函数式构造，Java 向函数式语言（例如 Lisp）迈出了决定性的（同时也是受欢迎的）一步。结果是 Java 更适合处理现代编程中常见的海量数据流。在函数式方向上的这一步还使以在前面的代码示例中突出显示的管道的方式编写清晰简洁的 Java 代码更加容易：dataStream .parallelStream() // 多线程以提高效率 .filter(...) // 阶段 1 .map(...) // 阶段 2 .filter(...) // 阶段 3 ... .collect(...); // 或者，也可以进行归约：阶段 N自动多线程，以 parallel 和 parallelStream 调用为例，建立在 Java 的 fork/join 框架上，该框架支持 任务窃取 task stealing 以提高效率。假设 parallelStream 调用后面的线程池由八个线程组成，并且 dataStream 被八种方式分区。某个线程（例如，T1）可能比另一个线程（例如，T7）工作更快，这意味着应该将 T7 的某些任务移到 T1 的工作队列中。这会在运行时自动发生。在这个简单的多线程世界中，程序员的主要职责是编写线程安全函数，这些函数作为参数传递给在流 API 中占主导地位的高阶函数。尤其是 lambda 鼓励编写纯函数（因此是线程安全的）函数。via: https://opensource.com/article/20/1/javastream作者：Marty Kalin 选题：lujun9972 译者：laingke 校对：wxy本文由 LCTT 原创编译，Linux中国 荣誉推出
想要入门密码学的基础知识，尤其是有关 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_CTX_new..."); BIO* bio = BIO_new_ssl_connect(ctx); if (NULL == bio) report_and_exit("BIO_new_ssl_connect..."); SSL* ssl = NULL; /* 链路 bio 通道，SSL 会话和服务器端点 */ sprintf(name, "%s:%s", hostname, "https"); BIO_get_ssl(bio, &ssl); /* 会话 */ SSL_set_mode(ssl, SSL_MODE_AUTO_RETRY); /* 鲁棒性 */ BIO_set_conn_hostname(bio, name); /* 准备连接 */ /* 尝试连接 */ if (BIO_do_connect(bio) <= 0) { cleanup(ctx, bio); report_and_exit("BIO_do_connect..."); } /* 验证信任库，检查证书 */ if (!SSL_CTX_load_verify_locations(ctx, "/etc/ssl/certs/ca-certificates.crt", /* 信任库 */ "/etc/ssl/certs/")) /* 其它信任库 */ report_and_exit("SSL_CTX_load_verify_locations..."); long verify_flag = SSL_get_verify_result(ssl); if (verify_flag != X509_V_OK) fprintf(stderr, "##### Certificate verification error (%i) but continuing...\n", (int) verify_flag); /* 获取主页作为示例数据 */ sprintf(request, "GET / HTTP/1.1\x0D\x0AHost: %s\x0D\x0A\x43onnection: Close\x0D\x0A\x0D\x0A", hostname); BIO_puts(bio, request); /* 从服务器读取 HTTP 响应并打印到输出 */ while (1) { memset(response, '\0', sizeof(response)); int n = BIO_read(bio, response, BuffSize); if (n <= 0) break; /* 0 代表流结束，< 0 代表有错误 */ puts(response); } cleanup(ctx, bio); } int main() { init_ssl(); const char* hostname = "www.google.com:443"; fprintf(stderr, "Trying an HTTPS connection to %s...\n", hostname); secure_connect(hostname); return 0; }可以从命令行编译和执行该程序（请注意 -lssl 和 -lcrypto 中的小写字母 L）：gcc -o client client.c -lssl -lcrypto该程序尝试打开与网站 www.google.com 的安全连接。在与 Google Web 服务器的 TLS 握手过程中，client 程序会收到一个或多个数字证书，该程序会尝试对其进行验证（但在我的系统上失败了）。尽管如此，client 程序仍继续通过安全通道获取 Google 主页。该程序取决于前面提到的安全工件，尽管在上述代码中只着重突出了数字证书。但其它工件仍在幕后发挥作用，稍后将对它们进行详细说明。通常，打开 HTTP（非安全）通道的 C 或 C++ 的客户端程序将使用诸如文件描述符或网络套接字之类的结构，它们是两个进程（例如，这个 client 程序和 Google Web 服务器）之间连接的端点。另一方面，文件描述符是一个非负整数值，用于在程序中标识该程序打开的任何文件类的结构。这样的程序还将使用一种结构来指定有关 Web 服务器地址的详细信息。这些相对较低级别的结构不会出现在客户端程序中，因为 OpenSSL 库会将套接字基础设施和地址规范等封装在更高层面的安全结构中。其结果是一个简单的 API。下面首先看一下 client 程序示例中的安全性详细信息。该程序首先加载相关的 OpenSSL 库，我的函数 init_ssl 中对 OpenSSL 进行了两次调用：SSL_load_error_strings(); SSL_library_init(); 下一个初始化步骤尝试获取安全上下文，这是建立和维护通往 Web 服务器的安全通道所需的信息框架。如对 OpenSSL 库函数的调用所示，在示例中使用了 TLS 1.2：const SSL_METHOD* method = TLSv1_2_client_method(); /* TLS 1.2 */如果调用成功，则将 method 指针被传递给库函数，该函数创建类型为 SSL_CTX 的上下文：SSL_CTX* ctx = SSL_CTX_new(method);client 程序会检查每个关键的库调用的错误，如果其中一个调用失败，则程序终止。现在还有另外两个 OpenSSL 工件也在发挥作用：SSL 类型的安全会话，从头到尾管理安全连接；以及类型为 BIO（ 基本输入/输出 Basic Input/Output ）的安全流，用于与 Web 服务器进行通信。BIO 流是通过以下调用生成的：BIO* bio = BIO_new_ssl_connect(ctx);请注意，这个最重要的上下文是其参数。BIO 类型是 C 语言中 FILE 类型的 OpenSSL 封装器。此封装器可保护 client 程序与 Google 的网络服务器之间的输入和输出流的安全。有了 SSL_CTX 和 BIO，然后程序在 SSL 会话中将它们组合在一起。三个库调用可以完成工作：BIO_get_ssl(bio, &ssl); /* 会话 */ SSL_set_mode(ssl, SSL_MODE_AUTO_RETRY); /* 鲁棒性 */ BIO_set_conn_hostname(bio, name); /* 准备连接 */安全连接本身是通过以下调用建立的：BIO_do_connect(bio);如果最后一个调用不成功，则 client 程序终止；否则，该连接已准备就绪，可以支持 client 程序与 Google Web 服务器之间的机密对话。在与 Web 服务器握手期间，client 程序会接收一个或多个数字证书，以认证服务器的身份。但是，client 程序不会发送自己的证书，这意味着这个身份验证是单向的。（Web 服务器通常配置为不需要客户端证书）尽管对 Web 服务器证书的验证失败，但 client 程序仍通过了连接到 Web 服务器的安全通道继续获取 Google 主页。为什么验证 Google 证书的尝试会失败？典型的 OpenSSL 安装目录为 /etc/ssl/certs，其中包含 ca-certificates.crt 文件。该目录和文件包含着 OpenSSL 自带的数字证书，以此构成 信任库 truststore 。可以根据需要更新信任库，尤其是可以包括新信任的证书，并删除不再受信任的证书。client 程序从 Google Web 服务器收到了三个证书，但是我的计算机上的 OpenSSL 信任库并不包含完全匹配的证书。如目前所写，client 程序不会通过例如验证 Google 证书上的数字签名（一个用来证明该证书的签名）来解决此问题。如果该签名是受信任的，则包含该签名的证书也应受信任。尽管如此，client 程序仍继续获取页面，然后打印出 Google 的主页。下一节将更详细地介绍这些。客户端程序中隐藏的安全性让我们从客户端示例中可见的安全工件（数字证书）开始，然后考虑其他安全工件如何与之相关。数字证书的主要格式标准是 X509，生产级的证书由诸如 Verisign 的 证书颁发机构 Certificate Authority （CA）颁发。数字证书中包含各种信息（例如，激活日期和失效日期以及所有者的域名），也包括发行者的身份和数字签名（这是加密过的加密哈希值）。证书还具有未加密的哈希值，用作其标识指纹。哈希值来自将任意数量的二进制位映射到固定长度的摘要。这些位代表什么（会计报告、小说或数字电影）无关紧要。例如， 消息摘要版本 5 Message Digest version 5 （MD5）哈希算法将任意长度的输入位映射到 128 位哈希值，而 SHA1（ 安全哈希算法版本 1 Secure Hash Algorithm version 1 ）算法将输入位映射到 160 位哈希值。不同的输入位会导致不同的（实际上在统计学上是唯一的）哈希值。下一篇文章将会进行更详细的介绍，并着重介绍什么使哈希函数具有加密功能。数字证书的类型有所不同（例如根证书、中间证书和最终实体证书），并形成了反映这些证书类型的层次结构。顾名思义，根证书位于层次结构的顶部，其下的证书继承了根证书所具有的信任。OpenSSL 库和大多数现代编程语言都具有 X509 数据类型以及处理此类证书的函数。来自 Google 的证书具有 X509 格式，client 程序会检查该证书是否为 X509_V_OK。X509 证书基于 公共密钥基础结构 public-key infrastructure （PKI），其中包括的算法（RSA 是占主导地位的算法）用于生成密钥对：公共密钥及其配对的私有密钥。公钥是一种身份：Amazon 的公钥对其进行标识，而我的公钥对我进行标识。私钥应由其所有者负责保密。成对出现的密钥具有标准用途。可以使用公钥对消息进行加密，然后可以使用同一个密钥对中的私钥对消息进行解密。私钥也可以用于对文档或其他电子工件（例如程序或电子邮件）进行签名，然后可以使用该对密钥中的公钥来验证签名。以下两个示例补充了一些细节。在第一个示例中，Alice 将她的公钥分发给全世界，包括 Bob。然后，Bob 用 Alice 的公钥加密邮件，然后将加密的邮件发送给 Alice。用 Alice 的公钥加密的邮件将可以用她的私钥解密（假设是她自己的私钥），如下所示： +------------------+ encrypted msg +-------------------+ Bob's msg--->|Alice's public key|--------------->|Alice's private key|---> Bob's msg +------------------+ +-------------------+理论上可以在没有 Alice 的私钥的情况下解密消息，但在实际情况中，如果使用像 RSA 这样的加密密钥对系统，则在计算上做不到。现在，第二个示例，请对文档签名以证明其真实性。签名算法使用密钥对中的私钥来处理要签名的文档的加密哈希： +-------------------+ Hash of document--->|Alice's private key|--->Alice's digital signature of the document +-------------------+假设 Alice 以数字方式签署了发送给 Bob 的合同。然后，Bob 可以使用 Alice 密钥对中的公钥来验证签名： +------------------+ Alice's digital signature of the document--->|Alice's public key|--->verified or not +------------------+假若没有 Alice 的私钥，就无法轻松伪造 Alice 的签名：因此，Alice 有必要保密她的私钥。在 client 程序中，除了数字证书以外，这些安全性都没有明确展示。下一篇文章使用使用 OpenSSL 实用程序和库函数的示例填充更多详细的信息。命令行的 OpenSSL同时，让我们看一下 OpenSSL 命令行实用程序：特别是在 TLS 握手期间检查来自 Web 服务器的证书的实用程序。调用 OpenSSL 实用程序可以使用 openssl 命令，然后添加参数和标志的组合以指定所需的操作。看看以下命令：openssl list-cipher-algorithms该输出是组成 加密算法套件 cipher suite 的相关算法的列表。下面是列表的开头，加了澄清首字母缩写词的注释：AES-128-CBC ## Advanced Encryption Standard, Cipher Block Chaining AES-128-CBC-HMAC-SHA1 ## Hash-based Message Authentication Code with SHA1 hashes AES-128-CBC-HMAC-SHA256 ## ditto, but SHA256 rather than SHA1 ...下一条命令使用参数 s_client 将打开到 www.google.com 的安全连接，并在屏幕上显示有关此连接的所有信息：openssl s_client -connect www.google.com:443 -showcerts端口号 443 是 Web 服务器用于接收 HTTPS（而不是 HTTP 连接）的标准端口号。（对于 HTTP，标准端口为 80）Web 地址 www.google.com:443 也出现在 client 程序的代码中。如果尝试连接成功，则将显示来自 Google 的三个数字证书以及有关安全会话、正在使用的加密算法套件以及相关项目的信息。例如，这是开头的部分输出，它声明证书链即将到来。证书的编码为 base64：Certificate chain 0 s:/C=US/ST=California/L=Mountain View/O=Google LLC/CN=www.google.com i:/C=US/O=Google Trust Services/CN=Google Internet Authority G3 -----BEGIN CERTIFICATE----- MIIEijCCA3KgAwIBAgIQdCea9tmy/T6rK/dDD1isujANBgkqhkiG9w0BAQsFADBU MQswCQYDVQQGEwJVUzEeMBwGA1UEChMVR29vZ2xlIFRydXN0IFNlcnZpY2VzMSUw ...诸如 Google 之类的主要网站通常会发送多个证书进行身份验证。输出以有关 TLS 会话的摘要信息结尾，包括加密算法套件的详细信息：SSL-Session: Protocol : TLSv1.2 Cipher : ECDHE-RSA-AES128-GCM-SHA256 Session-ID: A2BBF0E4991E6BBBC318774EEE37CFCB23095CC7640FFC752448D07C7F438573 ...client 程序中使用了协议 TLS 1.2，Session-ID 唯一地标识了 openssl 实用程序和 Google Web 服务器之间的连接。Cipher 条目可以按以下方式进行解析：ECDHE（ 椭圆曲线 Diffie-Hellman（临时） Elliptic Curve Diffie Hellman Ephemeral ）是一种用于管理 TLS 握手的高效的有效算法。尤其是，ECDHE 通过确保连接双方（例如，client 程序和 Google Web 服务器）使用相同的加密/解密密钥（称为会话密钥）来解决“密钥分发问题”。后续文章会深入探讨该细节。RSA（Rivest Shamir Adleman）是主要的公共密钥密码系统，并以 1970 年代末首次描述了该系统的三位学者的名字命名。这个正在使用的密钥对是使用 RSA 算法生成的。AES128（ 高级加密标准 Advanced Encryption Standard ）是一种 块式加密算法 block cipher ，用于加密和解密 位块 blocks of bits 。（另一种算法是 流式加密算法 stream cipher ，它一次加密和解密一个位。）这个加密算法是对称加密算法，因为使用同一个密钥进行加密和解密，这首先引起了密钥分发问题。AES 支持 128（此处使用）、192 和 256 位的密钥大小：密钥越大，安全性越好。通常，像 AES 这样的对称加密系统的密钥大小要小于像 RSA 这样的非对称（基于密钥对）系统的密钥大小。例如，1024 位 RSA 密钥相对较小，而 256 位密钥则当前是 AES 最大的密钥。GCM（ 伽罗瓦计数器模式 Galois Counter Mode ）处理在安全对话期间重复应用的加密算法（在这种情况下为 AES128）。AES128 块的大小仅为 128 位，安全对话很可能包含从一侧到另一侧的多个 AES128 块。GCM 非常有效，通常与 AES128 搭配使用。SHA256（ 256 位安全哈希算法 Secure Hash Algorithm 256 bits ）是我们正在使用的加密哈希算法。生成的哈希值的大小为 256 位，尽管使用 SHA 甚至可以更大。加密算法套件正在不断发展中。例如，不久前，Google 使用 RC4 流加密算法（RSA 的 Ron Rivest 后来开发的 Ron’s Cipher 版本 4）。 RC4 现在有已知的漏洞，这大概部分导致了 Google 转换为 AES128。总结我们通过安全的 C Web 客户端和各种命令行示例对 OpenSSL 做了首次了解，使一些需要进一步阐明的主题脱颖而出。下一篇文章会详细介绍，从加密散列开始，到对数字证书如何应对密钥分发挑战为结束的更全面讨论。via: https://opensource.com/article/19/6/cryptography-basics-openssl-part-1作者：Marty Kalin 选题：lujun9972 译者：wxy 校对：wxy本文由 LCTT 原创编译，Linux中国 荣誉推出
在以不同语言编写并在不同平台上运行的应用程序之间交换数据时，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 编码。现在，每种通用编程语言都具有支持 XML 和 JSON 的库。那么，是什么让我们回到 Protobuf 之类的二进制编码系统呢？让我们看一下负十进制值 -128。以 2 的补码二进制表示形式（在系统和语言中占主导地位）中，此值可以存储在单个 8 位字节中：10000000。此整数值在 XML 或 JSON 中的文本编码需要多个字节。例如，UTF-8 编码需要四个字节的字符串，即 -128，即每个字符一个字节（十六进制，值为 0x2d、0x31、0x32 和 0x38）。XML 和 JSON 还添加了标记字符，例如尖括号和大括号。有关 Protobuf 编码的详细信息下面就会介绍，但现在的关注点是一个通用点：文本编码的压缩性明显低于二进制编码。在 Go 中使用 Protobuf 的示例我的代码示例着重于 Protobuf 而不是 RPC。以下是第一个示例的概述：名为 dataitem.proto 的 IDL 文件定义了一个 Protobuf 消息，它具有六个不同类型的字段：具有不同范围的整数值、固定大小的浮点值以及两个不同长度的字符串。Protobuf 编译器使用 IDL 文件生成 Go 版本（以及后面的 Java 版本）的 Protobuf 消息及支持函数。Go 应用程序使用随机生成的值填充原生的 Go 数据结构，然后将结果序列化为本地文件。为了进行比较， XML 和 JSON 编码也被序列化为本地文件。作为测试，Go 应用程序通过反序列化 Protobuf 文件的内容来重建其原生数据结构的实例。作为语言中立性测试，Java 应用程序还会对 Protobuf 文件的内容进行反序列化以获取原生数据结构的实例。我的网站上提供了该 IDL 文件以及两个 Go 和一个 Java 源文件，打包为 ZIP 文件。最重要的 Protobuf IDL 文档如下所示。该文档存储在文件 dataitem.proto 中，并具有常规的.proto 扩展名。示例 1、Protobuf IDL 文档syntax = "proto3"; package main; message DataItem { int64 oddA = 1; int64 evenA = 2; int32 oddB = 3; int32 evenB = 4; float small = 5; float big = 6; string short = 7; string long = 8; }该 IDL 使用当前的 proto3 而不是较早的 proto2 语法。软件包名称（在本例中为 main）是可选的，但是惯例使用它以避免名称冲突。这个结构化的消息包含八个字段，每个字段都有一个 Protobuf 数据类型（例如，int64、string）、名称（例如，oddA、short）和一个等号 = 之后的数字标签（即键）。标签（在此示例中为 1 到 8）是唯一的整数标识符，用于确定字段序列化的顺序。Protobuf 消息可以嵌套到任意级别，而一个消息可以是另外一个消息的字段类型。这是一个使用 DataItem 消息作为字段类型的示例：message DataItems { repeated DataItem item = 1; }单个 DataItems 消息由重复的（零个或多个）DataItem 消息组成。为了清晰起见，Protobuf 还支持枚举类型：enum PartnershipStatus { reserved "FREE", "CONSTRAINED", "OTHER"; }reserved 限定符确保用于实现这三个符号名的数值不能重复使用。为了生成一个或多个声明 Protobuf 消息结构的特定于语言的版本，包含这些结构的 IDL 文件被传递到protoc 编译器（可在 Protobuf GitHub 存储库中找到）。对于 Go 代码，可以以通常的方式安装支持的 Protobuf 库（这里以 ％ 作为命令行提示符）：% go get github.com/golang/protobuf/proto将 Protobuf IDL 文件 dataitem.proto 编译为 Go 源代码的命令是：% protoc --go_out=. dataitem.proto标志 --go_out 指示编译器生成 Go 源代码。其他语言也有类似的标志。在这种情况下，结果是一个名为 dataitem.pb.go 的文件，该文件足够小，可以将其基本内容复制到 Go 应用程序中。以下是生成的代码的主要部分：var _ = proto.Marshal type DataItem struct { OddA int64 `protobuf:"varint,1,opt,name=oddA" json:"oddA,omitempty"` EvenA int64 `protobuf:"varint,2,opt,name=evenA" json:"evenA,omitempty"` OddB int32 `protobuf:"varint,3,opt,name=oddB" json:"oddB,omitempty"` EvenB int32 `protobuf:"varint,4,opt,name=evenB" json:"evenB,omitempty"` Small float32 `protobuf:"fixed32,5,opt,name=small" json:"small,omitempty"` Big float32 `protobuf:"fixed32,6,opt,name=big" json:"big,omitempty"` Short string `protobuf:"bytes,7,opt,name=short" json:"short,omitempty"` Long string `protobuf:"bytes,8,opt,name=long" json:"long,omitempty"` } func (m *DataItem) Reset() { *m = DataItem{} } func (m *DataItem) String() string { return proto.CompactTextString(m) } func (*DataItem) ProtoMessage() {} func init() {}编译器生成的代码具有 Go 结构 DataItem，该结构导出 Go 字段（名称现已大写开头），该字段与 Protobuf IDL 中声明的名称匹配。该结构字段具有标准的 Go 数据类型：int32、int64、float32 和 string。在每个字段行的末尾，是描述 Protobuf 类型的字符串，提供 Protobuf IDL 文档中的数字标签及有关 JSON 信息的元数据，这将在后面讨论。此外也有函数；最重要的是 Proto.Marshal，用于将 DataItem 结构的实例序列化为 Protobuf 格式。辅助函数包括：清除 DataItem 结构的 Reset，生成 DataItem 的单行字符串表示的 String。描述 Protobuf 编码的元数据应在更详细地分析 Go 程序之前进行仔细研究。Protobuf 编码Protobuf 消息的结构为键/值对的集合，其中数字标签为键，相应的字段为值。字段名称（例如，oddA 和 small）是供人类阅读的，但是 protoc 编译器的确使用了字段名称来生成特定于语言的对应名称。例如，Protobuf IDL 中的 oddA 和 small 名称在 Go 结构中分别成为字段 OddA 和 Small。键和它们的值都被编码，但是有一个重要的区别：一些数字值具有固定大小的 32 或 64 位的编码，而其他数字（包括消息标签）则是 varint 编码的，位数取决于整数的绝对值。例如，整数值 1 到 15 需要 8 位 varint 编码，而值 16 到 2047 需要 16 位。varint 编码在本质上与 UTF-8 编码类似（但细节不同），它偏爱较小的整数值而不是较大的整数值。（有关详细分析，请参见 Protobuf 编码指南）结果是，Protobuf 消息应该在字段中具有较小的整数值（如果可能），并且键数应尽可能少，但每个字段至少得有一个键。下表 1 列出了 Protobuf 编码的要点：编码示例类型长度varintint32、uint32、int64可变长度fixedfixed32、float、double固定的 32 位或 64 位长度字节序列string、bytes序列长度表 1. Protobuf 数据类型未明确固定长度的整数类型是 varint 编码的；因此，在 varint 类型中，例如 uint32（u 代表无符号），数字 32 描述了整数的范围（在这种情况下为 0 到 2 32 - 1），而不是其位的大小，该位大小取决于值。相比之下，对于固定长度类型（例如 fixed32 或 double），Protobuf 编码分别需要 32 位和 64 位。Protobuf 中的字符串是字节序列；因此，字段编码的大小就是字节序列的长度。另一个高效的方法值得一提。回想一下前面的示例，其中的 DataItems 消息由重复的 DataItem 实例组成：message DataItems { repeated DataItem item = 1; }repeated 表示 DataItem 实例是打包的：集合具有单个标签，在这里是 1。因此，具有重复的 DataItem 实例的 DataItems 消息比具有多个但单独的 DataItem 字段、每个字段都需要自己的标签的消息的效率更高。了解了这一背景，让我们回到 Go 程序。dataItem 程序的细节dataItem 程序创建一个 DataItem 实例，并使用适当类型的随机生成的值填充字段。Go 有一个 rand 包，带有用于生成伪随机整数和浮点值的函数，而我的 randString 函数可以从字符集中生成指定长度的伪随机字符串。设计目标是要有一个具有不同类型和位大小的字段值的 DataItem 实例。例如，OddA 和 EvenA 值分别是 64 位非负整数值的奇数和偶数；但是 OddB 和 EvenB 变体的大小为 32 位，并存放 0 到 2047 之间的小整数值。随机浮点值的大小为 32 位，字符串为 16（Short）和 32（Long）字符的长度。这是用随机值填充 DataItem 结构的代码段：// 可变长度整数 n1 := rand.Int63() // 大整数 if (n1 & 1) == 0 { n1++ } // 确保其是奇数 ... n3 := rand.Int31() % UpperBound // 小整数 if (n3 & 1) == 0 { n3++ } // 确保其是奇数 // 固定长度浮点数 ... t1 := rand.Float32() t2 := rand.Float32() ... // 字符串 str1 := randString(StrShort) str2 := randString(StrLong) // 消息 dataItem := &DataItem { OddA: n1, EvenA: n2, OddB: n3, EvenB: n4, Big: f1, Small: f2, Short: str1, Long: str2, }创建并填充值后，DataItem 实例将以 XML、JSON 和 Protobuf 进行编码，每种编码均写入本地文件：func encodeAndserialize(dataItem *DataItem) { bytes, _ := xml.MarshalIndent(dataItem, "", " ") // Xml to dataitem.xml ioutil.WriteFile(XmlFile, bytes, 0644) // 0644 is file access permissions bytes, _ = json.MarshalIndent(dataItem, "", " ") // Json to dataitem.json ioutil.WriteFile(JsonFile, bytes, 0644) bytes, _ = proto.Marshal(dataItem) // Protobuf to dataitem.pbuf ioutil.WriteFile(PbufFile, bytes, 0644) }这三个序列化函数使用术语 marshal，它与 serialize 意思大致相同。如代码所示，三个 Marshal 函数均返回一个字节数组，然后将其写入文件。（为简单起见，忽略可能的错误处理。）在示例运行中，文件大小为：dataitem.xml: 262 bytes dataitem.json: 212 bytes dataitem.pbuf: 88 bytesProtobuf 编码明显小于其他两个编码方案。通过消除缩进字符（在这种情况下为空白和换行符），可以稍微减小 XML 和 JSON 序列化的大小。以下是 dataitem.json 文件，该文件最终是由 json.MarshalIndent 调用产生的，并添加了以 ## 开头的注释：{ "oddA": 4744002665212642479, ## 64-bit >= 0 "evenA": 2395006495604861128, ## ditto "oddB": 57, ## 32-bit >= 0 but < 2048 "evenB": 468, ## ditto "small": 0.7562016, ## 32-bit floating-point "big": 0.85202795, ## ditto "short": "ClH1oDaTtoX$HBN5", ## 16 random chars "long": "xId0rD3Cri%3Wt%^QjcFLJgyXBu9^DZI" ## 32 random chars }尽管这些序列化的数据写入到本地文件中，但是也可以使用相同的方法将数据写入网络连接的输出流。测试序列化和反序列化Go 程序接下来通过将先前写入 dataitem.pbuf 文件的字节反序列化为 DataItem 实例来运行基本测试。这是代码段，其中去除了错误检查部分：filebytes, err := ioutil.ReadFile(PbufFile) // get the bytes from the file ... testItem.Reset() // clear the DataItem structure err = proto.Unmarshal(filebytes, testItem) // deserialize into a DataItem instance用于 Protbuf 反序列化的 proto.Unmarshal 函数与 proto.Marshal 函数相反。原始的 DataItem 和反序列化的副本将被打印出来以确认完全匹配：Original: 2041519981506242154 3041486079683013705 1192 1879 0.572123 0.326855 boPb#T0O8Xd&Ps5EnSZqDg4Qztvo7IIs 9vH66AiGSQgCDxk& Deserialized: 2041519981506242154 3041486079683013705 1192 1879 0.572123 0.326855 boPb#T0O8Xd&Ps5EnSZqDg4Qztvo7IIs 9vH66AiGSQgCDxk&一个 Java Protobuf 客户端用 Java 写的示例是为了确认 Protobuf 的语言中立性。原始 IDL 文件可用于生成 Java 支持代码，其中涉及嵌套类。但是，为了抑制警告信息，可以进行一些补充。这是修订版，它指定了一个 DataMsg 作为外部类的名称，内部类在该 Protobuf 消息后面自动命名为 DataItem：syntax = "proto3"; package main; option java_outer_classname = "DataMsg"; message DataItem { ...进行此更改后，protoc 编译与以前相同，只是所期望的输出现在是 Java 而不是 Go：% protoc --java_out=. dataitem.proto生成的源文件（在名为 main 的子目录中）为 DataMsg.java，长度约为 1,120 行：Java 并不简洁。编译然后运行 Java 代码需要具有 Protobuf 库支持的 JAR 文件。该文件位于 Maven 存储库中。放置好这些片段后，我的测试代码相对较短（并且在 ZIP 文件中以 Main.java 形式提供）：package main; import java.io.FileInputStream; public class Main { public static void main(String[] args) { String path = "dataitem.pbuf"; // from the Go program's serialization try { DataMsg.DataItem deserial = DataMsg.DataItem.newBuilder().mergeFrom(new FileInputStream(path)).build(); System.out.println(deserial.getOddA()); // 64-bit odd System.out.println(deserial.getLong()); // 32-character string } catch(Exception e) { System.err.println(e); } } }当然，生产级的测试将更加彻底，但是即使是该初步测试也可以证明 Protobuf 的语言中立性：dataitem.pbuf 文件是 Go 程序对 Go 语言版的 DataItem 进行序列化的结果，并且该文件中的字节被反序列化以产生一个 Java 语言的 DataItem 实例。Java 测试的输出与 Go 测试的输出相同。用 numPairs 程序来结束让我们以一个示例作为结尾，来突出 Protobuf 效率，但又强调在任何编码技术中都会涉及到的成本。考虑以下 Protobuf IDL 文件：syntax = "proto3"; package main; message NumPairs { repeated NumPair pair = 1; } message NumPair { int32 odd = 1; int32 even = 2; }NumPair 消息由两个 int32 值以及每个字段的整数标签组成。NumPairs 消息是嵌入的 NumPair 消息的序列。Go 语言的 numPairs 程序（如下）创建了 200 万个 NumPair 实例，每个实例都附加到 NumPairs 消息中。该消息可以按常规方式进行序列化和反序列化。示例 2、numPairs 程序package main import ( "math/rand" "time" "encoding/xml" "encoding/json" "io/ioutil" "github.com/golang/protobuf/proto" ) // protoc-generated code: start var _ = proto.Marshal type NumPairs struct { Pair []*NumPair `protobuf:"bytes,1,rep,name=pair" json:"pair,omitempty"` } func (m *NumPairs) Reset() { *m = NumPairs{} } func (m *NumPairs) String() string { return proto.CompactTextString(m) } func (*NumPairs) ProtoMessage() {} func (m *NumPairs) GetPair() []*NumPair { if m != nil { return m.Pair } return nil } type NumPair struct { Odd int32 `protobuf:"varint,1,opt,name=odd" json:"odd,omitempty"` Even int32 `protobuf:"varint,2,opt,name=even" json:"even,omitempty"` } func (m *NumPair) Reset() { *m = NumPair{} } func (m *NumPair) String() string { return proto.CompactTextString(m) } func (*NumPair) ProtoMessage() {} func init() {} // protoc-generated code: finish var numPairsStruct NumPairs var numPairs = &numPairsStruct func encodeAndserialize() { // XML encoding filename := "./pairs.xml" bytes, _ := xml.MarshalIndent(numPairs, "", " ") ioutil.WriteFile(filename, bytes, 0644) // JSON encoding filename = "./pairs.json" bytes, _ = json.MarshalIndent(numPairs, "", " ") ioutil.WriteFile(filename, bytes, 0644) // ProtoBuf encoding filename = "./pairs.pbuf" bytes, _ = proto.Marshal(numPairs) ioutil.WriteFile(filename, bytes, 0644) } const HowMany = 200 * 100 * 100 // two million func main() { rand.Seed(time.Now().UnixNano()) // uncomment the modulus operations to get the more efficient version for i := 0; i < HowMany; i++ { n1 := rand.Int31() // % 2047 if (n1 & 1) == 0 { n1++ } // ensure it's odd n2 := rand.Int31() // % 2047 if (n2 & 1) == 1 { n2++ } // ensure it's even next := &NumPair { Odd: n1, Even: n2, } numPairs.Pair = append(numPairs.Pair, next) } encodeAndserialize() }每个 NumPair 中随机生成的奇数和偶数值的范围在 0 到 20 亿之间变化。就原始数据（而非编码数据）而言，Go 程序中生成的整数总共为 16MB：每个 NumPair 为两个整数，总计为 400 万个整数，每个值的大小为四个字节。为了进行比较，下表列出了 XML、JSON 和 Protobuf 编码的示例 NumsPairs 消息的 200 万个 NumPair 实例。原始数据也包括在内。由于 numPairs 程序生成随机值，因此样本运行的输出有所不同，但接近表中显示的大小。编码文件字节大小Pbuf/其它 比例无pairs.raw16MB169%Protobufpairs.pbuf27MB—JSONpairs.json100MB27%XMLpairs.xml126MB21%表 2. 16MB 整数的编码开销不出所料，Protobuf 和之后的 XML 和 JSON 差别明显。Protobuf 编码大约是 JSON 的四分之一，是 XML 的五分之一。但是原始数据清楚地表明 Protobuf 也会产生编码开销：序列化的 Protobuf 消息比原始数据大 11MB。包括 Protobuf 在内的任何编码都涉及结构化数据，这不可避免地会增加字节。序列化的 200 万个 NumPair 实例中的每个实例都包含四个整数值：Go 结构中的 Even 和 Odd 字段分别一个，而 Protobuf 编码中的每个字段、每个标签一个。对于原始数据（而不是编码数据），每个实例将达到 16 个字节，样本 NumPairs 消息中有 200 万个实例。但是 Protobuf 标记（如 NumPair 字段中的 int32 值）使用 varint 编码，因此字节长度有所不同。特别是，小的整数值（在这种情况下，包括标签在内）需要不到四个字节进行编码。如果对 numPairs 程序进行了修改，以使两个 NumPair 字段的值小于 2048，且其编码为一或两个字节，则 Protobuf 编码将从 27MB 下降到 16MB，这正是原始数据的大小。下表总结了样本运行中的新编码大小。编码文件字节大小Pbuf/其它 比例Nonepairs.raw16MB100%Protobufpairs.pbuf16MB—JSONpairs.json77MB21%XMLpairs.xml103MB15%表 3. 编码 16MB 的小于 2048 的整数总之，修改后的 numPairs 程序的字段值小于 2048，可减少原始数据中每个四字节整数值的大小。但是 Protobuf 编码仍然需要标签，这些标签会在 Protobuf 消息中添加字节。Protobuf 编码确实会增加消息大小，但是如果要编码相对较小的整数值（无论是字段还是键），则可以通过 varint 因子来减少此开销。对于包含混合类型的结构化数据（且整数值相对较小）的中等大小的消息，Protobuf 明显优于 XML 和 JSON 等选项。在其他情况下，数据可能不适合 Protobuf 编码。例如，如果两个应用程序需要共享大量文本记录或大整数值，则可以采用压缩而不是编码技术。via: https://opensource.com/article/19/10/protobuf-data-interchange作者：Marty Kalin 选题：lujun9972 译者：wxy 校对：wxy本文由 LCTT 原创编译，Linux中国 荣誉推出
学习在 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 bytes */ saddr.sin_family = AF_INET; /* versus AF_LOCAL */ saddr.sin_addr.s_addr = htonl(INADDR_ANY); /* host-to-network endian */ saddr.sin_port = htons(PortNumber); /* for listening */ if (bind(fd, (struct sockaddr *) &saddr, sizeof(saddr)) < 0) report("bind", 1); /* terminate */ /* listen to the socket */ if (listen(fd, MaxConnects) < 0) /* listen for clients, up to MaxConnects */ report("listen", 1); /* terminate */ fprintf(stderr, "Listening on port %i for clients...\n", PortNumber); /* a server traditionally listens indefinitely */ while (1) { struct sockaddr_in caddr; /* client address */ int len = sizeof(caddr); /* address length could change */ int client_fd = accept(fd, (struct sockaddr*) &caddr, &len); /* accept blocks */ if (client_fd < 0) { report("accept", 0); /* don't terminated, though there's a problem */ continue; } /* read from client */ int i; for (i = 0; i < ConversationLen; i++) { char buffer[BuffSize + 1]; memset(buffer, '\0', sizeof(buffer)); int count = read(client_fd, buffer, sizeof(buffer)); if (count > 0) { puts(buffer); write(client_fd, buffer, sizeof(buffer)); /* echo as confirmation */ } } close(client_fd); /* break connection */ } /* while(1) */ return 0; }上面的服务器端程序执行典型的 4 个步骤来准备回应客户端的请求，然后接受其他的独立请求。这里每一个步骤都以服务器端程序调用的系统函数来命名。socket(…)：为套接字连接获取一个文件描述符bind(…)：将套接字和服务器主机上的一个地址进行绑定listen(…)：监听客户端请求accept(…)：接受一个特定的客户端请求上面的 socket 调用的完整形式为：int sockfd = socket(AF_INET, /* versus AF_LOCAL */ SOCK_STREAM, /* reliable, bidirectional */ 0); /* system picks protocol (TCP) */第一个参数特别指定了使用的是一个网络套接字，而不是 IPC 套接字。对于第二个参数有多种选项，但 SOCK_STREAM 和 SOCK_DGRAM（数据报）是最为常用的。基于流的套接字支持可信通道，在这种通道中如果发生了信息的丢失或者更改，都将会被报告。这种通道是双向的，并且从一端到另外一端的有效载荷在大小上可以是任意的。相反的，基于数据报的套接字大多是不可信的，没有方向性，并且需要固定大小的载荷。socket 的第三个参数特别指定了协议。对于这里展示的基于流的套接字，只有一种协议选择：TCP，在这里表示的 0。因为对 socket 的一次成功调用将返回相似的文件描述符，套接字可以被读写，对应的语法和读写一个本地文件是类似的。对 bind 的调用是最为复杂的，因为它反映出了在套接字 API 方面上的各种改进。我们感兴趣的点是这个调用将一个套接字和服务器端所在机器中的一个内存地址进行绑定。但对 listen 的调用就非常直接了：if (listen(fd, MaxConnects) < 0)第一个参数是套接字的文件描述符，第二个参数则指定了在服务器端处理一个拒绝连接错误之前，有多少个客户端连接被允许连接。（在头文件 sock.h 中 MaxConnects 的值被设置为 8。）accept 调用默认将是一个阻塞等待：服务器端将不做任何事情直到一个客户端尝试连接它，然后进行处理。accept 函数返回的值如果是 -1 则暗示有错误发生。假如这个调用是成功的，则它将返回另一个文件描述符，这个文件描述符被用来指代另一个可读可写的套接字，它与 accept 调用中的第一个参数对应的接收套接字有所不同。服务器端使用这个可读可写的套接字来从客户端读取请求然后写回它的回应。接收套接字只被用于接受客户端的连接。在设计上，服务器端可以一直运行下去。当然服务器端可以通过在命令行中使用 Ctrl+C 来终止它。示例 2. 使用套接字的客户端#include <string.h> #include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <sys/types.h> #include <sys/socket.h> #include <arpa/inet.h> #include <netinet/in.h> #include <netinet/tcp.h> #include <netdb.h> #include "sock.h" const char* books[] = {"War and Peace", "Pride and Prejudice", "The Sound and the Fury"}; void report(const char* msg, int terminate) { perror(msg); if (terminate) exit(-1); /* failure */ } int main() { /* fd for the socket */ int sockfd = socket(AF_INET, /* versus AF_LOCAL */ SOCK_STREAM, /* reliable, bidirectional */ 0); /* system picks protocol (TCP) */ if (sockfd < 0) report("socket", 1); /* terminate */ /* get the address of the host */ struct hostent* hptr = gethostbyname(Host); /* localhost: 127.0.0.1 */ if (!hptr) report("gethostbyname", 1); /* is hptr NULL? */ if (hptr->h_addrtype != AF_INET) /* versus AF_LOCAL */ report("bad address family", 1); /* connect to the server: configure server's address 1st */ struct sockaddr_in saddr; memset(&saddr, 0, sizeof(saddr)); saddr.sin_family = AF_INET; saddr.sin_addr.s_addr = ((struct in_addr*) hptr->h_addr_list[0])->s_addr; saddr.sin_port = htons(PortNumber); /* port number in big-endian */ if (connect(sockfd, (struct sockaddr*) &saddr, sizeof(saddr)) < 0) report("connect", 1); /* Write some stuff and read the echoes. */ puts("Connect to server, about to write some stuff..."); int i; for (i = 0; i < ConversationLen; i++) { if (write(sockfd, books[i], strlen(books[i])) > 0) { /* get confirmation echoed from server and print */ char buffer[BuffSize + 1]; memset(buffer, '\0', sizeof(buffer)); if (read(sockfd, buffer, sizeof(buffer)) > 0) puts(buffer); } } puts("Client done, about to exit..."); close(sockfd); /* close the connection */ return 0; }客户端程序的设置代码和服务器端类似。两者主要的区别既不是在于监听也不在于接收，而是连接：if (connect(sockfd, (struct sockaddr*) &saddr, sizeof(saddr)) < 0)对 connect 的调用可能因为多种原因而导致失败，例如客户端拥有错误的服务器端地址或者已经有太多的客户端连接上了服务器端。假如 connect 操作成功，客户端将在一个 for 循环中，写入它的请求然后读取返回的响应。在会话后，服务器端和客户端都将调用 close 去关闭这个可读可写套接字，尽管任何一边的关闭操作就足以关闭它们之间的连接。此后客户端可以退出了，但正如前面提到的那样，服务器端可以一直保持开放以处理其他事务。从上面的套接字示例中，我们看到了请求信息被回显给客户端，这使得客户端和服务器端之间拥有进行丰富对话的可能性。也许这就是套接字的主要魅力。在现代系统中，客户端应用（例如一个数据库客户端）和服务器端通过套接字进行通信非常常见。正如先前提及的那样，本地 IPC 套接字和网络套接字只在某些实现细节上面有所不同，一般来说，IPC 套接字有着更低的消耗和更好的性能。它们的通信 API 基本是一样的。信号信号会中断一个正在执行的程序，在这种意义下，就是用信号与这个程序进行通信。大多数的信号要么可以被忽略（阻塞）或者被处理（通过特别设计的代码）。SIGSTOP （暂停）和 SIGKILL（立即停止）是最应该提及的两种信号。这种符号常量有整数类型的值，例如 SIGKILL 对应的值为 9。信号可以在与用户交互的情况下发生。例如，一个用户从命令行中敲了 Ctrl+C 来终止一个从命令行中启动的程序；Ctrl+C 将产生一个 SIGTERM 信号。SIGTERM 意即终止，它可以被阻塞或者被处理，而不像 SIGKILL 信号那样。一个进程也可以通过信号和另一个进程通信，这样使得信号也可以作为一种 IPC 机制。考虑一下一个多进程应用，例如 Nginx 网络服务器是如何被另一个进程优雅地关闭的。kill 函数：int kill(pid_t pid, int signum); /* declaration */可以被一个进程用来终止另一个进程或者一组进程。假如 kill 函数的第一个参数是大于 0 的，那么这个参数将会被认为是目标进程的 pid（进程 ID），假如这个参数是 0，则这个参数将会被视作信号发送者所属的那组进程。kill 的第二个参数要么是一个标准的信号数字（例如 SIGTERM 或 SIGKILL），要么是 0 ，这将会对信号做一次询问，确认第一个参数中的 pid 是否是有效的。这样优雅地关闭一个多进程应用就可以通过向组成该应用的一组进程发送一个终止信号来完成，具体来说就是调用一个 kill 函数，使得这个调用的第二个参数是 SIGTERM 。（Nginx 主进程可以通过调用 kill 函数来终止其他工人进程，然后再停止自己。）就像许多库函数一样，kill 函数通过一个简单的可变语法拥有更多的能力和灵活性。示例 3. 一个多进程系统的优雅停止#include <stdio.h> #include <signal.h> #include <stdlib.h> #include <unistd.h> #include <sys/wait.h> void graceful(int signum) { printf("\tChild confirming received signal: %i\n", signum); puts("\tChild about to terminate gracefully..."); sleep(1); puts("\tChild terminating now..."); _exit(0); /* fast-track notification of parent */ } void set_handler() { struct sigaction current; sigemptyset(&current.sa_mask); /* clear the signal set */ current.sa_flags = 0; /* enables setting sa_handler, not sa_action */ current.sa_handler = graceful; /* specify a handler */ sigaction(SIGTERM, &current, NULL); /* register the handler */ } void child_code() { set_handler(); while (1) { /` loop until interrupted `/ sleep(1); puts("\tChild just woke up, but going back to sleep."); } } void parent_code(pid_t cpid) { puts("Parent sleeping for a time..."); sleep(5); /* Try to terminate child. */ if (-1 == kill(cpid, SIGTERM)) { perror("kill"); exit(-1); } wait(NULL); /` wait for child to terminate `/ puts("My child terminated, about to exit myself..."); } int main() { pid_t pid = fork(); if (pid < 0) { perror("fork"); return -1; /* error */ } if (0 == pid) child_code(); else parent_code(pid); return 0; /* normal */ }上面的停止程序模拟了一个多进程系统的优雅退出，在这个例子中，这个系统由一个父进程和一个子进程组成。这次模拟的工作流程如下：父进程尝试去 fork 一个子进程。假如这个 fork 操作成功了，每个进程就执行它自己的代码：子进程就执行函数 child_code，而父进程就执行函数 parent_code。子进程将会进入一个潜在的无限循环，在这个循环中子进程将睡眠一秒，然后打印一个信息，接着再次进入睡眠状态，以此循环往复。来自父进程的一个 SIGTERM 信号将引起子进程去执行一个信号处理回调函数 graceful。这样这个信号就使得子进程可以跳出循环，然后进行子进程和父进程之间的优雅终止。在终止之前，进程将打印一个信息。在 fork 一个子进程后，父进程将睡眠 5 秒，使得子进程可以执行一会儿；当然在这个模拟中，子进程大多数时间都在睡眠。然后父进程调用 SIGTERM 作为第二个参数的 kill 函数，等待子进程的终止，然后自己再终止。下面是一次运行的输出：% ./shutdown Parent sleeping for a time... Child just woke up, but going back to sleep. Child just woke up, but going back to sleep. Child just woke up, but going back to sleep. Child just woke up, but going back to sleep. Child confirming received signal: 15 ## SIGTERM is 15 Child about to terminate gracefully... Child terminating now... My child terminated, about to exit myself...对于信号的处理，上面的示例使用了 sigaction 库函数（POSIX 推荐的用法）而不是传统的 signal 函数，signal 函数有移植性问题。下面是我们主要关心的代码片段：假如对 fork 的调用成功了，父进程将执行 parent_code 函数，而子进程将执行 child_code 函数。在给子进程发送信号之前，父进程将会等待 5 秒：puts("Parent sleeping for a time..."); sleep(5); if (-1 == kill(cpid, SIGTERM)) { ...sleepkillcpidSIGTERM...假如 kill 调用成功了，父进程将在子进程终止时做等待，使得子进程不会变成一个僵尸进程。在等待完成后，父进程再退出。child_code 函数首先调用 set_handler 然后进入它的可能永久睡眠的循环。下面是我们将要查看的 set_handler 函数：void set_handler() { struct sigaction current; /* current setup */ sigemptyset(&current.sa_mask); /* clear the signal set */ current.sa_flags = 0; /* for setting sa_handler, not sa_action */ current.sa_handler = graceful; /* specify a handler */ sigaction(SIGTERM, &current, NULL); /* register the handler */ }上面代码的前三行在做相关的准备。第四个语句将为 graceful 设定为句柄，它将在调用 _exit 来停止之前打印一些信息。第 5 行和最后一行的语句将通过调用 sigaction 来向系统注册上面的句柄。sigaction 的第一个参数是 SIGTERM ，用作终止；第二个参数是当前的 sigaction 设定，而最后的参数（在这个例子中是 NULL ）可被用来保存前面的 sigaction 设定，以备后面的可能使用。使用信号来作为 IPC 的确是一个很轻量的方法，但确实值得尝试。通过信号来做 IPC 显然可以被归入 IPC 工具箱中。这个系列的总结在这个系列中，我们通过三篇有关 IPC 的文章，用示例代码介绍了如下机制：共享文件共享内存（通过信号量）管道（命名和无名）消息队列套接字信号甚至在今天，在以线程为中心的语言，例如 Java、C# 和 Go 等变得越来越流行的情况下，IPC 仍然很受欢迎，因为相比于使用多线程，通过多进程来实现并发有着一个明显的优势：默认情况下，每个进程都有它自己的地址空间，除非使用了基于共享内存的 IPC 机制（为了达到安全的并发，竞争条件在多线程和多进程的时候必须被加上锁），在多进程中可以排除掉基于内存的竞争条件。对于任何一个写过即使是基本的通过共享变量来通信的多线程程序的人来说，他都会知道想要写一个清晰、高效、线程安全的代码是多么具有挑战性。使用单线程的多进程的确是很有吸引力的，这是一个切实可行的方式，使用它可以利用好今天多处理器的机器，而不需要面临基于内存的竞争条件的风险。当然，没有一个简单的答案能够回答上述 IPC 机制中的哪一个更好。在编程中每一种 IPC 机制都会涉及到一个取舍问题：是追求简洁，还是追求功能强大。以信号来举例，它是一个相对简单的 IPC 机制，但并不支持多个进程之间的丰富对话。假如确实需要这样的对话，另外的选择可能会更合适一些。带有锁的共享文件则相对直接，但是当要处理大量共享的数据流时，共享文件并不能很高效地工作。管道，甚至是套接字，有着更复杂的 API，可能是更好的选择。让具体的问题去指导我们的选择吧。尽管所有的示例代码（可以在我的网站上获取到）都是使用 C 写的，其他的编程语言也经常提供这些 IPC 机制的轻量包装。这些代码示例都足够短小简单，希望这样能够鼓励你去进行实验。via: https://opensource.com/article/19/4/interprocess-communication-linux-networking作者：Marty Kalin 选题：lujun9972 译者：FSSlc 校对：wxy本文由 LCTT 原创编译，Linux中国 荣誉推出
Linux 下的进程间通信：使用管道和消息队列
学习在 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 descriptors */ char buf; /* 1-byte buffer */ const char* msg = "Nature's first green is gold\n"; /* bytes to write */ if (pipe(pipeFDs) < 0) report_and_exit("pipeFD"); pid_t cpid = fork(); /* fork a child process */ if (cpid < 0) report_and_exit("fork"); /* check for failure */ if (0 == cpid) { /*** child ***/ /* child process */ close(pipeFDs[WriteEnd]); /* child reads, doesn't write */ while (read(pipeFDs[ReadEnd], &buf, 1) > 0) /* read until end of byte stream */ write(STDOUT_FILENO, &buf, sizeof(buf)); /* echo to the standard output */ close(pipeFDs[ReadEnd]); /* close the ReadEnd: all done */ _exit(0); /* exit and notify parent at once */ } else { /*** parent ***/ close(pipeFDs[ReadEnd]); /* parent writes, doesn't read */ write(pipeFDs[WriteEnd], msg, strlen(msg)); /* write the bytes to the pipe */ close(pipeFDs[WriteEnd]); /* done writing: generate eof */ wait(NULL); /* wait for child to exit */ exit(0); /* exit normally */ } return 0; }上面名为 pipeUN 的程序使用系统函数 fork 来创建一个进程。尽管这个程序只有一个单一的源文件，在它正确执行的情况下将会发生多进程的情况。下面的内容是对库函数 fork 如何工作的一个简要回顾：fork 函数由父进程调用，在失败时返回 -1 给父进程。在 pipeUN 这个例子中，相应的调用是：pid_t cpid = fork(); /* called in parent */函数调用后的返回值也被保存下来了。在这个例子中，保存在整数类型 pid_t 的变量 cpid 中。（每个进程有它自己的进程 ID，这是一个非负的整数，用来标记进程）。复刻一个新的进程可能会因为多种原因而失败，包括进程表满了的原因，这个结构由系统维持，以此来追踪进程状态。明确地说，僵尸进程假如没有被处理掉，将可能引起进程表被填满的错误。假如 fork 调用成功，则它将创建一个新的子进程，向父进程返回一个值，向子进程返回另外的一个值。在调用 fork 后父进程和子进程都将执行相同的代码。（子进程继承了到此为止父进程中声明的所有变量的拷贝），特别地，一次成功的 fork 调用将返回如下的东西：+ 向子进程返回 `0` + 向父进程返回子进程的进程 ID在一次成功的 fork 调用后，一个 if/else 或等价的结构将会被用来隔离针对父进程和子进程的代码。在这个例子中，相应的声明为：if (0 == cpid) { /*** child ***/ ... } else { /*** parent ***/ ... } 假如成功地复刻出了一个子进程，pipeUN 程序将像下面这样去执行。在一个整数的数列里：int pipeFDs[2]; /* two file descriptors */来保存两个文件描述符，一个用来向管道中写入，另一个从管道中写入。（数组元素 pipeFDs[0] 是读端的文件描述符，元素 pipeFDs[1] 是写端的文件描述符。）在调用 fork 之前，对系统 pipe 函数的成功调用，将立刻使得这个数组获得两个文件描述符：if (pipe(pipeFDs) < 0) report_and_exit("pipeFD");父进程和子进程现在都有了文件描述符的副本。但分离关注点模式意味着每个进程恰好只需要一个描述符。在这个例子中，父进程负责写入，而子进程负责读取，尽管这样的角色分配可以反过来。在 if 子句中的第一个语句将用于关闭管道的读端：close(pipeFDs[WriteEnd]); /* called in child code */在父进程中的 else 子句将会关闭管道的读端：close(pipeFDs[ReadEnd]); /* called in parent code */然后父进程将向无名管道中写入某些字节数据（ASCII 代码），子进程读取这些数据，然后向标准输出中回放它们。在这个程序中还需要澄清的一点是在父进程代码中的 wait 函数。一旦被创建后，子进程很大程度上独立于它的父进程，正如简短的 pipeUN 程序所展示的那样。子进程可以执行任意的代码，而它们可能与父进程完全没有关系。但是，假如当子进程终止时，系统将会通过一个信号来通知父进程。要是父进程在子进程之前终止又该如何呢？在这种情形下，除非采取了预防措施，子进程将会变成在进程表中的一个僵尸进程。预防措施有两大类型：第一种是让父进程去通知系统，告诉系统它对子进程的终止没有任何兴趣：signal(SIGCHLD, SIG_IGN); /* in parent: ignore notification */第二种方法是在子进程终止时，让父进程执行一个 wait。这样就确保了父进程可以独立于子进程而存在。在 pipeUN 程序中使用了第二种方法，其中父进程的代码使用的是下面的调用：wait(NULL); /* called in parent */这个对 wait 的调用意味着一直等待直到任意一个子进程的终止发生，因此在 pipeUN 程序中，只有一个子进程。（其中的 NULL 参数可以被替换为一个保存有子程序退出状态的整数变量的地址。）对于更细粒度的控制，还可以使用更灵活的 waitpid 函数，例如特别指定多个子进程中的某一个。pipeUN 将会采取另一个预防措施。当父进程结束了等待，父进程将会调用常规的 exit 函数去退出。对应的，子进程将会调用 _exit 变种来退出，这类变种将快速跟踪终止相关的通知。在效果上，子进程会告诉系统立刻去通知父进程它的这个子进程已经终止了。假如两个进程向相同的无名管道中写入内容，字节数据会交错吗？例如，假如进程 P1 向管道写入内容：foo bar同时进程 P2 并发地写入：baz baz到相同的管道，最后的结果似乎是管道中的内容将会是任意错乱的，例如像这样：baz foo baz bar只要没有写入超过 PIPE_BUF 字节，POSIX 标准就能确保写入不会交错。在 Linux 系统中， PIPE_BUF 的大小是 4096 字节。对于管道我更喜欢只有一个写入方和一个读取方，从而绕过这个问题。命名管道无名管道没有备份文件：系统将维持一个内存缓存来将字节数据从写方传给读方。一旦写方和读方终止，这个缓存将会被回收，进而无名管道消失。相反的，命名管道有备份文件和一个不同的 API。下面让我们通过另一个命令行示例来了解命名管道的要点。下面是具体的步骤：开启两个终端。这两个终端的工作目录应该相同。在其中一个终端中，键入下面的两个命令（命令行提示符仍然是 %，我的注释以 ## 打头。）：% mkfifo tester ## 创建一个备份文件，名为 tester % cat tester ## 将管道的内容输出到 stdout 在最开始，没有任何东西会出现在终端中，因为到现在为止没有在命名管道中写入任何东西。在第二个终端中输入下面的命令：% cat > tester ## redirect keyboard input to the pipe hello, world! ## then hit Return key bye, bye ## ditto <Control-C> ## terminate session with a Control-C无论在这个终端中输入什么，它都会在另一个终端中显示出来。一旦键入 Ctrl+C，就会回到正常的命令行提示符，因为管道已经被关闭了。通过移除实现命名管道的文件来进行清理：% unlink tester正如 mkfifo 程序的名字所暗示的那样，命名管道也被叫做 FIFO，因为第一个进入的字节，就会第一个出，其他的类似。有一个名为 mkfifo 的库函数，用它可以在程序中创建一个命名管道，它将在下一个示例中被用到，该示例由两个进程组成：一个向命名管道写入，而另一个从该管道读取。示例 2. fifoWriter 程序#include <sys/types.h> #include <sys/stat.h> #include <fcntl.h> #include <unistd.h> #include <time.h> #include <stdlib.h> #include <stdio.h> #define MaxLoops 12000 /* outer loop */ #define ChunkSize 16 /* how many written at a time */ #define IntsPerChunk 4 /* four 4-byte ints per chunk */ #define MaxZs 250 /* max microseconds to sleep */ int main() { const char* pipeName = "./fifoChannel"; mkfifo(pipeName, 0666); /* read/write for user/group/others */ int fd = open(pipeName, O_CREAT | O_WRONLY); /* open as write-only */ if (fd < 0) return -1; /* can't go on */ int i; for (i = 0; i < MaxLoops; i++) { /* write MaxWrites times */ int j; for (j = 0; j < ChunkSize; j++) { /* each time, write ChunkSize bytes */ int k; int chunk[IntsPerChunk]; for (k = 0; k < IntsPerChunk; k++) chunk[k] = rand(); write(fd, chunk, sizeof(chunk)); } usleep((rand() % MaxZs) + 1); /* pause a bit for realism */ } close(fd); /* close pipe: generates an end-of-stream marker */ unlink(pipeName); /* unlink from the implementing file */ printf("%i ints sent to the pipe.\n", MaxLoops * ChunkSize * IntsPerChunk); return 0; }上面的 fifoWriter 程序可以被总结为如下：首先程序创建了一个命名管道用来写入数据：mkfifo(pipeName, 0666); /* read/write perms for user/group/others */ int fd = open(pipeName, O_CREAT | O_WRONLY);其中的 pipeName 是备份文件的名字，传递给 mkfifo 作为它的第一个参数。接着命名管道通过我们熟悉的 open 函数调用被打开，而这个函数将会返回一个文件描述符。在实现层面上，fifoWriter 不会一次性将所有的数据都写入，而是写入一个块，然后休息随机数目的微秒时间，接着再循环往复。总的来说，有 768000 个 4 字节整数值被写入到命名管道中。在关闭命名管道后，fifoWriter 也将使用 unlink 取消对该文件的连接。close(fd); /* close pipe: generates end-of-stream marker */ unlink(pipeName); /* unlink from the implementing file */一旦连接到管道的每个进程都执行了 unlink 操作后，系统将回收这些备份文件。在这个例子中，只有两个这样的进程 fifoWriter 和 fifoReader，它们都做了 unlink 操作。这个两个程序应该在不同终端的相同工作目录中执行。但是 fifoWriter 应该在 fifoReader 之前被启动，因为需要 fifoWriter 去创建管道。然后 fifoReader 才能够获取到刚被创建的命名管道。示例 3. fifoReader 程序#include <stdio.h> #include <stdlib.h> #include <string.h> #include <fcntl.h> #include <unistd.h> unsigned is_prime(unsigned n) { /* not pretty, but efficient */ if (n <= 3) return n > 1; if (0 == (n % 2) || 0 == (n % 3)) return 0; unsigned i; for (i = 5; (i * i) <= n; i += 6) if (0 == (n % i) || 0 == (n % (i + 2))) return 0; return 1; /* found a prime! */ } int main() { const char* file = "./fifoChannel"; int fd = open(file, O_RDONLY); if (fd < 0) return -1; /* no point in continuing */ unsigned count = 0, total = 0, primes_count = 0; while (1) { int next; int i; ssize_t count = read(fd, &next, sizeof(int)); if (0 == count) break; /* end of stream */ else if (count == sizeof(int)) { /* read a 4-byte int value */ total++; if (is_prime(next)) primes_count++; } } close(fd); /* close pipe from read end */ unlink(file); /* unlink from the underlying file */ printf("Received ints: %u, primes: %u\n", total, primes_count); return 0; }上面的 fifoReader 的内容可以总结为如下：因为 fifoWriter 已经创建了命名管道，所以 fifoReader 只需要利用标准的 open 调用来通过备份文件来获取到管道中的内容：const char* file = "./fifoChannel"; int fd = open(file, O_RDONLY);这个文件的是以只读打开的。然后这个程序进入一个潜在的无限循环，在每次循环时，尝试读取 4 字节的块。read 调用：ssize_t count = read(fd, &next, sizeof(int));返回 0 来暗示该流的结束。在这种情况下，fifoReader 跳出循环，关闭命名管道，并在终止前 unlink 备份文件。在读入 4 字节整数后，fifoReader 检查这个数是否为质数。这个操作代表了一个生产级别的读取器可能在接收到的字节数据上执行的逻辑操作。在示例运行中，在接收到的 768000 个整数中有 37682 个质数。重复运行示例， fifoReader 将成功地读取 fifoWriter 写入的所有字节。这不是很让人惊讶的。这两个进程在相同的机器上执行，从而可以不用考虑网络相关的问题。命名管道是一个可信且高效的 IPC 机制，因而被广泛使用。下面是这两个程序的输出，它们在不同的终端中启动，但处于相同的工作目录：% ./fifoWriter 768000 ints sent to the pipe. ### % ./fifoReader Received ints: 768000, primes: 37682消息队列管道有着严格的先入先出行为：第一个被写入的字节将会第一个被读，第二个写入的字节将第二个被读，以此类推。消息队列可以做出相同的表现，但它又足够灵活，可以使得字节块可以不以先入先出的次序来接收。正如它的名字所提示的那样，消息队列是一系列的消息，每个消息包含两部分：荷载，一个字节序列（在 C 中是 char）类型，以一个正整数值的形式给定，类型用来分类消息，为了更灵活的回收看一下下面对一个消息队列的描述，每个消息由一个整数类型标记： +-+ +-+ +-+ +-+ sender--->|3|--->|2|--->|2|--->|1|--->receiver +-+ +-+ +-+ +-+在上面展示的 4 个消息中，标记为 1 的是开头，即最接近接收端，然后另个标记为 2 的消息，最后接着一个标记为 3 的消息。假如按照严格的 FIFO 行为执行，消息将会以 1-2-2-3 这样的次序被接收。但是消息队列允许其他收取次序。例如，消息可以被接收方以 3-2-1-2 的次序接收。mqueue 示例包含两个程序，sender 将向消息队列中写入数据，而 receiver 将从这个队列中读取数据。这两个程序都包含的头文件 queue.h 如下所示：示例 4. 头文件 queue.h#define ProjectId 123 #define PathName "queue.h" /* any existing, accessible file would do */ #define MsgLen 4 #define MsgCount 6 typedef struct { long type; /* must be of type long */ char payload[MsgLen + 1]; /* bytes in the message */ } queuedMessage;上面的头文件定义了一个名为 queuedMessage 的结构类型，它带有 payload（字节数组）和 type（整数）这两个域。该文件也定义了一些符号常数（使用 #define 语句），前两个常数被用来生成一个 key，而这个 key 反过来被用来获取一个消息队列的 ID。ProjectId 可以是任何正整数值，而 PathName 必须是一个存在的、可访问的文件，在这个示例中，指的是文件 queue.h。在 sender 和 receiver 中，它们都有的设定语句为：key_t key = ftok(PathName, ProjectId); /* generate key */ int qid = msgget(key, 0666 | IPC_CREAT); /* use key to get queue id */ID qid 在效果上是消息队列文件描述符的对应物。示例 5. sender 程序#include <stdio.h> #include <sys/ipc.h> #include <sys/msg.h> #include <stdlib.h> #include <string.h> #include "queue.h" void report_and_exit(const char* msg) { perror(msg); exit(-1); /* EXIT_FAILURE */ } int main() { key_t key = ftok(PathName, ProjectId); if (key < 0) report_and_exit("couldn't get key..."); int qid = msgget(key, 0666 | IPC_CREAT); if (qid < 0) report_and_exit("couldn't get queue id..."); char* payloads[] = {"msg1", "msg2", "msg3", "msg4", "msg5", "msg6"}; int types[] = {1, 1, 2, 2, 3, 3}; /* each must be > 0 */ int i; for (i = 0; i < MsgCount; i++) { /* build the message */ queuedMessage msg; msg.type = types[i]; strcpy(msg.payload, payloads[i]); /* send the message */ msgsnd(qid, &msg, sizeof(msg), IPC_NOWAIT); /* don't block */ printf("%s sent as type %i\n", msg.payload, (int) msg.type); } return 0; }上面的 sender 程序将发送出 6 个消息，每两个为一个类型：前两个是类型 1，接着的连个是类型 2，最后的两个为类型 3。发送的语句：msgsnd(qid, &msg, sizeof(msg), IPC_NOWAIT);被配置为非阻塞的（IPC_NOWAIT 标志），是因为这里的消息体量上都很小。唯一的危险在于一个完整的序列将可能导致发送失败，而这个例子不会。下面的 receiver 程序也将使用 IPC_NOWAIT 标志来接收消息。示例 6. receiver 程序#include <stdio.h> #include <sys/ipc.h> #include <sys/msg.h> #include <stdlib.h> #include "queue.h" void report_and_exit(const char* msg) { perror(msg); exit(-1); /* EXIT_FAILURE */ } int main() { key_t key= ftok(PathName, ProjectId); /* key to identify the queue */ if (key < 0) report_and_exit("key not gotten..."); int qid = msgget(key, 0666 | IPC_CREAT); /* access if created already */ if (qid < 0) report_and_exit("no access to queue..."); int types[] = {3, 1, 2, 1, 3, 2}; /* different than in sender */ int i; for (i = 0; i < MsgCount; i++) { queuedMessage msg; /* defined in queue.h */ if (msgrcv(qid, &msg, sizeof(msg), types[i], MSG_NOERROR | IPC_NOWAIT) < 0) puts("msgrcv trouble..."); printf("%s received as type %i\n", msg.payload, (int) msg.type); } /** remove the queue **/ if (msgctl(qid, IPC_RMID, NULL) < 0) /* NULL = 'no flags' */ report_and_exit("trouble removing queue..."); return 0; }这个 receiver 程序不会创建消息队列，尽管 API 尽管建议那样。在 receiver 中，对int qid = msgget(key, 0666 | IPC_CREAT);的调用可能因为带有 IPC_CREAT 标志而具有误导性，但是这个标志的真实意义是如果需要就创建，否则直接获取。sender 程序调用 msgsnd 来发送消息，而 receiver 调用 msgrcv 来接收它们。在这个例子中，sender 以 1-1-2-2-3-3 的次序发送消息，但 receiver 接收它们的次序为 3-1-2-1-3-2，这显示消息队列没有被严格的 FIFO 行为所拘泥：% ./sender msg1 sent as type 1 msg2 sent as type 1 msg3 sent as type 2 msg4 sent as type 2 msg5 sent as type 3 msg6 sent as type 3 % ./receiver msg5 received as type 3 msg1 received as type 1 msg3 received as type 2 msg2 received as type 1 msg6 received as type 3 msg4 received as type 2上面的输出显示 sender 和 receiver 可以在同一个终端中启动。输出也显示消息队列是持久的，即便 sender 进程在完成创建队列、向队列写数据、然后退出的整个过程后，该队列仍然存在。只有在 receiver 进程显式地调用 msgctl 来移除该队列，这个队列才会消失：if (msgctl(qid, IPC_RMID, NULL) < 0) /* remove queue */总结管道和消息队列的 API 在根本上来说都是单向的：一个进程写，然后另一个进程读。当然还存在双向命名管道的实现，但我认为这个 IPC 机制在它最为简单的时候反而是最佳的。正如前面提到的那样，消息队列已经不大受欢迎了，尽管没有找到什么特别好的原因来解释这个现象；而队列仍然是 IPC 工具箱中的一个工具。这个快速的 IPC 工具箱之旅将以第 3 部分（通过套接字和信号来示例 IPC）来终结。via: https://opensource.com/article/19/4/interprocess-communication-linux-channels作者：Marty Kalin 选题：lujun9972 译者：FSSlc 校对：wxy本文由 LCTT 原创编译，Linux中国 荣誉推出