Giter Club home page Giter Club logo

blog's Introduction

blog's People

Contributors

ravencrown avatar

Stargazers

小小鲁班 avatar  avatar yellowlemon avatar erbao avatar  avatar  avatar  avatar  avatar HeroBing avatar luqimin avatar younglei avatar OSdio avatar

Watchers

James Cloos avatar  avatar

Forkers

baobao12356

blog's Issues

HTTP 系列之 协议入门

以 TCP/IP 协议栈为依托,由上至下、从应用层至基础设施介绍协议

  • 应用层
    • 第 1 部分:HTTP/1.1
    • 第 2 部分:Websocket
    • 第 3 部分:HTTP/2.0
  • 应用层的安全基础设施
    • 第 4 部分:TLS/SSL
  • 传输层
    • 第5部分:TCP
  • 网络层及数据链路层
    • 第 6 部分:IP 层和以太网

使用到的工具

  • Chrome
  • Wireshark
  • tcpdump
  • telnet

Http 的设计

  • HTTP/1 协议为什么会如此设计?
    • 网络分层原理、REST 架构
  • 协议的通用规则
    • 协议格式、URI、方法与响应码概览
  • 连接与消息的路由
  • 内容协商与传输
  • cookie 的设计与问题
  • 缓存的控制

HTTP/1 的协议升级

  • 支持服务器推送消息的 WebSocket 协议
    • 建立会话
    • 消息传输 • 心跳
    • 关闭会话
      -全面优化后的 HTTP/2.0 协议
  • HTTP/2.0 必须开启的 TLS/SSL 协议

TCP 与 IP 协议

  • 传输层的 TCP 协议
    • 建立连接
    • 传输数据
    • 拥塞控制
    • 关闭连接
  • 网络层的 IP 协议
    • IP 报文与路由
    • 网络层其他常用协议:ICMP、ARP、RARP
    • IPv6 的区别

TS 系列之 函数类型接口

函数类型接口

// 函数定义
function add1(x: number, y: number) {
    return x + y
}

let add2: (x: number, y: number) => number

type add3 = (x: number, y: number) => number

interface add4 {
    (x: number, y: number): number
}

add1(1, 2, 3)

function add5(x: number, y?: number) {
    return y ? x + y : x
}
add5(1)

function add6(x: number, y = 0, z: number, q = 1) {
    return x + y + z + q
}
add6(1, undefined, 3)

function add7(x: number, ...rest: number[]) {
    return x + rest.reduce((pre, cur) => pre + cur);
}
add7(1, 2, 3, 4, 5)

function add8(...rest: number[]): number;
function add8(...rest: string[]): string;
function add8(...rest: any[]) {
    let first = rest[0];
    if (typeof first === 'number') {
        return rest.reduce((pre, cur) => pre + cur);
    }
    if (typeof first === 'string') {
        return rest.join('');
    }
}
console.log(add8(1, 2))
console.log(add8('a', 'b', 'c'))

Nginx 系列之 日志切割

信号控制

TERM,INT Quick shutdown
QUIT Graceful shutdown 优雅的关闭进程,即等请求结束后再关闭
HUP Configuration reload ,Start the new worker processes with a new configuration Gracefully shutdown the old worker processes 改变配置文件,平滑的重读配置文件
USR1 Reopen the log files 重读日志,在日志按月/日分割时有用
USR2 Upgrade Executable on the fly 平滑的升级
WINCH Gracefully shutdown the worker processes 优雅关闭旧的进程(配合USR2来进行升级)

示例

#具体语法:
Kill -信号选项 nginx的主进程号
Kill -HUP 4873
 
Kill -信号控制 `cat /xxx/path/log/nginx.pid`
 
Kill -USR1 `cat /xxx/path/log/nginx.pid`

shell+定时任务+nginx信号管理,完成日志按日期存储

分析思路

  • 凌晨00:00:01,把昨天的日志重命名,放在相应的目录下
  • 再USR1信息号控制nginx重新生成新的日志文件

脚本一

#!/bin/bash
base_path='/usr/local/nginx/logs'
log_path=$(date -d yesterday +"%Y%m")
day=$(date -d yesterday +"%d")
mkdir -p $base_path/$log_path
mv $base_path/access.log $base_path/$log_path/access_$day.log
# echo $base_path/$log_path/access_$day.log
kill -USR1 `cat /usr/local/nginx/logs/nginx.pid`

脚本二

#!/bin/bash
LOGPATH=/usr/local/nginx/logs/z.com.access.log
BASEPATH=/data/$(date -d yesterday +%Y%m)
mkdir -p $BASEPATH
bak=$BASEPATH/$(date -d yesterday +%d%H%M).zcom.access.log
mv $LOGPATH $bak
touch $LOGPATH
kill -USR1 `cat /usr/local/nginx/logs/nginx.pid`

Crontab 编辑定时任务

示例

01 00 * * * /xxx/path/b.sh  #每天0时1分(建议在02-04点之间,系统负载小)

参考链接

TS 系列之 高级类型:条件类型

// T extends U ? X : Y

type TypeName<T> =
    T extends string ? "string" :
    T extends number ? "number" :
    T extends boolean ? "boolean" :
    T extends undefined ? "undefined" :
    T extends Function ? "function" :
    "object";
type T1 = TypeName<string>
type T2 = TypeName<string[]>
 
// (A | B) extends U ? X : Y
// (A extends U ? X : Y) | (B extends U ? X : Y)
type T3 = TypeName<string | string[]>

type Diff<T, U> = T extends U ? never : T
type T4 = Diff<"a" | "b" | "c", "a" | "e">
// Diff<"a", "a" | "e"> | Diff<"b", "a" | "e"> | Diff<"c", "a" | "e">
// never | "b" | "c"
// "b" | "c"

type NotNull<T> = Diff<T, null | undefined>
type T5 = NotNull<string | number | undefined | null>

// Exclude<T, U>
// NonNullable<T>

// Extract<T, U>
type T6 = Extract<"a" | "b" | "c", "a" | "e">

// ReturnType<T>
type T8 = ReturnType<() => string>

JS 系列之 JavaScript中的数据是如何存储?

栈空间和堆空间:数据是如何存储的?

对于前端开发者来说,JavaScript的内存机制是一个不被经常提及的概念 ,因此很容易被忽视。特别是一些非计算机专业的同学,对内存机制可能没有非常清晰的认识,甚至有些同学根本就不知道JavaScript的内存机制是什么。

但是如果你想成为行业专家,并打造高性能前端应用,那么你就必须要搞清楚JavaScript的内存机制了。

其实,要搞清楚JavaScript的内存机制并不是一件很困难的事,在接下来的三篇文章(数据在内存中的存放、JavaScript处理垃圾回收以及V8执行代码)中,我们将通过内存机制的介绍,循序渐进带你走进JavaScript内存的世界。

今天我们讲述第一部分的内容——JavaScript中的数据是如何存储在内存中的。虽然JavaScript并不需要直接去管理内存,但是在实际项目中为了能避开一些不必要的坑,你还是需要了解数据在内存中的存储方式的。

让人疑惑的代码
首先,我们先看下面这两段代码:

function foo(){
    var a = 1
    var b = a
    a = 2
    console.log(a)
    console.log(b)
}
foo()
function foo(){
    var a = {name:"极客时间"}
    var b = a
    a.name = "极客邦" 
    console.log(a)
    console.log(b)
}
foo()

若执行上述这两段代码,你知道它们输出的结果是什么吗?下面我们就来一个一个分析下。

执行第一段代码,打印出来a的值是2,b的值是1,这没什么难以理解的。

接着,再执行第二段代码,你会发现,仅仅改变了a中name的属性值,但是最终a和b打印出来的值都是{name:"极客邦"}。这就和我们预期的不一致了,因为我们想改变的仅仅是a的内容,但b的内容也同时被改变了。

要彻底弄清楚这个问题,我们就得先从“JavaScript是什么类型的语言”讲起。

JavaScript是什么类型的语言

每种编程语言都具有内建的数据类型,但它们的数据类型常有不同之处,使用方式也很不一样,比如C语言在定义变量之前,就需要确定变量的类型,你可以看下面这段C代码:

int main()
{
   int a = 1;
   char* b = "极客时间";
   bool c = true;
   return 0;
}

上述代码声明变量的特点是:在声明变量之前需要先定义变量类型。我们把这种在使用之前就需要确认其变量数据类型的称为静态语言。

相反地,我们把在运行过程中需要检查数据类型的语言称为动态语言。比如我们所讲的JavaScript就是动态语言,因为在声明变量之前并不需要确认其数据类型。

虽然C语言是静态,但是在C语言中,我们可以把其他类型数据赋予给一个声明好的变量,如:

c = a

前面代码中,我们把int型的变量a赋值给了bool型的变量c,这段代码也是可以编译执行的,因为在赋值过程中,C编译器会把int型的变量悄悄转换为bool型的变量,我们通常把这种偷偷转换的操作称为隐式类型转换。而支持隐式类型转换的语言称为弱类型语言,不支持隐式类型转换的语言称为强类型语言。在这点上,C和JavaScript都是弱类型语言。

对于各种语言的类型,你可以参考下图:

image

JavaScript的数据类型

现在我们知道了,JavaScript是一种弱类型的、动态的语言。那这些特点意味着什么呢?

  • 弱类型,意味着你不需要告诉JavaScript引擎这个或那个变量是什么数据类型,JavaScript引擎在运行代码的时候自己会计算出来。
  • 动态,意味着你可以使用同一个变量保存不同类型的数据。

那么接下来,我们再来看看JavaScript的数据类型,你可以看下面这段代码:

var bar
bar = 12 
bar = "极客时间"
bar = true
bar = null
bar = {name:"极客时间"}

从上述代码中你可以看出,我们声明了一个bar变量,然后可以使用各种类型的数据值赋予给该变量。

在JavaScript中,如果你想要查看一个变量到底是什么类型,可以使用“typeof”运算符。具体使用方式如下所示:

var bar
console.log(typeof bar)  //undefined
bar = 12 
console.log(typeof bar) //number
bar = "极客时间"
console.log(typeof bar)//string
bar = true
console.log(typeof bar) //boolean
bar = null
console.log(typeof bar) //object
bar = {name:"极客时间"}
console.log(typeof bar) //object

执行这段代码,你可以看到打印出来了不同的数据类型,有undefined、number、boolean、object等。那么接下来我们就来谈谈JavaScript到底有多少种数据类型。

其实JavaScript中的数据类型一种有8种,它们分别是:

image

了解这些类型之后,还有三点需要你注意一下。

第一点,使用typeof检测Null类型时,返回的是Object。这是当初JavaScript语言的一个Bug,一直保留至今,之所以一直没修改过来,主要是为了兼容老的代码。

第二点,Object类型比较特殊,它是由上述7种类型组成的一个包含了key-value对的数据类型。如下所示:

let myObj = {
  name:'极客时间',
  update:function(){....}
}

从中你可以看出来,Object是由key-value组成的,其中的vaule可以是任何类型,包括函数,这也就意味着你可以通过Object来存储函数,Object中的函数又称为方法,比如上述代码中的update方法。

第三点,我们把前面的7种数据类型称为原始类型,把最后一个对象类型称为引用类型,之所以把它们区分为两种不同的类型,是因为它们在内存中存放的位置不一样。到底怎么个不一样法呢?接下来,我们就来讲解一下JavaScript的原始类型和引用类型到底是怎么储存的。

内存空间

要理解JavaScript在运行过程中数据是如何存储的,你就得先搞清楚其存储空间的种类。下面是我画的JavaScript的内存模型,你可以参考下:

image

从图中可以看出, 在JavaScript的执行过程中, 主要有三种类型内存空间,分别是代码空间、栈空间和堆空间。

其中的代码空间主要是存储可执行代码的,这个我们后面再做介绍,今天主要来说说栈空间和堆空间。

栈空间和堆空间

这里的栈空间就是我们之前反复提及的调用栈,是用来存储执行上下文的。为了搞清楚栈空间是如何存储数据的,我们还是先看下面这段代码:

function foo(){
    var a = "极客时间"
    var b = a
    var c = {name:"极客时间"}
    var d = c
}
foo()

前面文章我们已经讲解过了,当执行一段代码时,需要先编译,并创建执行上下文,然后再按照顺序执行代码。那么下面我们来看看,当执行到第3行代码时,其调用栈的状态,你可以参考下面这张调用栈状态图:

image

从图中可以看出来,当执行到第3行时,变量a和变量b的值都被保存在执行上下文中,而执行上下文又被压入到栈中,所以你也可以认为变量a和变量b的值都是存放在栈中的。

接下来继续执行第4行代码,由于JavaScript引擎判断右边的值是一个引用类型,这时候处理的情况就不一样了,JavaScript引擎并不是直接将该对象存放到变量环境中,而是将它分配到堆空间里面,分配后该对象会有一个在“堆”中的地址,然后再将该数据的地址写进c的变量值,最终分配好内存的示意图如下所示:

image

从上图你可以清晰地观察到,对象类型是存放在堆空间的,在栈空间中只是保留了对象的引用地址,当JavaScript需要访问该数据的时候,是通过栈中的引用地址来访问的,相当于多了一道转手流程。

好了,现在你应该知道了原始类型的数据值都是直接保存在“栈”中的,引用类型的值是存放在“堆”中的。不过你也许会好奇,为什么一定要分“堆”和“栈”两个存储空间呢?所有数据直接存放在“栈”中不就可以了吗?

答案是不可以的。这是因为JavaScript引擎需要用栈来维护程序执行期间上下文的状态,如果栈空间大了话,所有的数据都存放在栈空间里面,那么会影响到上下文切换的效率,进而又影响到整个程序的执行效率。比如文中的foo函数执行结束了,JavaScript引擎需要离开当前的执行上下文,只需要将指针下移到上个执行上下文的地址就可以了,foo函数执行上下文栈区空间全部回收,具体过程你可以参考下图:

image

所以通常情况下,栈空间都不会设置太大,主要用来存放一些原始类型的小数据。而引用类型的数据占用的空间都比较大,所以这一类数据会被存放到堆中,堆空间很大,能存放很多大的数据,不过缺点是分配内存和回收内存都会占用一定的时间。

解释了程序在执行过程中为什么需要堆和栈两种数据结构后,我们还是回到示例代码那里,看看它最后一步将变量c赋值给变量d是怎么执行的?

在JavaScript中,赋值操作和其他语言有很大的不同,原始类型的赋值会完整复制变量值,而引用类型的赋值是复制引用地址。

所以d=c的操作就是把c的引用地址赋值给d,你可以参考下图:

image

从图中你可以看到,变量c和变量d都指向了同一个堆中的对象,所以这就很好地解释了文章开头的那个问题,通过c修改name的值,变量d的值也跟着改变,归根结底它们是同一个对象。

再谈闭包

现在你知道了作用域内的原始类型数据会被存储到栈空间,引用类型会被存储到堆空间,基于这两点的认知,我们再深入一步,探讨下闭包的内存模型。

这里关于闭包的一段代码为例:

function foo() {
    var myName = "极客时间"
    let test1 = 1
    const test2 = 2
    var innerBar = { 
        setName:function(newName){
            myName = newName
        },
        getName:function(){
            console.log(test1)
            return myName
        }
    }
    return innerBar
}
var bar = foo()
bar.setName("极客邦")
bar.getName()
console.log(bar.getName())

当执行这段代码的时候,你应该有过这样的分析:由于变量myName、test1、test2都是原始类型数据,所以在执行foo函数的时候,它们会被压入到调用栈中;当foo函数执行结束之后,调用栈中foo函数的执行上下文会被销毁,其内部变量myName、test1、test2也应该一同被销毁。

但是在那篇文章中,我们介绍了当foo函数的执行上下文销毁时,由于foo函数产生了闭包,所以变量myName和test1并没有被销毁,而是保存在内存中,那么应该如何解释这个现象呢?

要解释这个现象,我们就得站在内存模型的角度来分析这段代码的执行流程。

  1. 当JavaScript引擎执行到foo函数时,首先会编译,并创建一个空执行上下文。
  2. 在编译过程中,遇到内部函数setName,JavaScript引擎还要对内部函数做一次快速的词法扫描,发现该内部函数引用了foo函数中的myName变量,由于是内部函数引用了外部函数的变量,所以JavaScript引擎判断这是一个闭包,于是在堆空间创建换一个“closure(foo)”的对象(这是一个内部对象,JavaScript是无法访问的),用来保存myName变量。
  3. 接着继续扫描到getName方法时,发现该函数内部还引用变量test1,于是JavaScript引擎又将test1添加到“closure(foo)”对象中。这时候堆中的“closure(foo)”对象中就包含了myName和test1两个变量了。
  4. 由于test2并没有被内部函数引用,所以test2依然保存在调用栈中。

通过上面的分析,我们可以画出执行到foo函数中“return innerBar”语句时的调用栈状态,如下图所示:

image

从上图你可以清晰地看出,当执行到foo函数时,闭包就产生了;当foo函数执行结束之后,返回的getName和setName方法都引用“clourse(foo)”对象,所以即使foo函数退出了,“clourse(foo)”依然被其内部的getName和setName方法引用。所以在下次调用bar.setName或者bar.getName时,创建的执行上下文中就包含了“clourse(foo)”。

总的来说,产生闭包的核心有两步:第一步是需要预扫描内部函数;第二步是把内部函数引用的外部变量保存到堆中。

总结

好了,今天就讲到这里,下面我来简单总结下今天的要点。

我们介绍了JavaScript中的8种数据类型,它们可以分为两大类——原始类型和引用类型。

其中,原始类型的数据是存放在栈中,引用类型的数据是存放在堆中的。堆中的数据是通过引用和变量关联起来的。也就是说,JavaScript的变量是没有数据类型的,值才有数据类型,变量可以随时持有任何类型的数据。

然后我们分析了,在JavaScript中将一个原始类型的变量a赋值给b,那么a和b会相互独立、互不影响;但是将引用类型的变量a赋值给变量b,那会导致a、b两个变量都同时指向了堆中的同一块数据。

最后,我们还站在内存模型的视角分析了闭包的产生过程。

HTTP 系列之 报文结构

报文结构

你也许对TCP/UDP的报文格式有所了解,拿TCP报文来举例,它在实际要传输的数据之前附加了一个20字节的头部数据,存储TCP协议必须的额外信息,例如发送方的端口号、接收方的端口号、包序号、标志位等等。

有了这个附加的TCP头,数据包才能够正确传输,到了目的地后把头部去掉,就可以拿到真正的数据。

image

HTTP协议也是与TCP/UDP类似,同样也需要在实际传输的数据前附加一些头数据,不过与TCP/UDP不同的是,它是一个“纯文本”的协议,所以头数据都是ASCII码的文本,可以很容易地用肉眼阅读,不用借助程序解析也能够看懂。

HTTP协议的请求报文和响应报文的结构基本相同,由三大部分组成:

  1. 起始行(start line):描述请求或响应的基本信息;
  2. 头部字段集合(header):使用key-value形式更详细地说明报文;
  3. 消息正文(entity):实际传输的数据,它不一定是纯文本,可以是图片、视频等二进制数据。

这其中前两部分起始行和头部字段经常又合称为“请求头”或“响应头”,消息正文又称为“实体”,但与“header”对应,很多时候就直接称为“body”。

HTTP协议规定报文必须有header,但可以没有body,而且在header之后必须要有一个“空行”,也就是“CRLF”,十六进制的“0D0A”。

所以,一个完整的HTTP报文就像是下图的这个样子,注意在header和body之间有一个“空行”。

image

说到这里,我不由得想起了一部老动画片《大头儿子和小头爸爸》,你看,HTTP的报文结构像不像里面的“大头儿子”?

报文里的header就是“大头儿子”的“大头”,空行就是他的“脖子”,而后面的body部分就是他的身体了。

看一下我们用Wireshark抓的包吧。

image

在这个浏览器发出的请求报文里,第一行“GET / HTTP/1.1”就是请求行,而后面的“Host”“Connection”等等都属于header,报文的最后是一个空白行结束,没有body。

在很多时候,特别是浏览器发送GET请求的时候都是这样,HTTP报文经常是只有header而没body,相当于只发了一个超级“大头”过来,你可以想象的出来:每时每刻网络上都会有数不清的“大头儿子”在跑来跑去。

不过这个“大头”也不能太大,虽然HTTP协议对header的大小没有做限制,但各个Web服务器都不允许过大的请求头,因为头部太大可能会占用大量的服务器资源,影响运行效率。

请求行

了解了HTTP报文的基本结构后,我们来看看请求报文里的起始行也就是请求行(request line),它简要地描述了客户端想要如何操作服务器端的资源。

请求行由三部分构成:

  1. 请求方法:是一个动词,如GET/POST,表示对资源的操作;
  2. 请求目标:通常是一个URI,标记了请求方法要操作的资源;
  3. 版本号:表示报文使用的HTTP协议版本。

这三个部分通常使用空格(space)来分隔,最后要用CRLF换行表示结束。

image

还是用Wireshark抓包的数据来举例:

GET / HTTP/1.1

在这个请求行里,“GET”是请求方法,“/”是请求目标,“HTTP/1.1”是版本号,把这三部分连起来,意思就是“服务器你好,我想获取网站根目录下的默认文件,我用的协议版本号是1.1,请不要用1.0或者2.0回复我。”

别看请求行就一行,貌似很简单,其实这里面的“讲究”是非常多的,尤其是前面的请求方法和请求目标,组合起来变化多端,后面我还会详细介绍。

状态行

看完了请求行,我们再看响应报文里的起始行,在这里它不叫“响应行”,而是叫“状态行”(status line),意思是服务器响应的状态。

比起请求行来说,状态行要简单一些,同样也是由三部分构成:

  1. 版本号:表示报文使用的HTTP协议版本;
  2. 状态码:一个三位数,用代码的形式表示处理的结果,比如200是成功,500是服务器错误;
  3. 原因:作为数字状态码补充,是更详细的解释文字,帮助人理解原因。

image

看一下上一讲里Wireshark抓包里的响应报文,状态行是:

HTTP/1.1 200 OK

意思就是:“浏览器你好,我已经处理完了你的请求,这个报文使用的协议版本号是1.1,状态码是200,一切OK。”

而另一个“GET /favicon.ico HTTP/1.1”的响应报文状态行是:

HTTP/1.1 404 Not Found

翻译成人话就是:“抱歉啊浏览器,刚才你的请求收到了,但我没找到你要的资源,错误代码是404,接下来的事情你就看着办吧。”

头部字段

请求行或状态行再加上头部字段集合就构成了HTTP报文里完整的请求头或响应头,我画了两个示意图,你可以看一下。

image

image

请求头和响应头的结构是基本一样的,唯一的区别是起始行,所以我把请求头和响应头里的字段放在一起介绍。

头部字段是key-value的形式,key和value之间用“:”分隔,最后用CRLF换行表示字段结束。比如在“Host: 127.0.0.1”这一行里key就是“Host”,value就是“127.0.0.1”。

HTTP头字段非常灵活,不仅可以使用标准里的Host、Connection等已有头,也可以任意添加自定义头,这就给HTTP协议带来了无限的扩展可能。

不过使用头字段需要注意下面几点:

  1. 字段名不区分大小写,例如“Host”也可以写成“host”,但首字母大写的可读性更好;
  2. 字段名里不允许出现空格,可以使用连字符“-”,但不能使用下划线“_”。例如,“test-name”是合法的字段名,而“test name”“test_name”是不正确的字段名;
  3. 字段名后面必须紧接着“:”,不能有空格,而“:”后的字段值前可以有多个空格;
  4. 字段的顺序是没有意义的,可以任意排列不影响语义;
  5. 字段原则上不能重复,除非这个字段本身的语义允许,例如Set-Cookie。

常用头字段

HTTP协议规定了非常多的头部字段,实现各种各样的功能,但基本上可以分为四大类:

  1. 通用字段:在请求头和响应头里都可以出现;
  2. 请求字段:仅能出现在请求头里,进一步说明请求信息或者额外的附加条件;
  3. 响应字段:仅能出现在响应头里,补充说明响应报文的信息;
  4. 实体字段:它实际上属于通用字段,但专门描述body的额外信息。

