TCP/IP协议—TCP 传输控制协议

风尘

文章目录

  1. 1. TCP 首部
  2. 2. TCP 连接的建立与终止
  3. 3. 状态机转换解释
  4. 4. 数据传输
    1. 4.1. 交互式输入
      1. 4.1.1. 时延的确认
      2. 4.1.2. Nagel 算法

[TOC]

尽管TCPUDP使用相同的网络层,TCP却向应用层提供了与UDP完全不同的服务。

TCP提供了一种面向连接、可靠的字节流服务

面向连接 意味着两个使用TCP的应用(通常是一个客户和一个服务器)在彼此交换数据之前必须先建立一个TCP连接。这一过程与打电话很相似,先拨号振铃,等待对方摘机说“喂”,然后才说明是谁。

可靠性

  • 应用数据被分割成TCP认为最适合发送的数据块。这和UDP完全不同,应用程序产生的数据报长度将保持不变。由TCP传递给IP的信息单位称为报文段或段(segment)。
  • TCP发出一个段后,它启动一个定时器,等待目的端确认收到这个报文段。如果不能及时收到一个确认,将重发这个报文段。
  • TCP收到发自TCP连接另一端的数据,它将发送一个确认。这个确认不是立即发送,通常将推迟几分之一秒。
  • TCP将保持它首部和数据的检验和。这是一个端到端的检验和,目的是检测数据在传输过程中的任何变化。如果收到段的检验和有差错, TCP将丢弃这个报文段和不确认收到此报文段(希望发端超时并重发)。
  • TCP报文段作为IP数据报来传输,而IP数据报的到达可能会失序,因此TCP报文段的到达也可能会失序。如果必要,TCP将对收到的数据进行重新排序,将收到的数据以正确的顺序交给应用层。
  • IP数据报会发生重复,TCP的接收端必须丢弃重复的数据。
  • TCP还能提供流量控制。TCP连接的每一方都有固定大小的缓冲空间。TCP的接收端只允许另一端发送接收端缓冲区所能接纳的数据。这将防止较快主机致使较慢主机的缓冲区溢出。

两个应用程序通过TCP连接交换8 bit字节构成的字节流。 TCP不在字节流中插入记录标识符,将这称为 字节流服务( byte stream service)

TCP对字节流的内容不作任何解释。 TCP不知道传输的数据字节流是二进制数据,还是ASCII字符、EBCDIC字符或者其他类型数据。对字节流的解释由TCP连接双方的应用层解释。

这种对字节流的处理方式与Unix操作系统对文件的处理方式很相似。Unix的内核对一个应用读或写的内容不作任何解释,而是交给应用程序处理。对Unix的内核来说,它无法区分一个二进制文件与一个文本文件。

TCP 首部

TCP数据被封装在一个IP数据报中,如下图:

TCP数据在IP数据报中封装TCP数据在IP数据报中封装

TCP首部数据格式如下图:

TCP首部TCP首部

每个TCP段都包含 源端口目的端口号 字段,用于寻找发端和收端应用进程。这两个值加
IP首部中的源端IP地址和目的端IP地址唯一确定一个TCP连接。

一个IP地址和一个端口号也称为一个插口(socket)。这个术语出现在最早的TCP规范(RFC793)中,后来它也作为表示伯克利版的编程接口。

插口对(socket pair)(包含客户IP地址、客户端口号、服务器IP地址和服务器端口号的四元组 )可唯一确定互联网络中每个TCP连接的双方。

序号 字段,TCP是面向字节流的,在一个TCP连接中,传送的字节流中的每个字节都被顺序编号。序号是32bit无符号数,序号到达2^32-1后从0开始。该字段标识从TCP发端向收端发送的数据字节流中的第一个字节的序号。

当建立一个新的连接时, SYN标志被打开值为1。序列号字段包含此主机为此连接选择的 初始序号(Initial Sequence Number,ISN) 。该主机要发送数据的第一个字节序号为这个ISN1,因为SYN标志消耗了一个序号。

RFC 793中指出ISN被绑定在一个(可能是虚假的)32位时钟上,其低位大约每4微秒递增一次。直到超过2^32后又从0开始,这个周期大概是4.55小时。所以,如果TCP Segment 段最大寿命(Maximum Segment Lifetime,MSL) 即在网络上的存活时间不超过 4.55小时,那么就不会用到重复的ISN

确认序号 字段,包含发送确认的一端所期望收到的下一个序号,即应当是上次已成功收到数据字节序号加1。只有ACK标志为1时该字段才有效。

发送ACK无需任何代价,因为32bit的确认序号字段和ACK标志一样,总是TCP首部的一部分。因此,我们看到一旦一个连接建立起来,这个字段总是被设置, ACK标志也总是被设置为1

