https://pic4.zhimg.com/v2-e263475646652726731e13e44b5d2479_im.jpg

为什么TCP需要4次握手

为什么TCP需要4次握手 第一次: A没数据发送了则主动要求断开连接,于是A向B发送了一个FIN包 第二次: B收到A的FIN包则需要回复一个ACK表示收到了A的FIN请求,但是此时B可能还有数据需要发给A(比如之前没有收到A的ACK的数据需要重发等),这种情况下A可以继续接受B的数据但是不会主动发送数据给B只会回复ACK,所以B发送的ack号永远都是一样的 第三次: B将剩余的数据发送完毕之后就需要发送一个FIN包给A表示”自己的数据发送完毕了,我要断开了 第四次: 此时A必须回复这个FIN表示收到了B的断开请求,此时就进入 TIME_WAIT 状态等待一段时间才会断开,B如果收到了A的ACK则断开连接,如果没收到则还会继续发送FIN请求,此刻因为A还处于 TIME_WAIT 状态则会继续回复ACK ​ 为什么需要 TIME_WAIT 回复B最后一个FINACK ,让B端关闭连接 接受之前还遗留在网络上的数据包,防止对以后的同端口的TCP连接造成影响,可能另外的应用复用同一个端口,此时对方的FIN包还遗留在网络并且稍后又到达了,那么就会破坏新建立的TCP连接 ​ TIME_WAIT状态为什么是2MSL的时长 MSL Maximum Segment Lifetime最大报文段生存时间,和IP数据包的那个TTL(Time To Live)不同的是TTL是按照路由器跳数来计算的,超过了一定的跳数则丢弃该IP包,作用是限制IP数据包在计算机网络中的存在的时间。TTL的最大值是255,TTL的一个推荐值是64 而MSL是按照时间来计算的,超过了一定的时间则TCP报文就会被丢弃 考虑最坏的情况: A回复了ACK之后进入TIME_WAIT,这个ACK经过了MSL时间丢了,那么B中肯定RTO(RTO<=2MSL)时间到了,于是又回复FIN,这个包最差的情况也是进过了MSL时间之后就丢弃了。所以一来一回就是2MSL就可以保证所有的包都会在网络上消失 ​ 参考 TCP的运输连接管理

内存对齐

计算机总线中有一个 数据总线,用来传输内存上的数据到寄存器中,数据总线大小决定了一次能传输多个数据,比如32位总线一次能传输4字节,字长就是4字节,如果需要取一个8字节的数据就需要传输两次,64位总线一次能传输8字节,字长就是8字节,取一个8字节的数据只需要一次传输即可,所以CPU取数据都是以数据总线宽度(也就是CPU字长)为单位来取的 内存对齐就是为了提高CPU获取数据的效率,如果不进行内存对齐可能会增加CPU访问数据的次数 |a|a|a|b|b|b| | | #对齐前 |a|a|a| |b|b|b| | #对齐后 比如a、b各自占用3byte,CPU字长为4byte,不进行内存对齐而采用紧凑的方式存储那么访问a只需要一次,但是访问b就需要进行两次 并且一个struct的起始地址也是需要从CPU字长的整数倍开始的,同时如果一个struct的长度不足自身内存对齐边界(属性中长度最长的那个)的整数倍则会进行填充,这是为了在数组中保持struct依然是内存对齐的 因为struct会进行内存对齐,所以我们需要合理的布局struct中各个属性的分布,这样才能有效的减少内存大小 type A struct { a int8 b int16 c int32 } type B struct { a int8 c int32 b int16 } type C struct { a int32 c int64 b int8 } type D struct { c int64 a int32 b int8 } func main() { fmt.Println(unsafe.Sizeof(A{})) // 8 fmt.Println(unsafe.Sizeof(B{})) // 12 fmt.Println(unsafe.Sizeof(C{})) // 24 fmt.

密码加盐和彩虹表攻击