对HTTP报文的解析和处理实际上主要就是对头字段的处理,理解了头字段也就理解了HTTP报文。

后续的课程中我将会以应用领域为切入点介绍连接管理、缓存控制等头字段,今天先讲几个最基本的头,看完了它们你就应该能够读懂大多数HTTP报文了。

首先要说的是Host字段,它属于请求字段,只能出现在请求头里,它同时也是唯一一个HTTP/1.1规范里要求必须出现的字段,也就是说,如果请求头里没有Host,那这就是一个错误的报文。

Host字段告诉服务器这个请求应该由哪个主机来处理,当一台计算机上托管了多个虚拟主机的时候,服务器端就需要用Host字段来选择,有点像是一个简单的“路由重定向”。

例如我们的试验环境,在127.0.0.1上有三个虚拟主机:“www.chrono.com”“www.metroid.net”和“origin.io”。那么当使用域名的方式访问时,就必须要用Host字段来区分这三个IP相同但域名不同的网站,否则服务器就会找不到合适的虚拟主机,无法处理。

User-Agent是请求字段,只出现在请求头里。它使用一个字符串来描述发起HTTP请求的客户端,服务器可以依据它来返回最合适此浏览器显示的页面。

但由于历史的原因,User-Agent非常混乱,每个浏览器都自称是“Mozilla”“Chrome”“Safari”,企图使用这个字段来互相“伪装”,导致User-Agent变得越来越长,最终变得毫无意义。

不过有的比较“诚实”的爬虫会在User-Agent里用“spider”标明自己是爬虫,所以可以利用这个字段实现简单的反爬虫策略。

Date字段是一个通用字段,但通常出现在响应头里,表示HTTP报文创建的时间,客户端可以使用这个时间再搭配其他字段决定缓存策略。

Server字段是响应字段,只能出现在响应头里。它告诉客户端当前正在提供Web服务的软件名称和版本号,例如在我们的实验环境里它就是“Server: openresty/1.15.8.1”,即使用的是OpenResty 1.15.8.1。

Server字段也不是必须要出现的,因为这会把服务器的一部分信息暴露给外界,如果这个版本恰好存在bug,那么黑客就有可能利用bug攻陷服务器。所以,有的网站响应头里要么没有这个字段,要么就给出一个完全无关的描述信息。

比如GitHub,它的Server字段里就看不出是使用了Apache还是Nginx,只是显示为“GitHub.com”。

image

实体字段里要说的一个是Content-Length,它表示报文里body的长度,也就是请求头或响应头空行后面数据的长度。服务器看到这个字段,就知道了后续有多少数据,可以直接接收。如果没有这个字段,那么body就是不定长的,需要使用chunked方式分段传输。

小结

  1. HTTP报文结构就像是“大头儿子”,由“起始行+头部+空行+实体”组成,简单地说就是“header+body”;
  2. HTTP报文可以没有body,但必须要有header,而且header后也必须要有空行,形象地说就是“大头”必须要带着“脖子”;
  3. 请求头由“请求行+头部字段”构成,响应头由“状态行+头部字段”构成;
  4. 请求行有三部分:请求方法,请求目标和版本号;
  5. 状态行也有三部分:版本号,状态码和原因字符串;
  6. 头部字段是key-value的形式,用“:”分隔,不区分大小写,顺序任意,除了规定的标准头,也可以任意添加自定义字段,实现功能扩展;
  7. HTTP/1.1里唯一要求必须提供的头字段是Host,它必须出现在请求头里,标记虚拟主机名。

HTTP 系列之 HTTP协议格式

基于ABNF语义定义的HTTP消息格式

HTTP 协议格式

协议格式如图

image

ABNF (扩充巴科斯-瑙尔范式)操作符

image

ABNF (扩充巴科斯-瑙尔范式)核心规则

巴科斯范式的英文缩写为 BNF,它是以美国人巴科斯 (Backus) 和丹麦人诺尔 (Naur) 的名字命名的一种形式化的语法表示方法,用来描述语法的一种形式体系,是一种典型的元语言。又称巴科斯 - 诺尔形式 (Backus-Naur form)。它不仅能严格地表示语法规则,而且所描述的语法是与上下文无关的。它具有语法简单,表示明确,便于语法分析和编译的特点。

参考地址

CR 是 MAC 的换行方式
LF 是 Linux 的换行方式
CRLF 是 window 的换行方式

image

基于 ABNF 描述的 HTTP 协议格式

HTTP-message = start-line *( header-field CRLF ) CRLF [ message-body ]
包含:
1. 起始行
2. 0个或多个HTTP的头部,每个HTTP必须以 CRLF结尾
3. 可选的 message-body
4. header 和 body之间,必须有 CRLF

start-line(起始行) = request-line / status-line(请求行/响应行)
包含:
1. 请求行request-line(方法 + 空格 + 路径 + 空格 + HTTP版本)
2. 响应行status-line(HTTP版本 + 空格 +响应码 + 空格 + 原因短语)

header-field(头部) = field-name ":" OWS field-value OWS
包含:
1. field-name
2. field-value
3. name和value中间 使用 ':' 分割

message-body = *OCTET
包含:
1. 0或者多个OCTET,OCTET是二进制数据

image

示例

// telnet 操作

$ telnet www.taohui.pub 80

按回车后出现
Trying 116.62.160.193...
Connected to www.taohui.pub.
Escape character is '^]'.

$ GET /wp-content/plugins/Pure-Highlightjs_1.0/assets/pure-highlight.css?ver=0.1.0 HTTP/1.1
Host:www.taohui.pub

在按回车(shift + return)即可看到下面的图

image

HTTP 系列之 Https

什么是HTTPS

由于HTTP天生“明文”的特点,整个传输过程完全透明,任何人都能够在链路中截获、修改或者伪造请求/ 响应报文,数据不具有可信性。

比如,前几讲中说过的“代理服务”。它作为HTTP通信的中间人,在数据上下行的时候可以添加或删除部 分头字段,也可以使用黑白名单过滤body里的关键字,甚至直接发送虚假的请求、响应,而浏览器和源服 务器都没有办法判断报文的真伪。

HTTP 本身存在的问题

  • 通信使用明文(不加密),内容可能被窃听
  • 无法证明报文的完整性,所以可能遭篡改
  • 不验证通信方的身份,因此有可能遭遇伪装

HTTPS是在HTTP上建立SSL加密层,并对传输数据进行加密,是HTTP协议的安全版。现在它被广泛用于万维网上安全敏感的通讯,例如交易支付方面。

HTTPS其实是一个“非常简单”的协议,RFC文档很小,只有短短的7页,里面规定了新的协议 名“https”,默认端口号443,至于其他的什么请求-应答模式、报文结构、请求方法、URI、头字段、连接 管理等等都完全沿用HTTP,没有任何新的东西。

反观HTTPS协议,它比HTTP协议相比多了以下优势:

  • 数据隐私性:内容经过对称加密,每个连接生成一个唯一的加密密钥
  • 数据完整性:内容传输经过完整性校验
  • 身份认证:第三方无法伪造服务端(客户端)身份

HTTPS如何解决HTTP上述问题?

HTTPS并非是应用层的一种新协议。只是HTTP通信接口部分用SSL和TLS协议代替而已。

通常,HTTP直接和TCP通信。当使用SSL时,则演变成先和SSL通信,再由SSL和TCP通信了。简言之,所谓HTTPS,其实就是身披SSL协议这层外壳的HTTP。

image

在采用SSL后,HTTP就拥有了HTTPS的加密、证书和完整性保护这些功能。也就是说HTTP加上加密处理和认证以及完整性保护后即是HTTPS。

image

HTTPS 协议的主要功能基本都依赖于 TLS/SSL 协议,TLS/SSL 的功能实现主要依赖于三类基本算法:散列函数 、对称加密和非对称加密,其利用非对称加密实现身份认证和密钥协商,对称加密算法采用协商的密钥对数据加密,基于散列函数验证信息的完整性。

image

对称加密

“对称加密”很好理解,就是指加密和解密时使用的密钥都是同一个,是“对称”的。只要保证了密钥的安全,那整个通信过程就可以说具有了机密性。

举个例子,你想要登录某网站,只要事先和它约定好使用一个对称密码,通信过程中传输的全是用密钥加密后的密文,只有你和网站才能解密。黑客即使能够窃听,看到的也只是乱码,因为没有密钥无法解出明文,所以就实现了机密性。

image

非对称加密

公开密钥加密使用一对非对称的密钥。一把叫做私有密钥,另一把叫做公开密钥。顾名思义,私有密钥不能让其他任何人知道,而公开密钥则可以随意发布,任何人都可以获得。

使用公开密钥加密方式,发送密文的一方使用对方的公开密钥进行加密处理,对方收到被加密的信息后,再使用自己的私有密钥进行解密。利用这种方式,不需要发送用来解密的私有密钥,也不必担心密钥被攻击者窃听而盗走。

非对称加密的特点是信息传输一对多,服务器只需要维持一个私钥就能够和多个客户端进行加密通信。

这种方式有以下缺点:

  • 公钥是公开的,所以针对私钥加密的信息,黑客截获后可以使用公钥进行解密,获取其中的内容;
  • 公钥并不包含服务器的信息,使用非对称加密算法无法确保服务器身份的合法性,存在中间人攻击的风险,服务器发送给客户端的公钥可能在传送过程中被中间人截获并篡改;
  • 使用非对称加密在数据加密解密过程需要消耗一定时间,降低了数据传输效率;

image

混合加密

使用对称密钥的好处是解密的效率比较快,使用非对称密钥的好处是可以使得传输的内容不能被破解,因为就算你拦截到了数据,但是没有对应的私钥,也是不能破解内容的。就比如说你抢到了一个保险柜,但是没有保险柜的钥匙也不能打开保险柜。那我们就将对称加密与非对称加密结合起来,充分利用两者各自的优势,在交换密钥环节使用非对称加密方式,之后的建立通信交换报文阶段则使用对称加密方式。

具体做法是:发送密文的一方使用对方的公钥进行加密处理“对称的密钥”,然后对方用自己的私钥解密拿到“对称的密钥”,这样可以确保交换的密钥是安全的前提下,使用对称加密方式进行通信。所以,HTTPS采用对称加密和非对称加密两者并用的混合加密机制。

image

解决报文可能遭篡改问题——数字签名

网络传输过程中需要经过很多中间节点,虽然数据无法被解密,但可能被篡改,那如何校验数据的完整性呢?----校验数字签名。

数字签名有两种功效

  • 能确定消息确实是由发送方签名并发出来的,因为别人假冒不了发送方的签名。
  • 数字签名能确定消息的完整性,证明数据是否未被篡改过。

将一段文本先用Hash函数生成消息摘要,然后用发送者的私钥加密生成数字签名,与原文文一起传送给接收者。接下来就是接收者校验数字签名的流程了。

校验数字签名流程:

接收者只有用发送者的公钥才能解密被加密的摘要信息,然后用HASH函数对收到的原文产生一个摘要信息,与上一步得到的摘要信息对比。如果相同,则说明收到的信息是完整的,在传输过程中没有被修改,否则说明信息被修改过,因此数字签名能够验证信息的完整性。

假设消息传递在Kobe,James两人之间发生。James将消息连同数字签名一起发送给Kobe,Kobe接收到消息后,通过校验数字签名,就可以验证接收到的消息就是James发送的。当然,这个过程的前提是Kobe知道James的公钥。问题的关键的是,和消息本身一样,公钥不能在不安全的网络中直接发送给Kobe,或者说拿到的公钥如何证明是James的。

此时就需要引入了证书颁发机构(Certificate Authority,简称CA),CA数量并不多,Kobe客户端内置了所有受信任CA的证书。CA对James的公钥(和其他信息)数字签名后生成证书。

解决通信方身份可能被伪装的问题——数字证书

  • 服务器的运营人员向第三方机构CA提交公钥、组织信息、个人信息(域名)等信息并申请认证;

  • CA通过线上、线下等多种手段验证申请者提供信息的真实性,如组织是否存在、企业是否合法,是否拥有域名的所有权等;

  • 如信息审核通过,CA会向申请者签发认证文件-证书。证书包含以下信息:申请者公钥、申请者的组织信息和个人信息、签发机构 CA的信息、有效时间、证书序列号等信息的明文,同时包含一个签名。 其中签名的产生算法:首先,使用散列函数计算公开的明文信息的信息摘要,然后,采用 CA的私钥对信息摘要进行加密,密文即签名;

  • 客户端 Client 向服务器 Server 发出请求时,Server 返回证书文件;

  • 客户端 Client 读取证书中的相关的明文信息,采用相同的散列函数计算得到信息摘要,然后,利用对应 CA的公钥解密签名数据,对比证书的信息摘要,如果一致,则可以确认证书的合法性,即服务器的公开密钥是值得信赖的。

  • 客户端还会验证证书相关的域名信息、有效时间等信息; 客户端会内置信任CA的证书信息(包含公钥),如果CA不被信任,则找不到对应 CA的证书,证书也会被判定非法。

