# 码农翻身

## 码农翻身

### 第一章 计算机世界

#### 1 tcp/ip

如何建立稳定可靠的链接？

设立专门的连接通道固然可行，不过代价太大。而且如果中间暂停传输，资源就闲置了。

* 在两端通路上，设立一个个路由。类似于中转站。

由路由来判断，接下来的路怎么走，最方便。

* 发送的信息也被切成小块，一块块发过去。每个小块走的路可能不一样，到对方那里再拼回来。

发送可以不按照次序（失序）。中间环节也可能丢失、重复。

* 对方收到一个，就会发送一个 确认信息。只有全部都收到，才会发送 最后一块 的确认信息

  如果超时了还没收到 确认信息，那么就说明丢了，就要重新发。

**滑动窗口协议**

一次次发过去，路上有几个值可以设置。如果N=1，发了一个等对方返回接受确认，再发下一个。效率太低，也老占着资源。因此N一般为3，一次发3个，确认一个发一个。

tcp链接，是虚拟的，链接状态不会再路上保存，只在两个端点记录 / 维持。

**三次握手**

验证A、B两个端点的 **发信** 和 **收信** 能力。

1. A发信给B，B收到。

   B知道，A能发信 + B能收信。

   B确认了A的名字。
2. B发信给A，A收到。

   A知道，B能发信 + A能收信。且A知道了彼此接受能力都没问题。

   A确认了B的名字。
3. A再发信给B，B收到。

   B就知道了双方的发信/收信能力没问题。

   B也确认，A知道了B的名字，且要开始发信了。

#### 2 cpu

cpu从内存里读之令，计算后写回内存，周而复始，以此运行程序。

ram很小，只能记一点点；运行速度很快。

* 工作是运行指令，指令都放在内存里，cpu无法保存。 程序计数器，寄放要执行的下一条指令地址。
* 第一条指令放在0xFFFFFFF0

执行程序，包括操作系统，都要先放进内存，才能调用。

程序由**顺序、分支、循环**组成的。其中分支和循环，只是一种跳转。

**缓存**

硬盘机械式操作，速度慢，但不怕停电； 内存速度快，配合cpu，但一停电就啥都没了。 读写内存数据依然慢，而且访问某个内存位置之后，可能会再次访问这个区域。因此缓存就用来存这片区域。cpu先向缓存要，没有再向内存要。

**流水线**

cpu流水线做四件事： 1. 向内存要指令 2. 翻译指令 3. 执行指令 4. 结果写会内存

#### 3 进程

程序如果读/写硬盘，就会非常耗时，cpu待机。因此把程序保存在内存中，切换程序运行。 正在运行的程序叫**进程**，切换程序，需要保护现场。 如，运行的指针、寄存器、打开文件、使用时间、等待时间等等。 这些信息叫做“进程控制块”，processing control block，PCB。

内存放程序，会遇到“内存分配”的问题。 程序运行完了，那么其他程序还要挪动下位置，给下一个程序让出空间。

**地址重定位**

所有程序都是从地址0开始装载，如果内存有多个程序，就会覆盖其他程序的内容。 因此cpu要有个“基址寄存器”，每个运行的程序都保存其*起始地址* 寻找地址的时候，把程序地址+基址，算出真正的地址

此外还要有寄存器，记录程序在内存中的长度，避免程序访问越界，覆盖其他程序。

两个寄存器，合起来叫做**内存管理单元**MMU

**分时系统**

cpu的运行时间分成一个个**时间片**，每个程序运行完一个时间片，必须让出来给其他程序用。 换言之，每个程序都运行十几毫秒，看起来就像在同时运行一样。

**分块系统**

cpu细切时间，程序也切细了放内存，不然内存不够用。 局部性原理：之前访问过的指令/数据，可能被再次访问；之前访问过的储存单元，也可能被再次访问。 因此，内存装程序，以一小块**页框**来装，大约4kb。先把最重要的装载进来，其他一边运行一边装载。

**虚拟内存**

程序可以分块装入内存，换言之就能运行，比内存大的程序。用到啥拿啥。 这就是“虚拟内存”，地址是“虚拟地址” 把虚拟地址也分块，叫做**页**page，大小和物理内存的**页框**(page frame)一样。

