扒开TCP的“神秘外衣”,一文吃透它的本质!

在如今这个信息爆炸的时代,网络早已成为我们生活中不可或缺的一部分。无论是日常刷社交媒体、观看视频,还是进行在线办公、购物,背后都离不开数据在网络中的传输。而在这复杂的网络世界里,有一位默默奉献的 “明星”——TCP(Transmission Control Protocol),它在数据传输过程中扮演着举足轻重的角色 。

想象一下,你在网上购买了一件心仪已久的商品,下单后,商家就像发送数据的源头,而你则是接收数据的目的地。为了确保商品能准确无误地送到你手中,就需要一个可靠的运输系统,这就好比网络中的 TCP。TCP 就像是一位认真负责的快递员,会确保数据从发送方准确、完整地抵达接收方,并且保证数据的顺序正确,不会出现丢包、乱序的情况 。

从专业角度来讲,TCP 是一种面向连接的、可靠的、基于字节流的传输层通信协议 。简单来说,“面向连接” 意味着在数据传输之前,发送方和接收方需要先建立起一个连接,就像你要寄快递,得先和快递公司确定好收件人和寄件人的信息以及运输路线;“可靠” 表示 TCP 会采取各种机制来保证数据的准确传输,如确认应答、重传机制等;“基于字节流” 则是将数据看作是一连串的字节进行处理,而不是一个个独立的数据包。

一、TCP协议概述

TCP(传输控制协议)是一种常用的网络通信协议,它位于OSI模型的传输层。TCP提供可靠的、面向连接的数据传输服务。它通过使用序列号、确认应答和重传机制,保证数据在网络中的可靠传输。TCP还具有流量控制和拥塞控制功能,以确保发送方不会压倒接收方和网络。TCP协议是基于IP(Internet Protocol)协议之上构建的,它负责将应用程序分割成小块(称为报文段),并对这些报文段进行排序、重组和传输。

1.1 TCP历史历程

TCP 的诞生,就像是一部充满传奇色彩的科技史诗,它的出现彻底改变了网络世界的格局 。故事要从20世纪60年代末讲起,当时正处于美苏冷战时期,美国国防部为了在核战争威胁下确保通信的稳定性,启动了 ARPANET 项目 。这个项目旨在建立一个分散的、容错性强的通信网络,即使部分节点遭到破坏,网络的其他部分仍能正常通信 。1969 年,ARPANET 的首批 4 个节点建立,分别位于 UCLA、斯坦福研究所、加州大学圣巴巴拉分校和犹他大学,这便是互联网的雏形 。

随着 ARPANET 的发展,网络中的计算机数量不断增加,不同类型的计算机和网络之间需要一种统一的通信标准,TCP/IP 协议便应运而生 。1974 年,Vinton Cerf 和 Robert Kahn 发表了关于 TCP/IP 协议的论文,提出了一种新型的网络互联协议,奠定了 TCP/IP 协议的基础 。他们就像是网络世界的 “建筑师”,精心设计着 TCP/IP 协议这座宏伟的大厦 。TCP/IP 协议最初设计用于保证数据的可靠传输,它能够处理数据的传输、路由和网络连接等功能,其设计的弹性和可扩展性,使其能够适应网络的不断发展和变化 。

在接下来的几年里,TCP/IP 协议不断完善和发展。1978 年,TCP/IP 协议首次被实验性使用;1983 年 1 月 1 日,ARPANET 正式启用了 TCP/IP 协议,标志着互联网技术的正式成型 。这一天,就像是网络世界的 “成人礼”,TCP/IP 协议从此成为互联网的核心协议,开启了互联网发展的新纪元 。

此后,TCP/IP协议搭上了UNIX操作系统这辆 “快车”,很快推出了基于套接字(socket)的实际编程接口,这使得开发人员能够更方便地利用TCP/IP协议进行网络编程 。同时,TCP/IP 协议是免费或者少量收费的,这大大扩大了其使用人群 。这些因素共同作用,使得TCP/IP 协议逐渐成为全球范围内网络通信的事实标准 。

1.2TCP协议特点

TCP 协议最主要的特点如下:

面向连接:应用程序在使用 TCP 协议之前,必须先建立 TCP 连接。可靠性:TCP 提供可靠交付的服务。通过 TCP 连接传送的数据,无差错、不丢失、不重复,并且按序到达。如果数据包丢失或出现差错,则 TCP 负责重发数据。有序性:TCP 能够把发送的数据划分成一个个数据块,编号后发送,接收方根据编号将这些数据块组装成完整的数据。因此,在接收端可以确保数据块按照发送的顺序进行组装。流量控制:TCP 还提供了流量控制的功能,保证发送方的发送速度不会过快,导致接收方处理不及时,从而导致数据丢失。发送方会根据接收方返回的确认信息,调整自己的发送速度。拥塞控制:TCP 能够根据网络状况调整传输数据的速率,防止出现拥塞。如果网络出现拥塞,TCP 会通过降低发送方的数据传输速率和进行重传等措施来保证数据的可靠传输。

1.3TCP常见问题解析

(1)什么是面向连接?

面向连接是一种网络通信的方式,其中在通信前需要先建立一个双方都认可的连接。在这个连接建立之后,通信的双方可以进行数据传输。

面向连接的通信具有以下几个特点:

连接建立阶段:通信的双方首先要进行握手过程来建立连接,确保彼此能够相互识别和确认。可靠性:在连接建立后,数据传输过程中会使用各种机制来确保数据的可靠性,如序列号、确认应答、重传等。顺序性:数据在传输过程中会按照发送顺序进行传递,接收方能够按照发送顺序正确地接收和处理数据。面向字节流:面向连接的协议将数据视为连续的字节流,在发送端可能会对数据进行分割和组装,在接收端则负责将字节流重新组装成完整的数据。

