计算机网络面试
基础篇
物理层
通过光缆、电缆、双绞线、无线电波等方式将电脑连接起来,将数据包转化为0和1的电信号进行传输,主要规定网络的一些电气特性。
链接层
链接层规定了0和1的分组方式,即如何解读获得的01串。
以太网协议
以太网协议规定,一组电信号构成一个数据包:“帧”,每一帧包括标头(Head)和数据(Data)。
“标头”包含数据包的一些说明项,比如发送者、接受者、数据类型等等;”数据”则是数据包的具体内容。
“标头”的长度,固定为18字节。”数据”的长度,最短为46字节,最长为1500字节。因此,整个”帧”最短为64字节,最长为1518字节。如果数据很长,就必须分割成多个帧进行发送。
MAC地址
以太网规定,连入网络的所有设备,都必须具有”网卡”接口。数据包必须是从一块网卡,传送到另一块网卡。网卡的地址,就是数据包的发送地址和接收地址,叫做MAC地址。
每块网卡出厂的时候,都有一个全世界独一无二的MAC地址,长度是48个二进制位,通常用12个十六进制数表示,如00-B0-D0-86-BB-F7
,前6个十六进制数是厂商编号,后6个是该厂商的网卡流水号。有了MAC地址,就可以定位网卡和数据包的路径了。
广播
以太网数据包必须知道接收方的MAC地址,然后才能发送,需要用到ARP协议,这个留到后面介绍。
有了MAC地址后,为了把数据包准确送到接收方,以太网采用了一种很”原始”的方式,它向本网络内所有计算机发送数据包,每台计算机自己判断是否为接收方。
网络层
网络层的由来
互联网是无数子网络共同组成的一个巨型网络,以太网采用广播方式发送数据包,所有成员人手一”包”,不仅效率低,而且局限在发送者所在的子网络。也就是说,如果两台计算机不在同一个子网络,广播是传不过去的。这种设计是合理的,否则互联网上每一台计算机都会收到所有包,那会引起灾难。
因此,必须找到一种方法,能够区分哪些MAC地址属于同一个子网络,哪些不是。如果是同一个子网络,就采用广播方式发送,否则就采用”路由”方式发送。”路由”,就是指如何向不同的子网络分发数据包。遗憾的是,MAC地址本身无法做到这一点。它只与厂商有关,与所处网络无关。
这就导致了”网络层”的诞生。它引进一套新的地址,使得我们能够区分不同的计算机是否属于同一个子网络。这套地址就叫做”网络地址”,简称”网址”。
“网络层”出现以后,每台计算机有了两种地址,一种是MAC地址,另一种是网络地址。两种地址之间没有任何联系,MAC地址是绑定在网卡上的,网络地址则是管理员分配的,它们只是随机组合在一起。
网络地址帮助我们确定计算机所在的子网络,MAC地址则将数据包送到该子网络中的目标网卡。从逻辑上可以推断,必定是先处理网络地址,然后再处理MAC地址。
IP协议
规定网络地址的协议,叫做IP协议。它所定义的地址,就被称为IP地址。
目前,广泛采用的是IP协议第四版,简称IPv4。这个版本规定,网络地址由32个二进制位组成,习惯上,我们用分成四段的十进制数表示IP地址,从0.0.0.0
一直到255.255.255.255
。
互联网上的每一台计算机,都会分配到一个IP地址。这个地址分成两个部分,前一部分代表网络,后一部分代表主机。
那么,怎样才能从IP地址,判断两台计算机是否属于同一个子网络呢?这就要用到另一个参数”子网掩码”(subnet mask)。
所谓”子网掩码”,就是表示子网络特征的一个参数。它在形式上等同于IP地址,也是一个32位二进制数字,它的网络部分全部为1,主机部分全部为0。比如,IP地址172.16.254.1
,如果已知网络部分是前24位,主机部分是后8位,那么子网络掩码就是255.255.255.0
。
知道”子网掩码”,我们就能判断,任意两个IP地址是否处在同一个子网络。方法是将两个IP地址与子网掩码分别进行AND运算,如果结果相同的话,就表明它们在同一个子网络中。
总结一下,IP协议的作用主要有两个,一个是为每一台计算机分配IP地址,另一个是确定哪些地址在同一个子网络。
IP数据包
IP数据包在传输过程中会被放到以太网数据包的“数据”部分,分为”标头”和”数据”两个部分。
“标头”部分主要包括版本、长度、IP地址等信息,”数据”部分则是IP数据包的具体内容。
IP数据包的”标头”部分的长度为20到60字节,整个数据包的总长度最大为65,535字节。因此理论上,一个IP数据包的”数据”部分最长为65,515字节。前面说过,以太网数据包的”数据”部分,最长只有1500字节。因此,如果IP数据包超过了1500字节,它就需要分割成几个以太网数据包,分开发送了。
ARP协议
在发送方发送数据时,因为IP数据包是放在以太网数据包里发送的,所以我们必须知道对方的MAC地址和对方的IP地址。通常情况下,对方的IP地址是已知的(后文会解释),但是我们不知道它的MAC地址。
所以,我们需要一种机制,能够从IP地址得到MAC地址,这里可以分成两种情况。
第一种情况,两台主机不在同一个子网络,那么事实上没有办法得到对方的MAC地址,只能把数据包传送到两个子网络连接处的”网关”(gateway),让网关去处理。
第二种情况,两台主机在同一个子网络,那么我们可以用ARP协议,得到对方的MAC地址。ARP协议也是发出一个数据包(包含在以太网数据包中),其中包含它所要查询主机的IP地址,在对方的MAC地址这一栏,填的是FF:FF:FF:FF:FF:FF
,表示这是一个”广播”地址。它所在子网络的每一台主机,都会收到这个数据包,从中取出IP地址,与自身的IP地址进行比较。如果两者相同,做出回复,向对方报告自己的MAC地址。
传输层
传输层的由来
有了MAC地址和IP地址,可以在互联网上任意两台主机上建立通信。
问题是,同一台主机上有许多进程需要用到网络,也就是说,我们还需要一个参数,表示这个数据包到底供哪个进程使用。这个参数就叫做”端口”(port),它是每一个使用网卡的进程的编号。每个数据包都发到主机的特定端口,所以不同的程序就能取到自己所需要的数据。
“端口”是0到65535之间的一个整数,16个二进制位。0到1023的端口被系统占用,用户只能选用大于1023的端口。不管是浏览网页还是在线聊天,应用程序会随机选用一个端口,然后与服务器的相应端口联系。
“传输层”的功能,就是建立”端口到端口”的通信。相比之下,”网络层”的功能是建立”主机到主机”的通信。只要确定主机和端口,我们就能实现程序之间的交流。因此,Unix系统就把主机+端口,叫做”套接字”(socket)。有了它,就可以进行网络应用程序开发了。
UDP协议
现在,我们必须在数据包中加入端口信息,这就需要新的协议。最简单的实现叫做UDP协议,它的格式几乎就是在数据前面,加上端口号。
UDP数据包,也是由”标头”和”数据”两部分组成。”标头”部分主要定义了发出端口和接收端口,”数据”部分就是具体的内容。然后,把整个UDP数据包放入IP数据包的”数据”部分。
UDP数据包非常简单,”标头”部分一共只有8个字节,总长度不超过65,535字节,正好放进一个IP数据包。
TCP协议
UDP协议的优点是比较简单,容易实现,但是缺点是可靠性较差,一旦数据包发出,无法知道对方是否收到。
为了解决这个问题,提高网络可靠性,TCP协议就诞生了。这个协议非常复杂,但可以近似认为,它就是有确认机制的UDP协议,每发出一个数据包都要求确认。如果有一个数据包遗失,就收不到确认,发出方就知道有必要重发这个数据包了。
TCP协议并不能够确保数据不会遗失,但是可以确保数据一旦遗失,接收方可以得知这件事。它的缺点是过程复杂、实现困难、消耗较多的资源。
TCP数据包和UDP数据包一样,都是内嵌在IP数据包的”数据”部分。TCP数据包没有长度限制,理论上可以无限长,但是为了保证网络的效率,通常TCP数据包的长度不会超过IP数据包的长度,以确保单个TCP数据包不必再分割。
应用层
“应用层”的作用,就是规定应用程序的数据格式。
举例来说,TCP协议可以为各种各样的程序传递数据,比如Email、WWW、FTP等等。那么,必须有不同协议规定电子邮件、网页、FTP数据的格式,这些应用程序协议就构成了”应用层”。
这是最高的一层,直接面对用户。它的数据就放在TCP数据包的”数据”部分。
应用软件在应用层实现,应用层只需要专注于为用户提供应用功能,不用去关心数据是如何传输的。当两个不同设备的应用需要通信的时候,应用就把应用数据传给下一层,也就是传输层。
应用层工作在操作系统中的用户态,传输层及以下工作在内核态。
参考资料
HTTP篇
HTTP 是超文本传输协议,也就是HyperText Transfer Protocol。
HTTP/0.9
HTTP最早的版本是1991年发布的0.9版本,该版本极为简单,只有一个命令GET
GET /index.html
TCP 连接建立后,客户端向服务器请求网页index.html
。协议规定,服务器只能回应HTML格式的字符串,不能回应别的格式。
<html>
<body>Hello World</body>
</html>
服务器发送完毕,就关闭TCP连接。
HTTP/1.0
1996年5月,HTTP/1.0 版本发布,内容大大增加。
首先,任何格式的内容都可以发送。这使得互联网不仅可以传输文字,还能传输图像、视频、二进制文件。这为互联网的大发展奠定了基础。
其次,除了GET
命令,还引入了POST
命令和HEAD
命令,丰富了浏览器与服务器的互动手段。
方法 | 描述 |
---|---|
GET | 请求指定的页面信息,并返回具体内容,通常只用于读取数据。 |
HEAD | 类似于 GET 请求,只不过返回的响应中没有具体的内容,用于获取报头。 |
POST | 向指定资源请求数据处理,如提交表单或者上传文件,服务器返回处理结果。数据包含在请求体中。POST 请求可能会导致新的资源的建立或已有资源的更改。 |
再次,HTTP请求和回应的格式也变了。除了数据部分,每次通信都必须包括头信息(HTTP header),用来描述一些元数据。
其他的新增功能还包括状态码(status code)、多字符集支持、多部分发送(multi-part type)、权限(authorization)、缓存(cache)、内容编码(content encoding)等。
请求格式
GET / HTTP/1.0
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10_5)
Accept: */*
一行是请求命令,必须在尾部添加协议版本HTTP/1.0
。后面就是多行头信息,描述客户端的情况,其中User-Agent
字段标识了浏览器的身份。
回应格式
服务器的回应如下
HTTP/1.0 200 OK
Content-Type: text/plain
Content-Length: 137582
Expires: Thu, 05 Dec 1997 16:00:00 GMT
Last-Modified: Wed, 5 August 1996 15:55:28 GMT
Server: Apache 0.84
<html>
<body>Hello World</body>
</html>
回应的格式是”头信息 + 一个空行(\r\n
) + 数据”。其中,第一行是”协议版本 + 状态码(status code) + 状态描述”。
Content-Type 字段
关于字符的编码,1.0版规定,头信息必须是 ASCII 码,后面的数据可以是任何格式。因此,服务器回应的时候,必须告诉客户端,数据是什么格式,这就是Content-Type
字段的作用。常见的Content-Type
字段的值有:text/plain,text/html,text/css,image/jpeg,image/png,image/svg+xml,audio/mp4,video/mp4,application/javascript,application/pdf,application/zip,application/atom+xml等。
这些数据类型总称为MIME type
,每个值包括一级类型和二级类型,之间用斜杠分隔。MIME type
还可以在尾部使用分号,添加参数。
Content-Type: text/html; charset=utf-8
上面的类型表明,发送的是网页,而且编码是UTF-8。
客户端请求的时候,可以使用Accept
字段声明自己可以接受哪些数据格式。
Accept: */*
上面代码中,客户端声明自己可以接受任何格式的数据。
MIME type
还可以用在其他地方,如HTML网页。
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
<!-- 等同于 -->
<meta charset="utf-8" />
Content-Encoding 字段
由于发送的数据可以是任何格式,因此可以把数据压缩后再发送。Content-Encoding
字段说明数据的压缩方法。
Content-Encoding: gzip
Content-Encoding: compress
Content-Encoding: deflate
客户端在请求时,用Accept-Encoding
字段说明自己可以接受哪些压缩方法。
Accept-Encoding: gzip, deflate
状态码
HTTP/1.0 规定,所有HTTP响应的第一行都是状态行,依次是当前HTTP版本号,3位数字组成的状态代码,以及描述状态的短语,彼此由空格分隔。
HTTP/1.1 在 1.0 的基础上新增了 24 个错误状态响应码,随着HTTP协议的发展,状态码的种类也在不断扩大中。目前常用的状态码包括以下五类:
描述 | 常见状态码 | |
---|---|---|
1xx消息 | 请求已被服务器接收,继续处理 | |
2xx成功 | 请求已成功被服务器接收、理解、并接受 | 200、204、206 |
3xx重定向 | 需要后续操作才能完成这一请求 | 301、302、304 |
4xx请求错误 | 请求含有词法错误或者无法被执行 | 400、403、404 |
5xx服务器错误 | 服务器在处理某个正确请求时发生错误 | 500、501、502、503 |
HTTP/1.0 的缺点
HTTP/1.0 版的主要缺点是,每个TCP连接只能发送一个请求。发送数据完毕,连接就关闭,如果请求其他资源,就必须再新建一个连接。
TCP连接的新建成本很高,因为需要客户端和服务器三次握手,并且开始时发送速率较慢(slow start)。所以,HTTP 1.0版本的性能比较差。随着网页加载的外部资源越来越多,这个问题就愈发突出了。
为了解决这个问题,有些浏览器在请求时,用了一个非标准的Connection
字段。
Connection: keep-alive
这个字段要求服务器不要关闭TCP连接,以便其他请求复用。服务器同样回应Connection: keep-alive
。
一个可以复用的TCP连接就建立了,直到客户端或服务器主动关闭连接。但是,这不是标准字段,不同实现的行为可能不一致,因此不是根本的解决办法。
HTTP/1.1
1997年1月,HTTP/1.1 版本发布,只比 1.0 版本晚了半年,它进一步完善了 HTTP 协议,增加了六种请求方法:PUT
、PATCH
、 OPTIONS
、DELETE
、TRACE
和 CONNECT
。
方法 | 描述 |
---|---|
PUT | 替换指定的资源,没有的话就新增。 |
PATCH | 是对 PUT 方法的补充,用来对已知资源进行局部更新。 |
OPTIONS | 向服务器发送该方法,会返回对指定资源所支持的 HTTP 请求方法。 |
DELETE | 请求服务器删除 URL 标识的资源数据。 |
CONNECT | 将服务器作为代理,让服务器代替用户进行访问。 |
TRACE | 回显服务器收到的请求数据,即服务器返回自己收到的数据,主要用于测试和诊断。 |
持久连接
1.1 版的最大变化,就是引入了持久连接(persistent connection),即TCP连接默认不关闭,可以被多个请求复用,不用声明Connection: keep-alive
。
客户端和服务器发现对方一段时间没有活动,就可以主动关闭连接。不过,规范的做法是,客户端在最后一个请求时,发送Connection: close
,明确要求服务器关闭TCP连接。
管道机制
1.1 版还引入了管道机制(pipelining),即在同一个TCP连接里面,客户端可以同时发送多个请求。
举例来说,客户端需要请求两个资源。以前的做法是,在同一个TCP连接里面,先发送A请求,然后等待服务器做出回应,收到后再发出B请求。管道机制则是允许浏览器同时发出A请求和B请求,但是服务器还是按照顺序,先回应A请求,完成后再回应B请求。
Content-Length 字段
一个TCP连接现在可以传送多个回应,势必就要有一种机制,区分数据包是属于哪一个回应的。这就是Content-length
字段的作用,声明本次回应的数据长度。
Content-Length: 3495
上面代码告诉浏览器,本次回应的长度是3495个字节,这些字节会被截取作为本次回应。后面的字节作为下一个回应的内容。
分块传输编码
使用Content-Length
字段的前提条件是,服务器发送回应之前,必须知道回应的数据长度。
对于一些很耗时的动态操作来说,这意味着,服务器要等到所有操作完成,才能发送数据,显然这样的效率不高。更好的处理方法是,产生一块数据,就发送一块,采用”流模式”(stream)取代”缓存模式”(buffer)。
因此,1.1版规定可以不使用Content-Length
字段,而使用“分块传输编码”(chunked transfer encoding)。只要请求或回应的头信息有Transfer-Encoding
字段,就表明回应将由数量未定的数据块组成。
Transfer-Encoding: chunked
每个非空的数据块之前,会有一个16进制的数值,表示这个块的长度。最后是一个大小为0的块,就表示本次回应的数据发送完了。下面是一个例子。
HTTP/1.1 200 OK
Content-Type: text/plain
Transfer-Encoding: chunked
25
This is the data in the first chunk
1C
and this is the second one
3
con
8
sequence
0
HOST请求头
早期 HTTP/1.0 中认为每台服务器都绑定一个唯一的 IP 地址并提供单一的服务,请求消息中的 URL 并没有传递主机名。而随着虚拟主机的出现,一台物理服务器上可以存在多个虚拟主机,并且它们共享同一个 IP 地址。
为了支持虚拟主机,HTTP/1.1 中添加了 host 请求头,如Host: www.example.com
,请求消息和响应消息中应声明这个字段,若请求消息中缺少该字段时服务端会响应一个 404 错误状态码。
缺点
虽然1.1版允许复用TCP连接,但是同一个TCP连接里面,所有的数据通信是按次序进行的。服务器只有处理完一个回应,才会进行下一个回应。要是前面的回应特别慢,后面就会有许多请求排队等着。这称为“队头堵塞”(Head-of-line blocking)。
为了避免这个问题,只有两种方法:一是减少请求数,二是同时多开持久连接。这导致了很多的网页优化技巧,比如合并脚本和样式表、将图片嵌入CSS代码、域名分片(domain sharding)等等。如果HTTP协议设计得更好一些,这些额外的工作是可以避免的。
HTTPS
HTTP 由于是明文传输,所谓的明文,就是说客户端与服务端通信的信息都是肉眼可⻅的,随意使用一个抓包工具都可以截获通信的内容。
所以安全上存在以下三个⻛险
- 窃听风险:第三方可以获知通信内容;
- 篡改风险:第三方可以修改通信内容;
- 冒充风险:第三方可以冒充他人身份参与通信。
HTTPS 在 HTTP 与 TCP 层之间加入了SSL/TLS协议,来解决上述的⻛险。
- 所有信息都是加密传播,第三方无法窃听;
- 具有校验机制,一旦被篡改,通信双方会立刻发现;
- 配备身份证书,防止身份被冒充。
HTTPS经由HTTP进行通信,但利用SSL/TLS来加密数据包。HTTPS开发的主要目的,是提供对网站服务器的身份认证,保护交换资料的隐私与完整性。这个协议由网景公司(Netscape)在1994年首次提出,随后扩展到互联网上。
历史上,HTTPS连接经常用于万维网上的交易支付和企业信息系统中敏感信息的传输。在2000年代末至2010年代初,HTTPS开始广泛使用,以确保各类型的网页真实,保护账户和保持用户通信,身份和网络浏览的私密性。
在真正认识SSL/TLS以前,需要了解一些基础的加密算法知识。
加密算法
加密算法分为以下两种:
对称加密
甲方选择某一种加密规则,对信息进行加密;乙方使用同一种规则,对信息进行解密;
加密和解密使用同样规则(简称”密钥“)。
甲方必须把加密规则告诉乙方,否则无法解密。保存和传递密钥,就成了最头疼的问题。
后来,人们认识到,加密和解密可以使用不同的规则,只要这两种规则之间存在某种对应关系即可,这样就避免了直接传递密钥。
不对称加密
- 乙方生成两把密钥(公钥和私钥)。公钥是公开的,任何人都可以获得,私钥则是保密的;甲方获取乙方的公钥,然后用它对信息加密。乙方得到加密后的信息,用私钥解密;
- 不对称加密又称公钥加密法;
- 公钥和私钥是一一对应的关系,用公钥可以解开私钥加密的信息,反之亦成立;
- 同时生成公钥和私钥应该相对比较容易,但是从公钥推算出私钥,应该是很困难或不可能的;
- 在双钥体系中,公钥用来加密/验签,私钥用来解密/签名。
如果公钥加密的信息只有私钥解得开,那么只要私钥不泄漏,通信就是安全的。
数字证书
不对称加密过程中,双方的通信是建立在公钥可信的基础上的:
- 服务器对数据进行Hash生成摘要后,使用私钥对摘要进行签名运算生成数字签名,服务器将数据和数字签名一同发给客户端;
- 客户端对数据进行相同的Hash,并对数字签名使用服务器的公钥解密,如果解密结果与Hash结果相同,则确认为服务器发出。
如果一开始客户端拿到的公钥就是假的话,那么服务器后来发出的任何内容客户端均无法识别。问题在于客户端获取公钥的过程依然是不安全的,为此提出数字证书的方法,通过可信的第三方证书颁发机构(CA)保证服务器的公钥通过可信的方式传递:
- 服务器将自己的公钥及其他相关信息发送给CA,CA核实身份后为其颁发一个数字证书,证书包括服务器公钥,证书办法机构,有效期等数据。同时,CA生成一对公钥私钥,使用私钥对数字证书进行签名,并将签名添加到数字证书中,数字证书 = 服务器公钥等信息 + 这些信息的数字签名;
- 客户端系统默认安装了根证书,根证书里记录了可以信赖的CA机构信息及公钥信息,根证书预先安装在系统中杜绝被篡改的可能;
- 服务器将数据,数字签名,自身的数字证书一同发送给客户端;
- 客户端根据CA的公钥对数字证书中的签名解密,并对数字证书做Hash运算,两者相同则可以确保数字证书的可信性,从证书中拿到服务器的公钥即可进行验签。
为什么先进行摘要再进行签名?
原数据过大,加密算法耗时。
数字签名的作用
对数据进行校验,并确保数据的发送者。
SSL/TLS协议
SSL/TLS协议的基本思路是采用公钥加密法,也就是说,客户端先向服务器端索要公钥,然后用公钥加密信息,服务器收到密文后,用自己的私钥解密。
但是,这里有个问题:公钥加密计算量太大,如何减少耗用的时间?
采用混合加密。每一次对话,客户端和服务器端都生成一个”对话密钥“,用它来加密信息。由于”对话密钥”是对称加密,所以运算速度非常快,而服务器公钥只用于加密”对话密钥”本身,这样就减少了加密运算的消耗时间。
因此,SSL/TLS协议的基本过程是这样的:
客户端向服务器端索要并验证公钥;
双方协商生成”对话密钥”;
双方采用”对话密钥”进行加密通信;
上面过程的前两步,又称为”握手阶段”(SSL/TLS握手在TCP握手之后)。
SSL/TSL握手
基于RSA加密的握手过程,也是基础的SSL/TSL握手过程。
1. 客户端发出请求(ClientHello)
客户端向服务器发出加密通信的请求,称为ClientHello请求,客户端主要向服务器提供以下信息:
(1)支持的协议版本,比如TLS 1.0版。
(2)一个客户端生成的随机数Client random,稍后用于生成"对话密钥"。
(3)支持的加密方法,比如RSA公钥加密。
(4)支持的压缩方法。
2. 服务器回应(SeverHello)
服务器的回应包含以下内容:
(1)确认使用的加密通信协议版本,比如TLS 1.0版本。如果浏览器与服务器支持的版本不一致,服务器关闭加密通信。
(2)一个服务器生成的随机数Server random,稍后用于生成"对话密钥"。
(3)确认使用的加密方法,比如RSA公钥加密。
(4)服务器证书。
3. 客户端回应
客户端收到服务器回应以后,首先验证服务器证书。如果证书不是可信机构颁布、或者证书中的域名与实际域名不一致、或者证书已经过期,就会向访问者显示一个警告,由其选择是否还要继续通信。
如果证书没有问题,客户端就会从证书中取出服务器的公钥。然后,向服务器发送下面三项信息。
(1)一个随机数。该随机数用服务器公钥加密,防止被窃听。
(2)编码改变通知,表示随后的信息都将用双方商定的加密方法和密钥发送。
(3)客户端握手结束通知,表示客户端的握手阶段已经结束。这一项同时也是前面发送的所有内容的hash值,用来供服务器校验。
此外,如果前一步,服务器要求客户端证书,客户端会在这一步发送证书及相关信息。
上面第一项的随机数,是整个握手阶段出现的第三个随机数,又称”pre-master key”。有了它以后,客户端和服务器就同时有了三个随机数,接着双方就用事先商定的加密方法,各自生成本次会话所用的同一把”会话密钥”。
值得注意的是,该过程之前的阶段均为明文传输;而pre-master key使用服务器公钥加密,之后客户端与服务器端均生成会话密钥,数据传输均为加密传输。
4. 服务器的最后回应
服务器收到客户端的第三个随机数pre-master key之后,计算生成本次会话所用的”会话密钥”。然后,向客户端最后发送下面信息。
(1)编码改变通知,表示随后的信息都将用双方商定的加密方法和密钥发送。
(2)服务器握手结束通知,表示服务器的握手阶段已经结束。这一项同时也是前面发送的所有内容的hash值,用来供客户端校验。
至此,整个握手阶段全部结束。接下来,客户端与服务器进入加密通信,就完全是使用普通的HTTP协议,只不过用”会话密钥”加密内容。
使用 RSA密钥协商算法的最大问题是不支持前向保密。整个握手阶段都不加密(也没法加密),都是明文的。如果有人窃听通信,他可以知道双方选择的加密方法,以及三个随机数中的两个。整个通话的安全,只取决于第三个随机数(Premaster secret)能不能被破解。所以一旦服务端的私钥泄漏 了,过去被第三方截获的所有 TLS 通讯密文都会被破解。所以目前使用较多的为ECDHE加密法。
DH算法
离散对数
假定 a, p 均是素数,下面两个集合相等,证明过程请参考 Cryptography and Network Security 第八章:
{ a^1 mod p, a^2 mod p, ..., a^(p-1) mod p } = {1, 2, ... , p-1 } {} 表示集合
上述式子可概括成以下三点,对于 1 <= x,y <= p - 1,有:
- a^x mod p 一定属于 {1, 2, …, p -1 }
- 如果 x != y,则 a^x mod p != a^y mod p
- 对于 1 <= b <= p - 1,一定存在唯一的 1 <= x <= p-1,使得 b = a^x mod p
第三点在求解上有这么一个特点:已知 x 求 b 非常容易,已知 b 求 x 非常困难,特别当 p 很大时,求解的复杂度非常高,所以它又被称为离散对数问题 (Discrete logarithm),它是 DH 算法能够安全交换密钥的基础
求模公式
假设 q 为素数,对于正整数 a,x,y,有:
(a^x mod p)^y mod p = a^(xy) mod p
证明如下:
令 a^x = mp + n, 其中 m, n 为自然数, 0 <= n < p,则有
C = (a^x mod p)^y mod p
= ((mp + n) mod p)^y mod p
= n^y mod p
= (mp +n)^y mod p
= a^(xy) mod p
Deffie-Hellman 算法
- 首先 A, B 共同选取 p 和 a 两个素数,p 和 a 均公开;
- 之后 A 选择一个自然数 Xa < p,计算出 Ya = a^Xa mod p,Xa 保密,Ya 公开;
- 同理,B 选择 Xb < p 并计算出 Yb = a^Xb mod p,其中 Xb 保密,Yb 公开;
- A 用 Yb 和 Xa 计算出密钥 K = Yb^Xa mod p,而 B 用 Ya 和 Xb 计算密钥 K = Ya^Xb mod p。
流程如下:
+-------------------------------------------------------------------+
| Global Pulic Elements |
| |
| p prime number |
| a prime number, a < p |
+-------------------------------------------------------------------+
+-------------------------------------------------------------------+
| User A Key Generation |
| |
| Select private Xa Xa < p |
| Calculate public Ya Ya = a^Xa mod p |
+-------------------------------------------------------------------+
+-------------------------------------------------------------------+
| User B Key Generation |
| |
| Select private Xb Xb < p |
| Calculate public Yb Yb = a^Xb mod p |
+-------------------------------------------------------------------+
+-------------------------------------------------------------------+
| Calculation of Secret Key by User A |
| |
| Secret Key K K = Yb^Xa mod p |
+-------------------------------------------------------------------+
+-------------------------------------------------------------------+
| Calculation of Secret Key by User B |
| |
| Secret Key K K = Ya^Xb mod p |
+-------------------------------------------------------------------+
下面证明,A 和 B 计算出来的密钥 K 相同。
K = Yb^Xa mod p
= (a^Xb mod p)^Xa mod p
= a^(Xa * Xb) mod p 根据上述求模公式
= (a^Xa mod p)^Xb mod p
= Ya^Xb mod p
上面一共出现了 a, p, Xa, Ya, Xb, Yb, K 共 7 个数,其中:
- 公开的数:a, p, Ya, Yb
- 非公开数:Xa, Xb, K
通常情况下,a 一般为 2 或 5,而 p 的取值非常大,至少几百位,Xa 和 Xb 的取值也非常大,其复杂度至少为 O(p^0.5)。对于攻击者来说,已知 Ya,Xa 的求解非常困难,同理 Xb 的求解也很困难,所以攻击者难以求出 K,所以 DH 能够保证通信双方在透明的信道中安全的交换密钥。
DH 算法主要实现方法为DHE 算法,DHE 算法让双方的私钥在每次密钥交换通信时,都是随机生成的、临时的,E 全称是 ephemeral(临时性的)。这样就保证了 前向安全,即使曾经的通信过程被破解,不会影响现在的通信的安全性。
HTTP/2
2015年,HTTP/2 发布。它不叫 HTTP/2.0,是因为标准委员会不打算再发布子版本了,下一个新版本将是 HTTP/3。HTTP/2必须在HTTPS 环境才会生效。
二进制协议
HTTP/1.1 版的头信息肯定是文本(ASCII编码),数据体可以是文本,也可以是二进制。HTTP/2 则是一个彻底的二进制协议,头信息和数据体都是二进制,并且统称为”帧”(frame):头信息帧和数据帧。
二进制协议的一个好处是,可以定义额外的帧。HTTP/2 定义了近十种帧,为将来的高级应用打好了基础。如果使用文本实现这种功能,解析数据将会变得非常麻烦,二进制解析则方便得多。
多工
HTTP/2 复用TCP连接,在一个连接里,客户端和浏览器都可以同时发送多个请求或回应,而且不用按照顺序一一对应,这样就避免了”队头堵塞”。
举例来说,在一个TCP连接里面,服务器同时收到了A请求和B请求,于是先回应A请求,结果发现处理过程非常耗时,于是就发送A请求已经处理好的部分, 接着回应B请求,完成后,再发送A请求剩下的部分。
这样双向的、实时的通信,就叫做多工(Multiplexing)。
数据流
因为 HTTP/2 的数据包是不按顺序发送的,同一个连接里面连续的数据包,可能属于不同的回应。因此,必须要对数据包做标记,指出它属于哪个回应。
HTTP/2 将每个请求或回应的所有数据包,称为一个数据流(stream)。每个数据流都有一个独一无二的编号。数据包发送的时候,都必须标记数据流ID,用来区分它属于哪个数据流。另外还规定,客户端发出的数据流,ID一律为奇数,服务器发出的,ID为偶数。
数据流发送到一半的时候,客户端和服务器都可以发送信号(RST_STREAM
帧),取消这个数据流。1.1版取消数据流的唯一方法,就是关闭TCP连接。这就是说,HTTP/2 可以取消某一次请求,同时保证TCP连接还打开着,可以被其他请求使用。
客户端还可以指定数据流的优先级。优先级越高,服务器就会越早回应。
头信息压缩
HTTP 协议不带有状态,每次请求都必须附上所有信息。所以,请求的很多字段都是重复的,比如Cookie
和User Agent
,一模一样的内容,每次请求都必须附带,这会浪费很多带宽,也影响速度。
HTTP/2 对这一点做了优化,引入了头信息压缩机制(header compression)。一方面,头信息使用gzip
或compress
压缩后再发送;另一方面,客户端和服务器同时维护一张头信息表,所有字段都会存入这个表,生成一个索引号,以后就不发送同样字段了,只发送索引号,这样就提高速度了。
服务器推送
HTTP/2 允许服务器未经请求,主动向客户端发送资源,这叫做服务器推送(server push)。
常见场景是客户端请求一个网页,这个网页里面包含很多静态资源。正常情况下,客户端必须收到网页后,解析HTML源码,发现有静态资源,再发出静态资源请求。其实,服务器可以预期到客户端请求网页后,很可能会再请求静态资源,所以就主动把这些静态资源随着网页一起发给客户端了。
参考资料
koala bear.理解 Deffie-Hellman 密钥交换算法
TCP篇
TCP是面向连接
的、可靠
的、基于字节流
的传输层通信协议。
TCP头部
在介绍三次握手,四次挥手的过程之前,需要了解TCP头部的一些知识
源端口
和目的端口:确定主机中的进程;- 序号(seq):报文段中的的第一个数据字节在数据流中的序号,主要用来解决网络报乱序的问题;
- 确认号(ack_seq):所期望收到的下一个数据包的序号;
- ACK:1有效,表示已经收到数据包
- RST:1有效,连接复位请求
- SYN:1有效,表示申请与接收方建立连接
- FIN:1有效,表示申请与接收方断开连接
TCP三次握手
网络中A与B要建立TCP连接,可以分为以下四步:
(1)A请求向B发送消息
(2)B向A表示同意
(3)B请求向A发送消息
(4)A向B表示同意
可以发现,由于(2)(3)过程均为B向A发送消息,所以合为一次握手,实际上TCP建立连接只需要三次握手即可。
接下来介绍三次握手的具体过程
(1)服务器进程创建传输控制块TCB,准备接受连接请求,进入LISTEN(监听)状态;
(2)客户端进程创建传输控制块TCB,发送SYN=1,seq初始化为随机值X的连接请求报文段,之后进入SYN_SEND(同步已发送)状态;
(3)收到连接请求后,服务器进程向客户端进程发送一个响应+请求报文段,ACK=1,SYN=1,ack_seq=X+1,seq初始化为随机值Y,进入SYN_RCVD(同步收到)状态;
(3)客户端进程响应服务器进程的连接请求,向B发送一个ACK=1的报文段,置seq=X+第一次握手报文长度1,ack_seq=Y,之后客户端进程客户端进程进入ESTABLISHED(已建立连接)状态;
(4)服务器进程收到客户端进程响应后同样进入ESTABLISHED(已建立连接)状态。
半连接队列与全连接队列
服务器第一次收到客户端的 SYN之后,就会处于 SYN_RCVD状态,此时双方还没有完全建立连接。服务器会把这种状态下的请求连接放在一个队列里,我们把这种队列称之为半连接队列。
已经完成三次握手,建立起连接的就会放在全连接队列中。
如果半连接队列或全连接队列满了就有可能会出现丢包现象。
三次握手可以携带数据吗
第一次、第二次握手不可以携带数据,而第三次握手是可以携带数据的。
假如第一次握手可以携带数据的话,如果有人要恶意攻击服务器,在第一次握手中的 SYN 报文中放入大量的数据,疯狂重复发 SYN 报文,这会让服务器花费大量的内存空间来缓存这些报文。
对于第三次握手,此时客户端已经处于连接状态,他已经知道服务器的接收、发送能力是正常的了,所以可以携带数据是情理之中。
如果三次握手时每次握手信息对方没有收到
若第一次握手服务器未接收到客户端请求建立连接的数据包,客户端由于在一段时间内没有收到服务器发来的确认报文, 因此会重新发送 SYN 报文,若仍然没有回应,则重复上述过程直到发送次数超过最大重传次数限制后,建立连接的系统调用会返回 -1。
若第二次握手客户端未接收到服务器回应的 ACK 报文时,客户端会采取第一次握手失败时的动作,而服务器端此时将阻塞在 accept() 系统调用处等待 client 再次发送 ACK 报文。
若第三次握手服务器未接收到客户端发送过来的 ACK 报文,服务器端同样会采取类似于客户端的超时重传机制,若重传次数超过限制后仍然没有回应,则 accep() 系统调用返回 -1,服务器端连接建立失败。但此时客户端认为自己已经连接成功了,因此开始向服务器端发送数据,但是服务器端的 accept() 系统调用已返回,此时没有在监听状态。因此服务器端接收到来自客户端发送来的数据时会发送 RST 报文给 客户端,消除客户端单方面建立连接的状态。
为什么不可以两次握手
三次握手的主要目的是确认双方的收发能力。若采用两次握手,客户端能得出结论:服务端的接收、发送能力,客户端的接收、发送能力是正常的。但此时服务器并不能确认客户端的接收能力是否正常。
如果客户端未接收到第二次握手的包,或是客户端收到该包之前已不想建立TCP连接,而服务器并不知情,如果没有第三次握手告诉服务器,服务器端的端口就会一直开着,若客户端因超时重新发出请求时,服务器就会重新开启一个端口连接。这样的端口越来越多,就会造成服务器开销的浪费。
如建立连接后客户端出现故障
TCP还设有一个保活计时器,显然,客户端如果出现故障,服务器不能一直等下去,白白浪费资源。服务器每收到一次客户端的请求后都会重新复位这个计时器,时间通常是设置为2小时,若两小时还没有收到客户端的任何数据,服务器就会发送一个探测报文段,以后每隔75秒发送一次。若一连发送10个探测报文仍然没反应,服务器就认为客户端出了故障,接着就关闭连接。
TCP四次挥手
挥手过程,A与B断开连接之后,B可能还会有需要发个A的数据,不能立刻断开连接,需要有一个CLOSE_WAIT的过程,不能合为三次。
(1)A将FIN置为1,ACK置为1,seq设置为X=上一次对方传送过来的ack_seq,ack_seq设置为Y=为上一次对方传过来的seq+1。将数据发送至B,然后A进入FIN_WAIT_1状态;
(2)B收到了A发送的FIN报文段,向A回复,将ACK置为1,seq设置为Y,ack_seq设置为X+1。然后B进入CLOSE_WAIT状态,A收到B的回复后,进入FIN_WAIT_2状态;
(3)B再次向A发送报文,FIN置为1,ACK置为1,seq设置为Y,ack_seq设置为X+1,然后B进入LAST_ACK状态,A收到B的报文后,进入TIME_WAIT状态;
(4)A收到B发送的FIN报文段,向B回复,ACK置为1,seq设置为X+1,ack_seq设置为Y+1。然后A进入TIME_WAIT状态,B在收到报文后进入CLOSED状态。A在发送完报文等待了2MSL时间后进入CLOSED状态。
为什么 TIME_WAIT 状态要等待 2MSL 之后才关闭连接
2MSL表示两个MSL的时长,MSL全称为Maximum Segment Life,表示TCP Segment 生存时间的限制。
若服务器在 1 MSL 内没有收到客户端发出的 ACK 确认报文,再次向客户端发出 FIN 报文。如果客户端在 2 MSL 内收到了服务器再次发来的 FIN 报文,说明服务器并没有收到客户端发出的 ACK 确认报文。客户端将再次向服务器发出 ACK 确认报文,并重新开始 2 MSL 的计时。
若服务端将重发 FIN 报文时客户端并没有维持 TIME-WAIT 状态而直接关闭,当收到服务端重新发送的 FIN 包时,客户端就会用 RST 包来响应服务端,这将会使得对方认为是有错误发生,然而其实只是正常的关闭连接过程,并没有出现异常情况。
此外,如果客户端在收到服务端的 FIN 报文后立即关闭连接,此时服务端相应的端口并没有关闭,若客户端在相同的端口立即建立新的连接,则有可能接收到上一次连接中残留的数据包,可能会导致不可预料的异常出现。
TIME_WAIT 状态导致的问题
考虑高并发短连接的业务场景,在高并发短连接的 TCP 服务器上,当服务器处理完请求后主动请求关闭连接,这样服务器上会有大量的连接处于 TIME_WAIT 状态,服务器维护每一个连接需要一个 socket,也就是每个连接会占用一个文件描述符,而文件描述符的使用是有上限的,如果持续高并发,会导致一些正常的连接失败。
服务器可以设置 SO_REUSEADDR 套接字选项来通知内核,如果端口被占用,但 TCP 连接位于 TIME_WAIT 状态时可以重用端口。如果服务器程序停止后想立即重启,而新的套接字依旧希望使用同一端口,此时 SO_REUSEADDR 选项就可以避免 TIME-WAIT 状态。
也可以采用长连接的方式减少 TCP 的连接与断开,在长连接的业务中往往不需要考虑 TIME-WAIT 状态,但其实在长连接的业务中并发量一般不会太高。
参考资料
网络安全篇
安全攻击分类
被动攻击:攻击者窃听监听数据传输,从而获取到传输的数据信息。主要有:消息内容泄露攻击和流量分析攻击。由于并没有修改数据,这种攻击是很难被检测到的。
主动攻击:攻击者修改传输的数据流或者故意添加错误的数据流,如假冒用户身份从而得到一些权限,进行权限攻击,除此之外,还有重放、改写和拒绝服务等。
ARP攻击
ARP 是一种非常不安全的协议,局域网上的任何一台主机如果接收到一个 ARP 应答报文,并不会去检测这个报文的真实性,而是直接记入自己的 ARP 缓存表中,当 ARP 表中的某一记录长时间不适使用,就会被删除。ARP 攻击就是利用了这一点,攻击者疯狂发送 ARP 报文,源 IP 地址为被攻击者的 IP 地址,而源 MAC 地址为攻击者的 MAC 地址。通过不断发送这些伪造的 ARP 报文,网络内部的主机和网关的 ARP 表中被攻击者的 IP 地址对应攻击者的 MAC 地址。所有发送给被攻击者的信息都会发送到攻击者的主机上,从而产生 ARP 欺骗。ARP 欺骗分为以下几种:
- 洪泛攻击
攻击者恶意向局域网中的网关、路由器和交换机等发送大量 ARP 报文,设备 CPU 忙于处理 ARP 协议,难以响应正常的服务请求。表现通常为:网络中断或者网速很慢。
- 欺骗主机
也叫仿冒网关攻击,攻击者通过 ARP 欺骗使得被攻击者 ARP 表中网关对应的 MAC 地址为攻击者的 MAC。这样一来被攻击者要通过网关发送出去的数据流就会发往攻击者这里,造成用户数据外泄。
- 欺骗网关
和欺骗主机的攻击方式类似,不过欺骗对象是局域网的网关。当局域网中的主机向网关发送数据时,网关会把数据发送给攻击者,该攻击方式同样会造成用户数据外泄。
- 中间人攻击
攻击者同时欺骗网关和主机,局域网的网关和主机发送的数据最后都会到达攻击者这边。这样,网关和用户的数据就会泄露。
- IP 地址冲突
攻击者对局域网中的主机进行扫描,然后根据物理主机的 MAC 地址进行攻击,导致局域网内的主机产生 IP 冲突,使得用户的网络无法正常使用。
DDoS攻击
DDoS 全称 Distributed Denial of Service,分布式拒绝服务。一般来说是指攻击者利用不同位置上的“肉鸡”对目标网站在较短的时间内发起大量请求,大规模消耗目标网站的主机资源,让它无法正常服务。和单一的 DoS 攻击相比,DDoS 是借助数百台或者数千台已被入侵并添加了攻击进程的主机组成“僵尸网络”,一起发起网络攻击。
DDOS 不是一种攻击,而是一大类攻击的总称。它有几十种类型,新的攻击方法还在不断发明出来。
SYN Flood
这是一种利用TCP协议缺陷,发送大量伪造的TCP连接请求,从而使得被攻击方资源耗尽(CPU满负荷或内存不足)的攻击方式。
建立TCP连接,需要三次握手:客户端发送SYN报文,服务端收到请求并返回报文表示接受,客户端也返回确认,完成连接。如果客户端向服务器发送报文后死机或掉线,服务器在发出应答报文后就无法收到客户端的确认报文,这时服务器端一般会重试并等待一段时间后再丢弃这个未完成的连接。Linux下默认会进行5次重发 SYN-ACK 包,5次的重试时间间隔为1s, 2s, 4s, 8s, 16s, 总共31s, 称为指数退避
,第5次发出后还要等32s才知道第5次也超时了,所以,总共需要63s, TCP 才会把断开这个连接。由于 SYN 超时需要63秒,就给攻击者一个攻击服务器的机会,攻击者在短时间内发送大量的SYN包给Server,服务器端为了维护数以万计的半连接而消耗非常多的资源,最后Server的SYN队列被耗尽或可用端口被全部占用等。
防范SYN Flood的方法
- Syn Cache技术
这种技术是在收到SYN数据报文时不急于去分配TCB,而是先回应一个SYN ACK报文,并在一个专用HASH表(Cache)中保存这种半开连接信息,直到收到正确的回应ACK报文再分配TCB。在FreeBSD系统中这种 Cache每个半开连接只需使用160字节,远小于TCB所需的736个字节。在发送的SYN ACK中需要使用一个己方的Sequence Number,这个数字不能被对方猜到,否则对于某些稍微智能一点的Syn Flood攻击软件来说,它们在发送Syn报文后会发送一个ACK报文,如果己方的Sequence Number被对方猜测到,则会被其建立起真正的连接。因此一般采用一些加密算法生成难于预测的Sequence Number。
- Syn Cookie技术
Syn Cache虽然不分配TCB,但是为了判断后续对方发来的ACK报文中的Sequence Number的正确性,还是需要使用一些空间去保存己方生成的Sequence Number等信息,也造成了一些资源的浪费。
Syn Cookie技术则完全不使用任何存储资源,这种方法比较巧妙,它使用一种特殊的算法生成Sequence Number,这种算法考虑到了对方的IP、端口、己方IP、端口的固定信息,以及对方无法知道而己方比较固定的一些信息,如MSS(Maximum Segment Size,最大报文段大小,指的是TCP报文的最大数据报长度,其中不包括TCP首部长度。)、时间等,在收到对方 的ACK报文后,重新计算一遍,看其是否与对方回应报文中的(Sequence Number-1)相同,从而决定是否分配TCB资源。
- Syn Proxy防火墙
Syn Cache技术和Syn Cookie技术总的来说是一种主机保护技术,需要系统的TCP/IP协议栈的支持,而目前并非所有的操作系统支持这些技术。因此很多防火墙中都提供一种SYN代理的功能,其主要原理是对试图穿越的SYN请求进行验证后才放行:
防火墙在确认了连接的有效性后,才向内部的服务器(Listener)发起SYN请求,在右图中,所有的无效连接均无法到达内部的服务器。而防火墙采用的验证连接有效性的方法则可以是Syn Cookie等其他技术。
CC 攻击
CC攻击是目前应用层攻击的主要手段之一,攻击者模拟多个正常用户送来大量正常的请求,超出服务器的最大承受量,导致宕机。
防范 CC 攻击的方法
- 拦截HTTP请求
如果恶意请求有特征,对付起来很简单:直接拦截它就行了。
HTTP 请求的特征一般有两种:IP 地址和 User Agent 字段。比如,恶意请求都是从某个 IP 段发出的,那么把这个 IP 段封掉就行了。或者,它们的 User Agent 字段有特征(包含某个特定的词语),那就把带有这个词语的请求拦截。
- 带宽扩容
HTTP 拦截有一个前提,就是请求必须有特征。但是,真正的 DDOS 攻击是没有特征的,它的请求看上去跟正常请求一样,而且来自不同的 IP 地址,所以没法拦截。
对于网站来说,如果可以在短时间内急剧扩容,提供几倍或几十倍的带宽,就可以顶住大流量的请求。这就是为什么云服务商可以提供防护产品,因为他们有大量冗余带宽,可以用来消化 DDoS 攻击。当有大量请求时,DNS 将访问量均匀分配到这四台镜像服务器。
- CDN
CDN 指的是网站的静态内容分发到多个服务器,用户就近访问,提高速度。因此,CDN 也是带宽扩容的一种方法,可以用来防御 DDOS 攻击。
网站内容存放在源服务器,CDN 上面是内容的缓存。用户只允许访问 CDN,如果内容不在 CDN 上,CDN 再向源服务器发出请求。这样的话,只要 CDN 够大,就可以抵御很大的攻击。不过,这种方法有一个前提,网站的大部分内容必须可以静态缓存。对于动态内容为主的网站(比如论坛),就要想别的办法,尽量减少用户对动态数据的请求。
上一节提到的镜像服务器,本质就是自己搭建一个微型 CDN。各大云服务商提供的高防 IP,背后也是这样做的:网站域名指向高防 IP,它提供一个缓冲层,清洗流量,并对源服务器的内容进行缓存。
这里有一个关键点,使用 CDN,千万不要泄露源服务器的 IP 地址,否则攻击者可以绕过 CDN 直接攻击源服务器,前面的努力都白费。
参考资料
本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!