【酷壳】 Rust语言的编程范式

总是有很多很多人来问我对Rust语言怎么看的问题,在各种地方被at,其实,我不是很想表达我的想法。因为在不同的角度,你会看到不同的东西。编程语言这个东西,老实说很难评价,在学术上来说,Lisp就是很好的语言,然而在工程使用的时候,你会发现Lisp没什么人用,而Javascript或是PHP这样在学术很糟糕设计的语言反而成了主流,你觉得C++很反人类,在我看来,C++有很多不错的设计,而且对于了解编程语言和编译器的和原理非常有帮助。但是C++也很危险,所以,出现在像Java或Go 语言来改善它,Rust本质上也是在改善C++的。他们各自都有各自的长处和优势

因为各个语言都有好有不好,因此,我不想用别的语言来说Rust的问题,或是把Rust吹成朵花以打压别的语言,写成这样的文章,是很没有营养的事。本文主要想通过Rust的语言设计来看看编程中的一些挑战,尤其是Rust重要的一些编程范式,这样反而更有意义一些,因为这样你才可能一通百通

这篇文章的篇幅比较长,而且有很多代码,信息量可能会非常大,所以,在读本文前,你需要有如下的知识准备

  • 你对C++语言的一些特性和问题比较熟悉。尤其是:指针、引用、右值move、内存对象管理、泛型编程、智能指针……
  • 当然,你还要略懂Rust,不懂也没太大关系,但本文不会是Rust的教程文章,可以参看“Rust的官方教程”(中文版

因为本文太长,所以,我有必要写上 TL;DR ——

Java 与 Rust 在改善C/C++上走了完全不同的两条路,他们主要改善的问题就是C/C++ Safety的问题。所谓C/C++编程安全上的问题,主要是:内存的管理、数据在共享中出现的“野指针”、“野引用”的问题。

  • 对于这些问题,Java用引用垃圾回收再加上强大的VM字节码技术可以进行各种像反射、字节码修改的黑魔法。
  • 而Rust不玩垃圾回收,也不玩VM,所以,作为静态语言的它,只能在编译器上下工夫。如果要让编译器能够在编译时检查出一些安全问题,那么就需要程序员在编程上与Rust语言有一些约定了,其中最大的一个约定规则就是变量的所有权问题,并且速要在代码上“去糖”,比如让程序员说明一些共享引用的生命周期。
  • Rust的这些所有权的约定造成了很大的编程上的麻烦,写Rust的程序时,基本上来说,你的程序再也不要想可能轻轻松松能编译通过了。而且,在面对一些场景的代码编写时,如:函数式的闭包,多线程的不变数据的共享,多态……开始变得有些复杂,并会让你有种找不到北的感觉。
  • Rust的Trait很像Java的接口,通过Trait可以实现C++的拷贝构造、重载操作符、多态等操作……
  • 学习Rust的学习曲线并不平,用Rust写程序,基本上来说,一旦编译通过,代码运行起来是安全的,bug也是很少的。

如果你对Rust的概念认识的不完整,你完全写不出程序,那怕就是很简单的一段代码这逼着程序员必需了解所有的概念才能编码。但是,另一方面也表明了这门语言并不适合初学者……

变量的可变性

首先,Rust里的变量声明默认是“不可变的”,如果你声明一个变量 let x = 5;  变量 x 是不可变的,也就是说,x = y + 10; 编译器会报错的。如果你要变量的话,你需要使用 mut 关键词,也就是要声明成 let mut x = 5; 表示这是一个可以改变的变量。这个是比较有趣的,因为其它主流语言在声明变量时默认是可变的,而Rust则是要反过来。这可以理解,不可变的通常来说会有更好的稳定性,而可变的会代来不稳定性。所以,Rust应该是想成为更为安全的语言,所以,默认是 immutable 的变量。当然,Rust同样有 const 修饰的常量。于是,Rust可以玩出这么些东西来:

  • 常量:const LEN:u32 = 1024; 其中的 LEN 就是一个u32 的整型常量(无符号32位整型),是编译时用到的。
  • 可变的变量: let mut x = 5; 这个就跟其它语言的类似, 在运行时用到。
  • 不可变的变量:let x= 5; 对这种变量,你无论修改它,但是,你可以使用 let x = x + 10; 这样的方式来重新定义一个新的 x。这个在Rust里叫 Shadowing ,第二个 x  把第一个 x 给遮蔽了。

不可变的变量对于程序的稳定运行是有帮助的,这是一种编程“契约”,当处理契约为不可变的变量时,程序就可以稳定很多,尤其是多线程的环境下,因为不可变意味着只读不写,其他好处是,与易变对象相比,它们更易于理解和推理,并提供更高的安全性。有了这样的“契约”后,编译器也很容易在编译时查错了。这就是Rust语言的编译器的编译期可以帮你检查很多编程上的问题。

对于标识不可变的变量,在 C/C++中我们用const ,在Java中使用 final ,在 C#中使用 readonly ,Scala用 val ……(在Javascript 和Python这样的动态语言中,原始类型基本都是不可变的,而自定义类型是可变的)。

对于Rust的Shadowing,我个人觉得是比较危险的,在我的职业生涯中,这种使用同名变量(在嵌套的scope环境下)带来的bug还是很不好找的。一般来说,每个变量都应该有他最合适的名字,最好不要重名。

变量的所有权

这个是Rust这个语言中比较强调的一个概念。其实,在我们的编程中,很多情况下,都是把一个对象(变量)传递过来传递过去,在传递的过程中,传的是一份复本,还是这个对象本身,也就是所谓的“传值还是传引用”的被程序员问得最多的问题。

  • 传递副本(传值)。把一个对象的复本传到一个函数中,或是放到一个数据结构容器中,可能需要出现复制的操作,这个复制对于一个对象来说,需要深度复制才安全,否则就会出现各种问题。而深度复制就会导致性能问题。
  • 传递对象本身(传引用)。传引用也就是不需要考虑对象的复制成本,但是需要考虑对象在传递后,会多个变量所引用的问题。比如:我们把一个对象的引用传给一个List或其它的一个函数,这意味着,大家对同一个对象都有控制权,如果有一个人释放了这个对象,那边其它人就遭殃了,所以,一般会采用引用计数的方式来共享一个对象。引用除了共享的问题外,还有作用域的问题,比如:你从一个函数的栈内存中返回一个对象的引用给调用者,调用者就会收到一个被释放了个引用对象(因为函数结束后栈被清了)。

这些东西在任何一个编程语言中都是必需要解决的问题,要足够灵活到让程序员可以根据自己的需要来写程序。

在C++中,如果你要传递一个对象,有这么几种方式:

  • 引用或指针。也就是不建复本,完全共享,于是,但是会出现悬挂指针(Dangling Pointer)又叫野指针的问题,也就是一个指针或引用指向一块废弃的内存。为了解决这个问题,C++的解决方案是使用 share_ptr 这样的托管类来管理共享时的引用计数。
  • 传递复本,传递一个拷贝,需要重载对象的“拷贝构造函数”和“赋值构造函数”。
  • 移动Move。C++中,为了解决一些临时对象的构造的开销,可以使用Move操作,把一个对象的所有权移动到给另外一个对象,这个解决了C++中在传递对象时的会产生很多临时对象来影响性能的情况。

C++的这些个“神操作”,可以让你非常灵活地在各种情况下传递对象,但是也提升整体语言的复杂度。而Java直接把C/C++的指针给废了,用了更为安全的引用 ,然后为了解决多个引用共享同一个内存,内置了引用计数和垃圾回收,于是整个复杂度大大降低。对于Java要传对象的复本的话,需要定义一个通过自己构造自己的构造函数,或是通过prototype设计模式的 clone() 方法来进行,如果你要让Java解除引用,需要明显的把引用变量赋成 null 。总之,无论什么语言都需要这对象的传递这个事做好,不然,无论提供相对比较灵活编程方法。

在Rust中,Rust强化了“所有权”的概念,下面是Rust的所有者的三大铁律:

  1. Rust 中的每一个值都有一个被称为其 所有者owner)的变量。
  2. 值有且只有一个所有者。
  3. 当所有者(变量)离开作用域,这个值将被丢弃。

这意味着什么?

如果你需要传递一个对象的复本,你需要给这个对象实现 Copy trait ,trait 怎么翻译我也不知道,你可以认为是一个对象的一些特别的接口(可以用于一些对像操作上的约定,比如:Copy 用于复制(类型于C++的拷贝构造),Display 用于输出(类似于Java的 toString()),还有 Drop 和操作符重载等等,当然,也可以是对象的方法,或是用于多态的接口定义,后面会讲)。

对于内建的整型、布尔型、浮点型、字符型、多元组都被实现了 Copy 所以,在进行传递的时候,会进行memcpy 这样的复制(bit-wise式的浅拷贝)。而对于对象来说,则不行,在Rust的编程范式中,需要使用的是 Clone trait。

于是,CopyClone 这两个相似而又不一样的概念就出来了,Copy 主要是给内建类型,或是由内建类型全是支持 Copy 的对象,而 Clone 则是给程序员自己复制对象的。嗯,这就是浅拷贝和深拷贝的差别,Copy 告诉编译器,我这个对象可以进行 bit-wise的复制,而 Clone 则是指深度拷贝。

String 这样的内部需要在堆上分布内存的数据结构,是没有实现Copy 的(因为内部是一个指针,所以,语义上是深拷贝),需要复制的话,必需手动的调用其 clone() 方法,如果不这样的的话,当在进行函数参数传递,或是变量传递的时候,所有权一下就转移了,而之前的变量什么也不是了(这里编译器会帮你做检查有没有使用到所有权被转走的变量)。这个相当于C++的Move语义。

参看下面的示例,你可能对Rust自动转移所有权会有更好的了解(代码中有注释了,我就不多说了)。

// takes_ownership 取得调用函数传入参数的所有权,因为不返回,所以变量进来了就出不去了
fn takes_ownership(some_string: String) {
    println!("{}", some_string);
} // 这里,some_string 移出作用域并调用 `drop` 方法。占用的内存被释放

// gives_ownership 将返回值移动给调用它的函数
fn gives_ownership() -> String {
    let some_string = String::from("hello"); // some_string 进入作用域.
    some_string // 返回 some_string 并移出给调用的函数
}

// takes_and_gives_back 将传入字符串并返回该值
fn takes_and_gives_back(mut a_string: String) -> String {
    a_string.push_str(", world");
    a_string  // 返回 a_string 将所有权移出给调用的函数
}

fn main()
{
    // gives_ownership 将返回值移给 s1
    let s1 = gives_ownership();
    // 所有权转给了 takes_ownership 函数, s1 不可用了
    takes_ownership(s1);
    // 如果编译下面的代码,会出现s1不可用的错误
    // println!("s1= {}", s1);
    //                    ^^ value borrowed here after move
    let s2 = String::from("hello");// 声明s2
    // s2 被移动到 takes_and_gives_back 中, 它也将返回值移给 s3。
    // 而 s2 则不可用了。
    let s3 = takes_and_gives_back(s2);
    //如果编译下面的代码,会出现可不可用的错误
    //println!("s2={}, s3={}", s2, s3);
    //                         ^^ value borrowed here after move
    println!("s3={}", s3);
}

这样的 Move 的方式,在性能上和安全性上都是非常有效的,而Rust的编译器会帮你检查出使用了所有权被move走的变量的错误。而且,我们还可以从函数栈上返回对象了,如下所示:

fn new_person() -> Person {
    let person = Person {
        name : String::from("Hao Chen"),
        age : 44,
        sex : Sex::Male,
        email: String::from("haoel@hotmail.com"),
    };
    return person;
}

fn main() {
   let p  = new_person();
}

因为对象是Move走的,所以,在函数上 new_person() 上返回的 Person 对象是Move 语言,被Move到了 main() 函数中来,这样就没有性能上的问题了。而在C++中,我们需要把对象的Move函数给写出来才能做到。因为,C++默认是设用拷贝构造函数的,而不是Move的。

Owner语义带来的复杂度

Owner + Move 的语义也会带来一些复杂度。首先,如果有一个结构体,我们把其中的成员 Move 掉了,会怎么样。参看如下的代码:

#[derive(Debug)] // 让结构体可以使用 `{:?}`的方式输出
struct Person {
    name :String,
    email:String,
}

let _name = p.name; // 把结构体 Person::name Move掉
println!("{} {}", _name, p.email); //其它成员可以正常访问
println!("{?:}", p); //编译出错 "value borrowed here after partial move"
p.name = "Hao Chen".to_string(); // Person::name又有了。
println!("{?:}", p); //可以正常的编译了

上面这个示例,我们可以看到,结构体中的成员是可以被Move掉的,Move掉的结构实例会成为一个部分的未初始化的结构,如果需要访问整个结构体的成员,会出现编译问题。但是后面把 Person::name补上后,又可以愉快地工作了。

下面我们再看一个更复杂的示例——这个示例模拟动画渲染的场景,我们需要有两个buffer,一个是正在显示的,另一个是下一帧要显示的。

struct Buffer {
    buffer : String,
}

struct Render {
    current_buffer : Buffer,
    next_buffer : Buffer,
}
//实现结构体 `Render` 的方法
impl Render { 
    //实现 update_buffer() 方法,
    //更新buffer,把 next 更新到 current 中,再更新 next
    fn update_buffer(& mut self, buf : String) {
        self.current_buffer = self.next_buffer;
        self.next_buffer = Buffer{ buffer: buf};
    }
}

上面这段代码,我们写下来没什么问题,但是 Rust 编译不会让我们编译通过。它会告诉我们如下的错误:

error[E0507]: cannot move out of `self.next_buffer` which is behind a mutable reference
--> /.........../xxx.rs:18:31
|
14 | self.current_buffer = self.next_buffer;
|                          ^^^^^^^^^^^^^^^^ move occurs because `self.next_buffer` has type `Buffer`,
                                            which does not implement the `Copy` trait

编译器会提示你,Buffer 没有 Copy trait 方法。但是,如果你实现了 Copy 方法后,你又不能享受 Move 带来的性能上快乐了。于是,到这里,你开始进退两难了,完全不知道取舍了

  • Rust编译器不让我们在成员方法中把成员Move走,因为 self 引用就不完整了。
  • Rust要我们实现 Copy Trait,但是我们不想要拷贝,因为我们就是想把 next_buffer move 到 current_buffer

我们想要同时 Move 两个变量,参数 buf move 到 next_buffer 的同时,还要把 next_buffer 里的东西 move 到 current_buffer 中。 我们需要一个“杂需”的技能。

这个需要动用 std::mem::replace(&dest, src) 函数了, 这个函数技把 src 的值 move 到 dest 中,然后把 dest 再返回出来(这其中使用了 unsafe 的一些底层骚操作才能完成)。Anyway,最终是这样实现的:

use std::mem::replace
fn update_buffer(& mut self, buf : String) { 
  self.current_buffer = replace(&mut self.next_buffer, Buffer{buffer : buf}); 
}

引用(借用)和生命周期

下面,我们来讲讲引用,因为把对象的所有权 Move 走了的情况,在一些时候肯定不合适,比如,我有一个 compare(s1: Student, s2: Student) -> bool 我想比较两个学生的平均份成绩, 我不想传复本,因为太慢,我也不想把所有权交进去,因为只是想计算其中的数据。这个时候,传引用就是一个比较好的选择,Rust同样支持传引用。只需要把上面的函数声明改成:compare(s1 :&Student, s2 : &Student) -> bool 就可以了,在调用的时候,compare (&s1, &s2);  与C++一致。在Rust中,这也叫“借用”(嗯,Rust发明出来的这些新术语,在语义上感觉让人更容易理解了,当然,也增加了学习的复杂度了)

引用(借用)

另外,如果你要修改这个引用对象,就需要使用“可变引用”,如:foo( s : &mut Student) 以及 foo( &mut s);另外,为了避免一些数据竞争需要进行数据同步的事,Rust严格规定了——在任意时刻,要么只能有一个可变引用,要么只能有多个不可变引用

这些严格的规定会导致程序员失去编程的灵活性,不熟悉Rust的程序员可能会在一些编译错误下会很崩溃,但是你的代码的稳定性也会提高,bug率也会降低。

另外,Rust为了解决“野引用”的问题,也就是说,有多个变量引用到一个对象上,还不能使用额外的引用计数来增加程序运行的复杂度。那么,Rust就要管理程序中引用的生命周期了,而且还是要在编译期管理,如果发现有引用的生命周期有问题的,就要报错。比如:

let r;
{
    let x = 10;
    r = &x;
}
println!("r = {}",r );

上面的这段代码,程序员肉眼就能看到 x 的作用域比 r  小,所以导致 rprintln() 的时候 r 引用的 x 已经没有了。这个代码在C++中可以正常编译而且可以执行,虽然最后可以打出“内嵌作用域”的 x 的值,但其实这个值已经是有问题的了。而在 Rust 语言中,编译器会给出一个编译错误,告诉你,“x dropped here while still borrowed”,这个真是太棒了。

但是这中编译时检查的技术对于目前的编译器来说,只在程序变得稍微复杂一点,编译器的“失效引用”检查就不那么容易了。比如下面这个代码:

fn order_string(s1 : &str, s2 : &str) -> (&str, &str) {
    if s1.len() < s2.len() {
        return (s1, s2);
    }
    return (s2, s1);
}

let str1 = String::from("long long long long string");
let str2 = "short string";

let (long_str, short_str) = order_string(str1.as_str(), str2);

println!(" long={} nshort={} ", long_str, short_str);

我们有两个字符串,str1str2 我们想通过函数 order_string() 把这两个字串符返回成 long_strshort_str  这样方便后面的代码进行处理。这是一段很常见的处理代码的示例。然而,你会发现,这段代码编译不过。编译器会告诉你,order_string() 返回的 引用类型 &str 需要一个 lifetime的参数 – “ expected lifetime parameter”。这是因为Rust编译无法通过观察静态代码分析返回的两个引用返回值,到底是(s1, s2) 还是 (s2, s1) ,因为这是运行时决定的。所以,返回值的两个参数的引用没法确定其生命周期到底是跟 s1 还是跟 s2,这个时候,编译器就不知道了。

生命周期

如果你的代码是下面这个样子,编程器可以自己推导出来,函数 foo() 的参数和返回值都是一个引用,他们的生命周期是一样的,所以,也就可以编译通过。

fn foo (s: &mut String) -> &String {
    s.push_str("coolshell");
    s
}

let mut s = "hello, ".to_string();
println!("{}", foo(&mut s))

而对于传入多个引用,返回值可能是任一引用,这个时候编译器就犯糊涂了,因为不知道运行时的事,所以,就需要程序员来标注了。

fn long_string<'c>(s1 : &'c str, s2 : &'c str) -> (&'c str, &'c str) {
    if s1.len() > s2.len() {
        return (s1, s2);
    }
    return (s2, s1);
}

上述的Rust的标注语法,用个单引号加一个任意字符串来标注('static除外,这是一个关键词,表示生命周期跟整个程序一样长),然后,说明返回的那两个引用的生命周期跟 s1s2 的生命周期相同,这个标注的目的就是把运行时的事变成了编译时的事。于是程序就可以编译通过了。(注:你也不要以为你可以用这个技术乱写生命周期,这只是一种声明,是帮助编译器理解其中的生命周期,如果违反实际生命周期,编译器也是会拒绝编译的)

这里有两个说明,

  • 只要你玩引用,生命周期标识就会来了。
  • Rust编译器不知道运行时会发生什么事,所以,需要你来标注声明

我感觉,你现在开始有点头晕了吧?接下来,我们让你再晕一下。比如:如果你要在结构体中玩引用,那必需要为引用声明个生命周期,如下所示:

// 引用 ref1 和 ref2 的生命周期与结构体一致
struct Test <'life> {
    ref_int : &'life i32,
    ref_str : &'life str,
}

其中,生命周期标识 'life 定义在结构体上,被使用于其成员引用上。意思是声明规则——“结构体的生命周期 <= 成员引用的生命周期

然后,如果你要给这个结构实现两个 set 方法,你也得带上 lifetime 标识。

imp<'life> Test<'life> {
    fn set_string(&mut self, s : &'life str) {
        self.ref_str = s;
    }
    fn set_int(&mut self,  i : &'life i32) {
        self.ref_int = i;
    }
}

在上面的这个示例中,生命周期变量 'life 声明在 impl 上,用于结构体和其方法的入参上。 意思是声明规则——“结构体方法的“引用参数”的生命周期 >= 结构体的生命周期

有了这些个生命周期的标识规则后,Rust就可以愉快地检查这些规则说明,并编译代码了。

闭包与所有权

这种所有权和引用的严格区分和管理,会影响到很多地方,下面我们来看一下函数闭包中的这些东西的传递。函数闭包又叫Closure,是函数式编程中一个不可或错的东西,又被称为lambda表达式,基本上所有的高级语言都会支持。在 Rust 语言中,其闭包函数的表示是用两根竖线(| |)中间加传如参数进行定义。如下所示:

// 定义了一个 x + y 操作的 lambda f(x, y) = x + y;
let plus = |x: i32, y:i32| x + y; 
// 定义另一个lambda g(x) = f(x, 5)
let plus_five = |x| plus(x, 5); 
//输出
println!("plus_five(10)={}", plus_five(10) );
函数闭包

但是一旦加上了上述的所有权这些东西后,问题就会变得复杂开来。参看下面的代码。

struct Person {
    name : String,
    age : u8,
}

fn main() {
    let p = Person{ name: "Hao Chen".to_string(), age : 44};
    //可以运行,因为 `u8` 有 Copy Trait
    let age = |p : Person| p.age; 
    // String 没有Copy Trait,所以,这里所有权就 Mov e走了
    let name = |p : Person | p.name; 
    println! ("name={}, age={}" , name(p), age(p));
}

上面的代码无法编译通过,因为Rust编译器发现在调用 name(p) 的时候,p 的所有权被移走了。然后,我们想想,改成引用的版本,如下所示:

let age = |p : &Person| p.age;
let name = |p : &Person | &p.name;

println! ("name={}, age={}" , name(&p), age(&p));

你会现在还是无法编译,报错中说:cannot infer an appropriate lifetime for borrow expression due to conflicting requirements

error[E0495]: cannot infer an appropriate lifetime for borrow expression due to conflicting requirements
  --> src/main.rs:11:31
   |
11 |     let name = |p : &Person | &p.name;
   |                               ^^^^^^^

然后你开始尝试加 lifetime,用尽各种Rust的骚操作(官方Github上的 #issue 58052),然后,还是无法让你的程序可以编译通过。最后,上StackOverflow 里寻找帮助,得到下面的正确写法(这个可能跟这个bug有关系:#issue 41078

//下面的声明可以正确译
let name: for<'a> fn(&'a Person) -> &'a String = |p: &Person| &p.name;

最后,我们再来看另一个问题,下面的代码无法编译通过:

let s = String::from("coolshell");
let take_str = || s;
println!("{}", s); //ERROR
println!("{}",  take_str()); // OK

Rust的编译器会告诉你,take_str  把 s 的所有权给拿走了(因为需要作成返回值)。所以,后面的输出语句就用不到了。这里意味着:

  • 对于内建的类型,都实现了 Copy 的 trait,那么闭包执行的是 “借用”
  • 对于没有实现 Copy 的trait,在闭包中可以调用其方法,是“借用”,但是不能当成返回值,当成返回值了就是“移动”。

虽然有了这些“通常情况下是借用的潜规则”,但是还是不能满足一些情况,所以,还要让程序员可以定义 move 的“明规则”。下面的代码,一个有 move 一个没有move,他们的差别也不一样。

//-----------借用的情况-----------
let mut num = 5;
{
    let mut add_num = |x: i32| num += x;
    add_num(5);
}
println!("num={}", num); //输出 10

//-----------Move的情况-----------
let mut num = 5;
{
    // 把 num(5)所有权给 move 到了 add_num 中,
    // 使用其成为闭包中的局部变量。
    let mut add_num = move |x: i32| num += x;
    add_num(5);
    println!("num(move)={}", num); //输出10
}
//因为i32实现了 `Copy`,所以,这里还可以访问
println!("num(move)={}", num); //输出5
线程闭包

通过上面的示例,我们可以看到, move 关键词,可以把闭包外使用到的变量给移动到闭包内,成为闭包内的一个局部变量。这种方式,在多线程的方式下可以让线程运行地更为的安全。参看如下代码:

let name = "CoolShell".to_string();
let t = thread::spawn(move || {
    println!("Hello, {}", name);
});
println!("wait {:?}", t.join());

首先,线程 thread::spawn() 里的闭包函数是不能带参数的,因为是闭包,所以可以使用这个可见范围内的变量,但是,问题来了,因为是另一个线程,所以,这代表其和其它线程(如:主线程)开始共享数据了,所以,在Rust下,要求把使用到的变量给 Move 到线程内,这就保证了安全的问题—— name 在编程中永远不会失效,而且不会被别人改了。

你可能会有一些疑问,你会质疑到

  • 一方面,这个 name 变量又没有声明成 mut 这意味着不变,没必要使用move语义也是安全的。
  • 另一方面,如果我想把这个 name 传递到多个线程里呢?

嗯,是的,但是Rust的线程必需是 move的,不管是不是可变的,不然编译不过去。如果你想把一个变量传到多个线程中,你得创建变量的复本,也就是调用 clone() 方法。

let name = "CoolShell".to_string();
let name1 = name.clone();
let t1 = thread::spawn(move || {
    println!("Hello, {}", name.clone());
})
let t2 = thread::spawn(move || {
    println!("Hello, {}", name1.clone());
});
println!("wait t1={:?}, t2={:?}", t1.join(), t2.join());

然后,你说,这种clone的方式成本不是很高?设想,如果我要用多线程对一个很大的数组做统计,这种clone的方式完全吃不消。嗯,是的。这个时候,需要使用另一个技术,智能指针了。

Rust的智能指针

如果你看到这里还不晕的话,那么,我的文章还算成功(如果晕的话,请告诉我,我会进行改善)。接下来我们来讲讲Rust的短能指针和多态。

因为有些内存需要分配在Heap(堆)上,而不是Stack(堆)上,Stack上的内存一般是编译时决定的,所以,编译器需要知道你的数组、结构构、枚举等这些数据类型的长度,没有长度是无法编译的,而且长度也不能太大,Stack上的内存大小是有限,太大的内存会有StackOverflow的错误。所以,对于更大的内存或是动态的内存分配需要分配在Heap上。学过C/C++的同学对于这个概念不会陌生。

Rust 作为一个内存安全的语言,这个堆上分配的内存也是需要管理的。在C中,需要程序员自己管理,而在C++中,一般使用 RAII 的机制(面向对象的代理模式),一种通过分配在Stack上的对象来管理Heap上的内存的技术。在C++中,这种技术的实现叫“智能指针”(Smart Pointer)。

在C++11中,会有三种智能指针(这三种指针是什么我就不多说了):

  • unique_ptr。独占内存,不共享。在Rust中是:std::boxed::Box
  • shared_ptr。以引用计数的方式共享内存。在Rust中是:std::rc::Rc
  • weak_ptr。不以引用计数的方式共享内存。在Rust中是:std::rc::Weak

对于独占的 Box 不多说了,这里重占说一下共享的 RcWeak

  • 对于Rust的 Rc 来说,Rc指针内会有一个 strong_count 的引用持计数,一旦引用计数为0后,内存就自动释放了。
  • 需要共享内存的时候,需要调用实例的 clone() 方法。如: let another = rc.clone()
  • 有这种共享的引用计数,就意味着有多线程的问题,所以,如果需要使用线程安全的智能指针,则需要使用std::sync::Arc
  • 可以使用 Rc::downgrade(&rc) 后,会变成 Weak 指针,Weak指针增加的是 weak_count 的引用计数,内存释放时不会检查它是否为 0。

我们简单的来看个示例:

use std::rc::Rc;
use std::rc::Weak

//声明两个未初始化的指针变量
let weak : Weak; 
let strong : Rc;
{
    let five = Rc::new(5); //局部变量
    strong = five.clone(); //进行强引用
    weak = Rc::downgrade(&five); //对局部变量进行弱引用
}
//此时,five已析构,所以 Rc::strong_count(&strong)=1, Rc::weak_count(&strong)=1
//如果调用 drop(strong),那个整个内存就释放了
//drop(strong);

//如果要访问弱引用的值,需要把弱引用 upgrade 成强引用,才能安全的使用
match  weak_five.upgrade() {
    Some(r) => println!("{}", r),
    None => println!("None"),
} 

上面这个示例比较简单,其中主要展示了,指针共享的东西。因为指针是共享的,所以,对于强引用来说,最后的那个人把引用给释放了,是安全的。但是对于弱引用来说,这就是一个坑了,你们强引用的人有Ownership,但是我们弱引用没有,你们把内存释放了,我怎么知道?

于是,在弱引用需要使用内存的时候需要“升级”成强引用 ,但是这个升级可能会不成功,有可能会升成不成,因为内存已经全没有了。所以,会返回一个 Option 的枚举值,Option::Some(T) 表示成功了,而 Option::None 则表示失改了。你会说,这么麻烦,我们为什么还要 Weak ? 这是因为强引用的 Rc 会有循环引用的问题……

另外,如果你要修改 Rc 里的值,Rust 会给你两个方法,一个是 get_mut(),一个是 make_mut() ,这两个方法都有副作用或是限制。

get_mut() 需要做一个“唯一引用”的检查,也就是没有任何的共享才能修改

//修改引用的变量 - get_mut 会返回一个Option对象
//但是需要注意,仅当(只有一个强引用 && 没有弱引用)为真才能修改
if let Some(val) = Rc::get_mut(&mut strong) {
    *val = 555;
}

make_mut() 则是会把当前的引用给clone出来,再也不共享了, 是一份全新的。

//此处可以修改,但是是以 clone 的方式,也就是让strong这个指针独立出来了。
*Rc::make_mut(&mut strong) = 555;

如果不这样做,就会出现很多内存不安全的情况。这些小细节一定要注意,不然你的代码怎么运作的你会一脸蒙逼的。

嗯,如果你想更快乐地使用智能指针,这里还有个选择 – CellRefCell,它们弥补了 Rust 所有权机制在灵活性上和某些场景下的不足。他们提供了 set()/get() 以及 borrow()/borrow_mut() 的方法,让你的程序更灵活,而不会被限制得死死的。参看下面的示例。

use std::cell::Cell;
use std::cell::RefCell

let x = Cell::new(1);
let y = &x; //引用(借用)
let z = &x; //引用(借用)
x.set(2); // 可以进行修改,x,y,z全都改了
y.set(3);
z.set(4);
println!("x={} y={} z={}", x.get(), y.get(), z.get());

let x = RefCell::new(vec![1,2,3,4]);
{
    println!("{:?}", *x.borrow())
}

{
    let mut my_ref = x.borrow_mut();
    my_ref.push(1);
}
println!("{:?}", *x.borrow());

通过上面的示例你可以看到你可以比较方便地更为正常的使用智能指针了。

线程与智能指针

前面,我们还有个问题没有解决,那就是——我想在多个线程中共享一个只读的数据,比如:一个很大的数组,我开多个线程进行并行统计。我们肯定不能对这个大数组进行clone,但也不能把这个大数组move到一个线程中。根据上述的智能指针的逻辑,我们可以通过智指指针来完成这个事,下面是一个例程:

const TOTAL_SIZE:usize = 100 * 1000; //数组长度
const NTHREAD:usize = 6; //线程数

let data : Vec<i32> = (1..(TOTAL_SIZE+1) as i32).collect(); //初始化一个数据从1到n数组
let arc_data = Arc::new(data); //data 的所有权转给了 ar_data
let result  = Arc::new(AtomicU64::new(0)); //收集结果的数组(原子操作)

let mut thread_handlers = vec![]; // 用于收集线程句柄

for i in 0..NTHREAD {
    // clone Arc 准备move到线程中,只增加引用计数,不会深拷贝内部数据
    let test_data = arc_data.clone(); 
    let res = result.clone(); 
    thread_handlers.push( 
        thread::spawn(move || {
            let id = i;
            //找到自己的分区
            let chunk_size = TOTAL_SIZE / NTHREAD + 1;
            let start = id * chunk_size;
            let end = std::cmp::min(start + chunk_size, TOTAL_SIZE);
            //进行求和运算
            let mut sum = 0;
            for  i in start..end  {
                sum += test_data[i];
            }
            //原子操作
            res.fetch_add(sum as u64, Ordering::SeqCst);
            println!("id={}, sum={}", id, sum );
        }
    ));
}
//等所有的线程执行完
for th in thread_handlers {
    th.join().expect("The sender thread panic!!!");
}
//输出结果
println!("result = {}",result.load(Ordering::SeqCst));