常见的面向连接协议包括TCP(传输控制协议)和TLS(安全套接层协议),它们广泛应用于互联网通信、文件传输、电子邮件等场景。与之相对的是无连接通信,如UDP(用户数据报协议),它不需要事先建立连接而直接进行数据传输。

举例:就好比,你去医院看病,如果是专家号,一般要提前预约,对只要预约(三次握手建立了连接)上了,你去了就不会看不上病,这是 TCP ;而如果你没有预约,就直接跑过去,那不好意思,你只能看普通门诊,而普通门诊等的人很多,你就不一定能看得上病了。这是 UDP。既然是连接,必然是一对一的,就像绳子的两端,所以 TCP 是一对一发送消息;而 UDP 协议不需要连接,可以一对一,也可以一对多,也可以多对多发送消息。

(2)什么是可靠的通信协议?

可靠的通信协议是指在数据传输过程中能够确保数据的正确性、完整性和可达性的协议。它通过使用各种机制和算法来处理可能出现的错误、丢失、重复或乱序等问题,以保证数据能够按照预期的方式传输到目标地址。

以下是一些常见的用于实现可靠通信的机制和算法:

序列号:发送方对每个发送的数据包进行编号,接收方按序接收并确认已接收的序列号,从而保证数据按正确顺序传递。确认应答:接收方在成功接收到数据后向发送方发送确认消息,告知发送方数据已经到达,若发送方未收到确认则进行重发。超时重传:如果发送方在规定时间内未收到确认应答,则会将该数据包视为丢失,并重新发送。滑动窗口:通过设置滑动窗口大小来控制发送方可以连续发送多少个数据包,在接收方按顺序接收之前不断向前滑动,以提高效率和吞吐量。错误检测与纠正:利用校验和、循环冗余检测(CRC)等技术来检测并修复传输中可能引入的错误。流量控制:通过控制发送方的发送速率,以避免接收方无法及时处理大量数据导致缓冲溢出。拥塞控制:根据网络拥塞程度动态调整发送速率,防止网络过载而导致数据丢失或延迟增加。

TCP 自身有三次握手和超时重传等机制,所以无论网络如何变化,主要不是主机宕机等原因都可以保证一个报文可以到达目标主机。

与之对比, UDP 就比较不负责任了,不管你收不收得到,反正我就无脑发,网络拥堵我也发,它的职责是发出去。

(3)什么是面向字节流的?

面向字节流是一种通信模式,指的是数据在传输过程中被看作是连续的字节流,而不考虑数据之间的边界。在这种模式下,发送方将数据按照字节序列发送,接收方则按照相同的顺序接收和处理字节。

面向字节流的通信并不关心消息或数据包的边界。它将数据切分为一个个字节,并通过底层的传输协议(如TCP)将字节流逐个传送给接收方。接收方根据自己定义的解析规则来处理这些字节,并将它们组合成有意义的数据。

由于面向字节流不关心消息边界,因此需要使用特定的机制来标识消息或数据包的开始和结束位置。常见的做法是在数据流中加入特殊字符或者长度信息来标识消息边界。

面向字节流具有灵活性和可靠性,在实现上相对简单,但同时也带来了一些挑战,例如粘包(多个消息黏在一起)和拆包(一个消息被分割成多个部分)问题需要额外处理。

1.4TCP协议数据传输流程

TCP 协议的数据传输流程包括三个步骤:

建立连接(三次握手):

a. 客户端发送一个SYN(同步)包给服务器,并设置初始序列号。

b. 服务器收到SYN包后,回复一个带有确认码和新的序列号的SYN-ACK(同步-确认)包。

c. 客户端再次发送一个带有确认码和序列号的ACK(确认)包。

数据传输:

a. 连接建立后,客户端可以开始发送数据。将数据分割成适当大小的段,并添加序列号。

b. 接收方会对每个收到的段进行确认,确保数据按正确顺序到达,并且没有丢失或损坏。

c. 如果发现丢失的段,接收方会请求发送方重新传输该段。

连接关闭:

a. 当发送方完成数据传输后,它会发送一个FIN(结束)包来关闭连接。

b. 接收方收到FIN包后,回复一个ACK确认。

c. 然后接收方也发送一个FIN包给发送方来关闭双向连接。

d. 发送方回复一个ACK确认,最终连接关闭。

二、TCP协议的工作原理

TCP协议工作在OSI七层模型的第四层,即传输层。TCP协议通过三次握手建立连接,在连接建立后进行数据传输,并通过四次挥手关闭连接。

2.1tcp报文格式

①TCP报文是TCP层传输的数据单元,也叫报文段

图片

复制
1、端口号:用来标识同一台计算机的不同的应用进程。 1)源端口:源端口和IP地址的作用是标识报文的返回地址。 2)目的端口:端口指明接收方计算机上的应用程序接口。1.2.3.

TCP报头中的源端口号和目的端口号同IP数据报中的源IP与目的IP唯一确定一条TCP连接。

②序号和确认号:是TCP可靠传输的关键部分。序号是本报文段发送的数据组的第一个字节的序号。在TCP传送的流中,每一个字节一个序号。e.g.一个报文段的序号为300,此报文段数据部分共有100字节,则下一个报文段的序号为400。所以序号确保了TCP传输的有序性。确认号,即ACK,指明下一个期待收到的字节序号,表明该序号之前的所有数据已经正确无误的收到。确认号只有当ACK标志为1时才有效。比如建立连接时,SYN报文的ACK标志位为0。