密码明文存储 这种密码存储方式直接将用户密码明文存储在数据库中,一旦数据库被黑客获取,那么所有信息都泄露,有些用户的密码可能在好几个账户上都是相同的,这是一种最不完全的方式 ​ 密码hash加密存储 这种方式比上一种略微安全,用户注册时将用户的密码使用指定的hash算法进行加密,然后存储在数据库中,当用户登录的时候将登录的密码使用相同的hash算法进行计算hash值然后与数据库存储的hash值进行比较即可 这种方式可能造成 彩虹表攻击 ,攻击者建立一个 明文->密文 的一个巨大的映射表,如果凑巧你的密码密文被包含在了这个表中,那么黑客就可以知道你的密码明文了 这个数据字典很容易收集,CSDN 泄露的那 600w 个密码,就是很好的原始素材 如果用户密码很复杂那么被包含到彩虹表里面的可能性越小 ​ 密码加盐进行hash 这种方式会在用户注册的时候给用户随机上次一个salt值,然后再和用户的密码进行计算hash值 salt 可以是任意字母、数字、或是字母或数字的组合,但必须是随机产生的,每个用户的 Salt 都不一样,用户注册的时候,数据库中存入的不是明文密码,也不是简单的对明文密码进行散列,而是 hash( 明文密码 + Salt), 密码加盐之后还是无法保证100%的安全,如果数据库泄露了,黑客还是可以在他们原来的数据字典中的密码,加上我们泄露数据库中的 Salt,然后散列,然后再匹配,但是由于salt是随机产生的,假如我们的用户数据表中有 30w 条数据,数据字典中有 600w 条数据,那么需要获取30w的salt然后分别对600w的数据字典中的明文密码都加上一个salt计算hash,那最终加盐的彩虹表大小就是30w*600w的条目,这个计算量是巨大的,然后再根据用户数据表的hash值遍历这个彩虹表,这个遍历耗时也是巨大的 并且salt插入的地方也无法知道,可能插入在头、尾、中间任意位置都有可能,这就加大了破解的难度,所以密码加盐还是能够保证安全性的 ​ 参考 CSDN600万账户密码泄露事件 为什么要在密码里加点“盐”

建造者模式

