前言
按层次分,TCP 位于运输层,提供可靠的字节流服务。
也有人把运输层称之为传输层,理由是这一层使用的 TCP 协议叫传输控制协议(Transmission Control Protocol)。虽然从字面意思上,传输和运输的差别不大,但是 OSI 七层协议的第 4 层使用的是 Transport,而不是 Transmission,所以尽量使用运输层这个译名比较准确。

所谓的字节流服务(Byte Stream Service)是指,为了方便传输,将大块数据分割成以报文段(segment)为单位的数据包进行管理。
而可靠是指,通过 TCP 连接传送的数据,无差错、不丢失、不重复,并且按序到达。
TCP的连接
TCP 是面向连接的运输层协议。所谓的面向连接就是双方传输数据之前,必须先建立 TCP 连接,传输数据完毕之后,必须释放已经建立的 TCP 连接。例如,三次握手就是建立连接的一个过程,而四次挥手则是结束销毁连接的一个其中过程。就像打电话,通话之前需要拨打号码,通话结束之后要挂断。
TCP 连接的端点叫做套接字(socket),简单来说,就是IP地址:端口号
。
三次握手
TCP 建立连接的过程叫做握手。为了准确无误地将数据送达目标处,TCP 协议采用了三次握手(three-way handshaking)策略。
其实,三次握手这个说法并不准确,因为只是一次握手过程中交换了三个报文,并不是进行了三次握手,只是这个说法广泛流行。更确切应该说是“三报文握手”。
TCP报文段的首部格式
TCP 虽然是面向字节流的,但是 TCP 传送的数据单元却是报文段。
其中,有几个字段需要记住:序号、确认号、6 个控制位。
序号
:占 4 字节。在 TCP 连接中传送的字节流中的每一个字节都按顺序编号。例如,一报文段的序号字段值是 100,而携带的数据有 123 字节,那么下一个报文段的序号应该从 223 开始。确认号
:占 4 字节。是希望收到下一个报文段的第一个数据字节的序号。例如,A 收到 B 发送过来的一个报文段,其序号字段值是 501,数据长度为 200 字节,那么 A 希望收到 B 的下一个数据序号是 701,于是 A 在发送 B 的确认报文段中把确认号置为 701。紧急 URG
:当 URG = 1 时,表明紧急指针字段有效,它告诉系统此报文段中有紧急数据,应尽快发送(提高发送的优先级)。确认 ACK
:仅当 ACK = 1 时,确认号字段才有效。当 ACK = 0 时,确认号无效。TCP 规定,在连接建立后所有传送的报文段都必须把 ACK 置为 1。推送 PSH
:如果希望在键入一个命令后立即就能够收到对方的响应,发送方 TCP 把 PSH 置 1。复位 RST
:当 RST = 1 时,表明 TCP 连接中出现严重差错,必须释放连接,然后再重新建立运输连接;还可以用来拒绝一个非法的报文段或者拒绝打开一个链接。同步 SYN
:当 SYN = 1 而 ACK = 1 时,表明这是一个连接请求报文段。如果对方同意建立连接,则在响应的报文段中使 SYN = 1 和 ACK = 1;也就是说,SYN 置为 1 就表示这是一个连接请求或者连接接受报文。终止 FIN
:当 FIN = 1时,表明此报文段的发送方的数据已经发送完毕,并要求释放运输连接。
过程描述
TCP 连接的建立采用客户——服务器方式,主动发起连接建立的应用进程叫客户端(Client),而被动等待连接建立的应用进程叫服务器(Server)。
- 第一次握手:客户端将首部中的同部位 SYN 置 1,随机产生一个初始序号 seq = x 发送给 Server,进入 SYN_SENT(同步已发送)状态;
- 第二次握手:服务器收到客户端的 SYN = 1 之后,知道客户端请求建立连接,如果同意建立连接,在确认报文段中将 SYN 置1,ACK 置 1,确认号是 ack = x + 1(表示自己已经收到了客户端的 SYN),并随机产生一个自己的初始序号 seq = y,发送给客户端;进入 SYN_RCVD(同步收到)状态;
- 第三次握手:客户端的确认报文段中,将自己的 ACK 置为1,确认号 ack = y(服务器发出的序号) + 1,自己的序号 seq = x + 1,发送给服务器;进入 ESTABLISHED(已建立连接)状态;服务器检查 ACK 为 1 和 ack = y + 1之后,也进入 ESTABLISHED 状态;完成三次握手,连接建立。
两次握手
不可以。有两个原因:
首先,可能会出现已失效的连接请求报文段又传到了服务器端。
客户端发出的第一个连接请求报文段并没有丢失,而是在某个网络结点长时间的滞留了,以致延误到连接释放以后的某个时间才到达服务器。本来这是一个早已失效的报文段,但服务器收到此失效的连接请求报文段后,就误认为是客户端再次发出的一个新的连接请求。于是就向客户端发出确认报文段,同意建立连接。
假设不采用 “三次握手”,那么只要客户端发出确认,新的连接就建立了。由于现在客户端并没有发出建立连接的请求,因此不会理睬服务器的确认,也不会向服务器发送数据。但服务器却以为新的运输连接已经建立,并一直等待客户端发来数据。这样,服务器的很多资源就白白浪费掉了。
采用“三次握手”的办法可以防止上述现象发生:客户端不会向服务器的确认发出确认。服务器由于收不到确认,就知道客户端并没有要求建立连接。
其次,三次握手的作用是为了确认双方的接收与发送能力是否正常
。而两次握手无法保证客户端正确接收第二次握手的报文(服务器无法确认客户端是否收到),如果只握手 2 次,第二次握手时如果服务端发给客户端的确认报文段丢失,此时服务端已经准备好了收发数据(可以理解服务端已经连接成功),而客户端一直没收到服务端的确认报文,所以客户端就不知道服务端是否已经准备好了(可以理解为客户端未连接成功),这种情况下客户端不会给服务端发数据,也会忽略服务端发过来的数据。
再者,也无法保证客户端和服务器之间成功互换初始序号 seq。
就像男女之间表白,第一次男方说“我爱你”,女方收到后知道男方是爱自己的;第二次,女方回复说“我知道了,我也爱你”,男方收到后知道女方是爱自己的;第三次,男方回复说“我知道了”。这样一来,男女双方都知道了互相爱慕对方。如果第三次男方没回复女方,那么女方不知道男方是否收到了自己的表白(可能收到了,可能也没收到)。
四次握手
可以,但没必要。
服务器在第一次接收到客户端请求后,返回给客户端的确认报文段,可以拆成两个报文段。可以先发送一个确认报文段(ACK = 1,ack = x + 1)给客户端,再发送一个同步报文段(SYN = 1,seq = y)给客户端。这样,原来协议中的第三次握手变为第四次握手。
但是效果是一样的,出于优化目的,四次握手中的二、三可以合并。
四次挥手
- 第一次挥手:客户端将释放报文段首部的终止控制位 FIN 置为 1,发送一个序号 seq = u (u 等于前面已传送的数据的最后一个字节的序号 + 1)给服务器;进入 FIN-WAIT-1(终止等待 1)状态;等待服务器的确认。
- 第二次挥手:服务器收到客户端发送的释放报文段后即发出确认,发送一个 ACK = 1,确认号 ack = u(收到的序号) + 1,自己的序号 seq = v(v 等于服务器前面已传送的数据的最后一个字节的序号 + 1);进入 CLOSE-WAIT(等待关闭)状态。
从 客户端 -> 服务器 这个方向的连接就释放了,但从 服务器 -> 客户端 这个方向的连接并未关闭,也就是说,此时客户端已经没有要发送的数据了,但仍可以接受服务器发来的数据,此时的 TCP 连接处于半关闭
状态。客户端收到服务器的确认后,就进入 FIN-WAIT-2(终止等待 2)状态,等待服务器发出的连接释放报文段。 - 第三次挥手:服务器如果没有要向客户端发送的数据,其应用进程就通知 TCP 释放连接。这时候服务器发出的连接释放报文段必须将 FIN 置1,自己的序号 seq = w,重复上次已发送过的确认号 ack = u + 1 给 Client;然后服务器进入 LAST-ACK(最后确认)状态;等待客户端的确认。
- 第四次挥手:客户端收到服务器的连接释放报文段后,必须对此发出确认。在报文确认段中将 ACK 置 1,确认号 ack = w + 1,自己的序号 seq = u + 1 给服务器,进入TIME-WAIT(时间等待)状态。服务器只要收到后客户端的确认,就进入 CLOSED状态,不再向客户端发送数据。而客户端必须等待 2 * MSL(报文段最长寿命)时间后,也进入 CLOSED状态。
这样就完成四次挥手。
三次挥手
不可以。TCP 是全双工通信的。
客户端发出 FIN 报文时只能说明客户端没有数据发了,服务器还有没有数据要发送,客户端是不知道的。
服务器收到客户端的 FIN 报文后,只能先回复客户端一个确认报文来告诉客户端:服务器已经收到你的 FIN 报文了,但服务器可能还有一些数据没发完,等这些数据发完了我才能给你发 FIN 报文(所以不能一次性将确认报文和 FIN 报文发给客户端,就是这里多出来了一次)。
参考
计算机网络(第7版)- 谢希仁
CSDN - 小鹿动画学编程
CSDN - 小鹿动画学编程
Github - Waking-Up