③数据偏移/首部长度:4bits。由于首部可能含有可选项内容,因此TCP报头的长度是不确定的,报头不包含任何任选字段则长度为20字节,4位首部长度字段所能表示的最大值为1111,转化为10进制为15,15*32/8 = 60,故报头最大长度为60字节。首部长度也叫数据偏移,是因为首部长度实际上指示了数据区在报文段中的起始偏移值。

④保留:为将来定义新的用途保留,现在一般置0。

⑤控制位:URG ACK PSH RST SYN FIN,共6个,每一个标志位表示一个控制功能。

复制
1)URG:紧急指针标志,为1时表示紧急指针有效,为0则忽略紧急指针。 2)ACK:确认序号标志,为1时表示确认号有效,为0表示报文中不含确认信息,忽略确认号字段。 3)PSH:push标志,为1表示是带有push标志的数据,指示接收方在接收到该报文段以后, 应尽快将这个报文段交给应用程序,而不是在缓冲区排队。 4)RST:重置连接标志,用于重置由于主机崩溃或其他原因而出现错误的连接。或者用于拒绝非法的报文段和拒绝连接请求。 5)SYN:同步序号,用于建立连接过程,在连接请求中,SYN=1和ACK=0表示该数据段没有使用捎带的确认域, 而连接应答捎带一个确认,即SYN=1和ACK=1。 6)FIN:finish标志,用于释放连接,为1时表示发送方已经没有数据发送了,即关闭本方数据流。1.2.3.4.5.6.7.8.

⑥窗口:滑动窗口大小,用来告知发送端接受端的缓存大小,以此控制发送端发送数据的速率,从而达到流量控制。窗口大小时一个16bit字段,因而窗口大小最大为65535。

⑦校验和:奇偶校验,此校验和是对整个的 TCP 报文段,包括 TCP 头部和 TCP 数据,以 16 位字进行计算所得。由发送端计算和存储,并由接收端进行验证。

⑧紧急指针:只有当 URG 标志置 1 时紧急指针才有效。紧急指针是一个正的偏移量,和顺序号字段中的值相加表示紧急数据最后一个字节的序号。TCP 的紧急方式是发送端向另一端发送紧急数据的一种方式。

⑨选项和填充:最常见的可选字段是最长报文大小,又称为MSS(Maximum Segment Size),每个连接方通常都在通信的第一个报文段(为建立连接而设置SYN标志为1的那个段)中指明这个选项,它表示本端所能接受的最大报文段的长度。选项长度不一定是32位的整数倍,所以要加填充位,即在这个字段中加入额外的零,以保证TCP头是32的整数倍。

⑩数据部分:TCP 报文段中的数据部分是可选的。在一个连接建立和一个连接终止时,双方交换的报文段仅有 TCP 首部。如果一方没有数据要发送,也使用没有任何数据的首部来确认收到的数据。在处理超时的许多情况中,也会发送不带任何数据的报文段。

2.2三次握手

(1)具体步骤拆解

在数据传输之前,TCP 需要在客户端和服务器之间建立起可靠的连接,而这个建立连接的过程就像是一场精心编排的 “默契之舞”,被称为三次握手 。接下来,我们就来详细拆解一下这场 “舞蹈” 的每一个步骤 。

第一次握手:客户端向服务器发送一个 SYN(同步)报文,这个报文就像是客户端向服务器发出的一个连接邀请 。报文中会包含客户端随机生成的初始序列号(Initial Sequence Number,ISN),假设这个序列号为 x 。此时,客户端就像一个热情的舞者,已经做好了跳舞的准备,进入了 SYN_SEND 状态,满怀期待地等待着服务器的回应 。这个初始序列号的作用就像是给数据传输这条 “项链” 上的每颗 “珠子”(数据字节)都编上了号,以便后续接收方能够正确地将它们串起来,保证数据的顺序性 。第二次握手:服务器就像一位优雅的舞者,收到客户端的 SYN 报文后,会立即回应一个 SYN - ACK(同步确认)报文 。这个报文是服务器对客户端邀请的回应,它包含两部分重要信息 。一方面,服务器通过 ACK 标志位来确认收到了客户端的 SYN 报文,确认号(Acknowledgment Number)设置为 x + 1,表示服务器期望下一次收到的序列号是 x + 1 。这就好比在舞会上,一方收到邀请后回应说:“我收到你的邀请啦,下一个动作就按这个顺序来哦 。” 另一方面,服务器也会发送自己的 SYN 报文,带上自己随机生成的初始序列号,假设为 y 。这样,服务器也准备好了跳舞,进入了 SYN_RECV 状态 。第三次握手:客户端收到服务器的 SYN - ACK 报文后,就像舞者确认了对方的舞蹈节奏和顺序,会发送一个 ACK(确认)报文 。报文中的确认号设置为 y + 1,确认收到了服务器的 SYN 报文 。此时,客户端和服务器就像两位默契十足的舞者,已经确定了彼此的节奏和顺序,都进入了 ESTABLISHED 状态,成功建立了 TCP 连接,这场 “默契之舞” 也就圆满完成了 。之后,它们就可以开始愉快地进行数据传输 “舞蹈表演” 了 。(2)为什么是三次?

在了解了三次握手的具体步骤后,你可能会好奇,为什么建立连接偏偏需要三次握手呢?两次不行吗?四次会不会更保险呢?其实,三次握手背后蕴含着深刻的逻辑,是在可靠性和效率之间找到的最佳平衡点 。