操作系统要维持一个**页表**，类似映射*虚拟页面*和*物理页面*的地图。 还要记录程序的哪些页面已经装载、哪些没被装载。 访问了没装载的，就要在内存里面找空地方，去装载，然后写上页表。 内存满了，就通过算法把不用的页框，放回到硬盘上。

* 页表格可能多级
* 常用页表，会放在mmu的缓存里面。

地址=页号+偏移量

* 页号，通过页表，查询真实的物理地址；
* 偏移量，就是记录在MMU里面的程序的基址

**分段+分页**

还能再进化

* 有些代码需要被保护，就不能随便访问；
* 程序可以分成**代码段、数据段、堆栈段**，更方便再内存上分配地址。
* 还有需要共享的内容，专门放一个地址段之中。

因此，内存上每个程序就被拆成三段，分别放在内存上。操作系统记录全部的起止位置、长度。

地址=段号+偏移量

* 段号在段表上找到**基址**，和偏移量相加，成为**线性地址**（页表上地址）
* **线性地址**通过页表进行转换，找到真正的物理地址。

**程序的装载**

初始状态下，代码和数据都在硬盘上，不会装入内存。

只会读一下header信息，就在内存上记录一下程序在硬盘的位置。

开始使用，从内存记录的虚拟地址，换算物理地址。如果没启动过，“缺页处理程序”

在硬盘中找到程序，去除相应的一部分代码到内存，并修改一下页表，表明已经载入了这部分。

随着程序运行，数据和代码块不断被载入物理内存之中。一块块载入非连续。

进程结束，内存数据被清理。

**线程**

编辑文档要自动保存，而自动保存的i/o读写，会让电脑卡住一段时间。 因此把一个**进程**，分成多个**线程**。

进程之间，相互独立，有自己的虚拟地址空间。

线程之间，共用一部分资源，如地址空间、全局变量、文件源、数据、文件等。

每个线程，也要记住自己运行的指针，函数调用栈，态等等。

如，一个进程当中保存中文档数据，一个线程负责和用户交互，一个线程负责自动保存。

#### 4 线程

线程池里面的线程，和系统同寿，不重启就不会被kill。

cpu随机挑选线程执行任务。

先进入就绪状态，等待cpu调配进入运行。

执行过程中可能被打断，让出cpu的占用；或出现硬盘、数据库等耗时操作，也得让出cpu。

一个线程就&#x5728;**“就绪”、“等待”、“运行”**，三种状态轮回，直至把任务做完。

memcached线程，缓存了用户数据，分布在了很多机器上。请求数据，就不用每次去数据库，而是先调缓存，提高速度。

**金额处理**

金额增减处理中，也可能被打断。如果同一个账户另有增减，就可能导致账单对不上，钱会凭空消失。

因此，处理增减之前要先获得“**锁**”，即修改权限。**获取了权限才能修改**。

如果被打断，其他人没有权限，也就修改不了。直至当前金额处理完，才能让其他人进行下一步的处理。

如果A、B两个账户，相互转钱。就会出现，**两遍都获得了锁，都在等对方放锁的僵局**，即死锁。

这时候，操作系统就会随机kill掉一个进程。让其中一个进程获得两把锁，转账完了，才会交出两把锁。

一般会有个**获得锁的优先级排序算法**。

#### 5 硬盘

cpu、内存、硬盘速度不匹配，用缓存、直接内存访问、多进程/线程切换 等方法，来解决这些问题。

硬盘里面有`盘片`，像粘在主轴上的cd片一样，旁边有个`磁臂`，读/写数据。

`盘片`有一圈圈的`磁道`，每一小段是一个`扇区`，从上到下的一条，就是一个`柱面`

硬盘读取时间有两块：

* 寻道时间：找到那一轮磁道
* 旋转时间：找到那个扇区

对于用户，`文件`是最小的存储单位，在其上是`目录`，也就是个特殊的文件。

硬盘文件存储：索引式

专门有个磁盘块，叫`索引节点`inode，记录磁盘块、文件权限、所有者等等信息。还要记录文件的摆放位置。【拆散了放，才能最大化利用空间】