上面的这个例程,是用多线程的方式来并行计算一个大的数组的和,每个线程都会计算自己的那一部分。上面的代码中,

  • 需要向每个线程传入一个只读的数组,我们用Arc 智能指针把这个数组包了一层。
  • 需要向每个线程传入一个变量用于数据数据,我们用 Arc<AtomicU64> 包了一层。
  • 注意:Arc 所包的对象是不可变的,所以,如果要可变的,那要么用原子对象,或是用Mutex/Cell对象再包一层。

这一些都是为了要解决“线程的Move语义后还要共享问题”。

多态和运行时识别

通过Trait多态

多态是抽象和解耦的关键,所以,一个高级的语言是必需实现多态的。在C++中,多态是通过虚函数表来实现的(参看《C++的虚函数表》),Rust也很类似,不过,在编程范式上,更像Java的接口的方式。其通过借用于Erlang的Trait对象的方式来完成。参看下面的代码:

struct Rectangle {
    width : u32,
    height : u32,
} 

struct Circle {
    x : u32,
    y : u32,
    radius : u32,
}

trait  IShape  { 
    fn area(&self) -> f32;
    fn to_string(&self) -> String;
}

我们有两个类,一个是“长方形”,一个是“圆形”, 还有一个 IShape 的trait 对象(原谅我用了Java的命名方式),其中有两个方法:求面积的 area() 和 转字符串的 to_string()。下面相关的实现:

impl IShape  for Rectangle {
    fn area(&self) -> f32 { (self.height * self.width) as f32 }
    fn to_string(&self) ->String {
         format!("Rectangle -> width={} height={} area={}", 
                  self.width, self.height, self.area())
    }
}

use std::f64::consts::PI;
impl IShape  for Circle  {
    fn area(&self) -> f32 { (self.radius * self.radius) as f32 * PI as f32}
    fn to_string(&self) -> String {
        format!("Circle -> x={}, y={}, area={}", 
                 self.x, self.y, self.area())
    }
}

于是,我们就可以有下面的多态的使用方式了(我们使用独占的智能指针类 Box):

use std::vec::Vec;

let rect = Box::new( Rectangle { width: 4, height: 6});
let circle = Box::new( Circle { x: 0, y:0, radius: 5});
let mut v : Vec<Box> = Vec::new();
v.push(rect);
v.push(circle);

for i in v.iter() {
   println!("area={}", i.area() );
   println!("{}", i.to_string() );
}
向下转型

但是,在C++中,多态的类型是抽象类型,我们还想把其转成实际的具体类型,在C++中叫运行进实别RTTI,需要使用像 type_id 或是 dynamic_cast 这两个技术。在Rust中,转型是使用 ‘as‘ 关键字,然而,这是编译时识别,不是运行时。那么,在Rust中是怎么做呢?

嗯,这里需要使用 Rust 的 std::any::Any 这个东西,这个东西就可以使用 downcast_ref 这个东西来进行具体类型的转换。于是我们要对现有的代码进行改造。

首先,先得让 IShape 继承于 Any ,并增加一个 as_any() 的转型接口。

use std::any::Any;
trait  IShape : Any + 'static  {
    fn as_any(&self) -> &dyn Any; 
    …… …… …… 
}

然后,在具体类中实现这个接口:

impl IShape  for Rectangle {
    fn as_any(&self) -> &dyn Any { self }
    …… …… …… 
}
impl IShape  for Circle  {
    fn as_any(&self) -> &dyn Any { self }
    …… …… …… 
}

于是,我们就可以进行运行时的向下转型了:

let mut v : Vec<Box<dyn IShape>> = Vec::new();
v.push(rect);
v.push(circle);
for i in v.iter() {
    if let Some(s) = i.as_any().downcast_ref::<Rectangle>() {
        println!("downcast - Rectangle w={}, h={}", s.width, s.height);
    }else if let Some(s) = i.as_any().downcast_ref::<Circle>() {
        println!("downcast - Circle x={}, y={}, r={}", s.x, s.y, s.radius);
    }else{
        println!("invaild type");
    }
}

Trai 重载操作符

操作符重载对进行泛行编程是非常有帮助的,如果所有的对象都可以进行大于,小于,等于这亲的比较操作,那么就可以直接放到一个标准的数组排序的的算法中去了。在Rust中,在 std::ops 下有全载的操作符重载的Trait,在std::cmp 下则是比较操作的操作符。我们下面来看一个示例:

假如我们有一个“员工”对象,我们想要按员工的薪水排序,如果我们想要使用Vec::sort()方法,我们就需要实现这个对象的各种“比较”方法。这些方法在 std::cmp 内—— 其中有四个Trait : OrdPartialOrdEqPartialEq  。其中,Ord 依赖于 PartialOrdEq ,而Eq 依赖于 PartialEq,这意味着你需要实现所有的Trait,而Eq 这个Trait 是没有方法的,所以,其实现如下:

use std::cmp::{Ord, PartialOrd, PartialEq, Ordering};

#[derive(Debug)]
struct Employee {
    name : String,
    salary : i32,
}
impl Ord for Employee {
    fn cmp(&self, rhs: &Self) -> Ordering {
        self.salary.cmp(&rhs.salary)
    }
}
impl PartialOrd for Employee {
    fn partial_cmp(&self, rhs: &Self) -> Option<Ordering> {
        Some(self.cmp(rhs))
    }
}
impl Eq for Employee {
}
impl PartialEq for Employee {
    fn eq(&self, rhs: &Self) -> bool {
        self.salary == rhs.salary
    }
}

于是,我们就可以进行如下的操作了:

let mut v = vec![
    Employee {name : String::from("Bob"),     salary: 2048},
    Employee {name : String::from("Alice"),   salary: 3208},
    Employee {name : String::from("Tom"),     salary: 2359},
    Employee {name : String::from("Jack"),    salary: 4865},
    Employee {name : String::from("Marray"),  salary: 3743},
    Employee {name : String::from("Hao"),     salary: 2964},
    Employee {name : String::from("Chen"),    salary: 4197},
];

//用for-loop找出薪水最多的人
let mut e = &v[0];
for i in 0..v.len() {
    if *e < v[i] { 
        e = &v[i]; 
    }
}
println!("max = {:?}", e);

//使用标准的方法
println!("min = {:?}", v.iter().min().unwrap());
println!("max = {:?}", v.iter().max().unwrap());

//使用标准的排序方法
v.sort();
println!("{:?}", v);

小结

现在我们来小结一下:

  • 在Rust的中,最重要的概念就是“不可变”和“所有权”以及“Trait”这三个概念。
  • 在所有权概念上,Rust喜欢move所有权,如果需要借用则需要使用引用。
  • Move所有权会导致一些编程上的复杂度,尤其是需要同时move两个变量时。
  • 引用(借用)的问题是生命周期的问题,一些时候需要程序员来标注生命周期。
  • 在函数式的闭包和多线程下,这些所有权又出现了各种麻烦事。
  • 使用智能指针可以解决所有权和借用带来的复杂度,但带来其它的问题。
  • 最后介绍了Rust的Trait对象完成多态和函数重载的玩法。

Rust是一个比较严格的编程语言,它会严格检查你程序中的:

  • 变量是否是可变的
  • 变量的所有权是否被移走了
  • 引用的生命周期是否完整
  • 对象是否需要实现一些Trait

这些东西都会导致失去编译的灵活性,并在一些时候需要“去糖”,导致,你在使用Rust会有诸多的不适应,程序编译不过的挫败感也是令人沮丧的。在初学Rust的时候,我想自己写一个单向链接,结果,费尽心力,才得以完成。也就是说,如果你对Rust的概念认识的不完整,你完全写不出程序,那怕就是很简单的一段代码。我觉得,这种挺好的,逼着程序员必需了解所有的概念才能编码。但是,另一方面也表明了这门语言并不适合初学者。

没有银弹,任何语言都有些适合的地方和场景。

(全文完)


关注CoolShell微信公众账号和微信小程序

(转载本站文章请注明作者和出处 酷 壳 – CoolShell ,请勿用于任何商业用途)

——=== 访问 酷壳404页面 寻找遗失儿童。 ===——

陈皓 April 04, 2020 at 10:48PM
from 酷 壳 – CoolShell https://ift.tt/3dT197A

VN:F [1.9.22_1171]
Rating: 0.0/5 (0 votes cast)
VN:F [1.9.22_1171]
Rating: 0 (from 0 votes)
Share

【酷壳】 与程序员相关的CPU缓存知识

好久没有写一些微观方面的文章了,今天写一篇关于CPU Cache相关的文章,这篇文章会讲述一些多核 CPU 的系统架构以及其原理,包括对程序性能上的影响,以及在进行并发编程的时候需要注意到的一些问题。这篇文章我会尽量地写简单和通俗易懂一些,主要是讲清楚相关的原理和问题,而对于一些细节和延伸阅读我会在文章最好给出相关的资源。

本文比较长,主要分成这么几个部分:基础知识、缓存的命中、缓存的一致性、相关的代码示例和延伸阅读。

因为无论你写什么样的代码都会交给CPU来执行,所以,如果你想写出性能比较高的代码,这篇文章中的技术还是应该认真学习的。

基础知识

首先,我们都知道现在的CPU多核技术,都会有几级缓存,老的CPU会有两级内存(L1和L2),新的CPU会有三级内存(L1,L2,L3 ),如下图所示:

其中:

  • L1缓分成两种,一种是指令缓存,一种是数据缓存。L2缓存和L3缓存不分指令和数据。
  • L1和L2缓存在第一个CPU核中,L3则是所有CPU核心共享的内存。
  • L1、L2、L3的越离CPU近就越小,速度也越快,越离CPU远,速度也越慢。

再往后面就是内存,内存的后面就是硬盘。我们来看一些他们的速度:

  • L1 的存取速度:
  • L2 的存取速度:
  • L3 的存取速度:
  • RAM内存的存取速度

我们可以看到,L1的速度是RAM的27倍,但是L1/L2的大小基本上也就是KB级别的,L3会是MB级别的。例如:Intel Core i7-8700K ,是一个6核的CPU,每核上的L1是64KB(数据和指令各32KB),L2 是 256K,L3有12MB(我的苹果电脑是 Intel Core i9-8950HK,和Core i7-8700K的Cache大小一样)。

于是我们的数据就从内存向上,先到L3,再到L2,再到L1,最后到寄存器进行CPU计算。为什么会设计成三层?这里有下面几个方面的考虑:

  • 一个方面是物理速度,如果你要更在的容量就需要更多的晶体管,除了芯片的体积会变大,更重要的是大量的晶体管会导致速度下降,因为访问速度和要访问的晶体管的位置成反比,也就是当信号路径变长时,通信速度会变慢。这部分是物理问题。
  • 另外一个问题是,多核技术中,数据的状态需要在多个CPU中进行同步,并且,我们可以看到,cache和RAM的速度差距太大,所以,多级不同尺寸的缓存有利于提高整体的性能。

这个世界永远是平衡的,一面变得有多光鲜,另一面也会变得有多黑暗。建立这么多级的缓存,一定就会引入其它的问题,这里有两个比较重要的问题,

  • 一个是比较简单的缓存的命中率的问题。
  • 另一个是比较复杂的缓存更新的一致性问题。

尤其是第二个问题,在多核技术下,这就很像分布式的系统了,要对多个地方进行更新。

缓存的命中

在说明这两个问题之前。我们需要要解一个术语 Cache Line。缓存基本上来说就是把后面的数据加载到离自己进的地方,对于CPU来说,它是不会一个字节一个字节的加载的,因为这非常没有效率,一般来说都是要一块一块的加载的,在CPU的缓存技术中,这个术语叫“Cache Line”(有的中文编译成“缓存线”),一般来说,一个主流的CPU的Cache Line 是 64 Bytes(也有的CPU用32Bytes和128Bytes),也就是16个32位的整型。也就是说,CPU从内存中捞数据上来的最小数据单位。

比如:Cache Line是最小单位(64Bytes),所以先把Cache分布多个Cache Line,比如:L1有32KB,那么,32KB/64B = 500 个 Cache Line。

一方面,缓存需要把内存里的数据放到放进来,英文叫 CPU Associativity。Cache的数据放置的策略决定了内存中的数据块会拷贝到CPU Cache中的哪个位置,因为Cache的大小远远小于内存,所以,需要有一种地址关联的算法,能够让内存中的数据可以被映射到cache中来。这个有点像内存管理的方法。

基本上来说,我们会有如下的一些方法。

  • 一种方法是,任何一个内存地址的数据可以被缓存在任何一个Cache Line里,这种方法是最灵活的,但是,如果我们要知道一个内存是否存在于Cache中,我们就需要进行O(n)复杂度的Cache遍历,这是很没有效率的。
  • 另一种方法,为了降低缓存搜索算法,我们需要使用像Hash Table这样的数据结构,最简单的hash table就是做“求模运算”,比如:我们的L1 Cache有500个Cache Line,那么,公式:`(内存地址 mod 500)x 64` 就可以直接找到所在的Cache地址的偏移了。但是,这样的方式需要我们的程序对内存地址的访问要非常地平均,这成了一种非常理想的情况了。
  • 为了避免上述的两种方案的问题,于是就要容忍一定的hash冲突,也就出现了 N-Way 关联。也就是把连续的N个Cache Line绑成一组,然后,先把找到相关的组,然后再在这个组内找到相关的Cache Line。

那么,Cache的命中率会成为程序运行性能非常关键的事,所以,了解上面的这些东西,会有利于我们知道在什么情况下有可以导致缓存的失效。

对于 N-Way 关联我们取个例子,并多说一些细节(因为后面会用到),Intel 大多数处理器的L1 Cache都是32KB,8-Way 组相联,Cache Line 是64 Bytes。于是,

  • 32KB的可以分成,32KB / 64 = 512 条 Cache Line。
  • 因为有8 Way,于是会每一Way 有 512 / 8 = 64 条 Cache Line。
  • 于是每一路就有 64 x 64 = 4096 Byts 的内存。

为了方便索引内存地址,

  • Tag:每条 Cache Line 前都会有一个独立分配的 24 bits来存的 tag,其就是内存地址的前24bits
  • Index:内存地址后续的6个bits则是在这一Way的是Cache Line 索引,2^6 = 64 刚好可以索引64条Cache Line
  • Offset:再往后的6bits用于表示在Cache Line 里的偏移量

如下图所示:(更多的细节可以读一下《Cache: a place for concealment and safekeeping》)

(图片来自《Cache: a place for concealment and safekeeping》)

这意味着:

  • L1 Cache 可映射 36bits 的内存地址,一共 2^36 = 64GB的内存
  • 因为只要头24bits相同就会被映射到同一个Way中,所以,每4096个地址会放在一Way中。
  • 当CPU要访问一个内存的时候,通过这个内存的前24bits 和中间的6bits可以直接定位相应的Cache Line。

此外,当有数据没有命中缓存的时候,CPU就会以最小为Cache Line的单元向内存更新数据。当然,CPU并不一定只是更新64Bytes,因为访问主存是在是太慢了,所以,一般都会多更新一些。好的CPU会有一些预测的技术,如果找到一种pattern的话,就会预先加载更多的内存,包括指令也可以预加载。这叫 Prefetching 技术 (参看,Wikipedia 的 Cache Prefetching纽约州立大学的 Memory Prefetching)。比如,你在for-loop访问一个连续的数组,你的步长是一个固定的数,内存就可以做到prefetching。(注:指令也是以预加载的方式执行,参看本站的《代码执行的效率》中的第三个示例)

缓存的一致性

对于主流的CPU来说,缓存的写操作基本上是两种策略(参看本站《缓存更新的套路》),

  • 一种是Write Back,写操作只要在cache上,然后再flush到内存上。
  • 一种是Write Through,写操作同时写到cache和内存上。

为了提高写的性能,一般来说,主流的CPU(如:Intel Core i7/i9)采用的是Write Back的策略,因为直接写内存实在是太慢了。

好了,现在问题来了,如果有一个数据 x 在 CPU 第0核的缓存上被更新了,那么其它CPU核上对于这个数据 x 的值也要被更新,这就是缓存一致性的问题。(当然,对于我们上层的程序我们不用关心CPU多个核的缓存是怎么同步的,这对上层都是透明的)

一般来说,在CPU硬件上,会有两种方法来解决这个问题。

  • Directory 协议。这种方法的典型实现是要设计一个集中式控制器,它是主存储器控制器的一部分。其中有一个目录存储在主存储器中,其中包含有关各种本地缓存内容的全局状态信息。当单个CPU Cache 发出读写请求时,这个集中式控制器会检查并发出必要的命令,以在主存和CPU Cache之间或在CPU Cache自身之间进行数据同步和传输。
  • Snoopy 协议。这种协议更像是一种数据通知的总线型的技术。CPU Cache通过这个协议可以识别其它Cache上的数据状态。如果有数据共享的话,可以通过广播机制将共享数据的状态通知给其它CPU Cache。这个协议要求每个CPU Cache 都可以“窥探”数据事件的通知并做出相应的反应。

因为Directory协议是一个中心式的,会有性能瓶颈,而且会增加整体设计的复杂度。而Snoopy协议更像是微服务+消息通讯,所以,现在基本都是使用Snoopy的总线的设计。

这里,我想多写一些细节,因为这种微观的东西,不自然就就会更分布式系统相关联,在分布式系统中我们一般用Paxos/Raft这样的分布式一致性的算法。而在CPU的微观世界里,则不必使用这样的算法,原因是因为CPU的多个核的硬件不必考虑网络会断会延迟的问题。所以,CPU的多核心缓存间的同步的核心就是要管理好数据的状态就好了。

这里介绍几个状态协议,先从最简单的开始,MESI协议,这个协议跟那个著名的足球运动员梅西没什么关系,其主要表示缓存数据有四个状态:Modified(已修改), Exclusive(独占的),Shared(共享的),Invalid(无效的)。

这些状态的状态机如下所示:

下面是个示例(如果你想看一下动画演示的话,这里有一个网页(MESI Interactive Animations),你可以进行交互操作,这个动画演示中使用的Write Through算法):

当前操作 CPU0 CPU1 Memory 说明
1) CPU0 read(x)  x=1 (E) x=1 只有一个CPU有 x 变量,
所以,状态是 Exclusive
2) CPU1 read(x)  x=1 (S) x=1(S) x=1 有两个CPU都读取 x 变量,
所以状态变成 Shared
3) CPU0 write(x,9)  x=9 (M) x=1(I) x=1 变量改变,在CPU0中状态
变成 Modified,在CPU1中
状态变成 Invalid
4) 变量 x 写回内存  x=9 (M) X=1(I) x=9 目前的状态不变
5) CPU1  read(x)  x=9 (S) x=9(S) x=9 变量同步到所有的Cache中,
状态回到Shared

 

MESI 这种协议在数据更新后,会标记其它共享的CPU缓存的数据拷贝为Invalid状态,然后当其它CPU再次read的时候,就会出现 cache misses 的问题,此时再从内存中更新数据。可见,从内存中更新数据意味着20倍速度的降低。我们能不直接从我隔壁的CPU缓存中更新?是的,这就可以增加很多速度了,但是状态控制也就变麻烦了。还需要多来一个状态:Owner(宿主),用于标记,我是更新数据的源。于是,现了 MOESI 协议

MOESI协议的状态机和演示我就不贴了,我们只需要理解MOESI协议允许 CPU Cache 间同步数据,于是也降低了对内存的操作,性能是非常大的提升,但是控制逻辑也非常复杂。

顺便说一下,与 MOESI 协议类似的一个协议是 MESIF,其中的 F 是 Forward,同样是把更新过的数据转发给别的 CPU Cache 但是,MOESI 中的 Owner 状态 和MESIF 中的 Forward 状态有一个非常大的不一样—— Owner状态下的数据是dirty的,还没有写回内存,Forward状态下的数据是clean的,可以丢弃而不用另行通知。

需要说明的是,AMD用MOESI,Intel用MESIF。所以,F 状态主要是针对 CPU L3 Cache 设计的(前面我们说过,L3是所有CPU核心共享的)。(相关的比较可以参看StackOverlow上这个问题的答案

程序性能

了解了我们上面的这些东西后,我们来看一下对于程序的影响。

示例一

首先,假设我们有一个64M长的数组,设想一下下面的两个循环:

const int LEN = 64*1024*1024;
int *arr = new int[LEN];

for (int i = 0; i < LEN; i += 2) arr[i] *= i;

for (int i = 0; i < LEN; i += 8) arr[i] *= i;

按我们的想法来看,第二个循环要比第一个循环少4倍的计算量,其应该也是要快4倍的。但实际跑下来并不是,在我的机器上,第一个循环需要127毫秒,第二个循环则需要121毫秒,相差无几。这里最主要的原因就是 Cache Line,因为CPU会以一个Cache Line 64Bytes最小时单位加载,也就是16个32bits的整型,所以,无论你步长是2还是8,都差不多。而后面的乘法其实是不耗CPU时间的。

示例二

我们再来看一个与缓存命中率有关的代码,我们以一定的步长increment 来访问一个连续的数组。

for (int i = 0; i < 10000000; i++) {
    for (int j = 0; j < size; j += increment) {
        memory[j] += j;
    }
}

我们测试一下,在下表中, 表头是步长,也就是每次跳多少个整数,而纵向是这个数组可以跳几次(你可以理解为要几条Cache Line),于是表中的任何一项代表了这个数组有多少,而且步长是多少。比如:横轴是 512,纵轴是4,意思是,这个数组有 4*512 = 2048 个长度,访问时按512步长访问,也就是访问其中的这几项:[0, 512, 1024, 1536] 这四项。

表中同的项是,是循环1000万次的时间,单位是“微秒”(除以1000后是毫秒)

| count |   1    |   16  |  512  | 1024  |
------------------------------------------
|     1 |  17539 | 16726 | 15143 | 14477 |
|     2 |  15420 | 14648 | 13552 | 13343 |
|     3 |  14716 | 14463 | 15086 | 17509 |
|     4 |  18976 | 18829 | 18961 | 21645 |
|     5 |  23693 | 23436 | 74349 | 29796 |
|     6 |  23264 | 23707 | 27005 | 44103 |
|     7 |  28574 | 28979 | 33169 | 58759 |
|     8 |  33155 | 34405 | 39339 | 65182 |
|     9 |  37088 | 37788 | 49863 |156745 |
|    10 |  41543 | 42103 | 58533 |215278 |
|    11 |  47638 | 50329 | 66620 |335603 |
|    12 |  49759 | 51228 | 75087 |305075 |
|    13 |  53938 | 53924 | 77790 |366879 |
|    14 |  58422 | 59565 | 90501 |466368 |
|    15 |  62161 | 64129 | 90814 |525780 |
|    16 |  67061 | 66663 | 98734 |440558 |
|    17 |  71132 | 69753 |171203 |506631 |
|    18 |  74102 | 73130 |293947 |550920 |

我们可以看到,从[9,1024] 以后,时间显注上升。包括[17,512] 和 [18,512] 也显注上升。这是因为,我机器的 L1 Cache 是 32KB, 8 Way 的,前面说过,8 Way的一个组有64个Cache Line,也就是4096个字节,而1024个整型正好是 4096 Bytes,所以,一旦过了这个界,每个步长都无法命中 L1 Cache,每次都是 Cache Miss,所以,导致访问时间一下子就上升了。而 [16, 512]也是一样的,其中的几步开始导致L1 Cache开始失效。

示例三

接下来,我们再来看个示例。下面是一个二维数组的两种遍历方式,一个逐行遍历,一个是逐列遍历,这两种方式在理论上来说,寻址和计算量都是一样的,执行时间应该也是一样的。

const int row = 1024;
const int col = 512
int matrix[row][col];

//逐行遍历
int sum_row=0;
for(int r=0; r<row; r++) {
    for(int c=0; c<col; c++){
        sum_row += matrix[r];
    }
}

//逐列遍历
int sum_col=0;
for(int c=0; c<col; c++) {
    for(int r=0; r<row; r++){
        sum_col += matrix[r];
    }
}

然而,并不是,在我的机器上,得到下面的结果。

  • 逐行遍历:0.081ms
  • 逐列遍历:1.069ms

执行时间有十几倍的差距。其中的原因,就是逐列遍历对于CPU Cache 的运作方式并不友好,所以,付出巨大的代价。

示例四

接下来,我们来看一下多核下的性能问题,参看如下的代码。两个线程在操作一个数组的两个不同的元素(无需加锁),线程循环1000万次,做加法操作。在下面的代码中,我高亮了一行,就是p2指针,要么是p[1],或是 p[18],理论上来说,无论访问哪两个数组元素,都应该是一样的执行时间。

void fn (int* data) {
    for(int i = 0; i < 10*1024*1024; ++i)
        *data += rand();
}

int p[32];

int *p1 = &p[0];
int *p2 = &p[1]; // int *p2 = &p[30];

thread t1(fn, p1);
thread t2(fn, p2);

然而,并不是,在我的机器上执行下来的结果是:

  • 对于 p[0]p[1] :560ms
  • 对于 p[0]p[30]:104ms

这是因为 p[0]p[1] 在同一条 Cache Line 上,而 p[0]p[30] 则不可能在同一条Cache Line 上 ,CPU的缓冲最小的更新单位是Cache Line,所以,这导致虽然两个线程在写不同的数据,但是因为这两个数据在同一条Cache Line上,就会导致缓存需要不断进在两个CPU的L1/L2中进行同步,从而导致了5倍的时间差异

示例五

接下来,我们再来看一下另外一段代码:我们想统计一下一个数组中的奇数个数,但是这个数组太大了,我们希望可以用多线程来完成,这个统计。下面的代码中,我们为每一个线程传入一个 id ,然后通过这个 id 来完成对应数组段的统计任务。这样可以加快整个处理速度。

int total_size = 16 * 1024 * 1024; //数组长度
int* test_data = new test_data[total_size]; //数组
int nthread = 6; //线程数(因为我的机器是6核的)
int result[nthread]; //收集结果的数组

void thread_func (int id) {
    result[id] = 0;
    int chunk_size = total_size / nthread + 1;
    int start = id * chunk_size;
    int end = min(start + chunk_size, total_size);

    for ( int i = start; i < end; ++i ) {
        if (test_data[i] % 2 != 0 ) ++result[id];
    }
}

然而,在执行过程中,你会发现,6个线程居然跑不过1个线程。因为根据上面的例子你知道 result[] 这个数组中的数据在一个Cache Line中,所以,所有的线程都会对这个 Cache Line 进行写操作,导致所有的线程都在不断地重新同步 result[] 所在的 Cache Line,所以,导致 6 个线程还跑不过一个线程的结果。这叫 False Sharing。

优化也很简单,使用一个线程内的变量。

void thread_func (int id) {
    result[id] = 0; 
    int chunk_size = total_size / nthread + 1;
    int start = id * chunk_size;
    int end = min(start + chunk_size, total_size);

    int c = 0; //使用临时变量,没有cache line的同步了
    for ( int i = start; i < end; ++i ) {
        if (test_data[i] % 2 != 0 ) ++c;
    }
    result[id] = c;
}

我们把两个程序分别在 1 到 32 个线程上跑一下,得出的结果画一张图如下所示:

上图中,我们可以看到,灰色的曲线就是第一种方法,橙色的就是第二种(用局部变量的)方法。当只有一个线程的时候,两个方法相当,而且第二种方法还略差一点,但是在线程数增加的时候的时候,你会发现,第二种方法的性能提高的非常快。直到到达6个线程的时候,开始变得稳定(前面说过,我的CPU是6核的)。而第一种方法无论加多少线程也没有办法超过第二种方法。因为第一种方法不是CPU Cache 友好的。

篇幅问题,示例就写到这里,相关的代码参看我的Github相关仓库

延伸阅读

(全文完)


关注CoolShell微信公众账号和微信小程序

(转载本站文章请注明作者和出处 酷 壳 – CoolShell ,请勿用于任何商业用途)

——=== 访问 酷壳404页面 寻找遗失儿童。 ===——

陈皓 March 02, 2020 at 03:43AM
from 酷 壳 – CoolShell https://ift.tt/2VAUecw

VN:F [1.9.22_1171]
Rating: 0.0/5 (0 votes cast)
VN:F [1.9.22_1171]
Rating: 0 (from 0 votes)
Share

【酷壳】 别让自己“墙”了自己

这一两周与几个朋友聊天,有年轻的90后,也有大叔级的70后,这些人在我看来都是很有能力的人,但是一些喜好过于强烈,让我不经意地回顾了我工作20年来身边的人,有发展得好的,也有发展的不好的,有些人是很可惜的,因为限制他们的不是其它人,也不是环境,而是自己,所以,很想写下这篇文章。(注:这篇文章可能会是一篇说教的文章,所以,可能会让你看着犯困,所以,我会尽量地短一些,而且尽可能多讲故事,少道理,这里的故事,全是真实发生的)

几个故事

2019年年初,我面试了一个很年轻的小伙子(93/94年出生),这个小伙子特别有灵性,也很聪明,计算机专业出生,也很喜欢技术,基础和学习能力也很好。在我这20年来认识的人中,如果他能呆在北京、上海、深圳这样的城市,我保证不出三年,他会成为他们同龄人中非常出色的技术人员,如果有个好的舞台有一个好的团队带他,他的未来会非常成功。然而,这个小伙子有两大喜好:1)只愿呆在一个毫无IT的环境的三/四线城市,2)对技术有非常大的偏好,只喜欢Go语言,非常不喜欢其它的语言,比如:Java。

他的这两个喜好,足以让一个未来会很优秀的人毁掉,因为,这个时代没有限制他,他的能力也没有限制他,但是他的意识完完全全地限制了他。

  • 他把自己最宝贵的青春放在了很烂的项目上,就算能用一些新的技术,他也只能算是自娱自乐,在实验室中玩玩具罢了。
  • 他把自己的技术栈封闭起来,而直接放弃了这个时代最具工业化的技术Java,对于一个好的程序员来说,同时掌握几门语言和技术完全是没什么问题,但是自己封闭了自己的视野。

实在是非常可惜,我本来是可以为他介绍到一些很不错的公司的,但是他这样的习性,等于自己把自己未来的门给关上了,虽然我跟他长谈过,但是我也没有办法叫醒不想醒的人……

  • 视野、环境和舞台,对一个人的限制是非常大的。井蛙不知道大海,被空限维度所限制;夏虫不知道冬天,是被时间维度所限制;圈奍的动物没有斗志,是被自己意识所限制。
  • 偏见和不开放,对一个人的限制是真正有毁灭性的。主动让自己成为一个瞎子和聋子,主动把自己的能力阉割掉,这是一件令人痛心的事。想想大清的闭关锁国是如何让世界第一的北洋水师给毁掉的……

我还有个同学,他的技术并不差,就算呆在昆明这种很落后的地方,他也非常地好学,学习英文,学习各种新技术,对技术没有任何的偏好,喜欢C/C++/Java/Python/Shell,同样喜欢前端Javascript,对基础知识非常地踏实,他在技术上没有限制自己的潜力,有什么就学什么。后来,我带他玩Docker/Go/K8S……分布式架构,他也上手的很快……像他这样的人,技术能力完全没得说,比我还大一岁,44岁了,还是一样的天天追代码细节,看Youtube的各种大会,翻github里的各种issue和pull request……

我同学这人,拥有了成为一个技术牛人几乎的条件:基础知识过硬,细节扎得深,面很广,学习能力强,有英文能力,逻辑思维能力不错,非常的自律,执行力也很强,抓得住重点……然而,只有一个小问题,就是没有到大公司历练过,我三番五次叫他从昆明出来,但是最终他都呆在昆明这个城市没有出来,因为有所谓的家庭约束。然而,我身边还有好些人,把自己家从北京搬到上海,从上海搬到深圳,从厦门搬到深圳……这样的人大有人在……像他这样的能力,在哪个公司都会是主力和骨干,对于一个公司的主力和骨干来说,家庭上的这些问题都是小问题都是有很多解的……

另外,我这个同学还是一个比较悲观的人,任何事情都是先想到不好的事,他关注负面的东西会胜于正面的东西,而且他还有一定的社交恐惧,怕与人相处和交流,时间越长越害怕,甚至有时候直接跟我说,“我就是不想改变”这样的话……其实,我以前也是一个很害怕与人交流的人,面试的时候,我根本不敢正眼看面试官一眼,也不知道与人怎么交流。但是,我与他不一样,我努力克服,不断地面试,与人面对面的交流,到一线技术客服接用户的电话,在公司里做分享,慢慢地到外面分享……3-5年就完全克服掉了。

