TCP的全称是Transmission Control Protocol,即传输控制协议,TCP工作在传输层上
其职责是:实现主机间进程到进程的通信,其次还需要保证可靠性(不是安全性,换言之不能保证安全性)
什么是可靠性(重点在前3条):
- TCP会尽自己所能,尽量将数据发送给对方;但并不能保证100%可以发送给对方
- TCP会在数据发送不到对方的情况下,会给应用层一个错误提示,告知用户发送失败
- TCP可以保证接收方(应用层)严格按照发送时的数据顺序接收
- TCP保证数据不会出现无意间的损坏(UDP 也做到这点)
- TCP尽可能地维护网络质量
我们通过时序图(时间从上往下流失)来大致描述一下:
发送方发送了数据,如果对方收到了确认,代表对方收到了数据,如果没有收到确认,可以合理推测,对方没有收到数据
如果发送方同时发送了很多数据,如何知道对方确认收到的是哪一份
对数据进行编号,同时确认也带上编号,这样对方收到数据的时候,就能知道收到的是哪一份数据
如果没有收到对方的确认
如果没有收到确认,就重新发送信息,一般超过一定时间没有收到确认才进行重发,因此也称为超时重发机制
数据编号和确认编号
发送的数据编号被称为序列号(Sequence Number - SN)
确认的数据编号被称为确认序列号(Ackonwledge Sequence Number - ASN)
编号的规则:每个字节都要占用一个号,发送时的起始编号称为初始序列号(Initial SN - ISN),ISN不一定为0,而是一个随机值,不过编号是连续的,因此可以记为0(相对值)
SN在发送TCP Segment 的 header中如何体现:TCP 发送/接收的完整数据,一般称为segment(段),TCP segment = header + payload
当一次性发送一些数据的时候,SN只需要填写本次发送的数据中的第一个字节的数据即可,因为编号是连续的,且segment会携带payload长度
TCP协议正是通过序列号保证了传输顺序
参考博客:https://blog.csdn.net/wyq_tc25/article/details/51504642
TCP由于要进行发送,也要进行确认,所以实际上TCP Segment有两种不同的角色:
- send segment
- acknowledge segment
TCP 设计的时候,一个segment可以身兼两种不同的角色
无论什么时候,一个segment都视为send segment的角色,而当某个标志位被置位时,segment具备了acknowledge segment的角色
在TCP Header 中,通过ACK的标志位处理ack角色,一个ack只占据一个bit位的数据,也就是说,ack的值为0或1,为1的时候就是被置位了,表示ASN字段有意义,如果为0,则无论ASN为多少,其字段都无意义
TCP规定,在连接建立后所有传送的报文段都必须把ACK置1
ASN 的填写规则:
填写要接收的下一个字节的数据(本次收到的数据的最后一个字节的下一个)
如果没有收到对方发送的应答,可能的情况有:
- 对方没有收到发送的数据,所以没有应答,没收到的原因可能很多,比如数据还在路上,但是还未到达,或者数据已经在路上丢失了,永远不可能到达对方
- 对方收到发送的数据,也应答了,但我们没有收到,没收到的应答的原因,比如应答还在路上,但是还未到达,或应答已经丢失了
数据或者应答还在路上的情况,可以通过一定的超时机制解决该问题
如果数据在发送的过程中就丢包了,重发肯定没有问题
如果数据发送到对方了,但是应答在路上丢失了,这个时候的重发,可能会导致对方收到重复的数据
不过也没关系,通过序列号就能判断这个数据是不是重复的,TCP会保存SN,如果收到的信息的SN已经存在了,那就直接丢弃,如果不存在,说明是新的数据,那就保存并发送应答
超时时间,怎么计算比较合理
一般来说,设置一个稍大于Round Trip Time(RTT)的时间即可,RTT就是数据发送加上收到应答这个过程的时间
但是TCP没有办法知道RTT,所以现实中,无法做到特别理想
早期的时候,就直接给一个相对比较大的时间,保证比地球最远的两个主机之间的RTT都稍大一点,这就一定能保证超时时间大于RTT
另一种就是实时计算RTT,这个有点智能,目前还在实验阶段,暂时不需要了解
超时时间是会改变的,一开始会设置的比较短,出现ack丢失的情况后,会适当把超时时间延长
比如在Linux中,超时以500ms为一个单位进行控制,每次判定 超时重发的超时时间都是500ms的整数倍,如果重发一次之后,仍然得不到应答,等待 2 * 500ms 后再进行重传,如果仍然得不到应答,等待 4 * 500ms 进行重传
重发不会一直进行,否则如果物理层出现了问题,无论重发多少次,都是没有用的
所以一般就是尝试几次(不同的OS实现不同,但一般也能配置)重发,发现仍然收不到ack,则停止发送
然后就会通知应用层,告知发送者发送失败,在Java中write()
就会以异常的形式提示
最后放弃之前,会最后尝试联系下对方,会发送一种叫做reset segment
缓冲区
TCP是有发送缓冲区的,用于暂时保存已发送,未应答的数据,为什么要进行保存,因为TCP为了保证数据确切地被对方接收到,需要对方发送的ASN,如果对方没有应答,就需要重发,如果不对数据进行保存,就没有办法重发了,所以发送缓冲区主要也是为了保证可靠性而存在的
TCP有接收缓冲区,这与UDP协议一直,因为接收到的信息,不一定马上就能被应用层取走使用
应用层使用TCP进行数据发送,如果本次发送成功,只能意味着:数据放在本机TCP的发送缓冲区中
ISN值的设置
为什么ISN不设置成0,而是采用随机值,这主要是站在安全角度考虑
如果ISN设计成0,很容易有恶意的用户推算出来合法的SN的值,这样伪造TCP SN的成本很低,使用随机值,能一定程度避免安全隐患
作为一台主机上的TCP,需要:
- 内部针对每一条TCP的通信链路(信道),维护一组数据,至少包括:ISN、当前SN、ASN、发送缓冲区、接收缓冲区、五元组信息等等
- TCP为了可靠性以及保证数据的交换,在正式的数据通信之前,需要和对方(TCP)进行一定的同步(synchronize)操作,本机把一些初始的重要信息发送给对方,若收到了对方的应答,就能知道对方一定存在
根据这两点,TCP就有了连接和连接管理的概念(一条连接的一生 = 一开始创建 + 正式使用 + 销毁)
连接(Connection)是一个人为抽象的概念,是看不见摸不着的
主动连接方和被动连接方的连接就代表一条TCP信道,但实际在TCP两层的内部,仅仅是一些数据而已
用Java的视角,就是用一个Conection对象维护
连接建立称为握手(handshacke),销毁连接称为挥手
双方相互同步自己的基本信息
TCP的主动连接方,一般是客户端这个角色承担;TCP的被动连接方,一般是服务器这个角色承担;但实际上,离开了应用层,很少提客户端和服务器的概念,因此建议还是使用主动连接方和被动连接方
TCP的所有segment都要确认应答,同步信息segment也不例外,因此主动连接方发送SYN,然后被动连接方回应ACK,然后被动接收方也发送SYN,主动接收方也回应ACK,逻辑上至少要有4层,少任意一个就会存在漏洞
被动连接方的ACK和SYN几乎是同时的,且TCP也支持一个segment同时起到SYN和ACK的作用,所以这两个可以合并
最终就变成了主动连接方发送SYN,被动接收方发送并回应SYN + ACK,然后主动接收方收到后再次回应ACK,这就是三次握手
SYN标志位在header中也只占据一个bit位,当其值为1的时候,具有SYN功能,如果为0则不具备SYN功能
关于三次握手是否能够携带payload问题
- 第一次SYN,不能携带数据
- 第二次SYN + ACK携带数据(因为前两次不能确认连接一定是成功的,如果在这个阶段携带数据,会提升发送成本,但有可能失败,所以协议设计时,禁止了携带数据)
- 第三次ACK,可以携带数据,但不强制
交换信息双方的SN是独立,不同的:
- 第一次发送SYN:[SYN] (这个方括号表示置位) len = 0 SN = a ASN = 0(这里还没置位,什么值都无意义)
- 第二次发送SYN + ACK:[SYN, ACK] len = 0 SN = b ASN = a + 1
- 第三次发送ACK:[ACK] len > 0 SN = a + 1 ASN = b + 1
请求连接则置同步位SYN=1,确认连接就置确认位ACK=1(TCP规定,SYN报文段不能携带数据,但要消耗一个序号;ACK报文段可以携带数据,但如果不携带数据则不消耗序号)
这里的首部header长度表示TCP头部有多少个32bit位,也就是多少个4字节,上图中是8,所以总共32字节,然后整个segment也是32个字节,说明payload的长度为0
三次握手的状态转移过程
状态是指连接当前处于何种情况,引入状态就是因为通过状态,管理者(TCP)能了解每个连接当前处于何种情况
观察下面的状态转移图,三次握手阶段主要关注红色部分
虚线表示被动连接方,实线表示主动连接方;线的起点是起始状态,线的终点是转移后的状态
线上的文字有两层含义:第一个是因为什么原因导致的状态转移;第二个是状态转移期间需要做的动作
ServerSocket serverSocket = new ServerSocket(8888);
代码中,创建服务器套接字就开始监听了即close到listen
需要注意的是,这些状态并不代表某一时刻的状态,而是一个时期或者说一个过程,这一过程的任意时刻都是该状态
再次结合下图理解:
这个过程无法被应用层看到,换言之,应用层无法看到三次握手的中间过程
Socket socket = serverSocket.accept();
执行该语句前是listen状态,执行完之后就是establish状态了
为什么要有握手阶段(同步阶段)
为了可靠性,确保对方在线,并且需要同步给对方一些基本信息
三次握手过程中的状态转移
- 为什么要有状态
- 学会阅读状态转移图
- 状态变化和外部事件的关系
挥手的标志位:FIN
挥手过程中主机的角色:
- 主动挥手方:主动断开连接的一方
- 被动挥手方:被动断开连接的一方
- 同时关闭:双方均属于主动挥手方
要注意,主动挥手方并不一定就是主动连接方,两者没有直接关系,是相互独立的
四次挥手的变化:
梳理一下四次挥手的整体流程:
客户作为主动挥手方,发送断开连接的请求,状态从ESTABLISHED转换到FIN_WAIT1状态
服务器作为被动挥手方,接收到FIN后,状态从ESTABLISHED转换到CLOSE_WAIT(该状态将在下面做详细解释,这里先略过),然后发送应答,ACK标志位置位
然后主动挥手方在接收到对方发送的ACK之后,进入FIN_WAIT2状态,等待被动挥手方发送FIN
被动挥手方过了一段时间之后(这一段时间都在做什么?详见下文CLOSE_WAIT状态分析),发送FIN给主动挥手方,然后进入LAST_ACK状态,等待对方的ACK应答;
主动挥手方接收到了FIN之后,进入TIME_WAIT状态(关于为什么这里要等待一段时间,下文会与CLOSE_WAIT一起分析),然后发送ACK应答
被动挥手方收到应答之后就进入CLOSED状态,主动挥手方在经过一段时间等待之后也进入CLOSED状态,至此,连接断开
这里我们看到和三次握手不同的是FIN和ACK并没有合并,那是因为TCP协议允许一方挥手,而另一方不挥手的情况
当然,想合并也不是不行,来看一下状态转移图:
红色表示三次挥手,蓝色表示同时关闭 ,没有任何标注的是标准的四次挥手
三次挥手时序图:
同时挥手时序图:
关于CLOSE_WAIT和TIME_WAIT状态:
思考:如果服务器上出现了大量的CLOSE_WAIT状态的TCP连接,请问这种现象是否合理?并说明理由
答案是不确定;单纯从现象上看,无法断定是否合理
因为如果程序设计的时候,会出现较长时间的单方面关闭的情况时,出现大量的CLOSE_WAIT是合理现象
但如果程序没有这么设计,那么就是不合理,可能的原因是被动挥手方忘记调用
socket.close()
所致
为什么会有TIME_WAIT?是否有存在的必要(参考上面对TIME_WAIT的介绍)
先说结论,TIME_WAIT的存在是为了确保被动挥手方已经关闭,并且有存在的必要
在进入TIME_WAIT状态前,主动挥手方发送了ACK应答,如果该报文没有被对方接收,对方会再次发送FIN报文,为了确保连接能够正常断开,就不能直接释放连接,需要在该状态等待确认对方是否因为没收到ACK而再次发送FIN的情况,如果没有才可以正常关闭
针对TIME_WAIT状态,我们可以设想一个场景来说明其必要性:
路人甲拥有一个手机号码123123,有一天,甲注销了这个手机号,运营商回收该手机号,如果运营商回收之后,直接放开申请,然后路人乙就把这个号码申请了
结果路人甲的朋友丙想给甲打电话,于是拨通了手机号码123123,路人乙接到电话之后,发现丙不是来找自己的,两个都很奇怪:你是谁啊?
同样的,数据的传输中,TCP靠五元组来区分连接,五元组作为一条连接的主键(PK),如果主动挥手方不经过TIME_WAIT直接关闭连接之后,五元组又立刻被分配出去了,如果这个时候收到了发给五元组的segment(可能是网络传输较慢的数据),那这个数据到底是给谁传的?很显然,我们已经不能区分了
为什么TIME_WAIT的时间是2MSL?
首先说一下什么是MSL(Maximum Segment Live):一个Segment能在网络上活着的最大时间;MSL是个理论值,实际中很多OS取的是经验值,一般是一分钟,所以默认情况下,TIME_WAIT持续的时间是2分钟,但这个值可以被修改
2 * MSL时间过去之后,Segment的一个来回肯定是够了
如果在2MSL中没有收到segment,则说明:
- 认为对方收到了ack(即使对方没有收到,由于我们也没收到fin,说明网络出现了问题 )
- 网络上肯定没有发送给甲的segment了,之后五元组收到的segment一定是给新的连接的
思考:服务器上发现了大量的TIME_WAIT状态的TCP连接,是否合理?并说明理由
理论上来说,确实是合理的,从标准上来说,没有任何问题,代码正常地关闭了连接
但从实践的角度来看,是不合理的,因为维护连接是有成本的(最主要的硬件成本是内存)
客户端和服务器之间的压力是不同的,客户端身上背负的连接比较少(几百条),服务器身上背负的连接很多(几十万 - 几百万)
所以,如果让服务器背负这个TIME_WAIT连接的成本,相对压力较大,所以一般建议让客户端来背负这个成本
因此,一般做网络编程设计的时候,不建议服务器去主动关闭连接(某些特殊情况下该主动还是要主动)
总结:
三次握手、四次挥手中的细节都是TCP协议内部所做的事情,作为应用层,是看不到这些细节的,对Java来说,三次握手只有简单的connect() 和 accept()
情况1:在甲的任务管理器中,直接把A进程kill(停止)掉,请问,这条连接的命运如何?
虽然进程被kill掉了(即进程没有走完main方法,也没有执行close()方法),但是一个进程的资源都是由OS分配的,一个进程有哪些资源OS都直到,所以即使进程内部没有关闭TCP连接,OS也会走进程资源释放流程,将TCP连接正常关闭,因此这条连接看起来还是甲主动关闭,正常执行了四次挥手
如果资源通过close方法释放,那么OS还会不会执行资源释放流程了?
- TCP就是OS提供的机制,应用层可以通过Socket使用这些有OS提供的机制,比如socket.close()方法
- 由OS提供的系统调用称为System Call Interface - SCI
情况2:直接重启电脑,连接的命运是怎么样的?
点击重启之后,执行OS的逻辑,在电脑关闭之前,关闭所有的进程并释放所有进程的资源
因此也是正常的四次挥手
情况3:直接关机
同理,只要OS的代码还能执行,连接就会正常关闭
情况4:直接拔掉甲的电源或者强制关机
首先要知道OS是软件(软件就是程序,就是数据 + 指令,就是运行在CPU上的指令,以及指令要处理的数据),拔掉电源之后硬件都无法工作了,作为软件的OS更不能再执行任何操作
至于该链接的命运需要分情况讨论:
甲主机的命运:由于连接只是逻辑上的概念,表现在现实中,只不过是内存中的一段数据罢了,如果断开电源,作为硬件的内存无法工作,这些数据就不存在了,因此连接也就不存在了,但这个连接既不属于正常关闭,也不属于异常关闭,就是突然消失了
乙主机上的连接的命运,需要分情况讨论:
- 如果乙发生了写事件(即乙向甲主机发送数据了),由于甲主机都关机了,因此乙主机无法收到应答,即使超时重传之后还是收不到,经过多次尝试的乙主机开始走异常关闭流程:关闭该TCP连接;以异常的形式通知应用层;最后发送一条reset segment。从甲消失到乙最终断开连接需要一段时间,而不是瞬时的
- 如果乙只是在单纯的读取数据,那么乙根本无法得知甲到底还在不在,只能说甲一直没发送数据,乙也一直收不到数据,那么这条连接就永远无法断开,一直保持ESTABLISHED状态
针对这种情况4的解决方案
TCP层面有种Keepalive机制:定期地发送一些数据给对方(payload长度为0),segment长度不是0,就可以根据对方有没有应答来判断;这个机制应用不多。。。。
更常见的办法是应用层自己来做这个工作:
- 一种方法是应用在进行read的时候,不要无限制地read,而是带上一个超时时间(read timeout);
- 另一种方法是定期主动给对方发送数据(相互报平安),这种数据包称为heartbeat - 心跳包
标志位中的RST表示异常
Reset Segment - RST:收到这种rst segment,就代表异常了,立即关闭连接,不用在四次挥手了,然后以异常的方式通知应用层
通过命令行命令,可以查看主机上的TCP:由于macOS的操作有些不同,因此不在这里解释
Flow Control - 流量控制
流量控制(广义):发送端会根据对方接收能力和网络承载能力,动态地调节自己的发送流量
如果在对方只能接收少量的文件或者网络很堵的情况下,发送大量的数据,对方只能收到一部分,收不到的另一部分,就可能来不及接收或者根本就没收到而导致数据的丢失,因此通过流量控制来提高数据的到达率来提高可靠性
广义可以分为狭义的流量控制(其实指TCP协议下专门的流量控制)和拥塞控制 - Congestion Control
- 流量控制(狭义):根据对方的接收能力来调节发送流量
- 拥塞控制:根据网络的承载能力来调节发送流量
接下来就专门针对TCP协议下的流量控制进行介绍
流量控制:
让对方主动告知,也就是放在Segment Header中把接受能力携带发送过来
发送segment的时候,把自己的接收能力(接收窗口)填写到segment header的串口字段中,发送给对方
如何做到实时?
- 让所有发送的segment都携带接收窗口
- 即使一段时间内没有数据要发送,也时不时发送一个ACK + WINDOW过去
接收窗口大致 = 接收缓冲区大小 - 已用大小(接收的数据,暂时没被应用层读走)
最大发送量 = 对方的接收窗口
通过滑动窗口机制控制发送量
前置知识,梳理下发送缓冲区的逻辑部分有哪些:
应用层写入的数据有可能大于接收窗口也有可能小于接收窗口,为了便于理解,之后的介绍都将基于后者
其次,对于应用层写入的数据,TCP既可以全部发送,也可以部分发送
在TCP协议下发送的数据,也可能只有部分数据收到应答,也有可能全部都会应答
那么在上图中,发送并应答的数据就没必要再保留了,因此我们拿来做可用空间
整个发送缓冲区被看做逻辑上的几个部分:
- 未使用空间
- 应用层已写入数据(包括目前可发送的(处于滑动窗口内)、暂不可发送的)
- 目前已发送(包括已发送未应答、已发送已应答(这部分空间已经可以利用了))
如果发送方每次都只发送一个数据,接收方每收到一个数据就做一次应答,然后发送方收到应答之后再发送下一个数据,这样的效率是比较低的,那么我们可以一次发送多条数据,这样就可以大大提高性能;
比如一次性发送四个数据,然后发送方收到第一个数据的ACK后,滑动窗口向后移动,继续发送第五个段的数据;依次类推;操作系统内核为了维护这个滑动窗口,需要开辟发送缓冲区来记录当前还有哪些数据没有应答;只有确认应答过的数据,才能从缓冲区删掉
我们下图中发送方发送的数据就是建立在连续发送的基础上的
TCP主动发送数据情况下,导致窗口滑动的原因:
左侧是根据ASN进行变化,右侧是根据ASN + window进行变化的
左侧数据收到应答之后,就可以丢弃了,滑动窗口的左侧就要右移
而右侧则根据ASN应答,和window来决定
通过滑动窗口机制,保证了TCP不会发送过量,即不会发送对方没有能力接收的数据
如果滑动窗口发送的数据在中途丢包了会如何?
情况一:数据包已经到达,但是ACK在中途丢失了
这种情况无需担心,如果发送方接收到某个ACK,就能推测该ACK之前所有的数据都已经到达,因此前面的数据也可以通过这个ACK进行确认
情况二:数据报还未到达就丢失了
这种情况下,如果中间某个数据包丢失,那么后续的数据包的ACK,全是上图中1001这样的ASN,因为ASN的填写规则就是要接收的下一个字节的数据,0-1000之后的下一个数据就是1001,由于一直没有收到1001数据包,接收方收到数据就发送1001的应答,就好像在告诉发送方,我的下一个数据是1001
当发送方收到3次重复的应答时,就认为该数据包已经丢失了,因此不会等到超时重传,而是立即重发
当接收方正确收到这个1000-2000的数据以后,后续ASN恢复到要接收的下一个数据
这种机制被称为“高速重发控制”,也叫“快重传”
滑动窗口的变化和拥塞控制也有关,下面就对拥塞控制进行介绍
Congestion Control - 拥塞控制:根据网络目前的承载能力控制发送量
我们从3个角度来理解拥塞控制:
网络的承载能力就好像现实中路面拥堵的情况,这个值无法通过某角色,发送一个精确值告诉你
所以拥塞窗口实际上是一个通过动态算法来实时计算出来的结果 - 本质上是一个估算值
拥塞控制的算法有很多:慢启动、拥塞避免算法、拥塞状态时的算法、快速恢复算法;接下去我们要介绍的算法是「慢开启,快启动」,即上述的拥塞避免算法,这是一种比较古老的算法,该算法的精确度不够高,不适用于现在的网络环境,但是面试要考
该算法根据丢包率作为重要的因素来推算,丢包率 = 单位时间内,没有收到应答的占比,或者说TCP重发的次数占比
拥塞避免算法,又称为慢开始,快启动算法
- 横坐标:时间
- 纵坐标:当前计算出来的拥塞窗口
- 慢开始:cwnd的初始值非常小,为1
- 指数增长转变为线性增长的中间阈值:ssthresh
- 指数增长:cwnd = cwnd * C(常数,下图中取2)
- 线性增长:cwnd = cwnd + C(常数,下图中取1)
- 快启动:指数增长速率快于线性增长
当处于指数增长的过程中时,如果cwnd大于ssthresh,指数增长就变为线性增长,如果一直没有丢包,cwnd就会一直增长到最大值
当丢包率大于某个阈值了,就相当于网络拥塞了,会发生一下变化:
- ssthresh根据当前cwnd重新计算,得到一个新的ssthresh = cwnd / 2;
- cwnd会重新变为初始值,即cwnd = 1
这种算法一遇到网络拥塞,就把cwnd变为初始值,因为现代的网络哪怕是丢包也很久就会恢复,而早期网络不好的时候,一旦遇到丢包,可能一段时间内都会丢包,所以说该算法只适用于早期网络,不适用于现在的网络
至于为什么ssthresh要除以2,也好理解:如果cwnd在处于一个较高层次才发生丢包,说明当前网络状况还不错,可以适当提高阈值,让下一次开始的前期启动更快一些;如果cwns处于一个较低层次就发生了丢包,说明当前网络状况可能并不乐观,于是适当降低阈值,让前期的启动慢一些,尽可能避免发生拥塞
发送窗口 = m i n ( 拥塞窗口 , 接收窗口 ) min(拥塞窗口, 接收窗口) min(拥塞窗口,接收窗口)
如果拥塞窗口为100,接收窗口为10,那么发送窗口就是10
反过来如果拥塞窗口为10,接收窗口为100,那么发送窗口也是10
因此发送窗口并不需要担心拥塞窗口一直增长,因为接收窗口可能达不到那个大小
依然是通过滑动窗口,当拥塞窗口发生了变化(可能增长也可能发生拥塞而减少),滑动窗口就会根据拥塞窗口进行调整,由于只是发送窗口的变化导致的滑动窗口的变化,因此在滑动窗口的右侧改变,左侧不动
至此,我们能够直到滑动窗口的左边根据发送并应答移动,右边则是根据发送并应答 + 发送窗口移动,而发送窗口由拥塞窗口以及对方接收窗口决定
思考:
由于有流量控制、拥塞控制的存在,请问发送方的应用层本次写入[a, b, c, d]4个字节的数据,请问发送方的TCP层能包装数据是按照[a, b, c, d]作为完整的segment的方式去发送的吗?
并不一定,由于滑动窗口的存在,我们并不能保证 [a, b, c, d] 这四个字节的数据正好处在滑动窗口内,完全哪有可能就被一刀两断了
那么假设接收方收到数据:[a, b, c, d, e, f, g, h, i, j, k],请问接收方能够分辨发送方写了几次,每次是哪几个吗?
很显然是不能的,因此作为接收方的应用层也无法直到这些数据到底是分了几次来的,属于那一部分的
这就说明面向报文的特点已经无法做到了,TCP协议为了可靠性,放弃了面向报文的特性,称之为面向字节流
如果接收数据的主机立刻返回ACK应答,这时候返回的窗口可能比较小
假设接收方,每收到一次数据马上就应答,那么对于发送方来说,每次应答返回的窗口都很小,远小于接收端的接收缓冲区,这么小的数据又会很快被处理掉,这样的传输效率是不高的
因此可以让接收方收到数据之后先等一等,等到窗口比较大了,在发送应答,这样一次就能处理较多的数据,传输效率也就比较高
当然,并非一定要等到窗口足够大了才发送应答,还是存在限制的:
数量限制:每隔N个包就应答一次
时间限制:每隔最大延迟时间就应答一次
具体的数量和超时时间,依操作系统不同也有差异;一般N取2,超时时间取200ms
该机制是为了减少应答次数的,如果需要应答的时候正好有数据要发送,就“搭顺风车”一起发送给对方
面向字节流给应用层提出了新的挑战:
应用层的协议设计,必须手动设定边界,常见的方法有:
- 采用定长的信息:比如一个请求一定是100个字节
- 先定长发送后续数据的长度,再发送数据,也就是携带长度的数据:比如先发送4个字节的定长数据(内容是后续数据的长度),然后在发送该长度的数据
- 通过特殊字符来分隔,比如“/r/n”, "$$$"等,这个有程序员自己设定,只要不与正文内容冲突即可
关于TCP的标志位URG和PSH:
URG:Urgent(紧急)配合16位的紧急指针来使用(这套设计已经过时了,现在都是通过两条信道实现)
假设通过TCP协议发送了一段数据:[a, b, c, d, e, f, g],其中d这个字节的数据非常重要(称为紧急指令),
将urg置位为1,然后让紧急指针指向d字节所在的偏移量 = 3,让接收方优先处理这个字节,优先传递给对方应用层
现在比较好的处理是使用两条信道,让紧急指令单独走一条信道,其他走普通信道,接收方收到之后,会优先处理紧急指令所在信道的数据
PSH:Push(推)要求发送方和接收方的TCP尽快发送数据出去
TCP会针对数据发送做优化(一次尽量多发一点数据)
这个标志位主要是给接收方用的,让发送方赶紧发送数据,哪怕只有一点数据也先发过来的意思
在这个标志位现在基本失去了作用,因为被“滥用”,如果所有人的PSH都置位,也就是每个人都说自己很急,那就没办法处理了
TCP协议如何保证数据的有序性?
TCP协议通过序列号保证了数据的有序,发送端主机每次发送数据的时候,会带上SN,而对于接收端来说,它需要通过SN对发送来的数据进行确认,只有当接收端收到了连续的序号的数据时,才会将数据上交给应用层,否则它不会上交给应用层,比如发送0-1000,1000-2000,2000-3000的数据,中间的1000-2000丢包了,那么接收方最多只把0-1000上传给应用层,而不会把后面的数据上传给应用层,并且由于快速重传机制,会一直发送1001告诉发送方这部分数据没收到,当接收方收到这个数据之后,就对这些数据重新排序,所以就保证了数据的有序性
可靠性:
- 校验和
- 序列号(按序到达)
- 确认应答
- 超时重发
- 连接管理
- 流量控制
- 拥塞控制
提高性能:
- 滑动窗口
- 快速重传
- 延迟应答
- 捎带应答
其他:
- 定时器(超时重传定时器,保活定时器,TIME_WAIT定时器等)