本文的题目为什么只说TCP,为什么不包含UDP,因为在传输层当中,TCP的使用程度,复杂程度,相关的知识点是占大头的!!!
应用层
因为对于TCP/IP五层协议中,我们主要接触到的就是应用层以及传输层,所以我们先简单了解一下应用层相关的
我们在平时编写应用层代码的时候,主要是要自己自定义协议,前后端来确认传输的数据信息以及传输的数据格式,就好比消息传输的程序:我们就约定客户端要这样发送:用户名;消息(张三;你好)以包含这样的数据以及数据之间分号的格式进行传输,所以到了服务器这端,我们就知道了前面是用户的姓名,分号后面的是要发送的消息
传输层
UDP
UDP的协议格式

在传输层当中,我们主要是加上了16位源端口和目的端口,这里的16位表示的数字是0-65535,所以你的端口号也是在这之间进行选择,不过0-1023已经被提前选掉了,我们一般是从这之后进行选择
(窗口大小字段里的数字默认单位是bit,1byte=8bit,1024byte=1kb)
16位长度:16位可以表示0-65535,这里的单位是byte,所以可以表示0-64kb的数据,我们UDP首部固定有8个字节,所以根据16位长度就可以知道数据的大小
16位校验和:这里面有很多函数,主要是为了校验数据有没有正确的传输过去,在传输过去之前会根据函数计算一个数值(也就是校验和),在接收方收到数据后也会根据函数计算一个数值,如果两个数据一对比,发现一样了,那大概率是传输成功了
TCP

接下来我们主要引入TCP的一系列协议来对这个图进行一个理解
我们先介绍一下我们已知的
(窗口大小字段里的数字默认单位是bit,1byte=8bit,1024byte=1kb)
16位源/目的端口号:这个还是和UDP上面讲的一样,也是一个0-65535的数字选择,并且我们一般在1024-65535中进行选择
4位首部长度:4位的话表达的范围是0-15,单位依旧是字节,但是我们看到下面表识的是TCP的首部最小长度的20嘛,那好像就不对劲了,所以我们这边的单位其实是4位,也就是每一个数字都可以存4个字节,就是0-15这里面的所有数字,每一个数都是表示4个字节的,所以就是0-60字节,所以首部长度就是20-60
保留:这里保留了6位,也就是多给了6位的空间,用来方便以后加一些新功能
16位校验和:这里和UDP相同
接下来来介绍TCP的性质,也会逐渐介绍出上图的剩下内容
确认应答
确认应答这个机制是保证数据实现可靠传输的关键之一,应答也就是ACK,就是上图标志位的第二个,如果这个位置是1的话就代表他拥有应答的功能,就像下面这张图一样,客户端发送0-1000的数据,服务器就返回一个ack,来告知对方我收到了

后发先至
但是接下来就出现问题了,就是网络中可能会出现后发先至的情况,也就是你后面发出的问题,对方先一步接收到,然后给你回复,这时候你就会拿着后面的回答去对应前面的问题,这样显然是不合理的,那为什么会出现这样的问题呢:就是数据在网络传输过程中走的路线是不一样的,因为网络中的传输路线是很多的,并且每个数据等待的时间也是不一样的,也就是路由器中排队的时间不一样,当某一时刻数据传输的速度大于路由器接收的速度了,那么就会造成拥堵,我们一个数据从你的电脑到目标的电脑,会经过很多的路由器,所以路线的可能性会很多很多,所以结果也是很可能会有后发先至的,如下有一个数据经过路由器的简图

所以为了应对这个问题,我们就会讲到上图中的32位序号以及32位确认序号
为了让数据可以有正确的顺序,我们会对数据进行一个排序,又因为TCP是面向字节流的,所以我们没有一条数据,第二条数据重要的概念,所以我们是对字节流进行编号,并且是对第一个进行编号,后续的数据依次递增,也就是说,我们对第一条数据编号为0,那么后面的依次递增,也就可以表示比如我们这次传输的是从0-1000的数据了,那么确认序号又是什么,确认序号是服务器传给客户端的一个数字,就如下图,我传入0-1000的数据,服务器返回一个1001,这1001有两层的含义,第一层是告知对方我收到了0-1000的数据,还有一层是告诉对方我接下来期待1001开始的数据
这个排序的操作是在服务器的缓冲区进行的,那缓冲区还有哪些作用呢:1,暂存已经收到的但还没有读到的数据 2,暂存后发先至的数据 3,根据序号判断哪些数据已经收到 4,识别重复的数据 5,配合TCP的接收窗口,告诉对方我这里还剩多少缓冲区空间
确认应答的ack和确认序号是两个东西,不过这两个往往一起发送


