目录
一、TCP 服务的特点
传输层协议主要有 TCP 协议和 UDP 协议,前者相对于后者的特点是:面向连接、字节流和可靠传输。
使用 TCP 协议通信的双方必须先建立连接,然后才能开始数据的读写。双方都必须为该连接分配必要的内核资源,以管理连接的状态和连接上数据的传输。TCP 连接是全双工的,即双方的数据读写可以通过一个连接进行。完成数据交换之后,通信双方都必须断开连接以释放系统资源。
TCP 协议的这种连接是一对一的,所以基于广播和多播(目标是多个主机地址)的应用程序不能使用 TCP 服务,而无连接的 UDP 则非常适用于广播和多播。
- TCP 协议是基于字节流的,基于流的数据没有边界(长度)限制,它源源不断地从通信的一端流入另一端。发送端可以逐个字节地向数据流中写入数据,接收端也可以逐个字节地将它们读出。
- UDP 协议是基于数据报的,每个 UDP 数据报都有一个长度,接收端必须以该长度为最小单位将其所有内容一次性读出,否则数据将被截断。
上面两种区别应用在实际编程中,表现为通信双方是否必须执行相同次数的读、写操作。
- 当发送端应用程序连续执行多次写操作时,TCP 模块现将这些数据放入 TCP 发送缓冲区中;当 TCP 模块真正开始发送数据时,发送缓冲区中这些等待发送的数据可能被封装成一个或多个 TCP 报文段发出。因此 TCP 模块发送出的 TCP 报文段的个数和应用程序执行的写操作次数之间没有固定的数量关系。
- 当接收端收到一个或多个 TCP 报文段后,TCP 模块将他们携带的应用程序数据按照 TCP 报文段的序号一次放入 TCP 接收缓冲区中,并通知应用程序读取数据。接收端应用程序可以一次性将 TCP 接收缓冲区的数据全部读出,也可以分多次读取,这取决于用户指定的应用程序读缓冲区的大小。因此应用程序执行的读操作次数和 TCP 模块接收到的 TCP 报文段个数之间也没有固定的数量关系。
综上所述,发送端执行的写操作次数和接收端执行的读操作次数之间没有任何数量关系,这就是字节流的概念。而对于 UDP ,发送端应用程序每执行一次写操作,UDP 模块就将其封装成一个 UDP 数据报并发送之。接收端必须及时针对每一个 UDP 数据报执行读操作(通过 recvfrom 系统调用),否则就会丢包。并且,如果用户没有指定足够的应用程序缓冲区来读取 UDP 数据,则数据会被截断。
TCP 传输是可靠的:
- 首先,TCP 协议采用发送应答机制,即发送端发送的每个 TCP 报文段都必须得到接收方的应答,才认为这个 TCP 报文段传输成功;
- 其次,TCP 协议采用超时重传机制,发送端在发送出一个 TCP 报文段之后启动定时器,如果在定时时间内未收到应答,它将重发该报文段;
- 最后,因为 TCP 报文段最终是以 IP 数据报发送的,而 IP 数据报到达接收端可能乱序、重复,所以 TCP 协议还会对接收到的 TCP 报文段重排、整理,再交付给应用层。
UDP 协议和 IP 协议一样,提供不可靠服务,都需要上层协议来处理数据确认和超时重传。
二、TCP 结构
- 16 位端口号(port number):告知主机该报文段是来自哪里(源端口)以及传给那个上层协议或应用程序(目的端口)。进行 TCP 通信时,客户端通常使用系统自动选择的临时端口号,而服务器则使用知名服务端口号,定义在 /etc/services 文件中;
- 32 位序号(sequence number):一次 TCP 通信(从建立连接到断开)过程中某一个传输方向上的字节流的每个字节的编号:
- 假设主机 A 和 B 进行通信,A 发送给 B 的第一个报文中,序号之被系统初始化为某个随机值 ISN (Initial Sequence Number,初始序号值);
- 那么在该传输方向上,后续的 TCP 报文段中序号值将被系统设置成 ISN 加上该报文段所携带数据的第一个字节在整个字节流中的偏移;
- 例如存在 ISN ,此时某个报文起始字节为第 1025 字节,那么该报文序号值为:ISN + 1025 。
- 32 位确认号(acknowledgement number):用作对另一方发送来的 TCP 报文段的响应,其值是收到的 TCP 报文段的序号值加 1 :
- 假设两主机通信,A 发送除的 TCP 报文段不仅携带自己的序号,而且包含对 B 发送来的 TCP 报文段的确认号。
- 4 位头部长度(header length):标识该 TCP 头部有多少个 32 位字,因为 32 位最大表示 15 ,因此 TCP 头部最长是 60 字节;
- 6 位标志位:
- URG:标识紧急指针(urgent pointer)是否有效;
- ACK:标识确认号是否有效,若有效则为确认报文段;
- PSH:提示接收端应用程序应该立即从 TCP 接收缓冲区中读走数据,为接受后续数据腾出空间;
- RST:表示要求对方重新建立连接,称携带 RST 标志的 TCP 报文为复位报文段;
- SYN:表示请求建立一个连接,称携带 SYN 标志的 TCP 报文为同步报文段;
- FIN:通知对方本端要关闭连接了,称携带 FIN 标志的 TCP 报文为结束报文段;
- 16 位窗口大小(window size):是 TCP 流量控制的一个字段,此处窗口指接收通告窗口(Receiver Window,RWND),告诉对方本端的 TCP 接收缓冲区还能容纳多少字节的数据,以便对方控制发送数据的速度;
- 16 位校验和(TCP checksum):由发送端填充,接收端对 TCP 报文段执行 CRC 算法以检验 TCP 报文段在传输过程中是否损坏,包括头部和数据部分;
- 16 位紧急指针(urgent pointer):是一个正的偏移量,它和序号字段的值相加表示最后一个紧急数据的下一字节的序号。因此这个字段是紧急指针相对当前序号的偏移,用于发送紧急数据。
- 选项(options):可变长的可选信息:
- kind:说明选项的类型,部分报文只有 kind ,而没有后两项;
- length:指定该选项的总长度,包括 kind 和 length 字段占据的 2 字节;
- info:选项的具体信息。
TCP 选项有 7 种:
|
|
|
|
|
|
|
kind = 0 |
|
|
|
|
|
|
kind = 1 |
|
|
|
|
|
|
kind = 2 |
length = 4 |
最大 segment 长度(2 字节) |
|
|
|
|
kind = 3 |
length = 3 |
移位数(1 字节) |
|
|
|
|
kind = 4 |
length = 2 |
|
|
|
|
|
kind = 5 |
ltngth = N * 8 + 2 |
第 1 块左边沿 |
第 1 块右边沿 |
… |
第 2 块左边沿 |
第 2 块右边沿 |
kind = 8 |
length = 10 |
时间戳值(4 字节) |
时间戳回显应答(4 字节) |
|
|
|
- kind = 0:选项表结束选项;
- kind = 1:空操作选项,没有特殊含义,一般用于将 TCP 选项的总长度填充为 4 字节的整数倍;
- kind = 2:最大报文段长度选项,TCP 连接初始化时,通信双方使用该选项来协商最大报文段长度(Max Segment Size,MSS),通常将其设置为 MTU - 40 字节,减掉的 40 字节表示 TCP 头部和 IP 头部,为避免本机发生 IP 分片,通常将其设置为 1460 字节;
- kind = 3:窗口扩大因子选项,只能出现在同步报文段中,否则将被忽略。TCP 初始化时,通信双方使用该选项协商接收通告窗口的扩大因子。在 TCP 的头部中,接收通告窗口大小是用 16 位表示的,故最大为 65535 字节,但实际上允许接收通告窗口大小远不止这个数,因此采用该因子。假设 TCP 头部中接收通告窗口大小为 N ,窗口扩大因子(移位数)为 M ,那么 TCP 报文段的实际接收通告窗口大小是 N × 2 M N\times2^M N×2M ,或者说 N 左移 M 位,其中 M 取值范围是 0 ~ 14 。内核变量:/proc/sys/net/ipv4/tcp_window_scaling 用于启动或关闭该选项。
- kind = 4:选择性确认选项(Selective Acknowledgment,SACK),TCP 通信时,如果某个报文段丢失,则会重传最后被确认的 TCP 报文段后续的所有报文段,这样原先被正确传输的报文段可能被重复发送,从而降低性能。SACK 能够只重发丢失的报文段。内核变量:/proc/sys/net/ipv4/tcp_sack 用于启动或关闭该选项。
- kind = 5:SACK 实际工作的选项,该选项的参数告诉发送方本端已经收到并缓存的不连续的数据块,从而让发送端可以据此检查并重发丢失的数据块。每个块边沿(edge of block)参数包含一个 4 字节的序号,左边沿表示不连续块的第一个数据的序号,右边沿表示不连续块的最后一个数据的序号的下一个序号。这样一对参数之间的数据是没有收到的。由于一个块信息占用 8 字节,所以 TCP 头部选项中实际上最多可以包含 4 个这样的不连续块。
- kind = 8:时间戳选项,提供了较为准确的计算通信双方之间的回路时间(Round Trip Time,RTT)的时间。内核变量:/proc/sys/net/ipv4/tcp_timestamps 用于启动或关闭该选项。
三、TCP 连接的建立和关闭
本节参考:简单理解 TCP 三次握手四次挥手(看一遍你就懂)
为了保证客户端和服务器端的可靠连接,TCP 必须进行三次握手,这是为了确认双方的接受能力和发送能力是否正常:
最开始的时候双方都处于关闭(CLOSED)状态,主动打开连接的为客户端,被动接受连接的是服务器,TCP 服务器进程需要先创建传输控制块 TCB ,时刻准备接受客户进程的连接请求,此时服务器就进入了 LISTEN 监听状态:
- 第一次握手:TCP 客户进程先创建传输控制块 TCB ,然后向服务器发出连接请求报文,其中 SYN = 1 ,同时选择一个初始序列号 seq = x ,此时 TCP 客户端进入了 SYN-SENT 同步已发送状态;
- 第二次握手:TCP 服务器收到请求报文后,如果同意连接,则会向客户端发出确认报文,其中 ACK = 1 ,SYN = 1 ,确认号 seq = x + 1 ,同时也要为自己初始化一个序列号 seq = y,此时 TCP 服务器进程进入了 SYN-RCVD 同步收到状态;
- 第三次握手:TCP 客户端收到确认后,还要向服务器给出确认,其中 ACK = 1 ,ack = y + 1 ,自己的序列号 seq = x + 1 ,此时 TCP 连接建立,客户端进入 ESTABLISH 已建立连接状态。
进行三次握手的原因如下:
- 第一次握手:客户端向服务器端发送报文,证明客户端的发送能力正常;
- 第二次握手:服务器端接收到报文并向客户端发送报文,证明服务器端的接受能力、发送能力正常;
- 第三次握手:客户端向服务器端发送报文,证明客户端的接受能力正常。
当 TCP 连接结束时,需要进行四次挥手进行终止。数据释放完毕后,双方都可释放连接,最开始时,双方均处于 ESTABLISHED 状态,假设客户端主动关闭,服务器被动关闭:
- 第一次挥手:客户端发出连接释放报文,并且停止发送数据,其中 FIN = 1 ,序列号 seq = u ,此时客户端进入 FIN-WAIT-1(终止等待 1)状态;
- 第二次挥手:服务器端接收到连接释放报文后,发出确认报文,其中 ACK = 1 ,ack = u + 1 ,序列号 seq = v ,此时服务器端进入了 CLOSE-WAIT 关闭等待状态;
- 第三次挥手:客户端接收到服务器的确认请求后,会进入 FIN-WAIT-2(终止等待 2)状态,等待服务器发送连接释放报文。服务器将最后的数据发送完毕后,就像客户端发送连接释放报文,然后进入 LAST-ACK(最后确认)状态,等待客户端的确认;
- 第四次挥手:客户端收到服务器的连接释放报文后,必须发出确认,其中 ACK = 1 ,ack = w + 1 ,序列号 seq = u + 1 ,此时客户端就进入了 TIME-WAIT(时间等待)状态,但此时 TCP 连接还未终止,必须经过 2 MSL(最长报文寿命),当客户端撤销响应的 TCB 后, 客户端才会进入 CLOSED 关闭状态,服务器端接收到确认报文后,会立即进入 CLOSED 关闭状态,到这里连接断开,四次挥手完成。
第四次握手要等待 2MSL 是为了保证给客户端发送的第一个 ACK 报文能到达服务器,因为这个 ACK 报文可能丢失,并且 2MSL 是任何报文在网络上存在的最长时间,超过这个时间报文将被丢弃,这样新的连接中不会出现旧连接的请求报文。
四、复位报文段
在某些特殊条件下,TCP 连接的一端会向另一端发送携带 RST 标志的报文段,即复位报文段,以通知对方关闭连接或重新建立连接:
- 访问不存在的端口:当客户端访问一个不存在的端口(或者处于 TIME_WAIT 状态)时,目标主机将给它发送一个复位报文段,由于该报文段接收通告窗口为 0 ,因此客户端不应回复,而是关闭连接或者重新连接;
- 异常终止连接:给对方发送一个复位报文段后,发送端所有排队等待发送的数据都将被遗弃,接收端收到后应当关闭连接或重新连接;
- 处于半打开连接:当一方因为异常终止了连接,而另一方没有收到结束报文段,此时接收方还维持着原来的连接,而发送方没有该连接的任何信息,此时接收端的状态称为半打开连接。如果发送端往接收端的连接写入数据,则对方回应一个复位报文段。
五、TCP 数据流
TCP 报文所携带的应用程序数据按照长度分为两种:
- 交互数据:包含很少的字节,使用交互数据的应用程序(或协议),对实时性要求较高;
- 成块数据:长度通常为 TCP 报文允许的最大长度,对传输效率要求高,如 ftp 。
1. 交互数据流
服务器在发送确认报文之前,会等待片刻时间,查看本端是否有数据需要发送,如果有则与确认信息一起发出,这被称为延迟确认。而客户端通常由于数据量较小、处理较慢,因此通常直接进行确认而不延迟。
由于广域网数据量大,交互数据流可能经受很大的延迟,并且携带交互数据的微小 TCP 报文段数量一般很多,因此容易导致拥塞发生,可以采用 Nagle 算法进行处理:
- 首先,它要求通信双方在任意时刻最多只能发送一个未被确认的 TCP 报文,在该报文的确认到达之前不能发送其他报文;
- 另外,发送方在等待确认的同时收集本端需要发送的微量数据,并在确认到来时以一个 TCP 报文将他们全部发出。
这样就极大地减少了网络上微小 TCP 报文的数量,该算法另一个优点是:确认到达得越快,数据也就发送得越快。
2. 成块数据流
由于成块数据流对传输效率要求较高,因此发送端通常会连续发送多个 TCP 报文段,接收端可以一次性确认这些报文段。而发送端在收到确认报文后能再发送的报文段数量,取决于接收通告窗口的大小。
服务器每发送 4 个 TCP 报文就传送一个 PSH 标志,以通知客户端的应用程序尽快读取数据。
六、带外数据
有些传输层协议具有带外(Out Of Band,OOB)数据的概念,用于迅速通告对方本端发生的重要事件,因此带外数据比普通数据(带内数据)优先级更高,它应该总是立即被发送,而不论发送缓冲区中是否有排队等待发送的普通数据。带外数据的传输可以使用一条独立的传输层连接,也可以映射到传输普通数据的连接中。
UDP 没有实现带外数据传输,TCP 也没有真正的带外数据,而是利用其头部中的紧急指针标志和紧急指针两个字段,给应用程序提供了一种紧急方式。TCP 的紧急方式利用传输普通数据的连接来传输紧急数据,其含义和带外数据类似,因此可以将其称为带外数据。
七、超时重传
TCP 模块为每个 TCP 报文段都维护一个重传定时器,该定时器在 TCP 报文段第一次被发送时启动。如果超时时间内未收到接收方的应答,则将重传该报文并重置定时器。重传时间和重传次数由不同的超时策略决定:
- /proc/sys/net/ipv4/tcp_retries1 :指定在底层 IP 接管之前 TCP 最少执行的重传次数,默认为 3 ;
- /proc/sys/net/ipv4/tcp_retries2 :指定连接放弃前 TCP 最多可以执行的重传次数,默认为 15 ;
八、拥塞控制
TCP 模块还能够通过拥塞控制来提高网络利用率,降低丢包率,并保证网络资源对每条数据流的公平性,Linux 中有很多种拥塞控制的实现方法,如 reno 、vegas 、cubic 等,存放在 /proc/sys/net/ipv4/tcp_congestion_control 文件中。
拥塞控制的最终受控变量是发送端向网络一次连续写入(收到第一条数据的确认之前)的数据量,称为发送窗口(Send Windows,SWND)。由于发送端最终以 TCP 报文来发送数据,因此 SWND 限定了发送端能连续发送 TCP 报文数量,其中 TCP 报文段的最大长度(数据部分)称为 SMSS(Sender Maximum Segment Size,发送者最大段大小),其值一般等于 MSS 。
发送端需要合理的选择 SWND 的大小,由于接收方可通过其接收通告窗口(RWND)来控制发送端的 SWND ,但是对于拥塞控制来说还需在发送端引入拥塞窗口(Congestion Window,CWND)的状态变量。实际的 SWND 值是 RWND 与 CWND 中的较小者,需要注意的是 CWND 实际上是以字节为单位的:
- 慢启动:TCP 连接建立好后,CWND 被设置为初始值 IW(Initial Window),大小为 2 ~ 4 个 SMSS(新 Linux 内核提高了初始值以减小传输滞后),此时发送端最多能发送 IW 字节的数据。此后发送端每收到接收端的一个确认,其 C W N D + = m i n ( N , S M S S ) CWND += min(N, SMSS) CWND+=min(N,SMSS) ,其中 N 是此次确认中包含的之前未被确认的字节数,这样一来 CWND 将按照指数形式扩大。之所以设计成这样是因为 TCP 模块一开始并不知道网络状况,因此需要一种平滑地方式增加 CWND 。由于指数级增长最终会导致拥塞,因此定义了一个状态变量:慢启动门限(slow start threshold size,ssthresh),当 CWND 的大小超过该值时,TCP 拥塞控制将进入拥塞避免阶段。
- 拥塞避免:将 CWND 指数型增长的方式改为线性增长,有两种方式:
- 每个 RTT 时间内按照指数型计算,而不论该 RTT 时间内发送端收到多少个确认;
- 每收到一个对新数据的确认报文段,如下更新: C W N D + = S M S S × S M S S C W N D CWND+=SMSS\times\frac{SMSS}{CWND} CWND+=SMSS×CWNDSMSS 。
当发生拥塞时,判断依据有两个,倘若第一种情况先发生,则不论第二种情况是否发生都采用第一种处理方式:
- 传输超时,或者说 TCP 重传定时器溢出。此时发生拥塞,采用慢启动和拥塞避免,此时将改变门限值: s s t h r e s h = max ( F l i g h t S i z e 2 , 2 × S M S S ) C W M D ≤ S M S S ssthresh=\max(\frac{FlightSize}{2},2\times SMSS) CWMD\le SMSS ssthresh=max(2FlightSize,2×SMSS)CWMD≤SMSS ,其中 FightSize 是已经发送但未收到确认的字节数;
- 接收到重复的确认报文段。此时采用快速重传和快速恢复。
下面讨论第二种处理方式,由于发送端收到接收端重复确认有多种可能(TCP 报文丢失、接收端收到乱序 TCP 报文段并重排等),因此拥塞算法需要确认丢失原因从而判断网络是否真的发生拥塞:如果连续收到 3 个重复的确认报文段,则认为未发生拥塞,此时采用快速重传和快速恢复策略:
- 当收到第 3 个重复的确认报文段时,按传输超时的方式修改门限值,然后立即重传丢失的报文段,最后设置: C W N D = s s t h r e s h + 3 × S M S S CWND=ssthresh+3\times SMSS CWND=ssthresh+3×SMSS ;
- 每次收到 1 个重复的确认时,设置 C W N D = C W N D + S M S S CWND=CWND+SMSS CWND=CWND+SMSS ,此时发送端可以发送新的 TCP 报文段;
- 当收到新数据的确认时,设置 C W N D = s s t h r e s h CWND=ssthresh CWND=ssthresh 。