索引能记录文件摆放位置，也能记录索引位置。换言之，可以扩充一个文件所能用的磁盘块数。【随机设置磁盘块】可以有多次间接块。

操作某一步出现系统崩溃，那么就有可能出现空间无法释放。

因此需要有**日志**，重启的时候，检查日志，看哪些做了哪些没做，方便恢复。

管理空闲块：

位图法，每个磁盘块，用了是1，没用是0，形成一张位图。这样记录使用空间，非常省。

文件系统 Linux Ext2

* MBR, Mater Boot Record，再加上磁盘分区表。

  分区表记录了分区位置、是否活动。
* 分区：引导块+各个块组。
* 块组：超级块+块组描述+磁盘块位图+inode位图+inode表+具体的数据块

  `超级块`，记录磁盘块总数、每个块大小、空闲个数、inode个数。

#### 键盘

一等公民：CPU和内存 1.5等：硬盘，存储所有程序和数据 二等：I/O设备。 键鼠、显卡、声卡、网卡等

划分：

* 块设备：硬盘、CD-ROM、U盘等。数据存在固定大小的块中，且有地址。
* 字符设备：键鼠、打印机。没有块解构，只是字符组成的流。

存储设备、传输设备、人机交互设备。

**CPU如何与I/O设备联系？**

* 每个I/O都拉一根线，太麻烦，不方便增加设备
* 都挂到总线上。

  但联系一个的时候，总线被霸占，就不能联系其他设备了

每个设备编号，就是I/O端口 把端口映射到内存，CPU就能像访问内存一样，访问I/O。内存映射I/O

CPU速度太快，不能一直等着硬盘之类的机械设备完工。 因此，CPU给硬盘发指令之后，就回去干其他的事情。 硬盘干完，就发**中断指令**给CPU。CPU每次做完一个指令，都回去检查一下中断。

有中断，cpu就会把当前进程保存一下，然后去硬盘读取数据。 之后就有**中断控制器**，专门管理中断请求，来协调优先级。

中断，就是一种异步、事件驱动的处理思想。

**DMA**

CPU只从内存拿数据，那么硬盘、键鼠等的数据，都得搬运到内存中，才能被cpu使用。 大量的数据传输，如果让cpu来搬运，就又回占用cpu 因此产生了DMA，专门处理I/O设备和内存的数据传输。

CPU发指令，让硬盘某地址内容搬运到内存的某个地址。 DMA接手，负责搬运。 搬运完，告诉CPU。 在DMA占用总线的这段时间内，cpu可以用一二级缓存，来继续干活。

#### 数据库SQL

直接让人操作数据文件，会有许多问题：

* 数据会有冗余和不一致。一处修改了，另一处也得改。
* 直接去文件查找和计算，很麻烦

"所有计算机问题，都可以通过增加一个中间层来解决" 如物理层里面有各种文件，可以增加一个逻辑层，专门和用户交互。

* 文件映射到逻辑层的，就叫做**表**
* 文件内容映射到表内，就叫做**列**/字段/属性
* 每一列都有各自的类型。来限定输入的属性。

解析器，把对逻辑层的表的操作，解析成对具体文件的操作。 程序也能调用逻辑层来表层，就不用直接操作文件了 【感觉就是把操作文件的过程，封装了起来】

用户只需要关注逻辑层的“表”即可。 也能对物理层的文件存储进行优化，来加快访问速度。

局域网环境：

* 如果两个人都修改了同一个文件，后一个修改的，就会覆盖掉前一个人修改的操作 一次只能修改一行
* 电子账户的修改问题 金额操作都要加锁。没锁不能修改金额。不然就会有金额的丢失。

如果转账到了一半系统崩溃，就会出现金额对不上的问题 因此金额操作必须是原子的：要么全部发生，要么根本不发生。

要记录一个undo日志。执行操作之前，记录原有的金额。 1. 先写日志，再写入硬盘文件 2. 全部余额文件写完，再结束该任务

> 【开始T1】 【T1，a原有1000】 【T1，b原有2000】 【T1，a减少200，变800】 写入硬盘：a余额800 【T1，b增加200，变2200】 写入硬盘：b余额2200 【提交T1】

如果是断电了，需要回滚，那么回滚完的部分，就写【回滚T1】 这样下次遇到就不用回滚了