其实,很多事情,完全是有解的,也没有必要担心,自己的心理障碍也是可以克服的,重点就是自己愿不愿意,只要愿意完成了一半,接下来就是不断的摸爬滚打坚持了。

  • 不限制自己的人,会穷举各种方法来解决问题,限制自己的人,只会找各式各样的问题或借口。
  • 不限制自己的人,会努力改变自己的问题和缺陷,限制自己的人,会放任自己。

另外几个故事

我还有另外几个故事(活到四十多,能看到好多人十几年的发展过程,感觉有点上帝视角了)

我还有一个以前团队里的一个小伙,人是很聪明,但就完全就是野路子,他对技术没有什么偏好,一个PHP程序员,做那个Discuz!论坛,公司被并购了,转成Java,开始研究Java的各种细节,对技术从来没有什么偏见,有什么就玩什么,每做一个项目,就算是一样的他都要用新的技术做一遍,然后跟着我做云计算,我教他TCP,教他C/C++,Docker/Go,等等,一点就通,他是我见过学习能力最强的人。但是,有一个事他一直与我的想法不一样,就是我希望他先把软件设计好,再写代码,他非常不能理解,他习惯于直接动手开干,然后有什么问题就整什么问题,我也很难教育他。

有一天,他电话面了一下Facebook,电话面了15分钟后对方就放弃了,他受到了严重的打击。然后,他就开始找菲利宾人练英文口语了,我也让他做算法题,然后,他才发现,一道连算法都不是的纯编程题都提交几次都过不了,等他做完了Leetcode最初的那151道题后,整个人都改变了,写代码前认认真真地在纸上把程序的状态,处理时序以及可能遇到的一些条件先罗列出来,然后,进行逻辑设计后,再写,从此,他就开启他更大的天地了。我后来把他推荐给了微软,先在中国的Bing,在中国升好2-3级,然后去了美国的Azure,现在听说他准备要跟 k8s 的 co-founder Brendan Burns 混了(虽然,他现在还在印度人手下,但是,我真的不知道他未来能玩多大,因为今年他才33岁,而且非常聪明)

他以前是把自己封闭起来的,我叫他出来,他也不出来,后来因为一些办公室政治的原因不得不来找我,于是我就带着他玩了两年,跟他讲了很多外面的世界是怎么玩的,他这个人也是一个相当不善于社交的人,但是心是开放的,愿意接受新的东西,虽然对技术也有一定偏见,比如不喜欢Windows,但是也不会不喜欢到完全封闭。后来我跟他说,微软的技术相当的强的,你看到的技术只是表面,深层次的东西都是相通的,直到他到了微软后发现各种牛逼的东西,对微软系统的技术的态度也有了改变,而且我让他跟我说很多微软那边的事,我发现,他对技术了解的维度已经是越来越高级的了……

还是我以前团队的一个小伙,他是一个前端,他说前端的东西没什么意思,想来找我做后端,我也一点点带他……后来,我说,你如果想要玩得好,你必需来北京,无论现在你觉得过得有多好,你都要放弃掉,然后,尽最大可能出去经历一下世界最顶尖的公司,我甚至跟他说,如果他女朋友不跟来的话,就先分开一段时间,先自己立业,他来北京的时候,他之前的同事都等着看他的笑话,我说,那些人连想都不敢想,不必管他们。于是,他去了Amazon,再过了一年去了西雅图,我跟他说,接下来就是去AWS,然后,如果有足够的野心,用自己的年轻这个资本去硅谷创业公司赌一把……未来他怎么样我不知道,但至少他没有限制自己,他的未来不会有封顶……

也是我的同学,我跟他在大学是上下铺,后来他去了人民大学读计算机博士,大学的时候做国产数据库kingbase,然后去了一家外企,天天被派到用户那边做数据分析,后来,他想回科研单位做国产数据库,我说,别啊,你的技术比我好太多,还有博士理论加持,你不去国外顶尖公司玩玩,你不知道自己有多强的,于是他跟公司申请去了国外做核心,后来因为Hadoop的原因,公司的产品最终成为了历史,于是我说,你来了美国么,你一定要去AWS,于是他就去了AWS的Aurora团队,成为了AWS明星级产品的中坚力量,天天在改MySQL的核心源码,干了两年,被提升为Principle Software Engineer ……

这里我到不是说出国有多牛,也许你只关注能挣多少钱,但是我想说,他们之所以能有这样的际遇,除了他们本来就有实力,还更因为他们从来不给自己设制什么限制,就是那种“艺多不压身”,有什么就学什么,有更高的就去向更高的迈进,其它的像家庭什么的问题其实都是会有解的,真的不必担心太多……

 别限制了自己

上面的这些故事,也许你能看得懂,也许你看得不一定能懂,这里,让我来做个总结吧

  • 做有价值的事。这个世界对计算机人才的要求是供不应求的,所以,不要让自己为自己找各式各样的借口,让自己活在“玩玩具”、“搬砖”和“使蛮力加班”的境地。其实,我发现这世界上有能力的人并不少,但是有品味的人的确很少。所谓的有价值,就是,别人愿付高价的,高技术门槛的,有创造力的,有颠覆性的……
  • 扩大自己的眼界,开放自己的内心。人要变得开放,千万不要做一个狭隘的民族主义者,做一个开放的人,把目光放在全人类这个维度,不断地把自己融入到世界上,而不是把自己封闭起来,这里,你的英文语言能力对你能不能融入世界是起决定性的作用。开放自己的心态,正视自己的缺点,你才可能往前迈进。你的视野决定了你的知不知道要去哪,你的开放决定了你想不想去
  • 站在更高的维度。面的维度会超过点的维点,空间的维度会超过面的维度,在更高维度上思考和学习,你会获得更多。整天在焦虑那些低维度的事(比如自己的薪水、工作的地点、稳不稳定、有没有户口……),只会让你变得越来越平庸,只要你站在更高的维度(比如: 眼界有没有扩大、可能性是不是更多、竞争力是不是更强、能不能解决更大更难的问题、能创造多大的价值……),时间会让你明白那些低维度的东西全都不是事儿。技术学习上也一样,站在学习编程语气特性的维度和站在学习编程范式、设计模式的维度是两种完全不一样的学习方式。
  • 精于计算得失。很多人其实不是很懂计算。绝大多数人都是在算计自己会失去多少,而不会算会得到多少。而一般的人也总是在算短期内会失去什么,优秀则总是会算我投入后未来会有什么样的回报,前者在算计今天,目光短浅,而后者则是舍在今天,得在明天,计算的是未来。精于计算得失的,就懂得什么是投资,不懂的只会投机。对于赚钱,你可以投机,但是对于自己最好还是投资。
  • 勇于跳出传统的束缚。有时候,跳出传统并不是一件很容易的事,因为大多数人都会对未知有恐惧的心理。比如:我看到很多人才都被大公司垄断了,其实,有能力的人都不需要加入大公司,有能力的人是少数,这些少数的人应该是所有的公司share着用的,这样一来,对于所有的人都是利益最大化的。这样的事现在也有,比如:律师、设计师……。但是,绝大多数有能力的技术人员是不敢走出这步。我在2015年到2016年实践过一年半,有过这些实践,做“鸡”的比“二奶”好多了,收入也好很多很多(不好意思开车了)……

孟子说过几句话——

井蛙不可以语于海者,拘于虚也;//空间局限

夏虫不可以语于冰者,笃于时也;//时间局限

曲士不可以语于道者,束于教也。//认识局限

别自己墙了自己,人最可悲的就是自己限制自己,想都不敢想,共勉!

(全文完)


关注CoolShell微信公众账号和微信小程序

(转载本站文章请注明作者和出处 酷 壳 – CoolShell ,请勿用于任何商业用途)

——=== 访问 酷壳404页面 寻找遗失儿童。 ===——

陈皓 December 01, 2019 at 07:10PM
from 酷 壳 – CoolShell https://ift.tt/2L8vNxh

VN:F [1.9.22_1171]
Rating: 0.0/5 (0 votes cast)
VN:F [1.9.22_1171]
Rating: 0 (from 0 votes)
Share

【酷壳】 Unix 50 年:Ken Thompson 的密码

50年前,除了Apollo上天之外,还有一个大事的发生,就是Unix操作系统的诞生,若干年前我写过《Unix的传奇,上篇下篇》,Unix是我入行前十年伴我成长的操作系统,虽然现在Linux早已接过了Unix的时代交接棒,但是,Unix文化对我个人的技术观影响是非常大的(注:《Unix编程艺术》是一本对影响我很深的书),而对于 Ken Thompson 和 Dennis Ritchie 这两位 Unix 的缔造者,也是计算机圈中的神一般的人物。今天,Dennis已经去逝,Ken在Google里跟 Rob Pike和 Robert Griesemer 这两位大神在开发Go语言。

P.S. 今年,我一直想写篇Unix 50周年纪念的文章,但一直无从下手,因为不想写过大的命题,如果能写个轶事最好不过。正好过完国庆节,技术圈里有个“热搜”——Ken Thompson的密码。但一直没有时间,所以拖到今天才写下来。

正文开始,2014年,有个叫Leah Neukirchen的程序员(blog)在 BSD 3 的源代码中的 /etc/passwd 看到了早年Unix黑客们的被 hash了的密码,该文件如下所示:

root:OVCPatZ8RFmFY:0:10:Ernie Co-vax,4156427925:/:
daemon:*:1:1:The devil himself:/:
bill:.2xvLVqGHJm8M:8:10:& Joy,4156424948:/usr/bill:/bin/csh
ozalp:m5syt3.lB5LAE:40:10:& Babaoglu,4156423806:/usr/ozalp:/bin/csh
sklower:8PYh/dUBQT9Ss:2:10:Keith &,4156424972:/usr/staff/sklower:/bin/csh
kridle:4BkcEieEtjWXI:3:10:Bob &,4156426744:/usr/staff/kridle:/bin/csh
kurt:olqH1vDqH38aw:4:10:& Shoens,4156420572:/usr/staff/kurt:/bin/csh
schmidt:FH83PFo4z55cU:7:10:Eric &,4156424951:/usr/staff/schmidt:/bin/csh
hpk:9ycwM8mmmcp4Q:9:10:Howard Katseff,2019495337:/usr/staff/hpk:/bin/csh
tbl:cBWEbG59spEmM:10:10:Tom London,2019492006:/usr/staff/tbl:
jfr:X.ZNnZrciWauE:11:10:John Reiser:/usr/staff/jfr:
mark:Pb1AmSpsVPG0Y:12:10:& Horton,4156428311:/usr/staff/mark:/bin/csh
dmr:gfVwhuAMF0Trw:42:10:Dennis Ritchie:/usr/staff/dmr:
ken:ZghOT0eRm4U9s:52:10:& Thompson:/usr/staff/ken:
sif:IIVxQSvq1V9R2:53:10:Stuart Feldman:/usr/staff/sif:
scj:IL2bmGECQJgbk:60:10:Steve Johnson:/usr/staff/scj:
pjw:N33.MCNcTh5Qw:61:10:Peter J. Weinberger,2015827214:/usr/staff/pjw:/bin/csh
bwk:ymVglQZjbWYDE:62:10:Brian W. Kernighan,2015826021:/usr/staff/bwk:
uucp:P0CHBwE/mB51k:66:10:UNIX-to-UNIX Copy:/usr/spool/uucp:/usr/lib/uucp/uucico
srb:c8UdIntIZCUIA:68:10:Steve Bourne,2015825829:/usr/staff/srb:
finger::199:199:The & Program:/usr/ucb:/usr/ucb/finger
who::199:199:The & Program:/usr/ucb:/bin/who
w::199:199:The & Program:/usr/ucb:/usr/ucb/w
mckusick:AAZk9Aj5/Ue0E:201:10:Kirk &,4156424948:/usr/staff/mckusick:/bin/csh
peter:Nc3IkFJyW2u7E:202:10:& Kessler,4156424948:/usr/staff/peter:/bin/csh
henry:lj1vXnxTAPnDc:203:10:Robert &,4156424948:/usr/staff/henry:/bin/csh
jkf:9ULn5cWTc0b9E:209:10:John Foderaro,4156424972:/usr/staff/jkf:/bin/csh
fateman:E9i8fWghn1p/I:300:10:Richard &,4156421879:/usr/staff/fateman:/bin/csh
fabry:d9B17PTU2RTlM:305:10:Bob &,4156422714:/usr/staff/fabry:/bin/csh
network:9EZLtSYjeEABE:501:50:*:/usr/net/network:/usr/net/network/nsh
tty::504:50::/:/bin/tty我

(注,以前Unix是一个服务器,所有人都用一个终端到服务器上进行操作,于是,这个服务上的 /etc/password 下保存着所有的人的登录密码,能让所有的人都能读到,为了不让别人猜到,这个文件中的密码保存(第二列)被做过哈希处理)

这位程序员一看,这些个用户不就是Dennis Ritchie, Ken Thompson, Brian W. Kernighan, Steve Bourne, Bill Joy 这些神人的密码吗?!于是,他想看看这些人用什么样的密码。考虑到当时的加密算法用的是基于DES的 crypt(3) 算法(这个算法今天还在用,像Perl/PHP/Python/Ruby都提供crypt() 函数),而且当时的密码最长只支持8个长度,所以,感觉还是很容易暴力破解的。

一般来说,暴力破解的这种hash密码的工具主要是用hashcatjohn ,很快,Leah 破解了大多数人的密码,因为大多数都使用的是比较弱的密码,比如: Brian W. Kernighanbwk)使用了 /.,/., 这样的密码,而 Dennis Ritchiedmr)则使用了 dmac 这样的密码。然后,在破解到 Ken Thompson的密码时,搞不定了,花了好几天穷举完了所有的小写字母+数字都没有找到。

因为这个crypt的算法也是Ken Thompson 和 Robert Morris 写的,他们在40年前就发现,原来的hash算法太快了,这样很容易被暴力穷举,于是在第七版的Unix(1979年发布),他们把算法改成DES的算法,就是要让这个算法变慢。详细地说,用户密码被截断为八个字符,每个字符仅被压缩为7位。这形成56位DES密钥。然后,该密钥用于加密全零位块,然后再次使用相同的密钥对密文进行加密,依此类推,总共进行了25次DES加密。感觉跟区块链的“挖矿”有点像。在最早的Unix计算机上,这个算法需要花了整整一秒钟的时间来计算密码哈希

这几十年来,计算机的计算速度根据摩尔定律至少double了20次,所以,DES算法已经很容被攻击了,然而,对于Ken Thompson的密码,在2014年还是很不容易被破解的,因为,如果要加上所有的大小写字符数字和其它特殊字符,那么,在2014年,就算用最快的GPU来穷举所有的8位长度的密码,也需要花上至少2年以上的时间

在2019年10月份,在 The Unix Heritage Society 这个社区中,这个事又被人问起来,说以前有个人破解这些密码,不知道有没有全破解出来了?于是Leah看到了,就回应说,那个人是我,但是还是没干出来……于是好些人进来留言。

5天后,2019年10月08日,一个来自澳大利亚的程序员Nigel Williams说,Ken的密码我破解出来了,哈希串ZghOT0eRm4U9s 明文是 p/q2-q4!(果然是有数字有特殊字符),小伙说,我在 AMD Radeon Vega 64 的 GPU上运行了 hashcat 这个命令,干了我 4天多,每秒钟的“配速”是930MH/s (每秒钟9亿3千万次hash运算)。然后,Ken Thompson 也留言到 “恭喜” ,这样,Ken 的密码在40年后被破解了……

马上,就有人问到,这个密码是不是国际象棋的走棋?嗯,很像中国象棋中的“车五进一”,“马三退一”,这个密码中的 p 代表 pawn 小兵,从 q2 的位置走到 q4,这个看来是国际象棋中的开局进兵——用来做登录密码,非常合适。而且,Ken Thompson 在 Unix中写下的一个国际象棋的程序 Belle,在1978年首次参加计算机协会的北美计算机国际象棋锦标赛时,它获得了第一个冠军头衔,其搜索深度为八层。之后又赢得了四次冠军。1983年,它也成为第一台获得国际象棋“大师”称号的计算机。所以,Ken用这个做密码相当make sense!