HTTPS工作流程

  1. Client发起一个HTTPS(比如https://juejin.im/user/5a9a9cdcf265da238b7d771c)的请求,根据RFC2818的规定,Client知道需要连接Server的443(默认)端口。

  2. Server把事先配置好的公钥证书(public key certificate)返回给客户端。

  3. Client验证公钥证书:比如是否在有效期内,证书的用途是不是匹配Client请求的站点,是不是在CRL吊销列表里面,它的上一级证书是否有效,这是一个递归的过程,直到验证到根证书(操作系统内置的Root证书或者Client内置的Root证书)。如果验证通过则继续,不通过则显示警告信息。

  4. Client使用伪随机数生成器生成加密所使用的对称密钥,然后用证书的公钥加密这个对称密钥,发给Server。

  5. Server使用自己的私钥(private key)解密这个消息,得到对称密钥。至此,Client和Server双方都持有了相同的对称密钥。

  6. Server使用对称密钥加密“明文内容A”,发送给Client。

  7. Client使用对称密钥解密响应的密文,得到“明文内容A”。

  8. Client再次发起HTTPS的请求,使用对称密钥加密请求的“明文内容B”,然后Server使用对称密钥解密密文,得到“明文内容B”。

iterm2修改文件编码 解决中文问题

每次用 iterm2 的命令行创建脚手架等,或者创建文件,默认的编码格式都是 window1252,非常烦人,原来是 iterm2 的默认编码不是 UTF-8 引起的

这里教大家设置 iterm2 的编码

修改之前默认编码为, 执行 locale 显示:

LANG=
LC_COLLATE="C"
LC_CTYPE="UTF-8"
LC_MESSAGES="C"
LC_MONETARY="C"
LC_NUMERIC="C"
LC_TIME="C"
LC_ALL=

在文件 ~/.zshrc 末尾增加如下内容

export LANG=en_US.UTF-8
export LC_CTYPE="en_US.UTF-8"
export LC_NUMERIC="en_US.UTF-8"
export LC_TIME="en_US.UTF-8"
export LC_COLLATE="en_US.UTF-8"
export LC_MONETARY="en_US.UTF-8"
export LC_MESSAGES="en_US.UTF-8"
export LC_PAPER="en_US.UTF-8"
export LC_NAME="en_US.UTF-8"
export LC_ADDRESS="en_US.UTF-8"
export LC_TELEPHONE="en_US.UTF-8"
export LC_MEASUREMENT="en_US.UTF-8"
export LC_IDENTIFICATION="en_US.UTF-8"
export LC_ALL="en_US.UTF-8"

修改后输入,执行 source ~/.zshrc 及时生效
执行 locale 查看结果

LANG="en_US.UTF-8"
LC_COLLATE="en_US.UTF-8"
LC_CTYPE="en_US.UTF-8"
LC_MESSAGES="en_US.UTF-8"
LC_MONETARY="en_US.UTF-8"
LC_NUMERIC="en_US.UTF-8"
LC_TIME="en_US.UTF-8"
LC_ALL="en_US.UTF-8"

亲测有效

移动端body上overflow:hidden失效

起因

在做移动端H5项目的时候,经常性的需要弹框,每次弹框需要页面不可滚动,所以在body上添加overflow:hidden,但是在移动端浏览器(微信浏览器/safari浏览器)上并不生效

body{
    overflow: hidden;
}

解决办法

1.通过添加position:fixed解决

于是,这时候我添加position:fixed在body,这是可以禁止浏览器滚动了,不过会滚动到头部

body{
    overflow: hidden;
    position: fixed;
}

为了兼容其他浏览器,例如safari,需要加上width: 100%;

body{
    overflow: hidden;
    position: fixed;
    width: 100%;
}

参考链接

Overflow-x:hidden doesn't prevent content from overflowing in mobile browsers

TS 系列之 基本类型

基本类型 ES vs TS

image

类型注解

作用:相当于强类型语言中的类型声明
语法:(变量 / 函数): type

// 原始类型
let bool: boolean = true
let num: number | undefined | null = 123
let str: string = 'abc'
// str = 123

// 数组
let arr1: number[] = [1, 2, 3]
let arr2: Array<number | string> = [1, 2, 3, '4']

// 元组
let tuple: [number, string] = [0, '1']
// tuple.push(2)
// console.log(tuple)
// tuple[2]

// 函数
let add = (x: number, y: number) => x + y
let compute: (x: number, y: number) => number
compute = (a, b) => a + b

// 对象
let obj: { x: number, y: number } = { x: 1, y: 2 }
obj.x = 3

// symbol
let s1: symbol = Symbol()
let s2 = Symbol()
// console.log(s1 === s2)

// undefined, null
let un: undefined = undefined
let nu: null = null
num = undefined
num = null

// void
let noReturn = () => {}

// any
let x
x = 1
x = []
x = () => {}

// never
let error = () => {
    throw new Error('error')
}
let endless = () => {
    while(true) {}
}

TS 系列之 高级类型:交叉类型与联合类型

interface DogInterface {
    run(): void
}
interface CatInterface {
    jump(): void
}
let pet: DogInterface & CatInterface = {
    run() {},
    jump() {}
}

let a: number | string = 1
let b: 'a' | 'b' | 'c'
let c: 1 | 2 | 3

class Dog implements DogInterface {
    run() {}
    eat() {}
}
class Cat  implements CatInterface {
    jump() {}
    eat() {}
}
enum Master { Boy, Girl }
function getPet(master: Master) {
    let pet = master === Master.Boy ? new Dog() : new Cat();
    // pet.run()
    // pet.jump()
    pet.eat()
    return pet
}

interface Square {
    kind: "square";
    size: number;
}
interface Rectangle {
    kind: "rectangle";
    width: number;
    height: number;
}
interface Circle {
    kind: "circle";
    radius: number;
}
type Shape = Square | Rectangle | Circle
function area(s: Shape) {
    switch (s.kind) {
        case "square":
            return s.size * s.size;
        case "rectangle":
            return s.height * s.width;
        case 'circle':
            return Math.PI * s.radius ** 2
        default:
            return ((e: never) => {throw new Error(e)})(s)
    }
}
console.log(area({kind: 'circle', radius: 1}))

HTTP 系列之 HTTP是个啥鬼东西?

HTTP 是什么鬼?

首先我来问出这个问题:“你觉得HTTP是什么呢?”

你可能会不假思索、脱口而出:“HTTP就是超文本传输协议,也就是HyperText Transfer Protocol。”

如果再问:

  1. 你是怎么理解HTTP字面上的“超文本”和“传输协议”的?
  2. 能否谈一下你对HTTP的认识?越多越好。
  3. HTTP有什么特点?有什么优点和缺点?
  4. HTTP下层都有哪些协议?是如何工作的?
  5. ……

几乎所有面试时问到的HTTP相关问题,都可以从这个最简单的“HTTP是什么?”引出来。

HTTP是什么

咱们**有个成语“人如其名”,意思是一个人的性格和特点是与他的名字相符的。

先看一下HTTP的名字:“超文本传输协议”,它可以拆成三个部分,分别是:“超文本”“传输”和“协议”。我们从后往前来逐个解析,理解了这三个词,我们也就明白了什么是HTTP。

image

首先,HTTP是一个协议。不过,协议又是什么呢?

其实“协议”并不仅限于计算机世界,现实生活中也随处可见。例如,你在刚毕业时会签一个“三方协议”,找房子时会签一个“租房协议”,公司入职时还可能会签一个“保密协议”,工作中使用的各种软件也都带着各自的“许可协议”。

刚才说的这几个都是“协议”,本质上与HTTP是相同的,那么“协议”有什么特点呢?

第一点,协议必须要有两个或多个参与者,也就是“协”。

如果只有你一个人,那你自然可以想干什么就干什么,想怎么玩就怎么玩,不会干涉其他人,其他人也不会干涉你,也就不需要所谓的“协议”。但是,一旦有了两个以上的参与者出现,为了保证最基本的顺畅交流,协议就自然而然地出现了。

例如,为了保证你顺利就业,“三方协议”里的参与者有三个:你、公司和学校;为了保证你顺利入住,“租房协议”里的参与者有两个:你和房东。

第二点,协议是对参与者的一种行为约定和规范,也就是“议”。

协议意味着有多个参与者为了达成某个共同的目的而站在了一起,除了要无疑义地沟通交流之外,还必须明确地规定各方的“责、权、利”,约定该做什么不该做什么,先做什么后做什么,做错了怎么办,有没有补救措施等等。例如,“租房协议”里就约定了,租期多少个月,每月租金多少,押金是多少,水电费谁来付,违约应如何处理等等。

好,到这里,你应该能够明白HTTP的第一层含义了。

HTTP是一个用在计算机世界里的协议。它使用计算机能够理解的语言确立了一种计算机之间交流通信的规范,以及相关的各种控制和错误处理方式。

接下来我们看HTTP字面里的第二部分:“传输”。

计算机和网络世界里有数不清的各种角色:CPU、内存、总线、磁盘、操作系统、浏览器、网关、服务器……这些角色之间相互通信也必然会有各式各样、五花八门的协议,用处也各不相同,例如广播协议、寻址协议、路由协议、隧道协议、选举协议等等。

HTTP是一个“传输协议”,所谓的“传输”(Transfer)其实很好理解,就是把一堆东西从A点搬到B点,或者从B点搬到A点,即“A<===>B”。

别小看了这个简单的动作,它也至少包含了两项重要的信息。

第一点,HTTP协议是一个双向协议

也就是说,有两个最基本的参与者A和B,从A开始到B结束,数据在A和B之间双向而不是单向流动。通常我们把先发起传输动作的A叫做请求方,把后接到传输的B叫做应答方或者响应方。拿我们最常见的上网冲浪来举例子,浏览器就是请求方A,网易、新浪这些网站就是应答方B。双方约定用HTTP协议来通信,于是浏览器把一些数据发送给网站,网站再把一些数据发回给浏览器,最后展现在屏幕上,你就可以看到各种有意思的新闻、视频了。

第二点,数据虽然是在A和B之间传输,但并没有限制只有A和B这两个角色,允许中间有“中转”或者“接力”。

这样,传输方式就从“A<===>B”,变成了“A<=>X<=>Y<=>Z<=>B”,A到B的传输过程中可以存在任意多个“中间人”,而这些中间人也都遵从HTTP协议,只要不打扰基本的数据传输,就可以添加任意的额外功能,例如安全认证、数据压缩、编码转换等等,优化整个传输过程。

说到这里,你差不多应该能够明白HTTP的第二层含义了。

HTTP是一个在计算机世界里专门用来在两点之间传输数据的约定和规范。

讲完了“协议”和“传输”,现在,我们终于到HTTP字面里的第三部分:“超文本”。

既然HTTP是一个“传输协议”,那么它传输的“超文本”到底是什么呢?我还是用两点来进一步解释。

所谓“文本”(Text),就表示HTTP传输的不是TCP/UDP这些底层协议里被切分的杂乱无章的二进制包(datagram),而是完整的、有意义的数据,可以被浏览器、服务器这样的上层应用程序处理。

在互联网早期,“文本”只是简单的字符文字,但发展到现在,“文本”的涵义已经被大大地扩展了,图片、音频、视频、甚至是压缩包,在HTTP眼里都可以算做是“文本”。

所谓“超文本”,就是“超越了普通文本的文本”,它是文字、图片、音频和视频等的混合体,最关键的是含有“超链接”,能够从一个“超文本”跳跃到另一个“超文本”,形成复杂的非线性、网状的结构关系。

对于“超文本”,我们最熟悉的就应该是HTML了,它本身只是纯文字文件,但内部用很多标签定义了对图片、音频、视频等的链接,再经过浏览器的解释,呈现在我们面前的就是一个含有多种视听信息的页面。

OK,经过了对HTTP里这三个名词的详细解释,下次当你再面对面试官时,就可以给出比“超文本传输协议”这七个字更准确更有技术含量的答案:“HTTP是一个在计算机世界里专门在两点之间传输文字、图片、音频、视频等超文本数据的约定和规范”。

小结

  1. HTTP是一个用在计算机世界里的协议,它确立了一种计算机之间交流通信的规范,以及相关的各种控制和错误处理方式
  2. HTTP专门用来在两点之间传输数据,不能用于广播、寻址或路由
  3. HTTP传输的是文字、图片、音频、视频等超文本数据
  4. HTTP是构建互联网的重要基础技术,它没有实体,依赖许多其他的技术来实现,但同时许多技术也都依赖于它

把这些综合起来,使用递归缩写方式(模仿PHP),我们可以把HTTP定义为“与HTTP协议相关的所有应用层技术的总和”。

image

你可以对照这张图,看一下哪些部分是自己熟悉的,哪些部分是陌生的,又有哪些部分是想要进一步了解的

HTTP 系列之 http2

HTTP/1.x的缺陷

  1. 连接无法复用:连接无法复用会导致每次请求都经历三次握手和慢启动。三次握手在高延迟的场景下影响较明显,慢启动则对大量小文件请求影响较大(没有达到最大窗口请求就被终止)。

    • HTTP/1.0传输数据时,每次都需要重新建立连接,增加延迟。
    • HTTP/1.1虽然加入keep-alive可以复用一部分连接,但域名分片等情况下仍然需要建立多个connection,耗费资源,给服务器带来性能压力。
  2. Head-Of-Line Blocking(HOLB):导致带宽无法被充分利用,以及后续健康请求被阻塞。HOLB是指一系列包(package)因为第一个包被阻塞;当页面中需要请求很多资源的时候,HOLB(队头阻塞)会导致在达到最大请求数量时,剩余的资源需要等待其他资源请求完成后才能发起请求。

    • HTTP 1.0:下个请求必须在前一个请求返回后才能发出,request-response对按序发生。显然,如果某个请求长时间没有返回,那么接下来的请求就全部阻塞了。
    • HTTP 1.1:尝试使用 pipeling 来解决,即浏览器可以一次性发出多个请求(同个域名,同一条 TCP 链接)。但 pipeling 要求返回是按序的,那么前一个请求如果很耗时(比如处理大图片),那么后面的请求即使服务器已经处理完,仍会等待前面的请求处理完才开始按序返回。所以,pipeling 只部分解决了 HOLB。
  3. 协议开销大: HTTP1.x在使用时,header里携带的内容过大,在一定程度上增加了传输的成本,并且每次请求header基本不怎么变化,尤其在移动端增加用户流量。

  4. 安全因素:HTTP1.x在传输数据时,所有传输的内容都是明文,客户端和服务器端都无法验证对方的身份,这在一定程度上无法保证数据的安全性

HTTP/2 简介

1.二进制传输

HTTP/2 采用二进制格式传输数据,而非 HTTP 1.x 的文本格式,二进制协议解析起来更高效。 HTTP / 1 的请求和响应报文,都是由起始行,首部和实体正文(可选)组成,各部分之间以文本换行符分隔。HTTP/2 将请求和响应数据分割为更小的帧,并且它们采用二进制编码。

HTTP/2 中,同域名下所有通信都在单个连接上完成,该连接可以承载任意数量的双向数据流。每个数据流都以消息的形式发送,而消息又由一个或多个帧组成。多个帧之间可以乱序发送,根据帧首部的流标识可以重新组装。

2.多路复用

在 HTTP/2 中引入了多路复用的技术。多路复用很好的解决了浏览器限制同一个域名下的请求数量的问题,同时也接更容易实现全速传输,毕竟新开一个 TCP 连接都需要慢慢提升传输速度。

3.Header 压缩

4.Server Push

Nginx 系列之 Expires缓存设置

Expires 设置

对于网站的图片,尤其是新闻站, 图片一旦发布, 改动的可能是非常小的.我们希望 能否在用户访问一次后, 图片缓存在用户的浏览器端,且时间比较长的缓存.
可以, 用到 nginx的expires设置 .
nginx中设置过期时间,非常简单,
在location或if段里,来写.

格式

expire -1;
expire 0;
expire 1h;
expire max;
expire off;

这个指令表示在HTTP响应头中是否增加或修改Expires 和 Cache-Control ,仅当响应状态是200, 201, 204, 206, 301, 302, 303, 304, 或者 307的时候有效。

当expire 为负时,会在响应头增加Cache-Control: no-cache;

当为正或者0时,就表示Cache-Control: max-age=指定的时间(秒);

当为max时,会把Expires设置为 “Thu, 31 Dec 2037 23:55:55 GMT”, Cache-Control 设置到 10 年;

当为off时,表示不增加或修改Expires 和 Cache-Control 。

例如

$ curl -I http://su.bdimg.com/static/superplus/css/super_min_2b5190eb.css
HTTP/1.1 200 OK
Server: JSP3/2.0.4
Date: Fri, 31 Oct 2014 07:28:20 GMT
Content-Type: text/css
Content-Length: 25076
Connection: keep-alive
ETag: “1121644245”
Last-Modified: Wed, 24 Sep 2014 15:50:22 GMT
Expires: Mon, 23 Mar 2015 16:29:25 GMT
Age: 3164335
Cache-Control: max-age=15552000
Accept-Ranges: bytes
Vary: Accept-Encoding

Date 的意思是服务器发送消息的时间; Age 的意思有点复杂,它的存在暗示你访问的服务器不是源服务器,而是一台缓存服务器,Age 的大小表示这个资源已经”存活了”多长时间,所以这个值不会大于 源服务器设置的最大缓存时间。

这里Expires 表示过期时间,Cache-Control 表示最大的存活时间,在服务器端的Nginx 我们可以用 expires 指令来定义这两项。

另: 304 也是一种很好的缓存手段
原理是: 服务器响应文件内容是,同时响应etag标签(内容的签名,内容一变,他也变), 和 last_modified_since 2个标签值
浏览器下次去请求时,头信息发送这两个标签, 服务器检测文件有没有发生变化,如无,直接头信息返回 etag,last_modified_since
浏览器知道内容无改变,于是直接调用本地缓存.
这个过程,也请求了服务器,但是传着的内容极少.
对于变化周期较短的,如静态html,js,css,比较适于用这个方式

参考链接

TS 系列之 类

Demos

abstract class Animal {
    eat() {
        console.log('eat')
    }
    abstract sleep(): void
}
// let animal = new Animal()

class Dog extends Animal {
    constructor(name: string) {
        super()
        this.name = name
        this.pri()
    }
    public name: string = 'dog'
    run() {}
    private pri() {}
    protected pro() {}
    readonly legs: number = 4
    static food: string = 'bones'
    sleep() {
        console.log('Dog sleep')
    }
}
console.log(Dog.prototype)
let dog = new Dog('wangwang')
console.log(dog)

// dog.pri() // error
// dog.pro()
console.log(Dog.food)
dog.eat()

class Husky extends Dog {
    constructor(name: string, public color: string) {
        super(name)
        this.color = color
        // this.pri() // error
        this.pro()
    }
    // color: string
}
console.log(Husky.food)

class Cat extends Animal {
    sleep() {
        console.log('Cat sleep')
    }
}
let cat = new Cat()

let animals: Animal[] = [dog, cat]
animals.forEach(i => {
    i.sleep()
})

class Workflow {
    step1() {
        return this
    }
    step2() {
        return this
    }
}
new Workflow().step1().step2()

class MyFlow extends Workflow {
    next() {
        return this
    }
}
new MyFlow().next().step1().next().step2()

TS 系列之 类与接口

demos

interface Human {
    name: string;
    eat(): void;
}

class Asian implements Human {
    constructor(name: string) {
        this.name = name;
    }
    name: string
    eat() {}
    age: number = 0
    sleep() {}
}

interface Man extends Human {
    run(): void
}

interface Child {
    cry(): void
}

interface Boy extends Man, Child {}

let boy: Boy = {
    name: '',
    run() {},
    eat() {},
    cry() {}
}

class Auto {
    state = 1
    // private state2 = 1
}
interface AutoInterface extends Auto {

}
class C implements AutoInterface {
    state1 = 1
}
class Bus extends Auto implements AutoInterface {

}

TS 系列之 类型检查机制:类型保护

类型保护

  • TypeScript 能在特定的区块中保证变量属于某种确定的类型
  • 可以在此块中放心地引用此类型的属性,或者调用此类型的方法
enum Type { Strong, Week }

class Java {
    helloJava() {
        console.log('Hello Java')
    }
    java: any
}

class JavaScript {
    helloJavaScript() {
        console.log('Hello JavaScript')
    }
    js: any
}

function isJava(lang: Java | JavaScript): lang is Java {
    return (lang as Java).helloJava !== undefined
}

function getLanguage(type: Type, x: string | number) {
    let lang = type === Type.Strong ? new Java() : new JavaScript();
    
    if (isJava(lang)) {
        lang.helloJava();
    } else {
        lang.helloJavaScript();
    }

    // if ((lang as Java).helloJava) {
    //     (lang as Java).helloJava();
    // } else {
    //     (lang as JavaScript).helloJavaScript();
    // }

    // instanceof
    // if (lang instanceof Java) {
    //     lang.helloJava()
    //     // lang.helloJavaScript()
    // } else {
    //     lang.helloJavaScript()
    // }

    // in
    // if ('java' in lang) {
    //     lang.helloJava()
    // } else {
    //     lang.helloJavaScript()
    // }

    // typeof
    // if (typeof x === 'string') {
    //     console.log(x.length)
    // } else {
    //     console.log(x.toFixed(2))
    // }

    return lang;
}

getLanguage(Type.Week, 1)

TS 系列之 泛型

demos

// function log<T>(value: T): T {
//     console.log(value);
//     return value;
// }
// log<string[]>(['a', ',b', 'c'])
// log(['a', ',b', 'c'])

// type Log = <T>(value: T) => T
// let myLog: Log = log
// console.log(myLog)

// interface Log<T> {
//     (value: T): T
//     // <T>(value: T): T
// }

// let myLog: Log<number> = log
// myLog(1)

class Log<T> {
    run(value: T) {
        console.log(value)
        return value
    }
}
let log11 = new Log<number>()
log11.run(1)
let log22 = new Log()
log22.run({ a: 1 })

interface Length {
    length: number
}
function logAdvance<T extends Length>(value: T): T {
    console.log(value, value.length);
    return value;
}
logAdvance([1])
logAdvance('123')
logAdvance({ length: 3 })
logAdvance(1) // 类型“1”的参数不能赋给类型“Length”的参数

TS 系列之 高级类型:索引类型

let obj = {
    a: 1,
    b: 2,
    c: 3
}

// function getValues(obj: any, keys: string[]) {
//     return keys.map(key => obj[key])
// }

function getValues<T, K extends keyof T>(obj: T, keys: K[]): T[K][] {
    return keys.map(key => obj[key]) 
}
console.log(getValues(obj, ['a', 'b']))
// console.log(getValues(obj, ['d', 'e']))

// keyof T
interface Obj {
    a: number;
    b: string;
}
let key: keyof Obj

// T[K]
let value: Obj['a']


// T extends U

Nginx 系列之 Location语法

location 有”定位”的意思, 根据URI来进行不同的定位.
在虚拟主机的配置中,是必不可少的,location可以把网站的不同部分,定位到不同的处理方式上.
比如, 碰到.php, 如何调用PHP解释器? --这时就需要location

Location 的语法

location 有”定位”的意思, 根据Uri来进行不同的定位. 在虚拟主机的配置中,是必不可少的,location可以把网站的不同部分,定位到不同的处理方式上. 比如, 碰到.php, 如何调用PHP解释器? --这时就需要location location 的语法

Syntax:	location [ = | ~ | ~* | ^~ ] uri { ... }
location @name { ... }
Default:	—
Context:	server, location

中括号可以不写任何参数,此时称为一般匹配,也可以写参数
因此,大类型可以分为3种:
location = patt {} [精准匹配]
location patt{}  [一般匹配]
location ~ patt{} [正则匹配]

location示例

location = / {
    [ configuration A ]
}

location / {
    [ configuration B ]
}

location /documents/ {
    [ configuration C ]
}

location ^~ /images/ {
    [ configuration D ]
}

location ~* \.(gif|jpg|jpeg)$ {
    [ configuration E ]
}

如何发挥作用?

首先看有没有精准匹配,如果有,则停止匹配过程.

#如果 $uri == patt,匹配成功,使用configA
location = / {
    root   /var/www/html/;
    index  index.htm index.html;
}
    
  location / {
     root   /usr/local/nginx/html;
    index  index.html index.htm;
}

如果访问http://xxx.com/
定位流程是 

1: 精准匹配中 ”/”   ,得到index页为  index.htm
2: 再次访问 /index.htm , 此次内部转跳uri已经是”/index.htm” ,根目录为/usr/local/nginx/html
3: 最终结果,访问了/usr/local/nginx/html/index.htm

参考链接

Nginx系列之 Nginx的编译安装

这里主要讲解CentOS下 Nginx的编译安装,其他OS的安装方式类似

编译安装

下载地址: http://nginx.org/
安装准备: nginx依赖于pcre库,要先安装pcre

#安装依赖
[root@chengx ~]# yum install pcre pcre-devel gcc gcc-c++ autoconf automake make zlib zlib-devel openssl openssl--devel pcre pcre-devel
[root@chengx ~]# cd /usr/local/src/
[root@chengx src]# wget http://nginx.org/download/nginx-1.8.2.tar.gz
[root@chengx src]# tar zxvf nginx-1.8.2.tar.gz
[root@chengx src]# cd nginx-1.8.2
[root@chengx nginx-1.8.2]# ./configure --prefix=/usr/local/nginx --with-http_stub_status_module --with-http_ssl_module --with-http_gzip_static_module
[root@chengx nginx-1.8.2]# make && make install

常用编译选项说明

nginx大部分常用模块,编译时./configure --help以--without开头的都默认安装。

  • --prefix=PATH: 指定nginx的安装目录。默认 /usr/local/nginx
  • --conf-path=PATH: 设置nginx.conf配置文件的路径。nginx允许使用不同的配置文件启动,通过命令行中的-c选项。默认为prefix/conf/nginx.conf
  • --user=name: 设置nginx工作进程的用户。安装完成后,可以随时在nginx.conf配置文件更改user指令。默认的用户名是nobody。--group=name类似
  • --with-pcre: 设置PCRE库的源码路径,如果已通过yum方式安装,使用--with-pcre自动找到库文件。使用--with-pcre=PATH时,需要从PCRE网站下载pcre库的源码(版本4.4 - 8.30)并解压,剩下的就交给Nginx的./configure和make来完成。perl正则表达式使用在location指令和 ngx_http_rewrite_module模块中。
  • --with-zlib=PATH: 指定 zlib(版本1.1.3 - 1.2.5)的源码解压目录。在默认就启用的网络传输压缩模块ngx_http_gzip_module时需要使用zlib。
  • --with-http_ssl_module: 使用https协议模块。默认情况下,该模块没有被构建。前提是openssl与openssl-devel已安装
  • --with-http_stub_status_module : 用来监控 Nginx 的当前状态
  • --with-http_realip_module : 通过这个模块允许我们改变客户端请求头中客户端IP地址值(例如X-Real-IP 或 X-Forwarded-For),意义在于能够使得后台服务器记录原始客户端的IP地址
  • --add-module=PATH : 添加第三方外部模块,如nginx-sticky-module-ng或缓存模块。每次添加新的模块都要重新编译(Tengine可以在新加入module时无需重新编译)

示例

./configure \
> --prefix=/usr \
> --sbin-path=/usr/sbin/nginx \
> --conf-path=/etc/nginx/nginx.conf \
> --error-log-path=/var/log/nginx/error.log \
> --http-log-path=/var/log/nginx/access.log \
> --pid-path=/var/run/nginx/nginx.pid  \
> --lock-path=/var/lock/nginx.lock \
> --user=nginx \
> --group=nginx \
> --with-http_ssl_module \
> --with-http_stub_status_module \
> --with-http_gzip_static_module \
> --http-client-body-temp-path=/var/tmp/nginx/client/ \
> --http-proxy-temp-path=/var/tmp/nginx/proxy/ \
> --http-fastcgi-temp-path=/var/tmp/nginx/fcgi/ \
> --http-uwsgi-temp-path=/var/tmp/nginx/uwsgi \
> --with-pcre=../pcre-7.8
> --with-zlib=../zlib-1.2.3

Nginx启动、关闭、重载

Nginx的启动、关闭、重载命令

# 检查配置文件是否正确
/usr/local/nginx/sbin/nginx -t
./sbin/nginx -V     # 可以看到编译选项

# 启动nginx
/usr/local/nginx/sbin/nginx

# 关闭nginx
/usr/local/nginx/sbin/nginx -s stop
pkill nginx
# 重启,不会改变启动时指定的配置文件
/usr/local/nginx/sbin/nginx -s reload

Nginx 系统服务管理脚本

#!/bin/sh
#
# nginx - this script starts and stops the nginx daemin
#
# chkconfig:   - 85 15
# description:  Nginx is an HTTP(S) server, HTTP(S) reverse \
#               proxy and IMAP/POP3 proxy server
# processname: nginx
# config:      /usr/local/nginx/conf/nginx.conf
# pidfile:     /usr/local/nginx/logs/nginx.pid

# Source function library.
. /etc/rc.d/init.d/functions

# Source networking configuration.
. /etc/sysconfig/network

# Check that networking is up.
[ "$NETWORKING" = "no" ] && exit 0

nginx="/usr/local/nginx/sbin/nginx"
prog=$(basename $nginx)

NGINX_CONF_FILE="/usr/local/nginx/conf/nginx.conf"

lockfile=/var/lock/subsys/nginx

start() {
    [ -x $nginx ] || exit 5
    [ -f $NGINX_CONF_FILE ] || exit 6
    echo -n $"Starting $prog: "
    daemon $nginx -c $NGINX_CONF_FILE
    retval=$?
    echo
    [ $retval -eq 0 ] && touch $lockfile
    return $retval
}

stop() {
    echo -n $"Stopping $prog: "
    killproc $prog -QUIT
    retval=$?
    echo
    [ $retval -eq 0 ] && rm -f $lockfile
    return $retval
}

restart() {
    configtest || return $?
    stop
    start
}

reload() {
    configtest || return $?
    echo -n $"Reloading $prog: "
    killproc $nginx -HUP
    RETVAL=$?
    echo
}

force_reload() {
    restart
}

configtest() {
  $nginx -t -c $NGINX_CONF_FILE
}

rh_status() {
    status $prog
}

rh_status_q() {
    rh_status >/dev/null 2>&1
}

case "$1" in
    start)
        rh_status_q && exit 0
        $1
        ;;
    stop)
        rh_status_q || exit 0
        $1
        ;;
    restart|configtest)
        $1
        ;;
    reload)
        rh_status_q || exit 7
        $1
        ;;
    force-reload)
        force_reload
        ;;
    status)
        rh_status
        ;;
    condrestart|try-restart)
        rh_status_q || exit 0
            ;;
    *)
        echo $"Usage: $0 {start|stop|status|restart|condrestart|try-restart|reload|force-reload|configtest}"
        exit 2
esac

Nginx配置文件

Nginx配置文件主要分成四部分:main(全局设置)、server(主机设置)、upstream(上游服务器设置,主要为反向代理、负载均衡相关配置)和 location(URL匹配特定位置后的设置),每部分包含若干个指令。main部分设置的指令将影响其它所有部分的设置;server部分的指令主要用于指定虚拟主机域名、IP和端口;upstream的指令用于设置一系列的后端服务器,设置反向代理及后端服务器的负载均衡;location部分用于匹配网页位置(比如,根目录“/”,“/images”,等等)。他们之间的关系式:server继承main,location继承server;upstream既不会继承指令也不会被继承。它有自己的特殊指令,不需要在其他地方的应用。

下面的nginx.conf简单的实现nginx在前端做反向代理服务器的例子,处理js、png等静态文件,jsp等动态请求转发到其它服务器tomcat:

user  www www;
worker_processes  2;

error_log  logs/error.log;
#error_log  logs/error.log  notice;
#error_log  logs/error.log  info;

pid        logs/nginx.pid;


events {
    use epoll;
    worker_connections  2048;
}


http {
    include       mime.types;
    default_type  application/octet-stream;

    #log_format  main  '$remote_addr - $remote_user [$time_local] "$request" '
    #                  '$status $body_bytes_sent "$http_referer" '
    #                  '"$http_user_agent" "$http_x_forwarded_for"';

    #access_log  logs/access.log  main;

    sendfile        on;
    # tcp_nopush     on;

    keepalive_timeout  65;

  # gzip压缩功能设置
    gzip on;
    gzip_min_length 1k;
    gzip_buffers    4 16k;
    gzip_http_version 1.0;
    gzip_comp_level 6;
    gzip_types text/html text/plain text/css text/javascript application/json application/javascript application/x-javascript application/xml;
    gzip_vary on;

  # http_proxy 设置
    client_max_body_size   10m;
    client_body_buffer_size   128k;
    proxy_connect_timeout   75;
    proxy_send_timeout   75;
    proxy_read_timeout   75;
    proxy_buffer_size   4k;
    proxy_buffers   4 32k;
    proxy_busy_buffers_size   64k;
    proxy_temp_file_write_size  64k;
    proxy_temp_path   /usr/local/nginx/proxy_temp 1 2;

  # 设定负载均衡后台服务器列表
    upstream  backend  {
              #ip_hash;
              server   192.168.10.100:8080 max_fails=2 fail_timeout=30s ;
              server   192.168.10.101:8080 max_fails=2 fail_timeout=30s ;
    }

  # 很重要的虚拟主机配置
    server {
        listen       80;
        server_name  itoatest.example.com;
        root   /apps/oaapp;

        charset utf-8;
        access_log  logs/host.access.log  main;

        #对 / 所有做负载均衡+反向代理
        location / {
            root   /apps/oaapp;
            index  index.jsp index.html index.htm;

            proxy_pass        http://backend;
            proxy_redirect off;
            # 后端的Web服务器可以通过X-Forwarded-For获取用户真实IP
            proxy_set_header  Host  $host;
            proxy_set_header  X-Real-IP  $remote_addr;
            proxy_set_header  X-Forwarded-For  $proxy_add_x_forwarded_for;
            proxy_next_upstream error timeout invalid_header http_500 http_502 http_503 http_504;

        }

        #静态文件,nginx自己处理,不去backend请求tomcat
        location  ~* /download/ {
            root /apps/oa/fs;

        }
        location ~ .*\.(gif|jpg|jpeg|bmp|png|ico|txt|js|css)$
        {
            root /apps/oaapp;
            expires      7d;
        }
       	location /nginx_status {
            stub_status on;
            access_log off;
            allow 192.168.10.0/24;
            deny all;
        }

        location ~ ^/(WEB-INF)/ {
            deny all;
        }
        #error_page  404              /404.html;

        # redirect server error pages to the static page /50x.html
        #
        error_page   500 502 503 504  /50x.html;
        location = /50x.html {
            root   html;
        }
    }

  ## 其它虚拟主机,server 指令开始
}