为什么要使用建造者模式 建造者模式将对象本身和对象的创建进行解耦,用户不需要知道对象创建的细节和流程 建造者模式解决了对象的创建过程和流程,这些都不需要用户关心 建造者和工厂模式不同的是建造者更加关系构建对象的流程,各个部件的构建顺序,而工厂模式更加关心的是对象的整体,工厂模式创建的对象比较单一,没有很复杂的组件,而如果对象由好几个对象组成,并且对象属性比较多,构建顺序有一定的要求则需要使用建造者模式 ​ 实现 创建一个电脑 public class Computer { private String brand; private String cpu; private String mainBoard; private String hardDisk; private String displayCard; private String power; private String memory; // 省略 getter, setter, toString } Builder抽象类 public abstract class Builder { protected Computer computer = new Computer(); public abstract void buildBrand(); public abstract void buildCPU(); public abstract void buildMainBoard(); public abstract void buildHardDisk(); public abstract void buildDisplayCard(); public abstract void buildPower(); public abstract void buildMemory(); public Computer createComputer() { return computer; } } 具体的实现

原型模式

为什么要使用原型模式 原型模式又叫 克隆模式,是对一个对象的拷贝,当一个对象的创建比较复杂的时候就可以使用原型模式进行拷贝然后再修改需要修改的部分 ​ 代码实现 public class EnglishBook extends Book{ public EnglishBook(String title,Author author) { super(title,author); } @Override public String toString() { return String.format("%s:《%s》",this.author.getName(),this.title); } @Override protected Book clone() { return new EnglishBook(this.title,this.author); } } ​ 原型模式在Spring中的应用 Spring的Bean通常是单例的,如果我们显式需要多个对象则会使用原型模式创建多个对象

工厂模式

为什么要使用工厂模式 工厂模式是一种创建型的设计模式,专门用于创建对象,通过工厂模式创建对象我们就不需要手动创建对象了,直接重工厂类里面获取即可。 工厂方法主要有如下几个优点: 隐藏了创建对象的复杂度,用户不需要知道如何创建对象,只需要调用相关的方法API即可创建对象, 降低了代码耦合,一旦我们需要修改创建对象的方式或则参数,如果不使用工厂模式直接new的话那么我们需要手动查找并且修改所有new的代码,而如果使用了工厂模式因为我们获取对象都是通过工厂获取的,我们只需要修改工厂类的相关代码即可 ​ 工厂模式实现方法 简单工厂 工厂方法模式 抽象工厂 ​ 简单工厂 简单工厂模式就是只有一个工厂,然后用户通过传入不同的名字或则参数,此工厂类再根据用户传入的参数进行判断需要创建哪个对象(被创建的实例具有相同的接口或则父亲,当然了,返回值直接设置成Object也是可以的) 简单工厂模式有如下几个缺点: 违背开闭原则(对扩展开放,对修改关闭),如果需要增加一个类则需要修改工厂类 工厂类只有一个,职责过重,逻辑复杂 interface Computer { String Brand(); //品牌 } public class MacComputer implements Computer { public static final String BrandName = "Mac"; @Override public String Brand() { return BrandName; } } public class LenovoComputer implements Computer { public static final String BrandName = "Lenovo"; @Override public String Brand() { return BrandName; } } public enum ComputerBrand { Mac, Lenovo } 创建计算机的简单工厂,根据传入的Type不同分别创建不同品牌的计算机,如果需要增加一种品牌则需要改动此部分代码,所以违背了 开闭原则

单例模式

为什么要使用单例模式 有时候系统只需要一个对象,比如 数据库连接对象、线程池对象 等 线程池这种对象如果建立多个就会失去它的意义,因为线程池本来就是用来管理多个线程的只需要一个即可 数据库连接对象使用单例并不是指只有一个数据库连接,数据库连接对象里面保存的只是数据库连接相关的信息,比如数据库类型、用户名、密码、URL、字符集等 实际创建个多少TCP连接和实际的并发量有关,因此数据库连接对象也不必每次查询SQL的时候去创建对象,这样就显得多余和麻烦了 这种为了让系统中只存在一个对象的模式就叫 单例模式 ​ 单例模式实现方法 饿汉式 方法级别锁、双重检查锁 静态内部类 枚举 ​ 饿汉式单例 饿汉式单例指的就是在系统初始化的时候就创建对象 优点是无需考虑多线程问题 缺点是无法懒加载,万一对象始终用不到那就白加载了 Java类成员变量初始化实现 public class DataBaseManager { //初始化时就创建 private final static DataBaseManager db = new DataBaseManager("lyer","55555"); private String username; private String password; //必须隐藏构造方法 外界无法创建 private DataBaseManager(String username,String password){ this.username = username; this.password = password; } public static DataBaseManager getInstance(){ return db; } } Go全局变量实现 go中可以通过 全局变量 或则init函数实现,不过这不是最佳实现方式 var database1 = &DataBase{} func main() { database1.

BloomFilter布隆过滤器

布隆过滤器原理 布隆过滤器就是通过 3个hash函数,当然hash函数的数量不限,hash函数越多判断越准确,但是同时消耗也越大 将值进行一个hash,计算出对应的index,然后设置对应的bitmap 查找的时候再将值进行hash,分别查找对应的bitmap的位数是否为1,,如果都为1那么则表示找到,如果有一个不为1则表示不存在 BloomFilter 的缺点就是发生hash碰撞之后会出现误判的情况 ,也就是说有可能多个值都映射到相同的位置,所以如果判断出元素存在的话还需要进一步到真实的数据中去再次确认元素是否存在,但是如果判断出不存在,则肯定是不存在 同时布隆过滤器也有 删除问题,也就是说如果删除了一个值,那么其对应的bitmap就要置为0 ,但是如果和他产生hash碰撞的值也都无法判断了,比如原来本来存在的值其中一位和删除的值的bit位一样,但是被删除了,那么判断这个值得时候就会误判为不存在 综上,我们得出 BloomFilter 的主要作用如下: 一个元素如果判断结果为存在的时候元素不一定存在,但是判断结果为不存在的时候则一定不存在 布隆过滤器可以添加元素,但是不能删除元素 ​ BloomFilter的应用场景 网页爬虫对URL的去重,避免爬取相同的URL地址; 反垃圾邮件,从数十亿个垃圾邮件列表中判断某邮箱是否垃圾邮箱; 防止缓存击穿,将已存在的key放到布隆过滤器中,当访问不存在的缓存时迅速返回避免去真实的数据库查询缓存,这样如果黑客暴力故意查询一个不存在的键时也不会打到后台真实的服务器上而是直接在BloomFilter缓存中就已经判断出不存在了 ​ BloomFilter的实现 TODO ​ 布谷鸟过滤器 布谷鸟过滤器是布隆过滤器的升级版,其支持删除操作并且不影响判断,我在github上找到了一个实现 https://github.com/linvon/cuckoo-filter/blob/main/cuckoofilter.go ​ 参考 https://developer.aliyun.com/article/773205 【布隆过滤器,这一篇给你讲的明明白白】 https://segmentfault.com/a/1190000039156246 【布隆,牛逼!布谷鸟,牛逼!】讲的不错

B树B+树AVL树红黑树

B树B+树AVL树红黑树 把这四颗 可爱 的树放在一篇博客来写就是因为他们都有一个共同点: 都是平衡树 传统的二叉树一旦发生倾斜那么就会退化为链表,这是二叉树最致命的缺点,所以就出现了如此多种的树,来提高数据查找的效率 B树 不同于二叉树,B树一个节点可能会有多颗子树,相当于是一颗多叉树,并且按照一定的规则来保持这种状态那么这个树就会维持的比较 矮胖,又因为B树有搜索树的性质所以二分查找起来速度就快了 B+树 就是B树的升级版,B+树和B树的不同在于叶子节点,B+树的每个叶子节点都会用链表连接起来,这样就支持 范围查找,B+树的叶子节点存放了所有数据和索引,而非叶子节点则只保存索引,这个非叶子节点保存的索引代表了子树中最小或则最大的索引,此索引是冗余的,一旦找到起始节点那么中间的节点就可以一起获取到,就不需要每次都从root节点开始遍历了 AVL树 是一颗平衡的二叉搜索树,一旦左右子树的平衡因子太大或则太小了那么就表示树已经失衡了,就会通过旋转操作来继续维持平衡因子到指定的值,所以旋转操作执行次数多的话也是比较耗时的 红黑树 和AVL树一样也是通过一定规则并且靠旋转、变色来维持树的平衡 在学习这些数据结构的时候不需要背规则,知道个大致原理即可 ​ B树 又叫 多路查找树,和二叉树不同的是一个节点最多可以有多个子树 B树靠如下几个规则来保持树的平衡和矮胖: 根节点如果有孩子节点的话则至少有2个子节点 一颗m阶的B树中所有节点的最大孩子节点数量<=m,所以最多包含m-1个关键字(因为孩子节点是在两个关键字之间进行插入的) 中间节点关键字数量为k: ceil(m/2)<=k<=m-1 关键字之间是有序的,并且有搜索树的性质 所有叶子节点在同一层,也就是说根节点到所有叶子节点的距离都是一样的 ​ B+树 B+树就是在B树的基础上对叶子节点进行了改进,将所有的叶子节点都用链表连接起来,B+树的只在叶子节点保存数据,其他节点保存索引值 B+树和B树的区别如下: 子树数量和关键字数量一致(B树中子树数量比关键字数量多1) 叶子节点使用链表连接 非叶子节点会存放冗余的索引,叶子节点会存放具体的值和所有索引,所以非叶子节点有值重复(而B树所有节点都是唯一的) 因为B+树的所有叶子都使用链表连接,所以B+树支持范围查找,只需要找到起始节点即可 ​ AVL树 左旋 右旋 左右旋 右左旋 ​ 红黑树 节点非黑即红 根节点必须是黑色 叶子节点是黑色,一般省略叶子节点 相邻节点不能都为红色(红色节点的子节点或则父亲必须是黑色) 从一个节点出发的任意一条路径下的黑色节点数量相等 ​ 参考 B树-王道考研视频 【讲的很好】 数据结构-B树【宁哥算法课堂】 2020B站最详细红黑树结构-二叉树-哈希-B+树-HASH-平衡算法 MYSQL核心底层原理大盘点!看完心里有B树了吗? 【InnoDB讲的很好】 10分钟带你揭秘红黑树 通俗易懂的红黑树图解 (上)

位运算骚操作

XOR 异或: 相同为0,不同为1 x^0=x x^(~x)=全1 #位数和x一样的全1 x^x=0 交换两个整数变量 a^=b b^=a a^=b 判断奇偶性 x&1 == 1 #奇数 x&1 == 0 #偶数 计算某个数字二进制位1的个数 int countOne(int num) { int cnt = 0; for (int i = 0; i < 8 * sizeof(num); ++i) { cnt += num & 1; num >>= 1; } return cnt; } 判断某一位是否为1 (n>>(pos-1))&1 乘2除2 n>>1 #/2 n<<1 #*2 掩码操作,获取前面几位 mask=11110000 n=10100000 mask&n #获取前面4位 其它位置0 将最低位的1置0 num & (num - 1) 一个数组中其他数都出现了2次,只有一个数出现了1次,找出那个数