三大类权限： 1. 数据操作。 2. 结构操作。创建表 3. 管理操作。备份、创建用户

中间层就能剥离出来，成为数据库

#### Socket

socket把TCP/IP协议抽象了出来，上层应用可以在这个抽象层中编程。 socket（插座），即插即用，建立连接。不用管底层的三次握手了。 连接需要两个端点：**（客户端IP，客户端port）（服务器ip，服务器port）**

> 为什么需要port 服务器/客户端有多个进程，一个port对应一个进程的连接。 进程号，是动态生成的。如果服务器重启，进程号就变了，客户端就访问不到服务器了。 因此用不变的port来区分服务器。类似于一扇大门，等待客户端进程的访问。

```
// 客户端
clientfd = socket(...)

connect(clientfd, 服务器ip, 服务器port, ...) // 连接到服务器
send(clientfd, 数据)                                                // 发送数据
receive(clientfd, ...)                                         // 返回数据
close(clientfd)


// 服务器
listenfd = socket(...)
bind(listenfd, 服务器ip, 服务器port, ...) // 声明服务器端口的占用

listen(listenfd, ...)
while(true) {                                                     // 服务器一直提供服务
    connfd = accept(listenfd, ...)                // 监听到了之后，生成socket描述符
    receive(connfd, ...)
    send(connfd, ...)
}
```

服务器可以一直用同一个端口，因为可以用客户端ip或者port，来区分不同的进程

#### 从1加到100

简化版的内存和cpu：

* 内存

  一个个小格子。里面放了数据和程序（指令）。假设为#1到#n。

  这些数据和程序（指令），是cpu从硬盘里面拿过来的。
* cpu

  cpu构造复杂，这里关注`运算器`和`寄存器`。
* 寄存器类似于内存，是cpu内部的内存。假设为R1到Rn。

  cpu只能处理寄存器里面的东西，而不能直接操作内存里的东西。
* 运算器只能做四件事：

  > * 内存东西写入寄存器；
  > * 寄存器东西写入内存
  > * 进行数学和逻辑运算
  > * 根据条件进行跳转

**从1加到100** 1. 数字0放到#1 2. 数字1放到#2 3. #1取数字，放入R1 4. #2取数字，放入R2 5. 若R2值<=100，执行6；不然执行9 6. R1+R2，放入R1 7. R2+1 8. 跳转到5 9. R1值写回#1

#### 编译

计算机只认识01，因此最原始的编程，是把不同操作对应为不同数字，来进行寄存器的加减。

汇编语言，就是文字一一对应二进制编码。

> 0000: LOAD 1000: AX

汇编器就是负责翻译的。 但操作汇编语言，需要直接操作内存和cpu寄存器，难以结构化编程。

**高级语言**

1. 获得源程序

   > total = 1 + b
2. 把空格去了拆开，每个部分叫做一个Token

   > `total`, `=`, `1`, `+`, `b`

建立一个对照表，每样编号

| 编号  | 名称    | 类型  |
| --- | ----- | --- |
| id1 | total | 标识符 |
| id2 | =     | 赋值  |
| id3 | 1     | 数字  |
| id4 | +     | 加号  |
| id5 | b     | 标识符 |

各个编号对应内存中的值，需要分配空间，得到内存地址。 如果是引用的（比如b），就需要通过链接，获取到真正的变量地址。

Token按照语法规则（此处是ANTLR），递归组成一棵树

* 表达式可以是标识符或数字
* 表达式可以是表达式+或\*表达式
* 表达式可以是括号括起来的另一个表达式
* 中间代码生成、优化

  > id1 = id3 + id5

如果有中间变量，需要加个temp，再操作temp

> temp = id3 *id5 temp2 = temp* temp3

之类的

1. 翻译成汇编语言

   > MOV R1 id5 ；id5(b)的值放进R1 ADD R1 1 ；R1的值+1，放回R1 MOV id1 R1；把R1中计算好的值放回id1

#### 锁

只要有共享变量，那么在多线程并发运行的时候，总会出现问题。 两边同时修改一个变量，导致最终的结果并非想要的结果。

【为什么共享变量无法消除？】