我们先来看看两次握手的情况 。如果只有两次握手,客户端发送 SYN 报文,服务器响应 ACK 报文 。表面上看,似乎双方都表明了自己的态度,客户端说 “我想和你建立连接”,服务器回答 “好呀” 。但实际上,这里存在很大的问题 。因为服务器发送ACK报文后,它并不知道客户端是否能收到这个确认 。如果客户端因为网络问题没有收到 ACK 报文,那么客户端就会认为连接没有建立成功,可能会再次发送 SYN 报文 。而服务器却以为连接已经建立,在那里干等着客户端发送数据,这就导致了服务器资源的浪费,而且还可能出现数据传输错误 。就好比两个人约好见面,一个人说 “我在老地方等你”,另一个人回答 “我知道啦”,但如果回答的这个人声音太小,对方没听见,那么第一个人可能会一直等下去,而第二个人却不知道对方没收到自己的回应,这就容易产生误会 。

再说说四次握手 。从理论上来说,四次握手当然可以建立连接 。比如,客户端发送 SYN 报文,服务器先回复 ACK 报文确认收到,然后再发送 SYN 报文,客户端再回复 ACK 报文确认 。但这样做会增加连接建立的时间和网络开销 。在如今这个追求高效的网络时代,每一次额外的握手都会带来一定的延迟,就像你去餐厅吃饭,服务员如果每做一个小步骤都要跟你确认一次,虽然很保险,但会让你等得不耐烦 。而且,三次握手已经足以保证双方都能确认彼此的发送和接收能力,四次握手并没有带来实质性的好处,反而有点 “画蛇添足” 。

而三次握手就巧妙地解决了这些问题 。通过三次握手,客户端和服务器都能确认对方的接收和发送能力 。第一次握手,客户端发送 SYN 报文,证明客户端的发送能力正常;第二次握手,服务器接收并回复 SYN - ACK 报文,证明服务器的接收和发送能力正常;第三次握手,客户端接收并回复 ACK 报文,证明客户端的接收能力正常 。这样一来,双方都清楚地知道对方已经准备好了,而且避免了资源的浪费和不必要的延迟,就像两个经验丰富的舞者,通过简单而有效的沟通,迅速达成默契,开始精彩的舞蹈表演 。

2.3TCP的数据传输

应用层的数据放在write bytes中,通过应用程序将write bytes写入TCP Send buffer,TCP Send buffer里面是以Segment的形式来存储的,TCP进行发送的时候首先进行分段,数据发送出去后,接收方首先也会将接收到的这些段存储到接收缓存区里,应用程序可以调用相关的函数,从接收缓存区中将数据读出来,读到Read bytes里。数据在send buffer和receive buffer中都是以段的形式存储的。

图片

⑴数据传输

发送方将数据发送出去后,会附有下次发送的序列号,而接收方会回应两个比较重要的参数,一个是回应值,另一个是提示窗口,来告诉发送方下一次可以发送多大的数据。

图片

现在我们以一个简单的状态机来看一下TCP的发送过程。在考虑简单状态机的时候,设定以下条件:单向传输过程,同时不考虑流量,也不考虑拥塞控制。

那对于TCP的发送方来讲,应该有三个事件需要进行处理:

当从应用程序中接收到要发送的数据后,进行创建分段。当发现某一个段的回应号超时,需要进行重传,这是为了确保TCP的数据到达对方的一个方式。当看到对方回应ACK时,要对ACK进行处理,主要是看数据是否到达了对方。⑵TCP的接收方对ACK的处理方式

图片

⑶TCP交互式数据传输

交互式数据传输最典型的一种应用就是Telnet,Telnet和Rlogin比较偏向发送交换式的数据。

图片

注意:ACK与下一段的数据一起发送。TCP会将ACK和数据封成一个包,也叫作delay ACK。

2.4TCP的四次挥手

当数据传输完成后,就像一场精彩的演出结束,客户端和服务器需要优雅地结束它们之间的 TCP 连接,这个过程被称为四次挥手 。四次挥手的过程就像是两个人告别时的礼貌对话,确保双方都做好了结束交流的准备 。

图片

(1)挥手之前主动释放连接的客户端结束ESTABLISHED阶段。

随后开始“四次挥手”:

①首先客户端想要释放连接,向服务器端发送一段TCP报文,其中:标记位为FIN,表示“请求释放连接“;序号为Seq=U;随后客户端进入FIN-WAIT-1阶段,即半关闭阶段。并且停止在客户端到服务器端方向上发送数据,但是客户端仍然能接收从服务器端传输过来的数据。注意:这里不发送的是正常连接时传输的数据(非确认报文),而不是一切数据,所以客户端仍然能发送ACK确认报文。

②服务器端接收到从客户端发出的TCP报文之后,确认了客户端想要释放连接,随后服务器端结束ESTABLISHED阶段,进入CLOSE-WAIT阶段(半关闭状态)并返回一段TCP报文,其中:标记位为ACK,表示“接收到客户端发送的释放连接的请求”;序号为Seq=V;确认号为Ack=U+1,表示是在收到客户端报文的基础上,将其序号Seq值加1作为本段报文确认号Ack的值;随后服务器端开始准备释放服务器端到客户端方向上的连接。客户端收到从服务器端发出的TCP报文之后,确认了服务器收到了客户端发出的释放连接请求,随后客户端结束FIN-WAIT-1阶段,进入FIN-WAIT-2阶段前"两次挥手"既让服务器端知道了客户端想要释放连接,也让客户端知道了服务器端了解了自己想要释放连接的请求。于是,可以确认关闭客户端到服务器端方向上的连接了

③服务器端自从发出ACK确认报文之后,经过CLOSED-WAIT阶段,做好了释放服务器端到客户端方向上的连接准备,再次向客户端发出一段TCP报文,其中:标记位为FIN,ACK,表示“已经准备好释放连接了”。注意:这里的ACK并不是确认收到服务器端报文的确认报文。序号为Seq=W;确认号为Ack=U+1;表示是在收到客户端报文的基础上,将其序号Seq值加1作为本段报文确认号Ack的值。随后服务器端结束CLOSE-WAIT阶段,进入LAST-ACK阶段。并且停止在服务器端到客户端的方向上发送数据,但是服务器端仍然能够接收从客户端传输过来的数据。