常用指令说明

main全局配置

nginx在运行时与具体业务功能(比如http服务或者email服务代理)无关的一些参数,比如工作进程数,运行的身份等。

  • woker_processes 2 : 在配置文件的顶级main部分,worker角色的工作进程的个数,master进程是接收并分配请求给worker处理。这个数值简单一点可以设置为cpu的核数grep ^processor /proc/cpuinfo | wc -l,也是 auto 值,如果开启了ssl和gzip更应该设置成与逻辑CPU数量一样甚至为2倍,可以减少I/O操作。如果nginx服务器还有其它服务,可以考虑适当减少。
  • worker_cpu_affinity : 也是写在main部分。在高并发情况下,通过设置cpu粘性来降低由于多CPU核切换造成的寄存器等现场重建带来的性能损耗。如worker_cpu_affinity 0001 0010 0100 1000; (四核)。
  • worker_connections 2048 : 写在events部分。每一个worker进程能并发处理(发起)的最大连接数(包含与客户端或后端被代理服务器间等所有连接数)。nginx作为反向代理服务器,计算公式 最大连接数 = worker_processes * worker_connections/4,所以这里客户端最大连接数是1024,这个可以增到到8192都没关系,看情况而定,但不能超过后面的worker_rlimit_nofile。当nginx作为http服务器时,计算公式里面是除以2。
  • worker_rlimit_nofile 10240 : 写在main部分。默认是没有设置,可以限制为操作系统最大的限制65535。
  • use epoll : 写在events部分。在Linux操作系统下,nginx默认使用epoll事件模型,得益于此,nginx在Linux操作系统下效率相当高。同时Nginx在OpenBSD或FreeBSD操作系统上采用类似于epoll的高效事件模型kqueue。在操作系统不支持这些高效模型时才使用select。

http服务器

与提供http服务相关的一些配置参数。例如:是否使用keepalive啊,是否使用gzip进行压缩等。

  • sendfile on : 开启高效文件传输模式,sendfile指令指定nginx是否调用sendfile函数来输出文件,减少用户空间到内核空间的上下文切换。对于普通应用设为 on,如果用来进行下载等应用磁盘IO重负载应用,可设置为off,以平衡磁盘与网络I/O处理速度,降低系统的负载。
  • keepalive_timeout 65 : 长连接超时时间,单位是秒,这个参数很敏感,涉及浏览器的种类、后端服务器的超时设置、操作系统的设置,可以另外起一片文章了。长连接请求大量小文件的时候,可以减少重建连接的开销,但假如有大文件上传,65s内没上传完成会导致失败。如果设置时间过长,用户又多,长时间保持连接会占用大量资源。
  • send_timeout : 用于指定响应客户端的超时时间。这个超时仅限于两个连接活动之间的时间,如果超过这个时间,客户端没有任何活动,Nginx将会关闭连接。
  • client_max_body_size 10m : 允许客户端请求的最大单文件字节数。如果有上传较大文件,请设置它的限制值
  • client_body_buffer_size 128k : 缓冲区代理缓冲用户端请求的最大字节数

模块http_proxy

这个模块实现的是nginx作为反向代理服务器的功能,包括缓存功能

  • proxy_connect_timeout 60 : nginx跟后端服务器连接超时时间(代理连接超时)
  • proxy_read_timeout 60 : 连接成功后,与后端服务器两个成功的响应操作之间超时时间(代理接收超时)
  • proxy_buffer_size 4k : 设置代理服务器(nginx)从后端realserver读取并保存用户头信息的缓冲区大小,默认与proxy_buffers大小相同,其实可以将这个指令值设的小一点
  • proxy_buffers 4 32k : proxy_buffers缓冲区,nginx针对单个连接缓存来自后端realserver的响应,网页平均在32k以下的话,这样设置
  • proxy_busy_buffers_size 64k : 高负荷下缓冲大小(proxy_buffers*2)
  • proxy_max_temp_file_size : 当 proxy_buffers 放不下后端服务器的响应内容时,会将一部分保存到硬盘的临时文件中,这个值用来设置最大临时文件大小,默认1024M,它与 proxy_cache 没有关系。大于这个值,将从upstream服务器传回。设置为0禁用。
  • proxy_temp_file_write_size 64k : 当缓存被代理的服务器响应到临时文件时,这个选项限制每次写临时文件的大小。proxy_temp_path(可以在编译的时候)指定写到哪那个目录。

模块http_gzip

  • gzip on : 开启gzip压缩输出,减少网络传输。
  • gzip_min_length 1k : 设置允许压缩的页面最小字节数,页面字节数从header头得content-length中进行获取。默认值是20。建议设置成大于1k的字节数,小于1k可能会越压越大。
  • gzip_buffers 4 16k : 设置系统获取几个单位的缓存用于存储gzip的压缩结果数据流。4 16k代表以16k为单位,安装原始数据大小以16k为单位的4倍申请内存。
  • gzip_http_version 1.0 : 用于识别 http 协议的版本,早期的浏览器不支持 Gzip 压缩,用户就会看到乱码,所以为了支持前期版本加上了这个选项,如果你用了 Nginx 的反向代理并期望也启用 Gzip 压缩的话,由于末端通信是 http/1.0,故请设置为 1.0。
  • gzip_comp_level 6 : gzip压缩比,1压缩比最小处理速度最快,9压缩比最大但处理速度最慢(传输快但比较消耗cpu)
  • gzip_types : 匹配mime类型进行压缩,无论是否指定,”text/html”类型总是会被压缩的。
  • gzip_proxied any : Nginx作为反向代理的时候启用,决定开启或者关闭后端服务器返回的结果是否压缩,匹配的前提是后端服务器必须要返回包含”Via”的 header头。
  • gzip_vary on : 和http头有关系,会在响应头加个 Vary: Accept-Encoding ,可以让前端的缓存服务器缓存经过gzip压缩的页面,例如,用Squid缓存经过Nginx压缩的数据。。

访问控制allow/deny

Nginx 的访问控制模块默认就会安装,而且写法也非常简单,可以分别有多个allow,deny,允许或禁止某个ip或ip段访问,依次满足任何一个规则就停止往下匹配。如:

location /nginx-status {
    stub_status on;
    access_log off;
    #  auth_basic   "NginxStatus";
    #  auth_basic_user_file   /usr/local/nginx-1.6/htpasswd;

    allow 192.168.10.100;
    allow 172.29.73.0/24;
    deny all;
}

我们也常用 httpd-devel 工具的 htpasswd 来为访问的路径设置登录密码:

# htpasswd -c htpasswd admin
New passwd:
Re-type new password:
Adding password for user admin

# htpasswd htpasswd admin    //修改admin密码
# htpasswd htpasswd sean    //多添加一个认证用户

这样就生成了默认使用CRYPT加密的密码文件。打开上面nginx-status的两行注释,重启nginx生效。

列出目录

Nginx默认是不允许列出整个目录的。如需此功能,打开nginx.conf文件,在location,server 或 http段中加入autoindex on;,另外两个参数最好也加上去:

  • autoindex_exact_size off : 默认为on,显示出文件的确切大小,单位是bytes。改为off后,显示出文件的大概大小,单位是kB或者MB或者GB
  • autoindex_localtime on : 默认为off,显示的文件时间为GMT时间。改为on后,显示的文件时间为文件的服务器时间
location /images {
    root   /var/www/nginx-default/images;
    autoindex on;
    autoindex_exact_size off;
    autoindex_localtime on;
}

参考链接

HTTP 系列之 连接管理

短连接

HTTP协议最初(0.9/1.0)是个非常简单的协议,通信过程也采用了简单的“请求-应答”方式。

它底层的数据传输基于TCP/IP,每次发送请求前需要先与服务器建立连接,收到响应报文后会立即关闭连接。

因为客户端与服务器的整个连接过程很短暂,不会与服务器保持长时间的连接状态,所以就被称为“短连接”(short-lived connections)。早期的HTTP协议也被称为是“无连接”的协议。

短连接的缺点相当严重,因为在TCP协议里,建立连接和关闭连接都是非常“昂贵”的操作。TCP建立连接要有“三次握手”,发送3个数据包,需要1个RTT;关闭连接是“四次挥手”,4个数据包需要2个RTT。

而HTTP的一次简单“请求-响应”通常只需要4个包,如果不算服务器内部的处理时间,最多是2个RTT。这么算下来,浪费的时间就是“3÷5=60%”,有三分之二的时间被浪费掉了,传输效率低得惊人。

image

单纯地从理论上讲,TCP协议你可能还不太好理解,我就拿打卡考勤机来做个形象的比喻吧。

假设你的公司买了一台打卡机,放在前台,因为这台机器比较贵,所以专门做了一个保护罩盖着它,公司要求每次上下班打卡时都要先打开盖子,打卡后再盖上盖子。

可是偏偏这个盖子非常牢固,打开关闭要费很大力气,打卡可能只要1秒钟,而开关盖子却需要四五秒钟,大部分时间都浪费在了毫无意义的开关盖子操作上了。

可想而知,平常还好说,一到上下班的点在打卡机前就会排起长队,每个人都要重复“开盖-打卡-关盖”的三个步骤,你说着急不着急。

在这个比喻里,打卡机就相当于服务器,盖子的开关就是TCP的连接与关闭,而每个打卡的人就是HTTP请求,很显然,短连接的缺点严重制约了服务器的服务能力,导致它无法处理更多的请求。

长连接

针对短连接暴露出的缺点,HTTP协议就提出了“长连接”的通信方式,也叫“持久连接”(persistent connections)、“连接保活”(keep alive)、“连接复用”(connection reuse)。

其实解决办法也很简单,用的就是“成本均摊”的思路,既然TCP的连接和关闭非常耗时间,那么就把这个时间成本由原来的一个“请求-应答”均摊到多个“请求-应答”上。

这样虽然不能改善TCP的连接效率,但基于“分母效应”,每个“请求-应答”的无效时间就会降低不少,整体传输效率也就提高了。

这里我画了一个短连接与长连接的对比示意图。

image

在短连接里发送了三次HTTP“请求-应答”,每次都会浪费60%的RTT时间。而在长连接的情况下,同样发送三次请求,因为只在第一次时建立连接,在最后一次时关闭连接,所以浪费率就是“3÷9≈33%”,降低了差不多一半的时间损耗。显然,如果在这个长连接上发送的请求越多,分母就越大,利用率也就越高。

继续用刚才的打卡机的比喻,公司也觉得这种反复“开盖-打卡-关盖”的操作太“反人类”了,于是颁布了新规定,早上打开盖子后就不用关上了,可以自由打卡,到下班后再关上盖子。

这样打卡的效率(即服务能力)就大幅度提升了,原来一次打卡需要五六秒钟,现在只要一秒就可以了,上下班时排长队的景象一去不返,大家都开心。

连接相关的头字段

由于长连接对性能的改善效果非常显著,所以在HTTP/1.1中的连接都会默认启用长连接。不需要用什么特殊的头字段指定,只要向服务器发送了第一次请求,后续的请求都会重复利用第一次打开的TCP连接,也就是长连接,在这个连接上收发数据。

当然,我们也可以在请求头里明确地要求使用长连接机制,使用的字段是Connection,值是“keep-alive”。

不过不管客户端是否显式要求长连接,如果服务器支持长连接,它总会在响应报文里放一个“Connection: keep-alive”字段,告诉客户端:“我是支持长连接的,接下来就用这个TCP一直收发数据吧”。

用Chrome看一下服务器返回的响应头:

image

不过长连接也有一些小缺点,问题就出在它的“长”字上。

因为TCP连接长时间不关闭,服务器必须在内存里保存它的状态,这就占用了服务器的资源。如果有大量的空闲长连接只连不发,就会很快耗尽服务器的资源,导致服务器无法为真正有需要的用户提供服务。

所以,长连接也需要在恰当的时间关闭,不能永远保持与服务器的连接,这在客户端或者服务器都可以做到。

在客户端,可以在请求头里加上“Connection: close”字段,告诉服务器:“这次通信后就关闭连接”。服务器看到这个字段,就知道客户端要主动关闭连接,于是在响应报文里也加上这个字段,发送之后就调用Socket API关闭TCP连接。

服务器端通常不会主动关闭连接,但也可以使用一些策略。拿Nginx来举例,它有两种方式:

  1. 使用“keepalive_timeout”指令,设置长连接的超时时间,如果在一段时间内连接上没有任何数据收发就主动断开连接,避免空闲连接占用系统资源。
  2. 使用“keepalive_requests”指令,设置长连接上可发送的最大请求次数。比如设置成1000,那么当Nginx在这个连接上处理了1000个请求后,也会主动断开连接。

另外,客户端和服务器都可以在报文里附加通用头字段“Keep-Alive: timeout=value”,限定长连接的超时时间。但这个字段的约束力并不强,通信的双方可能并不会遵守,所以不太常见。

我们的实验环境配置了“keepalive_timeout 60”和“keepalive_requests 5”,意思是空闲连接最多60秒,最多发送5个请求。所以,如果连续刷新五次页面,就能看到响应头里的“Connection: close”了。

image

队头阻塞

看完了短连接和长连接,接下来就要说到著名的“队头阻塞”(Head-of-line blocking,也叫“队首阻塞”)了。

“队头阻塞”与短连接和长连接无关,而是由HTTP基本的“请求-应答”模型所导致的。

因为HTTP规定报文必须是“一发一收”,这就形成了一个先进先出的“串行”队列。队列里的请求没有轻重缓急的优先级,只有入队的先后顺序,排在最前面的请求被最优先处理。

如果队首的请求因为处理的太慢耽误了时间,那么队列里后面的所有请求也不得不跟着一起等待,结果就是其他的请求承担了不应有的时间成本。

image

还是用打卡机做个比喻。

上班的时间点上,大家都在排队打卡,可这个时候偏偏最前面的那个人遇到了打卡机故障,怎么也不能打卡成功,急得满头大汗。等找人把打卡机修好,后面排队的所有人全迟到了。

性能优化

因为“请求-应答”模型不能变,所以“队头阻塞”问题在HTTP/1.1里无法解决,只能缓解,有什么办法呢?

公司里可以再多买几台打卡机放在前台,这样大家可以不用挤在一个队伍里,分散打卡,一个队伍偶尔阻塞也不要紧,可以改换到其他不阻塞的队伍。

这在HTTP里就是“并发连接”(concurrent connections),也就是同时对一个域名发起多个长连接,用数量来解决质量的问题。

但这种方式也存在缺陷。如果每个客户端都想自己快,建立很多个连接,用户数×并发数就会是个天文数字。服务器的资源根本就扛不住,或者被服务器认为是恶意攻击,反而会造成“拒绝服务”。

所以,HTTP协议建议客户端使用并发,但不能“滥用”并发。RFC2616里明确限制每个客户端最多并发2个连接。不过实践证明这个数字实在是太小了,众多浏览器都“无视”标准,把这个上限提高到了6~8。后来修订的RFC7230也就“顺水推舟”,取消了这个“2”的限制。

但“并发连接”所压榨出的性能也跟不上高速发展的互联网无止境的需求,还有什么别的办法吗?

公司发展的太快了,员工越来越多,上下班打卡成了迫在眉睫的大问题。前台空间有限,放不下更多的打卡机了,怎么办?那就多开几个打卡的地方,每个楼层、办公区的入口也放上三四台打卡机,把人进一步分流,不要都往前台挤。

这个就是“域名分片”(domain sharding)技术,还是用数量来解决质量的思路。

HTTP协议和浏览器不是限制并发连接数量吗?好,那我就多开几个域名,比如shard1.chrono.com、shard2.chrono.com,而这些域名都指向同一台服务器www.chrono.com,这样实际长连接的数量就又上去了,真是“美滋滋”。不过实在是有点“上有政策,下有对策”的味道。

小结

  1. 早期的HTTP协议使用短连接,收到响应后就立即关闭连接,效率很低;
  2. HTTP/1.1默认启用长连接,在一个连接上收发多个请求响应,提高了传输效率;
  3. 服务器会发送“Connection: keep-alive”字段表示启用了长连接;
  4. 报文头里如果有“Connection: close”就意味着长连接即将关闭;
  5. 过多的长连接会占用服务器资源,所以服务器会用一些策略有选择地关闭长连接;
  6. “队头阻塞”问题会导致性能下降,可以用“并发连接”和“域名分片”技术缓解。

Nginx 系列之 重定向Rewrite

Rewrite语法

rewrite功能就是,使用nginx提供的全局变量或自己设置的变量,结合正则表达式和标志位实现url重写以及重定向。rewrite只能放在server{},location{},if{}中,并且只能对域名后边的除去传递的参数外的字符串起作用,例如 http://seanlook.com/a/we/index.php?id=1&u=str 只对/a/we/index.php重写。语法rewrite regex replacement [flag];

如果相对域名或参数字符串起作用,可以使用全局变量匹配,也可以使用proxy_pass反向代理。

表明看rewrite和location功能有点像,都能实现跳转,主要区别在于rewrite是在同一域名内更改获取资源的路径,而location是对一类路径做控制访问或反向代理,可以proxy_pass到其他机器。很多情况下rewrite也会写在location里,它们的执行顺序是:

  • 执行server块的rewrite指令
  • 执行location匹配
  • 执行选定的location中的rewrite指令

如果其中某步URI被重写,则重新循环执行1-3,直到找到真实存在的文件;循环超过10次,则返回500 Internal Server Error错误。

flag标志位

  • last : 相当于Apache的[L]标记,表示完成rewrite
  • break : 停止执行当前虚拟主机的后续rewrite指令集
  • redirect : 返回302临时重定向,地址栏会显示跳转后的地址
  • permanent : 返回301永久重定向,地址栏会显示跳转后的地址

因为301和302不能简单的只返回状态码,还必须有重定向的URL,这就是return指令无法返回301,302的原因了。这里 last 和 break 区别有点难以理解:

  • last一般写在server和if中,而break一般使用在location中
  • last不终止重写后的url匹配,即新的url会再从server走一遍匹配流程,而break终止重写后的匹配
  • break和last都能组织继续执行后面的rewrite指令

if指令与全局变量

if判断指令
语法为if(condition){...},对给定的条件condition进行判断。如果为真,大括号内的rewrite指令将被执行,if条件(conditon)可以是如下任何内容:

  • 当表达式只是一个变量时,如果值为空或任何以0开头的字符串都会当做false
  • 直接比较变量和内容时,使用=或!=
  • 正则表达式匹配,*不区分大小写的匹配,!~区分大小写的不匹配

-f和!-f用来判断是否存在文件
-d和!-d用来判断是否存在目录
-e和!-e用来判断是否存在文件或目录
-x和!-x用来判断文件是否可执行

例如

if ($http_user_agent ~ MSIE) {
    rewrite ^(.*)$ /msie/$1 break;
} //如果UA包含"MSIE",rewrite请求到/msid/目录下

if ($http_cookie ~* "id=([^;]+)(?:;|$)") {
    set $id $1;
} //如果cookie匹配正则,设置变量$id等于正则引用部分

if ($request_method = POST) {
    return 405;
} //如果提交方法为POST,则返回状态405(Method not allowed)。return不能返回301,302

if ($slow) {
    limit_rate 10k;
} //限速,$slow可以通过 set 指令设置

if (!-f $request_filename){
    break;
    proxy_pass  http://127.0.0.1; 
} //如果请求的文件名不存在,则反向代理到localhost 。这里的break也是停止rewrite检查

if ($args ~ post=140){
    rewrite ^ http://example.com/ permanent;
} //如果query string中包含"post=140",永久重定向到example.com

location ~* \.(gif|jpg|png|swf|flv)$ {
    valid_referers none blocked www.jefflei.com www.leizhenfang.com;
    if ($invalid_referer) {
        return 404;
    } //防盗链
}

全局变量

  • $args : #这个变量等于请求行中的参数,同$query_string
  • $content_length : 请求头中的Content-length字段。
  • $content_type : 请求头中的Content-Type字段。
  • $document_root : 当前请求在root指令中指定的值。
  • $host : 请求主机头字段,否则为服务器名称。
  • $http_user_agent : 客户端agent信息
  • $http_cookie : 客户端cookie信息
  • $limit_rate : 这个变量可以限制连接速率。
  • $request_method : 客户端请求的动作,通常为GET或POST。
  • $remote_addr : 客户端的IP地址。
  • $remote_port : 客户端的端口。
  • $remote_user : 已经经过Auth Basic Module验证的用户名。
  • $request_filename : 当前请求的文件路径,由root或alias指令与URI请求生成。
  • $scheme : HTTP方法(如http,https)。
  • $server_protocol : 请求使用的协议,通常是HTTP/1.0或HTTP/1.1。
  • $server_addr : 服务器地址,在完成一次系统调用后可以确定这个值。
  • $server_name : 服务器名称。
  • $server_port : 请求到达服务器的端口号。
  • $request_uri : 包含请求参数的原始URI,不包含主机名,如:”/foo/bar.php?arg=baz”。
  • $uri : 不带请求参数的当前URI,$uri不包含主机名,如”/foo/bar.html”。
  • $document_uri : 与$uri相同。

例如

例:http://localhost:88/test1/test2/test.php
$host:localhost
$server_port:88
$request_uri:http://localhost:88/test1/test2/test.php
$document_uri:/test1/test2/test.php
$document_root:/var/www/html
$request_filename:/var/www/html/test1/test2/test.php

常用正则

  • . : 匹配除换行符以外的任意字符
  • ? : 重复0次或1次
    • : 重复1次或更多次
    • : 重复0次或更多次
  • \d :匹配数字
  • ^ : 匹配字符串的开始
  • $ : 匹配字符串的介绍
  • {n} : 重复n次
  • {n,} : 重复n次或更多次
  • [c] : 匹配单个字符c
  • [a-z] : 匹配a-z小写字母的任意一个

小括号()之间匹配的内容,可以在后面通过$1来引用,$2表示的是前面第二个()里的内容。正则里面容易让人困惑的是\转义特殊字符。

rewrite实例

