目录
TCP特点概要
TCP协议段格式
TCP原理
确认应答
超时重传
连接管理(三次握手,四次挥手)
三次握手
四次挥手
流水线传输
滑动窗口
滑动窗口ACK丢失
滑动窗口数据报丢失
流量控制
拥塞控制
延迟应答
停止等待协议
回退N帧协议
面向字节流
缓冲区
粘包问题
TCP异常
😶🌫️创作不易多多支持.
TCP特点概要
- TCP是面向连接的运输层协议
- 每一条TCP连接只能连接两个端点, 每一条TCP连接只能是点对点的(一对一)
- TCP提供可靠交付的服务
- TCP提供全双工通信
- 面向字节流: TCP中的流(Stream)指的是流入或者流出进程的字节序列, 虽然应用程序和TCP的交互是一次一个数据块, 但TCP把应用程序交下来的数据看成仅仅是一连串无结构的字节流
什么是TCP面向流?
TCP不保证接收方应用程序所收到的数据块和发送方应用程序锁发送出的数据块具有对应的大小关系, 但接收方应用程序收到的字节流必须和发送方应用程序发出的字节流完全一样
TCP协议,即Transmission Control Protocol, 传输控制协议.
注意: 这里的字节流数据并不是应用程序直接发送和接收的, 而是要经过一个缓冲区, 发送方首先将字节流数据写入发送缓存, 然后再由发送缓存送入网络进行发送, 接收方将接收到的字节流数据首先存入缓冲区, 然后再由应用程序从缓冲区里面逐个读取.
TCP协议段格式
注解(数字单位: 位):
- 源/目的端口号: 表示数据来自哪个进程, 到哪个进程去
- 32位序号/ 确认号:
- 4位TCP报头长度: 表示TCP头部有多少个位bit, TCP头部最大的长度是60
- 6位标志位:
- URG: 紧急指针是否有效
- ACK: 确认号是否有效
- PSH: 提示接收应用程序立即从TCP缓冲区把数据读走
- PST: 对方要求重新建立连接(把懈怠PST表示的称为复位报文段)
- SYN:请求建立连接(懈怠SYN标识的称为同步报文段)
- FIN:通知对方,本端口要关闭了(懈怠FIN标识的为结束报文段)
- 16位窗口:
- 16位校验和: 检验数据是否存在误码
- 16位紧急指针: 标识哪部分数据是紧急数据
TCP原理
确认应答
TCP协议将每一个字节的数据都进行了编号, 即为序列号
每一个ACK都带有对应的确认序号,意思是告诉发送者, 我们已经收到了哪些数据, 下一次你从哪里开始发, 我们将上面确认应答的图进行修改:
超时重传
存在如下情况,: 主机A发送数据给B后, 可能因为网络阻塞等原因, 发送的数据无法到达主机B
400
主机A发送数据给主机B之后, 如果主机B没有收到来自A的数据,那么就不会给主机A发送应答报文, 主机A在超过一段时间没有接收到应答报文之后,就会进行重发, 但是主机A没有收到B的应答报文,也有可能是主机B发送给主机A的应答报文丢失了.
这样就出现了另外一个问题, 那就是重复接收到了数据1-1000. 这个时候TCP就能自己意识到哪些包是重复的, 并且把重复的包丢弃(利用前面的给字节定序号, 就很容易实现去重)
但是该如何确认超时的时间?
- 最理想的情况下, 需要找到一个最小时间, 来保证确认应答一定能在这和时间内返回
- 随着网络情况的不同, 这个时间的长短是有差异的
- 如果这个时间设置的太长, 就会影响整体的效率
- 如果这个时间太短, 就有可能会频繁发送重复的数据包
- 但是TCP为了保证无论在任何环境下都能有比较高的性能通信
当然确认应答也有可能在其中 延时到达:
主机A发送数据M1, 但是B发送的M1的应答报文因为网络的情况导致延到达, 在这个M1的确认报文到达之前, A认为M1的应答报文超时了, 会认为M1 的数据没有到达B(M1发送后, 在Tout时间段内没有收到ACK应答报文, 认为超时), 于是就重新传送M1数据, 但是M1已经被B接收了, 这个时候B就会丢弃重复的M1, 并向A发送M1对应的应答报文.
连接管理(三次握手,四次挥手)
TCP是一种非可靠的传输协议, 它使用三次握手建立连接, 以及四次挥手释放连接
三次握手
(SYN称为同步报文, 意思就是向另外一放申请连接, ACK为应答报文, 意思为同意建立连接)
三次握手本质是就是检测客户端和服务器各自的发送能力和接收能力是否正常.
TCP在建立连接的时候, 首先客户端会向服务器发送一个SYN同步报文, 表达自己想要和服务器建立连接, 服务器收到SYN报文之后, 回复客户端一个SYN/ACK(同步/确认报文), 表示自己同意客户端的请求, 然后发出与客户端进行连接的请求, 客户端收到SYN/ACK报文之后, 再向服务器发送ACK确认报文, 表示客户端也同样同意建立连接, 这样三次握手就结束了.
其中第二次握手的时候, 为什么非要将SYN和ACK报文一起发送? 理论上是可以将第二次握手拆成两步的, 分别发送ACK和SYN报文, 但是没有必要, 因为这里两个报文都是要发给客户端的, 但是TCP包本身就可以包含两个消息, 多握一次手就意味着更多的开销, 所以说TCP三次握手就可以了, 没必要四次.
下面观察TCP报头结构, 来详细观察SYN和ACK报文
四次挥手
为什么要挥手? 由于TCP半关闭的特性, TCP提供了连接的一段在结束它的发送后还能接受来自另一端的数据的能力, 任何一放都可以在数据传送结束后发出释放通知, 待对方确认后进入半关闭状态, 当另外一方也没有数据再传送的时候, 则发出释放通知, 对方确认之后就完全关闭了TCP连接
TCP连接释放的时候, 任意一方可以发送一个FIN结束报文, 表示自己想要关闭连接, 另一方接收到FIN报文之后, 也会回复一个ACK报文, 表示自己已经受到了对方的请求. 但是这个时候连接并没有立即关闭, 因为另外一方可能还有数据要进行传输, 等待数据传送完毕之后, 对方发送FIN结束报文请求结束, 这个时候就会回复一个ACK报文来确认已经收到了对方的请求.这样四次挥手就成功了.
下面是这个过程的简便理解:
流水线传输
我们前面所提到的这种简单的应答式的分组发送方式就是: 发送方每次就只发送一个分组,在收到确认之后才发送第二个分组, 这就是停止等待协议, 这种停止等待具有如下几个特点:
- 缓存: 发送完一个分组之后,, 发送方必须暂存已经发送的分组的数据或者副本, 以便于再没有收到确认应答的时候进行重发
- 编号: 对每一个分组和分组对应的确认都要进行编号
- 超时重传: 设定一个超时计时器, 如果超时计时器超时位收到确认, 发送方就会自动超时重传分组, 超时计时器的时间设置这个后面再讨论, 但是一般情况下, 重传时间应当比数据在分组的传输的平均往返世界更长一下, 放置不必要的重传
- 这种通信方式很简单, 但是信道的利用率非常低:
其中有一大半的时间RTT, 也就是往返时间, 都花在了数据的往返时间上, 当往返时间RTT远大于发送时间TD的时候, 信道的利用率就会非常的低, 例如我们的卫星通信, 这个RTT就非常大>
信道的利用率 =
为了提高信道的利用率, 可以采用流水线传输的形式:
由于信道上一直有数据不断的进行传输, 因此流水线传输可以获得非常高的信道利用率
滑动窗口
滑动窗口的数据传输就是基于上面的流水线形式. 首先我们先需要知道什么是窗口:
如图所示, 这个发送窗的大小就是5, 在从1开始发送到第五个字节的时候, 窗口保持不变,当接收方接收到数据,并返回这个最左端的数据对应的应答报文之后, 发接收方的窗口就往前滑动一个字节.
窗口大小是指的无需确认应答就可以继续发送的数据的最大值, 上图的窗口大小就是5(字节), 发送前四个字节的时候(前几个字节需要根据窗口大小而定), 不需要等待任何ACK就可以直接发送, 在收到第一个ACK之后, 发送窗口就会往后滑动, 继续发送后面的没有发送的并且在窗口里面的数据段, 操作系统为了维护这个滑动窗口, 需要开辟缓冲区,来逐个记录还有那些记录没有应答, 只有应答过了的数据才能从缓冲区中删除, 窗口越大, 网络的吞吐量就越大.
例如图中的白色区域就是窗口,被分为四个字节的数据, 就相当于批量发送这四个字节的数据, 发送窗口的数据之后, 就等待数据的应答,例如收到了来自1001~2001字节部分的应答, 窗口就会往后滑动到6001的位置.
此时的效果好像就是窗口就这么大,但是窗口往后挪动了一个格子, 如果这个时候接收到ACK的速度非常快,那么这个窗口滑动的速度就会非常快, 数据的吞吐量就会非常大.
既然是网络传输就肯定有数据丢失的情况, 接下来讨论两种情况
滑动窗口ACK丢失
这种情况, ACK应答,表示该序号之前的数据都接收到了, 例如在发送应答报文<下一个是5001>的时候, 就表示前面的1~500都接收到了, 对我们的可靠性没有任何影响. 这里如果超时没有收到ACK应答,照常拥有超时重传的机制, 假如是应答报文<下一个是6001>缺失了, 那么发送端在一定时间内没有收到应答报文就会重新传输5001~6000字节的数据.
这就是累积确认
如上图ACK5为累积确认, 确认了M1~M5的数据部分都正确接收.
优缺点:
- 很容易实现, 即使确认ACK报文丢失, 发送方也不需要重传
- 但是不能向发送方反映出接收方已经正确收到的所有分组的信息
滑动窗口数据报丢失
例如在发送数据1001~2000这个数据包的时候, 丢失了,但是主机B发送的应答报文是所要1001后面的内容, 这个时候由于滑动机制, 1001~2000这个部分的数据不需要应答就可以继续发送下面的数据2001~3000, 但是收到的还是1001这个序列号,直到主机A发现了1001~2000这个数据报超时了没有收到应答报文, 于是就超时重发, 这个时候主机B就会返回一个7001的序号的应答, 而不是1001, 因为1001这个序号的数据传给B后, 那么对于A,1~7000的数据就都传送完毕了
流量控制
通过滑动窗口我们知道, 这个窗口越大, 信道的利用率就越大, 但是窗口越大,一定就越好吗? 当然是否定的, 我们前面提到过, 不管是发送数据还是接收数据, 都要先放入缓冲区, 对于接收数据, 把接收的数据先放入缓冲区, 然后由进程应用一个一个从这个缓冲区拿取数据, 这就是一个典型的消费者生产者模型.
但是接收方处理数据的速度是有限的, 不可能说一下能把缓冲区的数据全部处理完, 所以当发送方发来的速度过快的话, 就会提前把接收缓冲区冲满, 这个时候再往里面发送数据, 就会造成丢包, 丢包就会引起丢包重传的一系列不利于效率提高的连锁反应.
因此TCP支持根据接收端处理能力来决定发送端的发送速度的机制, 这种机制就称为流量控制.
拥塞控制
TCP拥塞控制是一种网络流量控制机制,用于防止网络拥塞并确保网络的稳定性和可靠性。在TCP连接中,源主机发送的数据包数量和速率受到拥塞窗口大小的限制,该窗口大小是根据网络拥塞程度动态调整的。当网络出现拥塞时,TCP拥塞控制机制会减少发送速率,以避免网络拥塞的进一步恶化,同时也会及时恢复发送速率,以维持网络的正常传输。
有了滑动窗口, 就可以高效的传输数据, 但是如果窗口太大了, 接收端的处理能力不足, 不能及时清空缓冲区, 那么就会造成丢包, 如果一开始的窗口的大小不合适的话, 就会在开始的时候就造成丢包, 特别是在不知道当前网络状态的情况下贸然发送大量数据, 只会是雪上加霜, 因此我们需要从小的窗口开始慢慢试探
由此TCP引入慢启动机制, 先发送少量的数据, 试探当前网络的拥堵情况, 再来决定按什么速度传送数据.
发送端在前期试探的时候, 试探的窗口是指数增长的, 所谓慢启动只是指的开始的时候非常慢, 但是增长的特别快, 开始的时候拥塞窗口的大小为1, 没次收到一个应答,窗口的大小就随着函数来增长, 但是为了不增长的那么快,导致一下次突破了临界值, 当拥塞窗口超过一定的阈值之后, 就按照线性增长, 当网络拥塞严重的时候, 就使用乘法来减小拥塞窗口的数量.
所以当TCP开始通信的时候, 网络的吞吐量会逐渐提升, 在网络拥塞的情况下, 吞吐量会迅速减少.
TCP对滑动窗口的控制就相当于 > 流量控制 + 拥塞控制, 对于流量控制, 就是根据接收方的处理能力来决定. 拥塞控制就是根据网络的拥堵情况.
这种机制,TCP协议就是尽可能快的传输数据, 但是又要避免给网络或者对方带来太大的压力的折中方案.
延迟应答
延迟应答分为:
- 时间限制: 超过最大延迟时间就应答一次
- 数量限制: 每隔N个包就应答一次
如果接收数据的主机立即返回ACK应答, 这个时候返回的窗口可能比较小, 例如:
- 假设接收缓冲区为1M, 一次收到了500k的数据, 如果立刻应答, 返回窗口就是500k,
- 但是实际上处理的速度很快, 10ms内就把500k的数据从缓冲区消费掉了
- 在这种情况下, 接收端处理还远没有达到自己的极限, 即使窗口再放大一点, 也能处理过来
- 如果接收端稍微地延迟一会再应答, 比如等待200ms再应答, 那么这个窗口的大小就是1M
一窗口越大, 网络的吞吐量就越大, 传输效率就越高, 我们的传输数据的目标是在保证网络不拥塞的情况下尽量提高传送的效率
停止等待协议
停止等待协议采用一问一答的形式,
实现原理:
- 使用超时重传机制, 但是不可以使用否认机制, 但是如果点对点链路的误码率较高, 使用否认机制可以使发送方在超时计时器超时前就尽快重传
- 为了让接收方能够判断收到的数据分组是否重复, 需要给数据分组编号, 由于腾志等待协议的特性, 只需要一个比特编序就可以, 即0 或者1
- 为了让发送方可以判断收到的确认分组是否重复, 需要给确认分组编号, 所用比特的数量与数据分组所用的比特数量一样
- 超时计时器的超时重传时间应当自习选择, 一般将RTO设置为略大于收发双方的平均往返时间
- 重传的请求是发送方自动进行的, 而不是接收方请求发送方重传
回退N帧协议
回退N帧是滑动窗口的一种, 其中, 发送方有多个发送窗口,采用n个比特给分组编序号,序号范围是 0 ~ (2^n−1)。本例假设采用3个比特给分组编序号,则序号范围是0~7,采用n个比特给分组编序号,则W_T的取值范围是1<W_T≤(2^n−1)。本例假设采用3个比特给分组编序号,则W_T的取值范围是2~7,本例取W_T=5.
同时接收方也需要一个窗口Wr, 只有正确到达接收方(无误码), 且序号落入Wr内的数据分组才被接收方接收, Wr的取值只能是1, 这一点与停止等待协议相同.
回退N帧协议采用累积确认的方式:
- 接收方不必对每一个收到的数据分组都发送一个确认分组, 而是可以在连续接收到几个序号的数据分组之后, 对最后一个到达的数据分组进行发送确认分组即可
- 接收方何时发送累积确认分组有具体实现决定
- 确认分组ACKn表明需要为n及之前的所有数据分组已经被正确接收
存在的问题:
超时重传, 如果发送的数据分组里面, 例如发送五个数据分组(0,1,2,3,4)到接收端,但是接收端收到的分组里面出现了误码, 例如数据分组里面的2变成了6,也就是(0.1.6.3.4), 这个时候, 接收端根据检测码检测除了这个数据存在误码, 于是将其丢弃, 然后向发送端请求这个丢失的据, 随后触发超时重传,因为2超时, 导致之前已经正确传送的分组仍然需要重传, 这就是回退N帧, 在信道质量较差(容易出现误码)的情况下,回退N帧协议的信道利用率并不比停止-等待协议的信道利用率高.
面向字节流
TCP是面向字节流得了,虽然应用程序和TCP的交互是一次一个数据块,但是TCP把应用程序教下来的数据仅视为一连串的无结构的字节流
缓冲区
创建一个TCP的Socket, 同时会在内核中创建一个内存缓冲区,和一个接收缓冲区
- 调用write时,数据会写入发送缓冲区
- 如果发送的字节数太长,就会被拆分成多个TCP数据报发送
- 如果发送的字节数太短了,就会现在缓冲区里面等待,等到缓冲区长度够了,或者达到了其他的规定他条件之后进行一个发送
- 接收数据的时候,数据也是从网卡驱动程序到达内核的接收缓冲区
- 然后应用程序可以调用read从接收缓冲区域获取数据
- 另外一方面,TCP一个连接,既有发送缓冲区也有接收缓冲区,对于一个TCP连接,即可以读数据,也可以写数据,这叫做全双工
粘包问题
- 首先需要明确的是,粘包问题中的包是指的应用层的数据报
- 在TCP协议头中,没有如同UDP一样的报文长度的字段,但是有一个序号的字段
- 站在传输层的角度来看,TCP是一个一个报文传过来的,按照序号排好放在缓冲区里面
- 站在应用层的角度来看,看到的知识一串连续的字节流
- 那么应用层看到的是一连串的字节数据,就不知道从哪个部分开始到哪个部分结束是一个完整的应用程序数据包
例如上面的例子,我们发送几个简单的问题给接收端处理,接收端接收到问题处理之后(假设可能存在错误的处理结果),然后要求返回错误的结果让接收端重新计算, 但是送回数据的时候,发送严重的粘包
如图所示,系统无法分辨那个是1+1对应的计算结果,以此类推
解决办法,归根结底就是得明确两个包之间的界限:
- 对于定长的包,保证每次都按固定大小读取
- 对于变长的包,可以在包开头的位置,约定一个包总长度的字段,从而就知道了包结束的位置
- 对于变长的包, 还可以使包与包之间使用明确的分割符号(应用层协议, 是程序员自己来决定的,只要保证分隔符和正文之间不产生重推即可)
TCP异常
1.进程关闭 / 进程崩溃
进程异常终止了,或者说是进程关闭了, 但是socket是文件,随之被关闭,虽然进程没有了,但是连接还在, 依然可以继续四次挥手
2.主机关机(正常流程关机)
先关闭所有的用户进程, 也会触发四次挥手, 如果没有挥手完毕, 例如对方发送fin来请求结束,但是接收fin后还没来的及发送ack给对方就关机了, 这个时候对方在一定时间内没有手到ack就会进行重传fin,重传很多次fin后,发现还是没有ack,于是就重置连接,如果实在不行就直接释放连接.
3.主机断电
主机异常断电,来不及进行任何挥手操作
(1)对方是发送端, 对端就会接收不到ack,就会超时重传,重传很多次之后还是没有ack于是就重置连接或者是释放连接
(2)对方是接收端, 对端就会收不到你的任何信息,也不会知道,到底是你这边没有来得及发送新的数据,还是直接挂机了. 但是TCP内置了 心跳包 这个机制, 也就是周期性的传输信号,例如接收端给发送端一个信号a, 发送端必须给接收端发送一个b, 这个才算一个周期(一次心跳), 如果心跳多次没了,就代表连接出了问题
TCP小结
为什么TCP这么复杂?因为要保证可靠性,同时又尽可能的提高性能。
可靠性:
校验和
序列号(按序到达)
确认应答
超时重发
连接管理
流量控制
拥塞控制
提高性能:
滑动窗口
快速重传
延迟应答
捎带应答
其他:
定时器(超时重传定时器,保活定时器,TIME_WAIT定时器等)
基于TCP应用层协议
HTTP
HTTPS
SSH
Telnet
FTP
SMTP
当然,也包括你自己写TCP程序时自定义的应用层协议;