④客户端收到从服务器端发出的TCP报文,确认了服务器端已做好释放连接的准备,结束FIN-WAIT-2阶段,进入TIME-WAIT阶段,并向服务器端发送一段报文,其中:标记位为ACK,表示“接收到服务器准备好释放连接的信号”。序号为Seq=U+1;表示是在收到了服务器端报文的基础上,将其确认号Ack值作为本段报文序号的值。确认号为Ack=W+1;表示是在收到了服务器端报文的基础上,将其序号Seq值作为本段报文确认号的值。随后客户端开始在TIME-WAIT阶段等待2MSL

(2)为什么客户端发起了ack之后,服务端可以立马关闭,而客户端则要等待2MSL,才能进入关闭状态呢。

要理解这个问题,首先我们需要弄清楚什么叫MSL。MSL是Maximum Segment Lifetime的英文缩写,可译为“最长报文段寿命”,它是任何报文在网络上存在的最长的最长时间,超过这个时间报文将被丢弃,RFC793中规定MSL为2分钟。要记住TCP是可靠的协议,之所以要有2MSL的等待,是因为端口是可以复用的,保证服务器已经接收到来客户端的ack,然后正常关闭服务器测的连接,要不复用端口的时候会对新的连接造成干扰。

为了更方便说明,我们先假设我们认同是2MSL是最保守可靠的方案,什么现象表明服务器已经接收到了ack了,一个方法是服务器对于ack进行ack,客户端就ack又ack,这样子就无限循环了。还有一种就是TCP之所以可靠是因为还有针对唯有ack的数据段会有重发机制。所以如果服务器没有关闭了没有再次发送FIN请求,我们就基本可以假定服务器已经收到了ack了,所以2MSL=FIN报文(来)+ack报文(去),当然也可以是2MSL=ack报文(去)+FIN报文(来),在2MSL的时间再也没有收到服务器的fIN请求,所以我们可以认为服务已经收到ack关闭连接了。

服务器端收到从客户端发出的TCP报文之后结束LAST-ACK阶段,进入CLOSED阶段。由此正式确认关闭服务器端到客户端方向上的连接。客户端等待完2MSL之后,结束TIME-WAIT阶段,进入CLOSED阶段,由此完成“四次挥手”。后“两次挥手”既让客户端知道了服务器端准备好释放连接了,也让服务器端知道了客户端了解了自己准备好释放连接了。于是,可以确认关闭服务器端到客户端方向上的连接了,由此完成“四次挥手”。与“三次挥手”一样,在客户端与服务器端传输的TCP报文中,双方的确认号Ack和序号Seq的值,都是在彼此Ack和Seq值的基础上进行计算的,这样做保证了TCP报文传输的连贯性,一旦出现某一方发出的TCP报文丢失,便无法继续"挥手",以此确保了"四次挥手"的顺利完成。

“四次挥手”的通俗理解:

举个栗子:把客户端比作男孩,服务器比作女孩。通过他们的分手来说明“四次挥手”过程。

图片

“第一次挥手”:日久见人心,男孩发现女孩变成了自己讨厌的样子,忍无可忍,于是决定分手,随即写了一封信告诉女孩。“第二次挥手”:女孩收到信之后,知道了男孩要和自己分手,怒火中烧,心中暗骂:你算什么东西,当初你可不是这个样子的!于是立马给男孩写了一封回信:分手就分手,给我点时间,我要把你的东西整理好,全部还给你!男孩收到女孩的第一封信之后,明白了女孩知道自己要和她分手。随后等待女孩把自己的东西收拾好。“第三次挥手”:过了几天,女孩把男孩送的东西都整理好了,于是再次写信给男孩:你的东西我整理好了,快把它们拿走,从此你我恩断义绝!“第四次挥手”:男孩收到女孩第二封信之后,知道了女孩收拾好东西了,可以正式分手了,于是再次写信告诉女孩:我知道了,这就去拿回来!这里双方都有各自的坚持。女孩自发出第二封信开始,限定一天内收不到男孩回信,就会再发一封信催促男孩来取东西!男孩自发出第二封信开始,限定两天内没有再次收到女孩的信就认为,女孩收到了自己的第二封信;若两天内再次收到女孩的来信,就认为自己的第二封信女孩没收到,需要再写一封信,再等两天……

倘若双方信都能正常收到,最少只用四封信就能彻底分手!这就是“四次挥手”。

(3)为什么“握手”是三次,“挥手”却要四次?

TCP建立连接时之所以只需要"三次握手",是因为在第二次"握手"过程中,服务器端发送给客户端的TCP报文是以SYN与ACK作为标志位的。SYN是请求连接标志,表示服务器端同意建立连接;ACK是确认报文,表示告诉客户端,服务器端收到了它的请求报文。

即SYN建立连接报文与ACK确认接收报文是在同一次"握手"当中传输的,所以"三次握手"不多也不少,正好让双方明确彼此信息互通。

TCP释放连接时之所以需要“四次挥手”,是因为FIN释放连接报文与ACK确认接收报文是分别由第二次和第三次"握手"传输的。为何建立连接时一起传输,释放连接时却要分开传输?

建立连接时,被动方服务器端结束CLOSED阶段进入“握手”阶段并不需要任何准备,可以直接返回SYN和ACK报文,开始建立连接。释放连接时,被动方服务器,突然收到主动方客户端释放连接的请求时并不能立即释放连接,因为还有必要的数据需要处理,所以服务器先返回ACK确认收到报文,经过CLOSE-WAIT阶段准备好释放连接之后,才能返回FIN释放连接报文。