Ken在贝尔实验室调程序(图片来源:IEEE SPECTRUM

当然,还有一个人的密码是所有人里最难破解的,这个人就是Bill Joy,他最初作为加州大学伯克利分校的研究生,在校期间着手改进Unix 内核,并管理BSD发行版。他最著名的贡献是ex和vi编辑器以及C shell。在Sun公司成立6个月后,他正式成为公司的联合创始人,他在Sun公司的推动了NFS,SPARC处理器,以及Java语言。他还是一个风险投资人员。

在Ken的密被破解后两周(2019年10月19日),有人号称已经破解了Bill的密码,他在邮件组中这样写到

一开始,我使用了大小写字符和数字,8位长度来破解所有的组合,花了我6天的时间,失败了。然后,我开始尝试只用小写字母和控制字符,结果在40分钟内就破解了。但是因为Bill现健在,所以,只要bill同意他才公布这个密码。

在密码里存控制字符?这脑洞,Ctrl+C么?破解者还说,他在一个有三个结点的DELL 的HPC集群上完成这个工作,每个结点包括两个 Tesla V100 nVidia GPU 的显卡,一共30720个CUDA核…… 关于这个显卡多少钱,你可以上网搜吧…… 相当于一块劳力士吧……(我估计这组机器平时是用来挖矿的……[狗头])

好了,我们来看一下这个 /etc/passwd 中的这些人的密码是什么样的,但最主要的是向这些为人类做过巨大贡献的程序员科学家们致敬

  • Ken Thompson
    除了是Unix、B语言和Go语言作者之外,他还贡献过正则表达式,QED/ed编辑器,UTF-8编码定义,以及计算机国际象棋Belle……

    登录名 哈希串 密码
    ken ZghOT0eRm4U9s p/q2-q4!
  • Dennis Ritchie
    Unix和C语言之父,与Ken于1983年获图灵奖,1990年美国国家海明奖章,于2011年去世。

    登录名 哈希串 密码
    dmr gfVwhuAMF0Trw dmac
  • Brian W. Kernighan
    AWK的作者,是AWK中的“K”,也是与Dennis写的K&C的C语言编程书中的“K”,他还编写了很多Unix的其它程序,如:ditroff,而且,。设计了著名的启发式算法

    登录名 哈希串 密码
    bwk ymVglQZjbWYDE /.,/.,
  • Stephen R. Bourne
    Bourne shell(sh)的作者,Unix Shell作者,同时也是Unix调试器的作者。

    登录名 哈希串 密码
    srb c8UdIntIZCUIA bourne
  • Eric Schmidt
    你可能知道他是Google的CEO,苹果的董事,但是你可能不知道,他当年是是贝尔实施室的实习生,他对Unix的词法分析器 Lex 进行为了完全的重写。他的密码是中的wendy应该是他的妻子。

    登录名 哈希串 密码
    schmidt FH83PFo4z55cU wendy!!!
  • Stuart Feldman
    他除了是Unix系统小组的成员,他还是第一个Fortran 77 编译器的作者,也是 make 的作者。他还是楼上Shmidt慈善基金会的科学负责人,在Google/IBM Research任过职,也担任过ACM的主席。

    登录名 哈希串 密码
    sif IIVxQSvq1V9R2 axolotl
  • Mark Horton
    Unix贡献者,包括vi和curses,后来变性为女性,新的名字叫Mary Ann Horton。原来的照片在Unix Guru Universe

    登录名 哈希串 密码
    mark Pb1AmSpsVPG0Y uio
  • Kirk McKusick
    BSD贡献者,主要负责文件系统UFS以及fsck命令,同时也是gprof的贡献者,公开的同性恋者。

    登录名 哈希串 密码
    mckusick AAZk9Aj5/Ue0E foobar
  • Richard Fateman
    他在伯克利的VAX UNIX系统的开发工作中发挥了重要作用,以及开发了 Franz Lisp

    登录名 哈希串 密码
    fateman E9i8fWghn1p/I apr1744
  • Peter Kessler
    这位老兄能在网上查到的资料基本没有,可以查到他是 gprof 的贡献者,以及有名字的gprof的一篇论文

    登录名 哈希串 密码
    peter Nc3IkFJyW2u7E ...hello
  • Kurt Shoens
    BSD电子邮件开发者。Unix早期版本中使用 uuxsendmail 来进行远程消息传递,1978年,Kurt为Unix编写了一个邮件用户代理 Berkeley Mail。相关的历史可以参看这篇文章

    登录名 哈希串 密码
    kurt olqH1vDqH38aw sacristy
  • John Foderaro
    他为Berkeley的Lisp语言编写原始的编译器,Lisp语言是一种类似于数据代数的语言,在计算机历史上有和C语言一样的作用。后来他成立了Franz公司,主要开发和部署图形搜索解决方案。

    登录名 哈希串 密码
    jkf 9ULn5cWTc0b9E sherril.
  • Peter J. Weinberger
    他就是AWK中的那个“A”,同时也是Fortan编译器f77的贡献者,后来是Renaissance Technologies (一家对冲基金)的CTO,现在在Google工作,

    登录名 哈希串 密码
    pjw N33.MCNcTh5Qw uucpuucp
  • John Reiser
    他主要工作是将Unix和C移植到了DEC VAX上,这个机器在学术界相当流行(陈皓注:我在1994年上大学的时候,就是在这个机器上学习的C语言)。这扩大了Unix和C的影响力。

    登录名 哈希串 密码
    jfr X.ZNnZrciWauE 5%ghj
  • Steve Johnson
    曾在贝尔实验室和AT&T工作近20年。他以Yacc,Lint,spell和Portable C编译器而闻名。后来他去了硅谷,加入了一些创业公司,主要从事编译器的工作,以及2D和3D图形,大规模并行系统和嵌入式系统的开发工作。现在他在Wave Computing从事机器学习的工作。

    登录名 哈希串 密码
    scj IL2bmGECQJgbk pdq;dq
  • Bob Kridle
    这位老兄的资料在没有太多,只能在 Berkeley Unix 20 年 上看到他跟Ken Thompson混过一段时间。

    登录名 哈希串 密码
    kridle 4BkcEieEtjWXI jilland1
  • Keith Sklower
    BSD 的一个程序员。从他的主页上可以看到他目前在Berkeley大学,信息分析师,主要研究一些网络通信相关的技术。

    登录名 哈希串 密码
    sklower 8PYh/dUBQT9Ss theik!!!
  • Robert Henry
    网上的资料不多,只在Life with Unix这本电子书中查到,他写了 error

    登录名 哈希串 密码
    henry lj1vXnxTAPnDc sn74193n
  • Howard Katseff
    网上的资料不多,只在Life with Unix这本电子书中查到,他写了 sdblast

    登录名 哈希串 密码
    hpk 9ycwM8mmmcp4Q graduat;
  • Özalp Babaoğlu
    土耳其计算机科学家,1981年在Berkeley担任 BSD Unix的首席设计师,曾经与Sun的创造人Bill Joy在BSD上实现了虚拟内存。

    登录名 哈希串 密码
    ozalp m5syt3.lB5LAE 12ucdort
  • Bob Fabry
    他主要推动美国国防部高级研究计划局DARPA采用了Unix系统

    登录名 哈希串 密码
    fabry d9B17PTU2RTlM 561cml..
  • Tom London
    他和John Reiser在把Unix移植到了VAX-11机上。

    登录名 哈希串 密码
    tbl cBWEbG59spEmM ..pnn521

最后,再首尾呼应一下,在我的技术生涯中,Unix文化对我个人的技术观影响是非常大的,我个人认为 Unix 就像摇滚乐一样,上世纪60年代-80年代,是整个人类最经典最光亮的时代,值得我们每个人向那个时代的人和事致敬!

————————————————————————

P.S.

你可以浏览 Github 的 unix-history-repo 目录(注:本文给的这个链接不在master分支上),这个repo是40年前的代码,涵盖了从1970年创建时的2.5万行内核和26条命令到2017年为止广泛使用的2700万行系统。1.1GB的存储库包含大约一百万次提交和两千多次合并。通过这个链接你可以了解一下这个代码的历史!

下载这些代码需要你的1.5GB的硬盘空间,你可以查看各个大神写的代码,包括 Ken Thompson 和 Dennis的,以及相关的注释。

根据这些,你还可以找到 Ken Thompson的 Github账号 https://github.com/ken 以及别人为dmr建的github帐号 https://github.com/dmr-1941-2011

P.S.S

下面是一些和Unix相关的维基百科资料

还有Unix的社区:TUHS: The Unix Heritage Society – The Unix Tree

(全文完)


关注CoolShell微信公众账号和微信小程序

(转载本站文章请注明作者和出处 酷 壳 – CoolShell ,请勿用于任何商业用途)

——=== 访问 酷壳404页面 寻找遗失儿童。 ===——

陈皓 November 03, 2019 at 02:12PM
from 酷 壳 – CoolShell https://ift.tt/36qwY42

VN:F [1.9.22_1171]
Rating: 0.0/5 (0 votes cast)
VN:F [1.9.22_1171]
Rating: 0 (from 0 votes)
Share

【酷壳】 HTTP的前世今生

HTTP (Hypertext transfer protocol) 翻译成中文是超文本传输协议,是互联网上重要的一个协议,由欧洲核子研究委员会CERN的英国工程师 Tim Berners-Lee v发明的,同时,他也是WWW的发明人,最初的主要是用于传递通过HTML封装过的数据。在1991年发布了HTTP 0.9版,在1996年发布1.0版,1997年是1.1版,1.1版也是到今天为止传输最广泛的版本(初始RFC 2068 在1997年发布, 然后在1999年被 RFC 2616 取代,再在2014年被 RFC 7230 /7231/7232/7233/7234/7235取代),2015年发布了2.0版,其极大的优化了HTTP/1.1的性能和安全性,而2018年发布的3.0版,继续优化HTTP/2,激进地使用UDP取代TCP协议,目前,HTTP/3 在2019年9月26日 被 Chrome,Firefox,和Cloudflare支持,所以我想写下这篇文章,简单地说一下HTTP的前世今生,让大家学到一些知识,并希望可以在推动一下HTTP标准协议的发展。

HTTP 0.9 / 1.0

0.9和1.0这两个版本,就是最传统的 request – response的模式了,HTTP 0.9版本的协议简单到极点,请求时,不支持请求头,只支持 GET 方法,没了。HTTP 1.0 扩展了0.9版,其中主要增加了几个变化:

  • 在请求中加入了HTTP版本号,如:GET /coolshell/index.html HTTP/1.0
  • HTTP 开始有 header了,不管是request还是response 都有header了。
  • 增加了HTTP Status Code 标识相关的状态码。
  • 还有 Content-Type 可以传输其它的文件了。

我们可以看到,HTTP 1.0 开始让这个协议变得很文明了,一种工程文明。因为:

  • 一个协议有没有版本管理,是一个工程化的象征。
  • header是协议可以说是把元数据和业务数据解耦,也可以说是控制逻辑和业务逻辑的分离。
  • Status Code 的出现可以上请求双方以及第三方的监控或管理程序有了统一的认识。最关键是还是控制错误和业务错误的分离。

(注:国内很多公司HTTP无论对错只返回200,这种把HTTP Status Code 全部抹掉完全是一种工程界的倒退)

但是,HTTP1.0性能上有一个很大的问题,那就是每请注一个资源都要新建一个TCP链接,而且是串行请求,所以,就算网络变快了,打开网页的速度也还是很慢。所以,HTTP 1.0 应该是一个必需要淘汰的协议了。

 HTTP/1.1

HTTP/1.1 主要解决了HTTP 1.0的网络性能的问题,以及增加了一些新的东西:

  • 可以设置 keepalive 来让HTTP重用TCP链接,重用TCP链接可以省到广域网上的TCP的三次握手的巨大开销。这是所谓的“HTTP 长链接” 或是 “请求响应式的HTTP 持久链接”。英文叫 HTTP Persisten connection.
  • 然后支持pipeline网络传输,只要第一个请求发出去了,不必等其回来,就可以发第二个请求出去,可以减少整体的响应时间。(注:非幂等的POST 方法或是有依赖的请求是不能被pipeline化的)
  • 支持 Chunked responses ,也就是说,在Response的时候,不必说明 Content-Length 这样,客户端就不能断连接,直到收到服务端的EOF标识。这种技术又叫 “服务端Push模型”,或是 “服务端Push式的HTTP 持久链接
  • 还增加了 cache control 机制。
  • 协议头注增加了 Language, Encoding, Type 等等头,让客户端可以跟服务器端进行更多的协商。
  • 还正式加入了一个很重要的头—— HOST这样的话,服务器就知道你要请求哪个网站了。因为可以有多个域名解析到同一个IP上,要区分用户是请求的哪个域名,就需要在HTTP的协议中加入域名的信息,而不是被DNS转换过的IP信息。
  • 正式加入了 OPTIONS 方法,其主要用于 CORS – Cross Origin Resource Sharing 应用。

HTTP/1.1应该分成两个时代,一个是2014年前,一个是2014年后,因为2014年HTTP/1.1有了一组RFC(7230 /7231/7232/7233/7234/7235),这组RFC又叫“HTTP/2 预览版”。其中影响HTTP发展的是两个大的需求:

  • 一个需要是加大了HTTP的安全性,这样就可以让HTTP应用得广泛,比如,使用TLS协议。
  • 另一个是让HTTP可以支持更多的应用,在HTTP/1.1 下,HTTP已经支持四种网络协议:
    • 传统的短链接。
    • 可重用TCP的的长链接模型。
    • 服务端push的模型。
    • WebSocket模型。

自从2005年以来,整个世界的应用API越来多,这些都造就了整个世界在推动HTTP的前进,我们可以看到,自2014的HTTP/1.1 以来,这个世界基本的应用协议的标准基本上都是向HTTP看齐了,也许2014年前,还有一些专用的RPC协议,但是2014年以后,HTTP协议的增强,让我们实在找不出什么理由不向标准靠拢,还要重新发明轮子了。

HTTP/2

虽然 HTTP/1.1 已经开始变成应用层通讯协议的一等公民了,但是还是有性能问题,虽然HTTP/1.1 可以重用TCP链接,但是请求还是一个一个串行的发的,需要保证其顺序。然而,大量的网页请求中都是些资源类的东西,这些东西占了整个HTTP请求中最多的传输数据量。所以,理论上来说,如果能够并行这些请求,那就会增加更大的网络吞吐和性能。

另外,HTTP/1.1传输数据时,是以文本的方式,借助耗CPU的zip压缩的方式减少网络带宽,但是耗了前端和后端的CPU。这也是为什么很多RPC协议诟病HTTP的一个原因,就是数据传输的成本比较大。

其实,在2010年时,Google 就在搞一个实验型的协议,这个协议叫SPDY,这个协议成为了HTTP/2的基础(也可以说成HTTP/2就是SPDY的复刻)。HTTP/2基本上解决了之前的这些性能问题,其和HTTP/1.1最主要的不同是:

  • HTTP/2是一个二进制协议,增加了数据传输的效率。
  • HTTP/2是可以在一个TCP链接中并发请求多个HTTP请求,移除了HTTP/1.1中的串行请求。
  • HTTP/2会压缩头,如果你同时发出多个请求,他们的头是一样的或是相似的,那么,协议会帮你消除重复的部分。这就是所谓的HPACK算法(参看RFC 7541 附录A)
  • HTTP/2允许服务端在客户端放cache,又叫服务端push,也就是说,你没有请求的东西,我服务端可以先送给你放在你的本地缓存中。比如,你请求X,我服务端知道X依赖于Y,虽然你没有的请求Y,但我把把Y跟着X的请求一起返回客户端。

对于这些性能上的改善,在Medium上有篇文章你可看一下相关的细节说明和测试“HTTP/2: the difference between HTTP/1.1, benefits and how to use it

当然,还需要注意到的是HTTP/2的协议复杂度比之前所有的HTTP协议的复杂度都上升了许多许多,其内部还有很多看不见的东西,比如其需要维护一个“优先级树”来用于来做一些资源和请求的调度和控制。如此复杂的协议,自然会产生一些不同的声音,或是降低协议的可维护和可推展性。所以也有一些争议。尽管如此,HTTP/2还是很快地被世界所采用。

HTTP/2 是2015年推出的,其发布后,Google 宣布移除对SPDY的支持,拥抱标准的 HTTP/2。过了一年后,就有8.7%的网站开启了HTTP/2,根据 这份报告 ,截止至本文发布时(2019年10月1日 ), 在全世界范围内已经有41%的网站开启了HTTP/2。

HTTP/2的官方组织在 Github 上维护了一份各种语言对HTTP/2的实现列表,大家可以去看看。

我们可以看到,HTTP/2 在性能上对HTTP有质的提高,所以,HTTP/2 被采用的也很快,所以,如果你在你的公司内负责架构的话,HTTP/2是你一个非常重要的需要推动的一个事,除了因为性能上的问题,推荐标准也是架构师的主要职责,因为,你企业内部的架构越标准,你可以使用到开源软件,或是开发方式就会越有效率,跟随着工业办的标准的发展,你的企业会非常自然的享受到标准所带来的红利。

HTTP/3

然而,这个世界没有完美的解决方案,HTTP/2也不例外,其主要的问题是:苦干个HTTP的请求在复用一个TCP的连接,底层的TCP协议是不知道上层有多少个HTTP的请求的,所以,一旦发生丢包,造成的问题就是所有的HTTP请求都必需等待这个丢了的包被重传回来,那怕丢的那个包不是我这个HTTP请求的。因为TCP底层是没有这个知识了。

这个问题又叫Head-of-Line Blocking问题,这也是一个比较经典的流量调度的问题。这个问题最早主要的发生的交换机上。下图来自Wikipedia。

图中,左边的是输入队列,其中的1,2,3,4表示四个队列,四个队列中的1,2,3,4要去的右边的output的端口号。此时,第一个队列和第三个队列都要写右边的第四个端口,然后,一个时刻只能处理一个包,所以,一个队列只能在那等另一个队列写完后。然后,其此时的3号或1号端口是空闲的,而队列中的要去1和3号端号的数据,被第四号端口给block住了。这就是所谓的HOL blocking问题。

HTTP/1.1中的pipeline中如果有一个请求block了,那么队列后请求也统统被block住了;HTTP/2 多请求复用一个TCP连接,一理发生丢包,就会block住所有的HTTP请求。这样的问题很讨厌。好像基本无解了。

是的TCP是无解了,但是UDP是有解的 !于是HTTP/3破天荒地把HTTP地底层的TCP协议改成了UDP!

然后又是Google 家的协议进入了标准 – QUIC (Quick UDP Internet Connections)。接下来是QUIC协议的几个重要的特性,为了讲清楚这些特性,我需要带着问题来讲(注:下面的网络知识,如果你看不懂的话,你需要学习一下《TCP/IP详解》一书(在我写blog的这15年里,这本书推荐了无数次了),或是看一下本站的《TCP的那些事》。):

  • 首先是上面的Head-of-Line blocking问题,在UDP的世界中,这个就没了。
  • TCP是一个无私的协议,也就是说,如果网络上出殃拥塞,大家都会丢包,于是大家都会进入拥塞控制的算法中。QUIC才不管这个,是个相对比较激进的协议。QUIC有一套自己的丢包重传和拥塞控制的协,一开始QUIC是重新实现一TCP 的 CUBIC算法,但是随着BBR算法的成熟(BBR也在借鉴CUBIC算法的数学模型),QUIC也可以使用BBR算法。多扯几名,从模型来说,以前的TCP的拥塞控制算法玩的是数学模型,而新型的TCP拥塞控制算法是以BBR为代表的测量模型,理论上来说,后者会更好,但QUIC的团队在一开始觉得BBR不如CUBIC的算法好,所以没有用。现在的BBR 2.x借鉴了CUBIC数学模型让拥塞控制更公平。这里有文章大家可以一读“TCP BBR : Magic dust for network performance.
  • 接下来,现在要建议一个HTTP的连接,先是TCP的三次握手,然后是TLS的三次握手,要整出六次网络交互,一个链接才建好,虽说HTTP/1.1和HTTP/2的连接复用解决这个问题,但是基于UDP后,UDP也得要实现这个事。于是QUIC直接把TCP的和TLS的合并成了三次握手。

 

所以,QUIC是一个在UDP之上的伪TCP +TLS +HTTP/2的多路复用的协议。

但是其于UDP还是有一些挑战的,这个挑战主要来自互联网上的各种网络设备,这些设备根本不知道是什么QUIC,他们看QUIC就只能看到的就是UDP,所以,在一些情况下,UDP就是有问题的,

  • 比如在NAT的环境下,如果是TCP的话,NAT路由或是代理服务器,可以通过记录TCP的四元组(源地址、源端口,目标地址,目标端口)来做连接映射的,然而,在UDP的情况下不行了。于是,QUIC引入了个叫connection id的不透明的ID来标识一个链接,用这种业务ID很爽的一个事是,如果你从你的3G/4G的网络切到WiFi网络(或是反过来),你的链接不会断,因为我们用的是connection id,而不是四元组。
  • 然而就算引用了connection id,也还是会有总理 ,比如一些不够“聪明”的等价路由交换机,这些交换机会通过四元组来做hash把你的请求的IP转到后端的实际的服务器上,然而,他们不懂connection id,只懂四元组,这么导致属于同一个connection id但是四元组不同的网络包就转到了不同的服务器上,这就是导致数据不能传到同一台服务器上,数据不完整,链接只能断了。所以,你需要更聪明的算法(可以参看 Facebook 的 Katran 开源项目 )

好了,就算搞定上面的东西,还有一些业务层的事没解,这个事就是 HTTP/2的头压缩算法 HPACK,HPACK需要维护一个动态的字典表来分析请求的头中哪些是重复的,HPACK的这个数据结构需要在encoder和decoder端同步这个东西。在TCP上,这种同步是透明的,然而在UDP上这个事不好干了。所以,这个事也必需要重新设计了,基于QUIC的QPACK就出来了,利用两个附加的QUIC steam,一个用来发送这个字典表的更新给对方,另一个用来ack对方发过来的update。

目前看下来,HTTP/3目前看上去没有太多的业务上的东西,更多是HTTP/2 + QUIC协议。但,HTTP/3 因为动到了底层协议,所以,在普及方面上可能会比 HTTP/2要慢。但是,试想一下,QUIC这个协议真对TCP是个威胁,如果QUIC成熟了,TCP是不是会有可能成为历史?

未来十年,让我们看看UDP是否能够逆袭TCP……

(全文完)


关注CoolShell微信公众账号和微信小程序

(转载本站文章请注明作者和出处 酷 壳 – CoolShell ,请勿用于任何商业用途)

——=== 访问 酷壳404页面 寻找遗失儿童。 ===——

陈皓 October 01, 2019 at 07:21PM
from 酷 壳 – CoolShell https://ift.tt/2mAAPcE

VN:F [1.9.22_1171]
Rating: 0.0/5 (0 votes cast)
VN:F [1.9.22_1171]
Rating: 0 (from 0 votes)
Share

【酷壳】 闲话登月程序和程序员

2019年7月20日,是有纪念意义的一天,这天不是因为广大网民帮周杰伦在新浪微博上的超话刷到第一,而是阿波罗登月的50周年的纪念日。早在几年前,在Github上放出了当成Apollo飞船使用的源代码(当然是汇编的),但完全不明白为什么这几天会有一些中国的小朋友到这个github的issue里用灌水……,人类历史上这么伟大的一件事,为什么不借这个机会学习一下呢?下面是一些阿波罗登月与程序员相关的小故事,顺着这些东西,你可以把你的周末和精力用得更有价值。

首先,要说的是Apollo 11导航的源代码,这些代码的设计负责人叫Margaret Heafield Hamilton ,是一个女程序员,专业是数学和哲学,1960年得到一个MIT麻省理工大学的临时的软件开发职位,负责在PDP-1和LGP-30上运行天气预报的软件(注:在计算机历史上,PDP系统机器被称为Hack文化的重要推手,PDP-11且推了Unix操作系统,而Unix操作系统则是黑客文化的重要产品。参看《Unix传奇》)。然后,又为美国空军编写探测知敌方飞行的软件,之后,于1965年的时候,她加入了MIT仪器实验室,并成为了这个实验室的主管,这个实验实就是Apollo计划的一部分,她负责编写全新的月球登录的导航软件,以及后来该软件在其他项目中的各个版本。

上图是Hamilton站在她和她的麻省理工团队为阿波罗项目制作的导航软件源代码旁边,在Github上的开源的代码 – Apollo-11 (2016年开源)。我们可以看到,有两个重要的目录,一个目录叫“Comanche055”,一个目录叫“Luminary099”,前是指挥舱用的(英文为 Command Module )后者为登月舱用的(英文为 Lunar Module),这里需要说明一下的是,指挥舱是把登录舱推到月球上,在返回的时候,登录舱是被抛弃掉的,而返回到地球的是指挥舱。如果你想看这两份源代码的纸版,你可以访问这两个链接:Comanche 55 AGC Program ListingLuminary 99 REv.1 AGC Program Listing

如果你仔细比较一下这两个目录中的文件,你会发现有些文件是一样的,不但文件名一样,而且内容也一样。这说明这两个模块中的一些东西是相似的。

这些代码应该是很难读了,因为当时写这些代码的时候,C语言都没有被发明,所以基本上来说都是汇编代码了,而且还可以发现,这些代码的源文件全是以agc后缀结尾的, 看来这还不是我们平时所了解的汇编,所谓的AGC代表了运行这些代码的计算机 – Apollo Guideance Computer 。沿着这个Wikipedia的链接,你可以看到AGC这个电脑的指令是什么样的,看懂那几条指令后,这些源代码也就能读懂了。当然,因为是写成汇编的,所以,读起来还是要费点神的。不过,其中有一个文件是 THE_LUNAR_LANDING.apc 你会不会很好奇想去看看?

打开源文件,你还可以看到每个文件都有很多很多的注释,非常友好,但是也有一些注释比较有趣。这里有一组短视频带你读这些代码 – Exploring the Apollo Guidance Computer(AGC) Code,一供10个小视频,每个2分钟左右,如果你英文听边还行,可以看看,了解一下AGC的工作方式,挺有趣意思的。

下面是AGC在Apollo 1指挥舱里的样子(图片截自上面的视频),这个高质量的3D扫描来自 Simithsonian 3D: Apollo 11 Command Module

这个AGC的操作界又叫DSKY – Display 和 Keyboard的缩写,下图是一个 AGC 模拟器,其官方主页在 https://www.ibiblio.org/apollo/源代码在 Github/VirtualAGC。在这个界面上我们可以看到:下面的键盘上左边有两个键,一个是动词Verb一个是名词Noun,Verb指定操作类型,Noun指定要由Verb命令修改的数据。右边的显示器下面有三个5位的数字,这三个数值显示表示航天器姿态的矢量,以及所需速度变化的显示矢量。是的,当年的导航就靠这三个数字和里面的程序了。

 

如果你想了解AGC更多的细节,你可以看看 这篇 AGC for Dummies

另外,我在Youtube上找到了一个讲当时Apollo电脑的纪录片 – Navigation Computer,太有趣了。比如:21分51秒开始讲存储用的 Rope Memory 绕线内存,Hamilton 也出来讲了一下在这种内存上编程,画内切到一个人用个金属棍在一个像主板一样的东西上,左右穿梭,就像刺绣一样,但是绣的不是图案,而是程序……

看完上面这个纪录篇,我是非常之惊叹,惊叹于50年前的工程能力,惊叹于50年前这些人面对技术的的一丝不苟,对技术的尊重和严谨的这种精神和方法,一点都不比较今天差。

不过,最牛的还不是这个,我在Hamilton的Wikipedia词条上找到了他说的一个事件—— 当年Apollo登陆雷达开关放在了错误的位置,导致AGC收到了不少错误的信号。结果就是AGC既得执行着陆必须的计算,又要接受这些占用其15%时间的额外数据。但是AGC的程序居然可以用高优先级的任务打断低优先级的任务,于是,AGC自动剔除了低级别的任务以保证了重要的任务完成。Hamilton 原话说—— 如果当时的程序不能识别错误并从错误中恢复,我怀疑阿波罗不能成功登月。f the computer hadn’t recognized this problem and taken recovery action, I doubt if Apollo 11 would have been the successful moon landing it was。

看到这里,你有没有觉得——“这个女程序员的一小步,是整个人类的一大步”?

三年前,Apollo的源代码被开源时候,Twitter有个叫 Lin Clark 的人发了一条推:“我妈50年前的代码被放到Github上了”,是的,她是 Hamilton 的女儿,也是一个程序员,目前在 Mozilla工作,Staff Engineer,专长 WebAssembly, Rust, 和 JavaScript 。当她在Twitter上这么自豪地发了一条这样的推后,我不知道各位有什么想法?

 

尤其是那些到Apollo源代码的issue里灌水的人,你们是不是想让你们的孩子在登月100周年纪念的时候说——50年前我爹那个傻叉在Apollo的github的issue列表时灌了水?

(全文完)


关注CoolShell微信公众账号和微信小程序

(转载本站文章请注明作者和出处 酷 壳 – CoolShell ,请勿用于任何商业用途)

——=== 访问 酷壳404页面 寻找遗失儿童。 ===——

陈皓 July 21, 2019 at 07:00PM
from 酷 壳 – CoolShell https://ift.tt/2Z4Julw

VN:F [1.9.22_1171]
Rating: 0.0/5 (0 votes cast)
VN:F [1.9.22_1171]
Rating: 0 (from 0 votes)
Share

【酷壳】 如何超过大多数人

当你看到这篇文章的标题,你一定对这篇文章产生了巨大的兴趣,因为你的潜意识在告诉你,这是一篇“人生秘籍”,而且还是左耳朵写的,一定有很有参考意义,只要读完,一定可以找到超过大多数人的快车道和捷径……然而,当你看到我这样开篇时,你一定会觉得我马上就要有个转折,告诉你这是不可能的,一切都需要付出和努力……然而,你错了,这篇文章还真就是一篇“秘籍”,只要你把这些“秘籍”用起来,你就一定可以超过大多数人。而且,这篇文章只有我这个“人生导师”可以写得好。毕竟,我的生命过到了十六进制2B的年纪,踏入这个社会已超过20年,舍我其谁呢?!

P.S. 这篇文章借鉴于《如何写出无法维护的代码》一文的风格……嘿嘿

相关技巧和最佳实践

要超过别人其实还是比较简单的,尤其在今天的中国,更是简单。因为,你只看看中国的互联网,你就会发现,他们基本上全部都是在消费大众,让大众变得更为地愚蠢和傻瓜。所以,在今天的中国,你基本上不用做什么,只需要不使用中国互联网,你就很自然地超过大多数人了。当然,如果你还想跟他们彻底拉开,甩他们几个身位,把别人打到底层,下面的这些“技巧”你要多多了解一下。

在信息获取上,你要不断地向大众鼓吹下面的这些事:

  • 让大家都用百度搜索引擎查找信息,订阅微信公众号或是到知乎上学习知识……要做到这一步,你就需要把“百度一下”挂在嘴边,然后要经常在群或朋友圈中转发微信公众号的文章,并且转发知乎里的各种“如何看待……”这样的文章,让他们爱上八卦,爱上转发,爱上碎片。
  • 让大家到微博或是知识星球上粉一些大咖,密切关注他们的言论和动向……是的,告诉大家,大咖的任何想法一言一行都可以在微博、朋友圈或是知识星球上获得,让大家相信,你的成长和大咖的见闻和闲扯非常有关系,你跟牛人在一个圈子里你也会变牛。
  • 把今日头条和抖音这样的APP推荐给大家……你只需要让你有朋友成功地安装这两个APP,他们就会花大量的时间在上面,而不能自拔,要让他们安装其实还是很容易的,你要不信你就装一个试玩一会看看(嘿嘿嘿)。
  • 让大家热爱八卦,八卦并不一定是明星的八卦,还可以是你身边的人,比如,公司的同事,自己的同学,职场见闻,社会热点,争议话题,……这些东西总有一些东西会让人心态有很多微妙的变化,甚至花大量的时间去搜索和阅读大量的观点,以及花大量时间与人辩论争论,这个过程会让人上瘾,让人欲罢不能,然而这些事却和自己没有半毛钱关系。你要做的事就是转发其中一些SB或是很极端的观点,造成大家的一睦讨论后,就早早离场……
  • 利用爱国主义,让大家觉得不用学英文,不要出国,不要翻墙,咱们已经是强国了……这点其实还是很容易做到的,因为学习是比较逆人性的,所以,只要你鼓吹那些英文无用论,出国活得更惨,国家和民族都变得很强大,就算自己过得很底层,也有大国人民的感觉。

然后,在知识学习和技能训练上,让他们不得要领并产生幻觉

  • 让他们混淆认识和知识,以为开阔认知就是学习,让他们有学习和成长的幻觉……
  • 培养他们要学会使用碎片时间学习。等他们习惯利用碎片时间吃快餐后,他们就会失去精读一本书的耐性……
  • 不断地给他们各种各样“有价值的学习资料”,让他们抓不住重点,成为一个微信公众号或电子书“收藏家”……
  • 让他们看一些枯燥无味的基础知识和硬核知识,这样让他们只会用“死记硬背”的方式来学习,甚至直接让他们失去信心,直接放弃……
  • 玩具手枪是易用的,重武器是难以操控的,多给他们一些玩具,这样他们就会对玩具玩地得心应手,觉得玩玩具就是自己的专业……
  • 让他们喜欢直接得到答案的工作和学习方式,成为一个伸手党,从此学习再也不思考……
  • 告诉他们东西做出来就好了,不要追求做漂亮,做优雅,这样他们就会慢慢地变成劳动密集型……
  • 让他们觉得自己已经很努力了,剩下的就是运气,并说服他们去‘及时行乐’,然后再也找不到高阶和高效率学习的感觉……
  • 让他们觉得“读完书”、“读过书”就行了,不需要对书中的东西进行思考,进行总结,或是实践,只要囫囵吞枣尽快读完就等同于学好了……

最后,在认知和格局上,彻底打垮他们,让他们变成韭菜。

  • 让他们不要看到大的形势,只看到眼前的一亩三分地,做好一个井底之蛙。其实这很简单,比如,你不要让他们看到整个计算机互联网技术改变人类社会的趋势,你要多让他看到,从事这一行业的人有多苦逼,然后再说一下其它行业或职业有多好……
  • 宣扬一夜暴富以及快速挣钱的案例,最好让他们进入“赌博类”或是“传销类”的地方,比如:股市、数字货币……要让他们相信各种财富神话,相信他们就是那个幸运儿,他们也可以成为巴菲特,可以成为马云……
  • 告诉他们,一些看上去很难的事都是有捷径的,比如:21天就能学会机器学习,用区块链就能颠覆以及重构整个世界等等……
  • 多跟他们讲一些小人物的励志的故事,这样让他们相信,不需要学习高级知识,不需要掌握高级技能,只需要用低等的知识和低级的技能,再加上持续不断拼命重复现有的工作,终有一天就会成功……
  • 多让他们跟别人比较,人比人不会气死人,但是会让人变得浮躁,变得心急,变得焦虑,当一个人没有办法控制自己的情绪,没有办法让自己静下心来,人会失去耐性和坚持,开始好大喜欢功,开始装逼,开始歪门邪道剑走偏锋……
  • 让他们到体制内的一些非常稳定的地方工作,这样他们拥有不思进取、怕承担责任、害怕犯错、喜欢偷懒、得过且过的素质……
  • 让他们到体制外的那些喜欢拼命喜欢加班的地方工作,告诉他们爱拼才会赢,努力加班是一种福报,青春就是用来拼的,让他们喜欢上使蛮力的感觉……
  • 告诉他们你的行业太累太辛苦,干不到30岁。让他们早点转行,不要耽误人生和青春……
  • 当他们要做决定的时候,一定要让他们更多的关注自己会失去的东西,而不是会得到的东西。培养他们患得患失心态,让他们认识不到事物真正的价值,失去判断能力……(比如:让他们觉得跟对人拍领导的马屁忠于公司比自我的成长更有价值)
  • 告诉他们,你现有的技能和知识不用更新,就能过好一辈子,新出来的东西没有生命力的……这样他们就会像我们再也不学习的父辈一样很快就会被时代所抛弃……
  • 每个人都喜欢在一些自己做不到的事上找理由,这种能力不教就会,比如,事情太多没有时间,因为工作上没有用到,等等,你要做的就是帮他们为他们做不到的事找各种非常合理的理由,比如:没事的,一切都是最好的安排;你得不到的那个事没什么意思;你没有面好主要原因是那个面试官问的问题都是可以上网查得到的知识,而不没有问到你真正的能力上;这些东西学了不用很快会忘了,等有了环境再学也不迟……

最后友情提示一下,上述的这些“最佳实践”你要小心,是所谓,贩毒的人从来不吸毒,开赌场的人从来不赌博!所以,你要小心别自己也掉进去了!这就是“欲练神功,必先自宫”的道理。

相关原理和思维模型

对于上面的这些技巧还有很多很多,你自己也可以发明或是找到很多。所以,我来讲讲这其中的一些原理。

一般来说,超过别人一般来说就是两个维度:

  1. 在认知、知识和技能上。这是一个人赖以立足社会的能力(参看《程序员的荒谬之言还是至理名言?》和《21天教你学会C++》)
  2. 在领导力上。所谓领导力就是你跑在别人前面,你得要有比别人更好的能力更高的标准(参看《技术人员发展之路》)

首先,我们要明白,人的技能是从认识开始,然后通过学校、培训或是书本把“零碎的认知”转换成“系统的知识”,而有要把知识转换成技能,就需要训练和实践,这样才能完成从:认识 -> 知识 -> 技能 的转换。这个转换过程是需要耗费很多时间和精力的,而且其中还需要有强大的学习能力和动手能力,这条路径上有很多的“关卡”,每道关卡都会过滤掉一大部分人。比如:对于一些比较枯燥的硬核知识来说,90%的人基本上就倒下来,不是因为他们没有智商,而是他们没有耐心。

认知

要在认知上超过别人,就要在下面几个方面上做足功夫:

1)信息渠道。试想如果别人的信息源没有你的好,那么,这些看不见信息源的人,只能接触得到二手信息甚至三手信息,只能获得被别人解读过的信息,这些信息被三传两递后必定会有错误和失真,甚至会被传递信息的中间人hack其中的信息(也就是“中间人攻击”),而这些找不出信息源的人,只能“被人喂养”,于是,他们最终会被困在信息的底层,永世不得翻身。(比如:学习C语言,放着原作者K&R的不用,硬要用错误百出谭浩强的书,能有什么好呢?)

2)信息质量。信息质量主要表现在两个方面,一个是信息中的燥音,另一个是信息中的质量等级,我们都知道,在大数据处理中有一句名言,叫 garbage in garbage out,你天天看的都是垃圾,你的思想和认识也只有垃圾。所以,如果你的信息质量并不好的话,你的认知也不会好,而且你还要花大量的时间来进行有价值信息的挖掘和处理。

3)信息密度。优质的信息,密度一般都很大,因为这种信息会逼着你去干这么几件事,a)搜索并学习其关联的知识,b)沉思和反省,c)亲手去推理、验证和实践……一般来说,经验性的文章会比知识性的文章会更有这样的功效。比如,类似于像 Effiective C++/Java,设计模式,Unix编程艺术,算法导论等等这样的书就是属于这种密度很大的书,而像Netflix的官方blogAWS CTO的blog等等地方也会经常有一些这样的文章。

知识

要在知识上超过别人,你就需要在下面几个方面上做足功夫:

1)知识树(图)。任何知识,只在点上学习不够的,需要在面上学习,这叫系统地学习,这需要我们去总结并归纳知识树或知识图,一个知识面会有多个知识板块组成,一个板块又有各种知识点,一个知识点会导出另外的知识点,各种知识点又会交叉和依赖起来,学习就是要系统地学习整个知识树(图)。而我们都知道,对于一棵树来说,“根基”是非常重要的,所以,学好基础知识也是非常重要的,对于一个陌生的地方,有一份地图是非常重要的,没有地图的你只会乱窜,只会迷路、练路、走冤枉路!

2)知识缘由。任何知识都是有缘由的,了解一个知识的来龙去脉和前世今生,会让你对这个知识有非常强的掌握,而不再只是靠记忆去学习。靠记忆去学习是一件非常糟糕的事。而对于一些操作性的知识(不需要了解由来的),我把其叫操作知识,就像一些函数库一样,这样的知识只要学会查文档就好了。能够知其然,知其所以然的人自然会比识知识到表皮的人段位要高很多。

3)方法套路。学习不是为了找到答案,而是找到方法。就像数学一样,你学的是方法,是解题思路,是套路,会用方程式解题的和不会用方程式解题的在解题效率上不可比较,而在微积分面前,其它的解题方法都变成了渣渣。你可以看到,掌握高级方法的人比别人的优势有多大,学习的目的就是为了掌握更为高级的方法和解题思路

技能

要在技能上超过别人,你就需要在下面几个方面做足功夫:

1)精益求精。如果你想拥有专业的技能,你要做不仅仅是拼命地重复一遍又一遍的训练,而是在每一次重复训练时你都要找到更好的方法,总结经验,让新的一遍能够更好,更漂亮,更有效率,否则,用相同的方法重复,那你只不过在搬砖罢了。

2)让自己犯错。犯错是有利于成长的,这是因为出错会让人反思,反思更好的方法,反思更完美的方案,总结教训,寻求更好更完美的过程,是技能升级的最好的方式。尤其是当你在出错后,被人鄙视,被人嘲笑后,你会有更大的动力提升自己,这样的动力才是进步的源动力。当然,千万不要同一个错误重复地犯!

3)找高手切磋。下过棋,打个球的人都知道,你要想提升自己的技艺,你必需找高手切磋,在和高手切磋的过程中你会感受到高手的技能和方法,有时候你会情不自禁地哇地一下,我靠,还可以这么玩!

领导力

最后一个是领导力,要有领导力或是影响力这个事并不容易,这跟你的野心有多大,好胜心有多强 ,你愿意付出多少很有关系,因为一个人的领导力跟他的标准很有关系,因为有领导力的人的标准比绝大多数人都要高。

1)识别自己的特长和天赋。首先,每个人DNA都可能或多或少都会有一些比大多数人NB的东西(当然,也可能没有),如果你有了,那么在你过去的人生中就一定会表现出来了,就是那种大家遇到这个事会来请教你的寻求你帮助的现象。那种,别人要非常努力,而且毫不费劲的事。一旦你有了这样的特长或天赋,那你就要大力地扩大你的领先优势,千万不要进到那些会限制你优势的地方。你是一条鱼,你就一定要把别人拉到水里来玩,绝对不要去陆地上跟别人拼,不断地在自己的特长和天赋上扩大自己的领先优势,彻底一骑绝尘。

2)识别自己的兴趣和事业。没有天赋也没有问题,还有兴趣点,都说兴趣是最好的老师,当年,Linus就是在学校里对minx着迷了,于是整出个Linux来,这就是兴趣驱动出的东西,一般来说,兴趣驱动的事总是会比那些被动驱动的更好。但是,这里我想说明一下什么叫“真∙兴趣”,真正的兴趣不是那种三天热度的东西,而是那种,你愿意为之付出一辈子的事,是那种无论有多大困难有多难受你都要死磕的事,这才是“真∙兴趣”,这也就是你的“野心”和“好胜心”所在,其实上升到了你的事业。相信我,绝大多数人只有职业而没有事业的。

3)建立高级的习惯和方法。没有天赋没有野心,也还是可以跟别人拼习惯拼方法的,只要你有一些比较好的习惯和方法,那么你一样可以超过大多数人。对此,在习惯上你要做到比较大多数人更自律,更有计划性,更有目标性,比如,每年学习一门新的语言或技术,并可以参与相关的顶级开源项目,每个月训练一个类算法,掌握一种算法,每周阅读一篇英文论文,并把阅读笔记整理出来……自律的是非常可怕的。除此之外,你还需要在方法上超过别人,你需要满世界的找各种高级的方法,其中包括,思考的方法,学习的方法、时间管理的方法、沟通的方法这类软实力的,还有,解决问题的方法(trouble shooting 和 problem solving),设计的方法,工程的方法,代码的方法等等硬实力的,一开始照猫画虎,时间长了就可能会自己发明或推导新的方法。

4)勤奋努力执着坚持。如果上面三件事你都没有也没有能力,那还有最后一件事了,那就是勤奋努力了,就是所谓的“一万小时定律”了(参看《21天教你学会C++》中的十年学编程一节),我见过很多不聪明的人,悟性也不够(比如我就是一个),别人学一个东西,一个月就好了,而我需要1年甚至更长,但是很多东西都是死的,只要肯花时间就有一天你会搞懂的,耐不住我坚持十年二十年,聪明的人发明个飞机飞过去了,笨一点的人愚公移山也过得去,因为更多的人是懒人,我不用拼过聪明人,我只用拼过那些懒人就好了。

好了,就这么多,如果哪天你变得消极和不自信,你要来读读我的这篇文章,子曰:温故而知新。

(全文完)


关注CoolShell微信公众账号和微信小程序

(转载本站文章请注明作者和出处 酷 壳 – CoolShell ,请勿用于任何商业用途)

——=== 访问 酷壳404页面 寻找遗失儿童。 ===——

陈皓 June 22, 2019 at 01:47PM
from 酷 壳 – CoolShell http://bit.ly/2Y5Op5d

VN:F [1.9.22_1171]
Rating: 0.0/5 (0 votes cast)
VN:F [1.9.22_1171]
Rating: 0 (from 0 votes)
Share

【酷壳】 HTTP API 认证授权术

我们知道,HTTP是无状态的,所以,当我们需要获得用户是否在登录的状态时,我们需要检查用户的登录状态,一般来说,用户的登录成功后,服务器会发一个登录凭证(又被叫作Token),就像你去访问某个公司,在前台被认证过合法后,这个公司的前台会给你的一个访客卡一样,之后,你在这个公司内去到哪都用这个访客卡来开门,而不再校验你是哪一个人。在计算机的世界里,这个登录凭证的相关数据会放在两种地方,一个地方在用户端,以Cookie的方式(一般不会放在浏览器的Local Storage,因为这很容易出现登录凭证被XSS攻击),另一个地方是放在服务器端,又叫Session的方式(SessonID存于Cookie)。

但是,这个世界还是比较复杂的,除了用户访问,还有用户委托的第三方的应用,还有企业和企业间的调用,这里,我想把业内常用的一些 API认证技术相对系统地总结归纳一下,这样可以让大家更为全面的了解这些技术。注意,这是一篇长文!

本篇文章会覆盖如下技术:

  • HTTP Basic
  • Digest Access
  • App Secret Key + HMAC
  • JWT – JSON Web Tokens
  • OAuth 1.0 – 3 legged & 2 legged
  • OAuth 2.0 – Authentication Code & Client Credential

HTTP Basic

HTTP Basic 是一个非常传统的API认证技术,也是一个比较简单的技术。这个技术也就是使用 usernamepassword 来进行登录。整个过程被定义在了 RFC 2617 中,也被描述在了 Wikipedia: Basic Access Authentication 词条中,同时也可以参看 MDN HTTP Authentication

