计算机网络体系结构
在计算机网络的基本概念中,分层次的体系结构是最基本的
计算机网络体系结构的形成
分层
相互通信的两个计算机系统必须高度协调工作才行,而这种“协调”是相当复杂的。为了设计这样复杂的计算机网络,最初提出了分层的方法。“分层
”可将庞大而复杂的问题,转化为若干较小的局部问题,而这些较小的局部问题比较易于研究和处理
国际标准
全球经济的发展使得不同网络体系结构的用户迫切要求能够互相交换信息,国际标准化组织 ISO 提出了 OSI。只要遵循 OSI 标准,一个系统就可以和位于世界上任何地方的、也遵循这同一标准的其他任何系统进行通信
现今规则最大的、覆盖全球的、基于 TCP/IP 的互联网并未使用 OSI 标准。在20世纪90年代初期,虽然整套的 OSI 国际标准已制定出来,但基于 TCP/IP 的互联网已抢先在全球相当大的范围成功地运行了,而同时却几乎找不到有厂家生产出符合 OSI 标准的商业产品。OSI 只获得了一些理论研究的成果,市场化方面则彻底失败了
TCP/IP 常被称为是事实上的国际标准
协议与划分层次
网络协议
在计算机网络中要做到有条不紊地交换数据,就必须准守一些事先约定好的规则。这些规则明确了所交换的数据的格式以及有关的同步问题
为进行网络中的数据交换而建立的规则、标准或约定称为网络协议
。网络协议也可简称为协议
协议组成要素
语法
,即数据与控制信息的结构或格式语义
,即需要发出何种控制信息,完成何种动作以及做出何种响应同步
,即事件实现顺序的详细说明
协议通常有两种不同的形式。一种是使用便于人来阅读和理解的文字描述。另一种是使用让计算机能够理解的程序代码。这两种不同形式的协议都必须能够对网络上的信息交换过程做出精确的解释
划分层次
对于非常复杂的计算机网络协议,其结构应该是层次式的
分层可以带来很多好处
各层之间是独立的
。某一层并不需要知道它的下一层是如何实现的,而仅仅需要知道该层通过层间接口(即界面)所提供的服务灵活性好
。当任何一层发送变化时(例如由于技术的变化),只要层间接口关系保持不变,则在这层以上或以下各层均不受影响结构上可分割开
。各层都可以采用最合适的技术来实现易于实现和维护
。这种结构使得实现和调试一个庞大而又复杂的系统变得易于处理,因为整个的系统已被分解为若干个相对独立的子系统能促进标准化工作
。因为每一层的功能及其所提供的服务都已有了精确的说明
各层所要完成的功能
差错控制
使相应层次对等方的通信更加可靠流量控制
发送端的发送速率必须使接收端来得及接收,不要太快分段和重装
发送端将要发送的数据块划分为更小的单位,在接收端将其还原复用和分用
发送端几个高层会话复用一条逻辑连接,数据传送结束后释放连接连接建立和释放
交换数据前先建立一条逻辑连接,数据传送结束后释放连接
分层缺点
有些功能会在不同的层次中重复出现,因而产生了额外开销
体系结构
计算机网络的各层及其协议的集合就是网络的体系结构
。计算机网络的体系结构就是这个计算机网络及其构件所应完成的功能的精确定义
体系结构是抽象的,而实现则是具体的,是真正在运行的计算机硬件和软件
具有五层协议的体系结构
五层协议
OSI 的七层协议体系结构的概念清楚,理论也较完整,但它既复杂又不实用。TCP/IP 体系结构则不同,但它现在却得到了非常广泛的应用。TCP/IP 是一个四层的体系结构。在学习计算机网络的原理时往往采用折中的办法,即综合 OSI 和 TCP/IP 的优点,采用一种只有五层协议的体系结构,这样既简洁又能将概念阐述清楚
各层作用
- 应用层:应用层协议定义的是应用进程间通信和交互的规则
- 运输层:运输层的任务就是负责向
两台主机中进程之间的通信
提供通用的数据传输
服务 - 网络层:把运输层产生的报文段或用户数据报封装成
分组
或包
进行传送 - 数据链路层:将网络层交下来的 IP 数据报组装成帧,并在两个相邻结点间的链路上传送
- 物理层:利用物理媒体以
比特
形式传送数据
小结
- 把应用层交互的数据单元称为
报文
- 运输层主要协议:传输控制协议 TCP、用户数据报协议 UDP
- 在 TCP/IP 体系中,由于网络层使用 IP 协议,因此分组也叫
IP 数据报
,或简称为数据报
实体、协议、服务和服务访问点
当研究开放系统中的信息交换时,往往使用实体
(entity)这一较为抽象的名词表示任何可发送或接受信息的硬件或软件进程
协议是控制两个对等实体(或多个实体)进行通信的规则的集合
在协议的控制下,两个对等实体间的通信使得本层能够向上一层提供服务。要实现本层协议,还需要使用下面一层所提供的服务
协议是“水平的”,即协议是控制对等实体之间通信的规则。但服务是“垂直的”,即服务是由下层向上层通过层间接口提供的
计算机网络协议特点
协议必须把所有不利的条件事先都估计到,而不能假定一切都是正常的和非常理想的
非常仔细地检查这个协议能否应付各种异常情况
TCP/IP 的体系结构
TCP/IP 的体系结构比较简单,只有四层
应当指出,技术的发展并不是遵循严格的 OSI 分层概念。实际上现在的互联网使用的 TCP/IP 体系结构有时已经演变成为下图所示的那样,即某些应用程序可以直接使用 IP 层,或甚至直接使用最下面的网络接口层
还有一种方法,就是分层次画出具体的协议表示 TCP/IP 协议族,它的特点是上下两头大而中间小:应用层和网络接口层都有多种协议,而中间的 IP 层很小,上层的各种协议都向下汇聚到一个 IP 协议中
TCP/IP 协议可以为各式各样的应用提供服务,同时 TCP/IP 协议也允许 IP 协议在各式各样的网络构成的互联网上运行。IP 协议在互联网中充当着核心的作用
用户数据报协议 UDP
UDP 概述
用户数据报协议 UDP 只在 IP 的数据报服务之上增加了很少一点的功能,这就是复用和分用的功能以及查错检测的功能
UDP 的主要特点
- UDP 是
无连接的
,即发送数据之前不需要建立连接(发送数据结束时也没有连接可释放),减少了开销和发送数据之前的时延 - UDP 使用
尽最大努力交付
,即不保证可靠交付,主机不需要维持复杂的连接状态表 - UDP 是
面向报文
的,发送方的 UDP 对应用程序交下来的报文,在添加首部后就向下交付 IP 层。UDP 对应用层交下来的报文,既不合并,也不拆分,而是保留这些报文的边界
- UDP
没有拥塞控制
,网络出现的拥塞不会使源主机的发送速率降低。这对某些实时应用是很重要的 - UDP 支持一对一、一对多、多对一和多对多的交互通信
- UDP 的
首部开销小
,只有8个字节,比 TCP 的20个字节的首部要短
存在问题
- 某些实时应用需要使用没有拥塞控制的 UDP,但很多的源主机同时都向网络发送高速率的实时视频流时,网络就有可能发生拥塞,导致大家都无法正常接收。
- 还有一些使用 UDP 的实时应用,需要对 UDP 的不可靠传输进行适当的改进,以减少数据的丢失。应用进程可以在不影响应用的实时性的前提下,增加一些提高可靠性的措施,如采用前向纠错或重传已丢失的报文
UDP 的首部格式
用户数据报 UDP 有两个字段:数据字段
和首部字段
。首部字段很简单,只有8个字节,由四个字段组成,每个字段都是两个字节
首部字段
源端口
源端口号。在需要对方回信时。不需要时可用全0目的端口
目的端口号。这在终点交付报文时必须使用长度
UDP 用户数据报的长度,其最小值是8(仅有首部)检验和
检测 UDP 用户数据报在传输中是否有错。有错就丢弃
端口分用
当运输层从 IP 层收到 UDP 数据报时,就根据首部中的目的端口,把 UDP 数据报通过相应的端口,上交最后的终点——应用进程
如果接受方 UDP 发现收到的报文中的目的端口号不正确(即不存在对应于该端口号的应用程序),就丢弃该报文,并由网际控制报文协议 ICMP 发送“端口不可达”差错报文给发送方
伪首部
UDP 用户数据报首部中检验和的计算方法有些特殊。在计算检验和时,要在 UDP 用户数据报之前增加 12 个字节的伪首部
。所谓“伪首部”是因为这种伪首部并不是 UDP 用户数据报真正的首部。只是在计算检验和时,临时添加在 UDP 用户数据报前面,得到一个临时的 UDP 用户数据报。检验和就是按照这个临时用户数据报来计算的。伪首部既不向下传也不向上递交,而仅仅是为了计算检验和
传输控制协议 TCP
传输控制协议 TCP 概述
TCP 最主要的特点
- TCP 是
面向连接的运输层协议
。应用程序在使用 TCP 协议之前,必须先建立 TCP 连接。在传送数据完毕后,必须释放已经建立的 TCP 连接 - 每一条 TCP 连接只能有两个
端点
,每一条 TCP 连接只能是点对点
的(一对一) - TCP 提供
可靠交付
的服务。通过 TCP 连接传送的数据,无差错、不丢失、不重复,并且按序到达 - TCP 提供
全双工通信
。TCP 允许通信双方的应用进程在任何时候都能发送数据。TCP 连接的两端都设有发送缓存和接受缓存,用来临时存放双向通信的数据 面向字节流
。TCP 中的“流”指的是流入到进程或从进程流出的字节序列
面向字节流
“面向字节流”的含义是:虽然应用程序和 TCP 的交互式一次一个数据块(大小不等),但 TCP 把应用程序交下来的数据仅仅看成是一连串的无结构的字节流
。TCP 并不知道所传送的字节流的含义
TCP 不保证接收方应用程序所收到的数据块和发送方应用程序所发出的数据块具有对应大小的关系
例如,发送方应用程序交给发送方的 TCP 共10个数据块,但接收方的 TCP 可能只用了4个数据块就把收到的字节流交付上层的应用程序
接收方应用程序收到的字节流必须和发送方应用程序发出的字节流完全一样。接收方的应用程序必须有能力识别收到的字节流,把它还原成有意义的应用层数据
TCP 和 UDP 在发送报文时采用的方式完全不同。TCP 并不关心应用进程一次把多长的报文发送到 TCP 的缓存中,而是根据对方给出的窗口值和当前网络拥塞的程度来决定一个报文段应包含多少个字节(UDP 发送的报文长度是应用进程给出的)。如果应用进程传送到 TCP 缓存的数据块太长,TCP 就可以把它划分短一些再传送。如果应用进程一次只发来一个字节,TCP 也可以等待积累有足够多的字节后再构成报文段发送出去
TCP 的连接
TCP 把连接
作为最基本的抽象
。TCP 的许多特性都与 TCP 是面向连接的这个基本特性有关
TCP 连接的端点叫做套接字(socket)或插口
,根据 RFC 793 的定义:端口号拼接到(concatenated with) IP 地址即构成了套接字
套接字 socket = (IP 地址:端口号)
每一条 TCP 连接唯一地被通信两端的两个端点(即两个套接字)所确定
TCP 连接 ::= {socket1, socket2} = {(IP1: port1), (IP2: port2)}
TCP 连接就是由协议软件所提供的一种抽象。TCP 连接的端口是个很抽象的套接字
,即( IP地址
: 端口号
)。同一个 IP 地址可以有多个不同的 TCP 连接,而同一个端口号也可以出现在多个不同的 TCP 连接中
易混淆的 socket
同一个名词 socket 却可表示多种不同的意思,以下 socket 的意思跟本文中所引用的 RFC 793 定义的 socket(指端口号拼接到 IP 地址)不同
- 允许应用程序访问连网协议的
应用编程接口 API(Application Programming Interface)
,即运输层和应用层之间的接口,称为 socket API,并简称为 socket - 在 socket API 中使用的一个
函数名
也叫做 socket - 调用 socket 函数的
端点
称为 socket,如“创建一个数据报 socket” - 调用 socket 函数时,其
返回值
称为 socket 描述符,可简称为 socket - 在操作系统内核中连网协议的 Berkeley 实现,称为 socket
实现
可靠传输的工作原理
理想的传输条件
理想的传输条件有以下两个特点
- 传输信道不产生差错
- 不管发送方以多快的速度发送数据,接收方总是来得及处理收到的数据
实际的网络不具备以上两个理想条件。需要使用一些可靠的传输协议,当出现差错时让发送方重传出现差错的数据,同时在接收方来不及处理收到的数据时,及时告诉发送方适当减低发送数据的速度。这样,不可靠的传输信道就能够实现可靠传输了
停止等待协议
全双工通信的双方既是发送方也是接收方。把传送的数据单元都称为分组。“停止等待”就是每发完一个分组就停止发送,等待对方的确认。在收到确认后再发送下一个分组
无差错情况
出现差错
只要超过一段时间没有收到确认,就认为刚才发送的分组丢失了,因而重传前面发送过的分组。这就叫做超时重传
。要实现超时重传,就要在每发送完一个分组时设置一个超时计时器
- 发送完一个分组后,
必须暂时保留已发送的分组的副本
(在发生超时重传时使用)。只有在收到相应的确认后才能清除暂时保留的分组副本 - 分组和确认分组都必须进行
编号
。这样才能明确是哪一个发送出去的分组收到了确认,而哪一个分组还没有收到确认 - 超时计时器的重传时间
应当比数据在分组传输的平均往返时间更长一些
确认丢失和确认迟到
使用上述的确认和重传机制,我们就可以在不可靠的传输网络上实现可靠的通信
像上述的这种可靠传输协议常称为自动重传请求 ARQ(Automatic Repeat reQuest)
。重传的请求是自动进行的。接收方不需要请求发送方重传某个出错的分组
信道利用率
停止等待协议的优点是简单,但缺点是信道利用率太低
为了提高传输效率,发送方可以不使用低效率的停止等待协议,而是采用流水线传输
。流水线传输就是发送方可连续发送多个分组,不必每发完一个分组就停顿下来等待对方的确认。这样可使信道上一直有数据不间断地在传送。这种传输方式可以获得很高的信道利用率
连续 ARQ 协议
位于发送窗口内的5个分组都可以连续发送出去,而不需要等待对方的确认。可以提高信道利用率
接收方一般都是采用累积确认
的方式。接收方不需要对收到的分组逐个发送确认,而是在收到几个分组后,对按序到达的最后一个分组发送确认
积累确认有优点也有缺点。优点是:容易实现,即使确认丢失也不必重传。缺点是不能向发送方反映出接收方已经正确收到的所有分组的信息
TCP 报文段的首部格式
TCP 虽然是面向字节流的,但 TCP 传送的数据单元却是报文段。一个 TCP 报文段分为首部和数据两部分。TCP 报文段首部的前20个字节是固定的,后面有4n字节是根据需要而增加的选项(n是整数)。因此 TCP 首部的最小长度是20字节
首部字段
源端口
和目的端口
各占2个字节,分别写入源端口号和目的端口号序号
占4字节。序号范围是[0, 232-1],共232(即4 294 967 296)个序号。序号增加到232-1后,下一个序号就又回到0。在一个 TCP 连接中传送的字节流中的每一个字节都按顺序编号
确认号
占4字节,是期望收到对方下一个报文段的第一个数据字节的序号
数据偏移
占4字节,它指出 TCP 报文段的数据起始处距离 TCP 报文段的起始处有多远。这个字段实际上是指出 TCP 报文段的首部长度保留
占6位,保留为今后使用,但目前应置为0
下面有6个控制位
,用来说明本报文段的性质
-
紧急 URG(URGent)
当 URG=1 时,表明紧急指针字段有效。它告诉系统此报文段中有紧急数据,应尽快传送(相当于高优先级的数据),而不是按原先的排队顺序来传送 -
确认 ACK(ACKnowledgment)
仅当 ACK=1 时确认号字段才有效。当 ACK=0 时,确认号无效。TCP 规定,在连接建立后所有传送的报文段都必须把 ACK 置1 -
推送 PSH(Push)
当两个应用进程进行交互式的通信时,有时在一端的应用进程希望在键入一个命令后立即就能够收到对方的响应 -
复位 RST(ReSeT)
当 RST=1 时,表明 TCP 连接中出现严重差错(如由于主机崩溃或其他原因),必须释放连接,然后再重新建立运输连接 -
同步 SYN(SYNnchronization)
在连接建立时用来同步序号。当 SYN=1 而 ACK=0 时,表明这是一个连接请求报文段。对方若同意建立连接,则应在响应的报文段中使 SYN=1 和 ACK=1 -
终止 FIN(FINis)
用来释放一个连接。当 FIN=1 时,表明此报文段的发送发的数据已发送完毕,并要求释放运输连接 -
窗口
占2字节。窗口值是[0, 216-1]之间的整数。窗口值作为接收方让发送方设置其发送窗口的依旧 -
检验和
占2字节。检验和字段检验的范围包括首部和数据这两部分 -
紧急指针
占2字节。紧急指针仅在 URG=1 时才有意义,它指出本报文段中的紧急数据的字节数 -
选项
长度可变,最长可达40字节
TCP 可靠传输的实现
以字节为单位的滑动窗口
发送窗口构造
TCP 的滑动窗口是以字节为单位的。假定 A 收到了 B 发来
的确认报文段,其中窗口是20字节,而确认号是31(这表明 B 期望收到的下一个序号是31,而序号30为止的数据已经收到了)。根据这两个数据,A 就构造出自己的发送窗口
发送窗口标识:在没有收到 B 的确认的情况下,A 可以连续把窗口内的数据都发送出去。凡是已经发送出去的数据,在未收到确认之前都必须暂时保留,以便在超时重传时使用
发送窗口变化
发送窗口的位置由窗口前沿和后沿的位置共同确定。发送窗口后沿的变化情况有两种,即不动(没有收到新的确认)和前移(收到了新的确认)。发送窗口后沿不可能向后移动,因为不能撤销已收到的确认
发送窗口前沿通常是不断向前移动,但也有可能不动。这对应于两种情况:
- 一是没有收到新的确认,对应通知的窗口大小也不变
- 二是收到了新的窗口单对方通知的窗口缩小了,使得发送窗口前沿正好不动
发送窗口前沿也有可能向后收缩
。这发生在对方通知的窗口缩小了。但 TCP 的标准强烈不赞成这样做
。因为很可能发送方在收到这个通知以前已经发送了窗口中的许多数据,现在又要收缩窗口,不让发送这些数据,这样就会产生一些错误
要描述一个发送窗口的状态需要三个指针:P1,P2,P3。指针都指向字节的序号。这三个指针指向的几个部分的意义如下:
- 小于 P1 的是已发送并已收到确认的部分,而大于 P3 的是不允许发送的部分
- P3 - P1 =
A 的发送窗口
- P2 - P1 已发送但尚未收到确认的字节数
- P3 - P2 允许发送但当前尚未发送的字节数(又称为
可用窗口
或有效窗口
)
B 的接收窗口大小是20。在接收窗口外面,到30号为止的数据是已经发送过确认,并且已经交付主机了。因此在 B 可以不再保留这些数据。接收窗口内的序号(31~50)是允许接收的。在上图中,B 收到了序号为32和33的数据。这些数据没有按序到达,因为序号为31的数据没有收到(也许丢失了,也许滞留在网络中的某处)。请注意,B 只能对按序收到的数据中的最高序号给出确认,因此 B 发送的确认报文段中的确认号仍然是31(即期望收到的序号),而不是32或33
现在假定 B 收到了序号为31的数据,并把序号为31~33的数据交付主机,然后 B 删除这些数据。接着把接收窗口向前移动3个序号,同时给 A 发送确认,其中窗口值仍为20,但确认号是34。这表明 B 已经收到了到序号33为止的数据。B 还收到了序号为37,38和40的数据,但这些都没有按序到达,只能先暂存在接收窗口中。A 收到 B 的确认后,就可以把发送窗口向前滑动3个序号,但指针 P2 不动。现在 A 的可用窗口增大了,可发送的序号范围是42~53
A 在继续发送完序号42~53的数据后,指针 P2 向前移动和 P3 重合。发送窗口内的序号都已用完,但还没有再收到确认。由于 A 的发送窗口已满,可用窗口已减小到零,因此必须停止发送。发送窗口内所有的数据都已正确到达 B,B 也早已发出了确认。但所有这些确认都滞留在网络中。在没有收到 B 的确认时,A 不能猜测:”或许 B 收到了吧!“为了保证可靠传输,A 只能认为 B 还没有收到这些数据。于是,A 在经过一段时间后(由超时计时器控制)就重传这部分数据,重新设置超时计时器,直到收到 B 的确认为止。如果 A 收到确认号落在发送窗口内,那么 A 就可以发送窗口继续向前滑动,并发送新的数据
缓存和窗口
发送方维持的发送缓存和发送窗口,以及接收方维持的接收缓存和接收窗口
发送缓存用来暂时存放:
- 发送应用程序传送给对方 TCP 准备发送的数据
- TCP 已发送出但尚未收到确认的数据
已被确认的数据应当从发送缓存中删除,因此发送缓存和发送窗口的后沿是重合的。发送应用程序必须控制写入缓存的速率,不能太快,否则发送缓存就会没有存放数据的空间
接收缓存用来暂时存放:
- 按序到达的、但尚未被接收应用程序读取的数据
- 未按序到达的数据
收到的分组被检测出有差错,则丢弃。接收应用程序来不及读取收到的数据,接收缓存最终就会被填满,使接收窗口减小到零。接收应用程序能够及时从接收缓存中读取收到的数据,接收窗口就可以增大,最大亦不能超过接收缓存的大小
要点小结:
- 虽然 A 的发送窗口是根据 B 的接收窗口设置的,但在同一时刻,A 的发送窗口并不总是和 B 的接收窗口一样大。通过网络传送窗口值需要经历一定的时间滞后,该时间并不确定的
- 对于不按序到达的数据,TCP 通常是先临时存放在接收窗口,等字节流中所缺少的字节收到后,在
按序交付上层的应用进程
- TCP 要求接收方必须有累积确认的功能,这样可以减少传输开销
超时重传时间的选择
TCP 的发送方在规定的时间内没有收到确认就要重传已发送的报文段。这种重传的概念是很简单的,但重传时间的选择却是 TCP 最复杂的问题之一
由于 TCP 的下层是互联网环境,发送的报文段可能只经过一个高速率的局域网,也可能经过多个低速率的网络,并且每个 IP 数据报所选择的路由还可能不同。如果把超时重传时间设置得太短,就会引起很多报文段的不必要的重传,使网络负荷增大。但若把超时重传时间设置的过长,则又使网络的空闲时间增大,降低了传输效率
TCP 采用了一种自适应算法,它记录一个报文段发出的时间,以及收到相应的确认的时间。这两个时间之差就是报文段的往返时间 RTT
新的 RTTs = (1 - α) x (旧的 RTTs) + α x (新的 RTT 样本)
RTT:报文段往返时间
RTTs:加权平均往返时间
α: 0 ≤ α < 1,RFC 6298 推荐的 α 值为 1/8,即 0.125
RTO = RTTs + 4 x RTTD
RTO:超时重传时间
RTTD:RTT 的偏差的加权平均值
新的 RTTD = (1 - β) x (旧的 RTTD) + β x |RTTs - 新的 RTT 样本|
β:小于1的系数,推荐值是 1/4,即 0.25
TCP 流量控制
利用滑动窗口实现流量控制
流量控制(flow control):让发送方的发送速率不要太快,要让接收方来得及接收
利用滑动窗口机制可以很方便地在 TCP 连接上实现对发送方的流量控制
发送方的发送窗口不能超过接收方给出的接收窗口的数值
。TCP 的窗口单位是字节,不是报文段
避免死锁:TCP 为每一个连接设有一个持续计时器(persistence timer)
。只要 TCP 连接的一方收到对方的零窗口通知,就启动持续计时器。若持续计时器设置的时间到期,就发送一个零窗口探测报文段
(仅携带1字节的数据),而对方就在确认这个探测报文段时给出了现在的窗口值。如果窗口仍是零,那么收到这个报文段的一方就重新设置持续计时器。如果窗口不是零,那么死锁的僵局就可以打破了
TCP 的传输效率
发送机制
- TCP 维持一个变量,它等于
最大报文段长度 MSS
。只要缓存中存放的数据达到 MSS 字节时,就组装成一个 TCP 报文段发送出去 - 由发送方的应用进程指明要求发送报文段,即 TCP 支持的
推送(push)
操作 - 发送方的一个计时器期限到了,这时把当前已有的缓存数据装入报文段(但长度不能超过 MSS)发送出去
Nagle 算法
在 TCP 的实现中广泛使用 Nagle 算法
若发送应用进程把要发送的数据逐个字节地送到 TCP 的发送缓存,则发送方就把第一个数据字节先发送出去,把后面到达的数据字节都缓存起来。当发送方收到对第一个数据字符的确认后,再把发送缓存中的所有数据组装成一个报文段发送出去,同时继续对随后到达的数据进行缓存。只有在收到对前一个报文段的确认后才继续发送下一个报文段。当数据达到较快而网络速率较慢时,用这样的方法可明显地减少所用的网络宽带。Nagle 算法还规定,当到达的数据已达到发送窗口大小的一半或已达到报文段的最大长度时,就立即发送一个报文段。这样可以有效提高网络的吞吐量
糊涂窗口综合征
TCP 接收方的缓存已满,仅剩一个字节,并还将保持这种状态持续一段时间。导致发送方只能发送一个字节。导致网络的效率很低
为了解决这个问题,可以让接收方等待一段时间
,使得或者接受缓存已有足够空间容纳一个最长的报文段,或者等到接受缓存已有一半空闲的空间
。只要出现这两种情况之一,接收方就发出确认报文,并向发送方通知当前的窗口大小。发送方也不要发送大小的报文段,而是把数据积累成足够大的报文段,或达到接收方缓存的空间的一半大小
TCP 的拥塞控制
拥塞控制的一般原理
在计算机网络中的链路容量(即宽带)、交换结点中的缓存和处理机等,都是网络资源。在某段时间,若对网络中某一资源的需求超过了该资源所能提供的可用部分,网络的性能就要变坏。这种情况就叫做拥塞
(congestion)
拥塞控制
就是防止过多的数据注入到网络中,这样可以使网络中的路由器或链路不致过载
。拥塞控制所要做的都是一个前提,就是网络能够承受现有的网络负荷
TCP 的拥塞控制方法
TCP 进行拥塞控制的算法有四种,即慢开始
(slow-start)、拥塞避免
(congestion avoidance)、快重传
(fast retransmit)和快恢复
(fast recovery)
慢开始
当主机开始发送数据时,由于并不清楚网络的负荷情况,如果立即把大量数据字节注入到网络,就有可能引起网络发生拥塞。经验证明,较好的方法是先探测一下,即由小到大逐渐增大发送窗口
,也就是说,由小到大逐渐增大拥塞窗口数值
cwnd:发送方的拥塞窗口,开始发送方设置 cwnd = 1
拥塞避免
让拥塞窗口 cwnd 缓慢地增大,即每经过一个往返时间 RTT 就把发送方的拥塞窗口 cwnd 加1,而不是像慢开始阶段那样加倍增加。因此在拥塞避免阶段就有“加法增大
” AI(Additive Increase)的特点。这表明在拥塞避免阶段,拥塞窗口 cwnd 按线性规律缓慢增长
,比慢开始算法的拥塞窗口增长速率缓慢得多
“拥塞避免”并非完全能够避免拥塞,而是把拥塞窗口控制为按线性规律增长,使网络比较不容易出现拥塞
在执行慢开始算法时,发送方每收到一个对新报文段的确认 ACK,就把拥塞窗口值加1,然后开始下一轮的传输。因此拥塞窗口 cwnd 随着传输轮次按指数规律增长。当拥塞窗口 cwnd 增长到慢开始门限值 ssthresh 时,就改成执行拥塞避免算法,拥塞窗口按线性规律增长
ssthresh:慢开始门限,一般的,会有一个初始值,下图中为16个报文段
当拥塞窗口 cwnd = 24 时,网络出现了超时,发送方判断为网络拥塞。于是调整门限值 ssthresh = cwnd / 2 = 12,同时设置拥塞窗口 cwnd = 1,进入慢开始阶段
快重传
采用快重传算法可以让发送方尽早知道发生了个别报文段的丢失
。快重传算法首先要求接收方不要等待自己发送数据时才进行捎带确认,而是要立即发送确认
,即使收到了失序的报文段
也要立即发出对已收到的报文段的重复确认
快恢复
发送方知道当前只是丢失了个别的报文段。于是不启动慢开始,而是执行快恢复
算法。这时,发送方调整门限值 ssthresh = cwnd / 2 = 8,同时设置拥塞窗口 cwnd = ssthresh = 8,并开始执行拥塞避免算法
TCP Reno 版本:区别于老的 TCP Tahao 版本
TCP 的运输连接管理
TCP 是面向连接的协议。运输连接是用来传送 TCP 报文的。TCP 运输连接的建立和释放是每一次面向连接的通信中必不可少的过程。运输连接有三个阶段,连接建立
、数据传送
和连接释放
。运输的连接管理就是使运输连接的建立和释放都能够正常地进行
在 TCP 连接建立过程中要解决以下三个问题:
- 要使每一方能够确知对方的存在
- 要允许双方协商一些参数(最大窗口值、是否使用窗口扩大选项和时间戳选项以及服务质量等)
- 能够对运输实体资源(缓存大小、连接表中的项目等)进行分配
TCP 的连接建立
TCP 建立连接的过程叫做握手,握手需要在客户和服务器之间交换三个 TCP 报文段
连接建立过程
- 最初客户/服务器的 TCP 进程都处于
CLOSED(关闭)
状态。在本实例中,A主动打开连接
,而 B被动打开连接
- B 的 TCP 服务器进程先创建
传输控制块
TCB,并处于LISTEN(收听)
状态,等待客户的连接请求 - A 的 TCP 客户进程创建
传输控制模块
TCB。并向 B 发出连接请求报文段,首部中的同部位 SYN = 1,选择一个初始序号 seq = x。TCP 客户端进程进入SYN-SENT(同步已发送)
状态。TCP 规定,SYN 报文段(即 SYN = 1 的报文段)不能携带数据,但要消耗一个序号
- B 收到连接请求报文段后,如同意建立连接,则向 A 发送确认。在确认报文段中应把 SYN 位和 ACK 位都置1,确认号是 ack = x + 1,同时也为自己选择一个初始序号 seq = y。这时 TCP 服务器进程进入
SYN-RCVD(同步收到)
状态。这个报文段也不能携带数据,但同样要消耗掉一个序号
- TCP 客户进程收到 B 的确认后,还要向 B 给出确认。确认报文段的 ACK 置1,确认号 ack = y + 1,而自己的序号 seq = x + 1。TCP 的标准规定,ACK 报文段可以携带数据。但
如果不携带数据则不消耗序号
,在这种情况下,下一个数据报文段的序号仍是 seq = x + 1。这时,TCP 连接已经建立,A 进入ESTABLISHED(已建立连接)
状态 - 当 B 收到 A 的确认后,也进入
ESTABLISHED
状态
传输控制块
TCB(Transmission Control Block)存储了每一个连接中的一些重要信息,如:TCP 连接表,指向发送和接收缓存的指针,指向重传队列的指针,当前的发送和接收序号等等
四报文握手
B 发送给 A 的报文段,可拆成两个报文段。先发送一个确认报文段(ACK = 1,ack = x + 1),然后再发送一个同步报文段(SYN = 1,seq = y)。这样的过程就变成了四报文握手
,与三报文握手效果一致
异常情况
为什么 A 最后还要发送一次确认呢?这主要是为了防止已失效的连接请求报文段突然又传到了 B,因而产生错误
正常情况:A 发出连接请求,但因连接请求报文丢失而未收到确认。于是 A 再重传一次连接请求。后来收到了确认,建立了连接。数据传输完毕后,就释放了连接。A 共发送了两个连接请求报文段,其中第一个丢失,第二个到达了 B,没有“已失效的连接请求报文段”
异常情况:A 发出的第一个连接请求报文段并没有丢失,而是在某些网络结点长时间滞留了,以致延误到连接释放以后的某个时间才到达 B。本来这是一个早已失效的报文段。但 B 收到此失效的连接请求报文段后,就误认为是 A 又发出一次新的连接请求。于是就向 A 发出确认报文段,同意建立连接。假定不采用报文握手,那么只要 B 发出确认,新的连接就建立了。
现在 A 并没有发出建立连接的请求,因此不会理睬 B 的确认,也不会向 B 发送数据。但 B 却以为新的运输连接已经建立了,并一直等待 A 发来数据。B 的
许多资源就这样被浪费了
。
采用三报文握手的办法,可以防止上述现象的发生
TCP 的连接释放
连接释放过程
- A 的应用进程先向其 TCP 发出连接释放报文段,并停止再发送数据,主动关闭 TCP 连接。A 把连接释放报文段首部的终止控制位 FIN 置1,其序号 seq = u,它等于前面已传送过的数据的最后一个字节的序号加1。这时 A 进入
FIN-WAIT-1(终止等待1)
状态,等待 B 的确认。TCP 规定,FIN 报文段即使不携带数据,也消耗一个序号 - B 收到连接释放报文段后即发出确认,确认号是 ack = u + 1,而这个报文段自己的序号是 v,等于 B 前面已传送过的数据的最后一个字节的序号加1。B随即进入
CLOSE-WAIT(关闭等待)
状态。TCP 服务器进程这时应通知高层应用进程,因而从 A 到 B 这个方向的连接就释放了,这时的 TCP 连接处于半关闭(half-close)
状态,即 A 已经没有数据要发送了,但 B 若发送数据,A 仍要接收。也就是说,从 B 到 A 这个方向的连接并未关闭,这个状态可能会持续一段时间 - A 收到来自 B 的确认后,就进入
FIN-WAIT-2(终止等待2)
状态,等待 B 发出的连接释放报文段 - 若 B 已经没有要向 A 发送的数据,其应用进程就通知 TCP 释放连接。这时 B 发出的连接释放报文段必须使 FIN = 1。现假定 B 的序号为 w(在半关闭状态 B 可能又发送了一些数据)。B 还必须重复上次已发送过的确认号 ack = u + 1。这时 B 就进入
LAST-ACK(最后确认)
状态,等待 A 的确认 - A 在收到 B 的连接释放报文段后,必须对此发出确认。在确认报文段中把 ACK 置1,确认号 ack = w + 1,而自己的序号是 seq = u + 1(根据 TCP 标准,前面发送过的 FIN 报文段要消耗一个序号)。然后进入到
TIME-WAIT(时间等待)
状态。此时 TCP 连接还没有释放掉。必须经过时间等待计时器(TIME-WAIT timer)
设置的时间2MSL后,A 才进入到CLOSED
状态 - 当 A 撤销相应的传输控制块 TCB 后,就结束了这次的 TCP 连接
时间 MSL 叫做
最长报文段寿命
(Maximum Segment Lifetime),RFC 793建议设为2分钟。但这完全是从工程上来考虑的,对于现在的网络,MSL = 2分钟可能太长了一些
TIME-WAIT 等待时间
为什么 A 在 TIME-WAIT 状态必须等待 2MSL 的时间呢?
为了保证 A 发送的最后一个 ACK 报文段能够到达 B。这个 ACK 报文段有可能丢失,因而使处在 LAST-ACK 状态的 B 收不到对已发送的 FIN + ACK 报文段的确认。B 会超时 重传这个 FIN + ACK 报文段,而 A 就能在 2MSL 时间内收到这个重传的 FIN + ACK 报文段。接着 A 重传一次确认,重新启动 2MSL 计时器。最后,A 和 B 都正常进入到 CLOSED 状态。如果 A 在 TIME-WAIT 状态不等待一段时间,而是在发完 ACK 报文段后立即释放连接,那么就无法收到 B 重传的 FIN + ACK 报文段,因而也不会再发送一次确认报文段。这样,B 就无法安装正常步骤进入 CLOSED 状态
防止前面提到的“已失效的连接请求报文段”出现在本连接中。A 在发送完最后一个 ACK 报文段后,再经过时间 2MSL,就可以使本连接持续的时间内所产生的所有报文段都从网络中消失。这样就可以使下一个新的连接中不会出现这种旧的连接请求报文段
B 只要收到 A 发出的确认,就进入 CLOSED 状态。同样,B 在撤销相应的传输控制 TCB 后,就结束了这次的 TCP 连接。B 结束 TCP 连接的时间要比 A 早一些
保活计时器(keepalive timer)
:服务器每收到一次客户的数据,就重新设置保活计时器,时间的设置通常是两小时。若两小时没有收到客户的数据,服务器就发送一个探测报文段,以后则每隔75秒发送一次。若一连发送10个探测报文段后仍无客户的响应,服务器就认为客户端出了故障,接着就关闭这个连接
TCP 的有限状态机
为了更清晰地看出 TCP 连接的各种状态之间的关系,下图为 TCP 的有限状态机。图中每一个方框即 TCP 可能具有的状态。每个方框中的大写英文字符串是 TCP 标准所使用的 TCP 连接状态名。状态之间的箭头表示可能发生的状态变迁。箭头旁边的字,表明引起这种变迁的原因,或表明发生状态变迁后又出现什么动作。请注意图中有三种不同的箭头。粗实线箭头
表示对客户进程的正常变迁
。粗虚线箭头
表示对服务器进程的正常变迁
。另一种细线箭头
表示异常变迁
TCP 粘包拆包
粘包问题
在 TCP 这种字节流协议上做应用层分包
是网络编程的基本需求。分包指的是在发生一个消息(message)或一帧(frame)数据时,通过一定的处理,让接收方能从字节流中识别并截取(还原)出一个个消息。因此,“粘包问题”是个伪命题
短连接分包
对于短连接的 TCP 服务,分包不是一个问题,只要发送方主动关闭连接,就表示一个消息发送完毕,接收方 read() 返回0,从而知道消息的结尾
TCP 发送机制
为了提高 TCP 的传输效率,TCP 有一套自己的发送机制
- TCP 维持一个变量,它等于
最大报文段长度 MSS
。只要缓存中存放的数据达到 MSS 字节时,就组装成一个 TCP 报文段发送出去 - 由发送方的应用进程指明要求发送报文段,即 TCP 支持的
推送(push)
操作 - 发送方的一个计时器期限到了,这时把当前已有的缓存数据装入报文段(但长度不能超过 MSS)发送出去
长连接分包
对于长连接的 TCP 服务,分包有四种方法
- 消息长度固定
- 使用特殊的字符或字符串作为消息的边界,例如 HTTP 协议的 headers 以“\r\n”为字段的分隔符
- 在每条消息的头部加一个长度字段,这恐怕是最常见的做法
- 利用消息本身的格式来分包,例如 XML 格式的消息中
<root>
…</root>
的配对,或者 JSON 格式中的 { … } 的配对。解析这种消息格式通常会用到状态机(state machine)
复杂的分包
假如消息格式非常简单,“消息”本身是一个字符串,每条消息有一个4字节的头部,以网络序存放字符串的长度。消息直接没有间隙,字符串也不要求以 ‘\0’ 结尾
发送两条消息“hello”和“smartboy”,打包后的字节流共有21字节
0x00, 0x00, 0x00, 0x05, 'h', 'e', 'l', 'l', 'o',
0x00, 0x00, 0x00, 0x08, 's', 'm', 'a', 'r', 't', 'b', 'o', 'y'
假设数据最终都全部到达,数据解析逻辑至少能正确处理以下各种数据到达的次序
- 一个字节一个字节到达
- 数据分两次到达,第一次收到2个字节,不足消息的长度字段
- 数据分两次到达,第一次收到4个字节,刚好够长度字段,但是没有 body
- 数据分两次到达,第一次收到8个字节,长度完整,但 body 不完整
- 数据分两次到达,第一次收到9个字节,长度完整,但 body 也完整
- 数据分两次到达,第一次收到10个字节,第一条消息的长度完整、body 也完整,第二条消息长度不完整
- 请自行移动和增加分割点,一共有超过 100 万种可能(221-1)
- 数据一次就全部到达
HTTP 状态码
状态码
状态码是来告诉客户端,发生了什么事情。状态码为客户端提供了一种理解事务处理结果
的便捷方式
。状态码位于响应的起始行中
比如,在行 HTTP/1.0 200 OK 中,状态码就是200
客户端向一个 HTTP 服务器发送请求报文时,会遇到很多意想不到的情况,请求不一定能够成功完成。服务器可能会告诉你无法找到所请求的资源,你没有访问资源的权限,或者资源被移到了其他地方
状态码是在每条响应报文的起始行中返回的。会返回一个数字状态和一个可读的状态。数字码
便于程序进行差错处理,而原因短语
则便于人们理解
原因短语
原因短语是响应起始行中的最后一个组件。它为状态码提供了文本形式
的解释
比如,在行 HTTP/1.0 200 OK 中,OK 就是原因短语
原因短语和状态码是成对出现的。原因短语是状态码的可读
版本,应用程序开发者将其传送给用户,用于说明在请求间发生了什么情况。HTTP 规范并没有提供任何硬性规定,要求原因短语以何种形式出现
状态码分类
五大类
可以通过三位数字代码对不同状态码进行分类
- 200 到 299 之间的状态码表示成功
- 300 到 399 之间的代码表示资源已经被移走了
- 400 到 499 之间的代码表示客户端的请求出错了
- 500 到 599 之间的代码表示服务器出错了
状态码 | 整体范围 | 已定义范围 | 分类 |
---|---|---|---|
1XX | 100~199 | 100~101 | 信息提示 |
2XX | 200~299 | 200~206 | 成功 |
3XX | 300~399 | 300~305 | 重定向 |
4XX | 400~499 | 400~415 | 客户端错误 |
5XX | 500~599 | 500~505 | 服务器错误 |
当前的 HTTP 版本只为每类状态定义了几个代码。随着协议的发展,HTTP 规范中会正式地定义更多的状态码。若收到了不认识的状态码,可能是有人将其作为当前协议的扩展定义
的。可以根据其所处的范围,将它作为那个类别中一个普通的成员来处理
例如,若收到了状态码 515(在 5XX 代码的已定义范围之外),就应该认为这条响应指出了服务器的错误,这是 5XX 报文的通用类别
100 ~ 199,信息状态码
HTTP/1.1 向协议中引入了信息性状态码。这些状态码相对较新,关于其复杂性和感
知价值存在一些争论,而受到限制
状态码 | 原因短语 | 含义 |
---|---|---|
100 | Continue | 说明收到了请求的初始部分,请客户端继续。发送了这个状态码之后,服务器在收到请求之后必须进行响应 |
101 | Switching Protocols | 说明服务器正在根据客户端的指定,将协议切换成 Update 首部所列的协议 |
100 Continue 状态码的目的是对这样的情况进行优化:HTTP 客户端应用程序有一个实体的主体部分要发送给服务器,但希望在发送之前查看一下服务器是否会接受这个实体。客户端应用程序只有在避免向服务器发送一个服务器无法处理或使用的大实体
,才应该使用 100 Continue
200 ~ 299,成功状态码
客户端发起请求时,这些请求通常都是成功的。服务器有一组用来表示成功的状态码,分别对应于不同类型的请求
状态码 | 原因短语 | 含义 |
---|---|---|
200 | OK | 请求没问题,实体的主体部分包含了所请求的资源 |
201 | Created | 用于创建服务器对象的请求(比如:PUT)。响应的实体主体部分中应该包含引用了已创建的资源的URL,Location首部包含的则是最具体的引擎。服务器必须在发送这个状态码之前创建好对象 |
202 | Accepted | 请求已被接受,服务器还未对其执行任何动作。不能保证服务器会完成这个请求;接受请求时,它看起来是有效的。服务器应在实体的主体部分包含对请求状态的描述,或附加请求预计处理时间、信息获取指针 |
203 | Non-Authoritative Information | 实体首部包含的信息不是来自于源端服务器,而是来自资源的副本。如果中间节点上有一份副本,但无法或没有对元数据进行验证,就会出现这种情况 |
204 | No Content | 响应报文中包含若干首部和一个状态行,但没有实体的主体部分。主要用于在浏览器不转为显示新文档的情况下,对其进行更新(比如刷新一个表单页面) |
205 | Reset Content | 另一个主要用于浏览器的代码。负责告知浏览器清除当前页面中的所有 HTML 表单元素 |
206 | Partial Content | 成功执行了一个部分或 Range(范围)请求。客户端可以通过一些特殊的首部来获取部分或某个范围内的文档 |
300 ~ 399,重定向状态码
重定向状态码要么告知客户端使用替代位置来访问他们所感兴趣的资源,要么就提供一个替代的响应而不是资源的内容。如果资源已被移动,可发送一个重定向状态码和一个可选的 Location 首部来告知客户端资源已被移走,以及现在可以在哪里找到它。这样,浏览器就可以在不打扰使用者的情况下,透明地转入新的位置了
请求报文
GET /index.php HTTP/1.1
Host: blog.maplemark.cn
Accept: *
响应报文
HTTP/1.1 301 Moved Permanently
Server: nginx/1.12.2
Date: Fri, 19 Apr 2019 03:58:59 GMT
Content-Type: text/html; charset=UTF-8
X-Powered-By: PHP/7.2.16
Location: https://blog.maplemark.cn/
请求报文
GET / HTTP/1.1
Host: blog.maplemark.cn
Accept: *
响应报文
HTTP/1.1 200 OK
Server: nginx/1.12.2
Date: Fri, 19 Apr 2019 03:59:34 GMT
Content-Type: text/html; charset=UTF-8
Transfer-Encoding: chunked
Connection: keep-alive
...
状态码 | 原因短语 | 含义 |
---|---|---|
300 | Multiple Choices | 客户端请求一个实际指向多个资源的URL时会返回这个状态码,比如服务器上有某个HTML文档有多个语言版本。返回时会带有一个选项列表,用户可以选择期望使用的那项 |
301 | Moved Permanently | 在请求的 URL 已被移除时使用。响应的 Location 首部中应该包含资源现在所处的 URL |
302 | Found | 与 301 状态码类似;但是,客户端应该使用 Location 首部给出的URL 来临时定位资源。将来的请求仍应使用老的 URL |
303 | See Other | 告知客户端应该用另一个 URL 来获取资源。新的 URL 位于响应报文的 Location 首部。其主要目的是允许 POST 请求的响应将客户端定向到某个资源上去 |
304 | Not Modified | 客户端可以通过所包含的请求首部,使其请求变成有条件的。若用户发起了一个条件 GET 请求,而资源近期未被修改,可以通过该状态码表明。带有这个状态码的响应不应该包含实体的主体部分 |
305 | Use Proxy | 用来说明必须通过一个代理来访问资源;代理的位置由 Location首部给出。客户端是相对某个特定资源来解析这条响应的,不能假定所有请求,甚至所有对持有所请求资源的服务器的请求都通过这个代理进行。如果客户端错误地让代理介入了某条请求,可能会引发破坏性的行为,而且会造成安全漏洞 |
306 | (未使用) | 当前未使用 |
307 | Temporary Redirect | 与 301 状态码类似;但客户端应该使用 Location 首部给出的 URL来临时定位资源。将来的请求应该使用老的 URL |
302、303 和 307 状态码之间存在一些交叉。这些状态码的用法有着细微的差别,大部分差别都源于 HTTP/1.0 和 HTTP/1.1 应用程序对这些状态码
处理方式
的不同,为兼容 HTTP/1.0 而保留了一些状态码(例如 302 状态码)
400 ~ 499,客户端错误状态码
有时客户端会发送一些服务器无法处理
的东西,比如格式错误的请求报文,或者最常见的是,请求一个不存在的 URL
很多客户端错误都是由浏览器来处理的,甚至不会打扰到你。只有少量错误,比如404,还是会穿过浏览器来到用户面前
状态码 | 原因短语 | 含义 |
---|---|---|
400 | Bad Request | 用于告知客户端它发送了一个错误的请求 |
401 | Unauthorized | 与适当的首部一同返回,在这些首部中请求客户端在获取对资源的访问权之前,对自己进行认证 |
402 | Payment Required | 现在这个状态码还未使用,但已经被保留,以作未来之用 |
403 | Forbidden | 用于说明请求被服务器拒绝了。如果服务器想说明为什么拒绝请求,可以包含实体的主体部分来对原因进行描述。但这个状态码通常是在服务器不想说明拒绝原因的时候使用的 |
404 | Not Found | 用于说明服务器无法找到所请求的 URL。通常会包含一个实体,以便客户端应用程序显示给用户看 |
405 | Method Not Allowed | 发起的请求中带有所请求的 URL 不支持的方法时,使用此状态码。应该在响应中包含 Allow 首部,以告知客户端对所请求的资源可以使用哪些方法 |
406 | Not Acceptable | 客户端可以指定参数来说明它们愿意接收什么类型的实体。服务器没有与客户端可接受的 URL 相匹配的资源时,使用此代码。通常,服务器会包含一些首部,以便客户端弄清楚为什么请求无法满足 |
407 | Proxy Authentication Required | 与 401 状态码类似,但用于要求对资源进行认证的代理服务器 |
408 | Request Timeout | 如果客户端完成请求所花的时间太长,服务器可以回送此状态码,并关闭连接。超时时长随服务器的不同有所不同,但通常对所有的合法请求来说,都是够长的 |
409 | Conflict | 用于说明请求可能在资源上引发的一些冲突。服务器担心请求会引发冲突时,可以发送此状态码。响应中应该包含描述冲突的主体 |
410 | Gone | 与 404 类似,只是服务器曾经拥有过此资源。主要用于 Web 站点的维护,这样服务器的管理者就可以在资源被移除的情况下通知客户端了 |
411 | Length Required | 服务器要求在请求报文中包含 Content-Length 首部时使用 |
412 | Precondition Failed | 客户端发起了条件请求,且其中一个条件失败了的时候使用。客户端包含了 Expect 首部时发起的就是条件请求 |
413 | Request Entity Too Large | 客户端发送的实体主体部分比服务器能够或者希望处理的要大时,使用此状态码 |
414 | Request URI Too Long | 客户端所发请求中的请求 URL 比服务器能够或者希望处理的要长时,使用此状态码 |
415 | Unsupported Media Type | 服务器无法理解或无法支持客户端所发实体的内容类型时,使用此状态码 |
416 | Requested Range Not Satisfiable | 请求报文所请求的是指定资源的某个范围,而此范围无效或无法满足时,使用此状态码 |
417 | Expectation Failed | 请求的 Expect 请求首部包含了一个期望,但服务器无法满足此期望时,使用此状态码。如果代理或其他中间应用程序有确切证据说明源端服务器会为某请求产生一个失败的期望,就可以发送这个响应状态码 |
500 ~ 599,服务器错误状态码
有时客户端发送了一条有效请求,服务器自身却出错了。这可能是客户端碰上了服务器的缺陷,或者服务器上的子元素,比如某个网关资源,出了错
代理尝试着代表客户端与服务器进行交流时,经常会出现问题。代理会发布 5XX 服务器错误状态码来描述所遇到的问题
状态码 | 原因短语 | 含义 |
---|---|---|
500 | Internal Server Error | 服务器遇到一个妨碍它为请求提供服务的错误时,使用此状态码 |
501 | Not Implemented | 客户端发起的请求超出服务器的能力范围(比如,使用了服务器不支持的请求方法)时,使用此状态码 |
502 | Bad Gateway | 作为代理或网关使用的服务器从请求响应链的下一条链路上收到了一条伪响应(比如,它无法连接到其父网关)时,使用此状态码 |
503 | Service Unavailable | 用来说明服务器现在无法为请求提供服务,但将来可以。如果服务器知道什么时候资源会变为可用的,可以在响应中包含一个 RetryAfter 首部 |
504 | Gateway Timeout | 与状态码 408 类似,只是这里的响应来自一个网关或代理,它们在等待另一服务器对其请求进行响应时超时了 |
505 | HTTP Version Not Supported | 服务器收到的请求使用了它无法或不愿支持的协议版本时,使用此状态码。有些服务器应用程序会选择不支持协议的早期版本 |
HTTP方法详解
常见的 HTTP 方法
HTTP 请求方法用于告诉服务器要做什么。HTTP 规范中定义了一组常用的请求方法。
例如:GET 方法负责从服务器获取文档,POST 方法会向服务器发送需要处理的数据,OPTIONS 方法用于确定服务器的一般功能,或者服务器处理特定资源的能力
下图描述了7种 HTTP 方法,并不是所有服务器都实现了所有7种方法。有些方法的请求报文中有主体,有些则无主体的请求
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-bviZjyBZ-1676536368585)(./assets/network-http-method.png)]
由于 HTTP 设计易于扩展,除这些方法,其他服务器可能还会实现一些自己的请求方法。这些附加的方法是对 HTTP 规范的扩展,被称为扩展方法
安全方法
HTTP 定义了一组被称为安全方法
的方法。GET 方法和 HEAD 方法都被认为是安全的,这就意味着使用 GET 或 HEAD 方法的 HTTP 请求都不会产生什么动作
不产生动作,在这里意味着 HTTP 请求不会在服务器上产生什么结果。例如,你在 Colin 的五金商店购物时,点击了“提交购买”按钮。点击按钮时会提交一个带有信用卡信息的 POST 请求,那么在服务器上,就会为你执行一个动作。在这种情况下,为购买行为支付信用卡就是所执行的动作
安全方法并不一定是什么动作都不执行的(实际上,这是由 Web 开发者决定的)。使用安全方法的目的就是当使用可能引发某一动作的不安全方法时,允许 HTTP 应用程序开发者通知用户
。在 Colin 的五金商店的例子中,你的 Web 浏览器可能会弹出一条警告消息,说明你正在用不安全的方法发起请求,这样可能会在服务器上引发一些事件(比如用你的信用卡支付费用)
方法详解
GET 方法
GET 是最常用的方法。通常用于请求服务器发送某个资源。HTTP/1.1 要求服务器实现此方法
HEAD 方法
HEAD 方法与 GET 方法的行为很类似,但服务器在响应中只返回首部。不会返回实体的主体部分。这就允许客户端在未获取实际资源的情况下,对资源的首部进行检查。使用 HEAD,可以:
- 在不获取资源的情况下了解资源的情况(比如,判断其类型)
- 通过查看响应中的状态码,看看某个对象是否存在
- 通过查看首部,测试资源是否被修改了
服务器开发者必须确保返回的首部与 GET 请求所返回的首部完全相同。遵循 HTTP/1.1 规范,就必须实现 HEAD 方法
PUT 方法
与 GET 从服务器读取文档相反,PUT 方法会向服务器写入文档。有些发布系统允许用户创建 Web 页面,并用 PUT 直接将其安装到 Web 服务器上去
PUT 方法的语义就是让服务器用请求的主体部分来创建一个由所请求的 URL 命名的新文档,或者,如果那个 URL 已经存在的话,就用这个主体来替代它。
因为 PUT 允许用户对内容进行修改,所以很多 Web 服务器都要求在执行 PUT 之前,用密码登录
POST 方法
POST 方法起初是用来向服务器输入数据的。实际上,通常会用它来支持 HTML 的表单。表单中填好的数据通常会被送给服务器,然后由服务器将其发送到它要去的地方(比如,送到一个服务器网关程序中,然后由这个程序对其进行处理)
TRACE 方法
客户端发起一个请求时,这个请求可能要穿过防火墙、代理、网关或其他一些应用程序。每个中间节点都可能会修改原始的 HTTP 请求
。TRACE 方法允许客户端在最终将请求发送给服务器时,看看它变成了什么样子
TRACE 请求会在目的服务器端发起一个“环回”诊断。行程最后一站的服务器会弹回一条 TRACE 响应,并在响应主体中携带它收到的原始请求报文。这样客户端就可以查看在所有中间 HTTP 应用程序组成的请求/响应链上,原始报文是否,以及如何被毁坏或修改过 TRACE 方法主要用于诊断;也就是说,用于验证请求是否如愿穿过了请求/响应链。它也是一种很好的工具,可以用来查看代理和其他应用程序对用户请求所产生效果
尽管 TRACE 可以很方便地用于诊断,但它确实也有缺点,它假定中间应用程序对各种不同类型请求(不同的方法——GET、HEAD、POST 等)的处理是相同的。很多 HTTP 应用程序会根据方法的不同做出不同的事情——比如,代理可能会将 POST 请求直接发送给服务器,而将 GET 请求发送给另一个 HTTP 应用程序(比如Web 缓存)。TRACE 并不提供区分这些方法的机制。通常,中间应用程序会自行决定对 TRACE 请求的处理方式
TRACE 请求中不能带有实体的主体部分。TRACE 响应的实体主体部分包含了响应服务器收到的请求的精确副本
OPTIONS
OPTIONS 方法请求 Web 服务器告知其支持的各种功能。可以询问服务器通常支持哪些方法,或者对某些特殊资源支持哪些方法。(有些服务器可能只支持对一些特殊类型的对象使用特定的操作)
这为客户端应用程序提供了一种手段,使其不用实际访问那些资源就能判定访问各种资源的最优方式
DELETE
DELETE 方法所做的事情就是请服务器删除请求 URL 所指定的资源。但是,客户端应用程序无法保证删除操作一定会被执行。因为 HTTP 规范允许服务器在不通知客户端的情况下撤销请求
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-I0Dc66Wl-1676536373236)(null)]
扩展方法
HTTP 被设计成字段可扩展的,这样新的特性就不会使老的软件失效了。扩展方法指的就是没有在 HTTP/1.1 规范中定义的方法。服务器会为它所管理的资源实现一些 HTTP 服务,这些方法为开发者提供了一种扩展这些 HTTP 服务能力的手段。下图列出了一些常见的扩展方法实例。这些方法就是 WebDAV HTTP 扩展包含的所有方法,这些方法有助于通过 HTTP 将 Web 内容发布到 Web 服务器上去
并不是所有的扩展方法都是在正式规范中定义的,认识到这一点很重要。如果你定义了一个扩展方法,很可能大部分 HTTP 应用程序都无法理解。同样,你的 HTTP应用程序也可能会遇到一些其他应用程序在用的,而它并不理解的扩展方法
在这些情况下,最好对扩展方法宽容一些。如果能够在不破坏端到端行为的情况下将带有未知方法的报文传递给下游服务器,代理应尝试传递这些报文。如果可能破坏端到端行为则应以 501 Not Implemented(无法实现)状态码进行响应。最好按惯例“对所发送的内容要求严一点,对所接收的内容宽容一些”来处理扩展方法(以及一般的 HTTP 扩展)
HTTPS 细节介绍
HTTPS 是最常见的 HTTP 安全版本
。它得到了很广泛的应用,所有主要的商业浏览器和服务器上都提供 HTTPS。HTTPS 将 HTTP 协议与一组强大的对称、非对称和基于证书的加密技术结合在一起,使得 HTTPS 不仅很安全,而且很灵活,很容易在处于无序状态的、分散的全球互联网上进行管理
HTTPS 加速了因特网应用程序的成长,已经成为基于 Web 的电子商务快速成长的主要推动力。在广域网中对分布式 Web 应用程序的安全管理方面,HTTPS 也是非常重要的
HTTPS 概述
HTTPS 就是在安全的传输层上发送的 HTTP。HTTPS 没有将未加密的 HTTP 报文发送给 TCP,并通过世界范围内的因特网进行传输,它在将 HTTP 报文发送给 TCP 之前,先将其发送给了一个安全层,对其进行加密
现在,HTTP 安全层是通过 SSL 及其现代替代协议 TLS 来实现的。我们遵循常见的用法,用术语 SSL 来表示 SSL 或者 TLS
HTTPS 方案
现在,安全 HTTP 是可选的。因此,对 Web 服务器发起请求时,我们需要有一种方式来告知 Web 服务器去执行 HTTP 的安全协议版本。这是在 URL 的方案中实现的。通常情况下,非安全 HTTP 的 URL 方案前缀为 http,如下所示:
http://blog.maplemark.cn
在安全 HTTPS 协议中,URL 的方案前缀为 https,如下所示:
https://blog.maplemark.cn
请求一个客户端(比如 Web 浏览器)对某 Web 资源执行某事务时,它会去检查 URL 的方案
- 如果 URL 的方案为 http,客户端就会打开一条到服务器端口 80(默认情况下)
的连接,并向其发送老的 HTTP 命令 - 如果 URL 的方案为 https,客户端就会打开一条到服务器端口 443(默认情况下)
的连接,然后与服务器“握手”,以二进制格式与服务器交换一些 SSL 安全参数,
附上加密的 HTTP 命令
SSL 是个二进制协议,与 HTTP 完全不同,其流量是承载在另一个端口上的(SSL 通常是由端口 443 承载的)。如果 SSL 和 HTTP 流量都从端口 80 到达,大部分 Web 服务器会将二进制 SSL 流量理解为错误的 HTTP 并关闭连接。将安全服务进一步整合到 HTTP 层中去就无需使用多个目的端口了,在实际中这样不会引发严重的问题
建立安全传输
在未加密 HTTP 中,客户端会打开一条到 Web 服务器端口 80 的 TCP 连接,发送一条请求报文,接收一条响应报文,关闭连接
由于 SSL 安全层的存在,HTTPS 中这个过程会略微复杂一些。在 HTTPS 中,客户端首先打开一条到 Web 服务器端口 443(安全 HTTP 的默认端口)的连接。一旦建立了 TCP 连接,客户端和服务器就会初始化 SSL 层,对加密参数进行沟通,并交换密钥。握手完成之后,SSL 初始化就完成了,客户端就可以将请求报文发送给安全层了。在将这些报文发送给 TCP 之前,要先对其进行加密
SSL 握手
在发送已加密的 HTTP 报文之前,客户端和服务器要进行一次 SSL 握手,在这个握手过程中,它们要完成以下工作
- 交换协议版本号
- 选择一个两端都了解的密码
- 对两端的身份进行认证
- 生成临时的会话密钥,以便加密信道
在通过网络传输任何已加密的 HTTP 数据之前,SSL 已经发送了一组握手数据来建立通信连接了
这是 SSL 握手的简化版本。根据 SSL 的使用方式,握手过程可能会复杂一些,但总
的思想就是这样
服务器证书
SSL 支持双向认证,将服务器证书承载回客户端,再将客户端的证书回送给服务器。而现在,浏览时并不经常使用客户端证书。大部分用户甚至都没有自己的客户端证书。服务器可以要求使用客户端证书,但实际中很少出现这种情况。
另一方面,安全 HTTPS 事务总是要求使用服务器证书的。在一个 Web 服务器上执行安全事务,比如提交信用卡信息时,你总是希望是在与你所认为的那个组织对话。由知名权威机构签发的服务器证书可以帮助你在发送信用卡或私人信息之前评估你对服务器的信任度。
服务器证书是一个显示了组织的名称、地址、服务器 DNS 域名以及其他信息的 X.509 v3 派生证书。你和你所用的客户端软件可以检查证书,以确保所有的信息都是可信的
站点证书的有效性
SSL 自身不要求用户检查 Web 服务器证书,但大部分现代浏览器都会对证书进行简单的完整性检查,并为用户提供进行进一步彻查的手段。网景公司提出的一种 Web 服务器证书有效性算法是大部分浏览器有效性验证技术的基础。
- 日期检测
首先,浏览器检查证书的起始日期和结束日期,以确保证书仍然有效。如果证书过期了,或者还未被激活,则证书有效性验证失败,浏览器显示一条错误信息
- 签名颁发者可信度检测
每个证书都是由某些证书颁发机构(CA)签发的,它们负责为服务器担保。证书有不同的等级,每种证书都要求不同级别的背景验证。比如,如果申请某个电子商务服务器证书,通常需要提供一个营业的合法证明
任何人都可以生成证书,但有些 CA 是非常著名的组织,它们通过非常清晰的流程来验证证书申请人的身份及商业行为的合法性。因此,浏览器会附带一个签名颁发机构的受信列表。如果浏览器收到了某未知(可能是恶意的)颁发机构签发的证书,那它通常会显示一条警告信息。有些证书会携带到受信 CA 的有效签名路径,浏览器可能会选择接受所有此类证书。换句话说,如果某受信 CA 为“Sam 的签名商店”签发了一个证书,而 Sam 的签名商店也签发了一个站点证书,浏览器可能会将其作为从有效 CA 路径导出的证书接受
- 签名检测
一旦判定签名授权是可信的,浏览器就要对签名使用签名颁发机构的公开密钥,并将其与校验码进行比较,以查看证书的完整性
- 站点身份检测
为防止服务器复制其他人的证书,或拦截其他人的流量,大部分浏览器都会试着去验证证书中的域名与它们所对话的服务器的域名是否匹配。服务器证书中通常都包含一个域名,但有些 CA 会为一组或一群服务器创建一些包含了服务器名称列表或通配域名的证书。如果主机名与证书中的标识符不匹配,面向用户的客户端要么就去通知用户,要么就以表示证书不正确的差错报文来终止连接
SSL/TLS协议运行机制的概述
互联网的通信安全,建立在SSL/TLS协议之上。
本文简要介绍SSL/TLS协议的运行机制。文章的重点是设计思想和运行过程,不涉及具体的实现细节。如果想了解这方面的内容,请参阅RFC文档。
一、作用
不使用SSL/TLS的HTTP通信,就是不加密的通信。所有信息明文传播,带来了三大风险。
窃听风险
(eavesdropping):第三方可以获知通信内容。篡改风险
(tampering):第三方可以修改通信内容。冒充风险
(pretending):第三方可以冒充他人身份参与通信。
SSL/TLS协议是为了解决这三大风险而设计的,希望达到:
- 所有信息都是
加密传播
,第三方无法窃听。 - 具有
校验机制
,一旦被篡改,通信双方会立刻发现。 - 配备
身份证书
,防止身份被冒充。
互联网是开放环境,通信双方都是未知身份,这为协议的设计带来了很大的难度。而且,协议还必须能够经受所有匪夷所思的攻击,这使得SSL/TLS协议变得异常复杂。
二、历史
互联网加密通信协议的历史,几乎与互联网一样长。
1994年,NetScape公司设计了SSL协议(Secure Sockets Layer)的1.0版,但是未发布。
1995年,NetScape公司发布SSL 2.0版,很快发现有严重漏洞。
1996年,SSL 3.0版问世,得到大规模应用。
1999年,互联网标准化组织ISOC接替NetScape公司,发布了SSL的升级版TLS 1.0版。
2006年和2008年,TLS进行了两次升级,分别为TLS 1.1版和TLS 1.2版。最新的变动是2011年TLS 1.2的修订版。
目前,应用最广泛的是TLS 1.0,接下来是SSL 3.0。但是,主流浏览器都已经实现了TLS 1.2的支持。
TLS 1.0通常被标示为SSL 3.1,TLS 1.1为SSL 3.2,TLS 1.2为SSL 3.3。
三、基本的运行过程
SSL/TLS协议的基本思路是采用公钥加密法,也就是说,客户端先向服务器端索要公钥,然后用公钥加密信息,服务器收到密文后,用自己的私钥解密。
但是,这里有两个问题。
- 如何保证公钥不被篡改?
解决方法:将公钥放在数字证书中。只要证书是可信的,公钥就是可信的。
- 公钥加密计算量太大,如何减少耗用的时间?
解决方法:每一次对话(session),客户端和服务器端都生成一个"对话密钥"(session key),用它来加密信息。由于"对话密钥"是对称加密,所以运算速度非常快,而服务器公钥只用于加密"对话密钥"本身,这样就减少了加密运算的消耗时间。
因此,SSL/TLS协议的基本过程是这样的:
客户端向服务器端索要并验证公钥。
双方协商生成"对话密钥"。
双方采用"对话密钥"进行加密通信。
上面过程的前两步,又称为"握手阶段"(handshake)。
四、握手阶段的详细过程
"握手阶段"涉及四次通信,我们一个个来看。需要注意的是,"握手阶段"的所有通信都是明文的。
4.1 客户端发出请求(ClientHello)
首先,客户端(通常是浏览器)先向服务器发出加密通信的请求,这被叫做ClientHello请求。
在这一步,客户端主要向服务器提供以下信息。
支持的协议版本,比如TLS 1.0版。
一个客户端生成的随机数,稍后用于生成"对话密钥"。
支持的加密方法,比如RSA公钥加密。
支持的压缩方法。
这里需要注意的是,客户端发送的信息之中不包括服务器的域名。也就是说,理论上服务器只能包含一个网站,否则会分不清应该向客户端提供哪一个网站的数字证书。这就是为什么通常一台服务器只能有一张数字证书的原因。
对于虚拟主机的用户来说,这当然很不方便。2006年,TLS协议加入了一个Server Name Indication扩展,允许客户端向服务器提供它所请求的域名。
4.2 服务器回应(SeverHello)
服务器收到客户端请求后,向客户端发出回应,这叫做SeverHello。服务器的回应包含以下内容。
确认使用的加密通信协议版本,比如TLS 1.0版本。如果浏览器与服务器支持的版本不一致,服务器关闭加密通信。
一个服务器生成的随机数,稍后用于生成"对话密钥"。
确认使用的加密方法,比如RSA公钥加密。
服务器证书。
除了上面这些信息,如果服务器需要确认客户端的身份,就会再包含一项请求,要求客户端提供"客户端证书"。比如,金融机构往往只允许认证客户连入自己的网络,就会向正式客户提供USB密钥,里面就包含了一张客户端证书。
4.3 客户端回应
客户端收到服务器回应以后,首先验证服务器证书。如果证书不是可信机构颁布、或者证书中的域名与实际域名不一致、或者证书已经过期,就会向访问者显示一个警告,由其选择是否还要继续通信。
如果证书没有问题,客户端就会从证书中取出服务器的公钥。然后,向服务器发送下面三项信息。
一个随机数。该随机数用服务器公钥加密,防止被窃听。
编码改变通知,表示随后的信息都将用双方商定的加密方法和密钥发送。
客户端握手结束通知,表示客户端的握手阶段已经结束。这一项同时也是前面发送的所有内容的hash值,用来供服务器校验。
上面第一项的随机数,是整个握手阶段出现的第三个随机数,又称"pre-master key"。有了它以后,客户端和服务器就同时有了三个随机数,接着双方就用事先商定的加密方法,各自生成本次会话所用的同一把"会话密钥"。
至于为什么一定要用三个随机数,来生成"会话密钥",dog250解释得很好:
"不管是客户端还是服务器,都需要随机数,这样生成的密钥才不会每次都一样。由于SSL协议中证书是静态的,因此十分有必要引入一种随机因素来保证协商出来的密钥的随机性。
对于RSA密钥交换算法来说,pre-master-key本身就是一个随机数,再加上hello消息中的随机,三个随机数通过一个密钥导出器最终导出一个对称密钥。
pre master的存在在于SSL协议不信任每个主机都能产生完全随机的随机数,如果随机数不随机,那么pre master secret就有可能被猜出来,那么仅适用pre master secret作为密钥就不合适了,因此必须引入新的随机因素,那么客户端和服务器加上pre master secret三个随机数一同生成的密钥就不容易被猜出了,一个伪随机可能完全不随机,可是是三个伪随机就十分接近随机了,每增加一个自由度,随机性增加的可不是一。"
此外,如果前一步,服务器要求客户端证书,客户端会在这一步发送证书及相关信息。
4.4 服务器的最后回应
服务器收到客户端的第三个随机数pre-master key之后,计算生成本次会话所用的"会话密钥"。然后,向客户端最后发送下面信息。
编码改变通知,表示随后的信息都将用双方商定的加密方法和密钥发送。
服务器握手结束通知,表示服务器的握手阶段已经结束。这一项同时也是前面发送的所有内容的hash值,用来供客户端校验。
至此,整个握手阶段全部结束。接下来,客户端与服务器进入加密通信,就完全是使用普通的HTTP协议,只不过用"会话密钥"加密内容。