http {
    # 定义image日志格式
    log_format imagelog '[$time_local] ' $image_file ' ' $image_type ' ' $body_bytes_sent ' ' $status;
    # 开启重写日志
    rewrite_log on;
 
    server {
        root /home/www;
 
        location / {
                # 重写规则信息
                error_log logs/rewrite.log notice; 
                # 注意这里要用‘’单引号引起来,避免{}
                rewrite '^/images/([a-z]{2})/([a-z0-9]{5})/(.*)\.(png|jpg|gif)$' /data?file=$3.$4;
                # 注意不能在上面这条规则后面加上“last”参数,否则下面的set指令不会执行
                set $image_file $3;
                set $image_type $4;
        }
 
        location /data {
                # 指定针对图片的日志格式,来分析图片类型和大小
                access_log logs/images.log mian;
                root /data/images;
                # 应用前面定义的变量。判断首先文件在不在,不在再判断目录在不在,如果还不在就跳转到最后一个url里
                try_files /$arg_file /image404.html;
        }
        location = /image404.html {
                # 图片不存在返回特定的信息
                return 404 "image not found\n";
        }

}

对形如/images/ef/uh7b3/test.png的请求,重写到/data?file=test.png,于是匹配到location /data,先看/data/images/test.png文件存不存在,如果存在则正常响应,如果不存在则重写tryfiles到新的image404 location,直接返回404状态码。

例2

rewrite ^/images/(.*)_(\d+)x(\d+)\.(png|jpg|gif)$ /resizer/$1.$4?width=$2&height=$3? last;

对形如/images/bla_500x400.jpg的文件请求,重写到/resizer/bla.jpg?width=500&height=400地址,并会继续尝试匹配location。

参考链接

Nginx 系列之 虚拟主机配置

Nginx虚拟主机配置有三种方式:基于域名的虚拟主机配置、基于端口的虚拟主机配置、基于IP的虚拟主机配置

基于域名的虚拟主机配置

示例如下

server {
    listen 80;  #监听端口
    server_name a.com; #监听域名
    location / {
        root /var/www/html;   #绝对路径定位
        index index.html;
    }
}

server {
    listen 80;
    server_name z.com;

    location / {
        root z.com;  #相对路径定位,相对于nginx根目录定位
        index index.html;
    }
    access_log logs/z.com.access.log main;    #访问日志设置
}

基于端口的虚拟主机配置

示例如下

server {
    listen 8080;
    server_name z.com;

    location / {
        root html;
        index index.html;
    }
}

基于IP的虚拟主机配置

示例如下

server {
    listen 80;
    server_name 192.168.1.204;

    location / {
        root /var/www/html;
        index index.html;
    }
}

Nginx的配置段

# 全局区
worker_processes 1; # 有1个工作的子进程,可以自行修改,但太大无益,因为要争夺CPU,一般设置为 CPU数*核数
 
Event {
# 一般是配置nginx连接的特性
# 如1个word能同时允许多少连接
 worker_connections  1024; # 这是指 一个子进程最大允许连1024个连接
}
 
http {  #这是配置http服务器的主要段
     Server1 { # 这是虚拟主机段
      
            Location {  #定位,把特殊的路径或文件再次定位,如image目录单独处理
            }             # 如.php单独处理
 
     }
 
     Server2 {
     }
}

TS 系列之 对象类型接口

对象类型接口

interface List {
    readonly id: number;
    name: string;
    // [x: string]: any;
    age?: number;
}
interface Result {
    data: List[]
}
function render(result: Result) {
    result.data.forEach((value) => {
        console.log(value.id, value.name)
        if (value.age) {
            console.log(value.age)
        }
        // value.id++
    })
}
let result = {
    data: [
        {id: 1, name: 'A', sex: 'male'},
        {id: 2, name: 'B', age: 10}
    ]
}
render(result)

interface StringArray {
    [index: number]: string
}
let chars: StringArray = ['a', 'b']

interface Names {
    [x: string]: any;
    // y: number;
    [z: number]: number;
}

// let add: (x: number, y: number) => number
// interface Add {
//     (x: number, y: number): number
// }
type Add = (x: number, y: number) => number
let add: Add = (a: number, b: number) => a + b

interface Lib {
    (): void;
    version: string;
    doSomething(): void;
}

function getLib() {
    let lib = (() => {}) as Lib
    lib.version = '1.0.0'
    lib.doSomething = () => {}
    return lib;
}
let lib1 = getLib()
lib1()
let lib2 = getLib()
lib2.doSomething()

HTTP 系列之 域名解析

域名的形式

域名是一个有层次的结构,是一串用“.”分隔的多个单词,最右边的被称为“顶级域名”,然后是“二级域名”,层级关系向左依次降低。

最左边的是主机名,通常用来表明主机的用途,比如“www”表示提供万维网服务、“mail”表示提供邮件服务,不过这也不是绝对的,名字的关键是要让我们容易记忆。

看一下极客时间的域名“time.geekbang.org”,这里的“org”就是顶级域名,“geekbang”是二级域名,“time”则是主机名。使用这个域名,DNS就会把它转换成相应的IP地址,你就可以访问极客时间的网站了。

域名不仅能够代替IP地址,还有许多其他的用途。

在Apache、Nginx这样的Web服务器里,域名可以用来标识虚拟主机,决定由哪个虚拟主机来对外提供服务,比如在Nginx里就会使用“server_name”指令:

server {
    listen 80;                       #监听80端口
    server_name  time.geekbang.org;  #主机名是time.geekbang.org
    ...
}

域名本质上还是个名字空间系统,使用多级域名就可以划分出不同的国家、地区、组织、公司、部门,每个域名都是独一无二的,可以作为一种身份的标识。

举个例子吧,假设A公司里有个小明,B公司里有个小强,于是他们就可以分别说是“小明.A公司”,“小强.B公司”,即使B公司里也有个小明也不怕,可以标记为“小明.B公司”,很好地解决了重名问题。

因为这个特性,域名也被扩展到了其他应用领域,比如Java的包机制就采用域名作为命名空间,只是它使用了反序。如果极客时间要开发Java应用,那么它的包名可能就是“org.geekbang.time”。

而XML里使用URI作为名字空间,也是间接使用了域名。

域名解析

就像IP地址必须转换成MAC地址才能访问主机一样,域名也必须要转换成IP地址,这个过程就是“域名解析”。

目前全世界有几亿个站点,有几十亿网民,而每天网络上发生的HTTP流量更是天文数字。这些请求绝大多数都是基于域名来访问网站的,所以DNS就成了互联网的重要基础设施,必须要保证域名解析稳定可靠、快速高效。

DNS的核心系统是一个三层的树状、分布式服务,基本对应域名的结构:

  1. 根域名服务器(Root DNS Server):管理顶级域名服务器,返回“com”“net”“cn”等顶级域名服务器的IP地址;
  2. 顶级域名服务器(Top-level DNS Server):管理各自域名下的权威域名服务器,比如com顶级域名服务器可以返回apple.com域名服务器的IP地址;
  3. 权威域名服务器(Authoritative DNS Server):管理自己域名下主机的IP地址,比如apple.com权威域名服务器可以返回www.apple.com的IP地址。

image

在这里根域名服务器是关键,它必须是众所周知的,否则下面的各级服务器就无从谈起了。目前全世界共有13组根域名服务器,又有数百台的镜像,保证一定能够被访问到。

有了这个系统以后,任何一个域名都可以在这个树形结构里从顶至下进行查询,就好像是把域名从右到左顺序走了一遍,最终就获得了域名对应的IP地址。

例如,你要访问“www.apple.com”,就要进行下面的三次查询:

  1. 访问根域名服务器,它会告诉你“com”顶级域名服务器的地址;
  2. 访问“com”顶级域名服务器,它再告诉你“apple.com”域名服务器的地址;
  3. 最后访问“apple.com”域名服务器,就得到了“www.apple.com”的地址。

虽然核心的DNS系统遍布全球,服务能力很强也很稳定,但如果全世界的网民都往这个系统里挤,即使不挤瘫痪了,访问速度也会很慢。

所以在核心DNS系统之外,还有两种手段用来减轻域名解析的压力,并且能够更快地获取结果,基本思路就是“缓存”。

首先,许多大公司、网络运行商都会建立自己的DNS服务器,作为用户DNS查询的代理,代替用户访问核心DNS系统。这些“野生”服务器被称为“非权威域名服务器”,可以缓存之前的查询结果,如果已经有了记录,就无需再向根服务器发起查询,直接返回对应的IP地址。

这些DNS服务器的数量要比核心系统的服务器多很多,而且大多部署在离用户很近的地方。比较知名的DNS有Google的“8.8.8.8”,Microsoft的“4.2.2.1”,还有CloudFlare的“1.1.1.1”等等。

其次,操作系统里也会对DNS解析结果做缓存,如果你之前访问过“www.apple.com”,那么下一次在浏览器里再输入这个网址的时候就不会再跑到DNS那里去问了,直接在操作系统里就可以拿到IP地址。

另外,操作系统里还有一个特殊的“主机映射”文件,通常是一个可编辑的文本,在Linux里是“/etc/hosts”,在Windows里是“C:\WINDOWS\system32\drivers\etc\hosts”,如果操作系统在缓存里找不到DNS记录,就会找这个文件。

有了上面的“野生”DNS服务器、操作系统缓存和hosts文件后,很多域名解析的工作就都不用“跋山涉水”了,直接在本地或本机就能解决,不仅方便了用户,也减轻了各级DNS服务器的压力,效率就大大提升了。

下面的这张图比较完整地表示了现在的DNS架构。

image

在Nginx里有这么一条配置指令“resolver”,它就是用来配置DNS服务器的,如果没有它,那么Nginx就无法查询域名对应的IP,也就无法反向代理到外部的网站。

resolver 8.8.8.8 valid=30s;  #指定Google的DNS,缓存30秒

域名的“新玩法”

有了域名,又有了可以稳定工作的解析系统,于是我们就可以实现比IP地址更多的“新玩法”了。

第一种,也是最简单的,“重定向”。因为域名代替了IP地址,所以可以让对外服务的域名不变,而主机的IP地址任意变动。当主机有情况需要下线、迁移时,可以更改DNS记录,让域名指向其他的机器。

比如,你有一台“buy.tv”的服务器要临时停机维护,那你就可以通知DNS服务器:“我这个buy.tv域名的地址变了啊,原先是1.2.3.4,现在是5.6.7.8,麻烦你改一下。”DNS于是就修改内部的IP地址映射关系,之后再有访问buy.tv的请求就不走1.2.3.4这台主机,改由5.6.7.8来处理,这样就可以保证业务服务不中断。

第二种,因为域名是一个名字空间,所以可以使用bind9等开源软件搭建一个在内部使用的DNS,作为名字服务器。这样我们开发的各种内部服务就都用域名来标记,比如数据库服务都用域名“mysql.inner.app”,商品服务都用“goods.inner.app”,发起网络通信时也就不必再使用写死的IP地址了,可以直接用域名,而且这种方式也兼具了第一种“玩法”的优势。

第三种“玩法”包含了前两种,也就是基于域名实现的负载均衡。

这种“玩法”也有两种方式,两种方式可以混用。

第一种方式,因为域名解析可以返回多个IP地址,所以一个域名可以对应多台主机,客户端收到多个IP地址后,就可以自己使用轮询算法依次向服务器发起请求,实现负载均衡。

第二种方式,域名解析可以配置内部的策略,返回离客户端最近的主机,或者返回当前服务质量最好的主机,这样在DNS端把请求分发到不同的服务器,实现负载均衡。

前面我们说的都是可信的DNS,如果有一些不怀好意的DNS,那么它也可以在域名这方面“做手脚”,弄一些比较“恶意”的“玩法”,举两个例子:

  • “域名屏蔽”,对域名直接不解析,返回错误,让你无法拿到IP地址,也就无法访问网站;
  • “域名劫持”,也叫“域名污染”,你要访问A网站,但DNS给了你B网站。

好在互联网上还是好人多,而且DNS又是互联网的基础设施,这些“恶意DNS”并不多见,你上网的时候不需要太过担心。

小结

  1. 域名使用字符串来代替IP地址,方便用户记忆,本质上一个名字空间系统;
  2. DNS就像是我们现实世界里的电话本、查号台,统管着互联网世界里的所有网站,是一个“超级大管家”;
  3. DNS是一个树状的分布式查询系统,但为了提高查询效率,外围有多级的缓存;
  4. 使用DNS可以实现基于域名的负载均衡,既可以在内网,也可以在外网。

问1:在浏览器地址栏里随便输入一个不存在的域名,比如就叫“www.不存在.com”,试着解释一下它的DNS解析过程。

答:

  1. 检查本地dns缓存是否存在解析"www.不存在.com"域名的ip
    2.如果没有找到继续查找本地hosts文件内是否有对应的固定记录
    3.如果hosts中还是没有那就根据本地网卡被分配的 dns server ip 来进行解析,dns server ip 一般是“非官方”的ip,比如谷歌的“8.8.8.8”,本身它也会对查找的域名解析结果进行缓存,如果它没有缓存或者缓存失效,则先去顶级域名服务器“com”去查找“不存在.com”的域名服务器ip,结果发现不存在,于是直接返回告诉浏览器域名解析错误,当然这两次查找过程是基于udp协议

问2:如果因为某些原因,DNS失效或者出错了,会出现什么后果?

如果dns失效或出错,那就访问不了了呗,我现在有个域名在国外某些国家每天都有不少访问失败的http请求,客户端直接报“Failed host lookup”的错误

TS 系列之 高级类型:映射类型

几种映射类型

  • Readonly
  • Partial
  • Pick
  • Record
interface Obj {
    a: string;
    b: number;
}
type ReadonlyObj = Readonly<Obj>

type PartialObj = Partial<Obj>

type PickObj = Pick<Obj, 'a' | 'b'>

type RecordObj = Record<'x' | 'y', Obj>

type Nullable<T> = { [P in keyof T]: T[P] | null };

interface IPoint {
    x: number,
    y: number
}

type ReadonlyPoint = Readonly<IPoint>

type PartialPoint = Partial<IPoint>

type NullPonit = Nullable<IPoint>

// 源码
// type Readonly<T> = {
//     readonly [P in keyof T]: T[P];
// };

/**
 * Make all properties in T optional
 */

// type Partial<T> = {
//     [P in keyof T]?: T[P];
// };


/**
 * From T, pick a set of properties whose keys are in the union K
 */
// type Pick<T, K extends keyof T> = {
//     [P in K]: T[P];
// };

/**
 * Construct a type with a set of properties K of type T
 */
// type Record<K extends keyof any, T> = {
//     [P in K]: T;
// };

Nginx+PHP的编译安装

安装依赖包

yum install gd gd-devel
yum install ttf
yum install freetype
yum install mysql mysql-server
yum install libvpx libjpeg libpng zlib libXpm libXpm-devel FreeType t1lib  libt1-devel -y

nginx+php的配置比较简单,核心就一句话----
把请求的信息转发给9000端口的PHP进程,
让PHP进程处理 指定目录下的PHP文件.

PHP的编译

apache一般是把php当做自己的一个模块来启动的.
而nginx则是把http请求变量(如get,user_agent等)转发给 php进程,即php独立进程,与nginx进行通信. 称为 fastcgi运行方式.
因此,为apache所编译的php,是不能用于nginx的.

注意: 我们编译的PHP 要有如下功能:
连接mysql, gd, ttf, 以fpm(fascgi)方式运行

./configure  --prefix=/usr/local/fastphp \
--with-mysql=mysqlnd \
--enable-mysqlnd \
--with-gd \
--enable-gd-native-ttf \
--enable-gd-jis-conv
--enable-fpm

cd /usr/local/fastphp
cp /usr/local/src/php-5.5.16/php.ini-development ./lib/php.ini #制作php.ini文件
cp etc/php-fpm.conf.default etc/php-fpm.conf                   #制作fpm文件
./sbin/php-fpm

PHP命令

#php-fpm 启动:
/usr/local/php/sbin/php-fpm
#php-fpm 关闭:
kill -INT `cat /var/run/php-fpm/php-fpm.pid`
#php-fpm 重启:
kill -USR2 `cat /var/run/php-fpm/php-fpm.pid`
#批量删除进程
kill -9  ps aux | grep php-fpm | awk '{print $2}'

如下例子

#1:碰到php文件,
#2: 把根目录定位到 html,
#3: 把请求上下文转交给9000端口PHP进程,
#4: 并告诉PHP进程,当前的脚本是$document_root$fastcgi_scriptname
#(注:PHP会去找这个脚本并处理,所以脚本的位置要指对)
location ~ \.php$ {
    root html;
    fastcgi_pass   127.0.0.1:9000;
    fastcgi_index  index.php;
    fastcgi_param  SCRIPT_FILENAME  $document_root$fastcgi_script_name;
    include    fastcgi_params;
}

Nginx做负载均衡器以及proxy缓存配置

nginx-sticky-module

本文主要是完成一下几个功能

  • 结合proxy和upstream模块实现nginx负载均衡
  • 结合nginx_upstream_check_module模块实现后端服务器的健康检查
  • 使用nginx-sticky-module扩展模块实现Cookie会话黏贴(session-sticky效果)
  • 使用proxy模块实现静态文件缓存
  • 使用ngx_cache_purge实现更强大的缓存清除功能

项目地址 https://bitbucket.org/nginx-goodies/nginx-sticky-module-ng

这个模块的作用是通过cookie黏贴的方式将来自同一个客户端(浏览器)的请求发送到同一个后端服务器上处理,这样一定程度上可以解决多个backend servers的session同步的问题 —— 因为不再需要同步,而RR轮询模式必须要运维人员自己考虑session同步的实现。

另外内置的 ip_hash 也可以实现根据客户端IP来分发请求,但它很容易造成负载不均衡的情况,而如果nginx前面有CDN网络或者来自同一局域网的访问,它接收的客户端IP是一样的,容易造成负载不均衡现象。淘宝Tengine的 ngx_http_upstream_session_sticky_module 也是类似的功能。nginx-sticky-module的cookie过期时间,默认浏览器关闭就过期,也就是会话方式。

这个模块并不合适不支持 Cookie 或手动禁用了cookie的浏览器,此时默认sticky就会切换成RR。它不能与ip_hash同时使用。

img1

sticky配置

# 语法
# sticky [name=route] [domain=.foo.bar] [path=/] [expires=1h] [hash=index|md5|sha1] [no_fallback]
# name: 可以为任何的 string 字符,默认是 route
# domain:哪些域名下可以使用这个 cookie
# path:哪些路径对启用 sticky,例如 path/test,那么只有 test 这个目录才会使用 sticky 做负载均衡
# expires:cookie 过期时间,默认浏览器关闭就过期,也就是会话方式。
# no_fallbackup:如果设置了这个,cookie 对应的服务器宕机了,那么将会返回502(bad gateway 或者 proxy error),建议不启用

upstream backend {
    server 192.168.1.100:8080 weight=1;
    server 192.168.1.101:8080 weight=1;
    sticky;
}

配置起来超级简单,一般来说一个sticky指令就够了。

你在查看官方文档可能会注意到里面也有个 sticky 指令,要说它们的作用几乎是一样的,但是你可能注意到This directive is available as part of our commercial subscription.的说明 —— 这是nginx商业版本里才有的特性。包括后面的check指令,在nginx的商业版本里也有对应的health_check(配在 location )实现几乎一样的监控检查功能。

load-balance其它调度方案

这里顺带介绍一下nginx的负载均衡模块支持的其它调度算法:

  • 轮询(默认) : 每个请求按时间顺序逐一分配到不同的后端服务器,如果后端某台服务器宕机,故障系统被自动剔除,使用户访问不受影响。Weight 指定轮询权值,Weight值越大,分配到的访问机率越高,主要用于后端每个服务器性能不均的情况下。
  • ip_hash : 每个请求按访问IP的hash结果分配,这样来自同一个IP的访客固定访问一个后端服务器,有效解决了动态网页存在的session共享问题。当然如果这个节点不可用了,会发到下个节点,而此时没有session同步的话就注销掉了。
  • least_conn : 请求被发送到当前活跃连接最少的realserver上。会考虑weight的值。
  • url_hash : 此方法按访问url的hash结果来分配请求,使每个url定向到同一个后端服务器,可以进一步提高后端缓存服务器的效率。Nginx本身是不支持url_hash的,如果需要使用这种调度算法,必须安装Nginx 的hash软件包 nginx_upstream_hash 。
  • fair : 这是比上面两个更加智能的负载均衡算法。此种算法可以依据页面大小和加载时间长短智能地进行负载均衡,也就是根据后端服务器的响应时间来分配请求,响应时间短的优先分配。Nginx本身是不支持fair的,如果需要使用这种调度算法,必须下载Nginx的 upstream_fair 模块。

负载均衡与健康检查

严格来说,nginx自带是没有针对负载均衡后端节点的健康检查的,但是可以通过默认自带的 ngx_http_proxy_module 模块和 ngx_http_upstream_module 模块中的相关指令来完成当后端节点出现故障时,自动切换到下一个节点来提供访问。

示例

# weight : 轮询权值也是可以用在ip_hash的,默认值为1
# max_fails : 允许请求失败的次数,默认为1。当超过最大次数时,返回proxy_next_upstream 模块定义的错误。
# fail_timeout : 有两层含义,一是在 30s 时间内最多容许 2 次失败;二是在经历了 2 次失败以后,30s时间内不分配请求到这台服务器。
# backup : 预留的备份机器。当其他所有的非backup机器出现故障的时候,才会请求backup机器,因此这台机器的压力最轻。(为什么我的1.6.3版本里配置backup启动nginx时说invalid parameter "backup"?)
# max_conns: 限制同时连接到某台后端服务器的连接数,默认为0即无限制。因为queue指令是commercial,所以还是保持默认吧。
# proxy_next_upstream : 这个指令属于 http_proxy 模块的,指定后端返回什么样的异常响应时,使用另一个realserver

upstream backend {
    ip_hash;
    server 192.168.1.101:8080:8080 weight 2;
    server 192.168.1.101:8080 weight=1 max_fails=2 fail_timeout=30s;
    server 192.168.1.102:8080 backup;
}
server {
    location / {
        proxy_pass http://backend;
        proxy_next_upstream error timeout invalid_header http_500 http_502 http_503 http_504;
    }
}

nginx_upstream_check_module

nginx_upstream_check_module 是专门提供负载均衡器内节点的健康检查的外部模块,由淘宝的姚伟斌大神开发,通过它可以用来检测后端 realserver 的健康状态。如果后端 realserver 不可用,则后面的请求就不会转发到该节点上,并持续检查几点的状态。在淘宝自己的 tengine 上是自带了该模块。项目地址

https://github.com/yaoweibin/nginx_upstream_check_module

下面的是一个带后端监控检查的 nginx.conf 配置:

upstream backend {
	# interval : 向后端发送的健康检查包的间隔。
	# fall : 如果连续失败次数达到fall_count,服务器就被认为是down。
	# rise : 如果连续成功次数达到rise_count,服务器就被认为是up。
	# timeout : 后端健康请求的超时时间。
	# default_down : 设定初始时服务器的状态,如果是true,就说明默认是down的,如果是false,就是up的。默认值是true,也就是一开始服务器认为是不可用,要等健康检查包达到一定成功次数以后才会被认为是健康的。
	# type:健康检查包的类型,现在支持以下多种类型
		# tcp:简单的tcp连接,如果连接成功,就说明后端正常。
		# http:发送HTTP请求,通过后端的回复包的状态来判断后端是否存活。
		# ajp:向后端发送AJP协议的Cping包,通过接收Cpong包来判断后端是否存活。
		# ssl_hello:发送一个初始的SSL hello包并接受服务器的SSL hello包。
		# mysql: 向mysql服务器连接,通过接收服务器的greeting包来判断后端是否存活。
		# fastcgi:发送一个fastcgi请求,通过接受解析fastcgi响应来判断后端是否存活
	# port: 指定后端服务器的检查端口。你可以指定不同于真实服务的后端服务器的端口,比如后端提供的是443端口的应用,你可以去检查80端口的状态来判断后端健康状况。默认是0,表示跟后端server提供真实服务的端口一样。该选项出现于Tengine-1.4.0。
    sticky;     # or simple round-robin
    server 192.168.1.101:8080 weight=2;
    server 192.168.1.101:8081 weight=1 max_fails=2 fail_timeout=30s ;
    server 192.168.1.102:8080 weight=1 max_fails=2 fail_timeout=30s ;
    server 192.168.1.102:8081;
    
    check interval=5000 rise=2 fall=3 timeout=1000 type=http;
    check_http_send "HEAD / HTTP/1.0\r\n\r\n";
    check_http_expect_alive http_2xx http_3xx;
}
server {
    location / {
        proxy_pass http://backend;
    }
    location /status {
        check_status;
        access_log   off;
        allow 172.29.73.23;
        deny all;
    }
}

上面配置的意思是,对name这个负载均衡条目中的所有节点,每个5秒检测一次,请求2次正常则标记 realserver状态为up,如果检测 3 次都失败,则标记 realserver的状态为down,超时时间为1秒。

check指令只能出现在upstream中

如果 type 为 http ,你还可以使用check_http_send来配置http监控检查包发送的请求内容,为了减少传输数据量,推荐采用 HEAD 方法。当采用长连接进行健康检查时,需在该指令中添加keep-alive请求头,如: HEAD / HTTP/1.1\r\nConnection: keep-alive\r\n\r\n 。当采用 GET 方法的情况下,请求uri的size不宜过大,确保可以在1个interval内传输完成,否则会被健康检查模块视为后端服务器或网络异常。

check_http_expect_alive指定HTTP回复的成功状态,默认认为 2XX 和 3XX 的状态是健康的。

Nginx的proxy缓存使用

nginx的页面缓存功能与上面的负载均衡和健康检查是没有关系的,放在这里一是因为懒得再起一篇文章,二是再有load-balance的地方一般都会启用缓存的。

缓存也就是将js、css、image等静态文件从tomcat缓存到nginx指定的缓存目录下,既可以减轻tomcat负担,也可以加快访问速度,但这样缓存及时清理成为了一个问题,所以需要 ngx_cache_purge 这个模块来在过期时间未到之前,手动清理缓存。(这里有篇 文章,对比使用缓存、不使用缓存、使用动静分离三种情况下,高并发性能比较。使用代理缓存功能性能会高出很多倍)

# proxy_temp_path : 缓存临时目录。后端的响应并不直接返回客户端,而是先写到一个临时文件中,然后被rename一下当做缓存放在 proxy_cache_path 。0.8.9版本以后允许temp和cache两个目录在不同文件系统上(分区),然而为了减少性能损失还是建议把它们设成一个文件系统上。
# proxy_cache_path ... : 设置缓存目录,目录里的文件名是 cache_key 的MD5值。levels=1:2 keys_zone=cache_one:50m表示采用2级目录结构,Web缓存区名称为cache_one,内存缓存空间大小为100MB,这个缓冲zone可以被多次使用。文件系统上看到的缓存文件名类似于 /usr/local/nginx-1.6/proxy_cache/c/29/b7f54b2df7773722d382f4809d65029c 。inactive=2d max_size=2g表示2天没有被访问的内容自动清除,硬盘最大缓存空间为2GB,超过这个大学将清除最近最少使用的数据。
# proxy_cache : 引用前面定义的缓存区 cache_one
# proxy_cache_key : 定义cache_key
# proxy_cache_valid : 为不同的响应状态码设置不同的缓存时间,比如200、302等正常结果可以缓存的时间长点,而404、500等缓存时间设置短一些,这个时间到了文件就会过期,而不论是否刚被访问过。
# expires : 在响应头里设置Expires:或Cache-Control:max-age,返回给客户端的浏览器缓存失效时间。

http {
    ... // $upstream_cache_status记录缓存命中率
    log_format  main  '$remote_addr - $remote_user [$time_local] "$request" '
                      '$status $body_bytes_sent "$http_referer" '
                      '"$http_user_agent" "$http_x_forwarded_for"'
                      '"$upstream_cache_status"';

    proxy_temp_path   /usr/local/nginx-1.6/proxy_temp;
    proxy_cache_path /usr/local/nginx-1.6/proxy_cache levels=1:2 keys_zone=cache_one:100m inactive=2d max_size=2g;

    server {
        listen       80; 
        server_name  ittest.example.com;
        root   html;
        index  index.html index.htm index.jsp;

        location ~ .*\.(gif|jpg|png|html|css|js|ico|swf|pdf)(.*) {
            proxy_pass  http://backend;
            proxy_redirect off;
            proxy_set_header Host $host;
            proxy_set_header   X-Real-IP   $remote_addr;
            proxy_set_header   X-Forwarded-For $proxy_add_x_forwarded_for;

            proxy_cache cache_one;
            add_header Nginx-Cache $upstream_cache_status;
            proxy_cache_valid  200 304 301 302 8h;
            proxy_cache_valid 404 1m;
            proxy_cache_valid  any 2d;
            proxy_cache_key $host$uri$is_args$args;
            expires 30d;
        }

        location ~ /purge(/.*) {
            #设置只允许指定的IP或IP段才可以清除URL缓存。
            allow   127.0.0.1;
            allow   172.29.73.0/24;
            deny    all;
            proxy_cache_purge  cache_one $host$1$is_args$args;
            error_page 405 =200 /purge$1;
        }
    }
}

关于缓存的失效期限上面有三个选项:X-Accel-Expires、inactive、proxy_cache_valid、expires,它们之间是有优先级的,按上面的顺序如果在header里设置 X-Accel-Expires 则它的优先级最高,否则inactive优先级最高。更多资料请参考 nginx缓存优先级 或这里。

http://7q5fot.com1.z0.glb.clouddn.com/nginx-cache-hit.png

清除缓存

上述配置的proxy_cache_purge指令用于方便的清除缓存,但必须按照第三方的 ngx_cache_purge 模块才能使用,项目地址:
https://github.com/FRiCKLE/ngx_cache_purge

使用 ngx_cache_purge 模块清除缓存有2种办法(直接删除缓存目录下的文件也算一种办法):

echo -e 'PURGE / HTTP/1.0\r\n' | nc 127.0.0.1 80

http://7q5fot.com1.z0.glb.clouddn.com/nginx-cache-purge.png

参考链接

JS 系列之 V8是如何执行一段JavaScript代码的?

编译器和解释器:V8是如何执行一段JavaScript代码的?

前面我们已经花了很多篇幅来介绍JavaScript是如何工作的,了解这些内容能帮助你从底层理解JavaScript的工作机制,从而能帮助你更好地理解和应用JavaScript。

今天这篇文章我们就继续“向下”分析,站在JavaScript引擎V8的视角,来分析JavaScript代码是如何被执行的。

前端工具和框架的自身更新速度非常块,而且还不断有新的出现。要想追赶上前端工具和框架的更新速度,你就需要抓住那些本质的知识,然后才能更加轻松地理解这些上层应用。比如我们接下来要介绍的V8执行机制,能帮助你从底层了解JavaScript,也能帮助你深入理解语言转换器Babel、语法检查工具ESLint、前端框架Vue和React的一些底层实现机制。因此,了解V8的编译流程能让你对语言以及相关工具有更加充分的认识。

要深入理解V8的工作原理,你需要搞清楚一些概念和原理,比如接下来我们要详细讲解的编译器(Compiler)解释器(Interpreter)抽象语法树(AST)字节码(Bytecode)即时编译器(JIT) 等概念,都是你需要重点关注的。

编译器和解释器

之所以存在编译器和解释器,是因为机器不能直接理解我们所写的代码,所以在执行程序之前,需要将我们所写的代码“翻译”成机器能读懂的机器语言。按语言的执行流程,可以把语言划分为编译型语言和解释型语言。

编译型语言在程序执行之前,需要经过编译器的编译过程,并且编译之后会直接保留机器能读懂的二进制文件,这样每次运行程序时,都可以直接运行该二进制文件,而不需要再次重新编译了。比如C/C++、GO等都是编译型语言。

而由解释型语言编写的程序,在每次运行时都需要通过解释器对程序进行动态解释和执行。比如Python、JavaScript等都属于解释型语言。

那编译器和解释器是如何“翻译”代码的呢?具体流程你可以参考下图:

image

从图中你可以看出这二者的执行流程,大致可阐述为如下:

  1. 在编译型语言的编译过程中,编译器首先会依次对源代码进行词法分析、语法分析,生成抽象语法树(AST),然后是优化代码,最后再生成处理器能够理解的机器码。如果编译成功,将会生成一个可执行的文件。但如果编译过程发生了语法或者其他的错误,那么编译器就会抛出异常,最后的二进制文件也不会生成成功。
  2. 在解释型语言的解释过程中,同样解释器也会对源代码进行词法分析、语法分析,并生成抽象语法树(AST),不过它会再基于抽象语法树生成字节码,最后再根据字节码来执行程序、输出结果。

V8是如何执行一段JavaScript代码的

image

从图中可以清楚地看到,V8在执行过程中既有解释器Ignition,又有编译器TurboFan,那么它们是如何配合去执行一段JavaScript代码的呢? 下面我们就按照上图来一一分解其执行流程。

1. 生成抽象语法树(AST)和执行上下文

将源代码转换为抽象语法树,并生成执行上下文,而执行上下文我们在前面的文章中已经介绍过很多了,主要是代码在执行过程中的环境信息。

那么下面我们就得重点讲解下抽象语法树(下面表述中就直接用它的简称AST了),看看什么是AST以及AST的生成过程是怎样的。

高级语言是开发者可以理解的语言,但是让编译器或者解释器来理解就非常困难了。对于编译器或者解释器来说,它们可以理解的就是AST了。所以无论你使用的是解释型语言还是编译型语言,在编译过程中,它们都会生成一个AST。这和渲染引擎将HTML格式文件转换为计算机可以理解的DOM树的情况类似。

你可以结合下面这段代码来直观地感受下什么是AST:

var myName = "极客时间"
function foo(){
  return 23;
}
myName = "geektime"
foo()

这段代码经过javascript-ast站点处理后,生成的AST结构如下:

image

从图中可以看出,AST的结构和代码的结构非常相似,其实你也可以把AST看成代码的结构化的表示,编译器或者解释器后续的工作都需要依赖于AST,而不是源代码。

AST是非常重要的一种数据结构,在很多项目中有着广泛的应用。其中最著名的一个项目是Babel。Babel是一个被广泛使用的代码转码器,可以将ES6代码转为ES5代码,这意味着你可以现在就用ES6编写程序,而不用担心现有环境是否支持ES6。Babel的工作原理就是先将ES6源码转换为AST,然后再将ES6语法的AST转换为ES5语法的AST,最后利用ES5的AST生成JavaScript源代码。

除了Babel外,还有ESLint也使用AST。ESLint是一个用来检查JavaScript编写规范的插件,其检测流程也是需要将源码转换为AST,然后再利用AST来检查代码规范化的问题。

现在你知道了什么是AST以及它的一些应用,那接下来我们再来看下AST是如何生成的。通常,生成AST需要经过两个阶段。

第一阶段是分词(tokenize),又称为词法分析,其作用是将一行行的源码拆解成一个个token。所谓token,指的是语法上不可能再分的、最小的单个字符或字符串。你可以参考下图来更好地理解什么token。

image

从图中可以看出,通过var myName = “极客时间”简单地定义了一个变量,其中关键字“var”、标识符“myName” 、赋值运算符“=”、字符串“极客时间”四个都是token,而且它们代表的属性还不一样。

第二阶段是解析(parse),又称为语法分析,其作用是将上一步生成的token数据,根据语法规则转为AST。如果源码符合语法规则,这一步就会顺利完成。但如果源码存在语法错误,这一步就会终止,并抛出一个“语法错误”。

这就是AST的生成过程,先分词,再解析。

有了AST后,那接下来V8就会生成该段代码的执行上下文。至于执行上下文的具体内容,你可以参考前面几篇文章的讲解。

2. 生成字节码

有了AST和执行上下文后,那接下来的第二步,解释器Ignition就登场了,它会根据AST生成字节码,并解释执行字节码。

其实一开始V8并没有字节码,而是直接将AST转换为机器码,由于执行机器码的效率是非常高效的,所以这种方式在发布后的一段时间内运行效果是非常好的。但是随着Chrome在手机上的广泛普及,特别是运行在512M内存的手机上,内存占用问题也暴露出来了,因为V8需要消耗大量的内存来存放转换后的机器码。为了解决内存占用问题,V8团队大幅重构了引擎架构,引入字节码,并且抛弃了之前的编译器,最终花了将进四年的时间,实现了现在的这套架构。

那什么是字节码呢?为什么引入字节码就能解决内存占用问题呢?

字节码就是介于AST和机器码之间的一种代码。但是与特定类型的机器码无关,字节码需要通过解释器将其转换为机器码后才能执行。

理解了什么是字节码,我们再来对比下高级代码、字节码和机器码,你可以参考下图:

image

从图中可以看出,机器码所占用的空间远远超过了字节码,所以使用字节码可以减少系统的内存使用。

3. 执行代码

生成字节码之后,接下来就要进入执行阶段了。

通常,如果有一段第一次执行的字节码,解释器Ignition会逐条解释执行。在执行字节码的过程中,如果发现有热点代码(HotSpot),比如一段代码被重复执行多次,这种就称为热点代码,那么后台的编译器TurboFan就会把该段热点的字节码编译为高效的机器码,然后当再次执行这段被优化的代码时,只需要执行编译后的机器码就可以了,这样就大大提升了代码的执行效率。

V8的解释器和编译器的取名也很有意思。解释器Ignition是点火器的意思,编译器TurboFan是涡轮增压的意思,寓意着代码启动时通过点火器慢慢发动,一旦启动,涡轮增压介入,其执行效率随着执行时间越来越高效率,因为热点代码都被编译器TurboFan转换了机器码,直接执行机器码就省去了字节码“翻译”为机器码的过程。

其实字节码配合解释器和编译器是最近一段时间很火的技术,比如Java和Python的虚拟机也都是基于这种技术实现的,我们把这种技术称为即时编译(JIT)。具体到V8,就是指解释器Ignition在解释执行字节码的同时,收集代码信息,当它发现某一部分代码变热了之后,TurboFan编译器便闪亮登场,把热点的字节码转换为机器码,并把转换后的机器码保存起来,以备下次使用。

对于JavaScript工作引擎,除了V8使用了“字节码+JIT”技术之外,苹果的SquirrelFish Extreme和Mozilla的SpiderMonkey也都使用了该技术。

这么多语言的工作引擎都使用了“字节码+JIT”技术,因此理解JIT这套工作机制还是很有必要的。你可以结合下图看看JIT的工作过程:

image

JavaScript的性能优化

到这里相信你现在已经了解V8是如何执行一段JavaScript代码的了。在过去几年中,JavaScript的性能得到了大幅提升,这得益于V8团队对解释器和编译器的不断改进和优化。

虽然在V8诞生之初,也出现过一系列针对V8而专门优化JavaScript性能的方案,比如隐藏类、内联缓存等概念都是那时候提出来的。不过随着V8的架构调整,你越来越不需要这些微优化策略了,相反,对于优化JavaScript执行效率,你应该将优化的中心聚焦在单次脚本的执行时间和脚本的网络下载上,主要关注以下三点内容:

  1. 提升单次脚本的执行速度,避免JavaScript的长任务霸占主线程,这样可以使得页面快速响应交互;
  2. 避免大的内联脚本,因为在解析HTML的过程中,解析和编译也会占用主线程;
  3. 减少JavaScript文件的容量,因为更小的文件会提升下载速度,并且占用更低的内存。

总结

好了,今天就讲到这里,下面我来总结下今天的内容。

  1. 首先我们介绍了编译器和解释器的区别。
  2. 紧接着又详细分析了V8是如何执行一段JavaScript代码的:V8依据JavaScript代码生成AST和执行上下文,再基于AST生成字节码,然后通过解释器执行字节码,通过编译器来优化编译字节码。
  3. 基于字节码和编译器,我们又介绍了JIT技术。
  4. 最后我们延伸说明了下优化JavaScript性能的一些策略。

之所以在本专栏里讲V8的执行流程,是因为我觉得编译器和解释器的相关概念和理论对于程序员来说至关重要,向上能让你充分理解一些前端应用的本质,向下能打开计算机编译原理的大门。通过这些知识的学习能让你将很多模糊的概念关联起来,使其变得更加清楚,从而拓宽视野,上升到更高的层次。

HTTP 系列之 与HTTP相关的各种概念

与HTTP相关的应用技术

我希望借助这张图帮你澄清与HTTP相关的各种概念和角色,让你在实际工作中清楚它们在链路中的位置和 作用,知道发起一个HTTP请求会有哪些角色参与,会如何影响请求的处理,做到“手中有粮,心中不 慌”。

image

网络世界

你一定已经习惯了现在的网络生活,甚至可能会下意识地认为网络世界就应该是这个样子的:“一张平坦而且一望无际的巨大网络,每一台电脑就是网络上的一个节点,均匀地点缀在这张网上”。

这样的理解既对,又不对。从抽象的、虚拟的层面来看,网络世界确实是这样的,我们可以从一个节点毫无障碍地访问到另一个节点。

但现实世界的网络却远比这个抽象的模型要复杂得多。实际的互联网是由许许多多个规模略小的网络连接而成的,这些“小网络”可能是只有几百台电脑的局域网,可能是有几万、几十万台电脑的广域网,可能是用电缆、光纤构成的固定网络,也可能是用基站、热点构成的移动网络......

互联网世界更像是由数不清的大小岛屿组成的“千岛之国”。

互联网的正式名称是Internet,里面存储着无穷无尽的信息资源,我们通常所说的“上网”实际上访问的只 是互联网的一个子集“万维网”(World Wide Web),它基于HTTP协议,传输HTML等超文本资源,能力 也就被限制在HTTP协议之内。

互联网上还有许多万维网之外的资源,例如常用的电子邮件、BT和Magnet点对点下载、FTP文件下载、SSH 安全登录、各种即时通信服务等等,它们需要用各自的专有协议来访问。

不过由于HTTP协议非常灵活、易于扩展,而且“超文本”的表述能力很强,所以很多其他原本不属于HTTP 的资源也可以“包装”成HTTP来访问,这就是我们为什么能够总看到各种“网页应用”——例如“微信网 页版”“邮箱网页版”——的原因。

综合起来看,现在的互联网90%以上的部分都被万维网,也就是HTTP所覆盖,所以把互联网约等于万维网 或HTTP应该也不算大错。

浏览器

上网就要用到浏览器,常见的浏览器有Google的Chrome、Mozilla的Firefox、Apple的Safari、Microsoft的 IE和Edge,还有小众的Opera以及国内的各种“换壳”的“极速”“安全”浏览器。

image

那么你想过没有,所谓的“浏览器”到底是个什么东西呢?

浏览器的正式名字叫“Web Browser”,顾名思义,就是检索、查看互联网上网页资源的应用程序,名字里 的Web,实际上指的就是“World Wide Web”,也就是万维网。

浏览器本质上是一个HTTP协议中的请求方,使用HTTP协议获取网络上的各种资源。当然,为了让我们更好地检索查看网页,它还集成了很多额外的功能。

例如,HTML排版引擎用来展示页面,JavaScript引擎用来实现动态化效果,甚至还有开发者工具用来调试网 页,以及五花八门的各种插件和扩展。

在HTTP协议里,浏览器的角色被称为“User Agent”即“用户代理”,意思是作为访问者的“代理”来发 起HTTP请求。不过在不引起混淆的情况下,我们通常都简单地称之为“客户端”

Web服务器

刚才说的浏览器是HTTP里的请求方,那么在协议另一端的应答方(响应方)又是什么呢?

这个你一定也很熟悉,答案就是服务器,Web Server。 Web服务器是一个很大也很重要的概念,它是HTTP协议里响应请求的主体,通常也把控着绝大多数的网络资源,在网络世界里处于强势地位。

当我们谈到“Web服务器”时有两个层面的含义:硬件和软件。

硬件含义就是物理形式或“云”形式的机器,在大多数情况下它可能不是一台服务器,而是利用反向代理、 负载均衡等技术组成的庞大集群。但从外界看来,它仍然表现为一台机器,但这个形象是“虚拟的”。

软件含义的Web服务器可能我们更为关心,它就是提供Web服务的应用程序,通常会运行在硬件含义的服务 器上。它利用强大的硬件能力响应海量的客户端HTTP请求,处理磁盘上的网页、图片等静态文件,或者把 请求转发给后面的Tomcat、Node.js等业务应用,返回动态的信息。

比起层出不穷的各种Web浏览器,Web服务器就要少很多了,一只手的手指头就可以数得过来。

Apache是老牌的服务器,到今天已经快25年了,功能相当完善,相关的资料很多,学习门槛低,是许多创 业者建站的入门产品。

Nginx是Web服务器里的后起之秀,特点是高性能、高稳定,且易于扩展。自2004年推出后就不断蚕食 Apache的市场份额,在高流量的网站里更是不二之选。

此外,还有Windows上的IIS、Java的Jetty/Tomcat等,因为性能不是很高,所以在互联网上应用得较少。

CDN

浏览器和服务器是HTTP协议的两个端点,那么,在这两者之间还有别的什么东西吗?

当然有了。浏览器通常不会直接连到服务器,中间会经过“重重关卡”,其中的一个重要角色就叫做CDN。

CDN,全称是“Content Delivery Network”,翻译过来就是“内容分发网络”。它应用了HTTP协议里的缓 存和代理技术,代替源站响应客户端的请求。

CDN有什么好处呢?

简单来说,它可以缓存源站的数据,让浏览器的请求不用“千里迢迢”地到达源站服务器,直接在“半 路”就可以获取响应。如果CDN的调度算法很优秀,更可以找到离用户最近的节点,大幅度缩短响应时间。

打个比方,就好像唐僧西天取经,刚出长安城,就看到阿难与迦叶把佛祖的真经递过来了,是不是很省事?

CDN也是现在互联网中的一项重要基础设施,除了基本的网络加速外,还提供负载均衡、安全防护、边缘计 算、跨运营商网络等功能,能够成倍地“放大”源站服务器的服务能力,很多云服务商都把CDN作为产品的 一部分,我也会在后面用一讲的篇幅来专门讲解CDN。

爬虫

前面说到过浏览器,它是一种用户代理,代替我们访问互联网。

但HTTP协议并没有规定用户代理后面必须是“真正的人类”,它也完全可以是“机器人”,这些“机器人”的正式名称就叫做“爬虫”(Crawler),实际上是一种可以自动访问Web资源的应用程序。

“爬虫”这个名字非常形象,它们就像是一只只不知疲倦的、辛勤的蚂蚁,在无边无际的网络上爬来爬去,不停地在网站间奔走,搜集抓取各种信息。

据估计,互联网上至少有50%的流量都是由爬虫产生的,某些特定领域的比例还会更高,也就是说,如果你 的网站今天的访问量是十万,那么里面至少有五六万是爬虫机器人,而不是真实的用户。

爬虫是怎么来的呢?

绝大多数是由各大搜索引擎“放”出来的,抓取网页存入庞大的数据库,再建立关键字索引,这样我们才能够在搜索引擎中快速地搜索到互联网角落里的页面。

爬虫也有不好的一面,它会过度消耗网络资源,占用服务器和带宽,影响网站对真实数据的分析,甚至导致 敏感信息泄漏。所以,又出现了“反爬虫”技术,通过各种手段来限制爬虫。其中一项就是“君子协 定”robots.txt,约定哪些该爬,哪些不该爬。

无论是“爬虫”还是“反爬虫”,用到的基本技术都是两个,一个是HTTP,另一个就是HTML。

HTML/WebService/WAF

到现在我已经说完了图中右边的五大部分,而左边的HTML、WebService、WAF等由于与HTTP技术上实质关 联不太大,所以就简略地介绍一下,不再过多展开。

HTML是HTTP协议传输的主要内容之一,它描述了超文本页面,用各种“标签”定义文字、图片等资源和排 版布局,最终由浏览器“渲染”出可视化页面。

HTML目前有两个主要的标准,HTML4和HTML5。广义上的HTML通常是指HTML、JavaScript、CSS等前端技 术的组合,能够实现比传统静态页面更丰富的动态页面。

接下来是Web Service,它的名字与Web Server很像,但却是一个完全不同的东西。

Web Service是一种由W3C定义的应用服务开发规范,使用client-server主从架构,通常使用WSDL定义服务接口,使用HTTP协议传输XML或SOAP消息,也就是说,它是一个基于Web(HTTP)的服务架构技术,既可 以运行在内网,也可以在适当保护后运行在外网。

因为采用了HTTP协议传输数据,所以在Web Service架构里服务器和客户端可以采用不同的操作系统或编程 语言开发。例如服务器端用Linux+Java,客户端用Windows+C#,具有跨平台跨语言的优点。

WAF是近几年比较“火”的一个词,意思是“网络应用防火墙”。与硬件“防火墙”类似,它是应用层面 的“防火墙”,专门检测HTTP流量,是防护Web应用的安全技术。

WAF通常位于Web服务器之前,可以阻止如SQL注入、跨站脚本等攻击,目前应用较多的一个开源项目是 ModSecurity,它能够完全集成进Apache或Nginx。

小结

  1. 互联网上绝大部分资源都使用HTTP协议传输;
  2. 浏览器是HTTP协议里的请求方,即User Agent;
  3. 服务器是HTTP协议里的应答方,常用的有Apache和Nginx;
  4. CDN位于浏览器和服务器之间,主要起到缓存加速的作用;
  5. 爬虫是另一类User Agent,是自动访问网络资源的程序。

TS 系列之 枚举类型

枚举

枚举:一组有名字的产量集合。

// 数字枚举
enum Role {
    Reporter = 1,
    Developer,
    Maintainer,
    Owner,
    Guest
}
// console.log(Role.Reporter)
// console.log(Role)

// 字符串枚举
enum Message {
    Success = '恭喜你,成功了',
    Fail = '抱歉,失败了'
}

// 异构枚举
enum Answer {
    N,
    Y = 'Yes'
}

// 枚举成员
// Role.Reporter = 0
enum Char {
    // const member
    a,
    b = Char.a,
    c = 1 + 3,
    // computed member
    d = Math.random(),
    e = '123'.length,
    f = 4
}

// 常量枚举
const enum Month {
    Jan,
    Feb,
    Mar,
    Apr = Month.Mar + 1,
    // May = () => 5
}
let month = [Month.Jan, Month.Feb, Month.Mar]

// 枚举类型
enum E { a, b }
enum F { a = 0, b = 1 }
enum G { a = 'apple', b = 'banana' }

let e: E = 3
let f: F = 3
// console.log(e === f)

let e1: E.a = 3
let e2: E.b = 3
let e3: E.a = 3
// console.log(e1 === e2)
// console.log(e1 === e3)

let g1: G = G.a
let g2: G.a = G.a

Nginx 系列之 Gzip配置

Gzip配置参数

gzip配置的常用参数
gzip on|off;  #是否开启gzip
gzip_buffers 32 4K| 16 8K #缓冲(压缩在内存中缓冲几块? 每块多大?)
gzip_comp_level [1-9] #推荐6 压缩级别(级别越高,压的越小,越浪费CPU计算资源)
gzip_disable #正则匹配UA 什么样的Uri不进行gzip
gzip_min_length 200 # 开始压缩的最小长度(再小就不要压缩了,意义不在)
gzip_http_version 1.0|1.1 # 开始压缩的http协议版本(可以不设置,目前几乎全是1.1协议)
gzip_proxied          # 设置请求者代理服务器,该如何缓存内容
gzip_types text/plain  application/xml # 对哪些类型的文件用压缩 如txt,xml,html ,css
gzip_vary on|off  # 是否传输gzip压缩标志 Vary是用来标志缓存的依据.

#注意:
#图片/mp3这样的二进制文件,不必压缩
#因为压缩率比较小, 比如100->80字节,而且压缩也是耗费CPU资源的.
#比较小的文件不必压缩,

思考:

  • 如果2个人,一个浏览器支持gzip,一个浏览器不支持gzip2个同时请求同个页面, chinaCache缓存压缩后,还是未压缩的?
  • 如果1人,再次请求页面,chinaCache返回压缩后的缓存内容,还是压缩前的缓存内容?

这个时候 Vary的作用体现出来.即------缓存的内容受 Accept-Encoding头信息的影响.

所以如果
请求时,不支持gzip, 缓存服务器将会生成一份未gzip的内容.
请求时,支持gzip, 缓存服务器将会生成一份gzip的内容.

下次再请求时, 缓存服务器会考虑客户端的Accept-Encoding因素,并合理的返回信息

Nginx对于图片,js等静态文件的缓存设置
注:这个缓存是指针对浏览器所做的缓存,不是指服务器端的数据缓存.

主要知识点: location expires指令

location ~ \.(jpg|jpeg|png|gif)$ {
    expires 1d;
}

location ~ \.js$ {
    expires 1h;
}

设置并载入新配置文件,用firebug观察,
会发现 图片内容,没有再次产生新的请求,原因--利用了本地缓存的效果.

注: 在大型的新闻站,或文章站中,图片变动的可能性很小,建议做1周左右的缓存Js,css等小时级的缓存.

如果信息流动比较快,也可以不用expires指令,
用last_modified, etag功能(主流的web服务器都支持这2个头信息)

原理是:
响应: 计算响应内容的签名, etag 和 上次修改时间
请求: 发送 etatg, If-Modified-Since 头信息.
服务器收到后,判断etag是否一致, 最后修改时间是否大于if-Modifiled-Since
如果监测到服务器的内容有变化,则返回304,
浏览器就知道,内容没变,直接用缓存.

304 比起上面的expires 指令多了1次请求,但是比200状态,少了传输内容.

参考链接

JS 系列之 JavaScript中的垃圾回收机制

垃圾回收:垃圾数据是如何自动回收的?

在上一节文章中,我们提到了JavaScript中的数据是如何存储的,并通过例子分析了原始数据类型是存储在栈空间中的,引用类型的数据是存储在堆空间中的。通过这种分配方式,我们解决了数据的内存分配的问题。

不过有些数据被使用之后,可能就不再需要了,我们把这种数据称为垃圾数据。如果这些垃圾数据一直保存在内存中,那么内存会越用越多,所以我们需要对这些垃圾数据进行回收,以释放有限的内存空间。

不同语言的垃圾回收策略

通常情况下,垃圾数据回收分为手动回收和自动回收两种策略。

如C/C++就是使用手动回收策略,何时分配内存、何时销毁内存都是由代码控制的,你可以参考下面这段C代码:

//在堆中分配内存
char* p =  (char*)malloc(2048);  //在堆空间中分配2048字节的空间,并将分配后的引用地址保存到p中
 
 //使用p指向的内存
 {
   //....
 }
 
//使用结束后,销毁这段内存
free(p)
p = NULL;

从上面这段C代码可以看出来,要使用堆中的一块空间,我们需要先调用mallco函数分配内存,然后再使用;当不再需要这块数据的时候,就要手动调用free函数来释放内存。如果这段数据已经不再需要了,但是又没有主动调用free函数来销毁,那么这种情况就被称为内存泄漏。

另外一种使用的是自动垃圾回收的策略,如JavaScript、Java、Python等语言,产生的垃圾数据是由垃圾回收器来释放的,并不需要手动通过代码来释放。

对于JavaScript而言,也正是这个“自动”释放资源的特性带来了很多困惑,也让一些JavaScript开发者误以为可以不关心内存管理,这是一个很大的误解。

那么在本文,我们将围绕“JavaScript的数据是如何回收的”这个话题来展开探讨。因为数据是存储在栈和堆两种内存空间中的,所以接下来我们就来分别介绍“栈中的垃圾数据”和“堆中的垃圾数据”是如何回收的。

调用栈中的数据是如何回收的

首先是调用栈中的数据,我们还是通过一段示例代码的执行流程来分析其回收机制,具体如下:

function foo(){
    var a = 1
    var b = {name:"极客邦"}
    function showName(){
      var c = "极客时间"
      var d = {name:"极客时间"}
    }
    showName()
}
foo()

当执行到第6行代码时,其调用栈和堆空间状态图如下所示:

image

从图中可以看出,原始类型的数据被分配到栈中,引用类型的数据会被分配到堆中。当foo函数执行结束之后,foo函数的执行上下文会从堆中被销毁掉,那么它是怎么被销毁的呢?下面我们就来分析一下。

在上节文章中,我们简单介绍过了,如果执行到showName函数时,那么JavaScript引擎会创建showName函数的执行上下文,并将showName函数的执行上下文压入到调用栈中,最终执行到showName函数时,其调用栈就如上图所示。与此同时,还有一个记录当前执行状态的指针(称为ESP),指向调用栈中showName函数的执行上下文,表示当前正在执行showName函数。

接着,当showName函数执行完成之后,函数执行流程就进入了foo函数,那这时就需要销毁showName函数的执行上下文了。ESP这时候就帮上忙了,JavaScript会将ESP下移到foo函数的执行上下文,这个下移操作就是销毁showName函数执行上下文的过程。

你可能会有点懵,ESP指针向下移动怎么就能把showName的执行上下文销毁了呢?具体你可以看下面这张移动ESP前后的对比图:

image

从图中可以看出,当showName函数执行结束之后,ESP向下移动到foo函数的执行上下文中,上面showName的执行上下文虽然保存在栈内存中,但是已经是无效内存了。比如当foo函数再次调用另外一个函数时,这块内容会被直接覆盖掉,用来存放另外一个函数的执行上下文。

所以说,当一个函数执行结束之后,JavaScript引擎会通过向下移动ESP来销毁该函数保存在栈中的执行上下文。

堆中的数据是如何回收的

通过上面的讲解,我想现在你应该已经知道,当上面那段代码的foo函数执行结束之后,ESP应该是指向全局执行上下文的,那这样的话,showName函数和foo函数的执行上下文就处于无效状态了,不过保存在堆中的两个对象依然占用着空间,如下图所示:

image

从图中可以看出,1003和1050这两块内存依然被占用。要回收堆中的垃圾数据,就需要用到JavaScript中的垃圾回收器了。

所以,接下来我们就来通过Chrome的JavaScript引擎V8来分析下堆中的垃圾数据是如何回收的。

代际假说和分代收集

不过在正式介绍V8是如何实现回收之前,你需要先学习下代际假说(The Generational Hypothesis)的内容,这是垃圾回收领域中一个重要的术语,后续垃圾回收的策略都是建立在该假说的基础之上的,所以很是重要。

代际假说有以下两个特点:

  • 第一个是大部分对象在内存中存在的时间很短,简单来说,就是很多对象一经分配内存,很快就变得不可访问;
  • 第二个是不死的对象,会活得更久。

其实这两个特点不仅仅适用于JavaScript,同样适用于大多数的动态语言,如Java、Python等。

有了代际假说的基础,我们就可以来探讨V8是如何实现垃圾回收的了。

通常,垃圾回收算法有很多种,但是并没有哪一种能胜任所有的场景,你需要权衡各种场景,根据对象的生存周期的不同而使用不同的算法,以便达到最好的效果。

所以,在V8中会把堆分为新生代和老生代两个区域,新生代中存放的是生存时间短的对象,老生代中存放的生存时间久的对象。

新生区通常只支持1~8M的容量,而老生区支持的容量就大很多了。对于这两块区域,V8分别使用两个不同的垃圾回收器,以便更高效地实施垃圾回收。

  • 副垃圾回收器,主要负责新生代的垃圾回收。
  • 主垃圾回收器,主要负责老生代的垃圾回收。

垃圾回收器的工作流程

现在你知道了V8把堆分成两个区域——新生代和老生代,并分别使用两个不同的垃圾回收器。其实不论什么类型的垃圾回收器,它们都有一套共同的执行流程。

第一步是标记空间中活动对象和非活动对象。所谓活动对象就是还在使用的对象,非活动对象就是可以进行垃圾回收的对象。

第二步是回收非活动对象所占据的内存。其实就是在所有的标记完成之后,统一清理内存中所有被标记为可回收的对象。

第三步是做内存整理。一般来说,频繁回收对象后,内存中就会存在大量不连续空间,我们把这些不连续的内存空间称为内存碎片。当内存中出现了大量的内存碎片之后,如果需要分配较大连续内存的时候,就有可能出现内存不足的情况。所以最后一步需要整理这些内存碎片,但这步其实是可选的,因为有的垃圾回收器不会产生内存碎片,比如接下来我们要介绍的副垃圾回收器。

那么接下来,我们就按照这个流程来分析新生代垃圾回收器(副垃圾回收器)和老生代垃圾回收器(主垃圾回收器)是如何处理垃圾回收的。

副垃圾回收器

副垃圾回收器主要负责新生区的垃圾回收。而通常情况下,大多数小的对象都会被分配到新生区,所以说这个区域虽然不大,但是垃圾回收还是比较频繁的。

新生代中用Scavenge算法来处理。所谓Scavenge算法,是把新生代空间对半划分为两个区域,一半是对象区域,一半是空闲区域,如下图所示:

image

新加入的对象都会存放到对象区域,当对象区域快被写满时,就需要执行一次垃圾清理操作。

在垃圾回收过程中,首先要对对象区域中的垃圾做标记;标记完成之后,就进入垃圾清理阶段,副垃圾回收器会把这些存活的对象复制到空闲区域中,同时它还会把这些对象有序地排列起来,所以这个复制过程,也就相当于完成了内存整理操作,复制后空闲区域就没有内存碎片了。

完成复制后,对象区域与空闲区域进行角色翻转,也就是原来的对象区域变成空闲区域,原来的空闲区域变成了对象区域。这样就完成了垃圾对象的回收操作,同时这种角色翻转的操作还能让新生代中的这两块区域无限重复使用下去。

由于新生代中采用的Scavenge算法,所以每次执行清理操作时,都需要将存活的对象从对象区域复制到空闲区域。但复制操作需要时间成本,如果新生区空间设置得太大了,那么每次清理的时间就会过久,所以为了执行效率,一般新生区的空间会被设置得比较小。

也正是因为新生区的空间不大,所以很容易被存活的对象装满整个区域。为了解决这个问题,JavaScript引擎采用了对象晋升策略,也就是经过两次垃圾回收依然还存活的对象,会被移动到老生区中。

主垃圾回收器

主垃圾回收器主要负责老生区中的垃圾回收。除了新生区中晋升的对象,一些大的对象会直接被分配到老生区。因此老生区中的对象有两个特点,一个是对象占用空间大,另一个是对象存活时间长。

由于老生区的对象比较大,若要在老生区中使用Scavenge算法进行垃圾回收,复制这些大的对象将会花费比较多的时间,从而导致回收执行效率不高,同时还会浪费一半的空间。因而,主垃圾回收器是采用标记-清除(Mark-Sweep)的算法进行垃圾回收的。下面我们来看看该算法是如何工作的。

首先是标记过程阶段。标记阶段就是从一组根元素开始,递归遍历这组根元素,在这个遍历过程中,能到达的元素称为活动对象,没有到达的元素就可以判断为垃圾数据。

比如最开始的那段代码,当showName函数执行退出之后,这段代码的调用栈和堆空间如下图所示:

image

从上图你可以大致看到垃圾数据的标记过程,当showName函数执行结束之后,ESP向下移动,指向了foo函数的执行上下文,这时候如果遍历调用栈,是不会找到引用1003地址的变量,也就意味着1003这块数据为垃圾数据,被标记为红色。由于1050这块数据被变量b引用了,所以这块数据会被标记为活动对象。这就是大致的标记过程。

接下来就是垃圾的清除过程。它和副垃圾回收器的垃圾清除过程完全不同,你可以理解这个过程是清除掉红色标记数据的过程,可参考下图大致理解下其清除过程:

image

上面的标记过程和清除过程就是标记-清除算法,不过对一块内存多次执行标记-清除算法后,会产生大量不连续的内存碎片。而碎片过多会导致大对象无法分配到足够的连续内存,于是又产生了另外一种算法——标记-整理(Mark-Compact),这个标记过程仍然与标记-清除算法里的是一样的,但后续步骤不是直接对可回收对象进行清理,而是让所有存活的对象都向一端移动,然后直接清理掉端边界以外的内存。你可以参考下图:

image

全停顿

现在你知道了V8是使用副垃圾回收器和主垃圾回收器处理垃圾回收的,不过由于JavaScript是运行在主线程之上的,一旦执行垃圾回收算法,都需要将正在执行的JavaScript脚本暂停下来,待垃圾回收完毕后再恢复脚本执行。我们把这种行为叫做全停顿(Stop-The-World)。

比如堆中的数据有1.5GB,V8实现一次完整的垃圾回收需要1秒以上的时间,这也是由于垃圾回收而引起JavaScript线程暂停执行的时间,若是这样的时间花销,那么应用的性能和响应能力都会直线下降。主垃圾回收器执行一次完整的垃圾回收流程如下图所示:

image

在V8新生代的垃圾回收中,因其空间较小,且存活对象较少,所以全停顿的影响不大,但老生代就不一样了。如果在执行垃圾回收的过程中,占用主线程时间过久,就像上面图片展示的那样,花费了200毫秒,在这200毫秒内,主线程是不能做其他事情的。比如页面正在执行一个JavaScript动画,因为垃圾回收器在工作,就会导致这个动画在这200毫秒内无法执行的,这将会造成页面的卡顿现象。

为了降低老生代的垃圾回收而造成的卡顿,V8将标记过程分为一个个的子标记过程,同时让垃圾回收标记和JavaScript应用逻辑交替进行,直到标记阶段完成,我们把这个算法称为增量标记(Incremental Marking)算法。如下图所示:

image

使用增量标记算法,可以把一个完整的垃圾回收任务拆分为很多小的任务,这些小的任务执行时间比较短,可以穿插在其他的JavaScript任务中间执行,这样当执行上述动画效果时,就不会让用户因为垃圾回收任务而感受到页面的卡顿了。

总结

好了,今天就讲到这里,下面我们就来总结下今天的主要内容。

首先我们介绍了不同语言的垃圾回收策略,然后又说明了栈中的数据是如何回收的,最后重点讲解了JavaScript中的垃圾回收器是如何工作的。

从上面的分析你也能看出来,无论是垃圾回收的策略,还是处理全停顿的策略,往往都没有一个完美的解决方案,你需要花一些时间来做权衡,而这需要牺牲当前某几方面的指标来换取其他几个指标的提升。

其实站在工程师的视角,我们经常需要在满足需求的前提下,权衡各个指标的得失,把系统设计得尽可能适应最核心的需求。

生活中处理事情的原则也与之类似,古人很早就说过“两害相权取其轻,两利相权取其重”,所以与其患得患失,不如冷静地分析哪些才是核心诉求,然后果断决策牺牲哪些以使得利益最大化。

  • 调用栈数据回收
  • 堆数据回收
    • 主垃圾回收器,主要负责老生代的垃圾回收。
      • 标记清除
      • 标记-整理
      • 增量标记
    • 副垃圾回收器,主要负责新生代的垃圾回收。
      • Scavenge算法

Nginx 系列之 Location正则

Location正则

location / {
    root   /usr/local/nginx/html;
    index  index.html index.htm;
}
 
location ~ image {
    root /var/www/image;
    index index.html;
}

如果我们访问 http://xx.com/image/logo.png
此时, “/” 与”/image/logo.png” 匹配
同时,”image”正则 与”image/logo.png”也能匹配,谁发挥作用?
正则表达式的成果将会使用.

图片真正会访问 /var/www/image/logo.png

location / {
    root	/usr/local/nginx/html;
    index	index.html index.htm;
}
 
location /foo {
    root	/var/www/html;
    index	index.html;
}

我们访问http://xxx.com/foo
对于uri “/foo”, 两个location的patt,都能匹配他们
即 ‘/’能从左前缀匹配 ‘/foo’, ‘/foo’也能左前缀匹配’/foo’,
此时, 真正访问 /var/www/html/index.html
原因:’/foo’匹配的更长,因此使用之.;

Location的正则写法

location  = / {
    # 精确匹配 / ,主机名后面不能带任何字符串
    [ configuration A ] 
}

location  / {
    # 因为所有的地址都以 / 开头,所以这条规则将匹配到所有请求
    # 但是正则和最长字符串会优先匹配
    [ configuration B ] 
}

location /documents/ {
    # 匹配任何以 /documents/ 开头的地址,匹配符合以后,还要继续往下搜索
    # 只有后面的正则表达式没有匹配到时,这一条才会采用这一条
    [ configuration C ] 
}

location ~ /documents/Abc {
    # 匹配任何以 /documents/ 开头的地址,匹配符合以后,还要继续往下搜索
    # 只有后面的正则表达式没有匹配到时,这一条才会采用这一条
    [ configuration CC ] 
}

location ^~ /images/ {
    # 匹配任何以 /images/ 开头的地址,匹配符合以后,停止往下搜索正则,采用这一条。
    [ configuration D ] 
}

location ~* \.(gif|jpg|jpeg)$ {
    # 匹配所有以 gif,jpg或jpeg 结尾的请求
    # 然而,所有请求 /images/ 下的图片会被 config D 处理,因为 ^~ 到达不了这一条正则
    [ configuration E ] 
}

location /images/ {
    # 字符匹配到 /images/,继续往下,会发现 ^~ 存在
    [ configuration F ] 
}

location /images/abc {
    # 最长字符匹配到 /images/abc,继续往下,会发现 ^~ 存在
    # F与G的放置顺序是没有关系的
    [ configuration G ] 
}

location ~ /images/abc/ {
    # 只有去掉 config D 才有效:先最长匹配 config G 开头的地址,继续往下搜索,匹配到这一条正则,采用
    [ configuration H ] 
}

location ~* /js/.*/\.js
  • 以=开头表示精确匹配如 A 中只匹配根目录结尾的请求,后面不能带任何字符串。
  • ^~ 开头表示uri以某个常规字符串开头,不是正则匹配
  • ~ 开头表示区分大小写的正则匹配;
  • ~* 开头表示不区分大小写的正则匹配
  • / 通用匹配, 如果没有其它匹配,任何请求都会匹配到

顺序or优先级

(location =) > (location 完整路径) > (location ^~ 路径) > (location ~,~* 正则顺序) > (location 部分起始路径) > (/)

location解析过程如下图

location解析过程

参考链接

Nginx 系列之 日志管理

Nginx 允许针对不同的server做不同的log

我们观察nginx的server段,可以看到如下类似信息

access_log  logs/host.access.log  main;

这说明 该server, 它的访问日志的文件是 logs/host.access.log ,
使用的格式”main”格式.
除了main格式,你可以自定义其他格式.

main格式是什么?

log_format  main  '$remote_addr - $remote_user [$time_local] "$request" '
    '$status $body_bytes_sent "$http_referer" '
    '"$http_user_agent" "$http_x_forwarded_for"';

main格式是我们定义好一种日志的格式,并起个名字,便于引用.
以上面的例子, main类型的日志,记录的 remote_addr.... http_x_forwarded_for等选项.

日志格式 是指记录哪些选项

默认的日志格式: main

log_format  main  '$remote_addr - $remote_user [$time_local] "$request" '
    '$status $body_bytes_sent "$http_referer" '
	'"$http_user_agent" "$http_x_forwarded_for"';

如默认的main日志格式,记录这么几项
远程IP- 远程用户/用户时间 请求方法(如GET/POST) 请求体body长度 referer来源信息
http-user-agent用户代理/蜘蛛 ,被转发的请求的原始IP

http_x_forwarded_for:在经过代理时,代理把你的本来IP加在此头信息中,传输你的原始IP

例如:

58.132.169.60 - - [07/Oct/2015:11:40:19 +0800] "GET / HTTP/1.1" 200 30 "-" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.10; rv:41.0) Gecko/20100101 Firefox/41.0" "-"