超时重传
前面也说了,数据在网络里的路线是很复杂的,所以会出现后发先至的情况,那也会出现说数据丢了的情况,我们俗称丢包
当我们客户端发送一个数据但是迟迟没有收到服务器的ack应答的时候,可能是因为什么的数据丢了,或者是因为服务器发送的ack丢了,这两种都是有可能的,所以我们会等待一定的时间,对数据进行一个重新的传输,因为我们是无法判断是哪一种包丢了,所以如果数据没丢,我们再传一份数据不是服务器那边就堆积了两份吗,不是的,我们刚刚不是讲到一个服务器的缓冲区嘛,这里面还有一个很重要的功能是检查重复的数据,我们传过去的数据都是有编号的,再次传输过去,就会在缓冲区里面先检查一下是否已经有了这组数据,如果有了就会进行去重操作,重传的等待间隔是不固定的,并且一次会比一次长,并且我们重传的次数也是有上限的,当我们重传到一定次数之后,我们就会认为对方是网络故障,机会断开对方的TCP连接
确认应答以及超时重传是TCP中保证数据可靠性的两个核心机制!!!!
连接管理(面试重点)
我们这里的操作俗称三次握手,是操作系统内部进行的

我们建立连接的过程就相当于打招呼,我们会传输一个不带数据的一个包,来起到打招呼的一个效果,第一次syn:我要和你服务器建立连接,我要保存你的信息,第二次syn:我要和你客户端进行连接,我要保存你的心里,通过这两个操作之后,才是正式的建立了连接,因为我们服务器返回的ack以及syn可以算是同步的,中间没有其他的业务,所以称为三次操作,当上面标志位中的SYN为1的时候,就是这里的同步报文,这里面的标志位并不是绝对的只有一个是1,就这里的服务器返回的ack和syn一起,那么这两个就会同时是1
三次握手可以初步检查传输链路是否通畅,也可以检验一下上方的发送以及接收能力
三次握手还有协商的能力,他们会协商好我们上面说的32位序号,但是这里的协商并不是协商一个共同的编号出来,而是双方自己选定一个序号并且告知对方,比如客户端这边决定一个1000,服务器那边决定一个6000
并且每一次连接,起始的序号都会差距很大,不容易重复,这样就是防止久数据的混淆,网络传输不仅会丢包,甚至可能出现我数据转了很大一圈才到达服务器,那如果刚刚好我这个时候服务器已经断开连接了,那原来绕一大圈的数据过来就会造成混淆,比如我原来的数据是10000,并且我第二次连接后的起始值是0,那么就会在我陆续传输到10000这个数据的时候,就会分不清我第一次传的和我第二次传的,因为他们都叫10000,所以我们每一次连接的起始值都会差别很大,比如一次是23420,一次是100200,
断开连接
我们俗称四次挥手

这里和三次握手差不多,也是发一个没有载荷的头给对方,并且服务器和客户端都有可能会先发起断开连接,并且这里是不可以ack和fin合并的,这里和上面的三次握手不一样,因为三次握手的ack以及syn是告诉客户端我知道你要和我连接并且我也同意连接了,这是可以一起进行的,但是这里先发送的fin是为了告诉对面,我没有数据要发了,我这边可以断开连接了,但是由于TCP的全双工特性,服务器也是可以发送数据给对方的,所以在我服务器发送ack告诉对面我知道你可以断开连接之后,还会有可能自己的数据还要发,所以这里的fin和ack一般都不会同时进行,不过也有可能变成三次,这和下面讲的延时应答有关系
状态理解

这是一个TCP整体的建立连接,断开连接以及数据传输的过程,里面也包含了很多的状态,我们来观察一下,我们来简单聊一下一些不容易看出来的状态,LISTEN:这个是服务器启动,并且关联好端口号之后会显示的,这个时候就随时可以和客户端连接上了,ESTABLISHED:这个代表连接已经建立,接下来就可以发送数据了,CLOSE_WAIT:这个是收到断开连接一方的人会陷入的等待,这个时候确实就是在等待自己调用close方法,如果你代码中一直没有调用close,就可能出现好多个CLOSE_WAIT,TIME_WAIT:这个等待就是为了等待最后一个ack,也就是最后一个ack也可能丢包,所以会等待一会,等待后续没有FIN重传了,那就可以安心的释放TCP了
滑动窗口

我们确认应答机制每次都要发送一个数据,就要等待一会ack,那么这样效率就会很低,TCP为了缓解这个效率问题,有了滑动窗口这样一个机制,类似下面这个简图

