自顶向下框架性地整理复习了一遍计网
计算机网络
网络模型
OSI七层模型
- 应用层
- 表示层
- 会话层
- 传输层
- 网络层
- 数据链路层
- 物理层
TCP/IP五层模型
- 应用层
- 运输层
- 网络层
- 数据链路层
- 物理层
应用层
协议是计算机与网络设备要进行通信时要约定的规则。“计算机与网络设备要相互通信,双方就必须基于相同的方法。比如,如何探测到通信目标、由哪一边先发起通信、使用哪种语言进行通信、怎样结束通信等规则都需要事先确定。不同的硬件、操作系统之间的通信,所有的这一切都需要一种规则。而我们就把这种规则称为协议(protocol)。”
在应用层中,常见的协议有Http、Https、FTP(文件传输协议)、SMTP(邮件传输协议)、DNS等。其中Https、FTP、DNS都是常见且在TCP/IP协议簇中非常重要的协议。
Http
超文本传输协议(英语:HyperText Transfer Protocol),Http与Https协议是我们日常在使用internet时最常看见的协议,它是一个用于分布式、协作式和超媒体信息系统的应用层协议,是网络数据通信的基础。
Http协议的默认端口是80,常用的运输层协议是TCP。在发起请求时,在Http中,首先发起对目标主机的TCP连接,在建立TCP连接后使用该连接向目标主机发送Http协议的请求。常见的请求方法有
- GET:最基础的请求方法,使用GET方法应该只在请求资源时使用。
- GET方法不应该产生副作用,也就是说GET请求要求是幂等的,多次发起相同的GET请求不会对资源产生影响。
- 在Http0.9版本中,只定义了GET方法。 GET方法在请求时如果需要携带参数,参数被直接携带在URL中,在域名的末尾使用
?
来表示开始携带参数,参数与参数之间以&
隔开,参数内部以key=value
的格式编写。一般来说,浏览器可以选择/或者默认会 缓存GET请求以提高浏览器的响应速度。
- POST:向服务器提交资源的方法,常用的场景是向服务器提交表单或上传文件。
- 与
GET
方法不同的是,POST方法并不是一个幂等的方法,每次POST方法都有可能会新建或修改服务器上的资源,所以浏览器在提交相同的POST请求时会弹出弹窗让用户确认是否要重复提交表单。 - 在POST方法中,方法的参数一般来说被编码在请求的
body
中,与GET方法直接在编写在URL中的方式相比,POST方法可以说相对安全一些。但需要注意的是,由于HTTP协议是明文传输的,所以即使参数放在BODY中也是不安全的,在抓包时里面的数据都可以看得一清二楚。 因此,如果需要更加安全的传输,可以对POST方法的BODY部分的数据进行加密。 - 由于POST请求携带的数据放在BODY部分,所以它可以携带的数据一般来说比GET更多,GET请求的URL长度是有限制的,也就是说参数长度以及数量是有限的。
- 在许多情况下,浏览器会对POST请求做一些优化以提高网络服务的速度。由于POST请求常用来提交文件或表单,所以它的BODY部分的数据可能较大。浏览器会将POST优化成两个请求,在第一个请求中发送原请求的header,获得服务器的成功响应后再发送BODY。
- 与
- PUT:向服务器更新指定资源最新的数据
- HEAD:与GET方法一样是一个幂等的请求,并且只用来请求资源。但它不返回资源的真正数据部分,只返回资源的信息。
- DELETE:要求服务器删除某个特定资源。
副作用与幂等
假如在不考虑诸如错误或者过期等问题的情况下,若干次请求的副作用与单次请求相同或者根本没有副作用,那么这些请求方法就能够被视作“幂等(idempotence)”的。GET,HEAD,PUT和DELETE方法都有这样的幂等属性,同样由于根据协议,OPTIONS,TRACE都不应有副作用,因此也理所当然也是幂等的。
也就是说,根据协议来除了POST请求外其他的请求都要求是幂等的,但实际情况下可能不同
需要注意的是,假如一个由若干请求组成的请求序列产生的结果,在重复执行这个请求序列或者其中任何一个或多个请求后仍没有发生变化,则这个请求序列便是“幂等”的。但是,可能出现一个由若干请求组成的请求序列是“非幂等”的,即使这个请求序列中所有执行的请求方法都是幂等的。例如,这个请求序列的结果依赖于某个会在下次执行这个序列的过程中被修改的变量。
无状态
HTTP协议是一个无连接、无状态、不可靠的应用层传输协议。
- 无状态:指的是HTTP并不知道使用它进行通信双方的身份,也不保存每次请求的状态。简单的来说,就是它对事务的处理是没有记忆能力的,不保存上次处理过的事务的状态。
- 有状态:能保存之前处理过的事务的状态。在HTTP1.1中,由于发现在日常的网络中很需要保存事务的状态,比如用户的登录状态等,所以引入了
cookie
来实现状态的保存。当需要使用cookie时,服务器在响应报文header中的set-cookie
字段中放入一个cookie值,客户端收到后就将cookie保存下来。所以,cookie是保存在客户端的,而session是保存在服务器的。 在下次请求时,客户端在请求报文header的cookie
字段中填入之前保存的cookie值,服务器收到请求后读取cookie
字段的值并用该值查询数据库,就知道了该客户端之前的状态。 - 不可靠:HTTP提供的是
尽力而为
的交付服务,并不保证数据是否正确送达。所以HTTP是不安全、不可靠的协议,但如果HTTP使用了TCP协议,那么TCP协议将可以保证可靠交付。 - 无连接:在进行通信前是否需要与目标建立实际连接。
Cookie
Cookie是指某些服务端存储在用户本地终端上的数据(通常经过加密),用来存储用户的状态。
Cookie根据存放位置的不同有两个分类:
- 非持久性Cookie:该类Cookie被存放在浏览器的内存中,一般存活时间较短,当浏览器进程被关闭时该Cookie也随之被清除。
- 持久性Cookie:该类Cookie被存放在硬盘中,存放时间较长,会定期清除,用户也可以手动清除Cookie。
用途:
- 由于Http协议是无状态的,所以HTTP服务端并不知道客户端上一次访问服务时的状态,而在一些交互性的服务中这非常重要,比如在线购物等功能,所以需要Cookie来存储客户端在之前访问服务的状态。
缺陷:
- Cookie在使用时先由服务端在响应报文中的
set-cookie
字段中放入cookie值,再由客户端在下次请求时在头部中的cookie
字段写入保存的cookie值,所以cookie的使用显然会增加流量 - HTTP是一个明文协议,此时Cookie值非常容易被窃取,导致隐私泄露等问题
- Cookie被设计成小型文本文件,所以只能存储一些小型的数据
Session
session(会话)的概念也是为了解决HTTP协议无状态这一点而提出的。
我们想象一个情景,在一个在线购物网站中,我们在可以把选购商品->加入购物车
这一操作抽象成一个会话的概念,有的app或网站会记录下用户浏览的商品,以这个记录为根据给用户推荐用户可能感兴趣的商品,个人认为这种场景就非常适合使用Session技术,当然是否真实的浏览记录是这样实现的笔者不知道😂
这里最直接的思路是使用账号ID与浏览信息对应,但浏览信息显然有着动态变化、每次进行商品浏览产生的记录不同的特点,这要求记录信息需要定时更新,淘汰已经老旧的信息并将用户每次浏览产生的数据分开。
Session技术则是用一个Session标识符来唯一确认每一个Session,通过这个Session ID就可以获得服务端的Session数据,Session是保存在服务端的。
如果以每次连续时间段的浏览划分为一次选购的话,那么将每次选购的浏览记录数据对应到一次session,就满足了动态变化、定期更新的要求。
那么Session ID如何给到客户端呢?常见方法有两种。
- Cookie:一种当然是Cookie,使用Cookie可以很方便地将Session ID给到客户端
- 重写URL:在Cookie被禁用时,可以在重写URL使请求URL在参数部分携带上Session ID
显然,Session也存在着安全问题,如果Session被对应用户之外的人获取到的话很可能会暴露用户的信息。
会话劫持:这是一种通过获取用户Session ID后,使用该Session ID登录目标账号的攻击方法,此时攻击者实际上是使用了目标账户的有效Session。会话劫持的第一步是取得一个合法的会话标识来伪装成合法用户,因此需要保证会话标识不被泄漏。也可以算做是中间人攻击的一种。
防御手段:
- 设置HTTPOnly防止XSS(跨站脚本攻击)
- 验证HTTP请求头部信息
- 使用HTTPS
- 验证User-Agent
- 加入Token
URI
在使用HTTP进行通信时,我们通常需要一个地址,这个地址被称作URL(统一资源定位符)。其实URL是URI的一个子类,在实际中通常直接使用URL来代替URI的描述。
URI(Uniform Resource Identifier)统一资源标识符,可以唯一地标示互联网中的一个资源,所以它可以作为网络资源的地址。
为什么需要 url 编码?
- 在URL中的参数以key-value的格式存储,在host后以一个?开头,后面衔接参数,不同的参数之间以&隔开。然而,在参数中可能存在
value
中本身就存在=
的情况,这样接收方在解析URL时会错误地将value
中的=
认为是key-value
中的=
,该value
会被错误地解析成一对参数。所以要对URL编码,将如=
等特殊字符替换成%+对应的ascii码
,防止解析歧义。
各版本的HTTP
HTTP自诞生以来经历了HTTP0.9、HTTP1.0、HTTP1.1、HTTP2、HTTP3等版本。
- Http 0.9 : 是第一个版本的Http协议,具有
无状态
的特点。只具有Get
一种请求方式,并且不支持请求头。同时只支持一种内容即纯文本。无状态指的是每一个事务独立处理,处理结束后就释放这条连接。在请求时要先建立一条TCP连接,请求结束后就释放这条连接。 - Http 1.0: 相对与0.9版本,增加了请求头和响应头的支持,响应状态由一个响应状态行表示(200 OK)。由于增加了头域的支持,所以内容类型也不局限于纯文本。开始支持POST方法向Web服务器提交数据,支持了
Get
、Post
、Head
方法。 - Http 1.1: 是Http协议的第三个版本,是目前使用得最广泛的版本。包括了几个显著的新增特性。
- 持久连接:增加了
Keep-alive
字段。在HTTP1.0中使用长连接需要添加请求头 Connection: Keep-Alive,而在HTTP 1.1 所有的连接默认都是长连接,除非特殊声明不支持( HTTP请求报文首部加上Connection: close )。允许Http在进行一次事务后保留该条连接,以便在下次请求复用这个TCP连接。(避免了每次都要进行3次握手和慢启动) - chunked编码传输:该编码将实体分块传送并逐块标明长度,直到长度为0块表示传输结束,这在实体长度未知时特别有用(比如由数据库动态产生的数据)
- 字节范围请求:HTTP1.1支持传送内容的一部分。比方说,当客户端已经有内容的一部分,为了节省带宽,可以只向服务器请求一部分。该功能通过在请求消息中引入了range头域来实现,它允许只请求资源的某个部分。在响应消息中Content-Range头域声明了返回的这部分对象的偏移值和长度。如果服务器相应地返回了对象所请求范围的内容,则响应码206(Partial Content)
- Pipelining(请求流水线):在1.0版本中,HTTP协议发送一个请求后需要等待收到该请求的响应报文后才可以发送下一个请求。使用
Pipelining
技术允许HTTP协议按顺序直接发送请求,而不需要等待前一个请求的响应报文的到来。提高了网络的带宽和速度 - Host域:请求头和响应头都增加了一个
Host
字段,由于现代每台服务器不一定只对应一个IP地址,所以在Host
字段中可以传递具体的请求IP地址。 - 新增了OPTIONS,PUT, DELETE, TRACE, CONNECT等方法
- 新增缓存支持
- 持久连接:增加了
- Http 2: 主要是提升安全性和性能
- 头部压缩
- 引入了HPACK头部压缩算法,减少了报文中头部的开销
- 二进制分帧传输
- 有别于Http1.1的明文传输,Http/2使用了二进制来打包、传输服务器与客户端之间的信息。Http/2将每个TCP连接分成了若干个流,每个流可以传输若干个信息,每个信息直接由若干最小的数据帧组成,这是Http1.1与Http2最大的区别所在。
- 多路复用
- 一次客户端请求服务求可以多次响应
- Server Push:在Http1.1以及之前的版本中,都是一个请求对应一个响应,而在Http2中一个请求可以对应多个响应,Server可以主动向客户端Push信息
- 可以在一个TCP连接中并发请求
- 头部压缩
- Http 3: 基于QUIC,目前仍是草案阶段。
HTTP/1.1相较于HTTP/1.0协议的区别主要体现在:
- 缓存处理
- 带宽优化及网络连接的使用
- 错误通知的管理
- 消息在网络中的发送
- 互联网地址的维护
- 安全性及完整性
Http/2 与 Http/1.1 最大的区别是Http/2使用二进制来打包客户端与服务器之间的数据,而Http1.1以及之前的版本都是明文传输
报文
请求报文
- 请求头
GET /IMAGES/LOGO.GIF HTTP/1.1
由请求方法 URL 协议版本
构成。 - 请求体
HOST:www.baidu.com
,在请求体中,除HOST字段外的字段都是可选的。
- 请求头
响应报文
- 响应头
协议 状态码
- 响应体
- 响应头
常见字段
- Content-Type:指示资源的媒体类型,表示返回或发送的数据的内容类型
- Content-Length:指示报文实体的长度
- Content-Encoding:用于压缩媒体类型,使用此字段告诉客户知道采用何种解码方式以获得Content-Type引用的媒体类型
- header,cookie,返回码,UA,HOST,域 等基本概念;
- header:请求头(标题),里面包含了一系列字段标示了报文和连接的相关信息
- cookie:存储在用户本地终端上的数据,是一小段以Key-Value格式存储的文本数据。如果服务器需要客户端存储某些信息,就将信息存在response的
set-cookie
字段中发送给客户端,客户端就会将set-cookie
的内容存储在本地客户端的缓存数据中,下次给该服务器发送请求时将会在请求头中携带上cookie
- 返回码:表示此次请求的结果状态。
- 10x:继续 100-Continue 101-Switching protocols
- 20x:成功 200-ok 202-accepted
- 30x:重定向 300-multiple choices 301-moved permanently 302-Found 303-See other 307-Temporary Redirect 308-Permanently Redirect
- 40x:客户端出错 400-Bad request 401-Unauthorized
- 50x:服务器出错 502-Bad Gateway 504-Gateway timeout
- UA: User-Agent 即用户代理,简称“UA”,它是一个特殊字符串头。网站服务器通过识别 “UA”来确定用户所使用的操作系统版本、CPU 类型、浏览器版本等信息。而网站服务器则通过判断 UA 来给客户端发送不同的页面。
- Hosts:一个没有拓展名的文件,在里面记录了
域名->IP地址
的信息,在浏览器发起一个对域名的请求时首先会检查该文件中有没有该域名的记录。 - ETag:唯一确定一个资源的值(可能是由hash算法得出来的hash值),随着资源的改变会改变
- range:Http1.1之后新增了对range字段的支持,客户端可以在range字段中填写所需资源的指定部分。当使用该字段时服务器返回206的成功状态码
常见状态码
- 100 Continue
- 101 Switching protocols
- 102 Processing
- 200 OK
- 201 Created
- 206 Partial Content
- 300 Multiple Choices
- 301 Moved Permanently
- 302 Found 临时重定向
- 303 See Other
- 304 Not Modified
- 400 Bad Request
- 401 Unauthorized
- 403 Forbidden
- 404 Not Found
- 502 Bad Gateway
- 504 Gateway Timeout
实现断点续传的原理
http1.1
推出了range
字段,以提供客户端对服务器请求特定部分资源的能力,利用该字段就可以实现断点续传,当然前提是服务器支持range
和分块传输
。在连接建立后,客户端维护当前收到的资源大小,当连接或传输因某些原因中断时,客户端会再次向服务器发起资源请求,在这时的请求报文中range
字段写入已收到的资源大小。服务器收到该请求报文后解析range
字段就知道需要发给客户端的是哪部分内容,并在响应报文的content-range
字段写明是报文实体是哪部分的资源。这样就实现了断点续传。需要注意的是,如果服务器支持断点续传,那么在续传的响应报文状态码会是206
而不是200
,如果服务器不支持断点续传也就是range
字段,那么将会重新返回整个资源并返回200
状态码。- 在这个过程中,如果在客户端发起断点续传时服务器的该资源已经发生改动,那么响应要发生响应变化。一般通过
last-modified
字段或者eTag
字段来确定一个资源有没有被修改。
条件请求
在Http1.1中新增了对缓存管理的支持,在头部中可以使用
If-Match
,If-Modified
,If-Since
等字段来发送条件请求,已实现对缓存的验证等操作。在这类请求中,请求的结果随着时间和服务器上资源的变化而变化。
- 原理
在 HTTP 协议中,条件请求指的是请求的执行结果会因特定首部的值不同而不同。这些首部规定了请求的前置条件,请求结果则视条件匹配与否而有所不同。
在该类请求中,客户端(通常是代理服务器或缓存服务器)发起的请求中,在头部的If-Match
,If-Modified
,If-Since
等字段中置入对应的值,服务器收到后检验这些头部字段,比对资源最新的值,从而给出响应。
所有的条件请求首部都是试图去检测服务器上存储的资源是否与某一特定版本相匹配。为了达到这个目的,条件请求需要指明资源的版本。由于逐个字节去比较完整资源是不切实际的,况且这也并非总是想要的结果,所以在请求中会传递一个描述资源版本的值。这些值称为“验证器”,并且分为两大类:
- 文件的最后修改时间,即 last-modified (最后修改)时间。
- 一个意义模糊的字符串,指代一个独一无二的版本,称为“实体标签”,或者 etag 。
在这里需要引入一个强弱验证的概念。
- 强验证类型(Strong validation)应用于需要逐个字节相对应的情况,例如需要进行断点续传的时候。从理论上来说,
Last-modified
和etag
字段都可以为强弱验证提供支持,但在强验证中Last-modified
通常会有些乏力,使用etag
会是更好的选择。Etag
字段的实现方式可以是服务器端使用MD5算法来求得资源的散列值,这样资源即使只有一个字节的改动,它的散列值依然会与旧版本的不同,从而实现强验证。当然,在不同的服务器上也可以根据实际需求来使用不同的算法生成Etag值。 - 弱验证类型(Weak validation)应用于用户代理只需要确认资源内容相同即可。即便是有细微差别也可以接受,比如显示的广告不同,或者是页脚的时间不同。
常见的条件请求首部通常有以下几种
- If-Match
- 如果远端资源的实体标签与在 ETag 这个首部中列出的值相同的话,表示条件匹配成功。默认地,除非实体标签带有 ‘W/‘ 前缀,否者它将会执行强验证。
- If-None-Match
- 如果远端资源的实体标签与在 ETag 这个首部中列出的值都不相同的话,表示条件匹配成功。默认地,除非实体标签带有 ‘W/‘ 前缀,否者它将会执行强验证。
- If-Modified-Since
- 如果远端资源的 Last-Modified 首部标识的日期比在该首部中列出的值要更晚,表示条件匹配成功。
- If-Unmodified-Since
- 如果远端资源的 HTTPHeader(“Last-Modified”)}} 首部标识的日期比在该首部中列出的值要更早或相同,表示条件匹配成功。
- If-Range
- 与 If-Match 或 If-Unmodified-Since 相似,但是只能含有一个实体标签或者日期值。如果匹配失败,则条件请求宣告失败,此时将不会返回 206 Partial Content 响应码,而是返回 200 OK 响应码,以及完整的资源。
应用场景
1.在客户端没有缓存的情况下(可能是第一次请求该资源或本地缓存被清空),客户端向服务器发起一个Get请求,服务器会返回资源的实体并在头部中返回资源的Last-modified
和Etag
值,这些会跟资源实体一起在客户端缓存下来。
2.在之后的请求中,客户端再次请求该资源,如果缓存此时已失效,它可能会向服务端发起一个条件Get请求,在请求头部的If-Modified-Since
字段中写入之前缓存的Last-modified
字段值,在If-Match
中写入之前的Etag
字段值。
3.在服务器收到该请求时,检验这两个字段的值。如果资源未发生改动,此时服务器会发送304 Not Modified的响应,并不重新发送资源实体,这就大大节省了网络带宽。客户端收到该响应后,会更新该资源在本地缓存中的有效时间。
4.如果资源确实发送了变动,即If-modified-Since
和If-Match
值不匹配,则服务器会返回200 OK
状态码并发送最新版本的资源实体,客户端收到后更新该资源的本地缓存。
在增量下载中的应用
在Http1.1中,由于支持了Range
字段,所以可以用该字段来实现断点续传/增量下载,然而,如果在该过程中服务器上的资源发生了改动,那么最后获得的结果会是已损坏的。这时候需要使用条件请求来避免这个结果。
- 第一种解决办法是使用
If-Modified-Since
和If-Match
字段。使用这两个字段发送Range Request时,如果验证器不匹配服务器会先返回412 Precondition Failed
的响应,客户端再发起一个Get请求来获取最新版本的资源。可以看到,该方案会使得多了一次请求-响应的过程,对于性能敏感的应用可能会有一些影响。 - 第二种解决方案是使用
If-Range
字段,该字段中的值只能使用Etag
值。在使用If-Range
的请求发送到服务器后,如果资源被修改,则会直接返回200 OK
的响应并携带最新版本资源的实体和Last-modified
、Etag
值。
需要注意的是,在增量下载中,如果Range Request成功响应,返回的状态码是206 Partial Content
。
在更新丢失中的应用
在一些应用或者网站中,如维基百科、百度百科这类可以被多个客户端同时编辑的网站,如果只有一个客户端在一个时段内编辑提交该资源的更新,那么不会出现问题。但如果有多个用户在同一个时间向服务器提交该资源的更新,那么就会出现Race Condition的问题,如果不处理后到来的更新将会覆盖前面的更新。
使用If-Modified-Since
和IF-Match
可以解决该问题,这两个字段也是这类应用通常使用的乐观锁
的实现方式。
在该实现中,客户端在使用Put
请求向服务器更新资源时,要携带上If-Modified-Since
和IF-Match
这两个字段,第一个到来的更新成功匹配这两个字段的值并更新资源,之后修改该资源的Last-Modified
和Etag
值,后到来的更新由于If-Modified-Since
和IF-Match
的值无法通过验证,于是被拒绝更新。
POST/GET 区别
- Get为幂等操作,Post非幂等
- Get的参数拼接在URL中,POST的参数一般放在Body中
- Get请求只会发送一次,Post请求一般发送两次,第一次发送请求头,在收到允许后继续发送请求体
- Get由于只获取资源不修改资源,所以一般可缓存Get请求。Post一般为提交表单或修改资源,所以不缓存
- Get的URL是有长度限制的,Post可以发送的数据更多
- Post可以对请求体加密
DNS 域名解析系统
在日常的网络中,通常不使用IP地址来直接访问网络中的资源,而是使用人比较容易记住的域名
。所以在访问网络时,需要进行从域名->IP地址
的转换/翻译,提供这个服务的就是DNS域名解析系统。
DNS(Domain Name System)是一项服务,也是一个协议。DNS协议是属于应用层的协议,为网络提供域名到IP地址的转换。它直接使用UDP协议作为运输层的协议,而不经过HTTP等协议。 所以在最基础的DNS系统中,协议栈是DNS协议->UDP协议
协议栈
然而,虽然绝大部分情况下DNS协议都是使用UDP协议进行数据传输,但有时候也需要使用到TCP协议。
日常进行的域名请求的报文大小基本都比较小,所以UDP协议就可以支持DNS系统的查询。
DNS支持的常见查询类型包括
- A记录(IPv4)
- AAAA记录(IPv6)
- CNAME记录
然而,DNS也支持像AXFR类型
的特殊查询,该类型的查询通常用于DNS区域传输,它的作用就是在多个命名服务器之间快速迁移记录,即将DNS服务器的数据库信息迁移到别的数据库,可想而知其中的数据量是比较大的。
在RFC1034中提到,DNS查询可以通过UDP或者TCP协议来进行传输,但由于DNS系统对于数据的准确性有着高度要求,我们在进行AXFR类型
的特殊查询时必须采用TCP或者其他可靠的协议。
在RFC1035中提到,UDP协议携带的数据不应该超过512字节,当超过的时候即会被分片,并且由于数据在传输时可能会丢失,所以在AXFR类型
的特殊查询中需要重传的特性。
同时,在未来的DNS记录中,由于IPv6的地址增大且其他字段的增加,UDP协议的512字节可能会不足被DNS系统使用,这时我们就需要使用TCP协议来进行DNS请求。
总得来说,DNS系统使用TCP/UDP的主要理由有如下几点
- UDP 协议
- DNS 查询的数据包较小、机制简单
- UDP 协议的额外开销小、有着更好的性能表现
- TCP 协议
- DNS 查询由于
DNSSEC
和IPv6
的引入迅速膨胀,导致 DNS 响应经常超过 MTU 造成数据的分片和丢失,我们需要依靠更加可靠的 TCP 协议完成数据的传输 - 随着 DNS 查询中包含的数据不断增加,TCP 协议头以及三次握手带来的额外开销比例逐渐降低,不再是占据总传输数据大小的主要部分
- DNS 查询由于
也就是说,当DNS报文大小超过了512字节,甚至超过了MTU时,我们就应该选择TCP协议或者其他的可靠协议来进行DNS请求的传输。
当然,目前的DNS请求绝大部分都还是使用了UDP协议,通过Wireshark抓包也可以验证这一点。
DNS查询过程
在网络中一般存在多种DNS服务器。
- 本地域名服务器:一般是DNS客户端设置的DNS服务器,由本地的ISP服务商提供
- 根域名服务器:最高级的DNS服务器。DNS服务器群是一个树状结构,其中根域名服务器是该树状结构的根结点。
- 顶级域名服务器
- 权威域名服务器
DNS查询有两种方式:递归和迭代。DNS客户端设置使用的DNS服务器一般都是递归服务器,它负责全权处理客户端的DNS查询请求,直到返回最终结果。而DNS服务器之间一般采用迭代查询方式。
DNS的查询方式也有迭代和递归两种。
一般来说,DNS客户端在向本地域名服务器查询DNS信息时是通过递归查询,而DNS服务器群之间的查询则是迭代查询。
以查询zh.wikipedia.org
为例,正常情况下DNS服务的执行流程如下:
- 在浏览器输入域名并访问
- 查询本地浏览器的DNS缓存中是否存在该域名的DNS信息,如果有则查询结束。
- 查询操作系统的DNS缓存中是否有该域名的记录
- 查询本地HOST文件中是否存在该域名的静态映射,如果有则查询结束。
- 通过UDP发送查询报文到本地域名服务器,本地域名服务器查看自身的缓存中是否有该域名的记录
- 如果本地域名服务器中的记录老化或不存在,则:
- DNS服务器向根域名服务器发送查询报文”query zh.wikipedia.org”,根域名服务器返回顶级域 .org 的顶级域名服务器地址。
- DNS服务器向 .org 域的顶级域名服务器发送查询报文”query zh.wikipedia.org”,得到二级域 .wikipedia.org 的权威域名服务器地址。
- DNS服务器向 .wikipedia.org 域的权威域名服务器发送查询报文”query zh.wikipedia.org”,此时必然会得到主机 zh 的A记录,将该记录发送回给本地域名服务器,本地域名服务器存入自身缓存并返回给客户端。
DNS报文格式
典型的DNS报文格式是域名 地址类型 查询类 TTL Length 地址
域名 | 地址类型 | 查询类(互联网总是IN) | TTL | DATA Length | Address(AAAA表示为IPv6地址)
DNS挟持
在实际的网络中,会遇到恶意的DNS挟持。DNS挟持的意思是更改了DNS客户端中设置的本地域名服务器地址,从而达到返回错误或者有害的DNS报文信息给DNS客户端的目的。
- DNS挟持:由于DNS查询过程是先向运营商提供的local DNS服务器发起递归的查询请求,所以该过程容易被恶意程序篡改,修改了DNS客户端中设置的local DNS服务器,从而挟持了DNS客户端。
- 中间人攻击:在通信链路上的一环嗅探插入,具体的就是在通信链路中拦截并篡改DNS报文,并将篡改后的DNS报文发送给DNS客户端,DNS客户端对此过程是无感知的,它以为自己是在直接与DNS服务器交互,所以叫做中间人攻击。
目前解决DNS挟持的主流方案是HttpDNS
- HttpDNS:HttpDNS通过使用Http协议来直接向自己搭建的本地DNS服务器发送DNS查询报文,从而绕开了传统的DNS查询流程需要走的Local DNS服务器。在常见的DNS挟持中大都是挟持了本地DNS服务器,使用HttpDNS的方案即可绕过该本地服务器。
然而,采用HttpDNS
后也不能保证DNS查询请求的绝对安全,因为Http是一个不安全的协议,它是明文传输的,此时如果受到了中间人攻击,在通信链路上被挟持,那么DNS报文仍有可能被篡改并向DNS客户端发送一个包含错误域名信息的DNS响应报文,而这一切DNS客户端都是不知情的。
为了保证DNS服务的安全性,很容易想到可以使用在Https中使用的TLS/SSL技术。
目前业界常见的两种加密DNS服务的方式是DNS over HTTPS
与DNS over TLS
- DNS over Https:该方案在HttpDNS的基础上使用了TLS/SSL,用于DNS解析的递归服务器,也就是local DNS服务器。DNS查询过程中的报文都得益于TLS/SSL获得了加密,即使对网络链路进行了嗅探的设备也因为无法解密报文中的内容从而无法篡改报文中的内容。并且Https提供身份认证等服务,也进一步提高了安全性。由于本质上还是走了Http协议发送DNS请求,所以
DNS over Https
也可以绕过本地运营商提供的local DNS服务,从而避免local DNS服务器导致的域名挟持问题。 - DNS over TLS:同样是使用了TLS/SSL协议来加密DNS服务,区别是采用了传统的方法没有使用Http协议,而是直接使用TLS/SSL协议来包装DNS协议,从而达到防止中间人攻击、保护隐私的效果。
Https (Http over TLS)
由于Http是一个不安全的加密,它所有的通信都是明文传输的,于是推出了Https协议(Http over TLS)来保证通信的安全。
Https使用Http协议进行通信,但使用TLS/SSL来加密数据包。
- Https推出的主要作用是:向通信双方提供身份认证服务,并保证交换资料的完整性和安全性。
需要注意的是,Https的安全性基于提前安装在操作系统上的CA(证书颁发机构),证书提供了身份认证的功能,也是安全服务的保障。只有一个足够权威的证书才能保障连接能被信任。
- Http协议的默认端口是80,Https协议的默认端口是443
TLS/SSL 握手
流程大概是:1.客户端发起hello包生成一个随机数,并发送可供待选的加密套件 2.服务器收到后发起hello包并生成一个随机数,并返回选择的加密套件和服务器的证书 3.客户端收到后验证服务器的证书,在证书验证通过后再生成一个pre-master随机数,并与前面两个随机数一起生成连接的通信密钥。将pre-master用服务器的公钥加密,将之前通信中的各参数hash值用通信密钥加密,发送给服务器。 4.服务器收到后使用私钥解密得到pre-master,使用pre-master和两个随机数生成通信密钥,使用通信密钥解密hash值,同时计算自己在之前通信中的hash值,与解密出来的hash值对比。若一致,则将自己计算出来的hash值用通信密钥加密后发送给客户端。 5. 客户端收到后进行相同流程,若一致则握手成功,之后的通信都使用该通信密钥加密。
https协商过程
- 1.client_hello:客户端发起握手请求,以明文传输信息,将tls版本、加密套件可选列表、压缩算法可选列表、一个随机数random_c、拓展字段发送给对方。
- 2.server_hello:服务端收到握手请求,将选定的tls版本、加密套件、压缩算法、一个随机数random_s、拓展字段发送给客户端。并且服务端会将自己的证书发送给客户端。此时,如果成功传输,客户端已经拥有了双方随机产生的两个随机数random_c和random_s,服务端同理
- 3.证书校验:收到服务端的证书后,对其做证书校验,如若校验不通过则握手失败。合法性验证有:
- 证书链的可信性 trusted certificate path。
- 证书是否吊销 revocation,有两类方式离线 CRL 与在线 OCSP,不同的客户端行为会不同。
- 有效期 expiry date,证书是否在有效时间范围。
- 域名 domain,核查证书域名是否与当前的访问域名匹配,匹配规则后续分析。
- 4.合法性通过后,客户端再次生成一个随机数
pre-master
,并用服务端证书中的公钥加密,发送给服务器。- 此时客户端已经有两个明文随机数以及一个
pre-master
,将三个随机数使用算法计算得到协商密钥,并通知服务器在之后使用该协商密钥进行加密通信。 - 最后,结合之前所有通信参数的 hash 值与其它相关信息生成一段数据,采用协商密钥与算法进行加密,然后发送给服务器用于数据与握手验证。
- 此时客户端已经有两个明文随机数以及一个
- 5.服务器收到
pre-master
后,使用自己的私钥解密得到pre-master
的真实数据,加上之前的两个明文随机数生成协商密钥。- 计算之前所有参数的
hash值
,并用自己生成的协商密钥解密客户端发送过来的用客户端计算出来的协商密钥加密的数据,对比解密出来的hash值与自己计算出来的hash值,若相同则服务器确认密钥等数据都正确。 - 此时服务器也会发送一段由之前的参数计算出来的hash值并用自己的协商密钥加密的数据给客户端,客户端收到后进行相同的过程,如果验证正确那么服务器客户端都确认密钥正确,握手成功。
- 计算之前所有参数的
- TLS/SSL握手过程采用双向验证,在证书、pre-master、所有相关参数的hash值的验证中都要求客户端和服务器双方都进行一次验证,如若都正确才算是通过验证
证书验证
数字证书是用来验证公钥持有者身份合法性的一个手段,它本质上是一个文档。在Https中使用证书来进行身份验证。
签发证书的过程
- 申请者向CA机构提供自己的公钥、域名信息、组织信息等,并提交签发证书的申请
- CA机构在收到申请后,通过线上/线下的方式验证该申请中关于申请者信息的真实性
- 当验证真实性通过后,使用通用的hash算法计算出信息的hash值,并使用CA机构的公钥加密该hash值,该加密后的hash值被当作数字签名附加到证书中。此时数字证书即为已签名的证书
- 将已签名的证书派发给申请者
证书验证的过程
- 客户端向服务器发起Https连接
- 服务器将自己的已签名证书发送给客户端,在证书中包含了服务器的公钥
- 客户端收到后解压该证书,获得服务器证书的元数据部分和数字签名部分
- 在浏览器的客户端中,通常都已内置了可信任的CA机构的证书,在证书中包含了CA机构的公钥,如果找不到服务器证书的签发者则认为该证书不可信
- 计算元数据的hash值,使用CA机构的证书里的公钥解密数字签名,对比计算出来的hash值和解密出来的hash值,若一致则验证通过
- 同时还会查看服务器证书的有效期是否过期、域名是否与当前服务器相同等信息
- 服务器证书中也包含了服务器的公钥,这在之后的握手中才用到
SPDY | HTTP/2
SPDY是一种开放网络协议,是基于TCP协议的应用层协议,是HTTP/2的前身。在HTTP/2推出后已正式被HTTP/2取代。
SPDY旨在缩短网络的加载时间和安全性,使用如多路复用、压缩、优先级等机制来加快加载时间。
而在SPDY基础上创造出来的HTTP/2协议有着以下主要的改进
- HPACK头部压缩算法
- 多路复用(将多个请求复用同一个TCP连接,并可以并发请求。HTTP/2将一个TCP连接分成了多个流,每个流可以传输若干信息,每个信息由最小的二进制帧构成。在HTTP/2中采用了二进制打包,这也是HTTP/2与之前版本的最大区别所在)
- Server Push
- 修复队头阻塞问题(然而,由于HTTP/2是基于TCP协议的,所以如果有一个TCP数据包丢包,仍会遭受队头阻塞问题)
常见的网络安全问题
CSRF(Cross-site request forgery,跨站请求伪造):原理和防范
- 原理:攻击者盗用用户已受信任的身份向另外一个网站发送一个恶意请求。比如在银行页面先登陆,获取了本地的cookie后,攻击者利用这个cookie向转账页面发送一个转账请求。CSRF能够做的事情包括:以你名义发送邮件,发消息,盗取你的账号,甚至于购买商品,虚拟货币转账……造成的问题包括:个人隐私泄露以及财产安全。
- 防范:
- 验证HTTP Referer字段:在 HTTP 头中有一个字段叫 Referer,它记录了该 HTTP 请求的来源地址。但这个方法并不够安全,
Referer
值也存在被篡改的可能 - 在请求地址中添加Token字段:在Http请求的字段中添加一个Token字段,在服务器端添加一个拦截器来验证这个字段,若请求没有该Token字段或者Token字段的值不正确,则认为该请求是CSRF,并拒绝响应。
- 验证HTTP Referer字段:在 HTTP 头中有一个字段叫 Referer,它记录了该 HTTP 请求的来源地址。但这个方法并不够安全,
XSS(Cross Site Scripting,跨站脚本攻击):原理和防范
- 原理:跨站脚本攻击,一般是在web网站中嵌入一段恶意的JavaScript代码,已盗取用户的cookie。分为存储型XSS、反射型XSS、DOM型XSS
中间人攻击概念与防止
- 概念:中间人是指攻击者与进行通信的两端都建立连接,并交换他们的数据。通信两端都以为他们在直接与对方进行通信,但实际上整个通信都被中间人窃听了。
- 例子:A与B通过密钥通信,通信前A将公钥发送给B,但被中间的C窃听并窃取了A的公钥,C将自己的公钥发送给B,这时B以为这是A的公钥,并将自己的公钥发送给A,C再次窃取了公钥,并将自己的公钥发送给A。这时C同时拥有了A和B的公钥,在之后的通信中,A和B都使用了C的公钥进行加密,所以C可以用私钥进行解密阅读报文内容,同时也可以修改报文内容。C在拦截了通信的报文后,使用A或者B的公钥再次加密后发送给A或者B,此时A或者B都会以为是对方发送来的数据并且使用自己的私钥进行解密报文,而对中间人C无感知。这样的攻击是建立在C能窃听A和B的通信链路且A与B之间没有身份验证的前提下的。
- 防止:使用Https、利用证书等工具验证身份。
- SSL剥离:SSL剥离攻击是中间人攻击的一种,它的目的是阻止浏览器与服务器建立https连接。由于用户通常是通过点击链接或者收到3xx的重定向响应来进入新的页面,那么当在使用http页面时SSL剥离攻击将页面中所有的https链接替换成http链接,达到阻止https的目的。 通常使用HSTS可以有效防范SSL剥离攻击。
- HSTS:Http严格传输安全策略,要求必须使用https发送请求,运作机制是在第一次使用https请求时服务端返回的报文中包含了一个
Strict-Transport-Security
的字段,字段中设置了持续使用该策略的时间,在该时间内通过http请求发送给该服务端的请求都将会被改写成https。并且在该时间内,不允许用户忽略TSL证书未信任的警告。不足之处是在第一次请求时由于还没有收到HSTS,仍有可能是使用http来发送请求,解决方案有两种:1.浏览器预置HSTS列表 2.将HSTS信息加入域名系统
重放攻击
是一种恶意或欺诈的重复或延迟有效数据的网络攻击形式,是“中间人攻击”的一个较低级别版本。
工作原理:
- A向B提供自己的密码或哈希(假设是登录场景)
- C嗅探到了A的密码或哈希,保存了下来
- 在A与B结束会话后,C使用之前存储的A的密码或哈希,伪装成A连接到B
预防对策:
- 通过使用会话ID和组件编号标记每个加密的组件,可以防止重放攻击。之所以可行,是因为为程序的每次运行创建了唯一的随机会话ID,因此先前的结果更加难以复制。 由于每个会话的ID不同,攻击者无法执行重放。
- 需要注意的是,会话ID通常需要使用随机函数生成,否则容易被预测出来。
- 这也是在TCP握手中为什么SYN报文的Seq需要是随机生成的原因,否则容易被预测出未来将使用的Seq并通过伪造报文发起攻击。
Dos 拒绝访问攻击
亦称泛洪攻击,其目的是将目标电脑的网络或系统资源耗尽,使其服务暂时中止令用户无法访问。
当使用两个或两个以上的被攻陷的电脑或IP进行DoS攻击时,被称为分布式拒接服务攻击,即DDoS。
DDoS带宽消耗攻击可以分为两个不同的层次;洪泛攻击或放大攻击。
洪泛攻击的特点是利用僵尸程序发送大量流量至受损的受害者系统,目的在于堵塞其宽带。
放大攻击与其类似,是通过恶意放大流量限制受害者系统的宽带;其特点是利用僵尸程序通过伪造的源IP(即攻击目标IP)向某些存在漏洞的服务器发送请求,服务器在处理请求后向伪造的源IP发送应答,由于这些服务的特殊性导致应答包比请求包更长,因此使用少量的宽带就能使服务器发送大量的应答到目标主机上。
攻击方式:
- 带宽消耗性
- UDP洪水攻击:由于UDP不需要握手,大量的选取随机端口号的UDP数据包被发送到被攻击的主机上,可能会使带宽饱和以至于正常合法的服务无法访问被攻击的主机
- ICMP洪水攻击:使用ICMP向未设置好的路由器大量发送广播信息占用系统资源
- 资源消耗性
- SYN泛洪攻击:通过伪造TCP请求报文中的SYN报文,将大量包含了虚假源IP地址的SYN报文发送给目标主机,目标主机收到这些报文后为他们分配了资源并插入到SYN队列中,同时发送ACK报文给源IP,由于源IP大多是伪造的所以他们不会进一步进行第三次握手,此时被攻击的主机维护了大量无意义的半连接TCP连接,占用了大量的资源,使得正常的TCP连接请求无法跟被攻击的主机建立连接。
- CC攻击:该攻击使用了代理服务器给被攻击的主机大量的发送貌似合法的请求(通常为GET请求),由于可以利用大量免费且分布在各地的代理服务器,该攻击也被称为分布式HTTP洪水攻击。
防御方式:
- 防火墙:防火墙可以设置规则来过滤掉不正常的IP地址发出的请求,但并不能完全防护,因为如果需要完全防护的话也会过滤掉合法的请求
- 交换机:大多数交换机有一定的速度限制和访问控制能力。有些交换机提供自动速度限制、流量整形、后期连接、深度包检测和假IP过滤功能,可以检测并过滤拒绝服务攻击。例如SYN洪水攻击可以通过后期连接加以预防。基于内容的攻击可以利用深度包检测阻止。
- 流量清洗:当获取到流量时,通过DDoS防御软件的处理,将正常流量和恶意流量区分开,正常的流量则回注回客户网站,反之则屏蔽。这样一来可站点能够保持正常的运作,仅仅处理真实用户访问网站带来的合法流量。
运输层
运输层中常见的协议有TCP、UDP,在Https中使用的TLS/SSL在某种角度上来说也可以被划分到运输层协议。
TCP Transport Control Protocol
在IP层中,IP协议提供的是尽力而为的交付服务,而在一些场景下并不能容忍这样的不可靠服务,所以需要TCP协议来保障通信的可靠性。
TCP协议在协议栈中处于应用层的下层,网络层的上层。
TCP协议提供了保证交付的服务,具有按序到达、确认到达、超时重传、流量控制、拥塞控制等功能,同时还具有利用检验和来检验发送或者收到的数据是否有错误的能力。
应用层将数据传输给运输层,TCP收到后将数据分割成适当大小的报文段,其中报文段的大小通常取决于通信链路的最大传输单元(MTU)。
报文格式
在IP层使用了IP地址来确定目标主机,所以说网络层提供了主机与主机之间的逻辑通信。然而通常来说网络中的信息是一个程序发送给另一个主机上的特定程序的,而运输层则提供了这样的功能。运输层通过引入端口
来确定每一个程序,提供了进程与进程之间的逻辑通信。
TCP协议的报文格式如下
- 来源端口 | 目的端口 :两个端口号在TCP报文表头中占据了总共4个字节,一个端口号占2个字节,一共16个比特,也就是能表示2^16 =65536个端口号。
- 序列号:Seq,占4个字节。为了保证不丢包,在TCP中为每个数据包设置了一个序列号,接收方收到后要返回一个ACK,ACK包中包含了已收到的数据包的序列号。同时序列号也使得TCP支持按序到达。
- 确认号:ack,发送确认报文时填写目前已收到的最后一个报文的Seq
- 窗口大小:占2个字节,用来表示从确认号开始,本报文的发送方可以接收的字节数,即接收窗口的大小,用于流量控制
- 校验和:占2个字节,利用校验函数得出的16位的值,在发送和接收时都要验证校验和的值是否正确,以检验数据包是否出错。
- 选择字段:可选字段,其中有一个窗口扩大因子,取值0-14,用于将接收窗口的值左移,使窗口值翻倍。因为目前TCP的接收窗口通常都大于65535个字节
为什么要握手?
TCP是一个有连接的协议,在开始通信前首先通信双方要建立连接,之后才能开始通信。
而在三次握手之前,先来思考一个问题,为什么TCP连接需要握手这个过程?注意,问题问的是为什么要握手,而不是为什么要三次握手。
- 什么是连接?
学习过计算机网络知识的人都知道,三次握手后TCP连接就完成了建立,客户端甚至可以在第三次握手包里就放入需要发送的数据。然而,一个关键的问题是:什么是连接?TCP协议位于网络模型的运输层,其提供了进程和进程之间的逻辑通信,通过端口来标示一个进程。显然,TCP协议并没有使用一个网线来直接建立进程与进程之间的物理连接,TCP连接是一个抽象的连接。
在RFC793 - Transmission Control Protocol
中定义了TCP连接是什么。
简单总结,TCP连接是 用于保证可靠性和流控制机制的信息,包括 Socket、序列号以及窗口大小叫做连接。
也就是说,TCP连接的目的仍然是为了实现其协议的可靠性,TCP连接其实就是协议为了保证可靠性而存储的一组数据。
当我们在程序中使用API建立TCP连接时,要使用Socket来建立TCP连接。同时,在握手时通信的两端也要将随机生成的Seq发送给对方,因为Seq是随机生成的,为了不被猜出和避免重放攻击。同时,通信双方也需要初始化窗口大小来进行流量控制。
当一个TCP连接被发起时,服务端在建立连接后就为该条TCP连接保存了包括 Socket、序列号以及窗口大小等信息的数据,而这份数据就对应着一条TCP连接。
- 知道了连接是什么,那为什么要握手?
通过握手,TCP通信的双方才能拥有包括 Socket、序列号以及窗口大小等数据。由于TCP连接是一个全双工的协议,也就是说通信的双方都需要持有建立起的TCP连接,所以TCP连接理论上最少需要两次。但两次的握手实际上存在弊端,那么就引入了TCP三次握手。
- 两次握手的弊端
- 1.无法验证通信双方的收发功能是否都正常
- 2.服务端会收到已失效的TCP建立连接报文的影响
三次握手
虽然存在一对终端同时向对方发起TCP握手请求的可能,但一般来说都是服务器保持listen状态,即被动打开,客户端主动向服务器发起连接请求,即主动打开。
服务器上通常有着两个队列,分别是
- SYN队列:存放完成了二次握手的结果。 队列长度由listen函数的参数backlog指定
- ACCEPT队列:存放完成了三次握手的结果。队列长度由listen函数的参数backlog指定
连接需要经过三次握手,三次握手的大致流程如下
- 客户端通过
connect
函数向服务器发起一个连接请求,报文是一个SYN数据包,包中的SYN字段被置为1,序列号A为客户端随机选择。 - 服务器在收到该请求连接后,将该包放入到SYN队列中,并返回一个SYN为1且ack为A+1的应答包,应答包的序列号为服务器随机选择的序列号B。服务器在此次握手为该连接预先分配了资源
- 客户端收到该响应包后,明白连接已可以建立,返回一个ACK应答包,包中的SYN被置为0,ack为B+1,同时包的序号为A+1. 在这步中,已可以携带需要发送的数据。
通过这三次握手,客户端成功与服务端建立了TCP连接。
- 为什么是三次握手而不是两次或四次握手?
有两方面的原因:
- 如果只有两次握手,那么在第一次握手中服务端知道了客户端的发送功能正常,在第二次握手中客户端知道了服务端的收发功能都正常,然而服务端却不知道客户端的接收功能是否正常,这时如果服务端向客户端发送数据而客户端的接收功能不正常,那么会浪费大量资源。
- 另外,如果客户端一开始发送了第一个申请建立连接的握手包1,但这个握手包在某个网络节点中被长时间延滞了。这时客户端由于超时重传,会发送第二个申请建立连接的握手包2. 假设第二次的握手非常顺利,并且连接成功建立并且发送完了数据,之后连接被关闭。这时之前滞留的握手包1被发送至了服务器,服务器以为客户端正在申请一个新的连接,于是发送第二次握手的应答包给客户端,假如采用二次握手,此时连接已建立。但客户端并没有要求新连接,服务端会长时间保留该连接等待客户端发送数据,从而占用了许多资源。如果采用三次握手,服务器要等待客户端的第三次握手包才正式建立连接,就避免了这个问题。总的来说,这一点是为了防止已失效的连接请求报文到达服务端,从而产生错误。
第二条原因中,如果服务端收到了一个过期的TCP连接报文,他会正常地发送SYN报文给客户端。然而,客户端根据报文的Seq、ack可以验证出这是一个无效的TCP连接请求,于是发送RST=1
的报文给服务端,使其放弃该条连接。
而四次握手显然没有必要。
四次挥手
TCP连接的断开需要经过四次挥手,且连接的双方都可以发起挥手。
假设此时客户端希望关闭连接,调用了close()
函数
- 客户端首先向服务端发送第一个挥手包,其中的FIN被置为1,Seq为一个随机数A,此时客户端进入
FIN_WAIT_1
状态,并进入半开状态。即只接收数据,不发送数据 - 服务端收到挥手包后,首先向客户端回复一个ACK包,表示自己已经收到了关闭连接的请求,但仍有数据需要传输。ACK包中ACK被置为1,ack为A+1,Seq为一个随机数B。服务端在发送完该挥手包后变为
CLOSE_WAIT
状态(半关闭状态)。客户端收到该ACK包后,进入FIN_WAIT_2
状态(半关闭状态),等待服务端关闭连接。此时可以接收服务端的数据。 - 服务端在发送完数据后,发送同意关闭连接的第三次挥手包,包中FIN被置为1,Seq为一个随机数Y,发送完毕后进入
LAST_ACK
状态。由于TCP是一个全双工的协议,所以在断开连接时需要拆除双方的收发通道。 - 客户端收到服务端的FIN包后,向服务端发送最后的ACK包,包中的ACK被置为1,ack为Y+1.服务端收到该ACK包后真正关闭连接。客户端在发送ACK包后需要等待2个MSL的时间(Maximum Segment Lifetime 最大段生命周期),RFC793定义了MSL为2分钟,Linux设置成了30s。设置这个等待期目的是防止最后的ACK包没有成功送达 此时客户端处于
TIME_WAIT
状态。最后,客户端关闭连接,TCP连接断开。
为什么是四次挥手而不是三次挥手?
如果是三次挥手,在第二次挥手中一端需要同时发送FIN和ACK
,此时如果还有没发送完的数据就会出错,为了避免这种问题发送第二次挥手的那端可以等待数据发送完后再发送FIN包
。
所以,如果在收到第一次挥手的FIN包时,该端已经没有要发送的数据,可以直接将四次挥手简化成三次挥手,在第二次挥手时同时发送FIN和ACK
。
TCP快速打开 TCP Fast Open
TCP的三次握手为可靠连接提供了支持,但在一些情况下它也成为影响TCP性能表现的一个原因。TFO是一种目的为了简化握手过程的TCP拓展,用于提高通行双方连接建立的速度。
TFO的实施基于两个过程
- TFO请求
- TFO实施
在TFO请求时,即客户端第一次与该服务端建立连接(或Fast Open Cookie过期)
- 客户端首先发起TCP连接建立申请,即SYN数据包。该数据包包含了
Fast-Open
选项,并且该选项的Cookie为空,这代表客户端向服务端申请一个Fast Open Cookie
- 服务端在收到后,若服务端支持TFO,将生成一个
Fast Open Cookie
,并在接下来的SYN-ACK数据包中返回给客户端 - 客户端在收到后,向服务端发送ACK数据包,并将收到的
Fast Open Cookie
缓存到本地
在TFO实施阶段,客户端将利用缓存的Fast Open Cookie
- 客户端发起SYN数据包,并在SYN数据包中**包含需要发送的数据和
Fast Open Cookie
**。而在普通的TCP连接SYN数据包中,是不允许包含要发送的数据的。 - 服务端收到后,将检验
Fast Open Cookie
的有效性。若cookie有效,则服务端会在SYN-ACK包中通时对Seq和数据进行ack;若cookie无效,服务端将丢弃SYN包中的数据,并只对Seq进行ack。也就是说,若cookie无效,则进入普通的TCP连接建立流程。在这步中,若cookie得到确认,服务端也可以向客户端发送数据了。 - 客户端收到SYN-ACK后,若数据部分得到了服务端的确认,则会继续发送数据并发送对SYN-ACK数据包的ACK。若数据部分没有得到确认,客户端将会发送ACK并重新发送数据。
通过以上过程,可以知道通过使用TFO技术,通信双方在获取了TFO Cookie后的TCP连接,可以相当于免除握手的过程,客户端在SYN包中即可包含要发送的数据。大大降低了握手带来的性能开销。
SYN攻击
SYN 攻击是一种典型的 DoS/DDoS 攻击。通过大量的发起SYN包,使得服务器为这些请求都分配资源,并插入到SYN队列中,造成了正常的连接无法建立TCP连接
- 原理
- 攻击客户端在短时间内伪造大量不存在的IP地址,向服务器不断地发送SYN包,服务器回复确认包,并等待客户的确认。由于源地址是不存在的,服务器需要不断的重发直至超时,这些伪造的SYN包将长时间占用未连接队列,正常的SYN请求被丢弃,导致目标系统运行缓慢,严重者会引起网络堵塞甚至系统瘫痪。
- 预防
- 缩短超时(SYN Timeout)时间
- 增加最大半连接数
- 过滤网关防护
- SYN cookies技术
TCP Keep-alive心跳包
TCP连接中,如果一端断开另一端并不能马上知道,这时如果系统中存在大量这种半连接会占用很多资源,于是在TCP连接中服务端会定时地给客户端发送一个心跳包,如果收到心跳包的ACK则认为连接正常,如果多次重传仍收不到ACK则丢弃该连接。
可靠传输
完整的可靠传输依靠以下几点:
- Seq
- 确认到达机制
- 重传机制
TCP在每个报文段中都填写了一个序号Seq,以此实现可靠传输。Seq是一个4个字节的字段,即它的范围是0~2^32 -1 ,当到达2^32 时又变回0.
通过Seq,TCP可以实现确认送达和去除重复的报文段。
在接收方收到报文时,需要发送一个ACK报文给发送方,其中的ack即为收到报文的Seq。
通过使用序号和确认号,TCP层可以把收到的报文段中的字节按正确的顺序交付给应用层。在发送确认包时,为了防止对性能造成较大影响,可以使用延迟确认技术。即发送的确认包中的seq代表目前seq之前的数据包都已收到。
- 累计确认:发送的ACK中seq代表已正确收到seq及以前的所有报文
- 选择确认:发送的ACK可以指出成功接收的报文范围,比如发送了0~10000的报文,其中999号丢失了,此时可以发送1000~10000的确认,只重传0~999,而不必重发0~10000的报文
可靠性通过发送方检测到丢失的传输数据并重传这些数据。包括超时重传(Retransmission timeout,RTO)与重复累计确认(duplicate cumulative acknowledgements,DupAcks)。
- 重复累计确认重传(快重传)
- 由于采用了累计ack,当100号报文丢失时,接收方即使收到了后面的报文,发送的ack也是99,那么当接收方收到了超过3个的99时,就知道100号报文已丢失,这是接收方重传100号报文
- 超时重传
- 在发送出一个报文时启动一个计时器,在收到它的送达确认后就重置计时器,若超时则重传该报文,并将计时器的时间增大为原来的两倍,直至达到上限。(不能无限增大的原因是无限增大容易收到拒绝服务攻击,接收方故意不发送确认送达报文,使得发送方一直重发无意义的报文。)
流量控制
在实际的通信中,TCP收到正确按序的字节后,就将字节放入到缓存区中等待应用在缓存区中读取字节。然而应用可能并不是立刻开始读取,所以在发送方的速率过快时很容易造成缓存区溢出的问题。当溢出时,报文就会被丢失。
流量控制即是为了消除这种问题提出的发送速率与接受速率匹配的服务。
而为了知道接收方还可以接受多少数据,需要用到TCP报文中的Window
字段。发送方在向接收方发送字节时,收到的ACK包包含了已收到的Seq以及Window值。发送方在没有新的ACK包时至多只可发送之前的Window值大小的字节
当Window为0时,发送方启动一个保持定时器,在期间不向接收方发送数据。当定时器结束时,向接收方发送一个试探包,若返回的ACK中Window值不为0,则可以继续发送数据。
拥塞控制
拥塞控制是发送方根据网络的承载情况控制分组的发送量,以获取高性能又能避免拥塞崩溃(congestion collapse,网络性能下降几个数量级)。这在网络流之间产生近似最大最小公平分配。
发送方与接收方根据确认包或者包丢失的情况,以及定时器,估计网络拥塞情况,从而修改数据流的行为,这称为拥塞控制或网络拥塞避免。
TCP中拥塞控制涉及到四种算法:慢开始、拥塞避免、快重传、快恢复
- 慢开始:一开始TCP将发送窗口的大小设置为一个较小的值,通常是1个MSS状态。当收到报文的确认包时,就将发送窗口增加1个MSS。那么当有两个MSS时,收到确认包时就可以增加2个MSS。这样子在慢开始阶段时即是发送窗口在指数增长,直到遇到拥塞或进入拥塞避免模式。
- 拥塞避免:当拥塞窗口的大小达到门限值时,开始进入拥塞避免模式,此时每轮传输不再是翻倍,而是加1
- 快速恢复(Fast recovery):是Reno算法新引入的一个阶段,在将丢失的分段重传后,启动一个超时定时器,并等待该丢失分段包的分段确认后,再进入拥塞控制阶段。如果仍然超时,则回到慢启动阶段。
- 慢重传慢恢复:在传统的拥塞控制算法中采用的是慢重传和慢恢复,发送方在发送报文后设置一个定时器,若在定时间时间内没有收到报文的确认送达,才将该报文视为已丢失的报文,进行超时重传(慢重传)。此时慢重传将门限值设为发送超时时拥塞窗口的一半,然后将拥塞窗口重置为1,重新进行慢开始和拥塞避免(Tahoe版)
- 快重传慢恢复:如果收到三次重复确认——即第四次收到相同确认号的分段确认,并且分段对应包无负载分段和无改变接收窗口——的话,Tahoe算法则进入快速重传,将慢启动阈值改为当前拥塞窗口的一半,将拥塞窗口降为1个MSS,并重新进入慢启动阶段(Tahoe版)
- 快重传和快恢复:在慢重传中假定此时网络非常拥塞,然而如果在短时间内收到了重复的3个确认送达,那么可以假设当前网络并不拥塞,只是某个报文丢失了。所以此时,首先进行快重传,重传获得到的3个重复确认送达的下一个报文段。(Reno版)
- 在计算机网络自顶向下的书中,该图Reno版TCP的快重传算法是首先将拥塞窗口设为快重传发生时大小的一半,接着再根据收到了几个冗余ACK来增加拥塞窗口,假设收到了3个冗余ACK且发生快重传时窗口大小为12,那么快恢复后的拥塞窗口大小即为9.(12/2 + 3),此时的门限值也被设为9,并开始进入拥塞避免。
- 但在前一页的FSM描述图中,其实已经写明了慢开始、拥塞避免、快恢复三者之间的关系。可以看到在拥塞避免阶段,如果收到了三个冗余ACK则进入快速重传,并且将ssthresh设为拥塞窗口的一半,将拥塞窗口设为ssthresh+3*MSS。接着进入快速恢复阶段,在快速恢复阶段如果仍收到了冗余ACK则每个冗余ACK使拥塞窗口+1. 此时快速恢复阶段设了一个定时器,如果在定时器的时间范围内仍没有收到重传报文的ACK则将ssthresh设为拥塞窗口的一半并将拥塞窗口重置为1,进入慢开始状态;如果收到了重传报文的ACK,则将拥塞窗口设为ssthresh并进入拥塞避免阶段。
- 可以看到该图描述的过程符合快恢复到拥塞避免时最终拥塞窗口是门限值的一半,然而在上文中图3-52的TCP Reno的快恢复曲线似乎不太符合该规则。此处欢迎读者提出自己的见解,为什么图3-52中快恢复曲线是直接线性上升的,并且起始点为9.
- 在网络和其他书里也见到略微有差别的快恢复实现,其中的一种是将拥塞窗口的大小设置为发生丢包时的一半,并将门限值也设置为该大小的一半,接着进入快恢复状态,设置一个定时器,如果在定时器时间内收到了丢失报文的确认送达,则进入拥塞避免阶段,否则进入慢开始状态。
- 在计算机网络自顶向下的书中,该图Reno版TCP的快重传算法是首先将拥塞窗口设为快重传发生时大小的一半,接着再根据收到了几个冗余ACK来增加拥塞窗口,假设收到了3个冗余ACK且发生快重传时窗口大小为12,那么快恢复后的拥塞窗口大小即为9.(12/2 + 3),此时的门限值也被设为9,并开始进入拥塞避免。
性能问题
TCP协议虽然是一个可靠的传输协议,但它在被设计之初其实并没有考虑到当今如此复杂的网络环境。在现在,如电梯、高铁等网络信号不好的环境都会出现网络断断续续的现象,而这些可能就是TCP协议导致的,TCP协议在弱网环境下存在着性能瓶颈
底层的数据传输协议在设计时必须要对带宽的利用率和通信延迟进行权衡和取舍,所以想要解决实际生产中的全部问题是不可能的,TCP 选择了充分利用带宽,为流量而设计,期望在尽可能短的时间内传输更多的数据。
在网络通信中,从发送方发出数据开始到发送方接受到数据的确认时为止,称为一个往返时间(Round-Trip Time,RTT)。
在弱网环境下,丢包率会很高,而在这种环境下TCP的性能表现比较差。
而在弱网环境下,导致TCP协议性能下降的原因主要有三个:
TCP的拥塞控制算法,在遇到丢包时会主动下降吞吐量
TCP的三次握手增加了开销
TCP的累积确认机制导致了多余数据段的重传
拥塞控制
拥塞控制显然是主要的导致TCP在弱网环境下表现不佳的原因。在遇到丢包或超时时,TCP的拥塞控制算法会使TCP进入重传和恢复的阶段。而在TCP连接刚建立时,TCP的拥塞控制算法会进入慢启动和拥塞避免状态。
- 三次握手
三次握手的最优情况下是通过三次数据包的发送就快速建立起连接,在这种情况下,TCP三次握手的建立需要1.5个RTT的时间。
然而,在弱网环境下,TCP三次握手显然不能这么快速地建立。在TCP三次握手阶段同样可能会存在丢包等问题,所以当发生丢包时,握手带来的开销就远不止1.5个RTT的时间了。
- 重传机制
在TCP中常用的重传机制是建立在Seq和ack的基础上的,比较常用的是累积确认机制。在累积确认中,当接收方向发送方发送一个ack,发送方即认为在ack之前的报文已全部正确接收。
然而,这种机制在有些时候表现并不太好。比如当接收方收到了Seq为2-5的数据,而这时,依赖累积确认机制的接收方无法发送有效的ack,因为他没有收到Seq为1的报文。这时发送方只能重发Seq为1-5的数据,导致了性能的下降。
注:在这里,如果在应用了重复冗余ACK重传机制的情况下,接收方会发送多个ack为1的ACK包,接收方如果在短时间内收到了三个这样的ACK包后会触发重复冗余ACK重传机制直接重传Seq为1的数据包,就不用重传1-5了。
在比较新的实现中,提出了选择确认
这种较累积确认更灵活、表现更好的确认机制,来避免上述的问题。
为了解决TCP的性能问题,目前业界提出了多种解决方案,大致分为优化TCP和重建新协议两类解决方案
- 选择性 ACK(Selective ACK, SACK),TCP 快开启(TCP Fast Open, TFO)等技术被提出以优化TCP的性能
- 使用UDP构建的新协议如QUIC协议等被提出
TCP拆包/粘包
当应用层协议使用 TCP/IP 协议传输数据时,TCP/IP 协议簇可能会将应用层发送的数据分成多个包依次发送,而数据的接收方收到的数据可能是分段的或者拼接的,所以它需要对接收的数据进行拆分或者重组。
TCP/IP协议栈中,拆包有两种情况
- IP协议根据MTU(最大传输单元)来分片传输过大的数据包,避免物理设备的限制
- TCP协议会根据MSS(最大报文段大小)来分段传输过大的数据段,以保证传输的可靠性和性能
在第一种情况,即IP协议中,在传输前需要协商一个MTU,以确定能够发送的最大数据包大小。一般MTU由通信链路上的设备最小MTU决定。
该机制依赖于路径最大传输单元发现机制来确定链路的MTU。其中,以太网对数据帧的限制一般是1500字节
而在第二种情况,即TCP协议中,在传输前需要确定一个MSS,在正常情况下,TCP连接的 MSS 是 MTU - 40 字节,即 1460 字节;不过如果通信双方没有指定 MSS 的话,在默认情况下 MSS 的大小是 536 字节。
前面提到TCP协议会根据MSS来分段传输过大的数据段,为了保证传输的可靠性。
考虑如果没有MSS的情况,一个过大的TCP报文段会被IP协议分片传输,而其中的TCP报文头部只有一个,当被分片的IP分组出现了丢包现象时,接收方即没有办法重新组装该分组,并且因为没有头部,所以只能重传整个报文。这就是TCP使用MSS拆包的意义。
然而,TCP协议在设计时是被设计成了一个面向字节流的协议,并不是像UDP一样是面向数据包的协议。当TCP协议在运行时,可能会将应用层写入的多个不相关的数据写入到一个TCP报文中发送出去,而由于应用层协议设计的不周到,没有在数据中插入可以分割每个请求的信息,造成接收方无法处理同一个TCP报文中携带的多个请求,这就是TCP粘包的问题。
解决的办法,就是在应用层交给运输层的数据中加入消息边界。
消息边界通常有两类实现的方式
- 长度
- 终止符
而使用长度的一类解决方式又分为
- 固定长度
- 动态长度
在HTTP协议中,如果没有使用chunked编码传输的话,消息边界就是依靠长度来实现的。
在响应头部中,会有一个Content-Length
头部字段。接收方的应用层协议在从socket中读取数据时,根据Content-Length
字段中标示的长度就可以读出对应的数据,然后重组成应用层数据。
而在使用了chunked编码的HTTP/1.1中,实现消息边界依靠了终止符。
在chunked编码的请求中,HTTP请求被编码成了一个个的块发送出去,而在一个请求的最末尾使用一个负载为0的chunked块来标示消息的边界,从而与其他消息隔开。
而在UDP协议中,由于UDP协议是面向数据包的协议,它自身就存在了消息边界,所以就不存在粘包的问题。与之相对的,应用层的一个数据包大小应尽量的接近UDP协议的最大负载,以充分利用带宽。
但UDP的下层协议是IP协议,也就是说它的最大传输长度仍然受到MTU的限制。理论上UDP可以传输最大2^16 -1-8的数据,但一般来说MTU大小为1500,减去IP协议20字节的头部和UDP协议8字节的头部,也就是说UDP协议最好传输的数据不要超过1472字节,以避免被分片传输。
UDP User Datagram Protocol
相对于TCP,UDP提供了无连接、不可靠的传输服务。它在传输前并不需要先建立连接,同时它也没有确认到达等服务,并不保证数据的按序到达。对于TCP而言,TCP提供了一种数据的流传输,UDP则是提供了一种包传输。TCP可以传输更多的数据,UDP可以传输的数据相对较小。
UDP适用于不需要在应用中执行错误纠正以及不需要保证可靠性的情况,使用UDP可以避免协议栈在错误纠正等方面造成的开销。
常用场景
由于UDP不可靠、无连接的特性,它经常在以下一些更注重实时性的应用中使用
- 在线游戏
- 语音视频通话
可以看到如果在以上的应用使用了TCP的话,由于TCP的按序到达和差错检测,可能会出现抖动等情况。因为如果有一个包丢失,那么就得重传这个包,那在语音通话中可能就会出现语音乱掉的情况。而UDP则非常适合这种场景的应用。
还有一些服务也是使用了UDP作为运输层的协议
- DNS:由于DNS请求要求十分快速,所以采用不用建立连接的UDP协议是一个较好的选择
- DHCP
- RIP
由于UDP缺乏拥塞控制,所以需要基于网络的机制来减少因失控和高速UDP流量负荷而导致的拥塞崩溃效应。换句话说,因为UDP发送端无法检测拥塞,所以像使用包队列和丢弃技术的路由器之类的网络基础设备会被用于降低UDP过大流量。数据拥塞控制协议(DCCP)设计成通过在诸如流媒体类型的高速率UDP流中增加主机拥塞控制,来减小这个潜在的问题。
报头结构
可以看到UDP的报头结构仅有四个字段,每个字段各占两个字节。同时背景为粉色的来源端口和校验和在IPv4中甚至是可选字段,也就是说在IPv4中一个UDP报文最小报头消耗仅2个字节。在IPv6中,只有来源端口是可选字段。来源端口可选的原因是UDP不需要像TCP那样发送确认应答,所以其实并不太需要来源端口
从报头结构来看,可以发现UDP的报头开销比TCP要小得多,所以一些要求低延迟而不需要保证高可靠的应用通常采用UDP。
拥塞控制、流量控制
在UDP中并不提供这两项服务,但开发者可以在应用层来根据需要提供这两项服务。
数据拥塞控制协议(DCCP)就是通过在诸如流媒体类型的高速率UDP流中增加主机拥塞控制,来减小这个潜在的问题。
QUIC
QUIC协议是一个通用的传输层协议,它基于UDP协议创造,旨在保留UDP协议速度的同时拥有几乎与TCP协议相同的可靠性。QUIC协议提高了目前的TCP应用的性能,它通过UDP连接在两个端点之间创建多个多路连接来实现这一目标。
QUIC协议的改进行为与SPDY|HTTP/2协议类似,他们都使用了在端对端之间建立若干条连接来提高性能。然而由于HTTP/2协议是一个应用层协议,建立在TCP协议之上,所以它无法充分利用多路连接,在TCP丢包时仍然会遭遇队头阻塞。
在大多数实现中,TCP会将连接上的任何错误视为阻塞,停止进一步传输,直到错误得到解决或连接被视为失败。如果使用单个连接来发送多个数据流,就像在HTTP/2协议中那样,所有这些数据流都会被阻止,尽管其中只有一个可能有问题。例如,如果在下载用于收藏夹图标的GIF图像时出现一个错误,页面的其余部分将等待问题得到解决
但在QUIC协议中,其与HTTP/2中的多路复用协同工作,数据可以独立地通过端对端之间的多条连接发送,其中一个丢包也不会影响到别的数据。
改进
- 在运输层使用了多路复用,一个连接中的若干个流可以独立工作
- 将拥塞避免算法从两个端点的内核空间移动到了用户空间。拥塞避免算法是影响TCP在弱网情况下性能表现的一个非常大的因素,然而TCP是存储在内核空间的,升级只能跟随操作系统的升级。在拥塞避免算法移动到用户空间后,对算法的调整和升级将会变得便利很多
- 大大减少在连接创建时的开销。 在QUIC中,由于目前大部分的HTTP连接都使用了TLS,所以QUIC支持将TLS握手合并到连接建立时的握手一起进行,这样将可以降低HTTPS连接建立时所需要的RTT开销
- 提供QUIC层级的重传机制。 QUIC基于的UDP协议不提供重传机制,所以QUIC自身提供了重传机制,这对于可靠性来说是非常必要的。 QUIC的重传机制在一个流发生错误时,其他流仍然可以正常工作,QUIC只需要重传发生错误的流的数据即可
- QUIC提高了网络切换时的性能,这在TCP中也是非常影响性能的一大因素。 在TCP中,一旦用户切换了网络,比如Wi-Fi->蜂窝数据,TCP就需要等待所有的连接超时断开,然后再重新建立TCP连接,这在网络切换时的微信中十分常见。QUIC为了解决这个问题,使用一个唯一的连接标识符来标示每一条端对端的QUIC连接,在切换网络后只需要发送携带该连接标识符的数据包就可以快速地恢复连接,而不需要关系源IP地址是否变化。 而在TCP中,由于TCP连接由<源IP、源端口、目的IP、目的端口>四元组来唯一确定,所以当源IP发生变化时需要重建建立连接
- QUIC使用了更加适应当今网络环境的拥塞控制算法,使其在弱网环境下也有比TCP更好的表现
拥塞控制
- Google BBR
QUIC使用的拥塞控制算法为Google BBR
。在TCP的拥塞控制算法如Reno算法中,通常将所有丢包都视为网络拥塞,这使其性能受到了限制。
BBR 算法主要出发点是,数据包丢失可能并不意味着网络拥塞,网络中的一小处波动都有可能导致拥塞。因此,即使面对次优的网络条件,BBR也能提供持续的吞吐量性能。
网络层
IPv4
IP协议是TCP/IP协议簇中的核心协议,是网络通信中非常重要的一个协议。在TCP中,TCP定义了端口提供了进程与进程之间的逻辑通信;在IP中,IP定义了IP地址提供了主机与主机之间的逻辑通信。
IPv4(Internet Protocol version 4)是IP协议的第四个版本,也是目前广泛使用的IP版本。它是一个无连接的协议,提供尽力而为的交付服务,在协议栈中运行在数据链路层之上、运输层之下。
它不保证任何数据包均能送达目的地,也不保证所有数据包均按照正确的顺序无重复地到达。这些方面是由上层的传输协议(如传输控制协议)处理的。
IP地址
IP协议定义了IP地址,用来标示连接到网络中的主机。IPv4使用32位的IP地址,即4个字节。因此理论上IPv4只具有2^32 个IP地址,目前来说是不够当前的网络规模使用的。因此,子网划分等概念也随之被提出,同时IPv6也开始了部署,IPv6具有着更多的IP地址,足够目前的网络规模使用。
IPv4的地址通常被写成点分十进制的格式,即每个四个字节分别用点隔开,并用十进制表示每个字节。如255.255.255.0
首部结构
- 版本:占4bits,IPv4则值为4
- 首部长度:IP报文的首部最小固定长度为20字节,首部长度表示首部有多少个32位长(4字节),也就是说它最小是5(0101),最大为15
- 标识符:占16位(2字节),用来唯一地标示分片传输中的每一个分组,因为分片传输的分组不一定会按序到达,在组装是利用该标识符来按正确的顺序组装分组
- 标志:用来标志是否允许分片
- 分片偏移:当前分组在分片中的偏移量
- 源IP地址:占32位(4字节)
- 目的IP地址
- 选项:可选项
地址分类
根据第一个字节将地址分为ABCDE五种地址。
CIDR 无类别域间路由
无类别域间路由(Classless Inter-Domain Routing、CIDR)是一个用于给用户分配IP地址以及在互联网上有效地路由IP数据包的对IP地址进行归类的方法。
它使用前缀/掩码将IP地址分为了两个部分,网络号+主机号
。例如一个地址192.168.1.1/24
代表该地址的前24位是前缀,用来标示网络,后8位地址才是主机地址。
其中的/24
代表掩码,CIDR用可变长子网掩码 (VLSM,Variable Length Subnet Masking),根据各人需要来分配IP地址,而不是按照一个全网络约定的规则。所以,网络/主机的划分可以在地址内的任意位置进行。这个划分可以是递归进行的,即通过增加掩码位数,来使一部分地址被继续分为更小的部分。整个互联网现在都在使用CIDR/VLSM网络地址。
掩码可以被写成如IP地址一样的点分格式,/24
即代表高位的24位都为1,低位的8位为0,即/24
对应的子网掩码为11111111.11111111.11111111.00000000
,对应的十进制点分格式为255.255.255.000
.
通常来说,子网中的第一个地址(主机标识符中的所有二进制零的地址)都保留用于引用网络本身,而最后一个地址用作广播地址用于网络; 这样可以将可用于主机的地址数量减少2个。结果,主机标识符中只有一个二进制数字的/31网络将无法使用,因为这样的子网在减少之后将不提供可用的主机地址
通过使用该技术可以将网络划分为一个一个的子网,由于动态的调整掩码可以使同一个IP地址表示出不同子网中的不同主机,所以该技术可以缓解IP地址不足的问题。
NAT Network Address Translation
NAT 网络地址转换也是为了解决IPv4地址空间衰竭而提出的一项技术,它被使用在有多台主机但只使用一个公有IP地址访问网络的私有网络中。
在NAT服务中,多台本地主机连接在一个本地的专用网络中,主机被分配了一个专用网络的IP地址。同时路由器也连接在该网络中,且占有一个特殊的专用IP地址。同时路由器还应该拥有一个公有的IP地址,该专用网络使用该公有IP地址进行网络通信。
当子网中的主机要向互联网通信时,向路由器在专用网络中的地址发生报文,路由器记录下每条连接对应的专用网络中的主机,并使用将报文中的源IP地址改为公有的IP地址并向网络发送数据。当网络向主机发送应答时,路由器通过查询之前的记录,向报文的目的地址改为主机在专用网络中的地址并将报文发送给特定主机。
该技术使得多个主机可以只占用一个公有IP地址,但同时也降低了通信效率。
分片
互联网协议(IP)是整个互联网架构的基础,可以支持不同的物理层网络,即IP层独立于链路层传输技术。不同的链路层不仅在传输速度上有差异,还在帧结构和大小上有所不同,不同MTU参数描述了数据帧的大小。为了实现IP数据包能够使用不同的链路层技术,需要将IP数据包变成适合链路层的数据格式,IP报文的分片即是IP数据包为了满足链路层的数据大小而进行的分割。在IPv6不要求路由器执行分片操作,而是将检测路径最大传输单元大小的任务交给了主机。
当设备收到IP报文时,要转发IP报文前需要先通过路由表等机制找出它要在哪个通信链路上传送,然后根据该通信链路的MTU来决定是否需要分片。若报文的大小大于MTU,则需要对IP报文进行分片。每一片的长度都小于等于MTU减去IP首部长度。接下来每一片均被放到独立的IP报文中,并进行如下修改:
- 总长字段被修改为此分片的长度;
- 更多分片(MF)标志被设置,除了最后一片;
- 分片偏移量字段被调整为合适的值;
- 首部检验和被重新计算。
重组
当一个接收者发现IP报文的下列项目之一为真时:
- DF标志为0;
- 分片偏移量字段不为0。
它便知道这个报文已被分片,并随即将数据、标识符字段、分片偏移量和更多分片标志一起储存起来。
当接受者收到了更多分片标志未被设置的分片时,它便知道原始数据载荷的总长。一旦它收齐了所有的分片,它便可以将所有片按照正确的顺序(通过分片偏移量)组装起来,并交给上层协议栈。
IPv6
IPv6 Internet Protocol version 6,是网际协议的最新版本,旨在解决IPv4的地址枯竭问题,同时也有许多其他的改进
与IPv4对比
- IPv6定义了一种新的分组格式,旨在最小化路由器需要处理的分组标头
- IPv6具有128位的地址,即16个字节,而IPv4只有32位的地址,也就是说IPv6的地址范围扩大到了2^128 次方,远远超过目前所需要的地址数量
ICMP Internet Control Message Protocol
互联网控制消息协议(英语:Internet Control Message Protocol,缩写:ICMP)是互联网协议族的核心协议之一。它用于网际协议(IP)中发送控制消息,提供可能发生在通信环境中的各种问题反馈。通过这些信息,使管理者可以对所发生的问题作出诊断,然后采取适当的措施解决。
ICMP依靠IP来完成它的任务,通常他以广播的形式发送,一般不用于在两点之间传输数据,并且除了ping和traceroute这两个应用程序一般不直接使用ICMP。
在IPv4中,ICMP对应的版本是ICMPv4,在IPv6中对应ICMPv6.
报文结构
由于IP协议的报头占20字节(不包括可选部分),所以一般来说从第160个位开始是ICMP协议的内容。
- Type:ICMP报文的类型,标示错误的类型
- Code:结合Type进一步细分该ICMP报文代表的错误类型
- checksum:检验和字段
- Rest of Header:报头的其余部分,四字节字段,内容根据ICMP类型和代码而有所不同
内部网关路由协议
RIP Routing Information Protocol
RIP协议是一种内部网关协议(IGP),常被用在小型的网络中,是一种距离向量协议。
原理:
- RIP协议会每隔30s向网络上相邻的路由器发送信息以交换路由信息,并动态地创建路由表
RIP协议以跳(hop)为单位,最大仅支持到15跳,16跳代表不可达。所以RIP只适合小型的网络使用。
包结构
- 防止网络环路
- 水平分割:从某个接口学到的路由信息,不会再次发给该接口的设备
- 毒性逆转:从某接口学到路由信息后将该接口的跳数设置为16,然后发送回该接口的设备
- 抑制定时器
OSPF Open Shortest Path First 开放式最短路径优先
开放式最短路径优先(英语:Open Shortest Path First,缩写为 OSPF)是一种基于IP协议的路由协议。它是大中型网络上使用较为广泛的IGP协议。OSPF是对链路状态路由协议的一种实现,运作于自治系统内部。OSPF分为OSPFv2和OSPFv3两个版本:OSPFv2定义于RFC 2328(1998),支持IPv4网络;而OSPFv3定义于RFC 5340(2008),支持IPv6网络。
OSPF也是一种内部网关协议
BGP Border Gateway Protocol 边界网关协议
边界网关协议(英语:Border Gateway Protocol,缩写:BGP)是互联网上一个核心的去中心化自治路由协议。它通过维护IP路由表或“前缀”表来实现自治系统(AS)之间的可达性,属于矢量路由协议。BGP不使用传统的内部网关协议(IGP)的指标,而使用基于路径、网络策略或规则集来决定路由。因此,它更适合被称为矢量性协议,而不是路由协议。
BGP属于外部网关协议,用于子网与子网之间的路由信息更新。
数据链路层
数据链路层(Data Link Layer)是OSI参考模型第二层,位于物理层与网络层之间。在广播式多路访问链路中(局域网),由于可能存在介质争用,它还可以细分成介质访问控制(MAC)子层和逻辑链路控制(LLC)子层,介质访问控制(MAC)子层专职处理介质访问的争用与冲突问题。
数据链路层在两个网络实体之间提供数据链路连接的创建、维持和释放管理。构成数据链路数据单元(frame:数据帧或帧),并对帧定界、同步、收发顺序的控制。传输过程中的网络流量控制、差错检测和差错控制等方面。
ARP Address Resolution Protocol 地址解析协议
在网络通信中,从高层到底层寻址分别使用的是端口->IP地址->MAC地址,而ARP协议则在协议栈中提供了在数据链路层的IP地址->MAC地址的服务。
- MAC地址:IP地址是一个可以动态变化的地址,它标示了连接到互联网中的一个主机,然而它是可以变化的,并且一台主机可能会拥有多个IP地址。然而,MAC地址通常来说是不变的,它由主机使用的网卡决定。网卡的制造商在出厂时会赋予网卡一个原则上不允许重复的MAC地址号码,通过该号码就可以找到连接到网络中的唯一主机。
在IPv6中邻居发现协议(NDP)用于代替地址解析协议(ARP)。
当主机需要向当前子网外的主机发送信息时,是获取不到目标主机的MAC地址的。这时候通过ARP协议获取到的通常是一个可以向外转发的设备,例如路由器的MAC地址。这种情况称为委托ARP或ARP代理(ARP Proxy)。
报文格式
通过WireShark抓包,可以看到ARP协议的包结构。
- Hardware type:硬件类型,可以看到以太网为1
- Protocol type:使用的网络层协议,可以看到IPv4为0x0800
- Hardware size:硬件地址长度,单位为字节
- Protocol Size:网络层协议地址长度,单位为字节
- Opcode:操作代码,请求reques为1,reply为2
- Sender MAC address:发送方的MAC地址
- Sender IP address:发送方的IP地址
- Target MAC address:目标的MAC地址
- Target IP address:目标的IP地址
同时也可以看到响应报文的结构。
可以看到差别只是Opcode
的值变了,通过Opcode
的值来标示该报文是请求报文还是响应报文。
可以看到协议栈从高层到底层分别是ARP->Ethernet II
运行原理
在主机A向主机B发送一条信息时,首先封装出IP分组,接着将分组交给数据链路层。数据链路层需要加上以太网协议头,在其中写入目标主机的MAC地址。
每台使用TCP/IP协议的主机中都存有一个ARP缓存表。
当分组到达数据链路层时
- 主机A在ARP缓存表中查找是否有主机B的记录,有的话填入即可
- 如果没有,则发送一条ARP广播请求,在请求中写明目标主机的MAC地址和目标主机的IP地址,以及自己的MAC地址和IP地址跟Opcode、Hardware Type、Protocol Type、Hardware size、Protocol size,此时的目标主机MAC地址为广播地址,即
00.00.00.00.00.00
或FF.FF.FF.FF.FF.FF
- 只有主机B会响应该请求,主机B收到了该请求后就向主机A发送带有自己MAC地址的ARP响应报文(单播),主机A收到后将主机B的MAC地址写入ARP缓存表中
- 如果当前网段中没有主机B,则重新向该网段连接的路由器或网关发送一个ARP请求报文。当然,如果当前的ARP缓存表中没有网关或路由器的MAC地址,需要先进行一次ARP协议请求路由器或网关的MAC地址,然后再给路由器或网关发送请求报文,报文中的目的MAC地址都是路由器或网关的MAC地址。
也就是说,如果是同一个网段仅需要进行一次ARP请求就可以找到目标主机的MAC地址,如果不在一个网段的话最多需要三次。(第一次网段内请求失败,第二次请求网关的MAC地址,第三次请求目标主机的MAC地址)
ARP缓存表采用老化机制,在一段时间内如果表中的某一行没有使用,就会被删除,这样可减少缓存表的长度,加快查询速度。
更新于2022.1.4