其技术原理如下:

  1. usernamepassword 做成  username:password 的样子(用冒号分隔)
  2. 进行Base64编码。Base64("username:password") 得到一个字符串(如:把 haoel:coolshell 进行base64 后可以得到 aGFvZW86Y29vbHNoZWxsCg
  3. aGFvZW86Y29vbHNoZWxsCg放到HTTP头中 Authorization 字段中,形成 Authorization: Basic aGFvZW86Y29vbHNoZWxsCg,然后发送到服务端。
  4. 服务端如果没有在头里看到认证字段,则返回401错,以及一个个WWW-Authenticate: Basic Realm='HelloWorld' 之类的头要求客户端进行认证。之后如果没有认证通过,则返回一个401错。如果服务端认证通过,那么会返回200。

我们可以看到,使用Base64的目的无非就是为了把一些特殊的字符给搞掉,这样就可以放在HTTP协议里传输了。而这种方式的问题最大的问题就是把用户名和口令放在网络上传,所以,一般要配合TLS/SSL的安全加密方式来使用。我们可以看到 JIRA Cloud 的API认证支持HTTP Basic 这样的方式。

但我们还是要知道,这种把用户名和密码同时放在公网上传输的方式有点不太好,因为Base64不是加密协议,保是编码协议,所以就算是有HTTPS作为安全保护,给人的感觉还是不放心。

Digest Access

中文称“HTTP 摘要认证”,最初被定义在了 RFC 2069 文档中(后来被 RFC 2617 引入了一系列安全增强的选项;“保护质量”(qop)、随机数计数器由客户端增加、以及客户生成的随机数)。

其基本思路是,请求方把用户名口令和域做一个MD5 –  MD5(username:realm:password) 然后传给服务器,这样就不会在网上传用户名和口令了,但是,因为用户名和口令基本不会变,所以,这个MD5的字符串也是比较固定的,因此,这个认证过程在其中加入了两个事,一个是 nonce 另一个是 qop

  • 首先,调用方发起一个普通的HTTP请求。比如:GET /coolshell/admin/ HTTP/1.1
  • 服务端自然不能认证能过,服务端返回401错误,并且在HTTP头里的 WWW-Authenticate 包含如下信息:
 WWW-Authenticate: Digest realm="testrealm@host.com",
                        qop="auth,auth-int",
                        nonce="dcd98b7102dd2f0e8b11d0f600bfb0c093",
                        opaque="5ccc069c403ebaf9f0171e9517f40e41"
  • 其中的 nonce 为服务器端生成的随机数,然后,客户端做 HASH1=MD5(MD5(username:realm:password):nonce:cnonce) ,其中的 cnonce 为客户端生成的随机数,这样就可以使得整个MD5的结果是不一样的。
  • 如果 qop 中包含了 auth ,那么还得做  HASH2=MD5(method:digestURI) 其中的 method 就是HTTP的请求方法(GET/POST…),digestURI 是请求的URL。
  • 如果 qop 中包含了 auth-init ,那么,得做  HASH2=MD5(method:digestURI:MD5(entityBody)) 其中的 entityBody 就是HTTP请求的整个数据体。
  • 然后,得到 response = MD5(HASH1:nonce:nonceCount:cnonce:qop:HASH2) 如果没有 qopresponse = MD5(HA1:nonce:HA2)
  • 最后,我们的客户端对服务端发起如下请求—— 注意HTTP头的 Authorization: Digest ...
GET /dir/index.html HTTP/1.0
Host: localhost
Authorization: Digest username="Mufasa",
                     realm="testrealm@host.com",
                     nonce="dcd98b7102dd2f0e8b11d0f600bfb0c093",
                     uri="%2Fcoolshell%2Fadmin",
                     qop=auth,
                     nc=00000001,
                     cnonce="0a4f113b",
                     response="6629fae49393a05397450978507c4ef1",
                     opaque="5ccc069c403ebaf9f0171e9517f40e41"

维基百科上的 Wikipedia: Digest access authentication 词条非常详细地描述了这个细节。

摘要认证这个方式会比之前的方式要好一些,因为没有在网上传递用户的密码,而只是把密码的MD5传送过去,相对会比较安全,而且,其并不需要是否TLS/SSL的安全链接。但是,别看这个算法这么复杂,最后你可以发现,整个过程其实关键是用户的password,这个password如果不够得杂,其实是可以被暴力破解的,而且,整个过程是非常容易受到中间人攻击——比如一个中间人告诉客户端需要一个

App Secret Key + HMAC

先说HMAC技术,这个东西来自于MAC – Message Authentication Code,是一种用于给消息签名的技术,也就是说,我们怕消息在传递的过程中被人修改,所以,我们需要用对消息进行一个MAC算法,得到一个摘要字串,然后,接收方得到消息后,进行同样的计算,然后比较这个MAC字符串,如果一致,则表明没有被修改过(整个过程参看下图)。而HMAC – Hash-based Authenticsation Code,指的是利用Hash技术完成这一工作,比如:SHA-256算法。

 

(图片来自 Wikipedia – MAC 词条

我们再来说App ID,这个东西跟验证没有关系,只是用来区分,是谁来调用API的,就像我们每个人的身份证一样,只是用来标注不同的人,不是用来做身份认证的。与前面的不同之处是,这里,我们需要用App ID 来映射一个用于加密的密钥,这样一来,我们就可以在服务器端进行相关的管理,我们可以生成若干个密钥对(AppID, AppSecret),并可以有更细粒度的操作权限管理。

把AppID和HMAC用于API认证,目前来说,玩得最好最专业的应该是AWS了,我们可以通过S3的API请求签名文档看到AWS是怎么玩的。整个过程还是非常复杂的,可以通过下面的图片流程看个大概。基本上来说,分成如下几个步骤:

  1. 把HTTP的请求(方法、URI、查询字串、头、签名头,body)打个包叫 CanonicalRequest,作个SHA-256的签名,然后再做一个base16的编码
  2. 把上面的这个签名和签名算法 AWS4-HMAC-SHA256、时间戳、Scop,再打一个包,叫 StringToSign
  3. 准备签名,用 AWSSecretAccessKey来对日期签一个 DataKey,再用 DataKey 对要操作的Region签一个 DataRegionKey ,再对相关的服务签一个DataRegionServiceKey ,最后得到 SigningKey.
  4. 用第三步的 SigningKey来对第二步的 StringToSign 签名。

 

最后,发出HTTP Request时,在HTTP头的 Authorization字段中放入如下的信息:

Authorization: AWS4-HMAC-SHA256 
               Credential=AKIDEXAMPLE/20150830/us-east-1/iam/aws4_request, 
               SignedHeaders=content-type;host;x-amz-date, 
               Signature=5d672d79c15b13162d9279b0855cfba6789a8edb4c82c400e06b5924a6f2b5d7

 

其中的  AKIDEXAMPLE 是 AWS Access Key ID, 也就是所谓的 AppID,服务器端会根据这个AppID来查相关的 Secret Access Key,然后再验证签名。如果,你对这个过程有点没看懂的话,你可以读一读这篇文章——《Amazon S3 Rest API with curl》这篇文章里有好些代码,代码应该是最有细节也是最准确的了。

这种认证的方式好处在于,AppID和AppSecretKey,是由服务器的系统开出的,所以,是可以被管理的,AWS的IAM就是相关的管理,其管理了用户、权限和其对应的AppID和AppSecretKey。但是不好的地方在于,这个东西没有标准 ,所以,各家的实现很不一致。比如: Acquia 的 HMAC微信的签名算法 (这里,我们需要说明一下,微信的API没有遵循HTTP协议的标准,把认证信息放在HTTP 头的 Authorization 里,而是放在body里)

JWT – JSON Web Tokens

JWT是一个比较标准的认证解决方案,这个技术在Java圈里应该用的是非常普遍的。JWT签名也是一种MAC(Message Authentication Code)的方法。JWT的签名流程一般是下面这个样子:

  1. 用户使用用户名和口令到认证服务器上请求认证。
  2. 认证服务器验证用户名和口令后,以服务器端生成JWT Token,这个token的生成过程如下:
    • 认证服务器还会生成一个 Secret Key(密钥)
    • 对JWT Header和 JWT Payload分别求Base64。在Payload可能包括了用户的抽象ID和的过期时间。
    • 用密钥对JWT签名 HMAC-SHA256(SecertKey, Base64UrlEncode(JWT-Header)+'.'+Base64UrlEncode(JWT-Payload));
  3. 然后把 base64(header).base64(payload).signature 作为 JWT token返回客户端。
  4. 客户端使用JWT Token向应用服务器发送相关的请求。这个JWT Token就像一个临时用户权证一样。

当应用服务器收到请求后:

  1. 应用服务会检查 JWT  Token,确认签名是正确的。
  2. 然而,因为只有认证服务器有这个用户的Secret Key(密钥),所以,应用服务器得把JWT Token传给认证服务器。
  3. 认证服务器通过JWT Payload 解出用户的抽象ID,然后通过抽象ID查到登录时生成的Secret Key,然后再来检查一下签名。
  4. 认证服务器检查通过后,应用服务就可以认为这是合法请求了。

我们可以看以,上面的这个过程,是在认证服务器上为用户动态生成 Secret Key的,应用服务在验签的时候,需要到认证服务器上去签,这个过程增加了一些网络调用,所以,JWT除了支持HMAC-SHA256的算法外,还支持RSA的非对称加密的算法。

使用RSA非对称算法,在认证服务器这边放一个私钥,在应用服务器那边放一个公钥,认证服务器使用私钥加密,应用服务器使用公钥解密,这样一来,就不需要应用服务器向认证服务器请求了,但是,RSA是一个很慢的算法,所以,虽然你省了网络调用,但是却费了CPU,尤其是Header和Payload比较长的时候。所以,一种比较好的玩法是,如果我们把header 和 payload简单地做SHA256,这会很快,然后,我们用RSA加密这个SHA256出来的字符串,这样一来,RSA算法就比较快了,而我们也做到了使用RSA签名的目的。

最后,我们只需要使用一个机制在认证服务器和应用服务器之间定期地换一下公钥私钥对就好了。

这里强烈建议全文阅读 Anglar 大学的 《JSW:The Complete Guide to JSON Web Tokens

OAuth 1.0

OAuth也是一个API认证的协议,这个协议最初在2006年由Twitter的工程师在开发OpenID实现的时候和社交书签网站Ma.gnolia时发现,没有一种好的委托授权协议,后来在2007年成立了一个OAuth小组,知道这个消息后,Google员工也加入进来,并完善有善了这个协议,在2007年底发布草案,过一年后,在2008年将OAuth放进了IETF作进一步的标准化工作,最后在2010年4月,正式发布OAuth 1.0,即:RFC 5849 (这个RFC比起TCP的那些来说读起来还是很轻松的),不过,如果你想了解其前身的草案,可以读一下 OAuth Core 1.0 Revision A ,我在下面做个大概的描述。

根据RFC 5849,可以看到 OAuth 的出现,目的是为了,用户为了想使用一个第三方的网络打印服务来打印他在某网站上的照片,但是,用户不想把自己的用户名和口令交给那个第三方的网络打印服务,但又想让那个第三方的网络打印服务来访问自己的照片,为了解决这个授权的问题,OAuth这个协议就出来了。

  • 这个协议有三个角色:
    • User(照片所有者-用户)
    • Consumer(第三方照片打印服务)
    • Service Provider(照片存储服务)
  • 这个协义有三个阶段:
    • Consumer获取Request Token
    • Service Provider 认证用户并授权Consumer
    • Consumer获取Access Token调用API访问用户的照片

整个授权过程是这样的:

  1. Consumer(第三方照片打印服务)需要先上Service Provider获得开发的 Consumer Key 和 Consumer Secret
  2. 当 User 访问 Consumer 时,Consumer 向 Service Provide 发起请求请求Request Token (需要对HTTP请求签名)
  3. Service Provide 验明 Consumer 是注册过的第三方服务商后,返回 Request Token(oauth_token)和 Request Token Secret (oauth_token_secret
  4. Consumer 收到 Request Token 后,使用HTTP GET 请求把 User 切到 Service Provide 的认证页上(其中带上Request Token),让用户输入他的用户和口令。
  5. Service Provider 认证 User 成功后,跳回 Consumer,并返回 Request Token (oauth_token)和 Verification Code(oauth_verifier
  6. 接下来就是签名请求,用Request Token 和 Verification Code 换取 Access Token (oauth_token)和 Access Token Secret (oauth_token_secret)
  7. 最后使用Access Token 访问用户授权访问的资源。

下图附上一个Yahoo!的流程图可以看到整个过程的相关细节。

因为上面这个流程有三方:User,Consumer 和 Service Provide,所以,又叫 3-legged flow,三脚流程。OAuth 1.0 也有不需要用户参与的,只有Consumer 和 Service Provider 的, 也就是 2-legged flow 两脚流程,其中省掉了用户认证的事。整个过程如下所示:

  1. Consumer(第三方照片打印服务)需要先上Service Provider获得开发的 Consumer Key 和 Consumer Secret
  2. Consumer 向 Service Provide 发起请求请求Request Token (需要对HTTP请求签名)
  3. Service Provide 验明 Consumer 是注册过的第三方服务商后,返回 Request Token(oauth_token)和 Request Token Secret (oauth_token_secret
  4. Consumer 收到 Request Token 后,直接换取 Access Token (oauth_token)和 Access Token Secret (oauth_token_secret)
  5. 最后使用Access Token 访问用户授权访问的资源。

最后,再来说一说OAuth中的签名。

  • 我们可以看到,有两个密钥,一个是Consumer注册Service Provider时由Provider颁发的 Consumer Secret,另一个是 Token Secret。
  • 签名密钥就是由这两具密钥拼接而成的,其中用 &作连接符。假设 Consumer Secret 为 j49sk3j29djd 而 Token Secret 为dh893hdasih9那个,签名密钥为:j49sk3j29djd&dh893hdasih9
  • 在请求Request/Access Token的时候需要对整个HTTP请求进行签名(使用HMAC-SHA1和HMAC-RSA1签名算法),请求头中需要包括一些OAuth需要的字段,如:
    • Consumer Key : 也就是所谓的AppID
    • Token: Request Token 或 Access Token
    • Signature Method :签名算法比如:HMAC-SHA1
    • Timestamp:过期时间
    • Nonce:随机字符串
    • Call Back:回调URL

下图是整个签名的示意图:

图片还是比较直观的,我就不多解释了。

OAuth 2.0

在前面,我们可以看到,从Digest Access, 到AppID+HMAC,再到JWT,再到OAuth 1.0,这些个API认证都是要向Client发一个密钥(或是用密码)然后用HASH或是RSA来签HTTP的请求,这其中有个主要的原因是,以前的HTTP是明文传输,所以,在传输过程中很容易被篡改,于是才搞出来一套的安全签名机制,所以,这些个认证的玩法是可以在HTTP明文协议下玩的。

这种使用签名方式大家可以看到是比较复杂的,所以,对于开发者来说,也是很不友好的,在组织签名的那些HTTP报文的时候,各种,URLEncode和Base64,还要对Query的参数进行排序,然后有的方法还要层层签名,非常容易出错,另外,这种认证的安全粒度比较粗,授权也比较单一,对于有终端用户参与的移动端来说也有点不够。所以,在2012年的时候,OAuth 2.0 的 RFC 6749 正式放出。

OAuth 2.0依赖于TLS/SSL的链路加密技术(HTTPS),完全放弃了签名的方式,认证服务器再也不返回什么 token secret 的密钥了,所以,OAuth 2.0是完全不同于1.0 的,也是不兼容的。目前,Facebook 的 Graph API 只支持OAuth 2.0协议,Google 和 Microsoft Azure 也支持Auth 2.0,国内的微信和支付宝也支持使用OAuth 2.0。

下面,我们来重点看一下OAuth 2.0的两个主要的Flow:

  • 一个是Authorization Code Flow, 这个是 3 legged 的
  • 一个是Client Credential Flow,这个是 2 legged 的。
Authorization Code Flow

Authorization Code 是最常使用的OAuth 2.0的授权许可类型,它适用于用户给第三方应用授权访问自己信息的场景。这个Flow也是OAuth 2.0四个Flow中我个人觉得最完整的一个Flow,其流程图如下所示。

 

下面是对这个流程的一个细节上的解释:

1)当用户(Resource Owner)访问第三方应用(Client)的时候,第三方应用会把用户带到认证服务器(Authorization Server)上去,主要请求的是 /authorize API,其中的请求方式如下所示。

https://login.authorization-server.com/authorize?
        client_id=6731de76-14a6-49ae-97bc-6eba6914391e
        &response_type=code
        &redirect_uri=http%3A%2F%2Fexample-client.com%2Fcallback%2F
        &scope=read
        &state=xcoiv98CoolShell3kch

其中:

    • client_id为第三方应用的App ID
    • response_type=code为告诉认证服务器,我要走Authorization Code Flow。
    • redirect_uri意思是我跳转回第三方应用的URL
    • scope意是相关的权限
    • state 是一个随机的字符串,主要用于防CSRF攻击。

2)当Authorization Server收到这个URL请求后,其会通过 client_id来检查 redirect_uriscope是否合法,如果合法,则弹出一个页面,让用户授权(如果用户没有登录,则先让用户登录,登录完成后,出现授权访问页面)。

3)当用户授权同意访问以后,Authorization Server 会跳转回 Client ,并以其中加入一个 Authorization Code。 如下所示:

https://example-client.com/callback?
        code=Yzk5ZDczMzRlNDEwYlrEqdFSBzjqfTG
        &state=xcoiv98CoolShell3kch

我们可以看到,

    • 请流动的链接是第 1)步中的 redirect_uri
    • 其中的 state 的值也和第 1)步的 state一样。

4)接下来,Client 就可以使用 Authorization Code 获得 Access Token。其需要向 Authorization Server 发出如下请求。

POST /oauth/token HTTP/1.1
Host: authorization-server.com
 
code=Yzk5ZDczMzRlNDEwYlrEqdFSBzjqfTG
&grant_type=code
&redirect_uri=https%3A%2F%2Fexample-client.com%2Fcallback%2F
&client_id=6731de76-14a6-49ae-97bc-6eba6914391e
&client_secret=JqQX2PNo9bpM0uEihUPzyrh

5)如果没什么问题,Authorization 会返回如下信息。

{
  "access_token": "iJKV1QiLCJhbGciOiJSUzI1NiI",
  "refresh_token": "1KaPlrEqdFSBzjqfTGAMxZGU",
  "token_type": "bearer",
  "expires": 3600,
  "id_token": "eyJ0eXAiOiJKV1QiLCJhbGciO.eyJhdWQiOiIyZDRkM..."
}

其中,

    • access_token就是访问请求令牌了
    • refresh_token用于刷新 access_token
    • id_token 是JWT的token,其中一般会包含用户的OpenID

6)接下来就是用 Access Token 请求用户的资源了。

GET /v1/user/pictures
Host: https://example.resource.com

Authorization: Bearer iJKV1QiLCJhbGciOiJSUzI1NiI

 

 Client Credential Flow

Client Credential 是一个简化版的API认证,主要是用于认证服务器到服务器的调用,也就是没有用户参与的的认证流程。下面是相关的流程图。

这个过程非常简单,本质上就是Client用自己的 client_idclient_secret向Authorization Server 要一个 Access Token,然后使用Access Token访问相关的资源。

请求示例

POST /token HTTP/1.1
Host: server.example.com
Content-Type: application/x-www-form-urlencoded

grant_type=client_credentials
&client_id=czZCaGRSa3F0Mzpn
&client_secret=7Fjfp0ZBr1KtDRbnfVdmIw

返回示例

{
  "access_token":"MTQ0NjJkZmQ5OTM2NDE1ZTZjNGZmZjI3",
  "token_type":"bearer",
  "expires_in":3600,
  "refresh_token":"IwOGYzYTlmM2YxOTQ5MGE3YmNmMDFkNTVk",
  "scope":"create"
}

这里,容我多扯一句,微信公从平台的开发文档中,使用了OAuth 2.0 的 Client Credentials的方式(参看文档“微信公众号获取access token”),我截了个图如下所谓。我们可以看到,微信公众号使用的是GET方式的请求,把AppID和AppSecret放在了URL中,虽然这也符合OAuth 2.0,但是并不好,因为大多数网关代理会把整个URI请求记到日志中。我们只要脑补一下腾讯的网关的Access Log,里面的日志一定会有很多的各个用户的AppID和AppSecret……

 

小结

讲了这么多,我们来小结一下(下面的小结可能会有点散)

两个概念和三个术语
  • 区分两个概念:Authentication(认证) 和 Authorization (授权),前者是证明请求者是身份,就像身份证一样,后者是为了获得权限。身份是区别于别人的证明,而权限是证明自己的特权。Authentication为了证明操作的这个人就是他本人,需要提供密码、短信验证码,甚至人脸识别。Authorization 则是不需要在所有的请求都需要验人,是在经过Authorization后得到一个Token,这就是Authorization。就像护照和签证一样。
  • 区分三个概念:编码Base64Encode、签名HMAC、加密RSA。编码是为了更的传输,等同于明文,签名是为了信息不能被篡改,加密是为了不让别人看到是什么信息。
明白一些初衷
  • 使用复杂地HMAC哈希签名方式主要是应对当年没有TLS/SSL加密链路的情况。
  • JWT把 uid 放在 Token中目的是为了去掉状态,但不能让用户修改,所以需要签名。
  • OAuth 1.0区分了两个事,一个是第三方的Client,一个是真正的用户,其先拿Request Token,再换Access Token的方法主要是为了把第三方应用和用户区分开来。
  • 用户的Password是用户自己设置的,复杂度不可控,服务端颁发的Serect会很复杂,但主要目的是为了容易管理,可以随时注销掉。
  • OAuth 协议有比所有认证协议有更为灵活完善的配置,如果使用AppID/AppSecret签名的方式,又需要做到可以有不同的权限和可以随时注销,那么你得开发一个像AWS的IAM这样的账号和密钥对管理的系统。
相关的注意事项
  • 无论是哪种方式,我们都应该遵循HTTP的规范,把认证信息放在 Authorization HTTP 头中。
  • 不要使用GET的方式在URL中放入secret之类的东西,因为很多proxy或gateway的软件会把整个URL记在Access Log文件中。
  • 密钥Secret相当于Password,但他是用来加密的,最好不要在网络上传输,如果要传输,最好使用TLS/SSL的安全链路。
  • HMAC中无论是MD5还是SHA1/SHA2,其计算都是非常快的,RSA的非对称加密是比较耗CPU的,尤其是要加密的字符串很长的时候。
  • 最好不要在程序中hard code 你的 Secret,因为在github上有很多黑客的软件在监视各种Secret,千万小心!这类的东西应该放在你的配置系统或是部署系统中,在程序启动时设置在配置文件或是环境变量中。
  • 使用AppID/AppSecret,还是使用OAuth1.0a,还是OAuth2.0,还是使用JWT,我个人建议使用TLS/SSL下的OAuth 2.0。
  • 密钥是需要被管理的,管理就是可以新增可以撤销,可以设置账户和相关的权限。最好密钥是可以被自动更换的。
  • 认证授权服务器(Authorization Server)和应用服务器(App Server)最好分开。

(全文完)


关注CoolShell微信公众账号和微信小程序

(转载本站文章请注明作者和出处 酷 壳 – CoolShell ,请勿用于任何商业用途)

——=== 访问 酷壳404页面 寻找遗失儿童。 ===——

陈皓 May 09, 2019 at 09:37PM
from 酷 壳 – CoolShell http://bit.ly/2JdiUCG

VN:F [1.9.22_1171]
Rating: 0.0/5 (0 votes cast)
VN:F [1.9.22_1171]
Rating: 0 (from 0 votes)
Share

【酷壳】 打造高效的工作环境 – Shell 篇

注:本文由雷俊(Javaer/Emacser)和我一起编辑,所以文章版权归雷俊与我共同所有,转载者必需注明出处和我们两位作者。原文最早发于酷壳微信公众号,后来我又做了一些修改,再发到博客这边。

程序员是一个很懒的群体,总想着能够让代码为自己干活,他们不断地把工作生活中的一些事情用代码自动化了,从而让整个社会的效率运作地越来越高。所以,程序员在准备去优化这个世界的时候,都会先要优化自己的工作环境,是所谓“工欲善其事,必先利其器”。

我们每个程序员都应该打造一套让自己更为高效的工作环境。那怕就是让你少输入一次命令,少按一次键,少在鼠标和键盘间切换一次,都会让程序员的工作变得更为的高效。所以,程序员一般需要一台性能比较好,不会因为开了太多的网页或程序就卡得不行的电脑,还要配备多个显示器,一个显示器写代码,一个查文档,一个测试运行结果,而不必在各种窗口来来回回的切换……在大量的窗口间切换经常会迷路,而且也容易出错(分不清线上或测试环境)……

除了硬件上的装备,软件上也是能够提升程序员生产力的地方,在软件层面提升程序员生产力的东西有一个很重要的事就是命令行和脚本,使用鼠标和图形界面则会大大降低程序员的生产力。酷壳以前也写过一些,如《你可能不知道的Shell》和《 应该知道的Linux技巧》,但是Unix/Linux Shell就是一个大宝库,怎么写也写不完,不然,怎么会有“Where is the Shell, there is a way”。

命令行

在不同的操作系统下,都有着很不错的命令行工具,比如 Mac 下的 Iterm2,Linux 下的原生命令行,如果你是在 Windows 下工作,问题也不大,因为 Windows 下现在有了 WSL。WSL 提供了一个由微软开发的Linux兼容的内核接口(不包含Linux内核代码),然后可以在其上运行GNU用户空间,例如 Ubuntu,openSUSE,SUSE Linux Enterprise Server,Debian和Kali Linux。这样的用户空间可能包含 Bash shell 和命令语言,使用本机 GNU/Linux 命令行工具(sed,awk 等),编程语言解释器(Ruby,Python 等),甚至是图形应用程序(使用主机端的X窗口系统)。

使用命令行可以完成所有日常的操作,新建文件夹(mkdir)、新建文件(touch)、移动(mv)、复制(cp)、删除(rm)等等。而且使用 Linux/Unix 命令行最好的方式是可以用 awksedgrepxargsfindsort 等等这样的命令,然后用管道把其串起来,就可以完成一个你想要的功能,尤其是一些简单的数据统计功能。这是Linux命令行不可比拟的优势。比如:

  • 查看连接你服务器 top10 用户端的 IP 地址:

netstat -nat | awk '{print $5}' | awk -F ':' '{print $1}' | sort | uniq -c | sort -rn | head -n 10

  • 查看一下你最常用的10个命令:

cat .bash_history | sort | uniq -c | sort -rn | head -n 10 (or cat .zhistory | sort | uniq -c | sort -rn | head -n 10

(注:awk 和 sed 是两大神器,所以,我以前的也有两篇文章来介绍它们——《awk简明教程》和《sed简明教程》,你可以前往一读)

在命令行中使用 alias 可以将使用频率很高命令或者比较复杂的命令合并成一个命令,或者修改原生的命令。

下面这几个命令,可能是你天天都在敲的。所以,你应该设置成 alias 来提高效率

alias nis="npm install --save "
alias svim='sudo vim'
alias mkcd='foo(){ mkdir -p "$1"; cd "$1" }; foo '
alias install='sudo apt get install'
alias update='sudo apt-get update; sudo apt-get upgrade'
alias ..="cd .."
alias ...="cd ..; cd .."
alias www='python -m SimpleHTTPServer 8000'
alias sock5='ssh -D 8080 -q -C -N -f user@your.server'

你还可以参考如下的一些文章,看看别人是怎么用好 alias 的

命令行中除了原生的命令之外,还有很多可以提升使用体验的工具。下面罗列一些很不错的命令,把原生的命令增强地很厉害:

  • fasd 增强了 cd 命令 。
  • bat 增强了 cat 命令 。如果你想要有语法高亮的 cat,可以试试 ccat 命令。
  • exa 增强了 ls 命令,如果你需要在很多目录上浏览各种文件 ,ranger 命令可以比 cd 和 cat 更有效率,甚至可以在你的终端预览图片。
  • fd 是一个比 find 更简单更快的命令,他还会自动地忽略掉一些你配置在 .gitignore 中的文件,以及 .git 下的文件。
  • fzf 会是一个很好用的文件搜索神器,其主要是搜索当前目录以下的文件,还可以使用 fzf --preview 'cat {}'边搜索文件边浏览内容。
  • grep 是一个上古神器,然而,ackag 和 rg 是更好的grep,和上面的 fd一样,在递归目录匹配的时候,会使用你配置在 .gitignore 中的规则。
  • rm 是一个危险的命令,尤其是各种 rm -rf …,所以,trash 是一个更好的删除命令。
  • man 命令是好读文档的命令,但是man的文档有时候太长了,所以,你可以试试 tldr 命令,把文档上的一些示例整出来给你看。
  • 如果你想要一个图示化的ping,你可以试试 prettyping 。
  • 如果你想搜索以前打过的命令,不要再用 Ctrl +R 了,你可以使用加强版的 hstr  。
  • htop  是 top 的一个加强版。然而,还有很多的各式各样的top,比如:用于看IO负载的 iotop,网络负载的 iftop, 以及把这些top都集成在一起的 atop
  • ncdu  比 du 好用多了用。另一个选择是 nnn
  • 如果你想把你的命令行操作建录制成一个 SVG 动图,那么你可以尝试使用 asciinema 和 svg-trem 。
  • httpie 是一个可以用来替代 curlwget 的 http 客户端,httpie 支持 json 和语法高亮,可以使用简单的语法进行 http 访问: http -v github.com
  • tmux 在需要经常登录远程服务器工作的时候会很有用,可以保持远程登录的会话,还可以在一个窗口中查看多个 shell 的状态。
  • Taskbook 是可以完全在命令行中使用的任务管理器 ,支持 ToDo 管理,还可以为每个任务加上优先级。
  • sshrc 是个神器,在你登录远程服务器的时候也能使用本机的 shell 的 rc 文件中的配置。
  • goaccess  这个是一个轻量级的分析统计日志文件的工具,主要是分析各种各样的 access log。

关于这些增加命令,主要是参考自下面的这些文章

  1. 10 Tools To Power Up Your Command Line
  2. 5 More Tools To Power Up Your Command Line (Part 2 Of Series)
  3. Power Up Your Command Line, Part 3
  4. Power Up Your Command Line
  5. Hacker Tools

Shell 和脚本

shell 是可以与计算机进行高效交互的文本接口。shell 提供了一套交互式的编程语言(脚本),shell的种类很多,比如 shbashzsh 等。

shell 的生命力很强,在各种高级编程语言大行其道的今天,很多的任务依然离不开 shell。比如可以使用 shell 来执行一些编译任务,或者做一些批处理任务,初始化数据、打包程序等等。

现在比较流行的是 zsh + oh-my-zsh + zsh-autosuggestions 的组合,你也可以试试看。其中 zsh 和 oh-my-zsh 算是常规操作了,但是 zsh-autosuggestions 特别有用,可以超级快速的帮你补全你输入过的命令,让命令行的操作更加高效。

另外,fish 也是另外一个牛逼的shell,比如:命令行自动完成(根据历史记录),命令行命令高亮,当你要输入命令行参数的时候,自动提示有哪些参数…… fish在很多地方也是用起来很爽的。和上面的 oh-my-zsh 有点不分伯仲了。

你也许会说,用 Python 脚本或 PHP 来写脚本会比 Shell 更好更没有 bug,但我要申辩一下:

  • 其一,如果你有一天要维护线上机器的时候,或是到了银行用户的系统(与外网完全隔离,而且服务器上没有安装 Python/PHP 或是他们的的高级库,那么,你只有 Shell 可以用了)。
  • 其二,而且,如果要跟命令行交互很多的话,Shell 是不二之选,试想一下,如果你要去 100 台远程的机器上查access.log 日志中有没有某个错误,完成这个工作你是用 PHP/Python 写脚本快还是用 Shell 写脚本快呢?

所以,我们还要学会只使用传统的grep/awk/sed等等这些POSIX的原生的系统默认安装的命令

当然,要写好一个脚本并不容易,下面有一些小模板供你参考:

处理命令行参数的一个样例

while [ "$1" != "" ]; do
    case $1 in
        -s  )   shift	
		SERVER=$1 ;;  
        -d  )   shift
		DATE=$1 ;;
	--paramter|p ) shift
		PARAMETER=$1;;
        -h|help  )   usage # function call
                exit ;;
        * )     usage # All other parameters
                exit 1
    esac
    shift
done 

命令行菜单的一个样例

#!/bin/bash
# Bash Menu Script Example

PS3='Please enter your choice: '
options=("Option 1" "Option 2" "Option 3" "Quit")
select opt in "${options[@]}"
do
    case $opt in
        "Option 1")
            echo "you chose choice 1"
            ;;
        "Option 2")
            echo "you chose choice 2"
            ;;
        "Option 3")
            echo "you chose choice $REPLY which is $opt"
            ;;
        "Quit")
            break
            ;;
        *) echo "invalid option $REPLY";;
    esac
done

颜色定义,你可以使用 echo -e "${Blu}blue ${Red}red ${RCol}etc...." 进行有颜色文本的输出

RCol='\e[0m'    # Text Reset

