TCP 的优点

与UDP相比,TCP可靠、面向连接。她有如下的一些特点:

  1. 面向连接;
  2. 是点到点的;
  3. 提供可靠的交付服务,并且传送的数据无差错、不丢失、不重复并且有序;
  4. 全双工(双方都需要维护发送以及接受缓存);
  5. 面向字节流的(seq,ack都是针对字节流的)

TCP报文

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
TCP Header Format


0 1 2 3
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Source Port | Destination Port |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Sequence Number |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Acknowledgment Number |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Data | |U|A|P|R|S|F| |
| Offset| Reserved |R|C|S|S|Y|I| Window |
| | |G|K|H|T|N|N| |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Checksum | Urgent Pointer |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Options | Padding |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| data |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+

TCP Header Format

具体的阐述参见:RFC TCP

与UDP相同的是,TCP的校验也采用伪首部,具体细节同样参见RFC 793。

options可以用于确定MSS大小,最终取双方中较小值。

TCP连接的建立

TCP连接需要来回发送一共三条报文,也就是大名鼎鼎的“三次握手”。

1
2
3
4
5
6
7
8
9
10
11
12
13
TCP A                                                TCP B

1. CLOSED LISTEN

2. SYN-SENT --> <SEQ=100><CTL=SYN> --> SYN-RECEIVED

3. ESTABLISHED <-- <SEQ=300><ACK=101><CTL=SYN,ACK> <-- SYN-RECEIVED

4. ESTABLISHED --> <SEQ=101><ACK=301><CTL=ACK> --> ESTABLISHED

5. ESTABLISHED --> <SEQ=101><ACK=301><CTL=ACK><DATA> --> ESTABLISHED

Basic 3-Way Handshake for Connection Synchronization

首先,TCP A发送一条报文(进入SYN-SENT状态),其中SYN位置1,表示这是一条请求TCP连接建立的报文;

其次,当TCP B接受到SYN位置1的报文时(进入SYN-RECEIVED状态),它发送一条SYN,ACK位均置1的报文,其中报文的ack号是接收到的报文seq加一(需要注意的是,上图2中的报文是没有携带任何数据的,但是依然需要消耗一个序列号!);

最后,接收到TCP B发送报文的TCP A进入ESTABLISHED状态,并发送一条只有ACK置1的报文(这一条报文可以携带数据),表示连接建立成功。

为什么需要三次握手?

stackoverflow上的讨论
牛客网上的面经

概括一下,三次握手可以防止old deplicated syn,避免服务器的资源空耗,统一双方的seq以及ack。

TCP连接的释放

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
TCP A                                                TCP B

1. ESTABLISHED ESTABLISHED

2. (Close)
FIN-WAIT-1 --> <SEQ=100><ACK=300><CTL=FIN,ACK> --> CLOSE-WAIT

3. FIN-WAIT-2 <-- <SEQ=300><ACK=101><CTL=ACK> <-- CLOSE-WAIT

4. (Close)
TIME-WAIT <-- <SEQ=300><ACK=101><CTL=FIN,ACK> <-- LAST-ACK

5. TIME-WAIT --> <SEQ=101><ACK=301><CTL=ACK> --> CLOSED

6. (2 MSL)
CLOSED

Normal Close Sequence

1.数据传输结束后,客户端的应用进程发出连接释放报文段,并停止发送数据,客户端进入FIN_WAIT_1状态,此时客户端依然可以接收服务器发送来的数据。

2.服务器接收到FIN后,发送一个ACK给客户端,确认序号为收到的序号+1,服务器进入CLOSE_WAIT状态。客户端收到后进入FIN_WAIT_2状态。

3.当服务器没有数据要发送时,服务器发送一个FIN报文,此时服务器进入LAST_ACK状态,等待客户端的确认

4.客户端收到服务器的FIN报文后,给服务器发送一个ACK报文,确认序列号为收到的序号+1。此时客户端进入TIME_WAIT状态,等待2MSL(MSL:报文段最大生存时间),然后关闭连接。

四次挥手的原因

四次挥手的原因:由于连接的关闭控制权在应用层,所以被动关闭的一方在接收到FIN包时,TCP协议栈会直接发送一个ACK确认包,优先关闭一端的通信。然后通知应用层,由应用层决定什么时候发送FIN包。应用层可以使用系统调用函数read==0来判断对端是否关闭连接。

可靠传输机制

  1. 序号

    TCP为字节流维护序号,确保了数据在传递时的有序性

  2. 确认

    TCP报文中指出希望对方发送的下一个报文的序号,并使用累计确认机制。该机制可以用于及时发现哪些报文丢失,及时重传

  3. 重传

    在两种情况下重传:1. 报文段的计时器超时;2. 收到了冗余ACK

流量控制

着眼于点到点,确保连接双方的速率匹配,接收方通过TCP 报文中的rwnd字段来告诉发送方它还有多大的接受缓冲空间。

拥塞控制

着眼于网络全局,通过拥塞控制算法,TCP估计当前网络负载能力,计算出cwnd,最终发送方发送的报文大小取cwnd以及rwnd中的较小值。

拥塞控制算法v1:

  1. 设置ssthresh;

  2. 设置cwnd=1;

  3. 进入指数增长阶段,每一个RTT,将cwnd翻倍(另一种表述:每接受到一个ack,cwnd++);如果发现2*cwnd > ssthresh,令cwnd=ssthresh,进入3;如果出现网络拥堵(报文超时),那么将ssthresh = cwnd/2(至少为2),然后进入1;
  4. 进入加性增阶段,每一个RTT,cwnd++;如果出现网络拥堵(报文超时),那么将ssthresh = cwnd/2(至少为2),然后进入1;

算法v1存在一些问题:很多时候我们很难及时察觉出网络的阻塞。为此,我们引入快重传以及快恢复方法:

  1. 快重传:如果收到某一个报文的连续3次ack,那么重传对应序号报文;
  2. 快恢复:如果收到某一个报文的连续3次ack,那么设置ssthresh = cwnd/2,cwnd=ssthresh

一些细节问题

  1. TCP 中的滑动窗口机制

    image-20210323110050055

    可以看出,在使用滑动窗口机制下,即使一些ack没有成功收到,也有可能不会导致重发,因为之后的ack一旦收到,之前的所有报文也都一定被收到。

  2. 流量控制中的探测

    如前文所述,流量控制中的窗口大小是通过接收方的报文通知发送方的,因此,如果发送方在以为接收方没有任何缓冲区空间之后,接收方通知有新的缓冲区空间报文丢失,就会导致发送方“饥饿”。为此,发送方会在一个RTO之后尝试发送一个报文,通过接收方的ACK探测当前是否有新的缓冲区空间。

  3. 拥塞控制中的ssthresh

    《图解TCP/IP》中指出,TCP通信的开始不会直接指出ssthresh的值,而是在第一次网络拥塞之后计算出。

  4. 快恢复的cwnd

    《图解TCP/IP》中的说法是ssthresh+3,加3 的原因是因为收到3个重复的ACK。