文章目录
- 一、TCP报头
- 二、TCP的特性
- 2.1确认应答
- 2.2超时重传
- 2.3连接管理
- 2.3.1三次握手
- 2.3.2四次挥手
- 2.4滑动窗口
- 2.5流量控制
- 2.6拥塞控制
- 2.7延时应答
- 2.8捎带应答
- 2.9面向字节流
- 2.10TCP异常情况的处理
一、TCP报头
TCP报头(header)分为11个部分,TCP报头大小为(20~60个字节):
①16位源端口号。
②16位目的端口号。
③32位序号。
④32位确认序号。
⑤4位首部长度(TCP报头长度)。TCP报头长度不固定,因为报头中有一个部分叫选项(option)。TCP报头最短是20个字节(选项为0字节即没有选项),最长是60个字节(选项最多是40个字节)。
4位首部长度的4位是4个比特位的意思,这里以4个字节为一个单位,选项也是以4个字节为一个单位,这4个比特位最大表示的数为0xF(15),则首部长度最大为4*15字节(60字节)。
⑥保留(6位)。UDP有一个问题就是数据报长度最多为64kb而且改不了。TCP这里的保留(6位)是现在不用以后有需要时再使用,留下了扩展的余地。如今保留(6位)还没有被使用到。
⑦6个标志位(URG、ACK、PSH、RST、SYN、FIN)。
⑧16位窗口大小。
⑨16位校验和。
⑩16位紧急指针。
⑩①选项。
二、TCP的特性
可靠传输是TCP最核心的特性。引入可靠性是要付出代价的,最明显的代价是两方面:①传输效率变慢。②机制的复杂程度更高。
这也是UDP能够存在下来没有被TCP完全取代的原因,特别需要性能的场景下,UDP会比TCP更胜一筹。传输层协议主要是TCP和UDP,还有一些其它协议可以做到在TCP和UDP之间进行权衡(在可靠性和性能之间进行权衡),比如quic、kcp……
为了达到可靠传输的效果,TCP有一些核心的机制,比如确认应当、超时重传、连接管理等。
2.1确认应答
发送方把数据发给接收方之后,接收方收到数据之后就会给发送方返回一个应答报文(acknowledge(ack))。发送方如果收到这个应答报文,就可以知道自己的数据是否发送成功了。
在网络通信过程中会存在“后发先至”的情况,比如A向B发了第一条信息之后又发了第二条信息,B向A回复信息时本来是先回复第一条信息再回复第二条信息,但数据包在网络传输的过程中走的路径非常复杂不同的数据包走的路径可能不同,这就会导致B第二条回复信息先到达A、第一条回复信息后到达A的问题,比如在微信聊天的过程中这就会导致双方的聊天内容对应不上。
TCP在此处要完成两个工作:①确保应答报文和发送出去的数据能对应得上。②确保在出现后发先至的情况时能够让应用程序仍然按照正确的顺序来理解数据。
这依赖于TCP报头中的32位序号和32为确认序号。TCP可以针对A发出去的数据进行编号,同时应答报文也可以针对发过来的数据的序号进行应答,序号是整数序号其大小关系描述了数据的先后顺序。这样做一方面发送方可以根据应答报文的确认序号对应到自己刚才发送的数据上确保了应答报文和发送出去的数据能够对应得上,另一方面也可以通过序号或确认序号对数据报进行排序来解决后发先至的问题。
TCP面向字节流,按照字节来进行编号,TCP传输数据的时候,初始序号一般不从1开始(背后的设计逻辑这里暂时不做解释)。比如A第一次向B发送1000个字节的数据(第1个字节到第1000个字节的数据),假设这个TCP报头序号为1,在应答报文的确认序号中就会填写1001,应答报文的意思是收到的数据是第1个字节到第1000个字节的数据表明第1001个字节之前的数据都被B收到了(B接下来向A索要第1001个字节开始的数据),接下来A就会向B发送第1001个字节到第2000个字节的数据。
达到可靠传输最核心的机制是确认应答。
那么如何确认一个数据报是普通数据报还是应答数据报ack呢?TCP报头中有六个标志位,其中ACK为1表示当前数据报是应答报文,此时数据报的确认序号字段能够生效;ACK为0时表示当前数据报是普通报文,此时数据报的确认序号字段不生效。
2.2超时重传
确认应答针对的是网络通信比较理想的情况,如果在网络传输的过程中出现丢包,这时发送方就无法收到ack数据报了,针对这种情况TCP设置了超时重传机制,超时重传是针对确认应答进行的补充。
为什么会出现丢包?因为如果网络中的数据过多,超过了路由器/交换机传输数据的上限,路由器/交换机不会把积压的数据报都保存好,而是会把其中的大部分数据包都丢弃掉。出现丢包概率的大小取决于网络基础设施(硬件设备)也取决于网络环境(网络中的数据量)。
丢包是一个随机事件,TCP在传输过程中,有两种情况的丢包:①普通数据报丢了,②返回的应答数据报ack丢了。发送方无法区分这两种情况,所以无论出现哪种丢包情况发送发都会进行超时重传。
假设丢包的概率是%10(出现%10的概率丢包已经是非常严重的丢包情况了),一次传输丢包的概率是%10,则两次传输丢包的概率为%10*%10=%1,所以重传操作大幅度地提升了数据能够被传送过去的概率。
发送方根据等待时间来决定是否需要重传。发送方把数据发送出去之后会等待一段时间,如果在这个时间之内应答报文ack来了,此时就视为数据到达;如果到达这个时间之后应答报文ack还没来,就会触发重传机制。
等待时间:①初始的等待时间是可配置的,不同操作系统上的等待时间不一定一样,也可以通过修改一些内核参数来引起等待时间的变化。②等待的时间也会动态变化,每多经历一次超时,等待时间都会变长。
上诉的超时重传还存在一个问题。如果A传数据给B,A超过了等待时间也没有收到应答报文ack,则A会进行重传,如果这个过程B其实是收到了A传送过来的数据的只是B发给A的应答报文丢包了,这时就会导致A发的是一条数据而B收到的是两条一样的数据的情况,这是一个Bug。
TCP已经帮我们解决了这个问题,TCP会有一个“接收缓冲区”即一个内存空间,会保存当前已经收到的数据以及数据的序号。接收方如果发现发送方发来的数据是一个在“接收缓冲区”中已经存在的数据(即接收方收到了重复的数据),接收方就会把这个后来的数据给丢弃掉,确保应用程序进行read输入的时候,读到的是一条数据。
“接收缓冲区”不仅能进行去重,还能对数据报进行重新排序,确保发送数据报的顺序和应用程序读取数据报的顺序是一致的。
2.3连接管理
连接管理即建立连接和断开连接(三次握手和四次挥手(handshake))。
TCP的握手指的是给对方传输一个简短的、没有业务数据的数据报,通过这个数据报来唤起对方的注意从而触发后续的操作。在计算机中很多操作都会涉及到握手:比如手机充电插上充电线的过程涉及握手、U盘插进电脑的过程涉及握手……
2.3.1三次握手
TCP建立连接的过程中,通信双方一共进行三次握手才能完成建立连接。
比如A与B建立连接,(1)A先向B发送一个syn(同步报文段,是一个简短的、没有业务数据(没有载荷即没有应用层数据包)的数据报,段segment、包packet、报datagram、帧frame这几个术语都可以形容网络上传输数据的基本单位)。
TCP报头中的六个位中有一个SYN,如果SYN为1则表示这个报文是同步报文段;SYN为0则表示这个报文不是同步报文段。
(2)B返回一个应答报文ack给A。(3)同时B也发送一个同步报文段syn给A。(4)A也返回一个应答报文ack给B。
完成上诉四步后,A和B就记录了对方的信息即构成了逻辑上的连接。
其中(2)和(3)可以合并(B将一个ACK和SYN同时为1的数据发送给A),(2)(3)合并后一共就只有三步了因此可以称为三次握手。
三次握手要解决的是什么问题(有什么样的作用、意义)?通过四次握手是否可行?通过两次握手是否可行?
三次握手要解决的是什么问题(有什么样的作用、意义):TCP的目标是为了实现可靠传输,确认应答和超时重传有一个大前提就是当前的网络环境是基本可用的,如果当前网络存在重大故障此时确认应答和超时重传也就起不到什么作用了(此时就无法进行可靠传输了)。
三次握手的核心作用一:确认当前网络是否通畅(确认可靠传输的前提条件)。
三次握手的核心作用二:让发送方和接收方都能确认自己的发送能力和接收能力是否正常(确认可靠传输的前提条件)。(1)初始情况下A和B双方都不知道自己的发送能力和接收能力是否正常,A向B发送一条数据。(2)B接收到了A发过来的数据B就可以知道A的发送能力和B的接收能力是正常的,然后B向A发送一条数据。(3)A收到了B发过来的数据A就可以知道A的接收能力和B的发送能力是正常的,同时A也知道了A的发送能力和B的接收能力是正常的(因为B向A发送一条数据的前提是A的发送能力和B的接收能力是正常的),然后A再向B发送一条数据。(4)B收到A发过来的数据就知道A的接收能力和B的发送能力是正常的。
三次握手核心作用三:让通信双方在握手的过程中针对一些重要的参数进行协商。
比如TCP在通信过程中报头中的序号从几开始就是通信双方协商出来的,一般不是从1开始,每次建立连接的时候都会协商出一个比较大的、和上次不太一样的值。
这样设计是因为有时网络不太好,客户端和服务器之间的连接可能会断开再重新建立连接,比如新的连接建立好之后,旧连接的数据才来到目的端,这种迟到的数据应该丢弃掉不让其影响新连接的业务逻辑。那么如何判断这个数据是否来自旧连接呢?这可以通过上诉协商序号的设定来实现,如果发现收到的数据的序号和当前连接的数据的序号差异非常大就可以判定这个是旧连接的数据就可以直接丢弃掉。
通过四次握手是否可行:可以进行四次握手,但没有必要,我们可以将中间的两次握手合并成一次握手,这样做效率更高,因为如果两个数据不合并分别发送的话每一个数据报都要一层一层地封装一层一层地分用这样效率没有那么高。
通过两次握手是否可行:两次握手不可行,因为两次握手的话只是A找到A和B的发送能力和接收能力正常,而B只知道A的发送能力和B的接收能力正常(B无法知道自己的发送能力是否正常)。两次握手使接收方不能确认自己的发送能力是否正常。
三次握手的TCP状态(类似于线程状态):
LISTEN:这是服务端状态,服务器这边将ServerSocket对象创建好并且把端口号绑定好,此时就会进入LISTEN状态,此时允许客户端随时过来建立连接。
ESTABLISHED:这是服务器和客户端都会有的状态,表示连接建立完成,接下来客户端和服务器就可以进行正常通信了。
2.3.2四次挥手
建立连接一般是客户端主动发起,断开连接则是客户端或服务器都可以主动发起。
(1)A向B发送一个结束报文段FIN(FIN是TCP报头六个标志位之一),(2)B向A发送应答报文ACK,(3)B也向A发送结束报文段FIN,(4)A也向B发送应答报文ACK。
经历了上诉4步之后A和B把对端的信息都删除了相当于断开了连接。
至于(2)(3)能否合并,这不能确定。
不能合并的原因:(2)B向A发送应答报文ACK是内核进行触发的(即(1)完成之后会立刻执行(2)),而(3)B也向A发送结束报文段FIN是应用程序代码调用close()方法后才会触发(比如clientSocket.close())。从B收到结束报文段FIN(同时返回ACK),到B执行close()方法,这中间要经历多少代码和时间是不确定的(要看应用程序的代码是怎样写的)。
B向A发送结束报文段FIN会在Socket对象close的时候被发起,也可以是进程结束时发起。
能合并的原因:TCP中有一个延时应答的机制,能够拖延应答报文ACK回应的时间,一旦B向A发送的应答报文ACK的时间滞后,则B向A发送的应答报文ACK就有机会和B向A发送的结束报文FIN合并了。不是所有的数据报都会进行延时应答(数量限制:每隔N个包就延时应答一次,时间限制:超过某个最大时间就延时应答一次)。所以不能事先预测(2)(3)是否能合并。
所以4次挥手是有可能3次挥完的,而3次握手不可以4次握完或2次握完。
三次握手中间的(2)B返回一个应答报文ack给A和(3)同时B也发送一个同步报文段syn给A都是内核进行触发的,在同一个时机触发所以可以合并。
如果是正常的4次挥手,这时就按正常的流程来断开连接。
如果是不正常的挥手(B的应用程序Socket对象没有调用close且进程没有结束导致B一直没有将结束报文段FIN发给A,即没有挥完四次手),这时就按异常的流程来断开连接(流程会更加复杂一点)。
四次挥手的TCP状态:
CLOSED:这是客户端和服务器都会有的状态,当进入CLOSED状态时连接就彻底断开了。
TIME_WAIT:哪一方主动断开连接,哪一方就会进入TIME_WAIT状态(比如A主动断开连接,在A发送结束报文FIN给B、B发送应答报文给ACK、B发送结束报文FIN给A后,A会进入TIME_WAIT状态)。TIME_WAIT状态是为了防止最后一个应答报文ACK丢失(比如在这里为了防止A给B发送的应答报文ACK丢失)。如果A给B发送的应答报文ACK丢失,站在B的角度B就会触发超时重传,B会把结束报文FIN再传一次给A,如果A没有TIME_WAIT状态就进入CLOSED状态则A在上次接到结束报文FIN后就会立刻释放连接,这时就收不到B给A重传的结束报文FIN了,所以为了能收到B给A重传的结束报文FIN,A接收到上次的结束报文FIN后会进入TIME_WAIT状态等待一段时间,因为B有可能会重传结束报文FIN给A。
如果因为A没有TIME_WAIT状态导致B重传多次结束报文FIN给A,A都没有响应,此时B也会断开连接,但这种断开连接的方式不太好。
如果TIME_WAIT状态期间A一直没有收到B重传过来的结束报文FIN,则表示A发给B的应答报文没有丢包,所以A的TIME_WAIT状态结束之后A进入CLOSED状态。
那么TIME_WAIT状态等待多长时间才会进入CLOSED状态呢?假设网络上两个节点通信消耗的最大时间为MSL(MAL是可配置的参数),此时TIME_WAIT等待的时间就是2MSL(相当于第1个MSL等待A传输应答报文ACK给B,第2个MSL等待B重传结束报文FIN给A)。
2.4滑动窗口
确认应答、超时重传、连接管理都是在保证TCP的可靠性。
TCP的可靠传输会影响传输的效率(比如多出了等待应答报文ACK的时间导致单位时间内传输的数据变少了)。滑动窗口的引入就是为了让可靠传输对性能的影响更小一些。
滑动窗口可以缩短确认应答的等待时间:
比如A批量传输数据给B(相当于一个数据发送出去之后不等待应答报文ACK回来就直接又发下一条数据),这个批量传输的“批量”是有上限的,即发送数据达到上限之后会统一等待应答报文ACK,批量最多发送的这个数据量称为“窗口”。
比如A向B发送了3个数据,此时B也要向A回应3个应答报文ACK数据,A向B发送了3个数据之后A已经达到了窗口的大小,A在收到ACK之前是不能继续往后发数据的,A正在等待的3个应答报文ACK数据谁先到达A不能确定,所以A收到一个应答报文ACK数据就向后发一个数据,看上去好像窗口在向后滑动。滑动窗口越大,等待的应答报文ACK越多,传输的效率就越高(相当于一份等待时间等待的应答报文ACK变多了那么总的等待时间就变少了)。操作系统内核为了维护这个滑动窗口,开辟了发送缓冲区来记录当前还有哪些数据没有应答,只有确认应答过的数据,才能从缓冲区删掉。
在不出现丢包的情况下,滑动窗口可以完成确认应答的机制,但在滑动窗口的机制下又该如何解决丢包的情况呢?这与前面提到的超时重传有一些不同。
情况一:应答报文ACK丢包。在这种情况下,不需要任何的重传。因为确认序号表示的含义是当前确认序号之前的数据已经收到了下次应该从确认序号这里的数据开始往后发送。因为滑动窗口是批量发送,如果A的第1 ~ 第1000个字节的应答报文ACK(确认序号为1001)丢了,但是A收到了第1001 ~ 第2000个字节的应答报文ACK(确认序号为2001),A收到确认序号为2001的应答报文ACK说明A的1~2000个字节已经成功发送给了B,即ACK(确认序号为2001)包含了ACK(确认序号为1001)。
如果应答报文ACK全部丢包了,说明网络出现了严重故障,已经无法进行可靠传输了。
情况二:数据包丢包。比如A向B传输的第1001 ~ 第2000个字节的数据丢包了,此时B返回给A的应答报文ACK的确认序号为1001(即B向A索要第1001 ~ 第2000个字节的数据),因为滑动窗口会批量传输数据,所以A接下来继续向后不断地向B发送数据,接下来无论A向B发送的数据是第几个字节 ~ 第几个字节,B向A返回的应答报文ACK的确认序号都是1001(比如A向B发送了第2001 ~ 3000、3001 ~ 4000、4001 ~ 5000、5001 ~ 6000、6001 ~ 7000的数据,对应B回应了5个确认序号为1001的ACK给A),此时A发现B连续向自己发送的应答报文ACK的确认序号都是1001就知道A向B发送的第1001 ~ 第2000个字节的数据丢包了,于是重传第1001~第2000个字节的数据给B,这时B对应返回的ACK确认序号则为7001。
上述的重传是超时重传的变种,这种重传没有额外的冗余操作,是哪个数据丢了就重传哪个数据,没丢的数据则不需要重传,这导致整个A传输数据给B的过程是比较快速的,所以这种重传也称为快速重传。
如果通信双方传输的数据量比较小,传输数据也不频繁,TCP就仍然按照普通的确认应答和普通的超时重传机制处理数据。
如果通信双方传输的数据量比较大,传输数据比较频繁,TCP就会进入到滑动窗口模式,按照快速重传的方式处理数据。
2.5流量控制
通过滑动窗口的方式传输数据,效率会提升,滑动窗口越大传输效率越高。但不是滑动窗口设置得越大越好,因为如果传输速度过快,就可能会使接收方处理不过来,这时如果发送端继续发送数据,就会造成丢包,这样就降低了传输的可靠性,为了提升可靠性进而又要引发发送方重传,这样又降低了效率。
流量控制就是站在接收方的角度反向制约发送方的发送能力,让发送方的发送速率不要超过接收方的处理能力。
比如A向B发数据,数据到达B的操作系统内核中,TCP的socket对象上带有接收缓冲区,A向B发送的数据会先到达B的接收缓冲区中,B的应用程序中会调用read()这样的方法把数据从接收缓冲区中读出来进一步处理,一旦数据被读出来这个数据就可以从接收缓冲区中删除掉了。
那么如何量化接收方的处理能力呢?TCP通过接收方缓冲区的剩余空间大小作为衡量接收方处理能力的指标。剩余空间越大,则消费速度越快处理能力越强;剩余空间越小,则消费速度越慢处理能力越弱。
接收方每次收到数据之后,都会把接收缓冲区剩余空间大小通过应答报文ACK返回给发送方,发送方就会通过这个接收缓冲区剩余空间大小来调整下一论的发送速度(下一轮的窗口大小为接收缓冲区剩余空间大小)。如果接收方返回的应答报文ACK显示接收缓冲区剩余空间大小为0,发送方就会暂停发送业务数据,会周期性地发送窗口探测包(不携带业务数据的数据包),窗口探测包的任务是为了触发应答报文ACK进而查询接收方接收缓冲区剩余空间大小。
TCP报头中的16位窗口大小表示接收缓冲区剩余空间的大小。那16位窗口大小是否表示接收缓冲区剩余空间的大小最大只能是64kb呢?实际上不是,TCP报头中的选项里有一项是“窗口扩展因子”,通过“窗口扩展因子”可以让接收缓冲区剩余空间的大小表示成一个更大的值。
2.6拥塞控制
流量控制考虑的是接收方处理数据的能力。拥塞控制考虑的是网络通信过程中中间节点处理数据的能力。
接收方处理数据的能力很容易进行量化,而中间节点处理数据的能力不容易量化(因为中间要经历多少个节点不确定,可能这时这个节点出现数据堵塞,等一下又是另外一个节点出现数据堵塞,中间节点收到的数据会来自很多个发送方、发送的数据会发送到很多个接收方)。
因此使用实验的方式来衡量中间节点处理数据的能力,比如让发送方先按照比较低的速度发送数据(滑动窗口较小),如果数据传输得非常顺利没有丢包,则发送方再尝试使用更高的速度(更大的滑动窗口)发送数据。随着窗口增大到一定程度,这时可能中间节点处理数据处理不过来出现了丢包,发送方发现丢包了就把窗口大小调整小,此时如果发现还是继续丢包就继续缩小窗口,如果不丢包了就增大窗口。在这个过程中发送方不断地调整窗口大小,逐渐达成了动态平衡。
拥塞控制经典的坐标图:
经典版本的拥塞控制图:
横坐标传输轮次表示TCP是第几次发送数据。
纵坐标拥塞窗口表示拥塞控制下发送方应该按照多快的速度(多大的窗口)来进行发送。
拥塞窗口刚开始时从1开始按指数规律增长(开始时发送得满(慢开始/慢启动),因为是指数增长,接下来会发送得越来越快(拥塞窗口变得越来越大)),增长到ssthresh的初始值(慢启动阈值的初始值)时,会进行线性增长,增长达到网络拥塞情况出现丢包现象时,会立刻下降回1,然后慢启动阈值变为原来的一半(原来慢启动阈值等于拥塞窗口的最大值)。
一旦发现丢包就把拥塞窗口缩小,重新进行慢开始、指数增长、线性增长的过程。并且根据当前丢包的拥塞窗口的大小,重新开始指定开始线性增长的慢启动阈值(为了避免指数增长过快一下就达到了丢包的程度)。
新版本的拥塞控制图:拥塞窗口达到网络拥塞出现丢包情况时,不是让拥塞窗口下降回1,而是让拥塞窗口下降为新的慢启动阈值然后立刻又开始线性增长,这省去了指数增长的过程,因为指数增长的过程是不会出现丢包现象的,这又进一步提升了传输效率。
流量控制和拥塞控制都是在限制发送方发送窗口的大小。最终发送方发送窗口的大小是取流量控制和拥塞控制中的窗口(滑动窗口和拥塞窗口)的较小值。
2.7延时应答
正常情况下A发送数据给B,B就会立即返回应答报文ACK给A。
在延时应答机制下,A发送数据给B,B等一段时间再返回应答报文ACK给A。这本质上是为了提升传输效率。
发送方的窗口大小是传输效率的关键,流量控制就是根据接收缓冲区剩余空间的大小来决定发送速度,如果让流量控制得到的滑动窗口更大一点,发送速度就可以更快了。
而延时返回应答报文ACK,给了接收方更多时间读取接收缓冲区的数据(避免接收方还没有开始往接收缓冲区中读多少数据就立刻返回应答报文ACK了),此时接收缓冲区剩余的空间就变大了,返回应答报文ACK报头的16位窗口大小里的值就变大了,之后发送方发送的滑动窗口的大小也随之变大。
2.8捎带应答
在延时应答的基础上进一步提高效率。
网络通信中往往是一问一答这种通信模型。
比如:(1)A向B发送请求数据request,(2)B向A立刻返回应答报文ACK,(3)B向A发送应答数据response,(4)A再向B发送应答报文ACK。
B向A返回应答报文ACK是内核触发立即返回的,而B返回应答数据response给A是应用程序代码执行后再返回的,这两者返回的时机不一样。当TCP引入了延时应答后,B不一定是立即返回ACK给A,可能要等一段时间,在等待的这一段时间里B正好把应答数据response计算好了,这时就会把应答数据response返回给A,返回时顺便把刚才正在等待发送的ACK一起发回给A。
这相当于把(2)和(3)合并了,本来(2)和(3)要分开两次传输(要封装分用两次),如今合二为一提高了效率。
2.9面向字节流
TCP是面向字节流的,面向字节流会有一个很大的问题就是粘包。这里的包指的是应用层数据包,如果同时有多个应用层数据包传输,就容易出现粘包问题。
比如A向B发送aaa、bbb、ccc这三个应用层数据包,这时B的接收缓冲区里会有cccbbbaaa(aaa、bbb、ccc这三个应用层数据包以字节的形式紧紧地挨在一起),B应用程序不知道接收缓冲区里从哪里到哪里是一个完整的应用层数据包。
相比之下UDP面向数据包的网络通信方式就不会出现粘包问题,UDP的接收缓冲区中,相当于是一个一个的DatagramPacket对象(这些对象相当于一个链表结构(一个指向一个)),应用程序从接收缓冲区中读数据的时候,就可以知道从哪里到哪里是一个完整的应用层数据包。
那么如何解决粘包问题呢?解决粘包问题:通过定义应用层协议来明确应用层数据包之间的边界。
法一:引入分隔符。比如以\n作为分割符,即A向B发送aaa\n、bbb\n、ccc\n这三个应用层数据包,B的接收缓冲区中就会有\nccc\nbbb\naaa,这时B从接收缓冲区中读取数据的时候就可以持续读数据读到\n为止,B相当于就知道了一个数据包就是从数据开始的地方到\n。
法二:引入长度。A向B发送3aaa、4bbbb、5ccccc这三个应用层数据包,B的接收缓冲区中就会有ccccc5bbbb4aaa3,这时B从接收缓冲区中读取数据的时候先读到数据包的长度,再根据这个长度继续向下读取对应字节个数的数据。
自定义应用层数据包用得比较多的格式xml、json、protobuffer自身是明确了包的边界的。xml以标签作为边界、json以大括号作为边界、protobuffer通过长度来描述TCP数据包的大小从而区分各个数据包的范围。
2.10TCP异常情况的处理
TCP在使用过程中出现异常应该如何处理?
(1)进程崩溃。进程异常终止,文件描述符表也随之释放,相当于调用了操作系统socket.close(),此时本方就会触发发送结束报文FIN给另一方,另一方也会返回应答报文ACK和结束报文FIN给本方,本方再发送应答报文ACK给对方。这是正常的四次挥手断开连接的流程。TCP连接可以独立于进程存在,进程异常终止TCP连接不一定立刻断开。
(2)主机关机(正常关机)。进行关机时会触发强制终止进程的操作,此时本方会发送结束报文FIN给对方。
但本方不仅仅是进程没有了,因为关机操作系统也会关闭。如果本方在操作系统关闭之前收到对方发过来的应答报文ACK和结束报文FIN,此时本方还是可以返回ACK给对方,即进行正常的四次挥手。
如果是本方的操作系统已经关闭了,对方才开始发来应答报文ACK和结束报文FIN,此时本方无法继续向对方发应答报文ACK,站在对方的角度对方以为是自己发送的应答报文ACK和结束报文FIN丢包了于是重传应答报文ACK和结束报文FIN,对方重传几次见本方没有反应于是就断开了连接(把持有的对方的信息删掉)。
(3)主机断电(非正常关机)。断点是一瞬间的事情,来不及终止进程来不及发送FIN给对方本方就停机了。
站在对端的角度:1)如果对端是在发送数据给本端,发送数据后等了很久ACK还没有回来,于是触发超时重传,一段时间后触发TCP连接重置功能,发起“复位报文段”,如果发送“复位报文段”(TCP报头6个标志位中的RST)给本端后本端还是没有反应,此时对端就会释放连接。
2)如果对端是在接收来自本端的数据,等了很久本端还是没有发送数据给对端,这时对端是无法区分本端是一直没发数据还是本端已经崩溃了的?那么如何解决这个问题呢?TCP提高的心跳机制可以解决这个问题,在心跳机制下,接收方会周期性地给发送方发送一个特殊的、不带业务数据的数据包并且希望对方返回一个应答报文ACK,如果发送方没有应答并且接收方尝试多次重传之后发送方仍然没有反应,这时接收方就可以单方面地解除连接了。
所以如果对端是接收方,以根据心跳机制检测本端是否任然存活,本端不存活则对端单方面断开连接。
(4)网线断开。TCP处理网线断开和TCP处理主机断电非常类似。
比如A在向B发送数据,然后这两者之间的网线断开了。此时A会触发超时重传,一段时间后触发连接重置,然后再进行单方面释放连接。
B触发心跳包,然后发现A没有反应,然后单方面释放连接。