所以是“三次握手”,“四次挥手”。

三、TCP 协议的使用

3.1建立TCP连接

在C++中,可以使用Socket类来建立TCP连接。下面是一个简单的示例代码,展示了如何使用Socket类建立TCP连接的流程:

实例化 Socket 对象:可以使用带有主机名和端口号参数的构造器来新建 Socket 对象。建立连接:使用 Socket 类中的 connect() 方法来建立连接。
复制
#include <iostream> #include <sys/socket.h> #include <arpa/inet.h> #include <unistd.h> int main() { // 创建套接字 int clientSocket = socket(AF_INET, SOCK_STREAM, 0); // 设置服务器地址和端口号 struct sockaddr_in serverAddress; serverAddress.sin_family = AF_INET; serverAddress.sin_port = htons(8000); // 设置服务器端口号 inet_pton(AF_INET, "127.0.0.1", &(serverAddress.sin_addr)); // 设置服务器地址 // 建立连接 if (connect(clientSocket, (struct sockaddr*)&serverAddress, sizeof(serverAddress)) < 0) { std::cerr << "Failed to connect to the server." << std::endl; return -1; } // 发送数据 const char* message = "Hello, server!"; // 待发送的消息 if (send(clientSocket, message, strlen(message), 0) < 0) { std::cerr << "Failed to send data to the server." << std::endl; return -1; } // 接收数据 char buffer[1024]; ssize_t bytesRead = recv(clientSocket, buffer, sizeof(buffer) - 1, 0); if (bytesRead < 0) { std::cerr << "Failed to receive data from the server." << std::endl; return -1; } buffer[bytesRead] = \0; std::cout << "Received: " << buffer << std::endl; // 关闭连接 close(clientSocket); return 0; }1.2.3.4.5.6.7.8.9.10.11.12.13.14.15.16.17.18.19.20.21.22.23.24.25.26.27.28.29.30.31.32.33.34.35.36.37.38.39.40.41.42.43.44.45.46.

在这个示例中,首先创建了一个客户端套接字,然后设置服务器地址和端口号。之后使用connect()函数建立与服务器的连接;接下来,使用send()函数发送数据到服务器,并使用recv()函数从服务器接收数据。最后调用close()函数关闭连接。

3.2发送数据

在C++中,使用Socket类建立TCP连接后,可以通过send()函数向服务器发送数据,而无需使用getOutputStream()方法。下面是一个示例代码展示如何发送数据给服务器:

复制
#include <iostream> #include <sys/socket.h> #include <arpa/inet.h> #include <unistd.h> int main() { // 创建套接字 int clientSocket = socket(AF_INET, SOCK_STREAM, 0); // 设置服务器地址和端口号 struct sockaddr_in serverAddress; serverAddress.sin_family = AF_INET; serverAddress.sin_port = htons(8000); // 设置服务器端口号 inet_pton(AF_INET, "127.0.0.1", &(serverAddress.sin_addr)); // 设置服务器地址 // 建立连接 if (connect(clientSocket, (struct sockaddr*)&serverAddress, sizeof(serverAddress)) < 0) { std::cerr << "Failed to connect to the server." << std::endl; return -1; } // 发送数据 const char* message = "Hello, server!"; // 待发送的消息 if (send(clientSocket, message, strlen(message), 0) < 0) { std::cerr << "Failed to send data to the server." << std::endl; return -1; } // ... // 关闭连接 close(clientSocket); return 0; }1.2.3.4.5.6.7.8.9.10.11.12.13.14.15.16.17.18.19.20.21.22.23.24.25.26.27.28.29.30.31.32.33.34.35.

在这个示例中,通过调用send()函数将消息发送给服务器。你可以将要发送的数据存储在一个字符数组或字符串中,并指定其长度作为第三个参数传递给send()函数。请确保在发送之前将数据转换为适当的格式。

需要注意的是,这个示例仅展示了向服务器发送数据的部分,其他相关步骤(如建立连接、接收响应等)可以参考之前提供的代码。同时,你可能需要根据具体情况进行适当的错误处理和异常处理。

3.3接收数据

在C++中,建立连接并接收从服务器返回的数据通常使用套接字(socket)和标准库函数。不同于Java的Socket类,C++需要通过操作系统提供的底层API来进行网络编程。

以下是一个简单的示例代码,展示如何使用C++中的socket和recv函数来接收服务器返回的数据:

复制
#include <iostream> #include <sys/socket.h> #include <arpa/inet.h> int main() { // 创建套接字 int sockfd = socket(AF_INET, SOCK_STREAM, 0); // 设置服务器地址 struct sockaddr_in serverAddr{}; serverAddr.sin_family = AF_INET; serverAddr.sin_port = htons(8000); // 指定服务器端口号 inet_pton(AF_INET, "127.0.0.1", &(serverAddr.sin_addr)); // 指定服务器IP地址 // 连接到服务器 connect(sockfd, (struct sockaddr*)&serverAddr, sizeof(serverAddr)); // 接收从服务器返回的数据 char buffer[1024]; ssize_t bytesRead = recv(sockfd, buffer, sizeof(buffer) - 1, 0); if (bytesRead > 0) { buffer[bytesRead] = \0; // 添加字符串结束符 std::cout << "Received data: " << buffer << std::endl; } // 关闭套接字 close(sockfd); return 0; }1.2.3.4.5.6.7.8.9.10.11.12.13.14.15.16.17.18.19.20.21.22.23.24.25.26.27.28.29.30.31.32.