声明一个独特的log_format并命名

log_format  mylog '$remote_addr- "$request" '
 	'$status $body_bytes_sent "$http_referer" '
    '"$http_user_agent" "$http_x_forwarded_for"';

在下面的server/location,我们就可以引用 mylog

在server段中,这样来声明
Nginx允许针对不同的server做不同的Log ,(有的web服务器不支持,如lighttp)

access_log logs/access_8080.log mylog;  
#声明log   log位置          log格式;

参考链接

HTTP 系列之 与HTTP相关的各种协议

与HTTP相关的各种协议

这一节要讲的则是比较偏向于理论的各种HTTP相关协议,重点是TCP/IP、DNS、URI、HTTPS等,希望能够帮你理清楚它们与HTTP的关系。

image

TCP/IP

TCP/IP协议是目前网络世界“事实上”的标准通信协议,即使你没有用过也一定听说过,因为它太著名了。

TCP/IP协议实际上是一系列网络通信协议的统称,其中最核心的两个协议是TCP和IP,其他的还有UDP、ICMP、ARP等等,共同构成了一个复杂但有层次的协议栈。

这个协议栈有四层,最上层是“应用层”,最下层是“链接层”,TCP和IP则在中间:TCP属于“传输层”,IP属于“网际层”。协议的层级关系模型非常重要,我会在下一讲中再专门讲解,这里先暂时放一放。

