TCP协议又叫传输控制协议,TCP/IP协议是计算机通信网络中目前使用最多的协议,同时也融入了生活的方方面面,不管是浏览网页使用的http/https协议、物联网设备使用的MQTT/MQTTS协议与下载文件使用的ftp协议、工业以太网中使用的Modbus TCP协议等很多应用层协议,都是基于TCP/IP协议。
1. TCP协议段格式
各字段的含义:
- 源/目的端口号:表示数据是从哪个进程来, 到哪个进程去;
- 32位序号/32位确认号:分别代表TCP报文的字节数据的编号以及对方的确认。
- 4位首部长度:表示该TCP报头大小,单位为4字节。
- 6位保留字段:TCP报文中未使用的6个字节。
- 6个标志位:用于控制和管理TCP连接的各种状态和操作,确保数据传输的可靠性、有序性和连接的正确管理。
- 16位窗口大小:用来表示接收端期望从发送端接收的数据量大小,即接收缓冲区剩余空间的大小
- 16位校验和:发送端填充, CRC校验. 接收端校验不通过, 则认为数据有问题. 此处的检验和不光包含TCP首部, 也包含TCP数据部分.
- 16位紧急指针:标识哪部分数据是紧急数据;
- 40字节头部选项:TCP报头当中允许携带额外的选项字段,最多40字节。
1.1 TCP如何将报头与有效载荷进行分离?
TCP报头的前20个字节是固定大小,所以我们先提取前20个字节的大小,然后找到4位首部长度。(4位首部长度表示整个报文的大小),我们就能判断出整个报文的大小了,除了报文,剩下的就是有效载荷了。
关于4位首部长度
如果单位为1字节,4个bit位的取值范围就是【0, 15】,但是TCP报文至少为20字节。所以规定TCP首部长度的单位为4字节,所以4bit位的取值范围就是【0,60】,因为TCP报文至少20字节,所以TCP首部长度的取值范围是【20,60】,也就是说在实际的TCP报文中,填写的就是【5,15】。
1.2 有效载荷如何向上交付
传输层上面是应用层,而应用层的每个网络进程都会分配一个端口号,当我们收到一个报文后,会先将报头和有效载荷分离,然后提取出报头中的目的端口字段,有了目的端口,我们就知道交给上层的哪个应用了。
1.3 如何理解TCP报头
上一章传输层协议——UDP协议-CSDN博客我们提到过,报头其实就是结构化的数据,封装报头的过程其实就是开辟一段空间,将结构体中的数据填充好存放进去,再将上层交给这一层的数据存放进去(内核缓冲区)。
struct sk_buff
{
char* start;
char* data;
}
TCP层针对这个报文会操作两个指针,start指向的是TCP报头部分,data指向的是上层交付的数据。
TCP向下交付时,只要sk_buff向下交付即可,下几层只需要修改start和data指针即可。
TCP向上交付时,需要解包,也只需要start指针移到data位置。
1.4 TCP特点
TCP(Transmission Control Protocol,传输控制协议)
- 面向连接:TCP在传输数据前需要先建立连接,确保数据的可靠传输。
- 可靠传输:通过确认应答、超时重传、流量控制、拥塞控制等机制,TCP保证了数据传输的可靠性。
- 面向字节流:TCP不关心应用程序一次发送多少字节,而是根据对方给出的窗口值和当前网络状况来决定一个TCP报文段应包含多少个字节。
1.4.1 如何理解可靠与不可靠
在冯诺依曼体系结构中
各个设备之间都是以”线“连接起来的。存储器与其他设备连接的”线“叫做IO总线,存储器与CPU连接的”线“叫做系统总线。如果以单台机器而言,所有的线都是比较短的,也就是说传输过程中出现错误的概率很小。
然而到了网络中,由于传输距离变得很大,出现错误的概率也变大了,所以我们引入了可靠性的概念,可靠就是保证数据能成功送到对端主机,不可靠就是不能确保数据送到对端主机。其中最典型的两个协议就是TCP(可靠)和UDP(不可靠)。
所以网络出现不可靠的原因就是因为传输距离的增加。
- 所以可靠是褒义词,而不可靠是贬义词吗?
不是,可靠是个中性词,UDP虽然不可靠,但是她比较简单,效率高,速度快。而TCP为了可靠性,也需要付出很大的代价,TCP有很多机制去维护可靠性,导致他在速度和效率上是不如UDP的。
- 如何得知数据被对方收到了?
现在有两台主机a和b,a给b发送了一条消息,那么a如何得知自己的消息成功被b接受到了呢,就是要收到响应,很像现实生活中我们和别人交流,我和你说一句话,你给我回复了,就说明我上一句话你肯定听到了。
只有发送消息的一端收到了响应,才能保证上一条消息被对方收到了。
但是网络传输中,数据可能存在丢包问题。如果A发送的“你吃了吗”没有收到响应,那么可能是主机B没有收到“你吃了吗”这条报文,也可能是B发给A的响应“吃过了”丢包了。就算A收到了主机B的响应“吃过了”,但是站在B的角度,他也并不知道A是否成功收到了。
那你可能会想到,A再给B回复一下不就行了吗?但是通信双方总会有一条最新的消息不会被响应。例如A给B回复“收到”,但是A也不能保证“收到”被B收到。
结论:互联网通信中,不存在百分百的可靠性,通信双方总有最新数据得不到响应。
实际上,我们也并不需要保证所有数据都得到响应。只需要确保一些重要的信息不丢失即可,例如上面A发“你吃了吗”这条报文过了一段时间后没有得到响应,A就知道是丢包了,所以可以将这条报文重新发给对方(超时重传机制),这是比较重要的报文。但是对于A第二次给B发”收到“的时候,A就不用关心这条报文是否能成功被B收到了,因为如果B没有收到的话,就会将”吃过了“这一条报文再次发给A,A一收到就明白了,刚才的”收到“报文丢失了,所以我们可以通过上面这种方法来达到局部可靠性。
1.4.2 如何理解面向字节流
面向字节流:TCP不关心应用程序一次发送多少字节,而是根据对方给出的窗口值和当前网络状况来决定一个TCP报文段应包含多少个字节。
当上层调用send等IO类接口时,UDP在sendto时会直接把数据添加好报头,然后传给下一层(面向数据报),TCP协议中,数据是直接拷贝到了内核缓冲区中,具体什么时候发,TCP不关心,这是操作系统的工作。
也就是说上层数据通过TCP协议时,可能会和其他数据合并成一个报文,也可能被拆分成多个报文,这都是不确定的。
例如上图,我们决定好TCP报文大小为8个字节,可能就会出现上面两种情况:数据被填充进一个完整报文中,数据被拆分成多个报文。
如果接收端不知道发送方发送的消息边界时,就无法读出一个完整的用户信息。当两个消息的某部分被分到同一个TCP报文时,就会出现TCP粘包问题。
解决粘包问题的方式有很多,这里介绍一下以特殊字符作为边界。例如我们每个数据的结尾都添加上一个&,当你收到数据时就很容易区分出用户数据。
hello world&how are you&I'm
例如上面的hello world和how are you我们就知道他是两个报文了,而I'm后面没有&说明他不是一个完整的报文,等到下次收到报文时再将他做拼接。但是有个问题,如果用户数据中存在了&怎么办。此时我们就需要将用户的&转化成特殊字符了。
2. TCP协议报文字段
2.1 源端口号/目的端口
在网络通信中,每台进程都会被分配一个端口号用来标识进程的唯一性,源端口号表明这个TCP报文的数据是来自哪个进程的,目的端口表明我要将这个TCP报文交给接收端的主机的哪个进程上。
至于如何从这台主机找到另一台主机,那就不是传输层要关心的了。
2.2 32位序号与确认序号
32位序号
如果网络通信中,数据都是串行的(发送数据时,只有收到了应答才会发第二个),效率就非常低下
为了解决这个问题,我们可以考虑一次性发多个报文,为了区分不同的报文,我们给每个报文都进行编号,这个编号就是序列号也叫做序号。
同时,还有比较关键的一点,每个报文在网络传输中,会由于路径选择,网络状态等因素到达对端主机的时间也不一样。也就是说先发的报文不一定先到。有了序列号,我们就可以对报文排序。保证报文是有序的。
TCP将每个字节的数据都进行了编号. 即为序列号。
- 比如发送端要发送3000字节的数据,如果确定号每次发送1000字节,那么就需要用三个TCP报文来发送这3000字节的数据。
- 此时这三个TCP报文当中的32位序号填的就是发送数据中首个字节的序列号,因此分别填的是1、1001和2001。
由于这三个报文不一定按序到达,真实到达的顺序可能是1001,2001,1。但是因为有了序列号,我们就可以将这三个报文排好序,然后正常读取即可。
32位确认序号
32位确认序号的意义主要有两个:
- 1. 告诉对方在确认序号之前的所有数据,我都收到了
- 2. 下一次从哪里开始发
以上面的例子为例:A给B发了一个序列号为1的报文,被B收到后,B会计算出这个报文的大小为1000,所以B就会给A发一个确认序号为1001的报文,表示1001之前的数据我都收到了,你下次给我发从1001开始的报文就行。
网络传输中,丢包了怎么办。
比如序列号为1000的报文丢失了,那么B只收到了序列号为1和2000的报文。B可以通过计算得知报文丢失了,比如可以计算出报文的大小为1000,而序号1的报文就是发送端编号从1到1000的数据,序号为2000的就是发送端从2000到3000的数据,中间很明显缺少了一个从1001到2000的数据。
当B判断出丢包后,不能给A发送确认序号为3001的报文,因为3001表示3001之前的数据都收到了,就只会给A发确认序号为1001的报文,当A收到1001后,就会判断出报文丢失了,于是给B重新发送报文。
为什么存在两组序号?
我们上面说的过程,好像只有一个序号也能完成,B给A回复的时候将信息填写在序号位置难道不行吗?
TCP是全双工的。也就意味着,当一端发数据的时候,可能也会收到数据。
- 序列号表示我要发送数据的编号。
- 确认序号表示我收到你上次发的报文,并希望你下一次给我发什么
所以双方发出的报文当中,不仅需要填充32位序号来表明自己当前发送数据的序号。还需要填充32位确认序号,对对方上一次发送的数据进行确认,告诉对方下一次应该从哪一字节序号开始进行发送。
总结:
- 32位序号:发送报文的编号,同时保证数据的按序到达。
- 32位确认序号:告诉对端当前已经收到的字节数据,以及对端下一次发送数据时应该从哪一字节序号开始进行发送。
- 序号和确认序号是确认应答机制的数据化表示,确认应答机制就是由序号和确认序号来保证的。
- 此外,通过序号和确认序号还可以判断某个报文是否丢失。
2.3 4位首部长度
前面也介绍过,4位首部长度就是整个TCP报文的大小。单位为4字节。所以4bit的数据能表示的范围是【0,60】。但是TCP报文大小至少为20,最多为60。所以4位首部长度的取值范围是【5,15】。
2.4 6个保留位
主要用于以后可能的扩展和存储新的属性和数据。
当TCP协议需要引入新功能时,就可以使用这些保留位,而不需要对报头结构进行大规模修改,从而减少对现有设备和系统的影响。
2.5 6个标志位
网络传输中,报文也分很多种类,有的是为了建立连接,有的是为了数据传输,有的是为了断开连接。
而针对不同的报文,我们也要有不同的处理动作,例如数据传输的报文,TCP会将数据交给上层,建立连接的报文,TCP会完成三次握手的动作,断开连接,TCP会完成四次挥手。
针对不同类型,TCP使用6个bit位来对不同类型的报文进行标志。
SYN:
- 报文当中的SYN被设置为1,表明该报文是一个连接建立的请求报文。
- 只有在连接建立阶段,SYN才被设置,正常通信时SYN不会被设置
ACK:
- 报文当中的ACK被设置为1,表明该报文可以对收到的报文进行确认。
- 一般除了第一个请求报文没有设置ACK以外,其余报文基本都会设置ACK,因为发送出去的数据本身就对对方发送过来的数据具有一定的确认能力,因此双方在进行数据通信时,可以顺便对对方上一次发送的数据进行响应。
FIN:
- 报文当中的FIN被设置为1,表明该报文是一个连接断开的请求报文。
- 只有在断开连接阶段,FIN才被设置,正常通信时FIN不会被设置。
URG:
- 报文当中URG被设置为1,是告诉对方这个数据是要特殊尽快处理。
- 因为TCP是可靠传输,所以数据段一定是有序的被接收方收到,但是如果有数据段想要插队就可以设置URG。
表示TCP包的紧急指针域有效,用于标识紧急数据,确保TCP连接不被中断,并催促中间设备尽快处理。
接受端在接受到数据的时候,是按顺序读写的。如果我们有紧急数据想尽快向上交付,就可以设置URG标志,并且填写TCP报文中的紧急指针字段,标识特定的某一个字符。
URG一般用来发送带外数据,它不用走TCP流,因为接收方会直接处理。例如A想请求B的一个服务,B正在给A返回,但是A突然不想要了,就会给B发送一个URG标志(配合紧急指针),然后关闭套接字。
当我们使用send接口的时候会发现,其中有个字段是flag,flag有个选项MSG_OOB(out-of-band)可以帮助我们发送紧急数据。
同样的recv也有个flag字段,其中有个MSG_OOB选项,可以帮上层接受紧急数据。
PSH:
- 报文当中的PSH被设置为1,是在告诉对方上层尽快去取走数据。
- 因为可能接收方的窗口值比较小,而发送发就需要阻塞等待接收方取走缓冲区的数据后才能发送,那么此时就可以用PSH标志位来催促。
我们首先要明白一个概念。TCP拥有发送和接受缓冲区,我们调用send和recv,并不是直接将数据交给对方,或者从对方那拿去数据,而是将数据交给TCP的缓冲区中。
如果我们得知对方接受缓冲区的剩余大小不足时,就可以发送PSH标志位,催促对方赶紧取走缓冲区中的数据。
当然,也并不是只有对方没空间的时候才发,我们可以给每一条数据都发送PSH标志位。这样每一条数据都会被尽快向上交付。
RST:
- 报文当中的RST被设置为1,表示需要让对方重新建立连接。
- 在通信双方在连接未建立好的情况下,一方向另一方发数据,此时另一方发送的响应报文当中的RST标志位就会被置1,表示要求对方重新建立连接。
- 还有一种情况是服务端网线被把了,连接被断开了,但是客户端不知道,还会发消息,这时服务端就会把RST设置为1,让客户端建立一个新连接。
2.6 16位窗口大小
TCP具有发送缓冲区和接受缓冲区。
- 当应用层调用了send/write这样的输出型接口时,实际上是将数据拷贝到TCP的发送缓冲区中。
- 当应用层调用了recvc/read这样的输入型接口时,实际上是从TCP的接受缓冲区中拿取的。
这样会导致两种情况:
- 1.发送速度过快:接受缓冲区满了后,收到的其他报文直接丢弃。
- 2.发送速度过慢:影响效率
所以确定一个合适的发送速度很重要。发送速度主要还是要看对方接受缓冲区剩余空间的大小,如果空间足够我就发快一点,空间不够就慢一点。
如何得知对方接受缓冲区剩余空间的大小?
通过16位窗口大小通知对方接受缓冲区剩余空间的大小,发送端收到后就可以通过窗口大小来控制发送速度。
- 窗口大小字段越大,说明接收端接收数据的能力越强,此时发送端可以提高发送数据的速度。
- 窗口大小字段越小,说明接收端接收数据的能力越弱,此时发送端可以减小发送数据的速度。
- 如果窗口大小的值为0,说明接收端接收缓冲区已经被打满了,此时发送端就不应该再发送数据了。
因为窗口有16位,所以窗口最大的内存位64k。如果数据量太大了就可以用选项字段的一些选项把窗口扩大。
如果窗口大小为0,发送端停止发送,那么他如何得知什么时候可以继续发送?
- 当接收端的接受缓冲区有足够空间时,会给发送端发送一个不携带有效数据的报文,报文中填写16位窗口大小,通知对方可以继续发了。
- 发送端也会定期向接收端发送报文,因为确认应答机制(所有请求都必须有响应),所以接收端也会发送响应报文,响应报文中携带缓冲区剩余大小。
2.7 16位校验位
16位校验位的主要作用是确保TCP报文在传输过程中的数据准确性(数据没有发生改变)。这个校验位由发送端填充,而接收端则会对TCP报文段执行CRC(循环冗余校验)算法以检验TCP报文段在传输过程中是否损坏。
如果接收端在校验过程中发现数据存在问题,它可能会要求发送端重新发送该报文段,以确保数据的准确性和完整性。
2.8 16位紧急指针
在TCP报文中,16位紧急指针(Urgent Pointer)的主要作用是在TCP连接中提供紧急数据的机制。
当发送端需要发送一些紧急数据时,可以设置紧急指针来指示接收端,在接收到该指针之后尽快处理这些数据。紧急指针的值是一个相对于当前序列号的偏移量,用于指示紧急数据在整个数据流中的位置。
通常要搭配URG标志位来使用。
2.9 选项
CP报文中的选项部分是为了适应复杂的网络环境和更好地服务应用层而设计的。这些选项提供了额外的信息或功能,用于增强TCP协议的性能、安全性或满足特定应用的需求。
具体来说,TCP选项的作用包括但不限于以下几个方面:
- 扩展TCP功能:通过选项,TCP协议可以添加一些额外的功能,如窗口缩放(Window Scale)、时间戳(Timestamps)等。这些功能可以帮助TCP更好地适应高带宽、低延迟的网络环境,提高数据传输的效率和可靠性。
- 安全性增强:一些TCP选项可以用于增强安全性,如最大段大小(MSS)选项可以帮助防止TCP分段攻击,而安全套接字层(SSL)或传输层安全性(TLS)协议则使用TCP选项来协商加密参数和建立安全连接。
- 流量控制:TCP选项中的窗口缩放选项可以允许更大的TCP接收窗口(16位窗口大小),从而允许更多的数据在传输过程中被缓存,减少因网络拥塞而导致的丢包和重传。
- 性能优化:TCP选项还可以用于优化性能,如快速打开(Fast Open)选项可以减少建立TCP连接所需的时间,从而提高应用程序的响应速度。
需要注意的是,TCP选项是可选的,并且不是所有的TCP实现都支持所有的选项。此外,一些选项可能只在特定的网络环境中或特定的应用程序中使用。因此,在使用TCP选项时需要根据具体情况进行选择和配置。
3. TCP可靠性机制
3.1 确认应答(ACK)
确认应答机制是TCP保证可靠性的重要机制。通常是由TCP报文中的32位序号和确认序号保证的。
TCP对数据进行了编号,这个编号被称为序列号,例如我们现在发送一个序列号为1001的报文,大小为1000个字节,也就是说这次发送的数据的编号为1001-2000。接受端在收到这个报文后,可以给客户端发送一个确认序号为2001,表明2001之前的数据我都收到了。
通过TCP确认应答机制,发送方可以确保自己的数据被接收方正确接收,接收方也可以确保自己接收到的数据是完整且没有错误的。这种机制保证了TCP协议在数据传输过程中的可靠性和准确性。
3.2 超时重传
在网络通信中,出现丢包的问题在所难免。丢包分为两种情况
情况一:A给B发送数据的时候出现丢包。
情况二:B给A发送响应的时候出现丢包。
- 站在A的角度来看,他并不知道是上面哪一种情况丢的报文。当经过一段时间后,A会启动超时重传机制。给B重新发一份报文。
- 但是如果是上面情况二下丢的包,B就会收到两份相同的报文。此时B就会根据报文的32位序列号判断之前是否收到过相同的报文,如果是就去重。
- 由于可能会出现丢包的情况,所以发送端在发送数据后也不能立即将缓冲区中的内容释放。而是应该等待一段时间,如果收到了B的应答再释放。如果收不到就超时重传。
超时重传等待的时间
最理想的情况下, 找到一个最小的时间, 保证 "确认应答一定能在这个时间内返回",但是这个时间的长短, 随着网络环境的不同, 是有差异的,如果超时时间设的太长, 会影响整体的重传效率;如果超时时间设的太短, 有可能会频繁发送重复的包;
TCP为了保证无论在任何环境下都能比较高性能的通信, 因此会动态计算这个最大超时时间
- Linux中(BSD Unix和Windows也是如此), 超时以500ms为一个单位进行控制, 每次判定超时重发的超时时间都是500ms的整数倍.
- 如果重发一次之后, 仍然得不到应答, 等待 2*500ms 后再进行重传.
- 如果仍然得不到应答, 等待 4*500ms 进行重传. 依次类推, 以指数形式递增.
- 累计到一定的重传次数, TCP认为网络或者对端主机出现异常, 强制关闭连接
3.3 连接管理
3.3.1 TCP是面向连接的
TCP(Transmission Control Protocol,传输控制协议)是一种面向连接的协议。这种面向连接的方法意味着在两个端点之间建立了一条数据通信信道(或称为电路)。这条信道提供了一条在网络上顺序发送报文分组的预定义路径,类似于语音电话通信中的线路连接。
TCP连接只能是点到点的(一对一)。不允许多个发送方同时连接到一个接收方。同样地,也不允许一个发送方同时建立多个连接。。比如一台服务器启动后可能有多个客户端前来访问,如果TCP不是基于连接的,也就意味着服务器端只有一个接收缓冲区,此时各个客户端发来的数据都会拷贝到这个接收缓冲区当中,此时这些数据就可能会互相干扰。
3.3.2 TCP为什么要建立连接
TCP 需要建立连接是为了确保数据传输的可靠性、实现流量控制、错误控制、拥塞控制以及全双工通信等功能。这些功能使得 TCP 成为了一个广泛应用于各种网络环境中的可靠传输协议。
3.3.3 操作系统管理连接
在进行网络通信时,一定会存在大量的连接,那么操作系统如何管理大量的连接?
六字真言:“先描述再组织”,操作系统可以创建一个描述连接的结构体,填写具体的属性字段,在通过数据结构组织起来,此后,对连接的管理就变成了对数据结构的增删查改了。
- 建立连接:创建一个结构化的对象,然后填写属性字段,并将该对象插入到数据结构当中(如链表)。
- 数据传输:连接建立成功后,操作系统需要负责数据的传输。包括从发送方接收数据、将数据打包成TCP报文段、将数据发送到网络以及从网络接收数据并传递给接收方等过程。
- 维护连接:它通过使用心跳机制、超时重传等技术来监测连接的健康状况,并在连接出现异常时进行恢复。
- 断开连接:将相应的连接对象从连接队列中删除,并且释放某些资源。
3.3.4 TCP三次握手
双方在进行TCP通信之前需要先建立连接,建立连接的这个过程我们称之为三次握手。
- 第一次握手:客户端向服务器发送的报文当中的SYN位被设置为1,表示请求与服务器建立连接。
- 第二次握手:服务器收到客户端发来的连接请求报文后,紧接着向客户端发起连接建立请求并对客户端发来的连接请求进行响应,此时服务器向客户端发送的报文当中的SYN位和ACK位均被设置为1。
- 第三次握手:客户端收到服务器发来的报文后,得知服务器收到了自己发送的连接建立请求,并请求和自己建立连接,最后客户端再向服务器发来的报文进行响应。
客户端给服务端发送连接请求时,只会建立从客户端到服务端的连接信道,TCP对通信双方都是等价的,服务端在收到连接请求后,也要向客户端发起连接请求,建立从服务端到客户端的连接信道。
需要注意的是,每次发送的都是一个完整的TCP报文,而不是只发送了标志位。
连接建立失败?
连接建立不是百分百成功的,中间可能会出现数据丢包的问题。如果是前两次数据丢包,我们并不担心,因为TCP有超时重传机制,当一定时间没有收到ACK响应,发送端就会重新发送一份报文。
但是如果是第三次的ACK报文丢失呢。
服务端没有到ACK报文就不会建立连接,也就是说建立连接失败了。但是也不用担心,TCP已经想好了解决方案:
- 服务端发送的第二条报文没有收到ACK,他就会超时重传,客户端收到后就知道数据丢包了。
- 客户端向服务端方向上的信道已经建立好了,如果客户端给服务端发消息,服务端就知道,客户端那边连接已经建立成功了,但是给自己发送的ACK响应丢包了。
一次两次行不行?
TCP为什么是三次握手,而不是一次/两次握手呢?
首先先分析一次的情况,如果客户端发送SYN报文,请求建立连接后,双方就都连接连接。
- 无法保证服务端成功收到了请求,服务端可能已经收到了连接请求,但是无法给予相应报文,客户端以为服务端没有收到,于是重新发送,服务端在收到新的连接请求时,可能会认为这是一个新的连接请求,于是重新建立连接,浪费了很多的资源。
- 如果攻击者会伪造大量的源IP地址和SYN请求,向目标服务器发送大量的TCP连接请求。TCP使用1次握手来建立连接,那么服务器在收到SYN请求后就会立即认为连接已经建立,并开始分配资源来处理这个连接,随着时间的推移,服务器上的资源会被逐渐耗尽,无法为正常用户提供服务。这种现象叫做SYN洪水。
两次的情况也类似,客户端可能并不会收到ACK响应,如果收不到ACK响应,就和上面一次的情况一样了。
四次握手行不行?
这里的四次握手指的是在服务端收到ACK时,再给客户端发一个ACK响应。
首先,三次握手已经可以保证连接能够建立成功的,如果再添加一次就有点画蛇添足了。
其次,如果服务端是在给客户端第三次握手(发送ACK响应)时就建立了连接,客户端在第四次握手(收到服务端ACK)时才建立连接。但是客户端是可能收不到ACK的,也就是说四次握手可能会出现服务端连接建立好而客户端没建立连接的情况。
我们知道连接建立和维护都是需要消耗资源的。如果一个客户端向服务端发送大量的请求,消耗服务器资,导致SYN洪水问题。
为什么是三次?
在前面两次我们分析了一次,两次,四次都不行,那么为什么就是三次呢。
1.以最少的次数验证TCP全双工,保证通信的流畅
TCP是全双工的。如果是三次握手,能以最少的次数保证双方的发送和接受能力都是正常的。
- 第一次握手,客户端视角:并不能知道对方收没收到(不能保证发送和接受能力),服务端视角:收到了SYN请求(能保证接受能力)
- 第二次握手,客户端视角:刚刚的SYN对方收到了,并且收到了对方的SYN+ACK报文(发送和接受能力都能保证),服务端视角:并不知道对方收没收到(能保证接受能力)
- 第三次握手,服务端视角:刚刚的报文对方收到了(发送和接受能力都能保证)。
2.一定程度上防止SYN洪水攻击。
在上面我们分析到,如果是偶数次握手,会出现客户端没有建立连接,而服务端建立连接的情况,这样会导致服务端花费大量资源维护连接。并且有可能会出现SYN洪水攻击。
如果是三次握手,客户端第三次握手(发送ACK响应),只要报文一发送就会建立连接,而服务端只有在收到ACK报文时才建立连接。这样可以一定程度防止SYN洪水攻击(不可能完全避免)。如果服务器在一定时间内没有收到ACK包,就会认为这个连接请求是无效的,并释放相应的资源。这样,即使攻击者发送了大量的伪造SYN请求,服务器也不会立即耗尽资源,从而提高了系统的安全性。
3.互相保证对方已经收到了连接请求
三次握手的本质其实是四次握手。
只不过在第二次握手时采用了捎带应答机制,将ACK和SYN封装成一个报文了。如果将他看成四次握手,那么我们就能保证通信双方都已经收到了连接请求,因为每次请求都有ACK。
三次握手过程中的状态变化
- 开始客户端和服务端的状态都为CLOSED。
- 服务端调用listen函数,设置为监听状态,状态变为LISTEN
- 客户端发起连接建立请求后,状态变为SYN_SENT。服务端收到SYN请求后,状态变为SYN_RCVD。
- 服务端向客户端发起SYN+ACK。客户端收到后,向服务端发送ACK响应,然后状态变为ESTABLISHED,表示连接建立成功,可以正常通信了(客户端到服务端方向上)。
- 服务端在收到ACK响应时,状态变为ESTABLISHED,表示连接建立成功,可以正常通信了(服务端到客户端方向上)。
套接字与三次握手之间的关系
- 在客户端发起连接建立请求之前,服务器需要先进入LISTEN状态,此时就需要服务器调用对应listen函数。
- 当服务器进入LISTEN状态后,客户端就可以向服务器发起三次握手了,此时客户端对应调用的就是connect函数。
- 需要注意的是,connect函数不参与底层的三次握手,connect函数的作用只是发起三次握手。当connect函数返回时,要么是底层已经成功完成了三次握手连接建立成功,要么是底层三次握手失败。
- 如果服务器端与客户端成功完成了三次握手,此时在服务器端就会建立一个连接,但这个连接在内核的等待队列当中,服务器端需要通过调用accept函数将这个建立好的连接获取上来。
- 当服务器端将建立好的连接获取上来后,双方就可以通过调用read/recv函数和write/send函数进行数据交互了。
如何理解connect函数不参与底层三次握手
客户端调用connect()
函数,意图与服务器建立TCP连接。connect()
函数将连接请求传达给操作系统内核,并告诉内核要连接的服务器的IP地址和端口号。操作系统在底层自动完成三次握手的过程,connect()
函数在TCP连接建立完成之前会阻塞(除非设置了非阻塞模式)。
3.3.5 TCP四次挥手
当TCP断开连接时,需要进行四次挥手的过程。
通信双方谁想断开连接,就给对方发送FIN报文,表示想要断开连接,对方收到FIN时,会发送一个ACK响应,并且也发送一个FIN报文。
- 第一次挥手:客户端向服务器发送的报文当中的FIN位被设置为1,表示请求与服务器断开连接。
- 第二次挥手:服务器收到客户端发来的断开连接请求后对其进行响应。
- 第三次挥手:服务器收到客户端断开连接的请求,且已经没有数据需要发送给客户端的时候,服务器就会向客户端发起断开连接请求。
- 第四次挥手:客户端收到服务器发来的断开连接请求后对其进行响应。
四次挥手完成,双方才是真正意义上的断开连接。
为什么时四次挥手
- 因为通信信道时是双向的,断开连接时,在两个方向上都需要哦断开,如果是客户端给服务端发送FIN请求,并且收到了ACK应答,就会关闭从客户端到服务器方向上的连接,表示我(应用层)不会再给你发送数据了,接着服务端会给客户端发送FIN请求,并在收到ACK应答时断开服务器到客户端方向上的连接。
- 第二次和第三次挥手不能合并成一个,因为第三次握手是服务器端想要与客户端断开连接时发给客户端的请求,而当服务器收到客户端断开连接的请求并响应后,服务器不一定会马上发起第三次挥手,因为服务器可能还有某些数据要发送给客户端,只有当服务器端将这些数据发送完后才会向客户端发起第三次挥手。
四次挥手时的状态变化
- 在挥手前客户端和服务器都处于连接建立后的ESTABLISHED状态。
- 客户端为了与服务器断开连接主动向服务器发起连接断开请求,此时客户端的状态变为FIN_WAIT_1。
- 服务器收到客户端发来的连接断开请求后对其进行响应,此时服务器的状态变为CLOSE_WAIT。
- 当服务器没有数据需要发送给客户端的时,服务器会向客户端发起断开连接请求,等待最后一个ACK到来,此时服务器的状态变为LASE_ACK。
- 客户端收到服务器发来的第三次挥手后,会向服务器发送最后一个响应报文,此时客户端进入TIME_WAIT状态。
- 当服务器收到客户端发来的最后一个响应报文时,服务器会彻底关闭连接,变为CLOSED状态。
- 而客户端则会等待一个2MSL(Maximum Segment Lifetime,报文最大生存时间)才会进入CLOSED状态。
四次挥手结束,双方断开连接。
套接字和四次挥手之间的关系
客户端发起断开连接请求,对应就是客户端主动调用close函数关闭套接字。
服务器发起断开连接请求,对应就是服务器主动调用close函数关闭套接字。
一个close对应的就是两次挥手,双方都要调用close,因此就是四次挥手。
CLOSE_WAIT状态
- 双方在进行四次挥手时,如果只有客户端(主动发起断开连接)调用了close函数,而服务器不调用close函数,此时服务器就会进入CLOSE_WAIT状态,而客户端则会进入到FIN_WAIT_2状态。
- 但只有完成四次挥手后连接才算真正断开,此时双方才会释放对应的连接资源。如果服务器没有主动关闭不需要的文件描述符,此时在服务器端就会存在大量处于CLOSE_WAIT状态的连接,而每个连接都会占用服务器的资源,最终就会导致服务器可用资源越来越少。
- 因此如果不及时关闭不用的文件描述符,除了会造成文件描述符泄漏以外,可能也会导致连接资源没有完全释放,这其实也是一种内存泄漏的问题。
- 因此在编写网络套接字代码时,如果发现服务器端存在大量处于CLOSE_WAIT状态的连接,此时就可以检查一下是不是服务器没有及时调用close函数关闭对应的文件描述符。
TIME_WAIT状态
主动断开连接的一方会进入TIME_WAIT状态,顾名思义,就是会在这个状态上等待一段时间。
为什么都已经收到FIN报文了,为什么不尽快ACK,断开连接?
前3次发生丢包问题,我们根本不担心,因为有超时重传机制,但是如果第四次直接发送ACK报文,就进入CLOSED状态,表明连接已经断开成功了,我也不再发送和接受来自你的数据了,也就表明如果发生了丢包,服务器就得不到响应,也就不会立刻关闭连接。而是会不断询问客户端,一直没有答复的话一段时间后也会自动关闭。但是连接的维护也是需要成本的。
为了避免上面出现的这种问题,于是设置了TIME_WAIT状态,保证最后一个报文被对方收到。并且在断开连接之前,通信信道上可能还有丢包的报文,有了TIME_WAIT状态,也可以促进这些数据尽可能地消散。
- TIME_WAIT的时间应该设置为多少呢?
TCP协议规定,主动关闭连接的一方在四次挥手后要处于TIME_WAIT状态,等待两个MSL(报文最大生存时间)的时间才能进入CLOSED状态。
我们把从发送方到接收方经过的最大时间叫做MSL。
TIME_WAIT状态持续存在2MSL的话,就能保证在两个传输方向上的尚未被接收或迟到的报文段都已经消失。
Centos7上默认配置的值是60s。
bind失败的问题
我们写服务器的时候,有时候会出现bind失败的情况。其实就是因为主动断开连接的一方会出现TIME_WAIT状态,所以端口会被占用,也就导致bind失败。
我们之前的做法都是换个端口,如果不想换呢
使用setsockopt()设置socket描述符的 选项SO_REUSEADDR为1, 表示允许创建端口号相同但IP地址不同的多个socket描述符
TCP3次挥手
TCP是可能出现3次挥手的。当TCP连接的被动关闭方(例如服务器)没有数据要发送,并且开启了TCP延迟确认机制时,第二次 ACK 和第三次 FIN 可以合并传输,从而出现了三次挥手的情况。
比如双方同时调用close,就会出现这种情况。
3.3.6 TCP三次握手四次挥手流程
服务端状态变化:
- [CLOSED -> LISTEN] 服务器端调用listen后进入LISTEN状态, 等待客户端连接;
- [LISTEN -> SYN_RCVD] 一旦监听到连接请求, 就将该连接放入内核等待队列中,并向客户端发送SYN确认报文。
- [SYN_RCVD -> ESTABLISHED] 服务端一旦收到客户端的确认报文, 就进入ESTABLISHED状态, 可以进行读写数据了.
- [ESTABLISHED -> CLOSE_WAIT] 当客户端主动关闭连接(调用close), 服务器会收到结束报文段, 服务器返回确认报文段并进入CLOSE_WAIT;
- [CLOSE_WAIT -> LAST_ACK] 进入CLOSE_WAIT后说明服务器准备关闭连接(需要处理完之前的数据); 当服务器真正调用close关闭连接时, 会向客户端发送FIN, 此时服务器进入LAST_ACK状态, 等待最后一个ACK到来(这个ACK是客户端确认收到了FIN)
- [LAST_ACK -> CLOSED] 服务器收到了对FIN的ACK, 彻底关闭连接.
客户端状态变化:
- [CLOSED -> SYN_SENT] 客户端调用connect, 发送同步报文段;
- [SYN_SENT -> ESTABLISHED] connect调用成功, 则进入ESTABLISHED状态, 开始读写数据;
- [ESTABLISHED -> FIN_WAIT_1] 客户端主动调用close时, 向服务器发送结束报文段, 同时进入FIN_WAIT_1;
- [FIN_WAIT_1 -> FIN_WAIT_2] 客户端收到服务器对结束报文段的确认, 则进入FIN_WAIT_2, 开始等待服务器的结束报文段;
- [FIN_WAIT_2 -> TIME_WAIT] 客户端收到服务器发来的结束报文段, 进入TIME_WAIT, 并发出LAST_ACK;
- [TIME_WAIT -> CLOSED] 客户端要等待一个2MSL(Max Segment Life, 报文最大生存时间)的时间, 才会进入CLOSED状态.
3.4 流量控制
接收端处理数据的速度是有限的. 如果发送端发的太快, 导致接收端的缓冲区被打满, 这个时候如果发送端继续发送,就会造成丢包, 继而引起丢包重传等等一系列连锁反应.
因此TCP支持根据接收端的处理能力, 来决定发送端的发送速度. 这个机制就叫做流量控制(Flow Control);
- 接收端将自己可以接收的缓冲区大小放入 TCP 首部中的 "窗口大小" 字段, 通过ACK端通知发送端;
- 窗口大小字段越大, 说明网络的吞吐量越高;
- 接收端一旦发现自己的缓冲区快满了, 就会将窗口大小设置成一个更小的值通知给发送端;发送端接受到这个窗口之后, 就会减慢自己的发送速度;
- 如果接收端缓冲区满了, 就会将窗口置为0; 这时发送方不再发送数据, 但是需要定期发送一个窗口探测数据段, 使接收端把窗口大小告诉发送端
当接收端缓冲区满了,会将窗口大小设置为0,告知发送端。此后发送端通过两种方式得知接收端的接受缓冲区什么时候有空间。实际中哪种方式先到达就用哪种。
- 等待告知:接收端上层将接收缓冲区当中的数据读走后,接收端向发送端发送一个TCP报文,主动将自己的窗口大小告知发送端,发送端得知接收端的接收缓冲区有空间后就可以继续发送数据了。
- 主动询问:发送端每隔一段时间向接收端发送报文(窗口探测),该报文不携带有效数据,只是为了询问发送端的窗口大小,直到接收端的接收缓冲区有空间后发送端就可以继续发送数据了。
发送端第一次正常通信时如何得知对方的缓冲区剩余大小?
在TCP三次握手期间,双方已经互相交换过自己的缓冲区大小了。
16位的窗口大小,表示最大传输64K的数据吗?
TCP报头当中40字节的选项字段中包含了一个窗口扩大因子M,实际窗口大小是窗口字段的值左移M位得到的。
3.5 拥塞控制
当数据出现丢包时,我们可能会想到超时重传机制帮我们重新发送数据。但是针对大面积丢包问题呢?如果你发了1000个报文结果999个都丢包了,那怎么办。
针对大面积丢包问题,TCP会认为这是网络的问题,此时就没必要再重传了。
减少网络拥塞
- 当网络出现拥塞问题时,通信双方虽然不能提出特别有效的解决方案,但双方主机可以做到不加重网络的负担。
- 双方通信时如果出现大量丢包,不应该立即将这些报文进行重传,而应该少发数据甚至不发数据,等待网络状况恢复后双方再慢慢恢复数据的传输速率。
需要注意的是,网络拥塞时影响的不只是一台主机,而几乎是该网络当中的所有主机,此时所有使用TCP传输控制协议的主机都会执行拥塞避免算法。
拥塞控制
TCP引入 慢启动 机制, 先发少量的数据, 探探路, 摸清当前的网络拥堵状态, 再决定按照多大的速度传输数据;
- 此处引入一个概念程为拥塞窗口
- 发送开始的时候, 定义拥塞窗口大小为1;
- 每次收到一个ACK应答, 拥塞窗口加1;
- 每次发送数据包的时候, 将拥塞窗口和接收端主机反馈的窗口大小做比较, 取较小的值作为实际发送的窗口(就是前面说的滑动窗口的大小 = 对方接受能力和拥塞窗口的较小值);
像上面这样的拥塞窗口增长速度, 是指数级别的. "慢启动" 只是指初使时慢, 但是增长速度非常快
- 为了不增长的那么快, 因此不能使拥塞窗口单纯的加倍.
- 此处引入一个叫做慢启动的阈值当拥塞窗口超过这个阈值的时候, 不再按照指数方式增长, 而是按照线性方式增长
- 当TCP开始启动的时候, 慢启动阈值等于窗口最大值;
- 在每次超时重发的时候, 慢启动阈值会变成原来的一半, 同时拥塞窗口置回1;
前期慢开始是为了让网络自主恢复,后面指数增长是为了尽快恢复通信。
- 指数增长。刚开始进行TCP通信时拥塞窗口的值为1,并不断按指数的方式进行增长。
- 加法增大。慢启动的阈值初始时为对方窗口大小的最大值,图中慢启动阈值的初始值为16,因此当拥塞窗口的值增大到16时就不再按指数形式增长了,而变成了的线性增长。
- 乘法减小。拥塞窗口在线性增长的过程中,在增大到24时如果发生了网络拥塞,此时慢启动的阈值将变为当前拥塞窗口的一半,也就是12,并且拥塞窗口的值被重新设置为1,所以下一次拥塞窗口由指数增长变为线性增长时拥塞窗口的值应该是12。
拥塞窗口的意义就是检测网络健康状态。
每台主机认为拥塞窗口的大小不一定是一样的,即便是同区域的两台主机在同一时刻认为拥塞窗口的大小也不一定是完全相同的。因此在同一时刻,可能一部分主机正在进行正常通信,而另一部分主机可能已经发生网络拥塞了。
4. TCP提高性的机制
4.1 滑动窗口
TCP滑动窗口是TCP协议中的一个重要机制,主要用于控制和管理发送方和接收方之间的数据传输。它是TCP实现流量控制和拥塞控制的基础,并允许发送方和接收方之间实现流量控制和可靠性传输。
TCP可能会同时发送多个报文。于是我们可以将发送缓冲区分成这几个部分。
- 已经发送并且已经收到ACK的数据。
- 已经发送还但没有收到ACK的数据。
- 还没有发送的数据。
滑动窗口的本质其实就是发送缓冲区中间的一段,通过不断地滑动来划分这三个区域。
如何理解滑动窗口
因为接受缓冲区就是一个数组,而滑动窗口又是接受缓冲区的一个区域,所以我们可以用指针(数组下标)来表示,窗口滑动即是对指针的修改。
滑动窗口的大小
滑动窗口的大小和对方的接收能力有关,还和网络状况有关,实际上,滑动窗口的大小 = 对方接受能力和拥塞窗口的较小值
滑动窗口的大小如何变化
1.可能变大:当对方接受能力较强,网络状态较好时,发送端就会给接收端继续发送数据,若收到的报文数 < 发送的报文数时,滑动窗口就会变大。滑动窗口越大,则网络的吞吐率越高,同时也说明对方的接收能力很强。
2.可能不变:当发送速度等于接受速度,或者没有收到消息确认(ACK应答)并且对方缓冲区满了的情况下(不会发送数据),滑动窗口大小可能不变。
3.可能变小:如果对方缓冲区已满,并且自己收到了ACK应答,滑动窗口大小会变小。
滑动窗口滑动方向
1.当收到ACK应答时,滑动窗口就会向右滑动。
2.保持不动
既没有发送,也没有收到应答时,滑动窗口不移动。
滑动窗口不可能左移。首先左侧的下标不可能左移,因为左移会将已经确认应答的数据变成没有确认应答的;右侧也不可能,左移代表着数据没有发送过,显然不可能。
报文丢失怎么办
情况1:数据包已经抵达, ACK被丢了.
这种情况不必担心,因为确认序号的作用就是告诉对方序号之前的数据都已经收到了,1001的报文丢了没关系,收到2001的报文时,客户端就知道1-1000的数据对方已经收到了,不必再发了。
情况2:数据包就直接丢了.
- 当某一段报文段丢失之后, 发送端会一直收到 1001 这样的ACK, 就像是在提醒发送端 "我想要的是 1001"一样;
- 如果发送端主机连续三次收到了同样一个 "1001" 这样的应答, 就会将对应的数据 1001 - 2000 重新发送;
- 这个时候接收端收到了 1001 之后, 再次返回的ACK就是7001了(因为2001 - 7000)接收端其实之前就已经收到了, 被放到了接收端操作系统内核的接收缓冲区中;
上面这种机制叫做“快重传”
快重传需要在大量的数据重传和个别的数据重传之间做平衡,实际这个例子当中发送端并不知道是1001-2000这个数据包丢了,当发送端重复收到确认序号为1001的响应报文时,理论上发送端应该将1001-7000的数据全部进行重传,但这样可能会导致大量数据被重复传送,所以发送端可以尝试先把1001-2000的数据包进行重发,然后根据重发后的得到的确认序号继续决定是否需要重发其它数据包。
滑动窗口中的数据对方收到了吗?
滑动窗口当中可能有一部分数据已经被对方收到了,但可能因为滑动窗口内靠近滑动窗口左侧的一部分数据,在传输过程中出现了丢包等情况,导致后面已经被对方收到的数据得不到响应。
例如图中的1001-2000的数据包如果在传输过程中丢包了,此时虽然2001-5000的数据都被对方收到了,此时对方发来的确认序号也只能是1001,当发送端补发了1001-2000的数据包后,对方发来的确认序号就会变为5001,此时发送缓冲区当中1001-5000的数据也会立马被归置到滑动窗口的左侧。
超时重传和快重传
- 快重传是能够快速进行数据的重发,当发送端连续收到三次相同的应答时就会触发快重传,而不像超时重传一样需要通过设置重传定时器,在固定的时间后才会进行重传。
- 虽然快重传能够快速判定数据包丢失,但快重传并不能完全取待超时重传,因为有时数据包丢失后可能并没有收到对方三次重复的应答,此时快重传机制就触发不了,而只能进行超时重传。
- 因此快重传虽然是一个效率上的提升,但超时重传却是所有重传机制的保底策略,也是必不可少的。
4.2 延迟应答
如果接收数据的主机立刻返回ACK应答, 这时候返回的窗口可能比较小.
- 假设接收端缓冲区为1M. 一次收到了500K的数据; 如果立刻应答, 返回的窗口就是500K;
- 但实际上可能处理端处理的速度很快, 10ms之内就把500K数据从缓冲区消费掉了;
- 在这种情况下, 接收端处理还远没有达到自己的极限, 即使窗口再放大一些, 也能处理过来;
- 如果接收端稍微等一会再应答, 比如等待200ms再应答, 那么这个时候返回的窗口大小就是1M;
一定要记得, 窗口越大, 网络吞吐量就越大, 传输效率就越高. 我们的目标是在保证网络不拥塞的情况下尽量提高传输效率;
延迟应答的方式:
- 数量限制: 每隔N个包就应答一次;
- 时间限制: 超过最大延迟时间就应答一次;
具体的数量和超时时间, 依操作系统不同也有差异; 一般N取2, 超时时间取200ms;
4.3 捎带应答
在延迟应答的基础上, 我们发现, 很多情况下, 客户端服务器在应用层也是 "一发一收" 的. 意味着客户端给服务器说了 "How are you", 服务器也会给客户端回一个 "Fine, thank you";
那么这个时候ACK就可以搭顺风车, 和服务器回应的 "Fine, thank you" 一起回给客户端。
这种既发送数据又携带响应的方式就叫做捎带应答。
我们前面说TCP3次握手可以看作是4次握手。原因就是2次握手实际上就是一次捎带应答。
5. TCP衍生问题
5.1 面向字节流
- 调用write时, 数据会先写入发送缓冲区中;
- 如果发送的字节数太长, 会被拆分成多个TCP的数据包发出;
- 如果发送的字节数太短, 就会先在缓冲区里等待, 等到缓冲区长度差不多了, 或者其他合适的时机发送出去;
- 接收数据的时候, 数据也是从网卡驱动程序到达内核的接收缓冲区;
- 然后应用程序可以调用read从接收缓冲区拿数据;
- 另一方面, TCP的一个连接, 既有发送缓冲区, 也有接收缓冲区, 那么对于这一个连接, 既可以读数据, 也可以写数据. 这个概念叫做 全双工
由于缓冲区的存在, TCP程序的读和写不需要一一匹配
- 写100个字节数据时, 可以调用一次write写100个字节, 也可以调用100次write, 每次写一个字节;
- 读100个字节数据时, 也完全不需要考虑写的时候是怎么写的, 既可以一次read 100个字节, 也可以一次read一个字节, 重复100次
5.2 粘包问题
- 首先要明确, 粘包问题中的 "包" , 是指的应用层的数据包.
- 在TCP的协议头中, 没有如同UDP一样的 "报文长度" 这样的字段, 但是有一个序号这样的字段.
- 站在传输层的角度, TCP是一个一个报文过来的. 按照序号排好序放在缓冲区中.
- 站在应用层的角度, 看到的只是一串连续的字节数据.
- 那么应用程序看到了这么一连串的字节数据, 就不知道从哪个部分开始到哪个部分, 是一个完整的应用层数据包
那么如何避免粘包问题呢? 归根结底就是一句话, 明确两个包之间的边界.
- 对于定长的包, 保证每次都按固定大小读取即可; 例如上面的Request结构, 是固定大小的, 那么就从缓冲区从头开始按sizeof(Request)依次读取即可;
- 对于变长的包, 可以在包头的位置, 约定一个包总长度的字段, 从而就知道了包的结束位置;
- 对于变长的包, 还可以在包和包之间使用明确的分隔符(应用层协议, 是程序猿自己来定的, 只要保证分隔符不和正文冲突即可);
思考: 对于UDP协议来说, 是否也存在 "粘包问题" 呢?
- 对于UDP, 如果还没有上层交付数据, UDP的报文长度仍然在. 同时, UDP是一个一个把数据交付给应用层. 就有很明确的数据边界.
- 站在应用层的站在应用层的角度, 使用UDP的时候, 要么收到完整的UDP报文, 要么不收. 不会出现"半个"的情况.
6. TCP异常
进程终止:
当我们使用客户端时,如果突然崩溃了,此时建立好的连接怎么办
进程终止会释放文件描述符,相当于调用了close函数,操作系统在底层自动完成四次挥手的动作,也就是说,仍然可以发送FIN. 和正常关闭没有什么区别。
机器重启:
我们在使用客户端的时候,如果机器重启,建立好的连接怎么办?
当重启主机时,操作系统会先杀掉所有进程然后再进行关机重启,因此机器重启和进程终止的情况是一样的,此时双方操作系统也会正常完成四次挥手,然后释放对应的连接资源。
机器掉电/网线断开
当正在使用客户端的时候,如果客户端的网线突然断了,建立好的连接怎么办?
服务器端在短时间内无法知道客户端掉线了,因此在服务器端会维持与客户端建立的连接,但这个连接也不会一直维持,因为TCP是有保活策略的。
- 服务器会定期客户端客户端的存在状况,检查对方是否在线,如果连续多次都没有收到ACK应答,此时服务器就会关闭这条连接。
- 此外,客户端也可能会定期向服务器“报平安”,如果服务器长时间没有收到客户端的消息,此时服务器也会将对应的连接关闭。
其中服务器定期询问客户端的存在状态的做法,叫做基于保活定时器的一种心跳机制,是由TCP实现的。此外,应用层的某些协议,也有一些类似的检测机制,例如基于长连接的HTTP,也会定期检测对方的存在状态。
7. TCP小结
为什么TCP这么复杂? 因为要保证可靠性, 同时又尽可能的提高性能.
可靠性:
- 校验和
- 序列号(按序到达)
- 确认应答
- 超时重传
- 连接管理
- 流量控制
- 拥塞控制
提高性能:
- 滑动窗口
- 快速重传
- 延迟应答
- 捎带应答
其他:
定时器(超时重传定时器, 保活定时器, TIME_WAIT定时器等)