在这个示例中,我们首先创建了一个套接字(sockfd),然后设置了服务器的地址信息,并调用connect函数与服务器建立连接。接着,我们使用recv函数来接收从服务器返回的数据,并将其存储在buffer中。最后,我们打印出接收到的数据并关闭套接字,需要注意的是,在实际应用中,你可能还需要进行错误处理和异常处理,并确保适当地关闭套接字(socket)等资源。

3.4释放连接

在C++中释放连接通常使用socket和close函数来关闭套接字。

以下是一个简单的示例代码,展示如何在C++中释放连接:

复制
#include <iostream> #include <sys/socket.h> #include <arpa/inet.h> int main() { // 创建套接字 int sockfd = socket(AF_INET, SOCK_STREAM, 0); // 设置服务器地址 struct sockaddr_in serverAddr{}; serverAddr.sin_family = AF_INET; serverAddr.sin_port = htons(8000); // 指定服务器端口号 inet_pton(AF_INET, "127.0.0.1", &(serverAddr.sin_addr)); // 指定服务器IP地址 // 连接到服务器 connect(sockfd, (struct sockaddr*)&serverAddr, sizeof(serverAddr)); // 发送数据或进行其他操作 // 关闭套接字 close(sockfd); return 0; }1.2.3.4.5.6.7.8.9.10.11.12.13.14.15.16.17.18.19.20.21.22.23.24.

在这个示例中,在与服务器建立连接并完成相关操作后,我们调用close函数来关闭套接字(sockfd),以释放连接资源。需要注意的是,释放连接时应确保已经完成了所有需要传输的数据或操作,并根据实际需求进行适当的错误处理和异常处理。

四、可靠传输:TCP 的 “看家本领”

4.1序列号与确认应答机制

在 TCP 的世界里,数据就像是一列有序的火车,而序列号则是给每节车厢(字节)贴上的独一无二的编号 。TCP 会为每个发送的字节都编上号,这样接收方在收到数据后,就可以根据这些序列号来对数据进行排序,确保数据按序到达 。比如,你要发送一段包含 100 个字节的数据,TCP 会从某个初始序列号开始,依次为这 100 个字节编号 。

而确认应答机制就像是接收方给发送方的 “小纸条”,用来告诉发送方哪些数据已经成功接收 。当接收方收到数据后,会检查数据的序列号,然后发送一个 ACK(确认)报文给发送方 。报文中的确认号(Acknowledgment Number)会设置为下一个期望接收的序列号 。例如,接收方收到了序列号为 1 - 100 的字节,它会回复一个 ACK 报文,确认号设置为 101,表示它已经成功接收了 1 - 100 的字节,期望下一次收到序列号为 101 的字节 。通过这种方式,发送方就可以知道哪些数据已经被接收,哪些还需要重发 。

4.2超时重传机制

在网络传输过程中,就像现实中的交通一样,可能会出现各种意外情况,导致数据丢失或延迟 。超时重传机制就是 TCP 应对这些情况的 “秘密武器” 。当发送方发送数据后,会启动一个重传定时器 。如果在规定的时间内(这个时间被称为超时重传时间,Retransmission TimeOut,RTO)没有收到接收方的 ACK 确认报文,发送方就会认为数据可能丢失了,于是重新发送这些数据 。

重传定时器的作用就像是一个 “闹钟”,提醒发送方什么时候该重传数据 。而这个 “闹钟” 的时间设置可不是固定不变的,它需要根据网络状况动态调整 。如果 RTO 设置得过大,发送方会等待很长时间才发现数据丢失,这会降低数据传输的效率;如果 RTO 设置得过小,发送方可能会频繁重传一些并没有丢失的数据,浪费网络资源 。为了找到一个合适的 RTO 值,TCP 采用了自适应算法 。它会根据网络的往返时间(Round - Trip Time,RTT),也就是数据从发送方到接收方再返回发送方所需要的时间,来动态调整 RTO 。比如,如果当前网络比较拥堵,RTT 变长,TCP 就会相应地增大 RTO;如果网络状况良好,RTT 变短,RTO 也会随之减小 。

4.3流量控制:避免 “交通堵塞”

在网络传输中,发送方和接收方就像两个不同速度的 “搬运工”,如果发送方发送数据的速度太快,而接收方处理数据的速度跟不上,就会像交通堵塞一样,导致数据在接收方堆积,甚至丢失 。流量控制就是 TCP 为了避免这种情况而采取的措施,它通过滑动窗口机制来实现 。

滑动窗口机制就像是一个可伸缩的 “窗口”,控制着发送方和接收方之间的数据传输量 。接收方会根据自身的接收能力,在 ACK 报文中告诉发送方自己当前能够接收的数据量,这个数据量就是接收窗口的大小 。发送方会根据接收方通告的接收窗口大小,来调整自己的发送窗口大小 。例如,接收方的接收窗口大小为 1000 字节,发送方的发送窗口大小也会相应地设置为 1000 字节,这意味着发送方最多可以连续发送 1000 字节的数据而不需要等待接收方的确认 。当接收方处理完一部分数据后,接收窗口会向前滑动,发送方也会根据新的接收窗口大小来调整自己的发送窗口,继续发送数据 。这样,就可以确保发送方发送数据的速度与接收方的接收能力相匹配,避免数据丢失和网络拥塞 。

4.4拥塞控制:应对网络 “大堵车”

拥塞控制是 TCP 为了应对网络拥塞而采取的策略,就像是交通警察在道路拥堵时采取的疏导措施 。当网络中出现拥塞时,路由器可能会丢弃数据包,导致发送方收不到 ACK 确认报文,从而触发超时重传 。而超时重传又会进一步加重网络拥塞,形成恶性循环 。为了打破这个循环,TCP 采用了一系列拥塞控制算法 。

