TCP协议
TCP(Transmission Control Protocol,传输控制协议)是一种面向连接的、可靠的、基于字节流的传输层通信协议,由IETF的RFC 793定义。TCP在IP(Internet Protocol,互联网协议)网络层上提供可靠的数据传输服务。它是互联网上广泛使用的网络协议之一,与UDP(User Datagram Protocol,用户数据报协议)一同构成了互联网传输层的基石。
一、tcp协议格式
解释:
- 源/目的端口号:表示数据是从哪个进程来,到哪个进程去;
- 32位序号/32位确认号:保证可靠性的重要手段,下面讲
- 4位TCP报头长度:表示该TCP头部有多少个32位bit(有多少个4字节);所以TCP头部最大长度是15 * 4 = 60
- 16位窗口大小: 与滑动窗口有关下面详细讲,这个也是保证可靠性和效率的重要手段
- 6位校验和: 发送端填充, CRC校验.接收端校验不通过,则认为数据有问题.此处的检验和不光包含TCP首部,也包含TCP数据部分.
- 16位紧急指针: 标识紧急数据在缓冲区的偏移量,可以通过URG标志使用;
- 40字节头部选项: 暂时忽略;
六个标志位:
- URG: 紧急指针是否有效
- ACK: 确认号是否有效
- PSH: 提示接收端应用程序立刻从TCP缓冲区把数据读走
- RST: 对方要求重新建立连接;我们把携带RST标识的称为复位报文段
- SYN: 请求建立连接;我们把携带SYN标识的称为同步报文段
- FIN: 通知对方,本端要关闭了,我们称携带FIN标识的为结束报文段
如何解包?
当收到一个tcp报文时,直接解析他的前20个字节,从中提取中4位tcp报头长度,要注意这里的大小是带单位的(4字节),4位可以表示的长度为0000~1111(0-15),即报头最多有15*4=60个字节,tcp报头基本的大小为20个字节,所以选项的大小最大为40个字节。
有了报头大小以后减去20个字节,如果为0说明该报文不带选项,如果大于0那多出来字节就是选项的信息,报文大小减去报头的大小剩下的就是有效载荷。
二、确认应答(ACK)机制
tcp协议是如何保证可靠性的呢?确认应答机制就是一个重要的手段。
在我们日常生活中的通信,例如我们在微信上给别人发送一个信息,我们怎样才能确保对方收到了我们的信息呢?只要对方收到信息后给我们应答一下,我们就可以确保它收到了我们的信息。
网络中的确认应答机制也是如此,主机A向主机B发送信息,只要该主机A收到了主机B的应答,就可以确保信息被主机B接收到了,但是这里还存在一个问题,主机B想知道自己的应答是否被主机A收到了,那就需要主机A在应答回来,这样就产生了先有鸡还是先有蛋的问题。所以总有一条最新的消息无法被可靠的传输。故我们认为只要收到了应答对方一定收到了消息,没有收到应答,数据就是丢失了(尽管可能是对方的应答丢失了),再采取其他策略重发数据,这样就保证了数据的可靠性
2.1 理解序号与确认序号
2.1.1序号
我们知道tcp套接字内部维护两个缓冲区,一个主机向另一个主机发送数据的本质是将自己发送缓冲区的数据发送它的接收缓冲区,而缓冲区的本质我们可以把他看做一个数组,而数组本身就会自带一个下标。TCP将每个字节的数据都进行了编号.即为序列号,我们可以把它理解为数组下标号,由于TCP是面向字节流的,在通信时避免不了一条信息被拆分为多个报文发送,由于报文在发送途中选择的路径可能不同,导致奥文不一定是按序到达,而接收方可以根据序号来重新排列乱序到达的数据包,并检测数据包的丢失。
2.1.2 确认序号
确认序号是接受方返回给发送方的,用于告诉发送者,我已经收到了哪些数据;下一次你从哪里开始发.
比如发送端发送了一条确任号为1000的报文,当接收端接收到报文以后,通常会将报头中的确认区号设置为1001(确认号+1),用于告诉发送方1001之前的数据我收到了,下次发送数据从1001开始发。
通信双方共同遵守确认应答机制,就可以保证两个朝向上的通信可靠性
问题:为什么要设计序号和确认序号,只要序号不行吗?
在我们看来上述的确认应答貌似只需要序号就可以完成,接受方在返回应答时,将序号看做确认序号,但TCP协议没有这样做,因为在网络通信时其实是很复杂的,尤其是通信距离很远时,报文要经过许多路由等设备,如果接收方在收到数据时正好也想给发送方发送一些数据,为了提高数据的发送效率,就会采用捎带应答的方式,将应答和数据一起发送给发送方,这就需要两个序号了,一个序号用于标识数据,一个确认序号标识收到的数据
三、超时重传机制
在双方进行通信时,如果发送方发送完数据后,长时间没有接受到应答,此时TCP协议就认为数据丢失了,就会重新发送数据,这就是超时重传机制。
丢失数据分为两种情况:
- 发送方发送的数据包丢失,接收方压根就没收到数据
- 接收方收到了数据,但是应答丢了
由于发送方发送完数据并没有收到任何回应,它并不能分别出来到底是哪一种情况,所以发送方只能重新发送数据,但是问题来了,如果是ACK应答丢失的这种情况,接收方那里已经保存了一份数据,发送方再次发送数据就会导致接收方收到重复的数据,这时候序号就体现出作用来了,TCP协议就可以根据序号来去重,所以序号有两个作用:1. 使数据按序到达 2. 去重
那超时的时间怎么设定呢?
最理想的情况下,找到一个最小的时间,保证确认应答一定能在这个时间内返回,但是这个时间的长短,随着网络环境的不同,是有差异的.
- 如果超时时间设的太长,会影响整体的重传效率;
- 如果超时时间设的太短,有可能会频繁发送重复的包;
TCP为了保证无论在任何环境下都能比较高性能的通信,因此会动态计算这个最大超时时间.
- Linux中(BSD Unix和Windows也是如此),超时以500ms为一个单位进行控制,每次判定超时重发的超时时间都是500ms的整数倍.
- 如果重发一次之后,仍然得不到应答,等待2*500ms后再进行重传.
- 如果仍然得不到应答,等待4*500ms进行重传.依次类推,以指数形式递增.
- 累计到一定的重传次数, TCP认为网络或者对端主机出现异常,强制关闭连接.
四、连接管理机制
在正常情况下, TCP要经过三次握手建立连接,四次挥手断开连接
4.1 三次握手(建立连接)
三次握手是TCP建立连接的过程,通过这个过程,通信双方能够确认彼此的接收和发送能力,以及初始序列号,从而建立可靠的连接,具体步骤如下:
(1)客户端将向服务端的报头中的SYN标志位置为1,表示要向服务端建立连接
(2)服务端收到报头后,以捎带应答的方式向客户端发送SYN+ACK应答,表示收到了客户端的连接请求并向客户端也申请建立连接
(3)客户端收到SYN-ACK报文段后,向服务器发送一个ACK报文段,其中包含了确认号,表明它已经收到了服务器的SYN-ACK报文段。至此,三次握手完成,TCP连接成功建立。
4.2 三次握手后双方连接一定建立好了吗?
三次握手后并不能一定保证双方的连接建立好了。因为客户端的最后一次ACK应答并不能保证被服务端接收到了,客户端将ACK应答发送出去后,客户端这边是认为连接是建立好了,但是由于服务端并没有收到应答,所以此时服务端并不认为双方的连接建立好了,当客户端向服务端发送信息时,服务端就会明白,一定是客户端认为双方的连接建立好了,此时服务端会给客户端发送一个报头,其中的RST(reset)标志位被置为了1,告诉客户端三次握手的过程出现了异常,请求重新进行三次握手。
4.3 为什么一定是三次握手?
侧面理由:
如果是一次握手或者两次握手的话,如果客户端不断地向服务端发送连接请求而不对连接处理(SYN洪水),服务器可能就会存在大量重复的连接,这些连接一定是要被维护起来的,成本太大了。如果采用三次握手的话,双方构建连接的前提是客户端已经建立好了连接,所以客户端也需要将建立的连接维护起来,这样相比于一次握手和两次握手的漏洞更加小
正面理由:
理由一:
三次握手对于通信双方来说,都存在一次双向的数据通信,这样可以验证双方的全双工(验证网络的联通性)。对于客户端来说,他收到服务器的ACK应答说明自己发送的SYN请求被顺利收到了,可以验证自己的发送和接收能力没问题,对于服务起来说,他收到了客户端的SYN请求,说明自己的接收能力没问题,并且客户端收到自己的SYN请求后发回来的ACK应答也可以说明自己的发送能力没有问题。
理由二:
建立双方的通信意愿,双方建立的连接必须是自愿的,双方可以根据对方的SYN请求和ACK应答来确保双方的通信意愿。其实三次握手可以是四次握手,因为是服务器发送的ACK应答和SYN请求被合并起来通过捎带应答的方式发送了。
4.4 三次握手双方状态变化
服务端:
- [CLOSED -> LISTEN]服务器端调用listen后进入LISTEN状态,等待客户端连
- [LISTEN -> SYN_RCVD]一旦监听到连接请求(同步报文段),就将该连接放入内核等待队列中,并向客户端发送SYN确认报文.
- [SYN_RCVD -> ESTABLISHED]服务端一旦收到客户端的确认报文,就进入ESTABLISHED状态,可以进行读写数据了.
客户端:
- [CLOSED -> SYN_SENT]客户端调用connect,发送同步报文段
- [SYN_SENT -> ESTABLISHED] connect调用成功,则进入ESTABLISHED状态,开始读写数据;
4.5 四次挥手(断开连接)
当双方通信完成想要断开连接时就需要进行四次挥手,过程如下:
(1)当客户端的数据发送完成了,将向服务器发送的报头中的FIN标志位置为1,表示要与服务器断开连接
(2)服务器收到客户端的FIN请求后,向客户端发送ACK应答
(3)当服务器也没有数据需要发送时,也会向客户端发送FIN请求申请断开连接
(4)客户端收到FIN请求后,向服务器发送ACK应答,至此四次挥手完成,双方彻底断开连接
4.6 为什么是四次挥手?
为什么是四次挥手,不能向三次握手一样采用捎带应答,称为三次挥手吗?
四次挥手主要是因为TCP是全双工通信协议,即数据可以在两个方向上同时传输。在断开连接时,需要确保两个方向上的数据传输都已经被正确处理和确认,以避免数据丢失或连接状态不一致的问题。当客户端数据发送完成申请断开连接时,服务器会无条件同意,但是不能采用捎带应答的方式向客户端发送FIN请求,因为此时服务端可能还有数据要发送给客户端,只有当服务器的数据全部发送完成时才会向客户端发送FIN请求断开连接。
4.7 四次挥手双方状态变化
服务端:
- [ESTABLISHED -> CLOSE_WAIT]当客户端主动关闭连接(调用close),服务器会收到结束报文段,服务器返回确认报文段并进入CLOSE_WAIT;
- [CLOSE_WAIT -> LAST_ACK]进入CLOSE_WAIT后说明服务器准备关闭连接(需要处理完之前的数据);当服务器真正调用close关闭连接时,会向客户端发送FIN,此时服务器进入LAST_ACK状态,等待最后一个ACK到来(这个ACK是客户端确认收到了FIN)
- [LAST_ACK -> CLOSED]服务器收到了对FIN的ACK,彻底关闭连接.
客户端:
- [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状态.
4.8 CLOSE_WAIT和TIME_WAIT
1. CLOSE_WAIT
当客户端发送完数据向服务器发送FIN请求时,此时服务器可能还存在数据需要发送,他就会变为CLOSE_WAIT的状态,想要验证也很简单,我们不让服务端close(fd)即可。由于只有四次挥手完成时建立连接的资源才会释放,如果在写程序时我们忘记调用close关闭文件描述符,此时双方通信完成后就会卡在第三挥手使资源不能正常释放,导致服务器资源越来越少。所以当系统出现大量的CLOSE_WAIT时我们就要检查一下是否是程序没有close文件描述符
2. TIME_WAIT
当服务器也向客户端发送FIN请求后,虽然双方都互相发送了FIN,但是主动断开连接的一方(客户端)并不会直接变成CLOSE状态,而是出于TIME_WAIT等待一段时间,这个时间一般为2*MSL,这样做有两个目的,第一个是因为客户端最后一次ACK应答可能会丢失,在等待的这个时间内,客户端可以重新收到服务器超时重发的FIN请求,再次构建ACK应答完成四次挥手,可以保证最后一次ACK应答被接受的概率。第二个是在网络中可能积压了许多因为超时还未发送给对方的数据,等待一段时间可以保证对方在接收到以后将数据抛弃,防止用户再次以同样端口号构建连接后,导致在网络中的数据巧合般的发送给对方,使网络历史数据对新的通信构成影响
ps:上述例子讲的是客户端为主动断开方,在实际中双方都有权利先断开连接
4.9 如何解决TIME_WAIT引起的bind失败
我们知道只有四次挥手完成后连接才算彻底断开,由于TIME_WAIT在等待时,连接并没有断开完成,所以此时的端口号依旧被占用着,所以当我们断开连接后再次以相同的端口号构建连接时可能就会出现bind失败的情况,可以通过setsockopt函数解决
void BuildListenSocket(uint16_t port)
{CreateSocket();ReuseAddr();CreateBind(port);CreateListen();
}void ReuseAddr()
{int reuse = 1; // 启用套接字重用setsockopt(_sockfd, SOL_SOCKET, SO_REUSEADDR, &reuse, sizeof(reuse));
}
五、流量控制
接收端处理数据的速度是有限的. 如果发送端发的太快, 导致接收端的缓冲区被打满, 这 个时候如果发送端继续发送, 就会造成丢包, 继而引起丢包重传等等一系列连锁反应. 因此 TCP 支持根据接收端的处理能力, 来决定发送端的发送速度. 这个机制就叫做流量控制(Flow Control);
那发送端怎么知道接收端的接受能力是多少呢?
再报头中存在一个16为的窗口大小,接接收端将自己可以接收的缓冲区剩余大小放入 TCP 首部中的 "窗口大小" 字段, 通过 ACK 应答通知发送端,接收端一旦发现自己的缓冲区快满了, 就会将窗口大小设置成一个更小的值通知给发送端,发送端接受到这个窗口之后, 就会减慢自己的发送速度,如果接收端缓冲区满了, 就会将窗口置为 0;,这时发送方不再发送数据, 但是需要定期发送一个窗口探测数据段, 使接收端把窗口大小告诉发送端.
16 位数字最大表示 65535字节,如果觉得不够大的话TCP 首部 40 字节选项中还包含了一个窗口扩大因子 M, 实际窗口大小是窗口字段的值左移 M 位;
六、滑动窗口
对每一个发送的数据段, 都要给一个 ACK 确认应答. 收到ACK后再发送下一个数据段. 这样做有一个比较大的缺点, 就是性能较差.,尤其是数据往返的时间较长的时候,所以其实可以一次发送多条数据, 只要最后收到每条数据的应答就可以,这样就可以大大的提高性能(其实是将多个段的等待时间重叠在一起了)
- 滑动窗口的本质其实是两个下标 win_start win_end,在发送缓冲区中标识了一段空间,他将缓冲区分为了三个部分,已发送已确认、暂时不需要应答可以直接发送、待发送。
- 滑动窗口的大小其实也是根据接收方的接受能力动态变化的,一般滑动窗口的大小是min(窗口大小,拥塞窗口)
滑动窗口的移动
我们假设窗口是固定的4000字节,当主机A收到了主机B的确认序号为2001的应答,说明主机A发送的1001~2000的数据已经被接收到了,此时这一部分数据变为了已接收已确认的信息,win_start=2001,win_end=win_start+min(窗口大小,拥塞窗口),这样滑动窗口就向右边移动了
在实际过程中,当接收方的的接受能力很强时,相对来说win_start向右移动的较慢,wiin_end向右移动的快,体现出来滑动窗口就会变大,而当接收方的的接受能力变小时,win_start向右移动的较快,wiin_end向右移动的慢,体现出来滑动窗口就会变小,当然滑动窗口的大小也可能为0,当接收方接受能力为0时,此时win_end不会向右移动,窗口内的数据不断被发送接受,win_start不断向右移动,当win_start==win_end时,滑动窗口就为0了
问题:
滑动窗口可以向左移动吗? -----不可以
如果滑动窗口的左边沿可以向左移动,那么可能会导致发送方重复发送已经确认的数据,这不仅浪费了网络资源,还可能引起接收方的混乱。
滑动窗口会越界吗? ----不会
在上述对于缓冲区的描述我们认为他是一个数组,滑动窗口不断向右移动就会移出数组外部,其实实际上是一个由 sk_buff 结构体组成的队列,通常称为发送缓冲队列,而该队列是一个双向链表,当待发送的数据部分发送完后,会回头继续覆盖之前的已接收已确认的部分,循环往复。
这也说明了一个问题,发送缓冲区已经被发送过的数据并不会被直接删除,因为只有收到ACK应答这些数据才会被认为接收成功,才会被滑动窗口移动覆盖,所以只要数据没有收到应答,该数据就不会丢失,如果数据在传输时,我们依旧可以对这部分数据进行超时重传,所以tcp的超时重传是由滑动窗口实现的。
如果滑动窗口发送的数据丢包了怎么办?
情况一: 数据包已经抵达, ACK 被丢了.
这种情况下, 部分 ACK 丢了并不要紧, 因为可以通过后续的 ACK 进行确认;
例如此时序号为1000 3000 4000的ACK应答丢了,只收到了2001和6001的确认序号,由于接收到了6001的确认序号,因为确认序号的规定是该序号之前的数据都被接收了,主机A就知道了序号为1~1000 2001~3000 3001~4000的数据其实已经被对方接收到了,只是部分ACK应答丢失了,就可以不做处理。如果是6001的确认序号丢失的话,那我们就看5001的序号是否收到,收到5001的话,我们对5001~6000的数据进行重传即可。
情况二: 数据包丢失
1. 最左侧数据包丢失:假设此时1~1000的数据包丢失了,所有的应答确认序号一定是1,当发送端收到三个以上的重复确认序号后,此时发送端就可以明确知道是1~1000的数据丢失了,就会重新发送这部分数据,这是tcp的快重传机制。当收到2001的确认序号后,滑动窗口就可以继续向右移动
2. 中间的数据包丢失:假设此时是1001~2000 2001~3000的数据包丢失了,此时所有的应答确认序号一定是1001,滑动窗口左侧就会移动到1001,此时又变成了最左侧数据丢失的情况,当重新补发1001~2000的数据后,收到的应答应该为2001,说明2001~3000的数据也丢了,就会再次补发这块数据
3. 最右侧的数据包丢失:假设5001~6000的数据包丢失了,同理,我们会收到5001的报文,说明5001之前的数据全部收到了,只有5001~6000的数据丢失了,利用快重传和超市重传再次补发就可以
快重传与超时重传
因为超市重传需要等待一段时间才会重传数据,所以快重传的效率一定是优于超时重传的,那为什么还需要超时重传呢?其实超时重传是做兜底的,因为快重传的前提是收到3个及以上的重复报文才会执行,所以快重传和超时重传共同保证了TCP的可靠性
七、拥塞控制
虽然 TCP 有了滑动窗口这个大杀器, 能够高效可靠的发送大量的数据. 但是如果在刚开 始阶段就发送大量的数据, 仍然可能引发问题. 因为网络上有很多的计算机, 可能当前的网络状态就已经比较拥堵. 在不清楚当前网络状态下贸然发送大量的数据, 是很有可能引起雪上加霜的. TCP 引入慢启动 机制, 先发少量的数据探探路, 摸清当前的网络拥堵状态, 再决定按照多大的速度传输数据;
此处引入一个概念称为拥塞窗口 ,发送开始的时候, 定义拥塞窗口大小为 1,每次收到一个 ACK 应答, 拥塞窗口逐步增加。每次发送数据包的时候, 将拥塞窗口和接收端主机反馈的窗口大小做比较, 取较小的值作为实际发送的窗口;
像上面这样的拥塞窗口增长速度, 是指数级别的. "慢启动" 只是指初使时慢, 但是增长速度非常快.
为了不增长的那么快, 因此不能使拥塞窗口单纯的加倍. 此处引入一个叫做慢启动的阈值,当拥塞窗口超过这个阈值的时候, 不再按照指数方式增长, 而是按照线性方式增长
当 TCP 开始启动的时候, 慢启动阈值等于窗口最大值; 在每次超时重发的时候, 慢启动阈值会变成原来的一半, 同时拥塞窗口置回 1; 少量的丢包, 我们仅仅是触发超时重传; 大量的丢包, 我们就认为网络拥塞; 当 TCP 通信开始后, 网络吞吐量会逐渐上升; 随着网络发生拥堵, 吞吐量会立刻下降; 拥塞控制, 归根结底是 TCP 协议想尽可能快的把数据传输给对方, 但是又要避免给网络造成太大压力的折中方案
八、延迟应答
如果接收数据的主机立刻返回 ACK 应答, 这时候返回的窗口可能比较小.
- 假设接收端缓冲区为 1M. 一次收到了 500K 的数据; 如果立刻应答, 返回的窗口 就是 500K;
- 但实际上可能处理端处理的速度很快, 10ms 之内就把 500K 数据从缓冲区消费掉了;
- 在这种情况下, 接收端处理还远没有达到自己的极限, 即使窗口再放大一些, 也能处理过来;
- 如果接收端稍微等一会再应答, 比如等待 200ms 再应答, 那么这个时候返回的窗口大小就是 1M;
一定要记得, 窗口越大, 网络吞吐量就越大, 传输效率就越高. 我们的目标是在保证网络不拥塞的情况下尽量提高传输效率; 那么所有的包都可以延迟应答么? 肯定也不是;
- 数量限制: 每隔 N 个包就应答一次;
- 时间限制: 超过最大延迟时间就应答一次;
九、面向字节流
创建一个 TCP 的 socket, 同时在内核中创建一个发送缓冲区 和一个接收缓冲区;
- 调用 write 时, 数据会先写入发送缓冲区中;
- 如果发送的字节数太长, 会被拆分成多个 TCP 的数据包发出;
- 如果发送的字节数太短, 就会先在缓冲区里等待, 等到缓冲区长度差不多了, 或者其他合适的时机发送出去;
- 接收数据的时候, 数据也是从网卡驱动程序到达内核的接收缓冲区;
- 然后应用程序可以调用 read 从接收缓冲区拿数据;
- 另一方面, TCP 的一个连接, 既有发送缓冲区, 也有接收缓冲区, 那么对于这一 个连接, 既可以读数据, 也可以写数据. 这个概念叫做全双工
由于缓冲区的存在, TCP 程序的读和写不需要一一匹配, 例如:
- 写 100 个字节数据时, 可以调用一次 write 写100个字节, 也可以调用100次 write, 每次写一个字节;
- 读 100 个字节数据时, 也完全不需要考虑写的时候是怎么写的, 既可以一次read100个字节, 也可以一次read一个字节, 重复 100 次;
十、粘包问题
- 首先要明确, 粘包问题中的 "包" , 是指的应用层的数据包.
- 在TCP的协议头中, 没有如同 UDP 一样的 "报文长度" 这样的字段, 但是有一个序号这样的字段.
- 站在传输层的角度, TCP 是一个一个报文过来的. 按照序号排好序放在缓冲区中.
- 站在应用层的角度, 看到的只是一串连续的字节数据.
- 那么应用程序看到了这么一连串的字节数据, 就不知道从哪个部分开始到哪个部分, 是一个完整的应用层数据包.
那么如何避免粘包问题呢? 归根结底就是一句话, 明确两个包之间的边界.
- 对于定长的包, 保证每次都按固定大小读取即可; 例如上面的 Request 结构, 是固定大小的, 那么就从缓冲区从头开始按 sizeof(Request)依次读取即可;
- 对于变长的包, 可以在包头的位置, 约定一个包总长度的字段, 从而就知道了包的结束位置;
- 对于变长的包, 还可以在包和包之间使用明确的分隔符(应用层协议, 是程序猿 自己来定的, 只要保证分隔符不和正文冲突即可);
对于 UDP 协议来说, 是否也存在 "粘包问题" 呢?
对于 UDP, 如果还没有上层交付数据, UDP 的报文长度仍然在. 同时, UDP 是一 个一个把数据交付给应用层. 就有很明确的数据边界,站在应用层的站在应用层的角度, 使用 UDP 的时候, 要么收到完整的 UDP 报 文, 要么不收. 不会出现"半个"的情况.
十一、TCP 异常情况
进程终止: 当客户端与服务器在通信时,客户端因为异常程序终止了,进程终止会释放文件描述符, 相当于我们调用close函数,此时双方的OS依旧可以正常完成四次挥手断开连接,仍然可以发送 FIN,和正常关闭没有什么区别.
机器重启: 和进程终止的情况相同.
机器掉电/网线断开: 接收端认为连接还在, 一旦接收端有写入操作, 接收端发现连接已经不在了, 就会进行 reset. 即使没有写入操作, TCP 自己也内置了一个保活定时器, 会定期询问对方是否还在. 如果对方不在, 也会把连接释放. 另外, 应用层的某些协议, 也有一些这样的检测机制. 例如 HTTP 长连接中, 也会定期检测对方的状态. 例如 QQ, 在QQ断线之后, 也会定期尝试重新连接.
十二、TCP和UDP对比
我们说了 TCP 是可靠连接, 那么是不是 TCP 一定就优于 UDP 呢? TCP 和 UDP 之间的 优点和缺点, 不能简单, 绝对的进行比较
- TCP 用于可靠传输的情况, 应用于文件传输, 重要状态更新等场景;
- UDP 用于对高速传输和实时性要求较高的通信领域, 例如, 早期的 QQ, 视频传 输等. 另外 UDP 可以用于广播;
归根结底, TCP和UDP都是程序员的工具,什么时机用,具体怎么用,还是要根据具体的需求场景去判定.