- 全双工
- 点对点(一对一)
- 可靠数据传输
- 传输字节流(把应用层数据看成一连串的无结构的字节序列,TCP 不知道所传送字节流的含义)
- 拥有拥塞控制机制
TCP 报文由首部字段和数据字段两部分组成。
用于多路复用/分解
用于实现可靠数据传输服务。TCP 字节流每一个字节都按顺序编号。确认号是接收方期望从发送方收到的下一字节的序号。
告诉对方自己的接收窗口大小。
发送方维护一个称为接收窗口(rwnd)的变量用来提供流量控制,提示接收方还有多少可用的缓存空间。
接收窗口 = 接收方缓存 - (接收方已放入接收缓存中的数据流的最后一个字节编号 - 接收方应用程序已读取的最后一个字节编号)。
TCP 为每个连接设有一个持续计时器,只要收到零窗口(接收窗口为零,表示没有缓存空间了)通知,就启动计时器。若计时器到期了,就发送一个零窗口 探测报文(携带 1 字节数据),对方确认时会给出现在的窗口值,如果窗口仍是零,重新启动计时器。
指出 TCP 报文段的数据起始处距离 TCP 报文段的起始处有多远,实际上指出了 TCP 报文段的首部长度。
用于双方协商最大报文段长度(MSS)时使用,或在高速网络环境下用作窗口调节因子时使用。
PSH URG 一般不使用
- ACK 用于指示确认字段中的值是有效的 ACK=1 有效 ACK=0 无效。
- RST 重置报文段,释放连接,再重新建立连接。还用来拒绝打开一个连接或非法报文段。
- SYN 用于连接建立,SYN 为 1 时,表明这是一个请求建立连接报文。
- FIN 用于连接拆除,FIN 为 1 时,表明发送方数据已发送完毕,并要求释放连接。
- PSH 被设为 1 时,表示接收方应立即将数据交给上层,而不要等缓存填满了再上交。
- URG 表示报文段存在被发送方上层置为“紧急”的数据
长度可变,最多 40 字节,没有选项时,TCP 首部长度为 20 字节。
-
选项字段有一个窗口扩大选项,占 3 字节。其中有一个字节表示移位值 S,新的窗口值等于 TCP 首部中的窗口位数从 16 增大到 16 + S。移位值最大值为 14, 相当于窗口最大值增加到 2**30 - 1。窗口扩大选项在双方连接建立时进行协商,如果不需要扩大窗口,发送 S = 0,使窗口大小回到 16 位。
-
时间戳选项占 10 字节,其中时间戳值字段(4 字节),时间戳回送回答字段(4 字节)。时间戳选项有两个功能,一是用来计算往返时间 RTT;二是用于处理 TCP 序号超过 2**32 的情况,这称为防止序号绕回 PAWS。当序号超过上限值时,会从 0 重新开始计算。
TCP 标准规定,ACK 报文段可以携带数据,但不携带数据就不用消耗序号。
- 客户端发送一个不包含应用层数据的 TCP 报文段,首部的 SYN 置为 1,随机选择一个初始序号放在 TCP 报文段的序号字段中。(SYN 为 1 的时候,不能携带数据,但要消耗掉一个序号)
- TCP 报文段到达服务器主机后,提取报文段,为该 TCP 连接分配缓存和变量,并向客户端发送允许连接的 TCP 报文段(不包含应用层数据)。 这个报文段的首部包含 3 个信息:SYN 置为 1;确认号字段置为客户端的序号 + 1;随机选择自己的初始序号。
- 收到服务器的 TCP 响应报文段后,客户端也要为该 TCP 连接分配缓存和变量,并向服务器发送一个报文段。这个报文段将服务器端的序号 + 1 放置在确认号字段中, 用来对服务器允许连接的报文段进行响应,因为连接已经建立,所以 SYN 置为 0。 最后一个阶段,报文段可以携带客户到服务器的数据。并且以后的每一个报文段,SYN 都置为 0。
FIN 报文段即使不携带数据,也要消耗序号。
- 客户端发送一个 FIN 置为 1 的报文段。
- 服务器回送一个确认报文段。
- 服务器发送 FIN 置为 1 的报文段。
- 客户端回送一个确认报文段。
TCP 发送方仅需维护已发送但未确认的字节的最小序号(SendBase)和下一个要发送的字节的序号(NextSeqNum)。
- 客户进程通过套接字将数据传给 TCP,TCP 将数据放到该连接的发送缓存,接下来 TCP 会不时的从发送缓存里取出一块数据发送。
- 发送时 TCP 会为数据加上 TCP 首部,从而形成报文段。
- 报文段下发到网络层,网络层将其封装成 IP 数据报,最后经历链路层、物理层发送到网络中。
TCP 接收方会将正确接收但失序的报文缓存起来。
- 接收时从物理层、链路层、网络层一路解包,最后到达传输层。
- TCP 接收后会放到该连接的接收缓存,
- 应用程序通过套接字从此缓存读取数据流。
有三种发送机制
- TCP 维持一个变量,它等于 MSS,只要缓存中的数据达到 MSS 字节时,就组装成一个 TCP 报文段发送出去。
- 由发送方的应用进程指明要求发送报文段,即 TCP 支持的推送操作。
- 发送方的一个计时器时限到了,这时就把当前已有的缓存数据装入报文段(不能超过 MSS)发送出去。
在 TCP 的实现中广泛采用 Nagle 算法
- 发送方把第一个字节发送出去,把后面到达的字节缓存起来
- 在收到对第一个字节的确认后,再把缓存中的数据组装成一个报文段发送出去,随后继续缓存数据
- 只有收到对前一个报文段的确认后,才会继续发送下一个报文段
- 另外,如果缓存数据达到发送窗口的一半或者达到报文段的最大长度时,就立即发送一个报文段
Nagle 算法会引发几种 HTTP 性能问题。首先,小的 HTTP 报文可能无法填满一个分组,可能会因为等待那些永远不会到来的确认分组而产生时延。 其次,Nagle 算法与延迟确认之间的交互存在问题——Nagle 算法会阻止数据的发送,直到收到一个确认分组,但确认分组自身会被延迟确认算法延迟 100 ~ 200 毫秒。
HTTP 应用程序可以在自己的栈中设置参数 TCP_NODELAY,禁用 Nagle 算法,提高性能。
最大报文段长度,指报文段应用层数据的最大长度,不包括 TCP 头部,MSS 值和接收窗口没有关系。
最大链路层帧长度,以太网和 PPP 链路层协议具有 1500 字节的 MTU,因此 MSS 的典型值为 1460 字节(TPC/IP 首部长度通常为 40 字节)。
指从源到目的地的所有链路层上,所能发送的最大链路层帧
TCP 只确认从流开始到第一个丢失字节为止的字节,所以被称为累积确认。
假如发送方发送了 0-10,11-20,21-30 三个报文段,接收方由于某种原因只收到 0-10 和 21-30 字段,所以它下一个报文段的确认号为 11,表示期望收到字节 11(及以后)。
接收方不必对每个接收到的分组逐个发送确认,而是在收到几个分组后,对按序到达的最后一个分组发送确认,表示到这个分组为止的所有分组都已收到了。
要使用选择确认,必须在建立连接时,在首部选项中加上允许 SACK 的选项,双方都协商好。然而 SACK 文档没有指明发送方要怎么响应 SACK,所以大多数的实现还是重传所有 未被确认的数据块。
TCP 重传具有最小序号的还未被确认的报文段。
它被设置为平均往返时间加上一点余量。重传发生后,下一次的超时间隔被设置为之前的两倍。
当接收方接收到一个比期望序号大的失序报文时,说明有报文段丢失,接收方将对最后一次按序字节数据进行重复确认(产生一个冗余 ACK)。 因为发送方是流水线式(一个接一个)发送报文,如果一个报文段丢失,很可能会引起多个冗余的 ACK。当发送方接收到对相同数据的 3 个冗余 ACK 时, 表明这个确认过 3 次的报文段已经丢失,发送方就执行快速重传,即在定时器过期之前重传丢失的字段。
拥塞控制就是防止过多的数据注入网络,这样可以使网络中的路由或链路不过载。拥塞控制是一个全局性的过程,涉及所有的主机、路由器, 以及与降低网络传输性能有关的所有因素。
而流量控制是指点到点通信量的控制,是个端到端的问题。流量控制抑制发送端的发送速率,以使接收端来得及接收。
如何判断发生了拥塞?TCP 发生丢包就是出现了拥塞。
丢包事件:超时或者收到接收方的 3 个冗余 ACK。
发送方维护的一个额外变量,它对 TCP 发送流量速率进行了限制,在发送方中未被确认的数据量不会超过 cwnd 和 rwnd 中的最小值。 TCP 使用确认(成功收到接收方的 ACK)来触发增大它的拥塞窗口长度。
慢启动和拥塞避免是 TCP 的强制部分,两者的区别在于对收到的 ACK 做出反应时增加 cwnd 长度的方式。 慢启动比拥塞避免能更快地增加 cwnd 的长度。快速恢复是推荐部分,对于 TCP 发送方不是必需的。
慢启动 cwnd 初始值设为较小的值,每收到一个接收方的 ACK,就将 cwnd 的值翻倍(指数增长)。
结束慢启动有两种方式,一是当检查到拥塞时,这时将 ssthresh(慢启动阈值) 设为 cwnd/2,然后将 cwnd 置为 1 个 MSS,并重新开启慢启动。 二是到达或超过 ssthresh 值时,再翻倍就超过 cwnd 的值了,所以这时结束慢启动进入拥塞避免模式。
拥塞避免模式,每个 RTT 将 cwnd 的值增加一个 MSS。
结束拥塞避免有两种方式,第一种是超时,它和结束慢启动的方式一相同。第二种是收到 3 个冗余 ACK 时,将 ssthresh 置为 cwnd/2, 然后 cwnd = ssthresh + 3 * MSS, 接下来进入快速恢复模式。
在快速恢复阶段,每收到重复的ACK,则cwnd加1;收到非重复ACK时,置cwnd = ssthresh, 转入拥塞避免阶段;如果发生超时重传,则置ssthresh为当前cwnd的一半,cwnd = 1,重新进入慢启动阶段。
快速恢复阶段退出条件:收到非重复ACK。
step1:
if ( dupacks >= 3 ) {
ssthresh = max( 2 , cwnd / 2 ) ;
cwnd = ssthresh + 3 * SMSS ;
}
step2:重传丢失的分组
step3:此后每收到一个重复的ACK确认时,cwnd++
step4:当收到对新发送数据的ACK确认时,cwnd = ssthresh,这个ACK能够对那些在
丢失的分组之后,第一个重复ACK之前发送的所有包进行确认。
发送窗口里面的序号表示允许发送的序号,只有收到接收方的确认报文段,才能往前移动(继续发送序号大的报文)。
套接字 socket = (IP 地址:端口号)
TCP 连接 = { (ip1:port1), (ip2:port2) }
TCP 发送后需要收到 TCP 接收方发回的 ACK 确认报文才能确认这个报文段被对方收到。如果在规定时间内没有收到确认信息就会重传刚才发送的报文段。 由于确认报文很小,所以 TCP 允许在发往相同方向的输出数据分组中对其进行“捎带”,这样可以更有效地利用网络。
为了增加确认报文找到同向传输数据分组的可能性,很多 TCP 栈都实现了一种“延迟确认”算法。延迟确认算法会在一个特定的窗口时间(通常为 100 ~ 200 毫秒)内将确认信息存放在缓冲区中,以寻找能够捎带它的输出数据分组。如果在那个时间段内没有输出数据分组,就将确认信息放在单独的分组中发送。
但是 HTTP 具有双峰特征的请求-应答行为降低了捎带信息的可能性。当希望有相反方向回传分组的时候,偏偏没有那么多。 通常,延迟确认算法会引入相当大的时延。根据操作系统的不同,我们可以调整或禁止延迟确认算法。
当 TCP 关闭连接时,会在内存中维护一个小的控制块,用来记录最近所关闭连接的 IP 地址和端口号。这类信息只会维持一小段时间,通常是最大分段使用期的两倍(称为 2MSL,通常为 2 分钟)左右,以确保在这段时间内不会创建具有相同地址和端口号的新连接。
有些操作系统将 2MSL 设置为一个较小的值,但超过此值时要特别小心。如果来自之前连接的复制分组插入了具有相同连接值的新 TCP 流,会破坏 TCP 的数据。