慢启动:在 TCP 连接刚建立时,就像一辆刚启动的汽车,发送方会以较小的拥塞窗口(通常为 1 个最大报文段长度,MSS)开始传输数据 。然后,每收到一个 ACK 确认报文,拥塞窗口就会增加 1 个 MSS 。这样,拥塞窗口会以指数级的速度增长,逐渐探测网络的拥塞情况 。就好比汽车刚启动时,速度慢慢加快,观察路况 。拥塞避免:当拥塞窗口增长到一定程度(慢启动门限,ssthresh)时,就进入了拥塞避免阶段 。此时,拥塞窗口不再以指数级增长,而是每收到一个 ACK 确认报文,就增加 1/MSS 。这就像是汽车在路况良好时,保持稳定的速度行驶 。通过这种线性增长的方式,避免网络突然拥塞 。快速重传:如果接收方收到了失序的报文段,它会立即发送重复确认(对前面有序部分的确认),而不是等待自己发送数据时才捎带确认 。当发送方连续收到三个重复确认时,就会知道中间有报文段丢失了,于是立即重传丢失的报文段,而不是等待超时计时器超时后再重传 。这就好比你发现快递少了一件,马上联系商家补发,而不是等很久才发现 。快速恢复:当发送方收到三个重复确认,执行快速重传后,会进入快速恢复阶段 。此时,慢启动门限(ssthresh)会设置为当前拥塞窗口的一半,然后拥塞窗口会设置为慢启动门限加上3个MSS(因为收到了三个重复确认,说明有三个数据报文段已经离开了网络,到达了接收方) 。之后,拥塞窗口会以线性增长的方式继续调整,逐渐恢复到最佳的传输速率 。这就像是道路拥堵缓解后,汽车逐渐提速,但也不会一下子开得太快 。

通过这些拥塞控制算法,TCP 能够根据网络拥塞状况动态调整发送速率,确保网络的稳定和高效运行 。

五、TCP 的应用场景

TCP 在我们的日常生活中无处不在,就像一个隐形的助手,默默地保障着各种网络应用的稳定运行 。下面,我们就来看看 TCP 在一些常见场景中的应用 。

5.1网页浏览:让世界触手可及

当你在浏览器中输入一个网址,然后轻松地浏览着丰富多彩的网页时,背后离不开 TCP 的支持 。网页浏览通常使用 HTTP(超文本传输协议)或 HTTPS(安全超文本传输协议),而这两种协议都是基于 TCP 实现的 。比如,当你访问淘宝的网站时,浏览器会作为客户端,与淘宝的服务器建立 TCP 连接 。通过三次握手,双方确认连接无误后,浏览器向服务器发送 HTTP 请求,请求获取网页的内容 。服务器接收到请求后,将网页数据按照 TCP 协议的规则,分割成多个数据包,依次发送给浏览器 。

在这个过程中,TCP 的可靠传输机制确保了每个数据包都能准确无误地到达浏览器 。如果某个数据包在传输过程中丢失,TCP 会通过重传机制重新发送,保证网页数据的完整性 。同时,TCP 的流量控制和拥塞控制机制会根据网络状况调整数据传输速度,避免网络拥塞,让你能够快速、稳定地加载网页 。当你点击网页上的链接、查看图片、观看视频时,每一次的数据请求和传输,都有 TCP 在背后保驾护航 。

5.2电子邮件:跨越时空的信息传递

电子邮件是我们日常生活和工作中常用的通信方式之一,它的正常运行也依赖于 TCP 协议 。在电子邮件的发送过程中,发送方的邮件客户端会与发送方的邮件服务器建立 TCP 连接,通过 SMTP(简单邮件传输协议)将邮件发送到发送方的邮件服务器 。发送方的邮件服务器再通过 TCP 连接,将邮件转发给接收方的邮件服务器 。接收方的邮件客户端在需要接收邮件时,同样会与接收方的邮件服务器建立 TCP 连接,使用 POP3(邮局协议版本 3)或 IMAP(互联网邮件访问协议)从邮件服务器获取邮件 。

在这个过程中,TCP 确保了邮件在传输过程中的准确性和完整性 。想象一下,你给远方的朋友发送一封重要的邮件,包含了一些珍贵的照片和文件 。如果没有 TCP 的可靠传输机制,这些照片和文件可能会在传输过程中丢失或损坏,导致朋友无法正常接收 。而有了 TCP,你可以放心地发送邮件,不用担心邮件会 “迷路” 或 “受伤” 。

5.3文件传输:大数据的稳定搬运工

在网络中传输文件时,我们希望文件能够完整、准确地到达目的地,TCP 正好满足了这一需求 。许多文件传输协议,如FTP(文件传输协议)、SFTP(安全文件传输协议)等,都是基于TCP 实现的 。以 FTP 为例,当你使用 FTP 客户端上传文件到 FTP 服务器时,首先会建立TCP连接 。连接建立后,客户端和服务器之间会进行一系列的命令交互,确定传输的文件、传输模式等信息 。

然后,文件数据会被分割成多个TCP 数据包,在可靠传输机制的保障下,从客户端传输到服务器 。同样,在下载文件时,服务器也会通过TCP将文件数据准确地发送给客户端 。比如,你要将一份大型的项目文档上传到公司的文件服务器,或者从服务器上下载一些重要的资料 。这些文件可能包含了大量的数据,如果使用不可靠的传输方式,很容易出现数据丢失或错误的情况 。而 TCP 就像一个专业的搬运工,能够稳稳地将文件从一端搬运到另一端,确保文件的完整性 。

THE END