这样我们就可以一次性发送多组数据,完整过程就是,假设我们先一次性发送0-4000的数据,然后ack比如返回3001,那么我们不用前面1001以及2001的ack我们就知道对方已经收到0-3000的数据了,这个时候,我们的窗口就可以往后滑动,就滑动到3000的位置,然后接着向后发送数据,我们滑动窗口将数据分为了四个区域,如上图,1000就是已经发送并且收到ack的数据,2000,3000可能是发送了,但是还没收到ack的,4000,5000就是准备可以发送的数据,而6000,7000就是窗口外还没轮到的数据,那如果我一部分数据丢包了呢
就像我们这样的情况,1001-2000的数据并没有传到服务器,那么服务器就会多次返回一个2001的ack,那客户端收到多次同样为2001的ack时候,肯定就会想到是这部分数据丢了,那么客户端就会出现传一遍1001-2000的数据,这个重新传输也称为快速重传,当我们补齐这部分数据之后,服务器就会正常返回5001的ack了
那我们可能会思考超时重传以及快速重传会不会有冲突:当然是不会的,这两个是分工明确的重传,当我们数据发送很慢的时候,我们后续不会快速并且多次的发送数据的时候,也就不会多次收到重复的ack来启动快速重传,这个时候我们就会开启一个计时器,等待超过时间就会启动超时重传,相反的,快速发送多组数据并且丢了就会出发多次重复的ack,这个时候就是快速重传
流量控制
我们为了提高效率才有了滑动窗口,那窗口越大肯定效率越高,但是能一直增大吗,答案肯定是不可以的,因为我们数据到达服务器之前会进入服务器的缓冲区,但是缓冲区的大小是一定的,但是一旦缓冲区的大小满了之后,新的数据来了就会被丢弃,所以流量控制就是为了告诉对方我还有多少缓冲区的大小,这个是通过上面的16位窗口大小来表示的,这个数据就是搞清楚缓冲区还剩多少空间,然后通过ack一起告诉对方,【我们ack,确认应答号(32位确认序号)以及这里的16位窗口大小是不同的东西】如果我们的缓冲区可能很大,可能有20多mb,但是我们16位最多只能表示0-65535,也就是64kb的大小,无法真实或者更加准确的告诉对方我还剩多少空间,那这个时候TCP选项里面有一个机制,就是窗口扩展因子,这个扩大是通过让16位左移多少位来扩大空间
当我们缓冲区没有空间之后,对方就会停止发送数据了,那么问题来了,发送数据的一方怎么知道对方还有没有空间了呢,不是已经无法发送数据了,那也就没有TCP报文返回了,这个时候TCP就很聪明了,它有一个窗口探测包,没有载荷,只是为了触发ack,得到最新的大小数据
拥塞控制
我们刚刚讲到滑动窗口因为接收方的处理能力,我们要进行一个流量控制,那我们不仅需要考虑接收方的能力,还需要考虑到你传输过程的传输能力
我们中间会讲过许多的部件,路由器啊,交换机之类的,这里面就拿路由器举例子,如果我们传输的速度大于其中一个路由器的接收速度,那么就会造成堵塞,因为我们中间的路线很复杂,并且基本上每次走的都是不同的路线,所以我们将中间的线路统一理解为一个整体,我们就得去试探中间路线的处理能力,怎么去试呢,我就先按慢一点的速度去传输数据,如果没有丢包,就加大速度,如果丢包了,就减速,这也是控制窗口大小的一个重要指标,我们窗口大小不是固定的,而是动态的,我们通过流量控制来确定接收方的处理能力,通过拥塞控制来确定中间路线的处理能力,并且我们这两个指标会同时去考核,并且拿到其中较小的值来作为窗口的大小
拥塞窗口的变化规律(曲线)