任何线程想要操作共享变量，必须申请锁。有锁才能读取/修改值，做完任务才能释放锁。 锁是个boolean。就把它设为true。 进程就在等待队列里面一个个排队，有人交出了锁，才会让下一个人使用。

读取锁和改写锁是连在一起的步骤。test\_and\_set(lock) 运行这个函数的时候，总线会被锁住，其他进程不能访问内存，以免两个进程抢到了同一个lock。 【一个个去抢锁】

**递归**

第一次递归取到了锁，剩下的递归就拿不到锁，形成了死锁。 换言之，锁不能重新进入同一个函数（不可重入）

因此锁会记录调用人，以及计数器。 当调用人一致，就会给锁，并增加计数器。解锁的时候也是一层层解锁，直至计数器为0，才算交还了锁。

**信号量**

有些线程，必须等到其他线程完工之后，才能开始工作。 就有可能出现互相等待的情况。

```
wait(s) {
    while(s<=0) {
        ;// 死循环
    };
    s--;
}

signal(s) {
    s++;
}
```

使用：

```
int lock = 1;

wait(lock); // lock进去后变成了0，获得锁。其他的应用若想要调用，就进入死循环
// 这个进程就能在这里做事情

signal(lock); // lock变成1，释放锁
```

**进入等待队列，而不是在cpu空转：**

```
typedef struct{
    int value; // 用来规定有几个进程需要调用
    struct process * list;
}

wait(semaphore * s) {
    s->value--;
    if(s->value <0) {
        // 需要调用的进程之外调用，就会进入等待队列 s->list;
    }
}

signal(semaphore *s) {
    s->value++;
    if(s->value <=0){
        // 从等待队列中唤醒一个进程，令其执行
    }
}
```

**消费者和生产者同步：**

> 初始化：
>
> * 设置队列剩余空位（empty = 5）
> * 设置队列有几个文件（full = 0）
> * 锁(lock = 1)
>
> 生产者（搬运文件进队列的），发现有空位(empty>0)
>
> * 上锁(lock = 0)，消费者此时不能操作队列，进入等待
> * 添加文件进队列
> * 释放锁(lock=1)
> * 标记产生了新文件（full +1)
>
> 消费者（打印文件并销毁队列中的文件），发现有文件（full>0）
>
> * 加锁(lock=0)，生产者不能操作队列
> * 打印文件、删除文件
> * 释放锁(lock=1)
> * 标记产生了空位(empty-1)

java会用BlockingQueue来封装。自动判断队列中有没有空余值，就没那么麻烦了。

#### 加法器

如何只用4位加法器，完成正负数的加减？

**减法**

减法需要借用**补数** 补数，就是对所有二进制取反，再+1

> 0011 --> 1100 --> 1101

7-3，相当于，7+13（3的补数）再减掉溢出

> 7-3=0111-0011=0111+1101=10100 第五位溢出去掉，因此是0100，即4

做减法，就是做加法加到溢出，再去掉溢出。相当于在求模。

> 7-3=(7+13)mod 16

**负数**

需要专门用一位来表示符号 这样会出现+0和-0

负数用补码表示（负数每位取反），-0就是-8

> 1 111 = -1 1 110 = -2 1 000 = -8

【从1000到1111是-8到-1】

> 7-4 = 0111 + 1100 = 1 0011= 3 （舍弃溢出）

计算机内部，用补码来表示二进制数 正数就是本身，负数就每位取反（除了符号位）。

无符号数：\[0, 2^4 -1]，即\[0, 15] 有符号数：\[-2^3, 2^3-1]，即\[-8, 7]

#### 递归

递归就是一个函数调用自己。或者说调用另一个函数，而恰好长得像自己。

程序在内存中： 栈帧、堆、数据段、代码段，等等

* 函数编译后会放到代码段。递归的函数都一样，因此只会放一套代码。
* 每个栈帧，代表了被调用中的一个函数

栈帧内容： 上一个栈帧的指针、输入参数、返回值、返回地址，等等

**阶乘写法1:**

```
factorial(int n) {
    if(n==1){
        return 1;
    } else {
        return n * factorial(n-1);
    }
}
```

这样的写法会不断增加栈帧。 factorial(4)，返回4 \* factorial(3)。此时需要增加一个栈帧来代表factorial(3) 每个factorial都计算完毕，就会从factorial(1)开始逐个出栈。