# Regular           Bold                Underline           High Intensity      BoldHigh Intens     Background          High Intensity Backgrounds
Bla='\e[0;30m';     BBla='\e[1;30m';    UBla='\e[4;30m';    IBla='\e[0;90m';    BIBla='\e[1;90m';   On_Bla='\e[40m';    On_IBla='\e[0;100m';
Red='\e[0;31m';     BRed='\e[1;31m';    URed='\e[4;31m';    IRed='\e[0;91m';    BIRed='\e[1;91m';   On_Red='\e[41m';    On_IRed='\e[0;101m';
Gre='\e[0;32m';     BGre='\e[1;32m';    UGre='\e[4;32m';    IGre='\e[0;92m';    BIGre='\e[1;92m';   On_Gre='\e[42m';    On_IGre='\e[0;102m';
Yel='\e[0;33m';     BYel='\e[1;33m';    UYel='\e[4;33m';    IYel='\e[0;93m';    BIYel='\e[1;93m';   On_Yel='\e[43m';    On_IYel='\e[0;103m';
Blu='\e[0;34m';     BBlu='\e[1;34m';    UBlu='\e[4;34m';    IBlu='\e[0;94m';    BIBlu='\e[1;94m';   On_Blu='\e[44m';    On_IBlu='\e[0;104m';
Pur='\e[0;35m';     BPur='\e[1;35m';    UPur='\e[4;35m';    IPur='\e[0;95m';    BIPur='\e[1;95m';   On_Pur='\e[45m';    On_IPur='\e[0;105m';
Cya='\e[0;36m';     BCya='\e[1;36m';    UCya='\e[4;36m';    ICya='\e[0;96m';    BICya='\e[1;96m';   On_Cya='\e[46m';    On_ICya='\e[0;106m';
Whi='\e[0;37m';     BWhi='\e[1;37m';    UWhi='\e[4;37m';    IWhi='\e[0;97m';    BIWhi='\e[1;97m';   On_Whi='\e[47m';    On_IWhi='\e[0;107m';

取当前运行脚本绝对路径的示例:(注:Linux下可以用 dirname $(readlink -f $0) )

FILE="$0"
while [[ -h ${FILE} ]]; do
    FILE="`readlink "${FILE}"`"
done
pushd "`dirname "${FILE}"`" &gt; /dev/null
DIR=`pwd -P`
popd &gt; /dev/null

如何在远程服务器运行一个本地脚本

#无参数
ssh user@server 'bash -s' &lt; local.script.sh

#有参数
ssh user@server ARG1="arg1" ARG2="arg2" 'bash -s' &lt; local_script.sh

如何检查一个命令是否存在,用 which 吗?最好不要用,因为很多操作系统的 which 命令没有设置退出状态码,这样你不知道是否是有那个命令。所以,你应该使用下面的方式。

# POSIX 兼容:
command -v &lt;the_command&gt;


# bash 环境:
hash &lt;the_command&gt; 
type &lt;the_command&gt;

# 示例:
gnudate() {
    if hash gdate 2&gt; /dev/null; then
        gdate "$@"
    else
        date "$@"
    fi
}

