为什么你学不会递归?告别递归,谈谈我的一些经验
可能很多人在大一的时候,就已经接触了递归了,不过,我敢保证很多人初学者刚开始接触递归的时候,是一脸懵逼的,我当初也是,给我的感觉就是,递归太神奇了!
可能也有一大部分人知道递归,也能看的懂递归,但在实际做题过程中,却不知道怎么使用,有时候还容易被递归给搞晕。也有好几个人来问我有没有快速掌握递归的捷径啊。说实话,哪来那么多捷径啊,不过,我还是想写一篇文章,谈谈我的一些经验,或许,能够给你带来一些帮助。
为了兼顾初学者,我会从最简单的题讲起!
递归的三大要素
第一要素:明确你这个函数想要干什么
对于递归,我觉得很重要的一个事就是,这个函数的功能是什么,他要完成什么样的一件事,而这个,是完全由你自己来定义的。也就是说,我们先不管函数里面的代码什么,而是要先明白,你这个函数是要用来干什么。
例如,我定义了一个函数
// 算 n 的阶乘(假设n不为0)
int f(int n){
}
这个函数的功能是算 n 的阶乘。好了,我们已经定义了一个函数,并且定义了它的功能是什么,接下来我们看第二要素。
第二要素:寻找递归结束条件
所谓递归,就是会在函数内部代码中,调用这个函数本身,所以,我们必须要找出递归的结束条件,不然的话,会一直调用自己,进入无底洞。也就是说,我们需要找出当参数为啥时,递归结束,之后直接把结果返回,请注意,这个时候我们必须能根据这个参数的值,能够直接知道函数的结果是什么。
例如,上面那个例子,当 n = 1 时,那你应该能够直接知道 f(n) 是啥吧?此时,f(1) = 1。完善我们函数内部的代码,把第二要素加进代码里面,如下
// 算 n 的阶乘(假设n不为0)
int f(int n){
if(n == 1){
return 1;
}
}
有人可能会说,当 n = 2 时,那我们可以直接知道 f(n) 等于多少啊,那我可以把 n = 2 作为递归的结束条件吗?
当然可以,只要你觉得参数是什么时,你能够直接知道函数的结果,那么你就可以把这个参数作为结束的条件,所以下面这段代码也是可以的。
// 算 n 的阶乘(假设n>=2)
int f(int n){
if(n == 2){
return 2;
}
}
注意我代码里面写的注释,假设 n >= 2,因为如果 n = 1时,会被漏掉,当 n <= 2时,f(n) = n,所以为了更加严谨,我们可以写成这样:
// 算 n 的阶乘(假设n不为0)
int f(int n){
if(n <= 2){
return n;
}
}
第三要素:找出函数的等价关系式
第三要素就是,我们要不断缩小参数的范围,缩小之后,我们可以通过一些辅助的变量或者操作,使原函数的结果不变。
例如,f(n) 这个范围比较大,我们可以让 f(n) = n * f(n-1)。这样,范围就由 n 变成了 n-1 了,范围变小了,并且为了原函数f(n) 不变,我们需要让 f(n-1) 乘以 n。
说白了,就是要找到原函数的一个等价关系式,f(n) 的等价关系式为 n * f(n-1),即
f(n) = n * f(n-1)。
这个等价关系式的寻找,可以说是最难的一步了,如果你不大懂也没关系,因为你不是天才,你还需要多接触几道题,我会在接下来的文章中,找 10 道递归题,让你慢慢熟悉起来。
找出了这个等价,继续完善我们的代码,我们把这个等价式写进函数里。如下:
// 算 n 的阶乘(假设n不为0)
int f(int n){
if(n <= 2){
return n;
}
// 把 f(n) 的等价操作写进去
return f(n-1) * n;
}
至此,递归三要素已经都写进代码里了,所以这个 f(n) 功能的内部代码我们已经写好了。
这就是递归最重要的三要素,每次做递归的时候,你就强迫自己试着去寻找这三个要素。
还是不懂?没关系,我再按照这个模式讲一些题。
有些有点小基础的可能觉得我写的太简单了,没耐心看?少侠,请继续看,我下面还会讲如何优化递归。当然,大佬请随意,可以直接拉动最下面留言给我一些建议,万分感谢!
案例1:斐波那契数列
斐波那契数列的是这样一个数列:1、1、2、3、5、8、13、21、34....,即第一项 f(1) = 1,第二项 f(2) = 1.....,第 n 项目为 f(n) = f(n-1) + f(n-2)。求第 n 项的值是多少。
1、第一递归函数功能
假设 f(n) 的功能是求第 n 项的值,代码如下:
int f(int n){
}
2、找出递归结束的条件
显然,当 n = 1 或者 n = 2 ,我们可以轻易着知道结果 f(1) = f(2) = 1。所以递归结束条件可以为 n <= 2。代码如下:
int f(int n){
if(n <= 2){
return 1;
}
}
第三要素:找出函数的等价关系式
题目已经把等价关系式给我们了,所以我们很容易就能够知道 f(n) = f(n-1) + f(n-2)。我说过,等价关系式是最难找的一个,而这个题目却把关系式给我们了,这也太容易,好吧,我这是为了兼顾几乎零基础的读者。
所以最终代码如下:
int f(int n){
// 1.先写递归结束条件
if(n <= 2){
return 1;
}
// 2.接着写等价关系式
return f(n-1) + f(n - 2);
}
搞定,是不是很简单?
零基础的可能还是不大懂,没关系,之后慢慢按照这个模式练习!好吧,有大佬可能在吐槽太简单了。
案例2:小青蛙跳台阶
一只青蛙一次可以跳上1级台阶,也可以跳上2级。求该青蛙跳上一个n级的台阶总共有多少种跳法。
1、第一递归函数功能
假设 f(n) 的功能是求青蛙跳上一个n级的台阶总共有多少种跳法,代码如下:
int f(int n){
}
2、找出递归结束的条件
我说了,求递归结束的条件,你直接把 n 压缩到很小很小就行了,因为 n 越小,我们就越容易直观着算出 f(n) 的多少,所以当 n = 1时,你知道 f(1) 为多少吧?够直观吧?即 f(1) = 1。代码如下:
int f(int n){
if(n == 1){
return 1;
}
}
第三要素:找出函数的等价关系式
每次跳的时候,小青蛙可以跳一个台阶,也可以跳两个台阶,也就是说,每次跳的时候,小青蛙有两种跳法。
第一种跳法:第一次我跳了一个台阶,那么还剩下n-1个台阶还没跳,剩下的n-1个台阶的跳法有f(n-1)种。
第二种跳法:第一次跳了两个台阶,那么还剩下n-2个台阶还没,剩下的n-2个台阶的跳法有f(n-2)种。
所以,小青蛙的全部跳法就是这两种跳法之和了,即 f(n) = f(n-1) + f(n-2)。至此,等价关系式就求出来了。于是写出代码:
int f(int n){
if(n == 1){
return 1;
}
ruturn f(n-1) + f(n-2);
}
大家觉得上面的代码对不对?
答是不大对,当 n = 2 时,显然会有 f(2) = f(1) + f(0)。我们知道,f(0) = 0,按道理是递归结束,不用继续往下调用的,但我们上面的代码逻辑中,会继续调用 f(0) = f(-1) + f(-2)。这会导致无限调用,进入死循环。
这也是我要和你们说的,关于递归结束条件是否够严谨问题,有很多人在使用递归的时候,由于结束条件不够严谨,导致出现死循环。也就是说,当我们在第二步找出了一个递归结束条件的时候,可以把结束条件写进代码,然后进行第三步,但是请注意,当我们第三步找出等价函数之后,还得再返回去第二步,根据第三步函数的调用关系,会不会出现一些漏掉的结束条件。就像上面,f(n-2)这个函数的调用,有可能出现 f(0) 的情况,导致死循环,所以我们把它补上。代码如下:
int f(int n){
//f(0) = 0,f(1) = 1,等价于 n<=1时,f(n) = n。
if(n <= 1){
return n;
}
ruturn f(n-1) + f(n-2);
}
有人可能会说,我不知道我的结束条件有没有漏掉怎么办?别怕,多练几道就知道怎么办了。
看到这里有人可能要吐槽了,这两道题也太容易了吧??能不能被这么敷衍。少侠,别走啊,下面出道难一点的。
下面其实也不难了,就比上面的题目难一点点而已,特别是第三步等价的寻找。
案例3:反转单链表。
反转单链表。例如链表为:1->2->3->4。反转后为 4->3->2->1
链表的节点定义如下:
class Node{
int date;
Node next;
}
虽然是 Java语言,但就算你没学过 Java,我觉得也是影响不大,能看懂。
还是老套路,三要素一步一步来。
1、定义递归函数功能
假设函数 reverseList(head) 的功能是反转但链表,其中 head 表示链表的头节点。代码如下:
Node reverseList(Node head){
}
2. 寻找结束条件
当链表只有一个节点,或者如果是空表的话,你应该知道结果吧?直接啥也不用干,直接把 head 返回呗。代码如下:
Node reverseList(Node head){
if(head == null || head.next == null){
return head;
}
}
3. 寻找等价关系
这个的等价关系不像 n 是个数值那样,比较容易寻找。但是我告诉你,它的等价条件中,一定是范围不断在缩小,对于链表来说,就是链表的节点个数不断在变小,所以,如果你实在找不出,你就先对 reverseList(head.next) 递归走一遍,看看结果是咋样的。例如链表节点如下
我们就缩小范围,先对 2->3->4递归下试试,即代码如下
Node reverseList(Node head){
if(head == null || head.next == null){
return head;
}
// 我们先把递归的结果保存起来,先不返回,因为我们还不清楚这样递归是对还是错。,
Node newList = reverseList(head.next);
}
我们在第一步的时候,就已经定义了 reverseLis t函数的功能可以把一个单链表反转,所以,我们对 2->3->4反转之后的结果应该是这样:
我们把 2->3->4 递归成 4->3->2。不过,1 这个节点我们并没有去碰它,所以 1 的 next 节点仍然是连接这 2。
接下来呢?该怎么办?
其实,接下来就简单了,我们接下来只需要把节点 2 的 next 指向 1,然后把 1 的 next 指向 null,不就行了?,即通过改变 newList 链表之后的结果如下:
也就是说,reverseList(head) 等价于 ** reverseList(head.next)** + 改变一下1,2两个节点的指向。好了,等价关系找出来了,代码如下(有详细的解释):
//用递归的方法反转链表
public static Node reverseList2(Node head){
// 1.递归结束条件
if (head == null || head.next == null) {
return head;
}
// 递归反转 子链表
Node newList = reverseList2(head.next);
// 改变 1,2节点的指向。
// 通过 head.next获取节点2
Node t1 = head.next;
// 让 2 的 next 指向 2
t1.next = head;
// 1 的 next 指向 null.
head.next = null;
// 把调整之后的链表返回。
return newList;
}
这道题的第三步看的很懵?正常,因为你做的太少了,可能没有想到还可以这样,多练几道就可以了。但是,我希望通过这三道题,给了你以后用递归做题时的一些思路,你以后做题可以按照我这个模式去想。通过一篇文章是不可能掌握递归的,还得多练,我相信,只要你认真看我的这篇文章,多看几次,一定能找到一些思路!!
我已经强调了好多次,多练几道了,所以呢,后面我也会找大概 10 道递归的练习题供大家学习,不过,我找的可能会有一定的难度。不会像今天这样,比较简单,所以呢,初学者还得自己多去找题练练,相信我,掌握了递归,你的思维抽象能力会更强!
接下来我讲讲有关递归的一些优化。
有关递归的一些优化思路
1. 考虑是否重复计算
告诉你吧,如果你使用递归的时候不进行优化,是有非常非常非常多的子问题被重复计算的。
啥是子问题? f(n-1),f(n-2)....就是 f(n) 的子问题了。
例如对于案例2那道题,f(n) = f(n-1) + f(n-2)。递归调用的状态图如下:
看到没有,递归计算的时候,重复计算了两次 f(5),五次 f(4)。。。。这是非常恐怖的,n 越大,重复计算的就越多,所以我们必须进行优化。
如何优化?一般我们可以把我们计算的结果保证起来,例如把 f(4) 的计算结果保证起来,当再次要计算 f(4) 的时候,我们先判断一下,之前是否计算过,如果计算过,直接把 f(4) 的结果取出来就可以了,没有计算过的话,再递归计算。
用什么保存呢?可以用数组或者 HashMap 保存,我们用数组来保存把,把 n 作为我们的数组下标,f(n) 作为值,例如 arr[n] = f(n)。f(n) 还没有计算过的时候,我们让 arr[n] 等于一个特殊值,例如 arr[n] = -1。
当我们要判断的时候,如果 arr[n] = -1,则证明 f(n) 没有计算过,否则, f(n) 就已经计算过了,且 f(n) = arr[n]。直接把值取出来就行了。代码如下:
// 我们实现假定 arr 数组已经初始化好的了。
int f(int n){
if(n <= 1){
return n;
}
//先判断有没计算过
if(arr[n] != -1){
//计算过,直接返回
return arr[n];
}else{
// 没有计算过,递归计算,并且把结果保存到 arr数组里
arr[n] = f(n-1) + f(n-1);
reutrn arr[n];
}
}
也就是说,使用递归的时候,必要
须要考虑有没有重复计算,如果重复计算了,一定要把计算过的状态保存起来。
2. 考虑是否可以自底向上
对于递归的问题,我们一般都是从上往下递归的,直到递归到最底,再一层一层着把值返回。
不过,有时候当 n 比较大的时候,例如当 n = 10000 时,那么必须要往下递归10000层直到 n <=1 才将结果慢慢返回,如果n太大的话,可能栈空间会不够用。
对于这种情况,其实我们是可以考虑自底向上的做法的。例如我知道
f(1) = 1;
f(2) = 2;
那么我们就可以推出 f(3) = f(2) + f(1) = 3。从而可以推出f(4),f(5)等直到f(n)。因此,我们可以考虑使用自底向上的方法来取代递归,代码如下:
public int f(int n) {
if(n <= 2)
return n;
int f1 = 1;
int f2 = 2;
int sum = 0;
for (int i = 3; i <= n; i++) {
sum = f1 + f2;
f1 = f2;
f2 = sum;
}
return sum;
}
这种方法,其实也被称之为递推。
最后总结
其实,递归不一定总是从上往下,也是有很多是从下往上的,例如 n = 1 开始,一直递归到 n = 1000,例如一些排序组合。对于这种从下往上的,也是有对应的优化技巧,不过,我就先不写了,后面再慢慢写。这篇文章写了很久了,脖子有点受不了了,,,,颈椎病?害怕。。。。
说实话,对于递归这种比较抽象的思想,要把他讲明白,特别是讲给初学者听,还是挺难的,这也是我这篇文章用了很长时间的原因,不过,只要能让你们看完,有所收获,我觉得值得!有些人可能觉得讲的有点简单,没事,我后面会找一些不怎么简单的题。最后如果觉得不错,还请给我转发 or 点赞一波!
关于集合中一些常考的知识点总结
本章主要总结了集合的一些基础但有重点的知识点,例如他们的底层数据结构以及集合之间的区别,其中 HashMap 最为重点。
集合
Java的集合框架中可以分为两大类:第一类是按照单个元素存储的 Collection 集合,其中 Set, List, Queue 都实现了 Collection 接口。第二类是按照 Key-Value 存储的 Map 集合。
List
List常量的两个子类分别是 ArrayList 和 LinkedList 这两个集合。
(1)、ArrayList 的特点。
A. ArrayList 底层数据结构是数组,数组的特点就是可以快速随机访问,直接根据下标定位,缺点是插入和删除速度比较慢,需要移动元素。
B. ArrayList 每次扩容之后的大小为之前的 1.5 倍。默认初始容量大小为 10。
(2)、LinkedList 的特点
LinkedList 底层数据结构是双向链表,链表的特点就是随机访问速度慢,必须一个一个遍历,不能直接通过下标定位,不过在插入、删除方面速度就比较快。不过由于链表是内存分配不要求连续,内存的利用率比较高。
LinkedList 还实现了另外一个接口Deque,即 double-ended queue,使得 LinkedList 同时具有队列和栈的特性。
(3)、vector 的特点
vector 和 ArrayList 基本一样,不过 Vector 是线程安全的,而 ArrayList 是线程不安全的,
ArrayList 和 LinkedList 都是线程不安全的集合。
Map
Map 是一种 key-value 的集合,其常用的集合实现类有 HashMap, HashTable, TreeMap。
(1)、HashMap(重重点)
HashMap 的底层数据结构是 链表 + 数组,如果对他的底层结构不大懂的可以看我之前写的一篇文章:HashMap的存取原理你知道多少
HashMap 在进行 put 操作时,允许 key 和 value 为 null,且是线程不安全的,所以 HashMap 的性能非常好,只不过在多线程的环境下使用,需要给他加上对应的锁
重点数据:HashMap 的默认容量为 capacity = 16, 默认扩容因子 loadFactor = 0.75,至于扩容因子有什么用,下面会涉及到。
不过需要注意的是,HashMap 内部用变量 threshold 变量来表示 HashMap 中能放入的元素个数,且在 threshold 不超过最大值前提下, threshold = loadFactor * capacity。
也就是说,当元素的个数达到 threshold 之后,就会触发 HashMap 的扩容,而不是达到 capacity 才触发扩容。每次扩容之后的容量为之前的 2 倍。
而 ArrayList 则是元素达到 capacity 时才触发扩容。
还有一个需要注意的是,HashMap 容量并不会在 new 的时候分配,而是在第一次 put 的时候才完成创建的。
public V put(K key, V value){
if(table == EMPTY_TABLE){
// 初始化
inflateTable(threshold);
}
}
默认初始化容量大小 capacity = 16,如果我们在初始化的时候指定了容量的大小 initialCapacity,则会先计算出比 initialCapacity 大的 2 的幂存入 threshold,并且也会把初始化容量置为 capacity = threshold。例如当我们指定初始容量 initialCapacity = 26 的话,则 threshold = 32, capacity = 32。
(2)、HashTable的特点
a. HashTable 和 HashMap 在工作原理上几乎一样,不过 HashTable 是线程安全的,如图
不过锁是直接加在方法外面,所以在多线程环境下,性能极差。
不过在多线程的环境下,我们优先使用 ConcurrentHashMap 集合,这个集合在工作原理上也几乎和前面两个一样,但它是线程安全的,并且不像 HashTable 那样,把整个方法都给加锁了,而是把方法里面的关键代码加锁了,如图:
所以在处理速度上比较快。
b. HashTable 不允许 key 和 value 为 null。
c. HashMap 的迭代器是 fail-fast 机制(快速失败机制), 而 HashTable 则是 fail-safe 机制(快速安全),如果不知道 fail-fast 与 fail-safe 的,可以看我之前写 的一篇文章:谈谈fail-fast与fail-safe
(3)、LinkedHashMap 的特点
LinkedHashMap 是 HashMap 的一个子类,我们知道 HashMap是在插入的时候是根据哈希码来选择位置的,是无序的,而 LinkedHashMap 在插入的时候具有双向链表的特性,内部使用链表维护了插入的顺序,能够保证输出的顺序和输入时的相同。
LinkedHashMap 也是线程不安全的,并且允许 key-value 为 null。
(4)、TreeMap
TreesMap 的底层数据结构是红黑树,和 HashMap 不同,它的 get, put, remove 操作都是 O(logn) 的时间复杂度,并且元素是有序的。
同样,TreeMap 也是线程不安全的。
Set
Set 是一种不允许出现重复元素的集合类型,常用的三个实现类是 HashSet、TreeSet 和 LinkedHashSet。
(1)、HashSet
HashSet 实际上是用 HashMap 来实现的,如图
只是 Value 被固定为一个静态对象
使用 Key 来保证集合元素的唯一性,不过它不保证集合元素的顺序。
(2)、TreeSet
TreeSet 也是用 TreeMap 来实现的,底层为树结构,TreeSet 则能够保证集合元素是有序的。
(3)、LinkedHashSet
LinkedHashSet 继承 HashSet,具有 HashSet 优点,不过与 HashSet 不同的是,LinkedHashSet 内部使用了链表来维护元素的插入顺序。
这些知识点如果都能自己打开源码配合看一下,很多有关集合的面试题就可以应付了。
.net辗转java系列(一)视野
本文目的在于扩展你我视野,求各位大神帮忙补充下表格中的内容,特别是Java的相关内容。
下面的文字纯是为了凑足150个字。
本人作为一名普通的.net程序员,也快混了十年了。在.net方面的知识面较广,但是深度严重不够。我们从最下层次的开发说起:
1、 嵌入系统wince开发(基于.net compack framwork, Visual Studio 2008之后就不支持了)
2、 上位机开发(Winform为主,主要是硬件信号的收集)
3、 桌面程序开发(Winform、WPF、UWP)
4、 Web开发(WebForm、MVC)
5、 服务类(一般处理程序、Web Service、WCF、WebAPI)
6、 云技术(.net core相关被neter热捧中)
从来都知道自己不是什么大牛。只因在实业单位中做开发,难免经常一个人承担很多种角色:项目经理+需求+产品+UI+前端+后台+DB+面试官等等。最近迫于无奈,被要求转Java,转Java前希望对Java整个生态有个全盘的了解。
看到张大神的评论,补充了点内容。其实关于“视野” 这个命题确实是比较泛,还有很多东西我自己也知道,没列出来而已。这帖子可以一直更新!不过这一系列得继续前进。
.net辗转java系列之视野 | ||||
.net系 | java系 | 其它 | ||
语言 | ||||
C# | Java | |||
框架 | ||||
.net Framework Standard | java se | |||
.net core | java ee | |||
jave me | ||||
Java SE Subscription | ||||
.net compack framwork | Java Embedded | |||
Java TV | ||||
Java Card | ||||
Java Magazine | ||||
桌面 | ||||
winform | javax.swing | |||
wpf | ||||
uwp | ||||
windows服务 | JavaService | |||
H5桌面 | ||||
Electron | Electron.net | |||
Web | ||||
webform | ||||
asp.net mvc | spring mvc | |||
Blazor | ||||
spring.net | spring | Spring Data Spring MVC Spring Boot Spring Cloud Spring Cloud Data Flow Spring Batch Spring Security Spring AMQP |
||
服务 | ||||
一般处理程序 | Servlet | |||
web service | Servlet | |||
wcf | Servlet | |||
web api | Servlet | |||
移动端 | ||||
android | Xamarin | android | ||
其他 | ||||
游戏开发 | ||||
Unity3 | ||||
机器学习 | ||||
ML.NET | ||||
IOT | ||||
Windows 10 IoT | Java Embedded for IoT | |||
IDE | ||||
idea | Rider | IntelliJ IDEA | ||
Visual Studio Code | C# for Visual Studio Code | Language support for Java | ||
Visual Studio | ||||
Eclipse aCute | Eclipse | |||
MyEclipse | ||||
包管理 | ||||
Nuget | Apache Ant | |||
Apache Maven | ||||
Gradle | ||||
应用服务器 | ||||
Web服务器 | ||||
IIS | nginx+tomcat | |||
Http.sys | ||||
KestrelServer | ||||
WebListenerServer | ||||
文档 | ||||
Sandcastle | ||||
DocFX | ||||
swagger | Swashbuckle | |||
模板 | ||||
模板 | ||||
NVelocity | Velocity | |||
T4 | ||||
RazorEngine | ||||
JNTemplate | ||||
VTemplate | ||||
项目模板 | ||||
SideWaffle | ||||
实现 | ||||
IOC | ||||
AutoFac | ||||
Castle Windsor | ||||
MEF | ||||
Ninject | ||||
StructureMap | ||||
Unity | ||||
AOP | ||||
PostSharp | ||||
Mr.Advice | ||||
校验 | ||||
System.ComponentModel.DataAnnotations | ||||
FluentValidation | ||||
文件处理 | ||||
TemplateEngine.Docx | ||||
iTextSharp | ||||
PDFsharp | ||||
DocX | ||||
NOPI | ||||
Aspose | ||||
Html(Microsoft.mshtml.dll、Winista.HtmlParser.dll 和 HtmlAgilityPack.dll) | ||||
CSVHelper | ||||
ExcelDataReader | ||||
Scryber | ||||
LinqToExcel | ||||
DB | ||||
ORM | ||||
EntityFrameWork | JPA | |||
Dapper.net | ||||
Mybatis.net | Mybatis | |||
NHibernate | Hibernate | |||
PetaPoco | ||||
FluentData | ||||
ServiceStack.OrmLite | ||||
EmitMapper | ||||
Deft | ||||
Chloe.ORM | ||||
CYQ.Data | ||||
TierDeveloper | ||||
Lightspeed | ||||
LLBLGen | ||||
Simple.Data,massive | ||||
SubSonic | ||||
NoSql | ||||
Redis | redis-desktop-manager | |||
ServiceStack.Redis | ||||
StackExchange.Redis | ||||
NewLife.Redis | ||||
csredis | ||||
MongoDB | ||||
mongo-csharp-driver | ||||
通讯 | ||||
socket | ||||
Apache Mina | ||||
Supersocket | netty | |||
Cowboy.Sockets | netty | |||
DotNetty | netty | |||
WebSocket | SingalR | netty-socketio | ||
MQTT | MQTTnet | |||
Modbus | NModbus4 | |||
任务调度 | ||||
quartz.net | quartz | |||
Hangfire | ||||
Azure WebJobs | ||||
FluentScheduler | ||||
elastic-job | ||||
XXL-JOB | ||||
身份认证 | ||||
Forms验证 | ||||
Passport验证 | ||||
windows身份验证 | ||||
claims-based认证 | ||||
IdentityServer4 | Apache Shiro | |||
单点登录(Single Sign-On,缩写为SSO) | ||||
LDAP | ||||
CAS(Central Authentication Service) | ||||
OAuth 2.0 | DotNetOpenAuth | |||
双因素认证(2FA) | ||||
日志 | ||||
log4net | log4j | |||
Log4Net-Mongo | ||||
Log4j 2 | ||||
ExceptionLess | ||||
NLog | ||||
Serilog | ||||
Commons Logging | ||||
Slf4j | ||||
Logback | ||||
Jul | ||||
全文检索 | ||||
Solr | ||||
Elasticsearch.Net | Elasticsearch | |||
NEST | ||||
Lucene.Net | Lucene | |||
消息队列 | ||||
RabbitMQ(Erlang) | ||||
EasyNetQ | ||||
rabbitmq-dotnet-client | ||||
ActiveMQ | ||||
ZeroMQ(C语言) | NetMQ | |||
Equeue | ||||
Disque | Disque.Net | |||
流程引擎 | ||||
E8.net BPM | √ | |||
flowportal | ||||
G2 BPM | ||||
IBM BPM | ||||
Joget BPM | ||||
K2 BPM | √ | |||
Procwise BPM | ||||
RDIFramework.NET | ||||
奥哲H3 BPM | ||||
安码Ultimus BPM | ||||
炎黄盈动AWS BPM | ||||
起步X5 BPM | ||||
CCFlow | √ | |||
DragFlow | √ | |||
NetBPM | √ | |||
Roadflow | √ | |||
Windows Workflow Foundation | √ | |||
WorkflowEngine.NET | √ | |||
同步 | ||||
SyncML | ||||
SyncFramework | ||||
后台开发框架 | ||||
Hplus | ||||
ymnets | ||||
ABP | ||||
Aries | ||||
Magicodes.Admin | ||||
X-admin | ||||
微信 | ||||
Senparc.Weixin | weixin4j | |||
WeixinSDK.net | ||||
大数据 | ||||
Hadoop | HDInsight | |||
Apache Spark | ||||
WhereHows | LinkedIn数据中心工具 | |||
Druid | 一个拥有大数据实时查询和分析的高容错、高性能开源分布式系统(阿里) | |||
Tensor Flow | 开源机器学习框架 | |||
StreamSets | 侧重数据集成、数据加工流程构建的平台 | |||
Apache | ||||
Apache Kafka(Java) | Rdkafka | Kafka | ||
Apache Flink | 分布式处理引擎和框架 | |||
Apache Samza | 分布式流处理框架 | |||
Apache Spark | Mobius | |||
分布式 | ||||
分布式事务 | ||||
MS DTC | ||||
.NET Core CAP | ||||
分布式缓存 | ||||
Microsoft Velocity | ||||
Actor模型同步框架 | ||||
Akka(Scala) | Akka.NET | |||
Orleans | ||||
分布式分析系统 | ||||
Confluo(C++) | ||||
分布式云服务 | ||||
Azure微软系 | ||||
Service Fabric | ||||
Google谷歌系 | ||||
Kubernetes | ||||
全链路 | ||||
全链路-日志(Logging) | ||||
ELK(Elasticsearch+logstash+Kibana) | ||||
日志易 | ||||
全链路-跟踪(Tracing) | ||||
可扩展应用程序性能管理 (APM) 服务 | Application Insights | |||
OneAPM | ||||
听云 | ||||
Datadog | ||||
SkyAPM-dotnet | ||||
OpenTracking | ||||
全链路-度量(Metrics) | ||||
App.Metrics(.net)+InfluxDB(go)+Grafana | ||||
Prometheus(go)+Grafana |
1111111111
彻底理解cookie,session,token
发展史
1、很久很久以前,Web 基本上就是文档的浏览而已, 既然是浏览,作为服务器, 不需要记录谁在某一段时间里都浏览了什么文档,每次请求都是一个新的HTTP协议, 就是请求加响应, 尤其是我不用记住是谁刚刚发了HTTP请求, 每个请求对我来说都是全新的。这段时间很嗨皮
2、但是随着交互式Web应用的兴起,像在线购物网站,需要登录的网站等等,马上就面临一个问题,那就是要管理会话,必须记住哪些人登录系统, 哪些人往自己的购物车中放商品, 也就是说我必须把每个人区分开,这就是一个不小的挑战,因为HTTP请求是无状态的,所以想出的办法就是给大家发一个会话标识(session id), 说白了就是一个随机的字串,每个人收到的都不一样, 每次大家向我发起HTTP请求的时候,把这个字符串给一并捎过来, 这样我就能区分开谁是谁了
3、这样大家很嗨皮了,可是服务器就不嗨皮了,每个人只需要保存自己的session id,而服务器要保存所有人的session id ! 如果访问服务器多了, 就得由成千上万,甚至几十万个。
这对服务器说是一个巨大的开销 , 严重的限制了服务器扩展能力, 比如说我用两个机器组成了一个集群, 小F通过机器A登录了系统, 那session id会保存在机器A上, 假设小F的下一次请求被转发到机器B怎么办? 机器B可没有小F的 session id啊。
有时候会采用一点小伎俩: session sticky , 就是让小F的请求一直粘连在机器A上, 但是这也不管用, 要是机器A挂掉了, 还得转到机器B去。
那只好做session 的复制了, 把session id 在两个机器之间搬来搬去, 快累死了。
后来有个叫Memcached的支了招: 把session id 集中存储到一个地方, 所有的机器都来访问这个地方的数据, 这样一来,就不用复制了, 但是增加了单点失败的可能性, 要是那个负责session 的机器挂了, 所有人都得重新登录一遍, 估计得被人骂死。
也尝试把这个单点的机器也搞出集群,增加可靠性, 但不管如何, 这小小的session 对我来说是一个沉重的负担
4 于是有人就一直在思考, 我为什么要保存这可恶的session呢, 只让每个客户端去保存该多好?
可是如果不保存这些session id , 怎么验证客户端发给我的session id 的确是我生成的呢? 如果不去验证,我们都不知道他们是不是合法登录的用户, 那些不怀好意的家伙们就可以伪造session id , 为所欲为了。
嗯,对了,关键点就是验证 !
比如说, 小F已经登录了系统, 我给他发一个令牌(token), 里边包含了小F的 user id, 下一次小F 再次通过Http 请求访问我的时候, 把这个token 通过Http header 带过来不就可以了。
不过这和session id没有本质区别啊, 任何人都可以可以伪造, 所以我得想点儿办法, 让别人伪造不了。
那就对数据做一个签名吧, 比如说我用HMAC-SHA256 算法,加上一个只有我才知道的密钥, 对数据做一个签名, 把这个签名和数据一起作为token , 由于密钥别人不知道, 就无法伪造token了。
这个token 我不保存, 当小F把这个token 给我发过来的时候,我再用同样的HMAC-SHA256 算法和同样的密钥,对数据再计算一次签名, 和token 中的签名做个比较, 如果相同, 我就知道小F已经登录过了,并且可以直接取到小F的user id , 如果不相同, 数据部分肯定被人篡改过, 我就告诉发送者: 对不起,没有认证。
Token 中的数据是明文保存的(虽然我会用Base64做下编码, 但那不是加密), 还是可以被别人看到的, 所以我不能在其中保存像密码这样的敏感信息。
当然, 如果一个人的token 被别人偷走了, 那我也没办法, 我也会认为小偷就是合法用户, 这其实和一个人的session id 被别人偷走是一样的。
这样一来, 我就不保存session id 了, 我只是生成token , 然后验证token , 我用我的CPU计算时间获取了我的session 存储空间 !
解除了session id这个负担, 可以说是无事一身轻, 我的机器集群现在可以轻松地做水平扩展, 用户访问量增大, 直接加机器就行。 这种无状态的感觉实在是太好了!
Cookie
cookie 是一个非常具体的东西,指的就是浏览器里面能永久存储的一种数据,仅仅是浏览器实现的一种数据存储功能。
cookie由服务器生成,发送给浏览器,浏览器把cookie以kv形式保存到某个目录下的文本文件内,下一次请求同一网站时会把该cookie发送给服务器。由于cookie是存在客户端上的,所以浏览器加入了一些限制确保cookie不会被恶意使用,同时不会占据太多磁盘空间,所以每个域的cookie数量是有限的。
Session
session 从字面上讲,就是会话。这个就类似于你和一个人交谈,你怎么知道当前和你交谈的是张三而不是李四呢?对方肯定有某种特征(长相等)表明他就是张三。
session 也是类似的道理,服务器要知道当前发请求给自己的是谁。为了做这种区分,服务器就要给每个客户端分配不同的“身份标识”,然后客户端每次向服务器发请求的时候,都带上这个“身份标识”,服务器就知道这个请求来自于谁了。至于客户端怎么保存这个“身份标识”,可以有很多种方式,对于浏览器客户端,大家都默认采用 cookie 的方式。
服务器使用session把用户的信息临时保存在了服务器上,用户离开网站后session会被销毁。这种用户信息存储方式相对cookie来说更安全,可是session有一个缺陷:如果web服务器做了负载均衡,那么下一个操作请求到了另一台服务器的时候session会丢失。
Token
在Web领域基于Token的身份验证随处可见。在大多数使用Web API的互联网公司中,tokens 是多用户下处理认证的最佳方式。
以下几点特性会让你在程序中使用基于Token的身份验证
1.无状态、可扩展
2.支持移动设备
3.跨程序调用
4.安全
那些使用基于Token的身份验证的大佬们
大部分你见到过的API和Web应用都使用tokens。例如Facebook, Twitter, Google+, GitHub等。
Token的起源
在介绍基于Token的身份验证的原理与优势之前,不妨先看看之前的认证都是怎么做的。
基于服务器的验证
我们都是知道HTTP协议是无状态的,这种无状态意味着程序需要验证每一次请求,从而辨别客户端的身份。
在这之前,程序都是通过在服务端存储的登录信息来辨别请求的。这种方式一般都是通过存储Session来完成。
下图展示了基于服务器验证的原理
随着Web,应用程序,已经移动端的兴起,这种验证的方式逐渐暴露出了问题。尤其是在可扩展性方面。
基于服务器验证方式暴露的一些问题
1.Seesion:每次认证用户发起请求时,服务器需要去创建一个记录来存储信息。当越来越多的用户发请求时,内存的开销也会不断增加。
2.可扩展性:在服务端的内存中使用Seesion存储登录信息,伴随而来的是可扩展性问题。
3.CORS(跨域资源共享):当我们需要让数据跨多台移动设备上使用时,跨域资源的共享会是一个让人头疼的问题。在使用Ajax抓取另一个域的资源,就可以会出现禁止请求的情况。
4.CSRF(跨站请求伪造):用户在访问银行网站时,他们很容易受到跨站请求伪造的攻击,并且能够被利用其访问其他的网站。
在这些问题中,可扩展行是最突出的。因此我们有必要去寻求一种更有行之有效的方法。
基于Token的验证原理
基于Token的身份验证是无状态的,我们不将用户信息存在服务器或Session中。
这种概念解决了在服务端存储信息时的许多问题
NoSession意味着你的程序可以根据需要去增减机器,而不用去担心用户是否登录。
基于Token的身份验证的过程如下:
1.用户通过用户名和密码发送请求。
2.程序验证。
3.程序返回一个签名的token 给客户端。
4.客户端储存token,并且每次用于每次发送请求。
5.服务端验证token并返回数据。
每一次请求都需要token。token应该在HTTP的头部发送从而保证了Http请求无状态。我们同样通过设置服务器属性Access-Control-Allow-Origin:* ,让服务器能接受到来自所有域的请求。需要主要的是,在ACAO头部标明(designating)*时,不得带有像HTTP认证,客户端SSL证书和cookies的证书。
实现思路:
1.用户登录校验,校验成功后就返回Token给客户端。
2.客户端收到数据后保存在客户端
3.客户端每次访问API是携带Token到服务器端。
4.服务器端采用filter过滤器校验。校验成功则返回请求数据,校验失败则返回错误码
当我们在程序中认证了信息并取得token之后,我们便能通过这个Token做许多的事情。
我们甚至能基于创建一个基于权限的token传给第三方应用程序,这些第三方程序能够获取到我们的数据(当然只有在我们允许的特定的token)
Tokens的优势
无状态、可扩展
在客户端存储的Tokens是无状态的,并且能够被扩展。基于这种无状态和不存储Session信息,负载负载均衡器能够将用户信息从一个服务传到其他服务器上。
如果我们将已验证的用户的信息保存在Session中,则每次请求都需要用户向已验证的服务器发送验证信息(称为Session亲和性)。用户量大时,可能会造成
一些拥堵。
但是不要着急。使用tokens之后这些问题都迎刃而解,因为tokens自己hold住了用户的验证信息。
安全性
请求中发送token而不再是发送cookie能够防止CSRF(跨站请求伪造)。即使在客户端使用cookie存储token,cookie也仅仅是一个存储机制而不是用于认证。不将信息存储在Session中,让我们少了对session操作。
token是有时效的,一段时间之后用户需要重新验证。我们也不一定需要等到token自动失效,token有撤回的操作,通过token revocataion可以使一个特定的token或是一组有相同认证的token无效。
可扩展性()
Tokens能够创建与其它程序共享权限的程序。例如,能将一个随便的社交帐号和自己的大号(Fackbook或是Twitter)联系起来。当通过服务登录Twitter(我们将这个过程Buffer)时,我们可以将这些Buffer附到Twitter的数据流上(we are allowing Buffer to post to our Twitter stream)。
使用tokens时,可以提供可选的权限给第三方应用程序。当用户想让另一个应用程序访问它们的数据,我们可以通过建立自己的API,得出特殊权限的tokens。
多平台跨域
我们提前先来谈论一下CORS(跨域资源共享),对应用程序和服务进行扩展的时候,需要介入各种各种的设备和应用程序。
Having our API just serve data, we can also make the design choice to serve assets from a CDN. This eliminates the issues that CORS brings up after we set a quick header configuration for our application.
只要用户有一个通过了验证的token,数据和资源就能够在任何域上被请求到。
Access-Control-Allow-Origin: *
基于标准
创建token的时候,你可以设定一些选项。我们在后续的文章中会进行更加详尽的描述,但是标准的用法会在JSON Web Tokens体现。
最近的程序和文档是供给JSON Web Tokens的。它支持众多的语言。这意味在未来的使用中你可以真正的转换你的认证机制。