**写法2：**

```
int factorial(int n, int result) {
    if( n == 1) {
        return result;
    } else {
        return factorical(n-1, n * result);
    }
}
```

这种写法就不需要开第二个栈帧，每次都在回调自己 factorial(4, 1) = factorial(3, 4 *1) = factorial(2, 3* 4\*1); 一直调用自己直到出结果

这种叫**尾递归**，不用担心栈帧的大小限制

* 递归调用函数中，最后执行的语句
* 返回值不属于表达式的一部分

  【每次调用的返回值还是它本身】

### 第二章 java帝国

### 第三章 浪潮之巅的web

#### web起源

首先有了文本 文本之间能通过链接相互打开，成了**超文本(HyperText)**。 能解析链接，并跳转文本的，就是**浏览器**。 描述超文本界面的标记语言，就是HTML(HyperText Markup Language)

不同的文件放在了服务器上。让大家都能访问到这些超文本。 服务器和浏览器之间传输文本，通信方法就是**超文本传输协议**(HyperText Transfer Protocol, HTTP)

#### 程序之间的通信

在一台电脑里，两个进程的消息，通过**共享内存**来交流 但每次只能由一个进程处理共享内存。

**不同电脑里的网络通讯：**

通过ip地址和端口号，进行socket通信。需要有通信协议，来约定好消息的次序和格式

**防火墙：**

防火墙只开放两个端口：

* http是80端口
* https是443端口

web服务需要有endpoint，即一个url描述web服务的地址 通常用HTTP GET/POST + JSON

http的报文，打包在tcp报文段中，放到ip层的数据报中，形成链路层的帧，通过网卡发出去

#### 如何确保通信的安全？

明文通信，容易被人监听，所以要数据加密

* 对称数据加密

两边都用同一个密钥加密、解密。网络传输的是加密后的文本

但问题在于，双方如何约定密钥？ 倘若明文传输来敲定，那不就和没敲定一样了吗？

* 非对称加密：RSA

每个人有一对钥匙，保密的&#x53EB;**“私钥”**，公开的&#x53EB;**“公钥”**

> 私钥加密的数据，公钥才能解密； 公钥加密的数据，私钥才能解密

发消息的时候，用对方的**公钥**加密并传输，对方用其**私钥**解密

但这种加密方式，通讯比较慢

解决方法：用非对称加密，来传输对称加密的钥匙，然后用对称加密通讯。

**中间人劫持**

非对称加密一开始，需要双方明文发公钥。 倘若有人拦截了双方的公钥，并将其自身的公钥发给了对方，那么中间人就总能看到双方的消息了。 【感觉这个可以私下里面对面交流，也可以发在公开的网站上。不过也可能拦截掉全部的网络传输。。】

如何声明这个公钥确实是对方的？

建立一个认证中心，给人发布证书，来证明身份。其中包括了他的公钥是什么。 换言之，拿到了证书，就能获得他的公钥。

**数字证书**

那么如何保证证书的安全传输？

1. 将公钥和个人信息，通过hash算法来形成消息摘要。

   hash算法特点：
2. 只要原内容不同，hash值就一定不同
3. 不能从hash值反推出原消息
4. 算法结果均匀分布
5. 将消息摘要用CA的私钥来加密，形成数字签名
6. 数字证书 = 原始信息 + 数字签名

验证数字证书：

* 原始信息用相同hash算法生成摘要
* 数字签名用CA的公钥解密，得到摘要

两者如果一致，那就没人篡改了

不过如果中间人完全伪装成CA，就毫无办法了。 一般自动信任CA

> 《吴军·科技史纲60讲》59:量子通信 只要计算机速度足够快，还是能破译公钥的。 香农：理论上无法破译的，只有**一次性密码** 那么如何送达给接收方？
>
> 量子密钥分发，quantum key distribution, QKD 传输带有偏振信息的光子。一次传输会错1/4，倘若中间转手，就会错到45%。因此只要有监听，那么错误率就会提升。
>
> 量子密钥如何工作？ A传密码本给B，B量子通信回来。 如果只错1/4，就说明通信没问题。A知道哪些是错的，哪些是对的，就告诉B哪些可以丢弃，只留下正确的字符，成为约定的密码。 用这个密码进行一次通信，然后丢弃。这样即使有人能破解密码，也没用。 如果A发现错误超过1/4，就说明有人监听，就中断通信。
>
> 缺点是，如果真的被监听了，那么就始终无法把正确的消息发出去

