文章目录
- 1. TCP 协议概述
- 2. TCP原理
- 2.1 保持可靠性的机制
- 2.1.1 确认应答
- 2.1.2 超时重传
- 2.1.3 连接管理机制(安全机制)
- 2.1.3.1 三次握手
- 2.1.3.2 四次挥手
- 2.1.4 滑动窗口
- 2.1.5 流量控制
- 2.1.6 拥塞控制
- 2.1.7延时应答
- 2.1.8 捎带应答
- 2.1.9 粘包问题
- 2.1.10 TCP异常
- 2.1.11 TCP vs UDP
1. TCP 协议概述
TCP :传输控制协议
,要对数据的传输进行一系列的控制,是传输层的重要协议,负责数据能够从发送端传输接受端
首先说明一点,TCP的相关特性:有连接,可靠性传输,面向字节流,全双工。
UDP相关特性:无连接,不可靠性传输,面向数据报,全双工
有连接
:就好比我给你打电话,只有你接通之后,咱们两个才能进行通信。
无连接
:就好比给你微信发消息,直接输入文字发送即可,不用和你的手机建立连接。
可靠传输
:在传输的过程中,发送方知道自己发送的消息有没有到达接受方
不可靠传输
:在传输过程中,发送方不知道自己发送的消息有没有到达接搜方
面向字节流
:以字节为单位进行传输(类似于文件传输中的字节流)
面向数据报
:以数据报为单位进行传输(一个数据报有明确的大小),一次发送或者是接受必须是一个完整的数据报,不能是半个数据报,或者是一个半
全双工
:一条链路,双向通信(TCP任何一端既是发送数据方,同时也是接受数据方,因为TCP通信双方既要保证自己的发送能力同时也要保证自己的接收能力)
半双工
:一条链路,单向通信
TCP协议报文格式:
源端口号
:表示数据是从哪个进程来的
目的端口
:表示要到那个端口去
4位TCP报头长度
:表示该TCP头部有多少个32位bit(有多少个4字节),所以TCP头部最大的长度是15 * 4 = 60
6位标志位:
URG:紧急指针是否有效
ACK:确认号是否有效,其实就表示的是确认报文,如果发来SYN或者是FIN后使用确认报文ACK进行回复,这样发送方就知道接收端已经收到了所发送的信息。
PSH:提示接收端引用程序立即从TCP缓冲区把数据读走。
RST:对方要求重新建立连接,我们把携带RST标识的成为复位报文段
SYN:请求建立连接,把携带SYN标识的称为同步报文段
FIN:通知对方,本端要关闭了,我们称携带的FIN标识的为结束报文段。
16位校验和
:发送端填充,CRC校验,接受端校验不通过,则认为数据有问题,此处的校验和不光包含TCP首部,也包含TCP数据部分
16位紧急指针
:标识那部分数据是紧急数据
剩下的一些标识,如果没有介绍到的在下文你就直到是干什么的了。
2. TCP原理
TCP对数据传输提供了管控机制,主要体现在两个方面,一个就是安全,另一个就是效率
我们在保证数据传输安全的前提下,尽可能的提高数据的传输效率。
2.1 保持可靠性的机制
2.1.1 确认应答
首先说明一点,这个确认应答是可靠性传输的核心机制。
我们在前文中也说了,可靠性是 发送端知道自己发送的数据,接收端它有没有收到。那么发送端是怎么知道它发送的数据接收端有没有收到呢?
其实呀,最好的办法就是接受端在接收到数据之后,给发送到发送一个消息(我已经收到数据啦) 那么这个向发送端回复收到数据的报文就是我们的ACK
其实还有一种情况就是,我们在日常在微信或者一些通信软件上发消息的时候,我们有可能一次性发送多条数据,此时或许就会前面的消息后发出去,后发送的消息先发出去,这中就叫做后发先置。
此时就举一个例子,有一天我向我们暗恋对象发消息,先发了一个"有时间吗,一起吃饭走?" 然后在发了一条消息 "你做我女朋友好吗?"那么我此时就是发送方。然后过了一段时间暗恋的对象:他针对第一条我发的第一条消息 “好呀” 针对我发送的第二条消息 “滚”。此时的暗恋对象就是接收方。
那么就有可能发生两种情况:
其实网络传输数据接收的顺序,不一定和发送的顺序完全一样,存在后发先置的情况,后发先置对于网络来说再正常不过了,网络环境复杂,连续法的两个数据包,不一定就是同一条路。
那么为了较好的解决上述的问题,于是我们对这些发来的信息进行编号。
确认序号表示当前的这个应答报文,是针对那个消息进行确认应答的。
针对数据中的每个字节都分别进行了编号,即为序列号
每一个ACK都带有对应的确认序列号,意思就是告诉发送者,我已经收到了那个数据,下一个你从哪里开始发
2.1.2 超时重传
超时重传相当于是对确认应答做了补充。
我们可以想一想,我们在日常发送消息的时候,有没有过发送消息失败的时候,我们所知的确认应答是在网络一切正常的条件下,通过ACK通知发送方我收到数据了。
如果出现了丢包的情况,我们的超时重传就会起到效果。
这里的对丢包有两种可能。
一种就是当发送方发送数据的时候,压根就没有发送到接收方手中,直接中间的同步报文段没了就是SYN丢包了。
还有一种就是发送法已经把数据发送给了接送方,但是接受方返回的确认应答(ACK)丢包了。
那么此时的发送方就会想:既然我不知道是哪里出问题了,那么就在发送一遍数据(SYN).
但是这里的重发也不是立即重发,还是等一会给点时间反应,如果我此时等了10分钟(一个时间阈值)还是没有等到这ACK,那么我此时重发一遍数据。这就是超时重传机制。
正常情况下,如果出现了短时间的网络抖动,那么此时就会发生大量的丢包。但是过一会网络就正常了。还有就是在正常条件下,连续两次发送数据丢包的概率树非常小的,因此我就期望第二次发送能够成功。如果网络没有太大的问题,一般重传都会成功的。
还有就是如果接收端向发送端发送ACK的时候丢包了,那么此时就触发了超时重传,那么此时接收方就会得到两份相同的数据,但是我们会对相同的数据去重。
接收方收到的数据是先会放到操作系统内核的“接收缓冲区”中,这个缓冲区可以视为一个内存空间,并且也可以视为一个阻塞队列,收到了新的数据,TCP会根据序号来检查这个数据是不是在接收缓冲区中已经存在了,如果不存在,就放进去,如果存在,就直接丢弃。我们要保证应用程序调用socket api拿到的这个 数据一定是不重复的,应用程序感知不到超时重传的过程。
那么此时有一个问题:超时重传的这个数据,会重新封装吗?还是将之前封装的报文缓存下来重发?
重新封装!!!
发送出去的数据是由TCP负责发送的。
调用socket中的write操作,本质上就是把数据写到TCP的接收缓冲区中,由内核从发送缓冲区中取数据,通过网卡传输(也就是封装的过程)当触发超时重传的时候,内核再把接收缓冲区中的内容,重新进行封装再传输。
如果顺利取到了ACK,那么就会把这个数据从接收缓冲区中删除。
超时重传之后,发送方就一定会收到ACK吗?
答案:不一定
如果只是当前的网络出现了短暂的抖动,这时候超时重传是很容易成功的。如果此时到的网络遭到了严重的伤害,可能没有那么容易恢复。
这里的超时重传如果一次失败,还会进行第二次,但是也不是永不停止的重传。TCP采用动态时间的超时重传机制,就比如说第一次如果把消息发丢了,那么发送端会在500ms之后,在重发一个相同的消息,如果第二次发送消息再失败了,那么发送端就就会在1000ms之后,重发第三次请求的消息,如果累计到一定的次数,消息还是不能成功的发送出去,那么此时的TCP就会认为当前的主机存在异常,会强制关闭TCP连接。
连续几次重传都不行,就认为这个网络遇到了严重的问题,再怎么重传怕是也不行,那么只能放弃,自动的断开TCP连接。
还有就是重传的间隔时间也是不一样的,一般说重传的次数也多,中间的间隔就会越大,重传的频率就会越低。
我们可以想一想,一次传输失败的概率本来就很小,那么如果第二次再传输失败,那么此时传递数据成功的概率就会更小,此时的TCP就不指望这能把数据传递成功,其实重传的频率再高,网络不对付,那么大概率是没什么用的,还不如省省力气,
假设此时的丢包的概率为10%,那么来此重传的丢包的概率就是 10% * 10% =1%,那么就是说本来能造成两次重传的概率就很低了。那么连续两次丢包意为这当前的丢包概率已经大于10%了。
2.1.3 连接管理机制(安全机制)
说明一点这里的连接管理机制是在面试的时候屡见不鲜的题目,一定要知道中间的流程。
2.1.3.1 三次握手
三次握手:客户端和服务器之间通过三次交互,完成了建立通信连接的过程。这里的握手只是一个形象的比喻
客户端现在要想和服务器建立连接。
客户端主动向服务器发送请求(SYN同步报文段)。
那么此时的服务器如果接收到了这个连接请求(SYN同步报文段),那么此时就知道了此时客户端的发送能力和自己的接收能力是好的,并且会立即给客户端发送一个响应(ACK确认应答报文),同时还会发送一个SYN同步报文段,此时作为服务器它也想知道自己的发送能力行不行。
那么此时的客户端如果收到服务器发来的ACK报文段,那么此时的客户端就知道,自己的发送能力和接受能力是好的,同时也是到了服务器的接收能力和服务器的发动能力是OK的,那么此时作为客户端,它就知道了此时是客户端和服务器之间的链路是通的,可以进行通信。
并且此时的客户端得到了服务器发来的SYN也会给服务器发送一个ACK确认应答报文。表示客户端收到了服务器发来的请求。
那么此时的服务器就会知道 客户端的接收能力,和自己(服务器的)发送能力是OK的。那么此时的服务器也就知道了客户端和服务器通信的这条链路是通的。
再说明一下连接的时候,客户端和服务器之间的状态变化:
LISTEND:表示服务器启动成功,端口绑定成功,随时可以和客户端建立连接(就好比是我现在的手机通信是好的,别人都可以给我打电话)
ESTABLISHED:表示客户端端和服务器之间的通信连接已经成功,随时可以进行通信(就好比是,有人给我打电话,接通了就可以通信了)
SYN_SENT:表示请求连接,当你要访问其它的计算机的服务时首先要发个同步信号给该端口,此时状态为SYN_SENT,如果连接成功了就变为ESTABLISHED,此时SYN_SENT状态非常短暂。
SYN_RCVD:正处于连接初始同步状态,收到了对方的SYN,但是还没有收到自己发出去的SYN的ACK
那么三次握手有啥用?和可靠性有什么关系?
三次握手相当于投石问路,检查一下当前的网络状态是否可以满足可靠性传输的基本条件。就好比说如果你当前的网络是非常糟糕的,如果强制进行TCP传输,那么就会设计大量的丢包。更具体的说三次握手就是 想检查一通信双方的发送的接收能力到底行不行。
那么为什么在服务器给客户端发送ACK确认应答报文,和服务器给客户端发送SYN走得是同一条通信路线呢?
再举一个形象的例子,还是打电话
那么此时如果是四次握手呢?握手两次行吗?
首先四次,行,但是没有那个必要,分开传输会减低效率,不如和在一起好。
两次是肯定不行的,两次的话就对标了,发送端不给服务器发送ACK确认应答报文,那么此时的服务器就不知道自己的发送能力和客户端的接收能力是不是OK的
。此时服务器对于当下是否满足可靠性传输是不知道的,自己心里没底。
2.1.3.2 四次挥手
三次握手是让客户端和服务器之间建立好了连接,其实建立好连接之后,操作系统内核中,就需要使用一定的数据结构来保存连接的相关的信息。保存的数据最重要的就是“五元组”,即 源IP 目的IP 源端口 目的端口 TCP,那么既然保存了一部分的信息,那么肯定就要占用一部分的系统资源,那么有朝一日连接断开了,此时之前保存的连接信息就没有意义了,对应的空间就可以释放了。
四次挥手图示:
发送方,接收方分别向对方发送FIN(结束报文段)请求,并且各自对方发送一个ACK(确认报文段)
我们在上面所说的“三次握手”,一定是客户端主动发起的(主动的一方才能叫为客户端)
而我们这里的四次挥手,是断开连接请求的,可能是客户端主动发起的,也可能是服务器主动发起的。
三次握手,在建立连接的过程中 发送方和接收方交互三次即可,中间的发送ACK和发送SYN的时候,是能合并的,但是我们的四次挥手,中间的发送FIN(结束报文段)和发送ACK(请求报文段)可不一定可以合并哟。
那么在四次挥手时,为什么中间接收方向发送方,发送ACK和FIN时是不一定可以合并呢?
因为在三次握手中,接收方发送的ACK和SYN是同一时机发送的,那么此时两处通信就可以合并。此处的服务器给客户端发送的ACK和SYN都是操作系统内核负责执行的
当四次挥手时,服务器给客户端的ACK,是有内核负责的,但是服务器给客户端的FIN是由用户代码负责的,在客户端代码中调用了socket.close()方法,才会触发FIN,收到FIN立即就由内核返回ACK,执行到用户代码栈中的close才会触发,这里的断开连接,要看用户的代码咋写,如果这(发送ACK和发送FIN的时机)两个操作的时间间隔比较大,那么这个ACK 和 FIN 就不能合并,如果时间差比较小,这是有可能合并的。
发送方的状态:FIN_WAIT1,FIN_WAIT2,TIME_WAIT,CLOSED
接收方的状态:CLOSE_WAIT,LAST_ACK,CLOSED
状态解析:
close_wait: 这个状态就是在四次挥手挥了两次之后出现的状态,这个状态就是等待代码中调用socket.close方法,来进行后续的过程,正常情况下,一个服务器上不应该有大量的close_wait,如果存在,那么就说明代码大概率出了Bug,close没有执行到。
time_wait: 谁主动发起的FIN,谁就进入time_wait,它起的效果就是最后一次ACK提供超时重传机会。
但是我们在表面上看到,如果此时的客户端收到的ACK确认应答报文,那么此时就会客户端就没有关系了,应该销毁连接资源,但是此时并没有释放,而是进入了time_wait 状态等待一段时间之后,再来释放连接,等的这一会就是害怕最后一次发送方给服务器传ACK的时候,怕ACK给传丢了。
如果最后一个ACK传丢了,就意味着服务器过一会还要发一个FIN
如果最后一个ACK丢了,服务器就收不到ACK了,那么此时的服务器就不知道是自己传的FIN传丢了,还是客户端返回的ACK传丢了。那么此时的服务器就干脆重发一个FIN。但是假设此时的这个客户端已经释放连接资源了,那么此时客户端就不会处理这个FIN,那么就没有了ACK,那么此时的四次挥手(断开连接)就失败了。 这样就像是我买了东西,保修期要半年,但是此时的商家却半路给跑了.
那么我们此时的客户端的连接就就不能释放的太早,要等待一段时间,确保服务器不会重传FIN之后,在真正的销毁连接。
那么这个等待时间到底要多长呢?
默认设定是两个MSL其实这个MSL表示的是网络上的任意两点之间传输需要的最大时间,这个时间也是系统上可以配置的参数,一个典型的设置就是60s
2.1.4 滑动窗口
那么我们此时已经在前面说了,已经把客户端和服务器之间的通信链路打通了,那咱们接下来就说说在通信的过程中,怎样TCP通信才更高效!!!
其实这里的滑动窗口就是用来在保证可靠性的前提下,尽量的提升传递数据的效率。
没使用滑动窗口:
在这里我们可以看到由于确认应答机制的存在,导致了当前每次执行一次发送操作,都需要等待上一个ACK的到达,这样就会有大量的时间都花费在等ACK上了。
而我们这里的滑动窗口就是为了解决这一问题,我们能不能给他批量的发送数据呢?答案是可以的。
其实滑动窗口的本质就是批量的发送数据。一次性发送一波数据,然后在一次性等一波ACK。
一次发送4组数据,在发送者4组数据的过程中不进行等待,这4组都发完之后,同一等待ACK.
那么此时我们原本等待一个ACK的时间,我们使用滑动窗口机制之后,等一个ACK的时间,我们就可以等多个ACK,那么此时的效率就得到了提升。TCP是保证可靠性传输的,可靠传输的灵魂就是确认应答,如果没有这个ACK,那么我们的可靠性传输就形同虚设,所以说 等,还是要等滴!!!
如果一次性批量传输的数据为N,发送数据之后,在统一等到一个ACK的时间,这里的N就是我们要说的窗口。
那么至于滑动呢? 就是我们在传输一波数据的时候,然后再等待这波的ACK,我们并不是把这波数据的所有ACK都等到,我们向下执行,而是收到一个ACK,就继续往下发送一组。就例如说此时等待的ACK 分别是 1001 2001 3001 4001 四组ACK,不需要等到4001到了,才继续往下发送数据。
只要1001到了,就可以往下发送一组(4001-5000),如果2001到了,就继续在往下发下一组
当前这个窗口大小越大,可以认为就是传输的速度就会越快,窗口大了,同一份时间内等待的ACK就更多,总的等待ACK的时间就更少了。
那么会不会出现2001还没到,我们这里的3001就到了呢?其实这种情况是会发生的,这就要说到我们从服务器向客户端发送ACK的时候,如果有一组丢包了,那么就是这组的确认序列号没有被传到客户端,那么此时就会发生我们所说的出现了2001没到,3001到了。这也就是发送的ACK丢包了。还有一种就是你发送的数据SYN对了,我们这里想讨论第一中,如果ACK传丢了。
2001表示的是2001之前的数据都已经确认收到了,此时1001之前的数据就在2001的数据包含中,那么此时是否收到这个1001ACK已经无足轻重了。
ACK确认序列号的特定含义就是保证了后一条ACK就能涵盖前一条,因此2001意为这1-2000的数据都收到了,既然如此1-1000根式已经收到了。当发送到收到5001ACK的时候,意为这个1-5000的数据都已经收到了 3001 被丢包,也毫无影响,只要收到了5001就涵盖了3001表达的信息。
那么如果使我们的SYN数据传丢了呢?
当我们的接收方收到了三次同样的确认应答时就进行重传。由于1001-2000这个数据丢了,所以服务器就反复的索要1001这个数据即使客户端已经给服务器往后发送数据了,这个时候任然是索要1001,当索要若干次之后,服务器就知道了,就触发了超时重传
2.1.5 流量控制
实现流量控制是滑动窗口的延伸,目的就是为了保证TCP数据传输的可靠性。我们在前面说过滑动窗口越大,那么此时的传输效率就会越高,但是我们这里不光要考虑发送方的发送能力,还要考虑接收方的接收能力。
如果发送方发送数据贼快,接收方根本处理不过来,那么接收方就会把发来的数据给丢了,那么此时就会产生大量的丢包,那么接下来就会有大量的超时重传。
我们这里的流量控制,就是控制发送方向接收方发送数据的多少,就要处理好接收方的处理速度,此时直接就使用接收方接收缓冲区的剩余空间大小,来衡量当前的处理能力。
我们可以把这个接收缓冲区看做是一个阻塞队列,接收方的应用程序在调用read方法的时候,就会从接收缓冲区中取数据。这样的数据传输过程可以理解成一个生产者消费者模型,发送方是生产者,接收方式消费者,接收方的接收缓冲区就是消费交易场所。
那么此时随着发送方发送数据,那么接收缓冲区的剩余空间大小就会逐渐的变小。如果剩余的空间比较大,就认为接收方的数据处理能力是比较强的,就可以让发送方把数据发快点。如果剩余的空间比较小,就认为接收方的数据处理能力是比较弱的,就可以让发送方发慢点。
这个过程就像是,我们往水箱中注水。
那么如果此时的水位满了,就是此时不能向里面注水了,对标我们的接收缓冲区被添满了,那么我们的发送方是不是就不能向接收方发送数据了呢?
发送方确实是不发送数据了,但是也不是不发,我们的水池满了,不能向里注水,但是此时还在放水呀,满并不是一直满,满一会就不满了
发送方会发送探测报文,虽然接收端返回的是当前的发送缓冲剩余大小时0,当时发送法也不能不完全发送数据。需要定期的发送一个探测报文,这个探测报文中不传输实际的数据,只是为了触发接收方的ACK,只是想知道当前的接收缓冲区的剩余空间有多少。
2.1.6 拥塞控制
拥塞控制是衡量的是发送方到接收方,这整条链路之间的拥堵状况。
我们在发送数据的时候,不光是要经过发送方和接收方,还要经过中间的某些链路。
发送方和接收方中间节点有多少个,这是我们不知道的,其实是很多的,很难对这些准备做出衡量。
说白了,拥塞控制的处理方案就是,一个字 “试” 通过实验的凡是,逐渐的调整发送的速度,找到一个合适的发送数据的速度值。
发送方一开始以一个比较小的窗口来发送数据,如果数据很流畅的就到达了,逐渐增大窗口的大小,如果加到一定程度,出现了丢包,此时丢包就意味着通信链路出现了拥堵,这个时候再减少窗口大小。通过反复的增加和减少窗口大小,在这个过程中,我们就能找到一个合适的范围,那么拥塞窗口就在这个范围不断的变化,达到一个发送数据速度的动态平衡。
那么具体的拥塞窗口是怎样变化的呢?
在最开始的时候,取的初始窗口大小非常小,纵轴是一个单位。
我们最终希望的发送数据的速度快,但是又不希望丢包(那么此时的这个速度,就是临近丢包的那个速度),我们在初始情况下给的窗口大小太小了。可能合适的值是一个更大的值,通过上述的这个过程,就可以更快的接近合适值。
指数增长到一定程度,机会进入线性增长,线性增长也是增长,增长到一定程度,就会产生丢包,一旦丢包,此时发送方立即就让窗口变小(回归初始窗口大小)继续重复刚才的指数增长和线性增长的过程。
但是此时的阈值是不一样的,第二次的阈值就是第一次丢包时候的窗口大小的一半。24 / 2 = 12
直接让窗口回归初始值,一个主要的目的就是,网络情况是复杂的,不稳定的,如果出现了丢包,很可能先把速度降下来,不能解决问题,如果降的太慢,就会出现持续性的丢包,就对网络通信质量带来很大的影响,一下让窗口变得很小,就是希望这次传输,一定能成功。
阈值:决定了什么时候开始有指数型增长变成线性增长,这个阈值也不是一直不变的,每次出现丢包,这个阈值就会更新成当前丢包窗口的一半。
我们希望的理想效果是窗口的大小在阈值和丢包窗口之间。拥堵的值是取决于这次传输是否丢包,丢包了就视为拥堵,然后根据这个值来更新阈值。
2.1.7延时应答
延时应答其实是流量控制的一个延伸
流量控制是踩了一下刹车,让发送方发送数据的时候不要发的太快,害怕接收端处理不过来。
但是我们的宗旨是,在不违背TCP可靠性的前提下,尽量的提升数据传输的效率。
延时应答就是在这个基础上,能够尽量的在让窗口更大一些。
我们要知道注水的时候,同时也在出水,那么就是数据发送的同时,还有数据到达接收端。
我们不是有一个探测报文嘛,这个报文就是想得到当前的数据缓冲区中还有多大的空间。
我们在这里采取的策略就是不立即回答,而是稍微晚一点回答,迟一点回答的目的就是在这个延时的时间里,就会出更多的水。对标我们的发送数据,就是接收方晚一点回答这个探测报文,在这个延时时间里,因为此时接收缓冲区中的数据在往外读,那么把数据已读,数据就不在接收缓冲区了,那么此时的接收缓冲区的剩余空间就大了一点。那么此时传输数据的效率不就也提升了嘛。
2.1.8 捎带应答
这个捎带应答又是延时应答的一个补充
我们知道客户端和服务器之间的通信,有以下几种模型:
一问一答(客户端发送一个请求,然后服务器发送一个响应)
多问一答 上传文件
一问多答 下载文件
多问多答 主播 串流
还记着我们前面说的三次握手和四次挥手吗?
这两个东西时不同的时机触发的(不能合并)
因为延时应答的存在,导致ACK不一定立即返回,如果当前的延时应答,导致ACK的返回时机和应用代码中返回的响应时机重合了就会把这个ACK和这个响应数据合二为一。这就是捎带应答。
2.1.9 粘包问题
这里的粘包问题不仅仅是TCP存在粘包,其他面向字节流的机制也存在,比如读文件
TCP粘包指的是粘的应用层数据报,在TCP接收缓冲区中,若干个应用层的数据报,会在一起了。
这些数据报到达接收方之后,会经过层层的分用,分用意味着就是把TCP数据进行了解析,取出其中的应用层数据放到接收缓冲区中,以备应用程序来取,接收方的应用程序就需要通过read方法来从接收缓冲区中读取数据,因为TCP是面向字节流的,接收端在取的时候取出若干个字节,至于从哪里到哪里是一个完整的应用成数据包呢?
如果没有额外的限制,其实就很难进行区分了,归根到底就是数据之间没有明确的划分边界。
这一个读操作就把接收缓冲区中的数据 就是 aaabbbccc都读到了byte数组中,仍然分不清从哪里到哪里是一个完整的应用成数据报(这9个字节其实就是3个应用层数据报)
TCP报头中没有单个应用层数据包的字节大小
当前的接收缓冲区,可以视为一个分用后的数据(当前已经把TCP报头给去掉了,缓冲区中不到TCP报头)
解决粘包的方法:
首先解决这个问题的关键就是在应用层协议中,在包和包之间加上可以区分的边界
例如约定每个包之间使用;隔开
这个时候,只要能够按照;来进行切分,就可以区分出当前从哪到哪是一个完整的应用层协议的数据
如果(一般具有通用性)你是基于一些库,框架来完成网络通信,一帮来说粘包问题已经被库或者是框架处理了,如果是要自己实现一些库或者是框架,直接使用TCP,就要需要考虑粘包了。
2.1.10 TCP异常
这里的TCP异常分为3种。
1.进程终止
2.机器关机
3.机器掉电网线断开
先说一说线程终止
TCP连接时通过socket来建立的,socket本质上就是进程中打开的文件,文件其实就存在于进程的PCB中里面有一个文件描述符表
每次打开一个文件(包括socket) 都在这个文件描述符表中增加一项
每次关闭一个文件都在文件描述符表中删除一项。
如果直接杀死进程,这个PCB就没了。里面的文件描述符表也就没了,此时的文件相当于自动关闭
这个过程相当于和手动的调用socket.close()是一样的,都要有四次挥手。
机器关机:在这里我所说的机器关机是在自己的电脑上,点击电脑右下角的关机键,进行的关机
按照操作系统约定的正常的流程关机,还是会让操作系统杀死电脑中所有的进程,然后在关机。
机器掉电:那么此时我们就真的偷袭成功了
此时的操作系统根本来不及杀死所有的进程,操作系统不会有任何反应,更不会有任何处理的措施。
如果接接收方断电,就意味着发送方的数据不会在得到ACK了,那么那么发送方迟迟等不来自己要的ACK骂他不知道接收方收到了没有,也是就会进行超时重传。重传几次后,发送方发现这个连接已经出现了严重的问题,尝试重新连接失败之后就会放弃连接,并且发送方主动释放曾经和接收方连接的信息。
如果此时是发送方断电,那么此时的接收方就不会收到SYN 此时的接收方不知道当前的发送方是挂了,还是在休息,那么此时的接收方就会想发送方发送一个小的探测报文,为了得到发送方返回的ACK。
通过这个探测报文,发现发送方不会返回ACK,因此接收方就认为发送方出现了问题。
2.1.11 TCP vs UDP
那么什么时候使用TCP 什么时候使用UDP呢?
使用TCP :如果对可靠性有一定的要求,在日常开发中的大多数情况下都基于TCP的
使用UDP:如果对可靠性要求不是很高,对于效率要求更高,那么就使用UDP
此时还有一个经典的面试题:基于UDP如何实现一个可靠性传输,其实这个就是考你对TCP的理解 我们可以在应用层中基于UDP复制TCP 把TCP的功能加到UDP中。