IP协议是“Internet Protocol”的缩写,主要目的是解决寻址和路由问题,以及如何在两点间传送数据包。IP协议使用“IP地址”的概念来定位互联网上的每一台计算机。可以对比一下现实中的电话系统,你拿着的手机相当于互联网上的计算机,而要打电话就必须接入电话网,由通信公司给你分配一个号码,这个号码就相当于IP地址。

现在我们使用的IP协议大多数是v4版,地址是四个用“.”分隔的数字,例如“192.168.0.1”,总共有2^32,大约42亿个可以分配的地址。看上去好像很多,但互联网的快速发展让地址的分配管理很快就“捉襟见肘”。所以,就又出现了v6版,使用8组“:”分隔的数字作为地址,容量扩大了很多,有2^128个,在未来的几十年里应该是足够用了。

TCP协议是“Transmission Control Protocol”的缩写,意思是“传输控制协议”,它位于IP协议之上,基于IP协议提供可靠的、字节流形式的通信,是HTTP协议得以实现的基础。

“可靠”是指保证数据不丢失,“字节流”是指保证数据完整,所以在TCP协议的两端可以如同操作文件一样访问传输的数据,就像是读写在一个密闭的管道里“流动”的字节。

在第2讲时我曾经说过,HTTP是一个"传输协议",但它不关心寻址、路由、数据完整性等传输细节,而要求这些工作都由下层来处理。因为互联网上最流行的是TCP/IP协议,而它刚好满足HTTP的要求,所以互联网上的HTTP协议就运行在了TCP/IP上,HTTP也就可以更准确地称为“HTTP over TCP/IP”。