然后,如果要写出健壮性更好的脚本,下面是一些相关的技巧:

  • 使用 -e 参数,如:set -e 或是 #!/bin/sh -e,这样设置会让你的脚本出错就会停止运行,这样一来可以防止你的脚本在出错的情况下还在拼拿地干活停不下来。
  • 使用 -u 参数,如: set -eu,这意味着,如果你代码中有变量没有定义,就会退出。
  • 对一些变理,你可以使用默认值。如:${FOO:-'default'}
  • 处理你代码的退出码。这样方便你的脚本跟别的命令行或脚本集成。
  • 尽量不要使用 ; 来执行多个命令,而是使用 &&,这样会在出错的时候停止运行后续的命令。
  • 对于一些字符串变量,使用引号括起,避免其中有空格或是别的什么诡异字符。
  • 如果你的脚有参数,你需要检查脚本运行是否带了你想要的参数,或是,你的脚本可以在没有参数的情况下安全的运行。
  • 为你的脚本设置 -h 和 --help 来显示帮助信息。千万不要把这两个参数用做为的功能。
  • 使用 $() 而不是 “ 来获得命令行的输出,主要原因是易读。
  • 小心不同的平台,尤其是 MacOS 和 Linux 的跨平台。
  • 对于 rm -rf 这样的高危操作,需要检查后面的变量名是否为空,比如:rm -rf $MYDIDR/* 如果 $MYDIR为空,结果是灾难性的。
  • 考虑使用 “find/while” 而不是 “for/find”。如:for F in $(find . -type f) ; do echo $F; done 写成 find . -type f | while read F ; do echo $F ; done 不但可以容忍空格,而且还更快。
  • 防御式编程,在正式执行命令前,把相关的东西都检查好,比如,文件目录有没有存在。

你还可以使用ShellCheck 来帮助你检查你的脚本。

最后推荐一些 Shell 和脚本的参考资料。

各种有意思的命令拼装,一行命令走天涯:

下面是一些脚本集中营,你可以在里面淘到各种牛X的脚本:

甚至写脚本都可以使用框架:

Google的Shell脚本的代码规范:

最后,别忘了几个和shell有关的索引资源:

最后,如果你还有什么别的更好的玩的东西,欢迎在评论区留言,或是到 coolshellx/ariticles @ github 修改本文。

 


关注CoolShell微信公众账号可以在手机端搜索文章

(转载本站文章请注明作者和出处 酷 壳 – CoolShell ,请勿用于任何商业用途)

——=== 访问 酷壳404页面 寻找遗失儿童。 ===——

陈皓 March 17, 2019 at 01:53PM
from 酷 壳 – CoolShell https://ift.tt/2W6efEU

VN:F [1.9.22_1171]
Rating: 0.0/5 (0 votes cast)
VN:F [1.9.22_1171]
Rating: 0 (from 0 votes)
Share

【酷壳】 谈谈我的“三观”

也许是人到了四十多了,敢写这么大的命题,我也醉了,不过,我还是想把我的想法记录下来,算是对我思考的一个snapshot,给未来的我看看,要么被未来的我打脸,要么打未来我的脸。无论怎么样,我觉得对我自己都很有意义。注意,这篇文章是长篇大论。

三观是世界观、人生观和价值观,

  • 世界观代表你是怎么看这个世界的。是左还是右,是激进还是保守,是理想还是现实,是乐观还是悲观……
  • 人生观代表你要想成为什么样的人。是成为有钱人,还是成为人生的体验者,是成为老师,还是成为行业专家,是成为有思想的人,还是成为,
  • 价值观则是你觉得什么对你来说更重要。是名是利,是过程还是结果,是付出还是索取,是国家还是自己,是家庭还是职业……

人的三观其实是会变的,回顾一下我的过去,我感觉我的三观至少有这么几比较明显的变化,学生时代、刚走上社会的年轻时代,三十岁后的时代,还有现在。估计都差不多。

  • 学生时代的三观更多的是学校给的,用各种标准答案给的,是又红又专的
  • 刚走上社会后发现完全不是这么一回事,但学生时代的三观根深蒂固,三观开始分裂,内心开始挣扎
  • 三十岁后,不如意的事越来越多,对社会越来越了解,一部分人屈从现实,一部分人不服输继续奋斗,而一部分人已展露才能,分裂的三观开始收敛
  • 四十岁时,经历过的事太多,发现留给自己的时间不多,世界太复杂,而还有好多事没做,从而变得与世无争,也变得更为地自我。

年轻的时候,抵制过日货,虽然没上过街,但是也激动过,一次是1999南斯拉夫大使馆被炸,一次是2005反日示威,以前,我也是一个爱国愤青。但是后来,有过各种机会出国长时间生活工作,加拿大、英国、美国、日本……随着自己的经历和眼界的开阔,自己的三观自己也随着有了很多的变化,发现有些事并不是自己一开始所认识的那样,而且还是截然相反的。我深深感觉到,要有一个好的世界观,你需要亲身去经历和体会这个世界,而不是听别人说。所以,当我看到身边的人情绪激动地要抵制这个国家,搞死那个民族的时候,我都会建议他去趟那个国家最好在在那个国家呆上一段时间,亲自感受一下。

再后来发现,要抵制的越来越多,小时候的美英帝国主义,然后是日本,再后面是法国、韩国、菲利宾、印度、德国、瑞典、加拿大……从小时候的台独到现在的港独、藏独、疆独……发现再这样下去,基本上来说,自己的人生也不用干别的事了……另外,随着自己的成长,越来越明白,抵制这个抵制那个只不过是幼稚和狭隘的爱国主义,真想强国,想别让他人看得起,就应该把时间和精力放在努力学习放在精益求精上,做出比他们更好的东西来。另外,感觉用对内的爱国主义解决对外的外交问题也有点驴唇不对马嘴,无非也就是转移一下内部的注意力罢了,另外还发现爱国主义还可以成为消费营销手段……不是我不爱国,是我觉得世道变复杂了,我只是一个普通的老百姓,能力有限,请不要赋予我那么大的使命,我只想在我的专业上精进,能力所能及地帮助身边的人,过一个简单纯粹安静友善的生活……

另外,为什么国与国之间硬要比个你高我低,硬要分个高下,硬要争出个输赢,我也不是太理解,世界都已经发展到全球化的阶段了,很多产品早就是你中有我,我中有你的情况了。举个例子,一部手机中的元件,可能来自全世界数十个国家,我们已经说不清楚一部手机是究竟是哪个国家生产的了。即然,整个世界都在以一种合作共赢全球化的姿态下运作,认准自己的位置,拥抱世界,持续向先进国家学习,互惠互利,不好吗?你可能会说,不是我们不想这样,是别人不容我们发展……老实说,大的层面我也感受不到,但就我在的互联网计算机行业方面,我觉得整个世界的开放性越来越好,开源项目空前地繁荣,世界上互联网文化也空前的开放,在计算机和互联网行业,我们享受了太多的开源和开放的红利,人家不开放,我们可能在很多领域还落后数十年。然而现在很多资源我们都访问不了,用个VPN也非法,你说是谁阻碍了发展?我只想能够流畅地访问互联网,让我的工作能够更有效率,然而,我在自己的家里却像做贼一样去学习新知识新技术,随时都有可能被抓进监狱……

随着自己的经历越多,发现这个世界越复杂,也发现自己越渺小,很多国家大事并不是我不关心,是我觉得那根本不是我这个平头老百姓可以操心的事,这个世界有这个世界运作的规律和方法,而还有很多事情超出了我能理解的范围,也超出了我能控制的范围,我关心不关心都一个样,这些大事都不会由我的意志所决定的。而所谓的关心,无非就是喊喊口号,跟人争论一下,试图改变其它老百姓的想法,然而,对事情的本身的帮助却没有多大意义。过上几天,生活照旧,人家该搞你还不是继续搞你,而你自己并不因为做这些事而过得更好。

我对国与国之间的关系的态度是,有礼有节,不卑不亢,对待外国人,有礼貌但也要有节气,既不卑躬屈膝,也不趾高气昂,整体上,我并不觉得我们比国外有多差,但我也不觉得我们比国外有多好,我们还在成长,还需要帮助和协作,四海之内皆兄弟,无论在哪个国家,在老百姓的世界里,哪有那么多矛盾。有机会多出去走走,多结交几个其它民族的朋友,你会觉得,在友善和包容的环境下,你的心情和生活可以更好

我现在更多关心的是和我生活相关的东西,比如:上网、教育、医疗、食品、治安、税务、旅游、收入、物价、个人权益、个人隐私……这些东西对我的影响会更大一些,也更值得关注,可以看到过去的几十年,我们国家已经有了长足的进步,这点也让我让感到很开心和自豪的,在一些地方也不输给强国。虽然依然有一些事的仍然没有达到我的预期,并也超出了我个人的控制范围无法改变,但整体都在变好。不过,未来的变数谁也不好说,我的安全感似乎还不足够,所以,我还是要继续努力,以便我可以有更多的选项。有选项总比没得选要好。所以,我想尽一切办法,努力让选项多起来,无法改变无法影响,那就只能提高自己有可选择的可能性。

另外,在网上与别人对一些事或观点的争论,我觉得越来越无聊,以前被怼了,一定要怼回去,现在不会了,视而不见,不是怕了,是因为,网络上的争论在我看来大多数都是些没有章法,逻辑混乱的争论。

  • 很多讨论不是说事,直接就是怼人骂人。随意就给人扣个帽子。
  • 非黑即白的划分,你说这个不是黑的,他们就把你划到白的那边。
  • 飘移观点,复杂化问题。东拉西扯,牵强附会,还扯出其它不相关的事来混淆。
  • 杠精很多,不关心你的整体观点,抓住一个小辫子大作文章。

很明显,与其花时间教育这些人,不如花时间提升自己,让自己变得更优秀,这样就有更高的可能性去接触更聪明更成功更高层次的人。因为,一方面,你改变不了他们,另外,改变他们对你自己也没什么意义,改变自己,提升自己,让自己成长才有意义。时间是宝贵的,那些人根本不值得花时间,应该花时间去结交更有素质更聪明的人,做更有价值的事。

美国总统富兰克林·罗斯福妻子埃莉诺·罗斯福(Eleanor Roosevelt)说过下面的一句话。

Great minds discuss ideas;
Average minds discuss events;
Small minds discuss people

把时间多放在一些想法上,对自己对社会都是有意义的,把时间放在八卦别人,说长到短,你也不可能改善自己的生活,你批评这个批评那个,看不上这个看不起那个,不会让你有成长,也不会提升你的影响力,你的影响力不是你对别人说长道短的能力,而是别人信赖你并希望得到你的帮助的现象。多交一些有想法的朋友,多把自己的想法付诸实践,那怕没有成功,你的人生也会比别人过得有意义。

如果你看过我以前的文章,你会看到一些吐槽性质的文章,而后面就再也没有了。另外,我也不再没有针对具体的某个人做出评价,因为人太复杂的了,经历的越多,你就会发现你很难评价人,与其花时间在评论人和事上,不如把时间花在做一些力所能及的事来改善自己或身边的环境。所以,我建议大家少一些对人的指责和批评,通过对一件事来引发你的思考,想一想有什么可以改善,有什么方法可以做得更好,有哪些是自己可以添砖加瓦的?你会发现,只要你坚持这么做,你个人的提升和对社会的价值会越来越大,而你的影响力也会越来越大

现在的我,即不是左派也不是右派,我不喜欢爱国主义,我也不喜欢崇洋媚外,我更多的时候是一个自由派,哪边我都不站,我站我自己。因为,生活在这样的一个时代,能让自己过好都是一些比较奢望的事了。

《教父》里有这样的人生观:第一步要努力实现自我价值,第二步要全力照顾好家人,第三步要尽可能帮助善良的人,第四步为族群发声,第五步为国家争荣誉。事实上作为男人,前两步成功,人生已算得上圆满,做到第三步堪称伟大,而随意颠倒次序的那些人,一般不值得信任。这也是古人的“修身齐家治国平天下”!所以,在你我准备要开始要“平天下”的时候,也得先想想,自己的生活有没有过好了,家人照顾好了么,身边有哪些力所能及的事是可以去改善的……

穷则独善其身,达则兼济天下。提升自己,实现自我,照顾好自己的家人,帮助身边的人。这已经很不错了!

什么样的人干什么样的事,什么样的阶段做什么样的选择,有人的说,选择比努力更重要的,我深以为然,而且,我觉得选择和决定,比努力更难,努力是认准了一个事后不停地发力,而要决定去认谁哪一个事则是令人彷徨和焦虑的(半途而废的人也很多)。面对人生,你每天都在作一个一个的决定,在做一个又一个的选择,有的决定大,有的决定小,你的人生的轨迹就是被这一个一个的决定和选择所走走出来的。

我在24岁放弃了一房子离开银行到小公司的时候,我就知道,人生的选择就是一个翘翘板,你要一头就没有另一头,选择是有代价的,你不选择的代价更大;选择是要冒险的,你不敢冒险的风险更大;选择是需要放弃的,因为无论怎么选你都要放弃什么。想想你老了以后,回头一看,好多事情在年轻的时候都不敢做,而你再也没有机会,你就知道不敢选择不敢冒险的代价有多大了。选择就是一种 trade-off,这世上根本不会有什么完美,只要你想做事,你有雄心壮志,你的人生就是一个坑接着一个坑,你所以做的就是找到你喜欢的方向跳坑。

所以, 你要想清楚你要什么,不要什么,而且还不能要得太多,这样你才好做选择。否则,你影响你的因子太多,决定不好做,也做不好。

就像最前面说的一样,你是激进派还是保守派,你是喜欢领导还是喜欢跟从,你是注重长期还是注重短期,你是注重过程还是注重结果……等等,你对这些东西的坚持和守护,成为了你的“三观”,而你的三观则影响着你的选择,而你的选择影响着你的人生。

下面是一些大家经常在说,可能也是大多数人关心的问题,就这些问题,我也谈谈我的价值取向。

挣钱。挣钱是一个大家都想做的事,但你得解决一个很核心的问题,那就是为什么别人愿意给你钱?对于挣钱的价值观从我大学毕业到现我就没怎么变过,那就是我更多关注的是怎么提高自己的能力,让自己值那个价钱,让别人愿意付钱。另外一方面,我发现,越是有能力的人,就越不计较一些短期得失,越计较短期得失的人往往都是很平庸的人。有能力的人不会关心自己的年终奖得拿多少,会不会晋升,他们更多的关心自己真正的实力有没有超过更多的人,更多的关注的是自己长远的成长,而不是一时的利益。聪明的人从来不关心眼前的得失,不会关心表面上的东西,他们更多关心的是长期利益,关心长期利益的人一定不是投机者,一定是投资者,投资会把自己的时间精力金钱投资在能让自己成长和提升的地方,那些让自己可以操更大的盘的地方,他们培养自己的领导力和影响力,而投机者在职场上会通过溜须拍马讨好领导,在学习上追求速成,在投资上使用跟随策略,在创业上甚至会不择手段,当风险来临时,投机者是几乎完全没有抗风险能力的,他们所谓的能力只不过因为形势好。

 

技术。对于计算机技术来说,要学的东西实在是太多,我并不害怕要学的东西很多,因为学习能力是一个好的工程师必需具备的事,我不惧怕困难和挑战。我觉得在语言和技术争论谁好谁坏是一种幼稚的表现, 没有完美的技术,Engineering 玩的是 Tradeoff。所以,我对没有完美的技术并不担心,但是我反而担心的是,当我们进入到一些公司后,这些公司会有一些技术上的沉淀也就是针对公司自己的专用技术,比如一些中间件,一些编程框架,lib库什么的。老实说,我比较害怕公司的专用技术,因为一旦失业,我建立在这些专用技术上的技能也会随之瓦解,有时候,我甚至害怕把我的技术建立在某一个平台上,小众的不用说了,大众的我也比较担扰,比如Windows或Unix/Linux上,因为一旦这个平台不流行或是被取代,那么我也会随之淘汰(过去的这20年已经发生过太多这样的事了)。为了应对这样的焦虑,我更愿意花时间在技术的原理和技术的本质上,这导致我需要了解各种各样的技术的设计方法,以及内在原理。所以,当国内的绝大多数程序员们更多的关注架构性能的今天,我则花更多的时间去了解编程范式,代码重构,软件设计,计算机系统原理,领域设计,工程方法……因为只有原理、本质和设计思想才可能让我不会被绑在某个专用技术或平台上,除非,我们人类的计算机这条路没走对。

 

职业。在过去20多年的职业生涯中,我从基层工程师做到管理,很多做技术的人都会转管理,但我却还是扎根技术,就算是在今天,还是会抠很多技术细节,包括写代码。因为我心里觉得,不写代码的人一定是做不好技术管理的,因为做技术管理有人要做技术决定,从不上手技术的人是做不好技术决定的,另一方面,我觉得管理是支持性的工作,不是产出性的工作,大多数的管理者无非是因为组织大了,所以需要管人管事,所以,必然要花大量的时间和精力处理各种问题,甚至办公室政治,然而,如果有一天失业了,大环境变得不好了,一个管理者和一个程序员要出去找工作,程序员会比管理者更能自食其力。所以,我并不觉得管理者这个职业有意思,我还是觉得程序员这个有创造性的职业更有趣。通常来说,管理者的技能力需要到公司和组织里才能展现,而有创造力的技能的人是可以自己独立的能力,所以,我觉得程序员的技能比管理者的技能能让我更稳定更自地活着。所以,我更喜欢“电影工作组”那样的团队和组织形式。

 

打工。对于打工,也就是加入一家公司工作,无论是在一家小公司还是一家大公司工作,都会有好的和不好的,任何公司都有其不完美的地方,这个需要承认。首先第一的肯定是完成公司交给你的任务,然后我会尽我所能在工作找到可以提高效率的地方进行改善。在推动公司/部门/团队在一技术和工程方面进步并不是一件很容易的事,因为进步是需要成本的,这成本并不一定是公司和团队愿意接授的,而另外,从客观规律上来说,一件事的进步一定是会有和现状有一些摩擦的。有的人害怕有摩擦而忍了,而我则不是,我觉得与别人的摩擦并不可怕,因为大家的目标都是基本一致的,只是做事的标准和方式不一样,这是可能沟通的,始终是会相互理解的。而如果你没有去推动一个事,我觉得对于公司对于我个人来说,都是一种对人生的浪费,敬业也好,激情也好,其就是体现在你是否愿意冒险去推动一件于公于私都有利的事,而不是成为一个“听话”、“随大流”、“懒政”的人,即耽误了公司也耽误了自己。所以,我更信仰的是《做正确的事情,等着被开除》,这些东西,可参看《我看绩效考核》,以及我在Gitchat上的一些问答

 

创业。前两天,有个小伙来跟我说,说他要离开BAT要去创业公司了,说在那些更自由一些,没有大公司的种种问题。我毫不犹豫地教育了他一下,我说,你选择这个创业公司的动机不对啊,你无非就是在逃避一些东西罢了,你把创业公司当做是一个避风港,这是不对的,创业公司的问题可能会更多,去创业公司的更好的心态是,这个创业公司在干的事业是不是你的事业?说白了,如果你是为了你的事业,为了解决个什么,为了改进个什么,那么,创业是适合你的,也只有在做自己事业的时候,你才能不惧困难,才会勇敢地面对一切那种想找一个安稳的避风港呆着的心态是不会让你平静地,你要知道世界本来就是不平静的,找了自己的归宿和目标才可能让你真正的平静。所以,在我现的创业团队,我不要求大家加班,我也不洗脑,对于加入的人,我会跟他讲各种问题和各种机遇,并一直在拷问他的“灵魂”,我们在做的事是不是你自己的事业诉求?可不可以更好?每个人都应该为自己的事业为自己的理想去活一次,追逐自己的事业和理想并不容易,需要有很大的付出,而也只有你心底里的那个理想值得这么大的付出……

 

客户。基于上述的价值观,在我现在创业的时候,我在面对客户的时候,也是一样的,我并不会完全的迁就于客户,我的一些银行客户和互联网客户应该体会到我的做的方式了,我并不觉得迁就用户,用户要什么我就应该给什么,用户想听什么,我就说什么,虽然这样可以省着精力,更圆滑,但这都不是我喜欢的,我更愿意鲜明地表达我的观点,并拉着用户跟我一起成长,因为我并不觉得完成客户的项目有成就感,我的成就感来自客户的成长。所以,面对客户有些做得不对有问题有隐患的地方,或是我有什么做错的事,我基本上都是直言不讳地说出来,因为我觉得这是对客户和对自己最大的尊重。我并不是在这里装,因为,我也想做一些更高级更有技术含量的事,所以,对于一些还达到的客户,我如果不把他们拉上来,我也对不起自己。

 

在我“不惑之年”形成了这些价值观体系,也许未来还会变,也许还不成熟,总之,我不愿跟大多数人一样,因为大多数人都是随遇而安随大流的,因为这样风险最小,而我想走一条属于自己的路,做真正的自己,就像我24岁从银行里出来时想的那样,我选择对了一个正确的专业(计算机科学),呆在了一个正确的年代(信息化革命),这样的“狗屎运”几百年不遇,如果我还患得患失,那我岂不辜负活在这样一个刺激的时代?!我所要做的就是在这个时代中做有价值的事就好了!这个时代真的是太好了!

(全文完)


关注CoolShell微信公众账号可以在手机端搜索文章

(转载本站文章请注明作者和出处 酷 壳 – CoolShell ,请勿用于任何商业用途)

——=== 访问 酷壳404页面 寻找遗失儿童。 ===——

陈皓 February 26, 2019 at 04:02PM
from 酷 壳 – CoolShell https://ift.tt/2U7TXdJ

VN:F [1.9.22_1171]
Rating: 0.0/5 (0 votes cast)
VN:F [1.9.22_1171]
Rating: 0 (from 0 votes)
Share

【酷壳】 记一次Kubernetes/Docker网络排障

昨天周五晚上,临下班的时候,用户给我们报了一个比较怪异的Kubernetes集群下的网络不能正常访问的问题,让我们帮助查看一下,我们从下午5点半左右一直跟进到晚上十点左右,在远程不能访问用户机器只能远程遥控用户的情况找到了的问题。这个问题比较有意思,我个人觉得其中的调查用到的的命令以及排障的一些方法可以分享一下,所以写下了这篇文章。

问题的症状

用户直接在微信里说,他们发现在Kuberbnetes下的某个pod被重启了几百次甚至上千次,于是开启调查这个pod,发现上面的服务时而能够访问,时而不能访问,也就是有一定概率不能访问,不知道是什么原因。而且并不是所有的pod出问题,而只是特定的一两个pod出了网络访问的问题。用户说这个pod运行着Java程序,为了排除是Java的问题,用户用 docker exec -it 命令直接到容器内启了一个 Python的 SimpleHttpServer来测试发现也是一样的问题。

我们大概知道用户的集群是这样的版本,Kuberbnetes 是1.7,网络用的是flannel的gw模式,Docker版本未知,操作系统CentOS 7.4,直接在物理机上跑docker,物理的配置很高,512GB内存,若干CPU核,上面运行着几百个Docker容器。

问题的排查

问题初查

首先,我们排除了flannel的问题,因为整个集群的网络通信都正常,只有特定的某一两个pod有问题。而用 telnet ip port 的命令手工测试网络连接时有很大的概率出现 connection refused 错误,大约 1/4的概率,而3/4的情况下是可以正常连接的。

当时,我们让用户抓个包看看,然后,用户抓到了有问题的TCP连接是收到了 SYN 后,立即返回了 RST, ACK

我问一下用户这两个IP所在的位置,我们知道了,10.233.14.129 是 docker010.233.14.145 是容器内的IP。所以,这基本上可以排除了所有和kubernets或是flannel的问题,这就是本地的Docker上的网络的问题。

对于这样的情况,在 telnet 上会显示 connection refused 的错误信息,对于我个人的经验,这种 SYN完直接返回 RST, ACK的情况只会有三种情况:

  1.  TCP链接不能建立,绝大多数情况都是服务端没有相关的端口号
  2. TCP链接建错误,有可能是因为修改了TCP参数
  3. 有防火墙iptables的设置

因为当时还在开车,在等红灯的时候,我感觉到有点像 NAT 的网络中服务端开启了 tcp_tw_recycle 和 tcp_tw_reuse 的症况(详细参看《TCP的那些事(上)》),所以,让用户查看了一上TCP参数,发现用户一个TCP的参数都没有改,全是默认的,于是我们排除了TCP参数的问题。

然后,我也不觉得容器内还会设置上iptables,所以,我怀疑容器内的端口号没有侦听上,但是马上又好了,这可能会是应用的问题。于是我让用户那边看一下,应用的日志,并用 kublet describe看一下运行的情况,并把宿主机的 iptables 看一下。

然而,我们发现并没有任何的问题。这时,我们失去了所有的调查线索,感觉不能继续下去了……

重新梳理

这个时候,回到家,大家吃完饭,和用户通了一个电话,把所有的细节再重新梳理了一遍,这个时候,用户提供了一个比较关键的信息—— “抓包这个事,在 docker0 上可以抓到,然而到了容器内抓不到容器返回 RST, ACK ” !然而,根据我的知识,我知道在 docker0 和容器内的 veth 网卡上,中间再也没有什么网络设备了(参看《Docker基础技术:LINUX NAMESPACE(下)》)!

于是这个事把我们逼到了最后一种情况 —— IP地址冲突了!

Linux下看IP地址冲突还不是一件比较简单事的,而在用户的生产环境下没有办法安装一些其它的命令,所以只能用已有的命令,这个时候,我们发现用户的机器上有 arping 于是我们用这个命令来检测有没有冲突的IP地址。使用了下面的命令:

$ arping -D -I docker0 -c 2 10.233.14.145
$ echo $?

根据文档,-D 参数是检测IP地址冲突模式,如果这个命令的退状态是 0 那么就有冲突。结果返回了 1 。而且,我们用 arping IP的时候,没有发现不同的mac地址。 这个时候,似乎问题的线索又断了

因为客户那边还在处理一些别的事情,所以,我们在时断时续的情况下工作,而还一些工作都需要用户完成,所以,进展有点缓慢,但是也给我们一些时间思考问题。

柳暗花明

现在我们知道,IP冲突的可能性是非常大的,但是我们找不出来是和谁的IP冲突了。而且,我们知道只要把这台机器重启一下,问题一定就解决掉了,但是我们觉得这并不是解决问题的方式,因为重启机器可以暂时的解决掉到这个问题,而如果我们不知道这个问题怎么发生的,那么未来这个问题还会再来。而重启线上机器这个成本太高了。

于是,我们的好奇心驱使我们继续调查。我让用户 kubectl delete 其中两个有问题的pod,因为本来就服务不断重启,所以,删掉也没有什么问题。删掉这两个pod后(一个是IP为 10.233.14.145 另一个是 10.233.14.137),我们发现,kubernetes,在其它机器上重新启到了这两个服务的实例。然而,在问题机器上,这两个IP地址居然还可以ping得通

好了,IP地址冲突的问题可以确认了。因为10.233.14.xxx 这个网段是docker 的,所以,这个IP地址一定是在这台机器上。所以,我们想看看所有的 network namespace 下的 veth 网卡上的IP。

在这个事上,我们费了点时间。

  • 首先,我们到 /var/run/netns目录下查看系统的network namespace,发现没有。
  • 然后,我们到 /var/run/docker/netns 目录下查看Docker的namespace,发现有好些。
  • 于是,我们用指定位置的方式查看Docker的network namespace里的IP地址
$ ls /var/run/docker/netns | xargs -I {} nsenter --net=/var/run/docker/netns/{} ip addr 

我们发现了比较诡异的事情。

  • 10.233.14.145 我们查到了,docker的namespace下还有这个IP。
  • 10.233.14.137,这个IP没有在docker的network namespace下查到。

于是我上网查了一下,发现了一个docker的bug – 在docker remove/stop 一个容器的时候,没有清除相应的network namespace,这个问题被报告到了 Issue#31597 然后被fix在了 PR#31996,并Merge到了 Docker的 17.05版中。而用户的版本是 17.09,应该包含了这个fix。

要查看所有network namespace,只有最后一条路了,那就是到 /proc/ 目录下,把所有的pid下的 /proc/<pid>/ns 目录给穷举出来。好在这里有一个比较方便的命令可以干这个事 – lsns

于是我写下了如下的命令:

$ lsns -t net | awk ‘{print $4}' | xargs -t -I {} nsenter -t {} -n ip addr | grep -C 4 "10.233.14.137"

解释一下。

  • lsns -t net 列出所有开了network namespace的进程,其每4列是进程PID
  • 把所有开过network namespace的进程PID拿出来,转给 xargs 命令
  • xargs 命令把这些PID 依次传给 nsenter 命令,
    • xargs -t 的意思是会把相关的执行命令打出来,这样我知道是那个PID。
    • xargs -I {}  是声明一个占位符来替换相关的PID

最后,我们发现,虽然在 /var/run/docker/netns 下没有找到 10.233.14.137 ,但是在 lsns 中找到了三个进程,其用了10.233.14.137 这个IP,而且他们的MAC地址全是一样的!冲突了这么多。通过ps 命令,可以查到这三个进程,有两个是java的,还有一个是/pause (这个应该是kubernetes的沙盒)。

我们继续乘胜追击,穷追猛打,用pstree命令把整个进程树打出来。发现上述的三个进程的父进程都在多个同样叫 docker-contiane 的进程下!

这明显还是docker的,但是在docker ps 中却找不道相应的容器,什么鬼!我快崩溃了……

继续看进程树,发现,这些 docker-contiane 的进程的父进程不在 dockerd 下面,而是在 systemd 这个超级父进程PID 1下,我靠!进而发现了一堆这样的野进程(这种野进程或是僵尸进程对系统是有害的,至少也是会让系统进入亚健康的状态,因为他们还在占着资源)。

docker-contiane 应该是 dockerd 的子进程,被挂到了 pid 1 只有一个原因,那就是父进程“飞”掉了,只能找 pid 1 当养父。这说明,这台机器上出现了比较严重的 dockerd 进程退出的问题,而且是非常规的,因为 systemd 之所以要成为 pid 1,其就是要监管所有进程的子子孙孙,居然也没有管理好,说明是个非常规的。(注,关于 systemd,请参看《Linux PID 1 和 Systemd 》,关于父子进程的事,请参看《Unix高级环境编程》一书)

接下来就要看看 systemddockerd 记录的日志了……

总结

通过这个调查,可以总结一下,

1) 对于问题调查,需要比较扎实的基础知识,知道问题的成因和范围。

2)如果走不下去了,要生新梳理,回头看一下过的一些蛛丝马迹,但认真推敲每一个细节。

3) 各种诊断工具要比较熟悉,这会让你事半功倍。

4)系统维护和做清洁比较类似,你需要经党看看系统中是否有一些僵尸进程或是一些垃圾东西,这些东西要及时清理掉。

最后,多说一下,很多人都说,Docker适合放在物理机内运行,这并不完全对,因为他们只考虑到了性能成本,没有考虑到运维成本,在这样512GB中启动几百个容器的玩法,其实并不好,因为这本质上是个大单体,因为你一理要重启某些关键进程或是机器,你的影响面是巨大的

(全文完)


关注CoolShell微信公众账号可以在手机端搜索文章

(转载本站文章请注明作者和出处 酷 壳 – CoolShell ,请勿用于任何商业用途)

——=== 访问 酷壳404页面 寻找遗失儿童。 ===——

陈皓 December 08, 2018 at 11:57AM
from 酷 壳 – CoolShell https://ift.tt/2G500Nn

VN:F [1.9.22_1171]
Rating: 0.0/5 (0 votes cast)
VN:F [1.9.22_1171]
Rating: 0 (from 0 votes)
Share

【酷壳】 程序员练级攻略(2018) 与我的专栏

写极客时间8个月了,我的专栏现在有一些定的积累了,今天我想自己推荐一下我的专栏。因为最新的系列《程序员练级攻略(2018)版》正在连载中,我现有非足的自信向大家推荐我的这个技术专栏。推荐就从最新的这一系统的文章开始。

2011年,我在 CoolShell 上发表了 《程序员技术练级攻略》一文,得到了很多人的好评(转载的不算,在我的网站上都有近1000W的访问量了)。并且陆续收到了一些人的反馈,说跟着这篇文章找到了不错的工作。几年过去,也收到了好些邮件和私信,希望我把这篇文章更新一下,因为他们觉得有点落伍了。是的,老实说,抛开这几年技术的更新迭代不说,那篇文章写得也不算特别系统,同时标准也有点低,当时是给一个想要入门的朋友写的,所以,非常有必要从头更新一下《程序员练级攻略》这一主题

目前,我在我极客时间的专栏上更新《程序员练级攻略(2018版)》。升级版的《程序员练级攻略》会比Coolshell上的内容更多,也更专业。这篇文章有【入门篇】、【修养篇】、【专业基础篇】、【软件设计篇】、【高手成长篇】五大篇章,它们会帮助你从零开始,一步步地,系统地,从陌生到熟悉,到理解掌握,从编码到设计再到架构,从码农到程序员再到工程师再到架构师的一步一步进阶,完成从普通到精通到卓越的完美转身……

这篇文章是我写得最累也是最痛苦的文章,原因如下:

  •  学习路径的梳理。这是一份计算编程相关知识地图,也是一份成长和学习路径。所以有太多的推敲了,知识的路径,体,地图……这让我费了很多工夫,感觉像在编写一本教材一样,即不能太高大上,也不能误人子弟。
  • 新旧知识的取舍。另外,因为我的成长经历中很多技术都成了过去时,所以对于新时代的程序员应该学习新的技术,然后,很多基础技术在今天依然管用,所以,在这点上,哪些要那些不要,也花了我很多的工夫。
  • 文章书籍的推荐。为了推荐最好的学习资料和资源,老实说,我几乎翻遍了整个互联网,进行了大量的阅读和比较。这个过程让我也受益非浅。一开始,这篇文章的大小居然在500K左右,太多的信息就是没有信息,所以在信息的筛选上我花费了很多的工夫,删掉了60%的内容。但是,依然很宠大。

总之,你一定会被这篇文章的内容所吓到的,是的,我就是故意这样做的,因为,这本来就没有什么捷径,也不可能速成,很多知识都是硬骨头,你只能一口一口的啃,我故意这样做就是为了让你不要有“速成”的幻想,也可以轻而一举的吓退那些不想用功不想努力的人

但是,我们也要知道《易经》有云:“取法其上,得乎其中,取法其中,得乎其下,取法其下,法不得也”。所以,我这里会给你立个比较高标准,你要努力达到,相信我,就算是达不到,也会比你一开始期望的要高很多……

下面是这份练级攻略的目录,目前只在极客时间上发布,你需要付费阅读(在本文最后有相关的二维码)。

 

那么,除程序员练级攻略外,我还写了哪些内容?下面是迄今为止我所有的文章的目录。你可以在下面看一下相关的目录。这也算是我开收费专栏来8个月给大家的一份答卷吧。我也没有想到,我居然写了这么多的文章,而且对很多人都很有用。

首先是个人成长和经验之谈的东西,在这里的文章还没有完全更新完,未来要更新什么我也不清楚,但是可以呈现出来的内容和方向如下所示,供你参考。对于个人成长中的内容,都是我多年来的心得和体会,从读者的反馈来看是非常不错的,你一定要要阅读的。

分布式系统架构,我一共出了两个系列,一个是分布式系统架构的本质,另一个是设计模式。前者偏概念,后者偏技术。这里旨在让你看到整个分布式系统设计的一个非常系统的蓝图,但是因为在手机端上,不可能写得非常细,所以,会缺失一些细节,这些细节我是故意缺失的,主要是有几方面的原因,

  • 一方面,这是为了阅读的效果,手机上的文章不过长,所以,不能有太多的细节。
  • 另一方面,也是是想留给大家自行学习,而不是一定要我把饭喂到你的嘴里,你才能吃得着。学习不只是为要答案,而是学方法
  • 最后是我的私心,因为我也在创业,所以,技术细节上东西正是我在做的产品,所以,如果你想了解得更细,你需要和我有更商业合作。

 

区块链的技术专栏本来不在我的写作计划中的,但是因为来问我这方面的技术人太多了,所以,就被问了一系列的文章,这里的文章除了一些技术上的科普,同样有有很多我的观点,你不但可以学到技术,还可以了解一些金融知识和相关的逻辑,我个人觉得这篇文章是让你有独立思考的文章。

我的专栏还在继续,接下来还有一个系列的文章——《从技术到管理》,欢迎关注,也欢迎扫码订阅。

最后友情提示一下:在手机上学习并不是最好的学习方式,也不要在我的专栏上进行学习,把我的专栏当成一个你的助手,当成一个向导,当成一个跳板,真正的学习还是要在线下,专心的,系统地、有讨论地、不断实践地学习,这点希望大家切记!

 

(全文完)


关注CoolShell微信公众账号可以在手机端搜索文章

(转载本站文章请注明作者和出处 酷 壳 – CoolShell ,请勿用于任何商业用途)

——=== 访问 酷壳404页面 寻找遗失儿童。 ===——

陈皓 May 29, 2018 at 12:38PM
from 酷 壳 – CoolShell https://ift.tt/2IWQLAc

VN:F [1.9.22_1171]
Rating: 0.0/5 (0 votes cast)
VN:F [1.9.22_1171]
Rating: 0 (from 0 votes)
Share

【酷壳】 关于Facebook 的 React 专利许可证

随着Apache、百度、Wordpress都在和Facebook的React.js以及其专利许可证划清界限,似乎大家又在讨论Facebook的这个BSD+PATENT的许可证问题了。这让我想起了之前在Medium读过的一篇文章——《React, Facebook, and the Revocable Patent License, Why It’s a Paper》,我觉得那篇文章写的不错,而且还是一个会编程的律师写的,所以有必要把这篇文章传播到中文社区这边来。注意,我不会全部翻译,我只是用我的语言来负责搬运内容和观点,我只想通过这篇文章让大家了解一下这个世界以及专利相关的知识,这样可以避免你看到某乎的“怎么看待XXX”这类的问题中有自己的独立思考和自我判断。;-)

这篇文章的作者叫Dennis Walsh,他自称是亚历桑那和加利福尼亚州的律师,主要针对版权法和专利诉论的法律领域。但是这个律师不一样,他说他更很喜欢商业和软件多一些。现在他用React/GraphQL/Elixir在写一个汽车代理销售相关的软件,而且已经发布到第2版了。

首先,作者表明,专利法经常被人误解,因为其实充满了各种晦涩难懂的法律术语,所以,作者用个例子来讲述专利的一个原则 —— 专利并不是授于让你制造或开发的权利,而是授予你可以排他的权利。(事实上似乎也是这样,申请专利很多时候都不是为了制作相关的产品,而是为了防止别人使用类似的技术制作相关的产品)他用了一个非常通俗易懂的例子:

如果有公司X为铅笔申请了专利,而另一家公司Y为用于铅笔的橡皮擦申请了专利。那么,公司X可以阻止公司Y来生产铅笔,而对带橡皮擦的铅笔没办法,但是公司Y的专利可以让公司X不能生产带有橡皮擦的铅笔。

所以,公司Y的橡皮擦专利又被广泛地叫作“Blocking Patent”。公司Y不能说他发明了铅笔,因为这是公司X的专利,但是,他们可以让公司X无法对铅笔做出某些改进。

于是,因为这种 Blocking Patent 存在,对于开源的公司是不利的,因为根据上面的那个例子来说,开源公司就是公司X,他们做了一个基础的软件,而公司Y在上面做了些改进,并注册成了专利,从而导致开源的公司X无法对它基础开源软件作出被公司Y专利阻止的改进,开源的公司X希望能够免费地使用公司Y的橡皮擦专利,因为毕竟是它发明了铅笔并放弃了铅笔的专利。

于是就出来了“专利反击条款”(Patent Retaliation Clauses)。一般来说有两个专利条款,一种是弱条款,一种是强条款。

Weak Patent Retaliation Clauses – 这种条款声明,如果许可证持有者用某个专利来打击许可证颁布者,那么专利就视为终止。用人话来表达就是,公司X做了一个开源铅笔,而公司Y注册了橡皮檫专利。此时,公司X做了一支带像皮擦的铅笔,公司Y马上对公司X提起专利侵权诉讼。那么,公司Y就失去了对底层铅笔的专利控制。(正如前面所说的,公司Y的橡皮擦专利因为在起诉公司X的开源铅笔,而失去了对开源铅笔的专利排他权利)

Strong Patent Retailiation Clauses – 这种条款声明,比“弱条款”要的更多。具体来说就是,任何专利声明终结许可证,而不管这个专利有没有和你基础的软件有关系。用人话来说就是,公司Y使用他们的热气球专利来起诉公司X,那么公司Y就失去了他们对铅笔的专利限制。

我个人理解起来,这两种条款看上去是防御性质的。

Facebook的React的Patent License如下:

The license granted hereunder will terminate, automatically and without notice,if you (or any of your subsidiaries, corporate affiliates or agents) initiatedirectly or indirectly, or take a direct financial interest in, any Patent Assertion: (i) against Facebook or any of its subsidiaries or corporateaffiliates, (ii) against any party if such Patent Assertion arises in whole orin part from any software, technology, product or service of Facebook or any ofits subsidiaries or corporate affiliates, or (iii) against any party relating to the Software. Notwithstanding the foregoing, if Facebook or any of itssubsidiaries or corporate affiliates files a lawsuit alleging patentinfringement against you in the first instance, and you respond by filing apatent infringement counterclaim in that lawsuit against that party that isunrelated to the Software, the license granted hereunder will not terminateunder section (i) of this paragraph due to such counterclaim.

这些条款中和基础软件没有任何关系,所以,这个条款是“强专利反击条款”

在后面,本文的作者又解解释了,为什么React的“强专利反击条款”就跟没有似的。他在文中针对一些歇斯底里的言论,如:“Facebook不用害怕专利诉讼了,而且他可以随时偷袭你家的专利仓库”,也作出了一些解释来分析这个事。

Contractural Liability – 意思是说,专利方面的东西只会影响专利上的事,而不会影响和专利无关的事,React底层协议是BSD-3许可证还是会被保留。换句话说,React的“强专利反击条款”只生效于专利层面,而不会对非常专利的软件使用产生问题,如果和专利无关,React还是走BSD-3的许可协议。

Copyright Liability – 这个和Contractural Liablitity 一样。作者说,如果有人有特别的案例或是有说服力的论据来说明Facebook的这个条款会作用于非专利的地方,那么,请告诉他。

Patent Liability – 专利的责任和损害是两件事,非专业人士总是会把其搞混。

第一个问题是Liability, 要搞清这个事,得搞清“Patent’s Claims”,作者说,现在的很多专利都是一些想法,很多投机份子随便一拍脑袋就发明出一个想法,然后就去注册专利了。但是可以被用来法律执行的只有“Patent’s Claims”(专利的权利主张)。这些权利主张相当相当的晦涩难读,而且是会故意被模糊掉的,因为,当你清楚的定义了你的发明是什么,那么,就可以清楚的定义出来什么不是你的发明。比如:一个铅笔专利权利主张里说,“这一个用石墨和木头组合起来的写字工具”,那么,只要我不用木头和石墨做组合,那么我就不是专利侵权。所以,一般来说,一个好的专利主张是,“这是一个用于标记表面的装置,其包括:与标记端相连的握持端”。作者这里给了一个苹果公司的滑动解锁专利的示例。可以感受一下产品规格说明和专利权利主张完全是两码事。

上面这些说明,专利这些事吧,在法律界里是非常非常困难作出评估的。所以,这个社会每年都会给律师们几十亿美金来一遍又一遍地回答这些问题,而且还经常回答错了。而对于美国的法律,对于专利诉讼会有一个叫Markman hearing的审前听证会(马克曼听证会),自从1996年美国最高法的“马克曼诉威斯幽仪器公司案”这个听证会就变成了一个惯例,美国联邦法院用这个听证会来向决定专利权利主张的解释,然而,上诉法院经常性的推翻审判法院的裁决。(对于美国法律来说,一般是法官认证法律,陪审团认定事实,然而,对于专利而言,1996年的那个案件认为专利术语是一个需要法官决定的法律问题,而不是陪审团决定的事实问题。关于马克曼听证会的事,可以参看本文未尾的附录)

所以,要决定Facebook的专利责任,我们需要评估Facebook的专利及其权利主张。具体来说,要明确Facebook对于React这个底层技术的专利权利主张是什么?但是作者搜了一下,发现什么也没有找到。也就是说,对于USPTO(美国专利商标局)或法院来说,他们没办法对Facebook的这样没有为React申请专利的方式来执行任何和专利的诉讼,也就是说,Facebook的这个React License的条款,美国政府是无法在法律上支持的。

第二个问题是专利损害。就算是Facebook可以评估出来一个合法可执行的专利来保护React,对于专利损害也是很有问题的。作者说他到目前还没有发现一个开源软件被专利侵权的事,就算有这样的案例,也不会是这里说的这个事。作者觉得在这个事上操作起来就是一个笑话。

另外,作者认为,React 专利许可证这个事就是个纸老虎。因为,一方面,这个专利不像电信通讯里的那些专利,你拿不掉。作者认为要从你的代码中把React去掉虽然难,但是也不是什么很难的事,另外,要打这样的专利官司,一般来说,在美国至少要花100-200万美金的费用才能发起诉讼,而要胜诉则需要需要200多万到2000万美金的费用,你觉得你要花多钱才能把React从你的代码库中剔除?肯定比这钱少。

作者还认为,Facebook玩这个事虽然出发点不错,但是感觉并不聪明,从目前的情况看下来,就像他想咬你一口,但却没有牙。

后面,作者还说了一下,转成别的框架会不会有问题?比如:你用Preact/Vue或是你自研的东西?作者说,未必,如果Facebook真的为React注册了专利,比如:React里的组件技术、虚拟DOM渲染技术等等。那么,你用Pract/Vue或是带这样技术的自研的框架,那么,从你使用的第一天就在侵犯Facebook的专利权了。而,使用React反而不会有这么大的风险,因为Facebook让你免费的用React。作者说,用别的框架的法律风险比用其它替代品的风险更高。

后面,作者也更新了一篇文章 《Using GraphQL? Why Facebook Now Owns You》,意思是,用React可能还好,但是用GraphQL就有问题了。因为找到了GraphQL的专利—— “Graph Query Logic”

后来我查了一下,我发现,React也有个相关的专利—— “Efficient event delegation in browser scripts ”,看上去和虚拟DOM渲染有关。Holy Shit!

好了,用还是不用React我也不知道,总之,这个世界比较复杂,我只是想借这篇文章来学习一下法律上的相关东西,欢迎听到大家的观点。

最后,请允许我调侃一下来结束本文——“不用担心React的许可证问题,因为前端不是一年半就用新的框架重写一次么?”哈哈。

延伸阅读

马克曼听证会 – Markman Hearing

马克曼听证会的一些背景知识,下面的文字来源于《“马克曼听证”制度的由来及启示

与美国专利诉讼的悠长历史相比,1996年才经美国最高法院确立的“马克曼听证”(Markman Hearing,也称为Claim Construction,即权利要求书的解释)无疑是一项年轻的制度。但由于几乎所有的专利侵权诉讼中都会遇到涉案专利权利要求书的解释这一核心问题,且因“马克曼听证”结果往往清楚地预示了案件结果,经“马克曼听证”获得有利结论的一方一旦据此向法庭提起不审即判的动议,专利侵权诉讼往往可就此快速了结,因此该制度的确立成为美国专利诉讼历史上的一件大事。

“马克曼听证”制度的由来

“马克曼听证”制度确立之前,在专利侵权诉讼中的权利要求书解释,通常交由陪审团在对案件事实进行裁决时一并做出,且并不会在诉讼文件上单独就陪审团这一问题的判断进行记录。1991年,马克曼(Markman)先生因认为其拥有的专利号为RE33054的“干洗衣物贮存及追踪控制装置”专利权被Westview公司所侵犯,遂向宾夕法尼亚州东区联邦地方法院提起了专利侵权诉讼。

该专利是用扫描的方式,将客户的衣物编号扫描后输入电脑中做分类标示,并在衣物干洗过程中追踪衣物位置,干洗完成后自动将衣物放回客户固定的存贮位置。被告的产品则是同时运用扫描器和电脑两种方式,将客户干洗衣物的资料存入电脑并显示费用、日期等相关信息。本案陪审团的裁决认为被告装置构成对原告专利权利的侵犯,但该地方法院认为系争专利与被告装置在功能实施上并不一致,遂推翻陪审团的裁决,判决被告不构成侵权。

马克曼不服,于1995年向联邦上诉法院提起上诉,但其上诉理由仅为联邦地方法院错误地解释了陪审团关于专利权利要求书解释中某个词语的涵义。联邦上诉法院在审理该案时,将案件的核心问题定为两个:一是原告对于请求项解释有无权利请求陪审团裁决;二是联邦地方法院是否正确地解释了“Inventory”一词。该院多数法官经审理后认为,权利要求书范围的解释与确定,属于法律问题而非事实问题,因而属于法院权限,而不应交由陪审团决定,且此前将此问题交由陪审团确定并不妥当。同时,由于认为原告专利与被告装置存在实质功能上的差异,联邦上诉法院亦不认为被告构成专利侵权。少数持不同意见的该院法官主要是质疑这一结论违反了美国第七宪法修正案(即所有根据美国法律进行的普通法诉讼,只要争议金额超过20美元,即有要求陪审团审判的权利)。

马克曼不服,向最高法院提出上诉。1996年4月23日,美国最高法院就马克曼诉Westview器械公司案(Markman v. Westview Instruments, Inc. 517 U.S. 370 (1996))做出终审裁决,裁决认定:权利要求书的解释是联邦地区法院法官应当处理的法律问题,而不是应当由陪审团来认定的事实问题,尽管在解释权利要求书的过程中可能会包含一些对于事实问题的解释,且这样做并不违反第七修正案赋予给陪审团的权利。这一裁决标志着“马克曼听证”制度的正式确立。

“马克曼听证”制度的不足

该案判决是美国专利诉讼史上的一个重大转折。“马克曼听证”成为法官专门用于解释专利权利要求的一个经常性听证程序,用以解决专利侵权诉讼的核心问题。由于该听证并非普遍适用,因此,十几年来,联邦民事诉讼规则并未正式对其有任何规定,而是给予法院绝对的自由裁量权。但是,何时可以进行“马克曼听证”?如何进行?是否有必要进行?类似问题在一定程度上困扰了审理专利侵权案件较多的法院。

2001年,加州北区联邦地区法院率先制定了供本法院使用的专利审判专属规则(Patent Local Rules),其中第四部分即为权利要求书的解释程序(Claim Construction Proceddings),对“马克曼听证”的时间、流程、限制及当事人的义务均进行了规定。此后,各州纷纷效仿。目前,乔治亚州北区联邦法院、得克萨斯州东区联邦法院、得克萨斯州南区联邦法院、宾夕法尼亚州西区联邦法院等都制订了书面的“马克曼听证”程序指南。近年来,不断有新的案例在解释与细化着“马克曼听证”,如2006年的Wilson Sporting Goods Co.诉Hillerich & Bradsby Co.案,2005年的Phillips诉AWH Corp.案,2008年的Howmedica Osteonics Corp.诉Wright Medical Technology, Inc.案,这些司法实践大大拓展与丰富了“马克曼听证”使用的实体和程序规则,使之日渐成为美国专利诉讼中一个复杂、完备的司法程序。以至于竟然有人开发了模拟“马克曼听证”程序,只要你愿意,可以下载并训练,以熟悉和确保有真正的权利要求书解释时不会出现不利于自己的问题。

但是,该听证带来的问题也逐渐受到重视。有人质疑说该程序导致专利诉讼费用增加,因为“马克曼听证”通常会单独进行,且程序复杂,因此导致当事人花费大量的时间与精力,更为重要的是,由于40%至60%的联邦地区法院案件会在联邦巡回上诉法院被推翻,因此,花费巨大的“马克曼听证”似乎价值有限。同时,权利要求书的解释要求是不多不少,忠实于技术发明思想与发明事实,但由于地区法院分散,法官的相关技术知识不十分专业,将权利要求书解释这样的问题交给他们,难免会带来一些无法克服的问题。

“马克曼听证”制度的启示

我国民事诉讼中并无陪审团制度,案件的事实问题与法律问题均由法官审理与确定。在专利侵权诉讼中,对于案件中涉及到的技术问题可以通过专家鉴定等方式解决,但并不因此免除法官审理案件的义务,即法律问题的判断归于法官,事实的法律属性判断仍然归于法官。同时,权利要求书的解释在我国的专利侵权诉讼中并不是一个单独的程序,而是合并在案件审理过程中。因此,仅就我国的司法审判而言,“马克曼听证”制度并无直接的借鉴意义。

但是,对于那些已经走出和正在走出国门的企业来说,了解与掌握这一重要的专利诉讼程序却是极其重要的。通领科技集团的积极尝试充分证明了这一点,而且随着这一程序的不断成熟,美国国际贸易法院(ITC)也开始在审理时适用“马克曼听证”制度。所以,知道“马克曼听证”意味着什么,确保所提交的用于解释权利要求的文件确实充分,学会利用“马克曼听证”,无论是对于破解美国的专利诉讼威胁,还是为未来准备有效的法律武器,无疑都非常重要。(知识产权报 作者 魏玮)

 

(全文完)


关注CoolShell微信公众账号可以在手机端搜索文章

(转载本站文章请注明作者和出处 酷 壳 – CoolShell ,请勿用于任何商业用途)

——=== 访问 酷壳404页面 寻找遗失儿童。 ===——

陈皓 September 19, 2017 at 02:08PM
from 酷 壳 – CoolShell http://ift.tt/2wrxd0l

VN:F [1.9.22_1171]
Rating: 0.0/5 (0 votes cast)
VN:F [1.9.22_1171]
Rating: 0 (from 0 votes)
Share

【酷壳】 如何免费的让网站启用HTTPS

今天,我把CoolShell变成https的安全访问了。我承认这件事有点晚了,因为之前的HTTP的问题也有网友告诉我,被国内的电信运营商在访问我的网站时加入了一些弹窗广告。另外,HTTP的网站在搜索引擎中的rank会更低。所以,这事早就应该干了。现在用HTTP访问CoolShell会被得到一个 301 的HTTPS的跳转。下面我分享一下启用HTTPS的过程。

我用的是 Let’s Encrypt这个免费的解决方案。Let’s Encrypt 是一个于2015年推出的数字证书认证机构,将通过旨在消除当前手动创建和安装证书的复杂过程的自动化流程,为安全网站提供免费的SSL/TLS证书。这是由互联网安全研究小组(ISRG – Internet Security Research Group,一个公益组织)提供的服务。主要赞助商包括电子前哨基金会Mozilla基金会Akamai以及Cisco等公司(赞助商列表)。

2015年6月,Let’s Encrypt得到了一个存储在硬件安全模块中的离线的RSA根证书。这个由IdenTrust证书签发机构交叉签名的根证书被用于签署两个证书。其中一个就是用于签发请求的证书,另一个则是保存在本地的证书,这个证书用于在上一个证书出问题时作备份证书之用。因为IdenTrust的CA根证书目前已被预置于主流浏览器中,所以Let’s Encrypt签发的证书可以从项目开始就被识别并接受,甚至当用户的浏览器中没有信任ISRG的根证书时也可以。

以上介绍文字来自 Wikipedia 的 Let’s Encrypt 词条

为你的网站来安装一个证书十分简单,只需要使用电子子前哨基金会EFF的 Certbot,就可以完成。

1)首先,打开 https://certbot.eff.org 网页。

2)在那个机器上图标下面,你需要选择一下你用的 Web 接入软件 和你的 操作系统。比如,我选的,nginx 和 Ubuntu 14.04

3)然后就会跳转到一个安装教程网页。你就照着做一遍就好了。

以Coolshell.cn为例 – Nginx + Ubuntu

首先先安装相应的环境:

$ sudo apt-get update
$ sudo apt-get install software-properties-common
$ sudo add-apt-repository ppa:certbot/certbot
$ sudo apt-get update
$ sudo apt-get install python-certbot-nginx

然后,运行如下命令:

$ sudo certbot --nginx

certbot 会自动检查到你的 nginx.conf 下的配置,把你所有的虚拟站点都列出来,然后让你选择需要开启 https 的站点。你就简单的输入列表编号(用空格分开),然后,certbot 就帮你下载证书并更新 nginx.conf 了。

你打开你的 nginx.conf 文件 ,你可以发现你的文件中的 server 配置中可能被做了如下的修改:

listen 443 ssl; # managed by Certbot
ssl_certificate /etc/letsencrypt/live/http://ift.tt/2wOZrB9; # managed by Certbot
ssl_certificate_key /etc/letsencrypt/live/http://ift.tt/2vpjHpD; # managed by Certbot
include /etc/letsencrypt/options-ssl-nginx.conf; # managed by Certbot

 # Redirect non-https traffic to https
if ($scheme != "https") {
  return 301 https://$host$request_uri;
} # managed by Certbot

然后,就 nginx -s reload 就好了。

但是,Let’s Encrypt 的证书90天就过期了,所以,你还要设置上自动化的更新脚本,最容易的莫过于使用 crontab 了。使用 crontab -e 命令加入如下的定时作业(每个月都强制更新一下):

0 0 1 * * certbot renew --force-renewal

当然,你也可以每天凌晨1点检查一下:

0 1 * * * certbot renew 

注:crontab 中有六个字段,其含义如下:

  • 第1个字段:分钟 (0-59)
  • 第2个字段:小时 (0-23)
  • 第3个字段:日期 (1-31)
  • 第4个字段:月份 (1-12 [12 代表 December])
  • 第5个字段:一周当中的某天 (0-7 [7 或 0 代表星期天])
  • /path/to/command – 计划执行的脚本或命令的名称

这么方便的同时,我不禁要问,如果是一些恶意的钓鱼网站也让自己的站点变成https的,这个对于一般用来说就有点难以防范了。哎……

当然,在nginx或apache上启用HTTPS后,还没有结束。因为你可能还需要修改一下你的网站,不然你的网站在浏览时会出现各种问题。

启用HTTPS后,你的网页中的所有的使用 http:// 的方式的地方都要改成 https:// 不然你的图片,js, css等非https的连接都会导致浏览器抱怨不安全而被block掉。所以,你还需要修改你的网页中那些 hard code http:// 的地方。

对于我这个使用wordpress的博客系统来说,有这么几个部分需要做修改。

1)首先是 wordpress的 常规设置中的 “WordPress 地址” 和 “站点地址” 需要变更为 https 的方式。

2)然后是文章内的图片等资源的链接需要变更为 https 的方式。对此,你可以使用一个叫 “Search Regex” 插件来批量更新你历史文章里的图片或别的资源的链接。比如:把 http://coolshell.cn 替换成了 https://coolshell.cn

3)如果你像我一样启用了文章缓存(我用的是WP-SuperCache插件),你还要去设置一下 “CDN” 页面中的 “Site URL” 和 “off-site URL” 确保生成出来的静态网页内是用https做资源链接的。

基本上就是这些事。希望大家都来把自己的网站更新成 https 的。

嗯,12306,你什么时候按照这个教程做一下你的证书?

(全文完)


关注CoolShell微信公众账号可以在手机端搜索文章

(转载本站文章请注明作者和出处 酷 壳 – CoolShell ,请勿用于任何商业用途)

——=== 访问 酷壳404页面 寻找遗失儿童。 ===——

陈皓 August 26, 2017 at 02:06PM
from 酷 壳 – CoolShell http://ift.tt/2xAOZu8

VN:F [1.9.22_1171]
Rating: 0.0/5 (0 votes cast)
VN:F [1.9.22_1171]
Rating: 0 (from 0 votes)
Share

【酷壳】 我看绩效考核

(本来,这篇文章应该在5月份完成,我拖延症让我今天才完成)

前些天,有几个网友找我谈绩效考核的事,都是在绩效上被差评的朋友。在大致了解情况后,我发现他们感到沮丧和郁闷的原因,不全是自己没有做好事情,他们对于自己没有做好公司交给的事,一方面,持一些疑义,因为我很明显地感到他们和公司对一件是否做好的标准定义有误差,另一方面,他们对于自己的工作上的问题也承认。不过,让他们更多感到沮丧的原因则是,公司、经理或HR和他们的谈话,让他们感觉整个人都被完全否定了,甚至有一种被批斗的感觉。这个感觉实在是太糟糕了。

因为我也有相似的经历,所以,我想在这里写下一篇文章,谈谈自己的对一些绩效考核的感受。先放出我的两个观点:

1)制定目标和绩效,目的不是用来考核人的,而用来改善提高组织和人员业绩和效率的。

2)人是复杂的,人是有状态波动的,任何时候都不应该轻易否定人,绩效考核应该考核的是事情,而不是人。

我个人比较坚持的认为——绩效分应该打给项目,打给产品,打给部门,打给代码,而不是打给人。然而现在的管理体制基本上都是打给人,而很多根本不擅长管理的经理和HR以及很多不会独立思考的吃瓜群众基本上都会把矛头指向个人,所以,当然会有开批斗会的感觉。

 

举几个例子

为了讲清楚我的上述观点,请让我先铺垫一下,先说几个例子吧,韩寒的例子我就不说了。

苏步青同学在小学时成绩很糟糕,全班倒数第一。

华罗庚同学上学时数学还考不及格,要不是王维克老师的鼓励并让他受上了数学,他可能也就完全埋没了。

爱因斯坦同学三岁时不会讲话,九岁时说不清话,小学和中学成绩很普通,与人交往能力太差,老师和同学都不喜欢他,教他希腊文和拉丁文的老师对他更是厌恶,曾经公开骂他:“爱因斯坦,你长大后肯定不会成器。”而且因为怕他在课堂上会影响其他学生,竟想把他赶出校门。爱因斯坦也是想尽了办法逃学。十六岁的爱因斯坦报考了瑞士苏黎世的联邦工业大学工程系,可是入学考试却告失败。

郑渊洁上学时,老师要求写《早起的鸟有虫子吃》,郑渊洁唱反调写《早起的虫子被鸟吃》,再加上数学老师发难,于是被开除了。从此郑渊洁没有上过一天学。

列夫尔斯泰大贵族出身,2岁丧母,9岁丧父,16岁上大学,大学三年级自动退学回家进行改革。在青年时期不好好读书,考试不及格,留级。他赌博、借债、鬼混……

这个的例子太多了,我从另一个方面举几个体育运动相关的例子,可能年轻的朋友都不知道,可以问问你们的父母。

80年代,中国有一批非常优秀的体育运动员,比如:体操王子李宁,打破过世界跳高记录的朱建华,还有乒乓球世界冠军马文革,还有羽毛球世界冠军赵建华,记得有一年参加世界比较,他们全输了,而输的还很惨。于是国内的一些媒体和民众开始骂他们,甚至说他们是民族的败类、耻辱,还有很多人找上门要教训他们……

如果我们把绩效分比做在学校里的考试分,那么你是否会和我一样认为,考试的成绩只能代表这个人对这些知识点的掌握或理解,而且仅仅在这个时间点,根本不代表这个人根本就不行,更不代表他一直不行。因为挂科太多被学校开除的同学,并不见得这些人在社会上就无活生活下去,反而,他们中的有些人可能会考试成绩好的人还活得好。不是么?这样的例子在我们身边还少吗?

所以,当我看到某HR说某老员工——“他今天要不自己离开,未来一年也一定会因为绩效问题而被公司开了的”,除了感到居然有人类可以预知他人未来的可笑之外,我感到是一种悲哀,一种管理体制上的悲哀,我感到了在这HR考评背后一股非常强的暗流和不可见的力量让她干出了这样一件匪夷所思的事。

好些公司还考评价值观,价值观无可厚非,我觉得一个企业的价值观是非常必要的,但是考核价值观是件非常危险的事情。这个世界上和传统势力唱反调的人实在是太多了,而被定性为价值观有问题被迫害的人也是多了去了。被批斗被侮辱被毒打的老舍;因为同性恋问题,被迫害而自杀的图灵;因为不同意“地心说”被烧死的哥白尼,…… 这样的事情已经够多了,新的时代里不应该再发生这样的事了,无论大小。

考核价值观最大的问题就是非常容易的上纲上线,也非常容易的被制造政治斗争,也非常容易的扼杀各种不同思想,老实说,这从很大程度上是一种洗脑的手段——通过对人制造一种紧张或恐惧而达到控制思想的目的。

 

对公司和管理者想说的话

下面我来谈谈绩效考核我的一些观点。在谈这个观点前,你可以移步看一下这篇新闻报道——《绩效主义毁了索尼》。而近年来,“放弃绩效考核”的斗争已经从科技企业中的Adobe、戴尔、微软、亚马逊,席卷到德勤、埃森哲、普华永道等咨询服务类企业。甚至通用电气(GE)——曾经的绩效管理的鼻祖,也宣布抛弃正式的年度绩效考核。在刚过去的2016年,腾讯的张小龙对微信事业群发出“警惕KPI”的呼声;李彦宏在内部信中将百度的掉队归咎于“从管理层到员工对短期KPI的追逐”;雷军干脆宣布小米“继续坚持‘去KPI’的战略,放下包袱,解掉绳索,开开心心地做事。”;王石也在个人微博中感慨:“绩效主义像企业的脓包”。

绩效考核在本质上就是像学校教育以分数论英雄,而忽略员工的成长和素质教育是一个道理。当学生和老师只关注考试分数时,而只有考试分数来评价老师和学生的优良中差时,老师和学生就会开始使用一些非常形式的方式来达到这个目标,比如:死记硬被,记套路,题海战术…… 而学习的能力的考评彻底地沦为了一种形式主义。反而,分数考的越高,脑子越死。(注:美国现行教育是不允许通过学生考试成绩来评价老师的能力的)

近几年来,一些大公司开始使用 OKR – Objectives, Key Result ,但是在实践过程中,我发现好些公司用OKR,本质上还是KPI – Key Performance Indicator, 因为OKR里面有一个Key Result,用来衡量 Objectives 的结果指标。于是,使用者习惯性的设置上了KPI。我个人认为 OKR 有两个非常大的特性:0)由员工提出,1)以目标为导向。2)全员共享。

举个例子,OKR可能会是制定成下面这个样子的:

Objectives:增强用户体验,

Key Results:

1)用户操作步骤减少20%以上,

2)客服减少40%以上工单,

3)用户99.9%的系统操作的响应时间为100ms以下

然后,把这个目标分解给产品、用户体验、技术团队,形成子的Objectives并关连上相庆的父级的Key Result,比如,产品部门定义的Objectives:1)优化注册流程,减少2个步骤,2)优化红包算法,让用户更容易理解,3)提高商品质量,减少用户投诉。后端技术团队定义的Objectives: 1)定义SLA以及相关监控指标,2)自动化运维,减少故障恢复时间,3)提高性能,吞吐量在xxxqps下的99.9%的响应时间为xxms ……

这个Objective会从公司最高层一直分解到一线员工,信息完全透明,每个人都可以看到所有人被分解到目标,每个人都知道自己在为什么样的目标而奋头,而每个人也可以质疑,改进,建议调整最高层的目标和方向。而不是领到的是被层层消化过的变味的二手,三手甚至四五手的信息。

而 KPI 最大的问题就是用 OKR 里的 Key Results 拿来当目标,从而导致员工只知道要做什么,不知道为什么,不知道为什么,不能理解目标,工作也就成了实实在在的应付!

松下公司早在1933年,就召集168名员工,把松下未来250年的远景规划目标公布于众,从1956年开始,就定期宣布并解读自身的“五年计划”,帮助每位员工的目光从眼前的短期利益移开,树立自己的理想和目标,也促进了松下的可持续性发展。

然而,今时不同往昔,随着产品周期的不断缩减、竞争对手的持续涌入、高新技术的频频迭代,企业的战略的变化与调整变得更加频繁,朝令夕改的经营策略已经成为兵家常态。 在这一过程中,有多少员工了解调整之后的战略呢?员工的绩效指标又根据战略调整多少次了呢?

KPI本身是一种被动的、后置的考察,在工作完成之后考察员工的行为是否符合标准。因此,员工对于公司的目标漠不关心,只关心自己的KPI,因为这才是自己的最大的利益,为了达到KPI,有的员工开始不思考,并使用一些简单粗暴的玩法,其实这样既害了公司,也害了自己。自己的成长和进步也因为强大的 KPI 而抛在了脑后。

当然,KPI 绩效考核一般来说,不一定会毁掉公司的,相反,对于喜欢使用蛮力的劳动密集型的公司来说,可能还有所帮助,然而,KPI毁掉的一定是团队的文化和团队的挑战精神,以及创新和对事业的热情,甚至会让其中的人失去应有的正常的判断力(分不清充分和必要条件,分不清很多事的因果关系)。

 

对职场人想说的话

那么,对于个人来说,如何面对公司给自己的绩效考核呢?如何面对他们的绩效考核呢?

还是用学校考试分数来做对比,如果说,用考试分数论英雄,一个人考高分就是绩效上的人才,考不及格的人就是人渣,这对吗?当然不是。也许仅于对于考试来说可以把人分成三六九等,但是对于整个人生来说,考试成绩和一个人在这个社会里的的成就并没有非常直接的因果关系。面对现实的社会,最终很多成绩好的人为成绩差的人工作的例子也有很多很多了。

我想说什么?我想说的是——用一颗平常心来面对公司给你打的分数,因为那并不代表你的整个人生。但是,你要用一颗非常严肃的心来面对自己的个人发展和成长,因为这才是真正的事。

换句话说,如果要给一个人打绩效分,那不是由一个公司在一个短期的时间时打出来,而是由这个人在一个长期的时间里所能达到的成就得出来的。

就像WhatsApp的联合创始人Brian Acton 在 2009年时面试Facebook时没有面试通过,然而在 5 年以后,他把自己创办的公司以190亿美元卖给了FaceBook。阿里巴巴的马云不也一样吗?找工作各种被拒,开办的第一个公司成绩也不好,20年前,一堆人都说马云这也不行那也不行,然而,后面呢?反过来说,也很多职业经理人在公司里绩效非常好,然后到了创业公司却搞得非常的糟糕,这又说明了什么呢?

这就像动物一样,有的动物适合在水里生活,有的动物适合在陆地上,鱼在陆地上是无法生存的,你让老虎去完成游泳的工作,你让鱼去完成鸟类的工作,你能考核到什么呢?我们每个人都有适合自己的环境,找到适合自己的环境才是最关键的!与其去关注别人对自己的评价,不如去寻找适合自己的环境。

所以,一个特定环境下的绩效考核并不代表什么,而那些妄图用绩效考核去否定一个人的做法,或多或少就是“法西斯”或“红卫兵”的玩法

好了!让我们不要再说绩效考核了,让我们回到,真正让自己提高,让自己成长,让自己的强的话题上来吧。这里,我需要转引一篇文章《Do the Right Thing, Wait go get fired》,文中提到《 Team Geek》这本书中的一句话

做正确的事情,等着被开除。

谷歌新员工(我们称做“Nooglers”)经常会问我是如何让自己做事这么高效的。我半开玩笑的告诉他们这很简单:我选择做正确的事情,为谷歌,为世界,然后回到座位上,等着被开除。如果没有被开除,那我就是做了正确的事情——为所有人。如果被开除了,那选错了老板。总之,两方面,我都是赢。这是我的职业发展策略。

嗯,考试分数不是关键,别人对你的评价也不是关键,自己有没有成长有没有提高有没有上一个台阶才是关键。KPI不是关键,有没有在做正确的事,这才是关键!不是这样吗?

其它

我大学四年级时,觉得马上就要离开学校了,当时想干点以后再以没有机会干的事。想来想去,就是上学这么多年来,从来没有不及格过,于是我任性了一把,挂了一个科,去补考了一下。挂科的时候也收到一些同学的笑话,还有老师的批评,不过,这让我感觉我的学校经历更完整了。因为,这让我在22岁的时候,就经历并大概明白了一些人生的道理。

从98年工作到2013年来,就像一个好学生一样,我从来没有出现过任何的工作绩效问题,反正还经常在工作中成为标杠型的人,然并卵,只有自己成长才是最真实的感觉。“做正确的事,等着被开除”,这可能是我迄今为止在职场里做的最疯狂也是最正确的事了。因为,这让我有更多的经历,也让我内心变得越来越强大,也让我找到了更具挑战的事,更让我对自己有更清楚的认识。

最后,我知道一定会有人来怼我,所以,最后我还想留段话,留给那些还是想通过绩效来否定人的人。

如果你对我的绩效或技术能力有怀疑,没问题,那么希望你能做到下述我已做到的事,再来喷我,谢谢!

在你40岁,在父亲病重,孩子上学问题、房贷并未还清、你是全家唯一收入来源之类的中年危机的情况下,辞去你现在的工作,不加入任何一家公司,不用自己的任何一分钱积蓄,不要任何人的投资和帮助。只通过自己的技术能力,为别人解决相应的技术难题(不做任何无技术含量的外包项目),来生存养家,并除了能照顾好自己的家人没有降低自己的生活水平之外,还能在养活3个每人年薪36万元的工程师

请问这样的绩效能打个几分呢?呵呵。

当然,不管怎么说,我还有很路要走,还有很多不足,我还要继续努力。所以,我挑了一条对我来说最难走的路,作死创业……

(全文完)


关注CoolShell微信公众账号可以在手机端搜索文章

(转载本站文章请注明作者和出处 酷 壳 – CoolShell ,请勿用于任何商业用途)

——=== 访问 酷壳404页面 寻找遗失儿童。 ===——

陈皓 July 09, 2017 at 06:03PM
from 酷 壳 – CoolShell http://ift.tt/2tZDRsi

VN:F [1.9.22_1171]
Rating: 0.0/5 (0 votes cast)
VN:F [1.9.22_1171]
Rating: 0 (from 0 votes)
Share

【酷壳】 Go语言的修饰器编程

之前写过一篇《Python修饰器的函数式编程》,这种模式很容易的可以把一些函数装配到另外一些函数上,可以让你的代码更为的简单,也可以让一些“小功能型”的代码复用性更高,让代码中的函数可以像乐高玩具那样自由地拼装。所以,一直以来,我对修饰器decoration这种编程模式情有独钟,这里写一篇Go语言相关的文章。

看过Python修饰器那篇文章的同学,一定知道这是一种函数式编程的玩法——用一个高阶函数来包装一下。多唠叨一句,关于函数式编程,可以参看我之前写过一篇文章《函数式编程》,这篇文章主要是,想通过从过程式编程的思维方式过渡到函数式编程的思维方式,从而带动更多的人玩函数式编程,所以,如果你想了解一下函数式编程,那么可以移步先阅读一下。所以,Go语言的修饰器编程模式,其实也就是函数式编程的模式。

不过,要提醒注意的是,Go 语言的“糖”不多,而且又是强类型的静态无虚拟机的语言,所以,无法做到像 Java 和 Python 那样的优雅的修饰器的代码。当然,也许是我才才疏学浅,如果你知道有更多的写法,请你一定告诉我。先谢过了。

简单示例

我们先来看一个示例:

继续阅读

VN:F [1.9.22_1171]
Rating: 0.0/5 (0 votes cast)
VN:F [1.9.22_1171]
Rating: 0 (from 0 votes)
Share

如何重构“箭头型”代码

本文主要起因是,一次在微博上和朋友关于嵌套好几层的if-else语句的代码重构的讨论(微博原文),在微博上大家有各式各样的问题和想法。按道理来说这些都是编程的基本功,似乎不太值得写一篇文章,不过我觉得很多东西可以从一个简单的东西出发,到达本质,所以,我觉得有必要在这里写一篇的文章。不一定全对,只希望得到更多的讨论,因为有了更深入的讨论才能进步。

文章有点长,我在文章最后会给出相关的思考和总结陈词,你可以跳到结尾。

所谓箭头型代码,基本上来说就是下面这个图片所示的情况。

那么,这样“箭头型”的代码有什么问题呢?看上去也挺好看的,有对称美。但是……

关于箭头型代码的问题有如下几个:

1)我的显示器不够宽,箭头型代码缩进太狠了,需要我来回拉水平滚动条,这让我在读代码的时候,相当的不舒服。

2)除了宽度外还有长度,有的代码的if-else里的if-else里的if-else的代码太多,读到中间你都不知道中间的代码是经过了什么样的层层检查才来到这里的。

总而言之,“箭头型代码”如果嵌套太多,代码太长的话,会相当容易让维护代码的人(包括自己)迷失在代码中,因为看到最内层的代码时,你已经不知道前面的那一层一层的条件判断是什么样的,代码是怎么运行到这里的,所以,箭头型代码是非常难以维护和Debug的

微博上的案例 与 Guard Clauses

OK,我们先来看一下微博上的那个示例,代码量如果再大一点,嵌套再多一点,你很容易会在条件中迷失掉(下面这个示例只是那个“大箭头”下的一个小箭头)

FOREACH(Ptr<WfExpression>, argument, node->arguments) {
    int index = manager->expressionResolvings.Keys().IndexOf(argument.Obj());
    if (index != -1) {
        auto type = manager->expressionResolvings.Values()[index].type;
        if (! types.Contains(type.Obj())) {
            types.Add(type.Obj());
            if (auto group = type->GetTypeDescriptor()->GetMethodGroupByName(L"CastResult", true)) {
                int count = group->GetMethodCount();
                for (int i = 0; i < count; i++) { auto method = group->GetMethod(i);
                    if (method->IsStatic()) {
                        if (method->GetParameterCount() == 1 &&
                            method->GetParameter(0)->GetType()->GetTypeDescriptor() == description::GetTypeDescriptor<DescriptableObject>() &&
                            method->GetReturn()->GetTypeDescriptor() != description::GetTypeDescriptor<void>() ) {
                            symbol->typeInfo = CopyTypeInfo(method->GetReturn());
                            break;
                        }
                    }
                }
            }
        }
    }
}

上面这段代码,可以把条件反过来写,然后就可以把箭头型的代码解掉了,重构的代码如下所示:

FOREACH(Ptr<WfExpression>, argument, node->arguments) {
    int index = manager->expressionResolvings.Keys().IndexOf(argument.Obj());
    if (index == -1)  continue;
    
    auto type = manager->expressionResolvings.Values()[index].type;
    if ( types.Contains(type.Obj()))  continue;
    
    types.Add(type.Obj());

    auto group = type->GetTypeDescriptor()->GetMethodGroupByName(L"CastResult", true);
    if  ( ! group ) continue;
 
    int count = group->GetMethodCount();
    for (int i = 0; i < count; i++) { auto method = group->GetMethod(i);
        if (! method->IsStatic()) continue;
       
        if ( method->GetParameterCount() == 1 &&
               method->GetParameter(0)->GetType()->GetTypeDescriptor() == description::GetTypeDescriptor<DescriptableObject>() &&
               method->GetReturn()->GetTypeDescriptor() != description::GetTypeDescriptor<void>() ) {
            symbol->typeInfo = CopyTypeInfo(method->GetReturn());
            break;
        }
    }
}

这种代码的重构方式叫 Guard Clauses

这里的思路其实就是,让出错的代码先返回,前面把所有的错误判断全判断掉,然后就剩下的就是正常的代码了

抽取成函数

微博上有些人说,continue 语句破坏了阅读代码的通畅,我觉得他们一定没有好好读这里面的代码,其实,我们可以看到,所有的 if 语句都是在判断是否出错的情况,所以,在维护代码的时候,你可以完全不理会这些 if 语句,因为都是出错处理的,而剩下的代码都是正常的功能代码,反而更容易阅读了。当然,一定有不是上面代码里的这种情况,那么,不用continue ,我们还能不能重构呢?

当然可以,抽成函数:

bool CopyMethodTypeInfo(auto &method, auto &group, auto &symbol) 
{
    if (! method->IsStatic()) {
        return true;
    }
    if ( method->GetParameterCount() == 1 &&
           method->GetParameter(0)->GetType()->GetTypeDescriptor() == description::GetTypeDescriptor<DescriptableObject>() &&
           method->GetReturn()->GetTypeDescriptor() != description::GetTypeDescriptor<void>() ) {
        symbol->typeInfo = CopyTypeInfo(method->GetReturn());
        return false;
    }
    return true;
}

void ExpressionResolvings(auto &manager, auto &argument, auto &symbol) 
{
    int index = manager->expressionResolvings.Keys().IndexOf(argument.Obj());
    if (index == -1) return;
    
    auto type = manager->expressionResolvings.Values()[index].type;
    if ( types.Contains(type.Obj())) return;

    types.Add(type.Obj());
    auto group = type->GetTypeDescriptor()->GetMethodGroupByName(L"CastResult", true);
    if  ( ! group ) return;

    int count = group->GetMethodCount();
    for (int i = 0; i < count; i++) { auto method = group->GetMethod(i);
        if ( ! CopyMethodTypeInfo(method, group, symbol) ) break;
    }
}

...
...
FOREACH(Ptr<WfExpression>, argument, node->arguments) {
    ExpressionResolvings(manager, arguments, symbol)
}
...
...

你发出现,抽成函数后,代码比之前变得更容易读和更容易维护了。不是吗?

有人说:“如果代码不共享,就不要抽取成函数!”,持有这个观点的人太死读书了。函数是代码的封装或是抽象,并不一定用来作代码共享使用,函数用于屏蔽细节,让其它代码耦合于接口而不是细节实现,这会让我们的代码更为简单,简单的东西都能让人易读也易维护。这才是函数的作用。

嵌套的 if 外的代码

微博上还有人问,原来的代码如果在各个 if 语句后还有要执行的代码,那么应该如何重构。比如下面这样的代码。

for(....) {
    do_before_cond1()
    if (cond1) {
        do_before_cond2();
        if (cond2) {
            do_before_cond3();
            if (cond3) {
                do_something();
            }
            do_after_cond3();
        }
        do_after_cond2();
    }
    do_after_cond1();
}

上面这段代码中的那些 do_after_condX() 是无论条件成功与否都要执行的。所以,我们拉平后的代码如下所示:

for(....) {
    do_before_cond1();
    if ( !cond1 ) {
        do_after_cond1();
        continue
    } 
    do_after_cond1();

    do_before_cond2();
    if ( !cond2 ) { 
        do_after_cond2();
        continue;
    }
    do_after_cond2();

    do_before_cond3();
    if ( !cond3 ) {
        do_after_cond3();
        continue;
    }
    do_after_cond3();

    do_something();  
}

你会发现,上面的 do_after_condX 出现了两份。根据 DRY 原则,我们保留一份,那么直接掉到 if 条件前就好了,如下所示:

for(....) {
    do_before_cond1();
    do_after_cond1();
    if ( !cond1 ) continue;
 
    do_before_cond2();
    do_after_cond2();
    if ( !cond2 ) continue;

    do_before_cond3();
    do_after_cond3();
    if ( !cond3 ) continue;

    do_something();  
}

此时,你会说,我靠,居然,改变了执行的顺序,把条件放到 do_after_condX() 后面去了。这会不会有问题啊?

其实,你再分析一下之前的代码,你会发现,本来,cond1 是判断 do_before_cond1() 是否出错的,如果有成功了,才会往下执行。而 do_after_cond1() 是无论如何都要执行的。从逻辑上来说,do_after_cond1()其实和do_before_cond1()的执行结果无关,而 cond1 却和是否去执行 do_before_cond2() 相关了。如果我把断行变成下面这样,反而代码逻辑更清楚了。

for(....) {

    do_before_cond1();
    do_after_cond1();


    if ( !cond1 ) continue;  // <-- cond1 成了是否做第二个语句块的条件
    do_before_cond2();
    do_after_cond2();

    if ( !cond2 ) continue; // <-- cond2 成了是否做第三个语句块的条件
    do_before_cond3();
    do_after_cond3();

    if ( !cond3 ) continue; //<-- cond3 成了是否做第四个语句块的条件
    do_something(); 
 
}

于是乎,在未来维护代码的时候,维护人一眼看上去就明白,代码在什么时候会执行到哪里。 这个时候,你会发现,把这些语句块抽成函数,代码会干净的更多,再重构一版:

bool do_func3() {
   do_before_cond2();
   do_after_cond2();
   return cond3;
}

bool do_func2() {
   do_before_cond2();
   do_after_cond2();
   return cond2;
}

bool do_func1() {
   do_before_cond1();
   do_after_cond1();
   return cond1;
}

// for-loop 你可以重构成这样
for (...) {
    bool cond = do_func1();
    if (cond) cond = do_func2();
    if (cond) cond = do_func3();
    if (cond) do_something();
}

// for-loop 也可以重构成这样
for (...) {
    if ( ! do_func1() ) continue;
    if ( ! do_func2() ) continue;
    if ( ! do_func3() ) continue;
    do_something();
}

上面,我给出了两个版本的for-loop,你喜欢哪个?我喜欢第二个。这个时候,因为for-loop里的代码非常简单,就算你不喜欢 continue ,这样的代码阅读成本已经很低了。

状态检查嵌套

接下来,我们再来看另一个示例。下面的代码的伪造了一个场景——把两个人拉到一个一对一的聊天室中,因为要检查双方的状态,所以,代码可能会写成了“箭头型”。

int ConnectPeer2Peer(Conn *pA, Conn* pB, Manager *manager)
{
    if ( pA->isConnected() ) {
        manager->Prepare(pA);
        if ( pB->isConnected() ) {
            manager->Prepare(pB);
            if ( manager->ConnectTogther(pA, pB) ) {
                pA->Write("connected");
                pB->Write("connected");
                return S_OK;
            }else{
                return S_ERROR;
            }

        }else {
            pA->Write("Peer is not Ready, waiting...");
            return S_RETRY;
        }
    }else{
        if ( pB->isConnected() ) {
            manager->Prepare();
            pB->Write("Peer is not Ready, waiting...");
            return S_RETRY;
        }else{
            pA->Close();
            pB->Close();
            return S_ERROR;
        }
    }
    //Shouldn't be here!
    return S_ERROR;
}

重构上面的代码,我们可以先分析一下上面的代码,说明了,上面的代码就是对 PeerA 和 PeerB 的两个状态 “连上”, “未连上” 做组合 “状态” (注:实际中的状态应该比这个还要复杂,可能还会有“断开”、“错误”……等等状态), 于是,我们可以把代码写成下面这样,合并上面的嵌套条件,对于每一种组合都做出判断。这样一来,逻辑就会非常的干净和清楚。

int ConnectPeer2Peer(Conn *pA, Conn* pB, Manager *manager)
{
    if ( pA->isConnected() ) {
        manager->Prepare(pA);
    }

    if ( pB->isConnected() ) {
        manager->Prepare(pB);
    }

    // pA = YES && pB = NO
    if (pA->isConnected() && ! pB->isConnected()  ) {
        pA->Write("Peer is not Ready, waiting");
        return S_RETRY;
    // pA = NO && pB = YES
    }else if ( !pA->isConnected() && pB->isConnected() ) {
        pB->Write("Peer is not Ready, waiting");
        return S_RETRY;
    // pA = YES && pB = YES
    }else if (pA->isConnected() && pB->isConnected()  ) {
        if ( ! manager->ConnectTogther(pA, pB) ) {
            return S_ERROR;
        }
        pA->Write("connected");
        pB->Write("connected");
        return S_OK;
    }

    // pA = NO, pB = NO
    pA->Close();
    pB->Close();
    return S_ERROR;
}

延伸思考

对于 if-else 语句来说,一般来说,就是检查两件事:错误状态

检查错误

对于检查错误来说,使用 Guard Clauses 会是一种标准解,但我们还需要注意下面几件事:

1)当然,出现错误的时候,还会出现需要释放资源的情况。你可以使用 goto fail; 这样的方式,但是最优雅的方式应该是C++面向对象式的 RAII 方式。

2)以错误码返回是一种比较简单的方式,这种方式有很一些问题,比如,如果错误码太多,判断出错的代码会非常复杂,另外,正常的代码和错误的代码会混在一起,影响可读性。所以,在更为高组的语言中,使用 try-catch 异常捕捉的方式,会让代码更为易读一些。

检查状态

对于检查状态来说,实际中一定有更为复杂的情况,比如下面几种情况:

1)像TCP协议中的两端的状态变化。

2)像shell各个命令的命令选项的各种组合。

3)像游戏中的状态变化(一棵非常复杂的状态树)。

4)像语法分析那样的状态变化。

对于这些复杂的状态变化,其本上来说,你需要先定义一个状态机,或是一个子状态的组合状态的查询表,或是一个状态查询分析树。

写代码时,代码的运行中的控制状态或业务状态是会让你的代码流程变得混乱的一个重要原因,重构“箭头型”代码的一个很重要的工作就是重新梳理和描述这些状态的变迁关系

总结

好了,下面总结一下,把“箭头型”代码重构掉的几个手段如下:

1)使用 Guard Clauses 。 尽可能的让出错的先返回, 这样后面就会得到干净的代码。

2)把条件中的语句块抽取成函数。 有人说:“如果代码不共享,就不要抽取成函数!”,持有这个观点的人太死读书了。函数是代码的封装或是抽象,并不一定用来作代码共享使用,函数用于屏蔽细节,让其它代码耦合于接口而不是细节实现,这会让我们的代码更为简单,简单的东西都能让人易读也易维护,写出让人易读易维护的代码才是重构代码的初衷

3)对于出错处理,使用try-catch异常处理和RAII机制。返回码的出错处理有很多问题,比如:A) 返回码可以被忽略,B) 出错处理的代码和正常处理的代码混在一起,C) 造成函数接口污染,比如像atoi()这种错误码和返回值共用的糟糕的函数。

4)对于多个状态的判断和组合,如果复杂了,可以使用“组合状态表”,或是状态机加Observer的状态订阅的设计模式。这样的代码即解了耦,也干净简单,同样有很强的扩展性。

5) 重构“箭头型”代码其实是在帮你重新梳理所有的代码和逻辑,这个过程非常值得为之付出。重新整思路去想尽一切办法简化代码的过程本身就可以让人成长。

(全文完)


关注CoolShell微信公众账号可以在手机端搜索文章

(转载本站文章请注明作者和出处 酷 壳 – CoolShell ,请勿用于任何商业用途)

——=== 访问 酷壳404页面 寻找遗失儿童。 ===——

陈皓 April 05, 2017 at 06:07PM
from 酷 壳 – CoolShell http://ift.tt/2nJ6B2n

VN:F [1.9.22_1171]
Rating: 0.0/5 (0 votes cast)
VN:F [1.9.22_1171]
Rating: 0 (from 0 votes)
Share

AWS 的 S3 故障回顾和思考

Gitlab的误删除数据事件没几天,“不沉航母” AWS S3 (Simple Storage Service)几天前也“沉”了4个小时,墙外的半个互联网也跟着挂了。如约,按 AWS 惯例,AWS今天给出了一个简单的故障报告《Summary of the Amazon S3 Service Disruption in the Northern Virginia (US-EAST-1) Region》。这个故障和简单来说和Gitlab一样,也是人员误操作。先简单的说一下这份报中说了什么。

故障原因

简单来说,这天,有一个 AWS 工程师在调查 Northern Virginia (US-EAST-1) Region 上 S3 的一个和账务系统相关的问题,这个问题是S3的账务系统变慢了(我估计这个故障在Amazon里可能是Sev2级,Sev2级的故障在Amazon算是比较大的故障,需要很快解决),Oncall的开发工程师(注:Amazon的运维都是由开发工程师来干的,所以Amazon内部嬉称SDE-Software Developer Engineer 为 Someone Do Everything)想移除一个账务系统里的一个子系统下的一些少量的服务器(估计这些服务器上有问题,所以想移掉后重新部署),结果呢,有一条命令搞错了,导致了移除了大量的S3的控制系统。包括两个很重要的子系统:

继续阅读

VN:F [1.9.22_1171]
Rating: 0.0/5 (0 votes cast)
VN:F [1.9.22_1171]
Rating: 0 (from 0 votes)
Share

从Gitlab误删除数据库想到的

昨天,Gitlab.com发生了一个大事,某同学误删了数据库,这个事看似是个低级错误,不过,因为Gitlab把整个过程的细节都全部暴露出来了,所以,可以看到很多东西,而对于类似这样的事情,我自己以前也干过,而在最近的两公司中我也见过(Amazon中见过一次,阿里中见过至少四次),正好通过这个事来说说一下自己的一些感想和观点吧。我先放个观点:你觉得有备份系统就不会丢数据了吗?

事件回顾

整个事件的回顾Gitlab.com在第一时间就放到了Google Doc上,事后,又发了一篇Blog来说明这个事,在这里,我简单的回顾一下这个事件的过程。

首先,一个叫YP的同学在给gitlab的数据库做一些负载均衡的工作,在做这个工作时的时候突发了一个情况,Gitlab被DDoS攻击,数据库的使用飙高,在block完攻击者的IP后,发现有个staging的数据库(db2.staging)已经落后生产库4GB的数据,于是YP同学在Fix这个staging库的同步问题的时候,发现db2.staging有各种问题都和主库无法同步,在这个时候,YP同学已经工作的很晚了,在尝试过很多方法后,发现db2.staging都hang在那里,无法同步,于是他想把db2.staging的数据库删除了,这样全新启动一个新的复制,结果呢,删除数据库的命令错误的敲在了生产环境上(db1.cluster),结果导致整个生产数据库被误删除。(陈皓注:这个失败基本上就是 “工作时间过长” + “在多数终端窗口中切换中迷失掉了”

继续阅读

VN:F [1.9.22_1171]
Rating: 0.0/5 (0 votes cast)
VN:F [1.9.22_1171]
Rating: 0 (from 0 votes)
Share

Chrome开发者工具中的小技巧

Chrome的开发者工具是个很强大的东西,相信程序员们都不会陌生,不过有些小功能可能并不为大众所知,所以,写下这篇文章,有的功能可能会比较实用,有的则不一定,也欢迎大家交流。

话不多话,我们开始。

代码格式化

有很多css/js的代码都会被 minify 掉,你可以点击代码窗口左下角的那个 { }  标签,chrome会帮你给格式化掉。

强制DOM状态

有些HTML的DOM是有状态的,比如<a> 标签,其会有 active,hover, focus,visited这些状态,有时候,我们的CSS会来定关不同状态的样式,在分析网页查看网页上DOM的CSS样式时,我们可以点击CSS样式上的 :hov 这个小按钮来强制这个DOM的状态。

继续阅读

VN:F [1.9.22_1171]
Rating: 0.0/5 (0 votes cast)
VN:F [1.9.22_1171]
Rating: 0 (from 0 votes)
Share