**https**

没有Pre-Master Secret的https，就如同上面的流程 1. 浏览器发出https请求 2. 服务器发送数字证书给浏览器 3. 浏览器用预置的CA，验证证书。有问题就提示风险。 4. 没问题就生成随机对称密钥，用浏览器公钥加密，发送给服务器。 5. 服务器用自己的私钥解密，得到对称密钥 6. 之后用对称密钥进行通信

#### 单点登录SSO的CAS解决方案

* 登录：

输入用户名+密码，验证。验证通过就建立session【用来验证cookie】，把sessionid通过cookie发给浏览器。 下次访问，如果有cookie，就会认为登录过了，直接放行。

* 多个登录系统，如何统一登录？

单点登录SSO，一次登录，全部通行。就不用记录那么多账户密码了。 一共有三层：`浏览器、系统、认证中心`

1. 如果发现用户没登录，就重新定向到认证中心 在url后面加一段：`www.sso.com/login?redirect=www.page.com` 通过认证之后，还要重新定向回来当前系统的页面
2. 认证中心登录成功后
3. 建立一个session，给浏览器发个cookie
4. 创建ticket（类似随机字符串）
5. 重新定向回来。 url类似于：`www.page.com?ticket=abcd`
6. 验证ticket 然而一个cookie只能给一个域名使用，来验证登录。 因此浏览器再访问系统，系统会去认证中心，验证ticket

如果ticket有效，就注册该系统 建立session，发一个认证中心的cookie

因此浏览器获得了两个cookie，一个是当前系统的页面的cookie，另一个是认证中心的cookie

* 如果用户再访问该系统，就会用该系统的cookie（验证系统的session）登录
* 如果用户访问其他系统，就会用cookie登录认证中心 认证中心会建立session，注册新系统，并发新系统的cookie

本质上，就是共享认证中心的cookie+多个子系统的cookie。没登录就用认证中心的cookie登录，需要啥系统，就注册并发哪个系统的cookie

1. 单点退出

   用户在一个系统退出了，认证中心就要把自己的绘画和cookie消灭。

   再一个个通知已注册的系统，让他们都退出。