DNS

在TCP/IP协议中使用IP地址来标识计算机,数字形式的地址对于计算机来说是方便了,但对于人类来说却既难以记忆又难以输入。

于是“域名系统”(Domain Name System)出现了,用有意义的名字来作为IP地址的等价替代。设想一下,你是愿意记“95.211.80.227”这样枯燥的数字,还是“nginx.org”这样的词组呢?

在DNS中,“域名”(Domain Name)又称为“主机名”(Host),为了更好地标记不同国家或组织的主机,让名字更好记,所以被设计成了一个有层次的结构。

域名用“.”分隔成多个单词,级别从左到右逐级升高,最右边的被称为“顶级域名”。对于顶级域名,可能你随口就能说出几个,例如表示商业公司的“com”、表示教育机构的“edu”,表示国家的“cn”“uk”等,买火车票时的域名还记得吗?是“www.12306.cn”。

image

但想要使用TCP/IP协议来通信仍然要使用IP地址,所以需要把域名做一个转换,“映射”到它的真实IP,这就是所谓的“域名解析”。

继续用刚才的打电话做个比喻,你想要打电话给小明,但不知道电话号码,就得在手机里的号码簿里一项一项地找,直到找到小明那一条记录,然后才能查到号码。这里的“小明”就相当于域名,而“电话号码”就相当于IP地址,这个查找的过程就是域名解析。

域名解析的实际操作要比刚才的例子复杂很多,因为互联网上的电脑实在是太多了。目前全世界有13组根DNS服务器,下面再有许多的顶级DNS、权威DNS和更小的本地DNS,逐层递归地实现域名查询。

HTTP协议中并没有明确要求必须使用DNS,但实际上为了方便访问互联网上的Web服务器,通常都会使用DNS来定位或标记主机名,间接地把DNS与HTTP绑在了一起。

URI/URL

有了TCP/IP和DNS,是不是我们就可以任意访问网络上的资源了呢?

还不行,DNS和IP地址只是标记了互联网上的主机,但主机上有那么多文本、图片、页面,到底要找哪一个呢?就像小明管理了一大堆文档,你怎么告诉他是哪个呢?

所以就出现了URI(Uniform Resource Identifier),中文名称是 统一资源标识符,使用它就能够唯一地标记互联网上资源。

URI另一个更常用的表现形式是URL(Uniform Resource Locator), 统一资源定位符,也就是我们俗称的“网址”,它实际上是URI的一个子集,不过因为这两者几乎是相同的,差异不大,所以通常不会做严格的区分。

我就拿Nginx网站来举例,看一下URI是什么样子的。

http://nginx.org/en/download.html

你可以看到,URI主要有三个基本的部分构成:

  1. 协议名:即访问该资源应当使用的协议,在这里是“http”;
  2. 主机名:即互联网上主机的标记,可以是域名或IP地址,在这里是“nginx.org”;
  3. 路径:即资源在主机上的位置,使用“/”分隔多级目录,在这里是“/en/download.html”。

还是用打电话来做比喻,你通过电话簿找到了小明,让他把昨天做好的宣传文案快递过来。那么这个过程中你就完成了一次URI资源访问,“小明”就是“主机名”,“昨天做好的宣传文案”就是“路径”,而“快递”,就是你要访问这个资源的“协议名”。

HTTPS

在TCP/IP、DNS和URI的“加持”之下,HTTP协议终于可以自由地穿梭在互联网世界里,顺利地访问任意的网页了,真的是“好生快活”。

但且慢,互联网上不仅有“美女”,还有很多的“野兽”。

假设你打电话找小明要一份广告创意,很不幸,电话被商业间谍给窃听了,他立刻动用种种手段偷窃了你的快递,就在你还在等包裹的时候,他抢先发布了这份广告,给你的公司造成了无形或有形的损失。

有没有什么办法能够防止这种情况的发生呢?确实有。你可以使用“加密”的方法,比如这样打电话:

你:“喂,小明啊,接下来我们改用火星文通话吧。”
小明:“好啊好啊,就用火星文吧。”
你:“巴拉巴拉巴拉巴拉……”
小明:“巴拉巴拉巴拉巴拉……”

如果你和小明说的火星文只有你们两个才懂,那么即使窃听到了这段谈话,他也不会知道你们到底在说什么,也就无从破坏你们的通话过程。

HTTPS就相当于这个比喻中的“火星文”,它的全称是“HTTP over SSL/TLS”,也就是运行在SSL/TLS协议上的HTTP。

注意它的名字,这里是SSL/TLS,而不是TCP/IP,它是一个负责加密通信的安全协议,建立在TCP/IP之上,所以也是个可靠的传输协议,可以被用作HTTP的下层。

因为HTTPS相当于“HTTP+SSL/TLS+TCP/IP”,其中的“HTTP”和“TCP/IP”我们都已经明白了,只要再了解一下SSL/TLS,HTTPS也就能够轻松掌握。

SSL的全称是“Secure Socket Layer”,由网景公司发明,当发展到3.0时被标准化,改名为TLS,即“Transport Layer Security”,但由于历史的原因还是有很多人称之为SSL/TLS,或者直接简称为SSL。

SSL使用了许多密码学最先进的研究成果,综合了对称加密、非对称加密、摘要算法、数字签名、数字证书等技术,能够在不安全的环境中为通信的双方创建出一个秘密的、安全的传输通道,为HTTP套上一副坚固的盔甲。

你可以在今后上网时留心看一下浏览器地址栏,如果有一个小锁头标志,那就表明网站启用了安全的HTTPS协议,而URI里的协议名,也从“http”变成了“https”。

代理

代理(Proxy)是HTTP协议中请求方和应答方中间的一个环节,作为“中转站”,既可以转发客户端的请求,也可以转发服务器的应答。

代理有很多的种类,常见的有:

  1. 匿名代理:完全“隐匿”了被代理的机器,外界看到的只是代理服务器;
  2. 透明代理:顾名思义,它在传输过程中是“透明开放”的,外界既知道代理,也知道客户端;
  3. 正向代理:靠近客户端,代表客户端向服务器发送请求;
  4. 反向代理:靠近服务器端,代表服务器响应客户端的请求;

上一讲提到的CDN,实际上就是一种代理,它代替源站服务器响应客户端的请求,通常扮演着透明代理和反向代理的角色。

由于代理在传输过程中插入了一个“中间层”,所以可以在这个环节做很多有意思的事情,比如:

  1. 负载均衡:把访问请求均匀分散到多台机器,实现访问集群化;
  2. 内容缓存:暂存上下行的数据,减轻后端的压力;
  3. 安全防护:隐匿IP,使用WAF等工具抵御网络攻击,保护被代理的机器;
  4. 数据处理:提供压缩、加密等额外的功能。

关于HTTP的代理还有一个特殊的“代理协议”(proxy protocol),它由知名的代理软件HAProxy制订,但并不是RFC标准,我也会在之后的课程里专门讲解。

小结

这次我介绍了与HTTP相关的各种协议,在这里简单小结一下今天的内容。

  1. TCP/IP是网络世界最常用的协议,HTTP通常运行在TCP/IP提供的可靠传输基础上;
  2. DNS域名是IP地址的等价替代,需要用域名解析实现到IP地址的映射;
  3. URI是用来标记互联网上资源的一个名字,由“协议名+主机名+路径”构成,俗称URL;
  4. HTTPS相当于“HTTP+SSL/TLS+TCP/IP”,为HTTP套了一个安全的外壳;
  5. 代理是HTTP传输过程中的“中转站”,可以实现缓存加速、负载均衡等功能。

HTTP 系列之 前世今生

01-时势与英雄:HTTP的前世今生

HTTP协议在我们的生活中随处可见,打开手机或者电脑,只要你上网,不论是用iPhone、Android、Windows还是Mac,不论是用浏览器还是App,不论是看新闻、短视频还是听音乐、玩游戏,后面总会有HTTP在默默为你服务。

据NetCraft公司统计,目前全球至少有16亿个网站、2亿多个独立域名,而这个庞大网络世界的底层运转机制就是HTTP。

那么,在享受如此便捷舒适的网络生活时,你有没有想过,HTTP协议是怎么来的?它最开始是什么样子的?又是如何一步一步发展到今天,几乎“**”了整个互联网世界的呢?

常言道:“时势造英雄,英雄亦造时势”。

今天我就和你来聊一聊HTTP的发展历程,看看它的成长轨迹,看看历史上有哪些事件推动了它的前进,它又促进了哪些技术的产生,一起来见证“英雄之旅”。

在这个过程中,你也能够顺便了解一下HTTP的“历史局限性”,明白HTTP为什么会设计成现在这个样子。

史前时期

20世纪60年代,美国国防部高等研究计划署(ARPA)建立了ARPA网,它有四个分布在各地的节点,被认为是如今互联网的“始祖”。

然后在70年代,基于对ARPA网的实践和思考,研究人员发明出了著名的TCP/IP协议。由于具有良好的分层结构和稳定的性能,TCP/IP协议迅速战胜其他竞争对手流行起来,并在80年代中期进入了UNIX系统内核,促使更多的计算机接入了互联网。

创世纪

1989年,任职于欧洲核子研究中心(CERN)的蒂姆·伯纳斯-李(Tim Berners-Lee)发表了一篇论文,提出了在互联网上构建超链接文档系统的构想。这篇论文中他确立了三项关键技术。

  1. URI:即统一资源标识符,作为互联网上资源的唯一身份;
  2. HTML:即超文本标记语言,描述超文本文档;
  3. HTTP:即超文本传输协议,用来传输超文本。

这三项技术在如今的我们看来已经是稀松平常,但在当时却是了不得的大发明。基于它们,就可以把超文本系统完美地运行在互联网上,让各地的人们能够自由地共享信息,蒂姆把这个系统称为“万维网”(World Wide Web),也就是我们现在所熟知的Web。

所以在这一年,我们的英雄“HTTP”诞生了,从此开始了它伟大的征途。

HTTP/0.9

20世纪90年代初期的互联网世界非常简陋,计算机处理能力低,存储容量小,网速很慢,还是一片“信息荒漠”。网络上绝大多数的资源都是纯文本,很多通信协议也都使用纯文本,所以HTTP的设计也不可避免地受到了时代的限制。

这一时期的HTTP被定义为0.9版,结构比较简单,为了便于服务器和客户端处理,它也采用了纯文本格式。蒂姆·伯纳斯-李最初设想的系统里的文档都是只读的,所以只允许用“GET”动作从服务器上获取HTML文档,并且在响应请求之后立即关闭连接,功能非常有限。

HTTP/0.9虽然很简单,但它作为一个“原型”,充分验证了Web服务的可行性,而“简单”也正是它的优点,蕴含了进化和扩展的可能性,因为:

“把简单的系统变复杂”,要比“把复杂的系统变简单”容易得多。

HTTP/1.0

1993年,NCSA(美国国家超级计算应用中心)开发出了Mosaic,是第一个可以图文混排的浏览器,随后又在1995年开发出了服务器软件Apache,简化了HTTP服务器的搭建工作。

同一时期,计算机多媒体技术也有了新的发展:1992年发明了JPEG图像格式,1995年发明了MP3音乐格式。

这些新软件新技术一经推出立刻就吸引了广大网民的热情,更的多的人开始使用互联网,研究HTTP并提出改进意见,甚至实验性地往协议里添加各种特性,从用户需求的角度促进了HTTP的发展。

于是在这些已有实践的基础上,经过一系列的草案,HTTP/1.0版本在1996年正式发布。它在多方面增强了0.9版,形式上已经和我们现在的HTTP差别不大了,例如:

  1. 增加了HEAD、POST等新方法;
  2. 增加了响应状态码,标记可能的错误原因;
  3. 引入了协议版本号概念;
  4. 引入了HTTP Header(头部)的概念,让HTTP处理请求和响应更加灵活;
  5. 传输的数据不再仅限于文本。

但HTTP/1.0并不是一个“标准”,只是记录已有实践和模式的一份参考文档,不具有实际的约束力,相当于一个“备忘录”。

所以HTTP/1.0的发布对于当时正在蓬勃发展的互联网来说并没有太大的实际意义,各方势力仍然按照自己的意图继续在市场上奋力拼杀。

HTTP/1.1

1995年,网景的Netscape Navigator和微软的Internet Explorer开始了著名的“浏览器大战”,都希望在互联网上占据主导地位。

这场战争的结果你一定早就知道了,最终微软的IE取得了决定性的胜利,而网景则“败走麦城”(但后来却凭借Mozilla Firefox又扳回一局)。

“浏览器大战”的是非成败我们放在一边暂且不管,不可否认的是,它再一次极大地推动了Web的发展,HTTP/1.0也在这个过程中经受了实践检验。于是在“浏览器大战”结束之后的1999年,HTTP/1.1发布了RFC文档,编号为2616,正式确立了延续十余年的传奇。

从版本号我们就可以看到,HTTP/1.1是对HTTP/1.0的小幅度修正。但一个重要的区别是:它是一个“正式的标准”,而不是一份可有可无的“参考文档”。这意味着今后互联网上所有的浏览器、服务器、网关、代理等等,只要用到HTTP协议,就必须严格遵守这个标准,相当于是互联网世界的一个“立法”。

不过,说HTTP/1.1是“小幅度修正”也不太确切,它还是有很多实质性进步的。毕竟经过了多年的实战检验,比起0.9/1.0少了“学术气”,更加“接地气”,同时表述也更加严谨。HTTP/1.1主要的变更点有:

  1. 增加了PUT、DELETE等新的方法;
  2. 增加了缓存管理和控制;
  3. 明确了连接管理,允许持久连接;
  4. 允许响应数据分块(chunked),利于传输大文件;
  5. 强制要求Host头,让互联网主机托管成为可能。

HTTP/1.1的推出可谓是“众望所归”,互联网在它的“保驾护航”下迈开了大步,由此走上了“康庄大道”,开启了后续的“Web 1.0”“Web 2.0”时代。现在许多的知名网站都是在这个时间点左右创立的,例如Google、新浪、搜狐、网易、腾讯等。

不过由于HTTP/1.1太过庞大和复杂,所以在2014年又做了一次修订,原来的一个大文档被拆分成了六份较小的文档,编号为7230-7235,优化了一些细节,但此外没有任何实质性的改动。

HTTP/2

HTTP/1.1发布之后,整个互联网世界呈现出了爆发式的增长,度过了十多年的“快乐时光”,更涌现出了Facebook、Twitter、淘宝、京东等互联网新贵。

这期间也出现了一些对HTTP不满的意见,主要就是连接慢,无法跟上迅猛发展的互联网,但HTTP/1.1标准一直“岿然不动”,无奈之下人们只好发明各式各样的“小花招”来缓解这些问题,比如以前常见的切图、JS合并等网页优化手段。

终于有一天,搜索巨头Google忍不住了,决定“揭竿而起”,就像马云说的“如果银行不改变,我们就改变银行”。那么,它是怎么“造反”的呢?

Google首先开发了自己的浏览器Chrome,然后推出了新的SPDY协议,并在Chrome里应用于自家的服务器,如同十多年前的网景与微软一样,从实际的用户方来“倒逼”HTTP协议的变革,这也开启了第二次的“浏览器大战”。

历史再次重演,不过这次的胜利者是Google,Chrome目前的全球的占有率超过了60%。“挟用户以号令天下”,Google借此顺势把SPDY推上了标准的宝座,互联网标准化组织以SPDY为基础开始制定新版本的HTTP协议,最终在2015年发布了HTTP/2,RFC编号7540。

HTTP/2的制定充分考虑了现今互联网的现状:宽带、移动、不安全,在高度兼容HTTP/1.1的同时在性能改善方面做了很大努力,主要的特点有:

  1. 二进制协议,不再是纯文本;
  2. 可发起多个请求,废弃了1.1里的管道;
  3. 使用专用算法压缩头部,减少数据传输量;
  4. 允许服务器主动向客户端推送数据;
  5. 增强了安全性,“事实上”要求加密通信。

虽然HTTP/2到今天已经四岁,也衍生出了gRPC等新协议,但由于HTTP/1.1实在是太过经典和强势,目前它的普及率还比较低,大多数网站使用的仍然还是20年前的HTTP/1.1。

HTTP/3

看到这里,你可能会问了:“HTTP/2这么好,是不是就已经完美了呢?”

答案是否定的,这一次还是Google,而且它要“革自己的命”。

在HTTP/2还处于草案之时,Google又发明了一个新的协议,叫做QUIC,而且还是相同的“套路”,继续在Chrome和自家服务器里试验着“玩”,依托它的庞大用户量和数据量,持续地推动QUIC协议成为互联网上的“既成事实”。

“功夫不负有心人”,当然也是因为QUIC确实自身素质过硬。

在去年,也就是2018年,互联网标准化组织IETF提议将“HTTP over QUIC”更名为“HTTP/3”并获得批准,HTTP/3正式进入了标准化制订阶段,也许两三年后就会正式发布,到时候我们很可能会跳过HTTP/2直接进入HTTP/3。

算法系列之数据结构思维导图

思维导图

这里面有10个数据结构:

  1. 数组
  2. 链表
  3. 队列
  4. 散列表
  5. 二叉树
  6. 跳表
  7. Trie树

10个算法:

  1. 递归
  2. 排序
  3. 二分查找
  4. 搜索
  5. 哈希算法
  6. 贪心算法
  7. 分治算法
  8. 回溯算法
  9. 动态规划
  10. 字符串匹配算法

TS 系列之 类型检查机制:类型兼容性

类型兼容

image

demos

/*
 * X(目标类型) = Y(源类型),X 兼容 Y
 */

let s: string = 'a'
// str = null

// 接口兼容性
interface X {
    a: any;
    b: any;
}
interface Y {
    a: any;
    b: any;
    c: any;
}
let x: X = {a: 1, b: 2}
let y: Y = {a: 1, b: 2, c: 3}
x = y
// y = x

// 函数兼容性
type Handler = (a: number, b: number) => void
function hof(handler: Handler) {
    return handler
}

// 1)参数个数
let handler1 = (a: number) => {}
hof(handler1)
let handler2 = (a: number, b: number, c: number) => {}
// hof(handler2)

// 可选参数和剩余参数
let a = (p1: number, p2: number) => {}
let b = (p1?: number, p2?: number) => {}
let c = (...args: number[]) => {}
a = b
a = c
// b = a
// b = c
c = a
c = b

// 2)参数类型
let handler3 = (a: string) => {}
// hof(handler3)

interface Point3D {
    x: number;
    y: number;
    z: number;
}
interface Point2D {
    x: number;
    y: number;
}
let p3d = (point: Point3D) => {}
let p2d = (point: Point2D) => {}
p3d = p2d
// p2d = p23

// 3) 返回值类型
let f = () => ({name: 'Alice'})
let g = () => ({name: 'Alice', location: 'Beijing'})
f = g
// g = f

// 函数重载
function overload(a: number, b: number): number
function overload(a: string, b: string): string
function overload(a: any, b: any): any {}
// function overload(a: any): any {}
// function overload(a: any, b: any, c: any): any {}
// function overload(a: any, b: any) {}

// 枚举兼容性
enum Fruit { Apple, Banana }
enum Color { Red, Yellow }
let fruit: Fruit.Apple = 1
let no: number = Fruit.Apple
// let color: Color.Red = Fruit.Apple

// 类兼容性
class A {
    constructor(p: number, q: number) {}
    id: number = 1
    private name: string = ''
}
class B {
    static s = 1
    constructor(p: number) {}
    id: number = 2
    private name: string = ''
}
class C extends A {}
let aa = new A(1, 2)
let bb = new B(1)
// aa = bb
// bb = aa
let cc = new C(1, 2)
aa = cc
cc = aa

// 泛型兼容性
interface Empty<T> {
    // value: T
}
let obj1: Empty<number> = {};
let obj2: Empty<string> = {};
obj1 = obj2

let log1 = <T>(x: T): T => {
    console.log('x')
    return x
}
let log2 = <U>(y: U): U => {
    console.log('y')
    return y
}
log1 = log2

HTTP 系列之 浏览器发起HTTP请求的典型场景

浏览器发起HTTP请求的典型场景

Hypertext Transfer Protocol (HTTP) 协议

a stateless application-level request/response protocol that uses extensible semantics and self-descriptive message payloads for flexible interaction with network-based hypertext information systems (RFC7230 2014.6)

一种无状态的、应用层的、以请求/应答方式运行的协议,它使用可扩展的 语义和自描述消息格式,与基于网络的超文本信息系统灵活的互动

浏览器发起HTTP请求的典型场景

  1. 用户在用户界面(浏览器)输入URL地址,当URL只输入到一半的时候,这时候如果已经访问过这个URL地址,浏览器引擎可以联想出完整的URL地址,这还是因为之后访问的URL地址会存入浏览器存储数据库。
  2. 输入地址按回车之后,浏览器引擎会调用渲染引擎(webkit 引擎),渲染引擎首先会通过网络模块发出请求,等到响应回来之后,如果有JS脚本,会再调用网络模块加载JS脚本,加载完之后调用JS解释器解释执行JS。
  3. 当渲染引擎拿到html/css/img等文件之后,会通过UI后端把完整的界面绘制到用户界面中。

image

一个HTTP完整请求的过程

  1. 浏览器首先从URL中解析出域名,然后根据域名查询DNS,获取到域名对应的IP地址。
  2. 接着,浏览器用这个IP地址与服务器建立TCP连接,如果用的是HTTPS,还有完成TLS/SSL的握手。
  3. 在建立好连接以后呢,构造HTTP请求,在构造请求的过程中,要填充上下文至HTTP头部
  4. 然后通过这个TCP连接发起HTTP请求,当服务器收到这个HTTP请求以后,返回给浏览器以HTTP页面作为包体的HTTP响应。
  5. 浏览器渲染引擎解析响应,根据这个响应中其他的超链接(js/css/img)等资源再次发起HTTP请求。
  6. 根据包体渲染用户界面。

image

TS 系列之 类型检查机制:类型推断

类型检查机制

image

类型推断

image

demos

let a = 1;
let bbb = [1, null, 'a']
let ccc = {x: 1, y: 'a'}

let d = (x = 1) => x + 1

window.onkeydown = (event) => {
    console.log(event.button)
}

interface Foo {
    bar: number
}
// let foo = {} as Foo
// let foo = <Foo>{}
let foo: Foo = {
    bar: 1
}
// foo.bar = 1

TS 系列之 强类型和弱类型语言

强类型语言

在强类型语言中,当一个对象从调用函数传递到被调用函数时,其类型必须与被调用函数中的声明类型兼容

通俗意义是说:强类型语言不允许改变变量的数据类型,除非进行强制类型转换。

弱类型语言

在弱类型语言中,变量可以被赋予不同的数据类型

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.