TCP为应用层提供 全双工服务 。这意味数据能在两个方向上独立地进行传输。因此,连接的每一端必须保持每个方向上的传输数据序号。

全双工 : 指可以同时(瞬时)进行信号的双向传输(A→BB→A)。指A→B的同时B→A,是瞬时同步的。

半双工 :指一个时间内只有一个方向的信号传输(A→BB→A)。

长度 字段,首部中32 bit的数目。占4bit,因此最大值为1111,十进制表示为15,所以以TCP首部最大字节为15*(32/8)=60字节。

6标志 比特字段,它们可同时被设置为1,具体如下:

URG 紧急指针(urgent pointer)有效

ACK 确认序号有效。

PSH 接收方应该尽快将这个报文段交给应用层。

RST 重建连接。

SYN 同步序号用来发起一个连接。

FIN 发端完成发送任务。

窗口大小 字段,TCP的流量控制由连接的每一端通过声明的窗口大小来提供。窗口大小为字节数,起始于确认序号字段指明的值,这个值是接收端真正期望接收的字节。窗口大小占16bit,因此窗口大小最大字节为2^16-1=65535字节。

检验和 字段,覆盖了整个TCP报文段(TCP首部+数据)。这是一个强制性字段,由发端计算和存储,并由收端进行验证。计算方法与UDP类似,使用一个伪首部。

紧急指针 字段,只有当URG被设置(即值为1时),该字段才生效。它是一个正的偏移量,和序号字段中的值相加表示紧急数据最后一个字节的序号。

选项 字段,长度可变,最长可达4字节。

TCP最初只规定了一种选项,即 最大报文段长度(Maximum Segment Szie,MSS)MSS是每一个TCP报文段中的数据字段的最大长度。数据字段加上TCP首部才等于整个的TCP报文段。所以MSS并不是整个TCP报文段的最大长度,而是TCP报文段长度减去TCP首部长度”。

TCP 连接的建立与终止

使用telnet命令建立一个TCP连接:

$ telnet 192.168.1.134 13101 discard # discard 是一个服务类似于 Linux 中 /dev/null 作用,用于 tcp/ip 测试
Trying 192.168.1.134..
Connected to 192.168.1.134.
Escape character is '^]'.
^]            # 进入 telnet 命令行
telnet> quit  # 退出 telnet
Connection closed.

-----------------------------------------------
# tcpdump 命令监控
$ tcpdump
78: 192.168.31.62.54258 > 39.106.86.134.13101: S , seq 3984939603, win 65535, length 0
74: 39.106.86.134.13101 > 192.168.31.62.54258: S , seq 2131422913, ack 3984939604, win 28960, length 0
66: 192.168.31.62.54258 > 39.106.86.134.13101: . , ack 1, win 2058, length 0
144: 39.106.86.134.13101 > 192.168.31.62.54258: P , seq 1:79, ack 1, length 78
66: 192.168.31.62.54258 > 39.106.86.134.13101: . , ack 79, win 2057, length 0

对于TCP段,每行输出格式如下:

源 > 目的 : 标志

其中标志代表TCP首部6个标志中的4个,字符含义如下:

字符 标志
S SYN
F FIN
R RST
P PSH
. 表示以上四个标志比特均置 0

上表中四个标志比特中的多个可能同时出现在一个报文段中,但通常一次只见到一个。

TCP首部中的其他两个标志比特—ACKURGtcpdump将作特殊显示。

第一行中,seq表示序号,ack表示确认序号,win表示窗口大小,length表示发送数据长度(上例中没有发送任何数据所以为0)。

为什么,第三行,ack值为1,而不是前一个seq+1

是因为tcpdump命令默认显示相对序号值,如果想强制显示绝对序列值可以加上-S选项。

第四行,序号为1:79,表示 开始序号:结尾序号(不包含) 。这种格式只在相对序号模式下显示,结尾序号值为 开始序号+length 的和,这样显示方便看出数据的长度。因为是不包含关系,所以它就的第五行确认序号ack

状态机与连接、传输数据、断开连接状态机与连接、传输数据、断开连接

如上图,建立一个链接需要 三次握手 ,因为通信双方要相互通知对方自己的初始化序号,作为以后数据通信序号,以保证应用层接收到的数据不会因为网络上的传输的问题而乱序(TCP会用这个序号来拼接数据)。

断开一个链接需要 四次挥手 ,这是由TCP的半关闭(half-close)造成的。一个TCP连接是全双工,因此每个方向必须单独地进行关闭。只不过,有一方是被动的。

