TCP(Transmission Control Protocol,传输控制协议)是一种面向连接、可靠的、基于字节流的传输层协议。TCP协议是互联网协议栈中最重要的协议之一,它提供了可靠的数据传输服务,保证了数据的完整性、可靠性和有序性。
TCP协议通过建立连接、传输数据和断开连接三个步骤来完成数据传输。在建立连接时,双方需要通过三次握手协议来确认彼此的身份和可用性。在传输数据时,TCP协议会把数据分割成TCP数据包,并对每个数据包进行编号和校验,确保数据的完整性和有序性。在断开连接时,双方需要通过四次挥手协议来协商关闭连接。
TCP协议还提供了流量控制和拥塞控制功能,以便在网络拥塞或带宽限制的情况下保证数据传输的顺畅。此外,TCP协议还支持多路复用,可以在同一个连接上传输多个应用程序的数据。
总之,TCP协议是一种可靠、安全、高效的传输层协议,广泛应用于互联网中。
- 源端口和目的端口:源端口是发送端口,目的端口是接收端口。它们分别占用16个比特,总共32个比特。在这里插入图片描述
- 序号与确认序号:序列号表示TCP报文段中第一个字节的序列号,确认号表示期望接收到的下一个字节的序列号。它们各占用32个比特,总共64个比特。
- 数据偏移:其中填充了TCP报头的长度,以4字节为单位,一般为20字节,最长为60字节(20字节的固定首部加上加上选项的大小)。
- 保留字段:TCP报头中暂未被使用的6个比特位。
- 控制位:包括URG、ACK、PSH、RST、SYN和FIN六个标志位,用于指示TCP报文段的不同控制信息。
- 窗口大小:保证TCP通信的可靠性和效率的字段。
- 校验和:包括TCP报头和TCP数据两部分,校验和用于检测TCP报文段中是否有错误,占用16个比特。
- 紧急指针:用于指示紧急数据的位置,占用16个比特,需要配合标志位中的URG标识使用。
- 选项和填充:选项用于扩展TCP报文段,占用可变长度,填充用于对齐TCP报文段的长度,占用可变长度。最大40字节。
32位序号:
如果双方在进行数据通信时,只有收到了上一次发送数据的响应才能发下一个数据,那么此时双方的通信过程就是串行的,效率可想而知。
因此双方在进行网络通信时,允许一方向另一方连续发送多个报文数据,只要保证发送的每个报文都有对应的响应消息就行了,此时也就能保证这些报文被对方收到了。
但在连续发送多个报文时,由于各个报文在进行网络传输时选择的路径可能是不一样的,因此这些报文到达对端主机的先后顺序也就可能和发送报文的顺序是不同的。但报文有序也是可靠性的一种,因此TCP报头中的32位序号的作用之一实际就是用来保证报文的有序性的。
TCP将发送出去的每个字节数据都进行了编号,这个编号叫做序列号。
比如现在发送端要发送3000字节的数据,如果发送端每次发送1000字节,那么就需要用三个TCP报文来发送这3000字节的数据。此时这三个TCP报文当中的32位序号填的就是发送数据中首个字节的序列号,因此分别填的是1、1001和2001。
当接收方收到这三个报文之后,就会根据其中的序列号进行排序,排序完成后再放入TCP缓冲区中,这样就能保证发送出去的和接收到的数据顺序保持一致。
32位确认号:
TCP报头当中的32位确认序号是告诉对方,当前已经收到了哪些数据,并且数据下一次应该从哪里开始发送
以上文的例子为例,当主机B收到主机A发送过来的32位序号为1的报文时,由于该报文当中包含1000字节的数据,因此主机B已经收到序列号为1-1000的字节数据,于是主机B发给主机A的响应数据的报头当中的32位确认序号的值就会填成1001。
如果报文在传输过程中丢失了,例如最终只要序号为1和2001的报文被主机B收到,那么当B对报文进行排序的时候,就会发现少了1001 ~ 2000 之间的数据,那么此时向主机A响应的报文中的32位确认序号的值就是1001,告诉主机A下次再次发送1001开始的数据。
【注意】
如果此时主机B在给主机A响应时,其32位确认序号不能填3001,因为1001-2000是在3001之前的,如果直接给主机A响应3001,就说明序列号在3001之前的字节数据全都收到了。因此主机B只能给主机A响应1001,当主机A收到该确认序号后就会判定序号为1001的报文丢包了,此时主机A就可以选择进行数据重传。
TCP报头中有了序号和确认号的机制,一定程度上保证了数据传输的完整性,同时也保证了TCP传输的可靠性。
TCP报文的种类多种多样,除了正常通信时发送的普通报文,还有建立连接时发送的请求建立连接的报文,以及断开连接时发送的断开连接的报文等等。收到不同种类的报文时需要对应执行动作,因此需要用标志位进行区分不同的报文类型。这六个标志位都只占用一个比特位,为0表示假,为1表示真。
URG:
双方在进行网络通信的时候,由于TCP是保证数据按序到达的,即便发送端将要发送的数据分成了若干个TCP报文进行发送,最终到达接收端时这些数据也都是有序的,因为TCP可以通过序号来对这些TCP报文进行顺序重排,最终就能保证数据到达对端接收缓冲区中时是有序的。
虽然TCP的有序到达是我们想要的目的,并且接收方的对端上层也是从接收缓冲区中按顺序读取的,但是有时候发送方也会发送紧急数据,那么就要让接收方的对端上层也要紧急读取该数据,因此就需要使用的URG标志位。
ACK:
PSH:
当PSH标志位设置为1时,会提示接收端应用程序立刻从TCP缓冲区读取数据,并交付给上层应用。
一般我们会认为当使用read从缓冲区读取数据时,如果缓冲区中有数据,那么这些数据就会被返回,如果没有数据就会阻塞式的等待write向缓冲区中写入数据再进行读取。
其实这种说法并不准确,因为在缓冲区中都有应该水位线的概念,例如下图:
当缓冲区存储的数据没有达到水位线的时候,read就会进行阻塞等待,只要超过水位线后才会进行读取。因为如果缓冲区中有一点数据就进行读取的话会导致频繁的调用read,势必会造成效率的低下。
当报文当中的PSH被设置为1时,实际就是在告知对方操作系统,尽快将接收缓冲区当中的数据交付给上层,尽管接收缓冲区当中的数据还没到达所指定的水位线。
RST:
SYN:
FIN:
当发送端要将数据发送给对端时,本质是把自己发送缓冲区当中的数据发送到对端的接收缓冲区当中。但缓冲区是有大小的,如果接收端处理数据的速度小于发送端发送数据的速度,那么总有一个时刻接收端的接收缓冲区会被填满,这时发送端再发送数据过来就会造成数据丢包,进而引起丢包重传等一系列的连锁反应。
因此TCP报头中就引入的16位窗口大小来加以控制。这个16位窗口中填充的就是自身缓冲区剩余空间的大小,发送给对方后就能让对方知道自己缓冲区的存储能力,从而控制传输的速率。
确认应答机制是保证TCP通信可靠性的机制之一,它是由32位序号和32位确认序号来保证的。
TCP是面向字节流的,它会为每个字节的数据都进行了编号,即序列号:
每一个ACK都带有对应的确认序列号,意思是告诉发送者,当前接收方已经收到了哪些数据,下一次发送方应该发送哪些数据。
主机A发送数据给主机B之后,可能因为网络拥堵等原因,导致数据无法到达主机B。如果主机A在一个特定时间间隔内没有收到主机B发来的确认应答,就会进行数据重传,这就是超时重传机制。
如果主机A也没有收到来自主机B的确认应答,也可能是因为ACK丢失了。
当ACK发生丢包时,由于存在超时重传机制,主机B就会收到重复的数据,此时主机B就会意识到自己发送的确认应答有可能发生了丢包,导致主机A没有收到,因此就会重新发送确认应答,并且主机B会根据其前面接收到的数据的序号,丢弃掉重复的数据。
那么超时重传的时间该如何设定呢?
最理想的情况下,找到一个最小的时间,保证确认应答一定能在这个时间内返回。但是这个时间的长短,随着网络环境的不同,而存在差异。如果超时时间设的太长,会影响整体的重传效率;如果超时时间设的太短,有可能会频繁发送重复的包。
TCP为了保证无论在任何环境下都能比较高性能的通信,因此会动态计算这个最大超时时间:
在正常情况下,TCP要经过三次握手建立连接,四次挥手断开连接,以下是TCP连接到断开的全部过程:
当客户端要与服务端之间相互通信时,首先就需要建立连接,此时客户端会主动向服务器发送建立连接的请求,然后双方实现三次握手。
第一次握手:客户的给服务端发送SYN报文,初始序列号为x,并且需要消耗应该序号。此时客户端进入SYN_SENT
状态。当SYN=1,而ACK=0时,表明这是一个请求连接的报文。注意SYN=1时的报文不能携带数据,因此第一次握手和第二次握手都不会携带数据。因为如果携带数据的话,假如有人想要攻击服务器,只需要每次第一次握手时在SYN报文中携带大量的数据,导致服务器花费大量的缓冲区,造成服务器的崩溃。
通过第一次握手服务器可以知道:客户端发送数据的能力,以及以及自己的接受能力都处于正常状态。
第二次握手:服务端接收到了来的客户端的SYN报文后,对其进行确认,并且会把自己的SYN响应给对方,此时标志位ACK=1就是对第一次握手的报文进行的确认,并且ack = seq + 1,即为x + 1,初始序列号为y。此时服务器进入SYN_RECV
状态。
通过第二次握手客户端能够知道:服务端的接收和发送能力正常,客户端自己的发送和接收能力也正常。但是服务器不知道客户端的接收能力是否正常,因此就需要进行第三次握手。
第三次握手:客户端收到了服务端的SYN+ACK数据包,此时客户端就会认为自己和服务端都愿意进行连接,因此客户端会进入ESTABLISHED
状态,并且会给服务端发送一个ACK报文,表示自己收到了来自服务端的SYN+ACK响应。此时确认序号ack = y + 1,发送给服务器的第二个报文段的seq = x + 1。
通过第三次握手服务端就能够知道:客户端的接收能力以及自己的发送能力都正常,此时双方建立连接成功,可以进行网络通信了。
为什么是三次握手,而不是其他的握手次数呢?
假如是两次握手,举个打电话的例子:
- 假如在半夜你有一个重要的事情要告诉你的对象,于是打了一个电话,打通了过后,你向电话里面说:“喂,你听得到我说话吗?我有事跟你说。"
- 对方:“你有毛病啊,大半夜打什么电话,有什么屁快放。”,此时却从电话里面再也听不到你的消息,非常生气,以为你大半夜搞恶作剧呢,便把你拉黑了。
其实可能是你对象麦克风或者网络的原因,导致自己听不到对方说话,引起误会。客户端和服务端之间也是如此,如果只有两次握手的话,服务端就不知道客户端的接收能力以及自己发送数据的能力,所以还需要进行第三次握手才行。
为什么要三次握手,难道四次或更多次不可以吗?
因为三次握手是安全的,并且效率是最高的。如果客户端发送请求时出现了丢包情况,因为自己没发送,又重新传了一遍,然而等数据传输完成后客户端和服务端都释放了连接,重发的传输的数据在释放连接前给服务器传了过去,但第一次传输的数据假如由于网络原因滞留的时间长了,在释放连接后到达了服务端,这个时候服务端就会误以为客户端又发出了一次新的请求,服务端确认了客户端第一次发出的报文段并同意建立了新的连接,发送报文给客户端,此时服务端会一直等待客户端的答复,而客户端此时正处于释放连接状态,所以导致白白浪费了资源。
半连接队列和全连接队列
SYN_RCVD
状态,此时双方还没有完全建立连接,这个时候的socket会放到半连接队列。ESTABLISHED
。当双方结束通信,断开连接的过程称为四次挥手。四次挥手,顾名思义就是客户端和服务端四个步骤的释放连接,断开连接需要发送四个包,别名连接终止协议。因为TCP连接是全双工的,因此每个方向的连接都必须分别断开。断开的基本原则是,双方完成了数据传输的任务之后,由一方先发起一个FIN的报文来终止这个方向上的连接。收到一个FIN只意味着这一方向上没有数据流动,但另一方向上还可以发送数据,因此需要对方再发送一个FIN报文断开另一个方向上的连接。例如:
双方再断开连接前都处于ESTABLISHED
状态,
第一次挥手:客户端主动断开连接,向服务端发送一个带FIN的报文。其中包含将FIN标志位置为1,序列号seq = u。发送完毕之后客户端进入FIN_WAIT_1
状态,即关闭自己到服务端的连接,等待客户端的回应,但是此时可以接收服务端发来的报文。
第二次挥手:服务端收到FIN后,知道了客户端想要与自己断开连接,因此进入CLOSE_WAIT
状态,并且向客户端响应一个带ACK的确认报文,此时客户端收到该报文就知道服务端接收到了自己的断开连接请求,但是此时服务端还可能会发送数据。
第三次挥手:此时服务端要与客户端断开自己这个方向上的连接,向客户端发送一个FIN报文,然后服务端进入LAST_ACK
状态,等待来自客户端最后的确认。
第四次挥手:客户端收到 FIN 报文之后,同样会发送一个 ACK 报文作为应答,此时客户端进入TIME_WAIT
状态,TIME_WAIT
状态是为了等待足够的时间以确保服务器能够接收到到连接中断请求的确认。
注意:
**为什么是等待2MSL? **
为什么客户端需要TIME_WAIT状态?
同样的全双工,为什么握手是三次,挥手是四次?
为什么三次挥手不可行?
每发送一个数据就发出一个确认应答,直到发送端收到ACK报文再发送下一个数据段,这样的做最大的缺点就是性能比较差,尤其是在数据往返时间较长的时候表现得尤为明显。
既然这样一发一收的方式性能较低,那么就可以考虑一次发送多条数据,就可以大大的提高数据传输的性能。实际上就是是将多个数据段的等待时间重叠在一起,这样TCP就引入了滑动窗口的机制。
滑动窗口的概念:
滑动窗口(Sliding window)是一种流量控制技术。早期的网络通信中,通信双方不会考虑网络的拥挤情况直接发送数据。由于大家不知道网络拥塞状况,同时发送数据,导致中间节点阻塞掉包,谁也发不了数据,所以就有了滑动窗口机制来解决此问题。滑动窗口协议是用来改善吞吐量的一种技术,即容许发送方在接收任何应答之前传送附加的包。接收方告诉发送方在某一时刻能送多少包(称窗口尺寸)。
滑动窗口原理:
滑动窗口协议的基本原理就是在任意时刻,发送方都维持了一个连续的允许发送的帧的序号,称为发送窗口;同时,接收方也维持了一个连续的允许接收的帧的序号,称为接收窗口。发送窗口和接收窗口的序号的上下界不一定要一样,甚至大小也可以不同。不同的滑动窗口协议窗口大小一般不同。发送方窗口内的序列号代表了那些已经被发送,但是还没有被确认的帧,或者是那些可以被发送的帧。
主机B向主机A发送请求序列号为2001的报文,主机A收到确认报文后,滑动窗口向右移动至2001的位置。
如果出现了丢包的情况,窗口又该如何滑动呢?
情况一:主机A发送的数据包已经被主机B接收了,但是发给主机A的确认报文ACK丢失了。
这种情况下,部分ACK丢失并不会影响数据包的正常传输,因为主机A接收到的对后续数据包的确认,同时也能对前面发出的数据包进行确认。
情况二:主机A发送的数据包部分直接丢失了。
这种机制被称为 “高速重发机制”,也叫做 “快重传”。
接收端处理数据的速度是有限的,如果发送端发送得太快,就会导致接收端的缓冲区迅速被写满,如果这个时候发送端继续发送数据就会造成丢包,继而引起丢包重传等一系列连锁反应。
因此TCP支持根据接收端的处理能力来决定发送端的发送速度,这个机制就叫做流量控制(Flow Control)。
流量控制的基本原理如下:
接收端如何把窗口大小告诉发送端呢?回忆前面的TCP首部中,有一个16位窗口字段,存放的就是窗口大小信息。那么问题来了,16位数字最大表示65535,那么TCP窗口最大就是65535字节么?
实际上,TCP首部40字节选项中还包含了一个窗口扩大因子M,实际窗口大小是窗口字段的值左移 M 位。
虽然TCP有了滑动窗口机制,能够高效可靠的发送大量数据,但是如果在刚刚传输数据的阶段就发送大量的数据,仍然可能会引起问题。因为当今世界上时时刻刻都存在着大量的计算机在进行网络通信,可能当前的网络状态就比较拥堵了,如果此时在不清楚网络状况的情况下就贸然发送大量的数据,还是可能会造成丢包等情况。
为了解决这个问题,TCP就引入了 “慢启动” 机制,即开始先发送少量的数据,去摸清当前网络的拥堵状况,再决定按照多大的速度发送数据。
此时便为 “慢机制” 引入了一个拥塞窗口的概念,其基本原理如下:
像上面这样的拥塞窗口大小的增长速度是指数级别的增长,“满启动” 只是指初始传输数据的速度满,但其增长的速度很快。解决方法如下:
当数据传输过程中出现少量的丢包,仅仅会触发TCP的超时重传机制,如果出现大量的丢包现象,那么就可以判断为出现了网络拥塞。当TCP通信开始时,网络吞吐量会逐渐上升;当网络出现了拥塞,吞吐量就会立即下降。拥塞控制,归根结底是TCP协议尽快可能的想将数据传输给对方,但是又要避免给网络造成太大压力的折中方案。
如果每次接收端接收到数据就立刻响应ACK应答的话,这个时候返回的滑动窗口大小就会比较小,例如:
但是我们一定要记住,窗口越大,网络吞吐量就越大,传输的效率就越大,但是是在保证网络不拥塞的基础之上尽量提高传输效率的。因此不少所有的包都可以延迟应答,例如:
具体的数量和延迟时间并不统一,随着操作系统的不同也会存在差异,但是一般n取2,最大延迟时间取200ms。
在延迟应答的基础上, 我们发现, 很多情况下, 客户端服务器在应用层也是 “一发一收” 的,意味着客户端给服务器说了 “How are you”,服务器也会给客户端回一个 “Fine, thank you” 。那么这个时候ACK就可以搭顺风车,和服务器回应的 “Fine, thank you” 一起发送给客户端。这就是捎带应答。
我们都知道TCP传输数据是面向字节流的,比如创建一个TCP的socket,同时会在系统内核中创建一个发送缓冲区和一个接收缓冲区。
由于缓冲区的存在,TCP程序的读和写不需要一一匹配,例如:
粘包问题中的 “包” ,是指的应用层的数据包。在TCP的协议头中,没有如同UDP一样的 “报文长度” 这样的字段,但是有一个序号这样的字段。站在传输层的角度,TCP是将一个一个报文传输过来的,按照序号排好序放在缓冲区中。但是站在应用层的角度,看到的只是一串连续的字节数据,那么应用程序看到了这么一连串的字节数据,就不知道从哪个部分开始到哪个部分,是一个完整的应用层数据包。
那么如何避免粘包问题呢?归根结底就是一句话,明确两个包之间的边界。
对于UDP协议来说,是否也存在 “粘包问题” 呢?
对于UDP,如果还没有上层交付数据,UDP的报文长度仍然在。同时,UDP是把一个一个把数据交付给应用层,因此就有很明确的数据边界。站在应用层的站在应用层的角度,使用UDP的时候,要么收到完整的UDP报文,要么不收,不会出现"半个" 的情况。因此对于UDP而言不会出现 “粘包问题” 。
TCP常见的异常情况如下:
为什么TCP协议会这么复杂?因为既要保证其可靠性,同时又尽可能的提高性能,导致实现TCP就变得很困难。
TCP的可靠性得益于:
- 校验和
- 序列号(按序到达)
- 确认应答
- 超时重传
- 连接管理
- 流量控制
- 拥塞控制
提供性得益于:
- 滑动窗口
- 快速重传
- 延迟应答
- 捎带应答
其他:
- 定时器(超时重传定时器、保活定时器、TIME_WAIT定时器等)