> [sso](https://zhuanlan.zhihu.com/p/40045193)
>
> * 共享cookie，就是共享session。本质上cookie只是存储session-id的介质。
>
>   一般是一个server一个session。因此就有了第二种方法
> * SSO-Token，或称为Ticket。这在整个server群里面唯一，且都能验证。
>
> 只要没登录，就会用ticket来去验证中心来验证ticket，返回该系统的cookie
>
> [cookie 和 token有啥区别？](https://www.jianshu.com/p/c33f5777c2eb)
>
> * cookie是有状态的，验证记录和会话，要一直在服务器端保存。 登录后，服务器创建session存在数据库，然后把session id 放在cookie中，存在浏览器中。
> * token是无状态的，服务器不记录token 一般存在用户的local storage或session storage或cookie中 服务器就直接验证token是否有效

#### OAuth中的三种认证方式

`客户端`想要用`资源服务器`的数据，而这些数据需要先在`授权服务器`登录，才能使用。

**1. 资源所有者密码凭据许可**

`资源所有者`（知道密码的人），告诉`客户端`账号密码，然后`客户端`拿着去`授权服务器`登录。登录完成后就扔。

但无法获得`资源所有者`的信任

**2. 隐式许可**

`客户端`登录的时候，跳转到`授权服务器`的登录网站，登录后返回token，意味着授权`客户端`访问`资源服务器`的数据。

`客户端`就能用token，通过`资源服务器`的api来访问数据了。

这样密码就不会经手客户端了。

```
https://www.a.com/callback#token=<token>
```

## 是Hash Fragment，只会停留在浏览器端，只有js能访问到，不能再通过http发送到其他服务器。提高安全性。

`授权服务器`返回token，可以在历史记录或访问日志中查到，不安全。

**3. 授权码许可**

既然token可能被查到，那就不让浏览器接触到token。 不返回token，而是返回一个授权码Authorization Code。获取授权码之后，再访问授权服务器，才返回真正的token。 换言之，申请token的过程就全在后端完成。

那么这个授权码不就暴露了吗？ 这可以加入措施防御。如，和申请的app绑定、超时code失效、只能换一次token等等。

【感觉也可以把code围堵在里面，无法兑换token】

#### 后端风云

数据库用SQL语言，Structured Query Language结构化查询语言。它是声明性的。 【为什么没有函数式编程的语法？】

发送SQL，需要建立数据库连接Connection，但这个连接很昂贵，需要开辟缓冲区，来读取表中的数据。 当用户增加，能建立的Connection数量，就成了数据库的瓶颈。即使增加了内存，也终有用完的一天。

**增加缓存**

在应用程序和数据库之间增加一个抽象层——缓存。 用户先去缓存找，找不到再去数据库找。

数据从 浏览器 出发，先到 应用服务器，再到 数据库服务器。

应用服务器 里面，数据是先进入 Nginx ，再进入 Tomcat。

Tomcat里面就包括了应用程序和数据库缓存

**分家**

全在一台服务器上，一挂就全挂了。

* 浏览器数据先进入Nginx，单独在一台服务器
* 再进入Tomcat，被分为很多个服务器
* 数据库缓存单独放到一个服务器
* 数据库服务器不变

**Tomcat优化**

* 缓存和Tomcat不在一台服务器上，网络访问很麻烦

用redis，能快速存储海量key-value

如果需要存对象，可以变成json格式存

Tomcat和redis的交互，通过Jedis

* redis服务器不止一台，要存的均匀、取得到；增加/减少服务器也要均匀

hash槽：

每台服务器负责一部分的槽，求余算出数据要在哪个服务器上；

新增的话就每个服务器都让出一点空间。访问找不到，就让redis重新定向去找

* 负载均衡

Tomcat有session。如果挂了登录信息依然要保留。方法是放到redis里面去存取。

故障转移：

hash槽分为两组，里面有一台服务器是master，两个slaver作为备份。master不行了随时替换。

**Nginx优化**

两台Nginx服务器，一台坏了换另一台

通过keepalived来控制，外面看起来像只有一台

**mysql读写分离**

分成几个数据库，一个master，多个slaver

* master可读可写，主要负责写，然后把写的内容复制到其他slaver。
* slaver主要负责读

master挂了就替换slaver当master

加一层mysql proxy来作为中间层，方便访问

#### rpc

如果数据和函数都在本地，那调用起来没有问题。即**本地过程调用**。

如果数据在一个服务器，计算的函数在另一个服务器，那么就需要通过**rpc**调用。即**远程过程调用**。

调用方法看起来和本地调用是一样的

也是传几个参数，通过 **客户端代理stub**，把它序列化，然后传输，然后对面服务器有一个 **服务器端代理skeleton**，把这些反序列化在拿给函数计算。

简言之rpc通过两个代理，来完成数据的交互。如socket或者http等

#### 微服务

代码和环境一起成为一个镜像，镜像放到服务器端的docker里面运行。这样开发、测试、生产环境就能保持一致了。

缺点

* 数据库分区，难以保证一致性
* 服务多了客户调用起来非常麻烦
* 出了问题监控很麻烦

#### 框架

前人实践下来的最优的轮子，即最佳实践。

框架预置了一些公认的最佳实践，只需要吧业务代码填充进去就行了。

#### http server

应用程序通过socket来收发数据。

1. 发给操作系统一个集合，这些socket的数据发送出去
2. 过一会询问，集合中哪些socket有返回？
3. 阻塞
4. 操作系统返回几个有返回值的，拿着这些去处理。

### 第四章 代码管理

### 第五章 编程语言简史

**js**

XMLHttpRequest，能局部刷新页面。

精简数据，发明JSON格式。 即用对象和数组来存放数据。js可以直接调用

Node.js 服务器端用js，前后端都能用同样的开发语言。 能用一个线程来处理所有的请求，由事件驱动编程。需要等待和其他人`连调`的请求，就异步操作。

### 第六章 精进
