后台开发-核心技术与应用实践--TCP协议
网络模型
为使不同计算机厂家的计算机能够互相通信,国际标准化组织 ISO 1981 年正式推荐了一个网络系统结构一一七层参考模型,也叫作开放系统互连模型。
ISO 七层网络模型及其功能展示:
这个七层网络模型在数据的传输过程中还会对数据进行封装,封装过程如图所示:
ISO 七层网络模型中,当一台主机需要传送用户的数据( data 时,数据首先通过应用层的接口进入应用层。在应用层,用户的数据被加上应用层的报头( Ppplication Header, AH ),形成应用层协议数据单元( Protocol Data Unit, PDU ),然后被递交到下层表示层。表示层并不‘关心’上层应用层的数据格式而是把整个应用层递交的数据包看成是一个整体(应用层数据)进行封装,即加上表示层的报头( Presentation Header, PH 然后,递交到下层会话层 同样,会话层、传输层、网络层(假设用 TCP 传输,则是 TCP 数据+ IP 包头)、数据链路层(把上层的 TCP 数据+ IP 头统一称为帧数据,即帧 +帧数据+帧尾( CRC)) 也都要分别给上层递交下来的数据加上自己的报头。它们是:会话层报头( Session Header, SH )、传输层报头( Transport Header, TH )、 网络层报头( Network Header, NH )和数据链路层报头( Data link Header, DH 其中,数据链路层还要给网络层递交的数据加上数据链路层报尾(Data link Termination, DT )形成最终的一帧数据。
当一帧数据通过物理层传送到目标主机的物理层时,该主机的物理层把它递交到上层一一数据链路层。数据链路层负责去掉数据帧的帧头部 DH 和尾部 DT (同时还进行数据校验)。如果数据没有出错,则递交到上层网络层。同样,网络层、传输层、会话层、表示层、
应用层也要做类似的工作。最终,原始数据被递交到目标主机的具体应用程序中。
五层网络模型:
- 应用层:确定进程之间通信的性质以满足用户需求。应用层协议有很多,如支持万维网应用的 http 协议、支持电子邮件的 SMTP 协议、支持文件传送的句协议,等等
- 运输层:负责主机间不同进程的通信。这一层中的协议有面向连接的 TCP (传输控制协议)、无连接的 UDP (用户数据报协议);数据传输的单位称为报文段或用户数据报
- 网络层:负责分组交换网中不同主机间的通信。作用为:发送数据时,将运输层中的报文段或用户数据报封装成 IP 数据报,并选择合适路由
- 数据链路层:负责将网络层的 IP 数据报组装成帧
- 物理层:透明地传输比特流
使用最广泛的为四层模型--TCP/IP 分层模型(TCP/IP Layering Model)。它有因特网分层模型( Internet Layering Model )和因特网参考模型( nternet Reference Model )之称。
四层网络模型表示:
TCP/IP 分层模型的4个协议层分别完成以下的功能:
- 网络接口层
网络接口层包括用于协作 IP 数据在已有网络介质上传输的协议 实际上 TCP/IP 标准并不定义与 ISO 数据链路层和物理层相对应的功能。相反,它定义了像 APP ( Address Resolution Protocol ,地址解析协议)这样的协议,提供 TCP/IP 协议的数据结构和实际物理硬件之间的接口。
- 网间层
网间层对应于 OSI 七层参考模型的网络层。本层包含 IP 协议、RIP 协议( Routing Information Protocol ,路由信息协议),负责数据的包装、寻址和路由。同时还包含 ICMP (Internet Control Message Protocol ,网间控制报文协议)用来提供网络诊断信息
- 传输层
传输层对应于 OSI 七层参考模型的传输层,它提供两种端到端的通信服务。其中 TCP 协议( Transmission Control Protocol )提供可靠的数据流运输服务, UDP 协议( Use Datagram Protocol )提供不可靠的用户数据报服务
- 应用层
应用层对应于 OSI 七层参考模型的应用层和表示层。因特网的应用层协议包括 Finger Who is FTP (文件传输协议) Gopher HTTP (超文本传输协议)、 Telent (远程终端协议)、SMTP (简单邮件传送协议)、 IRC (因特网中继会话)、NNTP (网络新闻传输协议)等
综上,TCP 协议在网络 OSI 的七层模型中的第四层传输层, IP 协议在第三层网络层, ARP 协议在第二层数据链路层;在第二层上的数据叫 Frame ,在第三层上的数据叫 Packet ,第四层的数据叫 Segment。所有程序的数据首先会打包到 TCP Segment 中,然后 TCP Segment 会打包到 IP Packet ,然后再打包到以太网 Ethernet Frame 中,传到对端后,各个层解析自己的协议,然后把数据交给更高层的协议处理。
TCP头部
TCP头部格式如下:
- 16位端口号:告知主机该报文段是来自哪里(源端口)以及传给哪个上层协议或应用程序(目的端口)的
- 32位序列号:TCP 通信(从 TCP 连接建立到断开)过程中某一个传输方向上的字节流的每个字节的编号
- 32位确认号:用作对另一方发送来的 TCP 报文段的响应,其值是收到的 TCP 报文段的序号值加一。假设主机A和主机B进行 TCP 通信,那么A发送出的 TCP 报文段不仅携带自己的序号,而且包含对B发送来的 TCP 报文段的确认号。反之,B发送出的 TCP 报文段也同时携带自己的序号和对A发送来的报文段的确认号。
- 4位头部长度::标识该 TCP 头部有多少个 32bit (4 Byte)。 因为最大能表示 15 ,所以 TCP 头部最长是 60 Byte。
- 6位标志位包含如下几项:
- URG 标志,表示紧急指针(urgent pointer )是否有效
- ACK 标志,表示确认号是否有效,一般称携带 ACK 标志的 TCP 报文段为“确认报文段”
- PSH 标志,提示接收端应用程序应该立即从 TCP 接收缓冲区中读走数据,为接收后续数据腾出空间(如果应用程序不将接收到的数据读走,它们就会一直停留在 TCP 接收缓冲区中)
- RST 标志,表示要求对方重新建立连接,一般称携带 RST 标志的 TCP 报文段为“复位报文段”
- SYN 标志,表示请求建立一个连接,一般称携带 SYN 标志的 TCP 报文段为“同步报文段”
- FIN 标志,表示通知对方本端要关闭连接了,一般称携带 FIN 标志的 TCP 报文段为“结束报文段”
- 16 位窗口大小( window size ):是 TCP 流量控制的一个手段。这里说的窗口,指的是接收通告窗口 (Receiver Window), RWND 它告诉对方本端的 TCP 接收缓冲区还能容纳多少字节的数据,这样对方就可以控制发送数据的速度
- 16 位校验和(TCP checksum ):由发送端填充,接收端对 TCP 报文段执行 CRC 算法,以检验 TCP 报文段在传输过程中是否损坏。注意,这个校验不仅包括 TCP 头部,也包括数据部分。这也是 TCP 可靠传输的一个重要保障
- 16 位紧急指针( urgent pointer ):是一个正的偏移量。它和序号字段的值相加表示最后一个紧急数据的下一字节的序号。因此,确切地说,这个字段是紧急指针相对当前序号的偏移,不妨称之为“紧急偏移”。TCP 的紧急指针是发送端向接收端发送紧急数据的方法。
需要注意以下几点:
- TCP 的包是没有 IP 地址的,那是 IP 层上的事,但是有源端口和目的端口
- 一个TCP 连接需要四个元组(src_ip, src_port, dst_ip, dst_port) 来表示是同一个连接。准确说是五元组,还有一个是协议
- Sequence Number 是包的序号,用来解决网络包乱序(reordering )问题
- Acknowledgement Number 就是 ACK ,用于确认收到,用来解决不丢包的问题
- Window Advertised Window ,也就是著名的滑动窗口 Sliding Window ),用于解决流控问题
- TCP Flag ,也就是包的类型,主要是用于操控 TCP 的状态机的
TCP的三次握手与四次挥手
TCP 连接的建立可以简单地称为3次握手,而连接的中止则可以称为4次握手
- 第一次握手:建立连接时,客户端发送 SYN 包( SYN )到服务器,并进入 SYN_SEND 状态,等待服务器确认
- 第二次握手:服务器收到 SYN 包,必须确认客户的 SYN ( ACK=J+ 1 ),同时自己也发送一个 SYN 包( SYN=K ),即 SYN+ACK 包,此时服务器进入 SYN_RECV 状态
- 第三次握手:客户端收到服务器的 SYN+ACK 包,向服务器发送确认包 ACK(ACK=K+l ),此包发送完毕,客户端和服务器进入 ESTABLISHE状态,完成三次握手
完成三次握手,客户端与服务器开始传送数据,也就是 ESTABLISHED 状态
结束连接:
TCP 一个特别的概念叫作半关闭,这个概念是说,TCP 的连接是全双工(可以同时发送和接收)连接,因此在关闭连接的时候,必须关闭传和送两个方向上的连接。客户机给服务器 FIN的TCP 报文,然后服务器返回给客户端一个确认 ACK 报文,并且发送一个 FIN 报文,当客户机回复 ACK 报文后(4次握手),连接就结束了
为什么建连接要3次握手,断连接需要4次挥手?
对于建连接的3次握手,主要是要初始化 Sequence Number 的初始值。通信的双方要互相通知对方自己的初始化的 Sequence Numbe -- 所以叫 SYN。 也就上图中的J 和 K。这个号要作为以后的数据通信的序号,以保证应用层接收到的数据不会因为网络上的传输问题而乱序( TCP 会用这个序号来拼接数据)
对于4次挥手,其实仔细看则是两次,因为 TCP 是全双工的,所以,发送方和接收方都需要 FIN ACK 只不过,有一方是被动的,所以看上去就成了所谓的4次挥手。如果两边同时断连接,那就会就进入到 CLOSING 状态,然后到达 TIME_WAIT状态
TCP状态图
- CLOSED:表示初始状态
- LISTEN:表示服务器端的某个 socket 处于监昕状态,可以接受连接
- SYN_SENT:在服务端监听后,客户端 socket 执行 CONNECT 连接时,客户端发送 SYN 报文,此时客户端就进入 SYN_SENT 状态,等待服务端的确认
- 表示服务端接收到了SYN 报文
- ESTABLISHED:表示连接已经建立了
- FIN_WAIT_1:这个是已经建立连接之后,其中一方请求终止连接,等待对方的 FIN 报文
- FIN_WAIT_2:实际上 FIN_WAIT_2 状态下的 socket ,表示半连接,即有一方要求关闭连接,但另外还告诉对方:我暂时有一点数据需要传送给你,请稍后再关闭连接
- TIME_WAIT:表示收到了对方的 FIN 报文,并发送出了 ACK 报文,就等 2MSL后即可回到 CLOSED 可用状态了。如果在 FIN_WAIT 状态下,收到了对方同时带 FIN 标志和ACK 标志的报文时,可以直接进入到 TIME_WAIT 状态,而无需经过 FIN_WAIT_2 状态
- CLOSING:属于一种比较罕见的例外状态,当发送 FIN 报文后,按理来说是应该先收到(或同时收到)对方的 ACK 报文,再收到对方的 FIN 报文。但是 CLOSING 状态表示你发送 FIN 报文后,并没有收到对方的 ACK 报文,反而收到了对方的 FIN 报文。如果双方几乎在同时关闭一个 socket 的话,那么就出现了双方同时发送 FIN 报文的情况,就会出现 CLOSING 状态,表示双方都正在关闭 socket 连接。
- CLOSE_WAIT :这种状态的含义其实是表示在等待关闭
- LAST_ACK :这个状态还是比较好理解的,它是被动关闭一方在发送 FIN 报文后,最后等待对方的 ACK 报文
- CLOSED 当收到 ACK 报文后,也即可以进入到 CLOSED 可用状态了
2MSL 等待状态:有一个 TIME_WAIT 等待状态,这个状态又叫作 2MSL状态,说的是在 TIME_WAIT_2 发送了最后一个 ACK 数据报以后,要进入 TIME_WAIT状态。这个状态是防止最后一次握手的数据报没有传送到对方那里而准备的
FIN_WAIT_2 状态:这就是著名的半关闭状态了,这是在关闭连接时,客户端和服务器两次握手之后的状态。在这个状态下,应用程序还有接收数据的能力,但是已经无法发送数据,但是也有一种可能是,客户端一直处于 FIN_WAIT 状态,而服务器则一直处于 WAIT_CLOSE 状态,直到应用层来决定关闭这个状态。
TCP超时重传
下图给出了正常与3中异常的网络传输情况:
当出现以上异常情况时,TCP就会超时重传。TCP 每发送一个报文段,就对这个报文段设置一次计时器,只要计时器设置的重传时间到了,但还没有收到确认,就要重传这一报文段,这个就叫作 超时重传
TCP 协议必须适应两个方面的时延差异:一个是达到不同目的端的时延的差异;另一个是统一连接上的传输时延随业务量负载的变化而出现的差异。为此, TCP 协议使用自适应算法( Adaptive Retransmission Algorithm )以适应互联网分组传输时延的变化。
这种算法的基本要点是 TCP 监视每个连接的性能(即传输时延),由每一个 TCP 的连接情况推算出合适的 RTO 值, 当连接时延性能变化时, TCP 也能够相应地自动修改 RTO 的设定,以适应这种网络的变化。
注:RTO ( Retransmission Timeout ,重传超时时间),指发送端发送数据后、重传数据前等待接受方收到该数据报文的 ack 时间
为了动态地设置, TCP 引入了 RTT (Round Trip Time ),也就是连接往返时间,指发送端从发送 TCP 包开始到接收它的立即响应所耗费的传输时间。
自适应重传算法的关键就在于对当前 RTT 的准确估计,以便适时调整 RTO。
RFC793 中定义的经典算法是这样的: 1. 先采样 RTT ,记下最近几次的 RTT 值;2. 然后做平滑计算 SRTT (Smoothed RTT)。公式中的 \(\alpha\) 取值为 0.8-0.9 ,这个算法叫加权移动平均:
UBOUND 是最大的 timeout 时间,上限值; LBOUND 最小的 timeout 时间 ,下限值;\(\beta\) 取值1.3-2.0
TCP 滑动窗口
TCP 的滑动窗口主要有两个作用:一是提供 TCP 的可靠性;二是提供 TCP 的流控特性。同时滑动窗口机制还体现了 TCP 面向字节流的设计思路
TCP 头部中滑动窗口所处的位置:
TCP 的窗口是 16bit 位字段它代表的是窗口的字节容量,也就是 TCP 的标准窗口最大为 \(2^{16}-1 = 65535\)字节
TCP 的选项字段中还包含了 TCP 窗口扩大因子, option-kind为3, option-length为3, option-data 取值范围 0-14。窗口扩大因子用来扩大 TCP 窗口,可把原来 16bit 的窗口,扩大为 32 bit。
对于 TCP 话的发送方,任何时候在其发送缓存内的数据都可以分为4类:1. 已经发送并得到对端 ACK;2. 发送但还未收到对端 ACK;3. 未发送但对端允许发送; 4. 未发送且对端不允许发送。其中“已发送但还未收到对端 ACK 的”和“未发送但对端允许发送的”这两部分数据称之为发送窗口
对于 TCP 的接收方,在某一时刻在它的接收缓存内存在3种状态:1. 已接收 2. 未接收准备接收 3. 未接收且为准备接收。其中“未接收准备接收”称之为接收窗口
TCP是双工协议,会话的双方都可以同时接收、发送数据 TCP 会话的双方都各自维一个“发送窗口”和一个“接收窗口”。其中各自的“接收窗口”大小取决于应用、系统、硬件的限制(TCP 传输速率不能大于应用的数据处理速率),各自的“发送窗口”则要求取决于对端通告的“接收窗口”,要求相同
滑动窗口实现面向流的可靠性来源于“确认重传”机制。TCP 的滑动窗口的可靠性也是建立在“确认重传”基础上的。
TCP拥塞控制
TC 的拥塞控制由4个核心算法组成:慢开始(Slow Start)、拥塞避免(Congestion Voidance)、快速重传(Fast Retransmit)和快速恢复(Fast Recovery)
发送方维持一个叫作拥塞窗口 cwnd ( congestion window )的状态变量。拥塞窗口的大小取决于网络的拥塞程度,并且动态地在变化,发送方让自己的发送窗口等于拥塞窗口,另外考虑到接受方的接收能力,发送窗口可能小于拥塞窗口
慢开始算法的思路就是,不要一开始就发送大量的数据,先探测一下网络的拥塞程度,也就是说由小到大逐渐增加拥塞窗口的大小
慢开始的原理如下所述:
- 当主机开始发送数据时,如果立即将较大的发送窗口的全部数据字节都注入到网络中,那么由于不清楚网络的情况,有可能引其网络拥塞
- 比较好的方法是试探一下,即从小到大逐渐增大发送端的拥塞控制窗口数值
- 在刚刚开始发送报文段时可先将拥塞窗口 cwnd 设置为一个最大报文段的 MSS 的数值。在每收到一个对新报文段确认后,将拥塞窗口增加至多 MSS 的数值,当 rwind 足够大的时候,为了防止拥塞窗口 cwind 的增长引起网络拥塞,还需要另外一个变量,即慢开始门限 ssthresh
ssthresh的用法如下:(发送方每收到一个确认就把窗口 cwnd+l)
拥塞避免算法让拥塞窗口缓慢增长,即每经过一个往返时间 RTT 就把发送方的拥塞窗口cwnd 加1,而不是加倍
拥塞控制具体过程如下所述:
- TCP 连接初始化,将拥塞窗口设置为1
- 执行慢开始算法, cwind 按指数规律增长,直到 cwind = ssthress 时,开始执行拥塞避免算法, cwnd 按线性规律增长
- 当网络发生拥塞,把 ssthresh 更新为拥塞前 ssthresh 值的一半, cwnd 重新设置为1,按照步骤(2)执行
慢开始和拥塞避免算法的实现举例:
快重传和快恢复
快重传要求接收方在收到一个失序的报文段后就立即发出重复确认(为的是使发送方及早知道有报文段没有到达对方),而不要等到自己发送数据时捎带确认。快重传算法规定,发送方只要一连收到3个重复确认就应当立即重传对方尚未收到的报文段,而不必继续等待设置的重传计时器时间到期。
快重传示意图:
快重传配合使用的还有快恢复算法,有以下两个要点:
- 当发送方连续收到三个重复确认时,就执行“乘法减小”算法,把 ssthresh 门限减半,但是接下去并不执行慢开始算法
- 考虑到如果网络出现拥塞的话就不会收到好几个重复的确认,所以发送方现在认为网络可能没有出现拥塞。所以此时不执行慢开始算法,而是将 cwnd 设置为 ssthresh 的大小,然后执行拥塞避免算法
从连续收到3个重复的确认转入拥塞避免示意图:
从整体上来讲, TCP 拥塞控制窗口变化的原则是加法增大、乘法减小
TCP网络编程
在网络中,进程使用三元组 (Ip地址,协议,端口)来标识网络中的进程,网络中的进程通信就可以利用这个标志与其他进程进行交互。
网络中的进程是通过 socket 来通信的,用 "打开 (open) -> 读写 (write/read) -> 关闭 (close)" 模式来操作。 socket 是一种特殊的文件, 一些 socket 函数就是对其进行的操作(读/写 打开、关闭)。
使用 TCP/IP 协议的应用程序通常采用应用编程接口: UNIX BSD 的套接字 (socket),来实现网络进程之间的通信。
以 TCP 协议通信的 socket 为例,其交互流程大概如图所示:
具体流程如下:
- 服务器根据地址类型( ipv4, ipv6 )、 socket 类型、协议创建 socket
- 服务器为 socket 绑定 IP 地址和端口号
- 服务器 socket 监听端口号请求,随时准备接收客户端发来的连接,这时候服务器的 socket 并没有被打开
- 客户端创建 socket
- 客户端打开 socket,根据服务器 IP 地址和端口号试图连接服务器 socket
- 服务器 socket 接收到客户端 socket 请求,被动打开,开始接收客户端请求,直到客户端返回连接信息,这时候 socket 进入阻塞状态,所谓阻塞即 accept()方法一直到客户端返回连接信息后才返回,开始接收下一个客户端连接请求
- 客户端连接成功,向服务器发送连接状态信息
- 服务器 accept 方法返回,连接成功
- 客户端向 socket 写入信息
- 服务器读取信息
- 客户端关闭
- 服务器端关闭
TCP 头部的选项部分是为了 TCP 适应复杂的网络环境和更好地服务于应用层而进行设计的。TCP 选项部分最长可以达到 40 Byte ,再加上 TCP 选项外的固定的 20 Byte 部分, TCP的最长头部可达 60 Byte。TCP 头部长度可以通过 TCP 头部中的“数据偏移”位来查看。值得注意的是: TCP 偏移量的单位是 32bit ,也就是 4Byte。TCP 偏移量共占 4bite,取最大 llll (B) 计算也就是十进制的 15。15*4 Byte=60 Byte,这个也是 TCP 首部不超过 60 Byte 的原因。
网络字节序与主机序
不同的 CPU 有不同的字节序类型,这些字节序是指整数在内存中保存的顺序,称为主机序。最常见的有两种:1. Little Endian ,将低序字节存储在起始地址;2. Big Endian,将高序字节存储在起始地址
大小端存储数字 Oxl2345678 示意图:
C/C++ 语言编写的程序里数据存储顺序是跟编译平台所在的 CPU 相关的,而 Java 编写的程序则唯一采用 Big Endian 方式来存储数据,所有网络协议也都是采用 Big Endian 的方式来传输数据的。所以有时也会把 Big Endian 方式称之为网络字节序。当两台采用不同字节序的主机通信时,在发送数据之前都必须经过字节序的转换成为网络字节序后再进行传输。
TCP 是个“流”协议,所谓流,就是没有界限的一串数据。在进行数据传输时,由于网络或Nagle 算法的原因会导致“粘包”的情况,即,加上要同时发送A和B数据,A中的部分数据和B一起被接收或B中的部分数据和A一起被接收。可使用封包和拆包的方法来解决该问题。由于UDP本身就是个数据包协议,因此不存在该问题。
封包就是给一段数据加上包头,这样一来数据包就分为包头和包体两部分内容了。包头其实上是个大小固定的结构体,其中有个结构体成员变量表示包体的长度,这是个很重要的变量,其他的结构体成员可根据需要自己定义。根据固定的包头长度以及包头中含有的包体长度的变量值就能正确的拆分出一个完整的数据包。所以为了解决“粘包”的问题,大家通常会在所发送的内容前加上发送内容的长度,所以
对方就会先收4 Byte,解析获得接下来需要接收的长度,再进行收包。