TCP/IP协议—TCP 传输控制协议
[TOC]
尽管TCP
和UDP
使用相同的网络层,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
首部数据格式如下图:

每个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) 。该主机要发送数据的第一个字节序号为这个ISN
加1
,因为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→B
且B→A
)。指A→B
的同时B→A
,是瞬时同步的。半双工 :指一个时间内只有一个方向的信号传输(
A→B
或B→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
首部中的其他两个标志比特—ACK
和URG
—tcpdump
将作特殊显示。
第一行中,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
状态。此时就可以后续的数据传输了。

如上图,建立连接一系列状态变化:
-
客户 和 服务器 初始状态是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
通过状态图,可以看出总共有三种状态可以进入该状态:
-
CLOSING
进入同时关闭时,当客户和服务器都收到
ACK
时,会进入该状态。 -
FIN_WAIT_1
进入客户执行主动关闭,已经发送了
FIN
并且等待ACK
;此时服务端也发起了一个主动关闭,并且发送了FIN
。客户端收到了前一个ACK
,也收到了服务器FIN
,并且发送了ACK
。此时进入该状态。该状态转换与进入
CLOSING
接收FIN
与ACK
顺序不一样。 -
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
、电子邮件
),一类是交互数据(如Telnet
、Rlogin
)。按分组数量计算,两类数据各占一半;按照字节计算比例约为9:1
。这是因为成块数据的报文段基本上都是满长度(通常为512
字节数据),而交互数据则小的多(通常小于10
个字节)。TCP
需要同时处理这两类数据,但使用的处理算法则有所不同。
交互式输入

上图是一个Rlogin
连接上键入一个交互命令时所产生的数据流。注意到通常每一个交互按键都会产生一个数据分组,也就是说,每次从客户传到服务器的是一个字节的按键(而不是每次一行)。并且,Rlogin
需要远程服务器回显客户键入的字符,这样就会产生4
个报文段:
- 来自客户的交互按键。
- 来自服务器的按键确认。
- 来自服务器的按键回显。
- 来自客户的按键回显确认。
Rlogin
每次总是从客户发送一个字节到服务器,Telnet
则有一个选项允许客户发送一行到服务器,通过这个选项可以减少网络负载。BDS/386
通过设置一个Rlogin
连接的TOS
来获得最小时延。
时延的确认
通常可以将Rlogin
报文段2
和3
进行合并,将按键确认和按键回显一起发送,这种合并的技术称为时延的确认。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
。