当一端收到一个FIN只意味着在这一方向上没有数据流动。一个TCP连接在收到一个FIN后仍能发送数据。而这对利用半关闭的应用来说是可能的,尽管在实际应用中只有很少的TCP应用程序这样做。

为了使用这个特性,编程接口必须为应用程序提供一种方式来说明 “ 我已经完成了数据传送,因此发送一个文件结束(FIN)给另一端,但我还想接收另一端发来的数据,直到它给我发来文件结束(FIN ”。

如果应用程序不调用close而调用shutdown,且第二个参数值为1,则socket接口支持半关闭。

典型的例子是Unix中的rsh命令,它将完成在另一个系统上执行一个命令。它的操作很简单,就是将输入的指令复制给TCP连接,并将结果从TCP链接中复制给标准输出。当输入指令后,rsh客户端执行半关闭,并继续接收来自TCP另一端的数据直到结束。

状态机转换解释

建立链接三次握手:


  • CLOSED

    起点,当超时或连接关闭时进入该状态。它并不是一个真正状态,而是这个状态图的假想起点和终点。

  • LISTEN

    服务器等待连接的状态,服务器进入该状态后开始监听客户发送的连接请求。这被称为“被动打开”。

  • SYN_SENT

    当第一次握手时,客户启动一个连接。客户调用connect(),并向服务器发送一个SYN消息,然后进入该状态,等待服务器的确认。如果服务器不能被连接,它将进入CLOSED状态。

  • SYN_RCVD

    第二握手时发生,服务器从客户接收SYN,服务器从LISTEN进入该状态。然后向客户发送一个SYN + ACK

    状态图还描述一种情况,当客户发送一个SYN,并且从服务器接收了一个SYN请求。也就是,同时发起两个连接请求( 同时打开 ),这时客户状态将从SYN_SENT状态转换到SYN_RCVD状态。

  • ESTABLISHED

    在第三次握手阶段,客户从服务器接收SYN + ACK后,它将发送一个ACK确认给服务器,客户进入ESTABLISHED状态,指示客户已经准备好。但是TCP需要两端都做好传输数据准备,因此在服务器接收到客户的ACK后从SYN_RCVD状态进入ESTABLISHED状态。此时就可以后续的数据传输了。

TCP建立链接状态转换图TCP建立链接状态转换图

如上图,建立连接一系列状态变化:

  • 客户服务器 初始状态是CLOSED

  • 程序调用UNIX “listen()”将触发 “被动打开动作”,这个状态转换不触发任何响应。服务器 进入LISTEN状态。

  • 当要建立一个TCP链接时,客户程序调用UNIX “connect()”,这个事件被称为“主动打开”。这个转换将向 服务器 传送一个SYN信息。客户 进入SYN_SENT状态。

  • SYN信息被 服务器 接收后,它会向 客户 发送SYN + ACK信息。服务器 进入SYN_RCVD状态。

  • SYN + ACK客户 接收,它会向 服务器 发送一个ACK信息。客户 进入ESTABLISHED状态。

    客户完成,客户可以发送和接收数据消息。

  • 最后,当ACK服务器 接收后,服务器 进入ESTABLISHED状态。

    服务器完成,服务器也可以发送和接收消息。


断开链接四次挥手


  • FIN_WAIT_1

    第一次挥手,客户执行主动关闭,就会从ESTABLISHED进入该状态。然后发送一个FIN信息。

  • CLOSE_WAIT

    接收客户发送来的FIN,同时发送出ACK,此时服务器进入该状态。

  • FIN_WAIT_2

    接收到服务器的ACK消息,客户进入该状态。

  • CLOSING

    当客户与服务器同时发起一个关闭请求( 同时关闭 ), 两边都在等待接收到对方发给自己的ACK之前收到对方的FIN,此时两边都进入CLOSING状态。

  • LAST_ACK

    服务器发起一个关闭请求,并从CLOSE_WAIT状态进入该状态。

  • TIME_WAIT

    通过状态图,可以看出总共有三种状态可以进入该状态:

    1. CLOSING进入

      同时关闭时,当客户和服务器都收到ACK时,会进入该状态。

    2. FIN_WAIT_1进入

      客户执行主动关闭,已经发送了FIN并且等待ACK;此时服务端也发起了一个主动关闭,并且发送了FIN。客户端收到了前一个ACK,也收到了服务器FIN,并且发送了ACK。此时进入该状态。

      该状态转换与进入CLOSING接收FINACK顺序不一样。

    3. FIN_WAIT_2进入

      客户端完成它自己发起的关闭请求后,接收了服务器发送的FIN,然后响应了ACK后进入该状态。

  • 2MSL

    TIME_WAIT状态也称为2MSL,每个实现必须选择一个值作为 报文段最大生存时间(Maximum Segment Lifetime,MSL) ,它是任何报文段被丢弃前在网络内的最长时间。

    对于一个具体实现给定的MSL值,处理原则是:当一个TCP执行主动关闭时,并发回最后一个ACK,该连接必须保持TIME_WAIT状态为两倍的MSL。这样可以让TCP再次发送最后的ACK以防这个ACK丢失(另一端超时并重发最后的FIN)。

    这种2MSL等待的另一种影响是,socket定义的双向连接(客户IP、客户端口、服务器IP、服务器端口)在2MSL等待结束前不能被重新使用。因此在连接处于2MSL等待时,任何迟到的报文段将被丢弃。

    某些实现和API提供了一种避开该限制的方法,使用Socket API时可以指定SO_REUSEADDR选项。它允许调用者在2MSL等待中为自己分配一个本地端口号。但TCP不能允许一个新的连接建立在相同的插口对上。

    客户执行主动关闭并进入TIME_WAIT是正常的。服务器通常执行被动关闭,不会进入TIME_WAIT状态。如果终止一个客户程序,并立即重新启动这个客户程序,则这个新客户程序将不能重用相同的本地端口。这不会带来什么问题,因为客户使用临时本地端口,而并不关心这个临时端口号是什么。

    然而,对于服务器,情况就有所不同,因为服务器使用熟知端口。如果我们终止一个已经建立连接的服务器程序,并试图立即重新启动这个服务器程序,服务器程序将不能把它的这个熟知端口赋值给它的端点,因为那个端口是处于2MSL连接的一部分。在重新启动服务器程序前,它需要在1 ~ 4分钟。

    如果我们试图从其他主机来建立这个连接会如何?

    首先我们必须在sun服务器上以-A标记来重新启动服务器程序,因为它需要的端口还处于2MSL等待连接的一部分。

    [root@study ~]# sock -A -s 6666
    

    接着,在2MSL等待结束前,我们在另一台主机上启动客户程序:

    [root@study ~]# sock -b1098 sun 6666
    connected on 38.106.86.134.1098 to 38.106.86.133.6666
    

    结果连接成功了!这违反了TCP规范,但被大多数的伯克利版实现所支持。这些实现允许一个新的连接请求到达仍处于TIME_WAIT状态的连接,只要新的序号大于该连接前一个替身的最后序号。

数据传输

TCP通常需要处理两类数据,一类是成块数据(如:FTP电子邮件),一类是交互数据(如TelnetRlogin)。按分组数量计算,两类数据各占一半;按照字节计算比例约为9:1。这是因为成块数据的报文段基本上都是满长度(通常为512字节数据),而交互数据则小的多(通常小于10个字节)。TCP需要同时处理这两类数据,但使用的处理算法则有所不同。

交互式输入

TCP数据交互式按键回显TCP数据交互式按键回显

上图是一个Rlogin连接上键入一个交互命令时所产生的数据流。注意到通常每一个交互按键都会产生一个数据分组,也就是说,每次从客户传到服务器的是一个字节的按键(而不是每次一行)。并且,Rlogin 需要远程服务器回显客户键入的字符,这样就会产生4 个报文段:

  1. 来自客户的交互按键。
  2. 来自服务器的按键确认。
  3. 来自服务器的按键回显。
  4. 来自客户的按键回显确认。

Rlogin 每次总是从客户发送一个字节到服务器,Telnet 则有一个选项允许客户发送一行到服务器,通过这个选项可以减少网络负载。BDS/386 通过设置一个Rlogin连接的TOS来获得最小时延。

时延的确认

通常可以将Rlogin报文段23进行合并,将按键确认和按键回显一起发送,这种合并的技术称为时延的确认TCP 在接收到数据时并不立即发送ACK,它会推迟发送以便将ACK与需要沿该方向发送的数据一起发送(这种现象有时也称为数据稍带ACK)。

绝大多数实现采用的时延为200ms,也就是说,TCP将以最大200ms 的时延等待是否有数据一起发送。

观察下图客户接到数据和发送ACK之间的时间差,就会发现它们似乎是随机的:123.5、65.6、109.0、132.2。而发送ACK的实际时间(从 0 开始):139.9、539.3、940.1(用黄色字体标出),这些时间差则是200ms整数倍。出现这两种现象的原因是TCP使用了一个200ms定时器,该定时器以相对内核引导的200ms固定时间溢出;由于要确认的数据是随机到达的(16.4、474.3、831.1 等),所以TCP在内核的200ms 定时器的下一次溢出时得到通知可能是将来的1~200ms中的任何时刻。

时延的确认时延的确认

Host Requirements RFC声明TCP需要实现一个经受时延的ACK,但时延必须小于500ms

Nagel 算法