这是一张拥塞窗口的变化图,我们首先是慢启动,做一个试探先,然后进行指数级的增长,当达到我们设定的阈值之后,我们会进行线性的增长,直到我们快的丢包了,接下来有两种方案,一种是回到慢增长的时候,然后进行指数级的增长,然后到达阈值之后再次线性增长,还有一种方案是我们的起点是丢包的地方/2,然后从那个地方开始线性增长
延时+捎带应答
这两个机制可以放在一块去说,延时就是当我们发送数据之后,我们会等一会再给出ack,这种情况有哪些好处呢,首先可以减少我们ack的发送量,可以增加效率,就好比我们滑动窗口发送0-4000的数据,我们不需要每次都从1001,2001的ack传起,我们可以延时,然后传回3001的ack,这样不仅ack少了,并且也可以让发送方知道对方收到了0-3000的数据,还有就是上面讲到的服务器的缓冲器的空间,我们可以等一会返回剩余量,等处理一些数据之后,返回的ack并且告知一个更大的空间,这样可以一次性发更多的数据,也可以提高效率
至于捎带应答呢,往往是和延时应答结合一起的,就好比我们服务器需要返回的数据很快就能处理好,并且我们ack也延时了一会,这个时候,返回的数据以及ack就可以一起发送回去,上面讲到的四次挥手为什么也可以减少为三次,就是因为如果我们服务器最后的程序比较快,这样就可以和延时的ack一起返回了
面向字节流-粘包
我们讲到过TCP是面向字节流的,如果我们第一次发送aaa,第二次发送bbb,第三次发送ccc,这样TCP在读取的时候,他想怎么读就怎么读,这样我们就没办法保证我们读取出来的是一个完整的应用层数据包了,这三份数据会粘在一起,就可能读到aa,或者aaab这样的情况,在UDP里面,它是面向数据报的,就一份一份的通过socketpacket读,就不会有这样的烦恼,所以我们会怎么做呢,第一种:我们在一份数据的结尾添加一个特殊的分割符,来区分每一份数据,第二种:就是在我们的数据头部添加一个属性,比如约定前两个字符表示数据长度,这样也可以很好的区分开。不过我们平时在开发中很少会遇到这样的问题,因为我们一般是使用现成的框架,在框架里面一般已经把粘包问题解决了
异常情况
(1)进程崩溃
进程崩溃和四次握手完全相同,当我们向服务器发起退出的时候,可能我们的客户端在服务器刚返回ack的时候就进程崩溃了,这个时候我们的操作系统任然会持有这个TCP的连接,所以还是会跟四次握手一样读取对方的fin以及返回ack,操作系统会帮助我们释放掉资源,就相当于是调用了close方法
(2)正常情况主机关机
就像这样的情况,再我们发完fin之后,我们的主机就进行关机了,所以服务器再等待我们的ack的时候,就等不到,服务器没法确定是什么情况,所以会进行重传,当重传达到一定的次数之后,我们服务器就会单方面的断开连接,也就是删除对方的信息,释放掉一些资源,就算对方没有断开并且删除信息,起码服务器自己删除掉了,也是断开了连接,这个情况下,我们的客户端的TCP其实还保存着这个连接状态,所以服务器会尽可能的将四次握手走完
(3)断电情况下主机关机
3.1接收方挂了
当我们发送方发送数据之后,接收方因为掉电无法回复ack,这个时候,发送方就会还是一样的进行超时重传,当到达一定的次数之后,发送方知道对方已经不会传来ack了,就会主动断开连接
【不过如果我发送方还保持着久连接,还没断开,然后我们接收方断电重启了,但是旧连接的信息已经丢失了,这个时候发送方要求重新连接,但是接收方压根不知道对方信息了已经,所以这个时候,接收方就会发送一个复位报文也就是上面标志位中的RST】
3.2发送方挂了
那如果是发送方挂了,再接收方眼里就是发送方的一端突然沉默了,不知道对方是挂了,还是要休息一会,所以我们接收方会发送出去一个周期性的心跳包,看看对面还或者没有,如果真的挂了,接受方就会解除连接(这个也是看这条连接是否长时间空闲,还是说一直平凡交互来决定心跳包的效果,这里主要是为了引出心跳包这个概念)
(4)网线断了
当我们网线断了的时候,我们依旧会给对方重传数据,看看到底是什么情况,并且心跳包也会周期性的传输,但是当我们数据很多的时候,可能心跳包的作用就不那么明显,就会被我们的重传效果掩盖,当我们数据基本上没有的时候,心跳包的效果会更明显一点,在当我们已经知道对方无法连接上的情况下,我们要不是告诉应用层说连接断开,要不然就是使用RST强制断开连接
主动断开(FIN)= 正常结束连接;RST = 立刻强制废掉连接
到底走 FIN 正常断开,还是发 RST 强制断开,主要是由“应用程序的关闭方式 + 操作系统 TCP 协议栈当前看到的连接状态”共同决定的。
剩余部分
关于TCP的协议格式我们差不多都讲的差不多了,还剩一些我们可以了解一下,标志中的URG,这个表示紧急指针有效,这个配合那个16位紧急指针来使用,因为我们一般读数据不是顺序读下去嘛,但是如果我们需要一些数据先读,就是有插队的情况,就需要用到这个,标志位中的PSH,这个是一个催促标志位,就是希望接收方尽快返回数据,不过这个一般没什么用,我们一般是是看应用程序的代码中是怎么写的
转载自 CSDN-专业IT技术社区
原文链接:https://blog.csdn.net/java_nnnn/article/details/164756156




