码农翻身
码农翻身
第一章 计算机世界
1 tcp/ip
如何建立稳定可靠的链接?
设立专门的连接通道固然可行,不过代价太大。而且如果中间暂停传输,资源就闲置了。
在两端通路上,设立一个个路由。类似于中转站。
由路由来判断,接下来的路怎么走,最方便。
发送的信息也被切成小块,一块块发过去。每个小块走的路可能不一样,到对方那里再拼回来。
发送可以不按照次序(失序)。中间环节也可能丢失、重复。
对方收到一个,就会发送一个 确认信息。只有全部都收到,才会发送 最后一块 的确认信息
如果超时了还没收到 确认信息,那么就说明丢了,就要重新发。
滑动窗口协议
一次次发过去,路上有几个值可以设置。如果N=1,发了一个等对方返回接受确认,再发下一个。效率太低,也老占着资源。因此N一般为3,一次发3个,确认一个发一个。
tcp链接,是虚拟的,链接状态不会再路上保存,只在两个端点记录 / 维持。
三次握手
验证A、B两个端点的 发信 和 收信 能力。
A发信给B,B收到。
B知道,A能发信 + B能收信。
B确认了A的名字。
B发信给A,A收到。
A知道,B能发信 + A能收信。且A知道了彼此接受能力都没问题。
A确认了B的名字。
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。
一个线程就在“就绪”、“等待”、“运行”,三种状态轮回,直至把任务做完。
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寄存器,难以结构化编程。
高级语言
获得源程序
total = 1 + b
把空格去了拆开,每个部分叫做一个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
之类的
翻译成汇编语言
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
每个人有一对钥匙,保密的叫“私钥”,公开的叫“公钥”
私钥加密的数据,公钥才能解密; 公钥加密的数据,私钥才能解密
发消息的时候,用对方的公钥加密并传输,对方用其私钥解密
但这种加密方式,通讯比较慢
解决方法:用非对称加密,来传输对称加密的钥匙,然后用对称加密通讯。
中间人劫持
非对称加密一开始,需要双方明文发公钥。 倘若有人拦截了双方的公钥,并将其自身的公钥发给了对方,那么中间人就总能看到双方的消息了。 【感觉这个可以私下里面对面交流,也可以发在公开的网站上。不过也可能拦截掉全部的网络传输。。】
如何声明这个公钥确实是对方的?
建立一个认证中心,给人发布证书,来证明身份。其中包括了他的公钥是什么。 换言之,拿到了证书,就能获得他的公钥。
数字证书
那么如何保证证书的安全传输?
将公钥和个人信息,通过hash算法来形成消息摘要。
hash算法特点:
只要原内容不同,hash值就一定不同
不能从hash值反推出原消息
算法结果均匀分布
将消息摘要用CA的私钥来加密,形成数字签名
数字证书 = 原始信息 + 数字签名
验证数字证书:
原始信息用相同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,一次登录,全部通行。就不用记录那么多账户密码了。 一共有三层:浏览器、系统、认证中心
如果发现用户没登录,就重新定向到认证中心 在url后面加一段:
www.sso.com/login?redirect=www.page.com
通过认证之后,还要重新定向回来当前系统的页面认证中心登录成功后
建立一个session,给浏览器发个cookie
创建ticket(类似随机字符串)
重新定向回来。 url类似于:
www.page.com?ticket=abcd
验证ticket 然而一个cookie只能给一个域名使用,来验证登录。 因此浏览器再访问系统,系统会去认证中心,验证ticket
如果ticket有效,就注册该系统 建立session,发一个认证中心的cookie
因此浏览器获得了两个cookie,一个是当前系统的页面的cookie,另一个是认证中心的cookie
如果用户再访问该系统,就会用该系统的cookie(验证系统的session)登录
如果用户访问其他系统,就会用cookie登录认证中心 认证中心会建立session,注册新系统,并发新系统的cookie
本质上,就是共享认证中心的cookie+多个子系统的cookie。没登录就用认证中心的cookie登录,需要啥系统,就注册并发哪个系统的cookie
单点退出
用户在一个系统退出了,认证中心就要把自己的绘画和cookie消灭。
再一个个通知已注册的系统,让他们都退出。
共享cookie,就是共享session。本质上cookie只是存储session-id的介质。
一般是一个server一个session。因此就有了第二种方法
SSO-Token,或称为Ticket。这在整个server群里面唯一,且都能验证。
只要没登录,就会用ticket来去验证中心来验证ticket,返回该系统的cookie
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来收发数据。
发给操作系统一个集合,这些socket的数据发送出去
过一会询问,集合中哪些socket有返回?
阻塞
操作系统返回几个有返回值的,拿着这些去处理。
第四章 代码管理
第五章 编程语言简史
js
XMLHttpRequest,能局部刷新页面。
精简数据,发明JSON格式。 即用对象和数组来存放数据。js可以直接调用
Node.js 服务器端用js,前后端都能用同样的开发语言。 能用一个线程来处理所有的请求,由事件驱动编程。需要等待和其他人连调
的请求,就异步操作。
第六章 精进
Last updated
Was this helpful?