我们的tcp协议相比于udp协议复杂不少,今天我们就来一起学习一下tcp协议报文和原理
首先我们报头第一行里的端口号和udp的端口号是一致的,都是用两个字节来表示。
32位序号和32位确认序号: 在这里先不给大家解释,等会我们再将tcp确认应答原理时会给大家讲到。
4位首部长度: 正是因为这个首部长度存在,我们的tcp的报头长度是可变的,而我们的udp报头长度是8个字节固定死的,首先我们需要明白,我们选项之前的长度是固定的20字节,而我们首部长度可以调节的就是选项长度,我们首部长度的单位是4字节,选项长度等于首部长度 - 20字节,如果我们的首部长度是6,那么tcp报头就是24字节(选项相当于是4字节)。
**保留(6位): ** 我们这里的保留六位是为了以后扩展考虑的,因为我们的网络协议是一件成本较高的事情,就拿udp来说,报文长度是2字节,因此一个包最大是64kb,如果我们想让它兼容更大一些的长度,理论可行但是实际操作极高,但是如果我们引入了保留字,我们后面tcp引入了新功能就可以使用这些保留字,不仅对报头结构影响极小,而且老设备也不需要升级也可兼容。
这个我们在后面讲原理的时候会介绍到。
16位窗口大小,16位紧急指针: 这些我们在后面会介绍到,16位窗口和我们的滑动窗口和流量控制原理有关。
16位校验和: 和我们的udp校验原理相同,crc,md5,sha1
选项: 选项(option),这个是对tcp一些功能扩展和tcp一些属性进行解释说明的。
tcp报文 = tcp报头 tcp载荷(payload),载荷也就是我们的数据。
我们的tcp相比于udp是一个比较复杂的协议,也包含很多机制,我们主要学习10大核心机制
1.确认应答
tcp的特点中有可靠传输的特性,确认应答就是实现可靠传输的核心机制。大家还需要明白,我们这里的可靠指的不是我们发送方百分百能将数据发送到接收方,而是尽可能的发过去即使没有发过去我们的发送方也能够知道。
比如我们有这样一个场景,一个宿舍中的a同学和b同学要通信。
我们这里的不上就称之为"应答报文",也叫ack(acknowledge).
当a同学收到不上的时候,就可以知道我们上号这个消息b同学已经看到了(证明没有丢包),如果我们一段时间后没有收到ack证明我们的消息大概率不见了(也就是丢包)。
tcp可靠传输主要就是靠这个确认应答机制来保证的,我们a给b发消息,b收到之后就会返回一个ack,a收到这个应答之后,就证明我们的数据顺利到达了(没有丢包)。
上述的通信是比较简单的单一通信,我们来看一个稍微复杂一点的。
同学a可能会连续发多条消息,发第二条消息时,不需要等待第一个消息的应答,我们的b同学收到消息立马回复,我们在网络通信时,很有可能发生后发先至,因为我们的网络环境是错综复杂的,很有可能发的晚的消息先到的情况。
上面的情况是正常的情况,那么我们来看一个不正常的情况。
因为这种后发先至的情况,我先收到了上,后收到了不上,这样我们的应答错乱后表示的含义就截然不同了。
首先网络上后发先至这种情况是客观存在无法避免的,我们应该考虑的是如何应对这种情况所带来的异义。
我们采取的办法是给传输的数据和应答报文编号。
当我们引入序号后,即使出现先发后至的情况,消息的顺序即使乱了,我们通过序号也可以明确将每条应答报文与其对应了。
我们任何一条数据(包括应答报文)都是有序号的,确认序号只有应答报文有,是否为应答报文取决于ack这个标志位是否为1,如果为1证明是应答报文,如果是0表示不是应答报文。
我们实际上的tcp序号并不是简单的1,2这种方式编号,我们tcp是面向字节流的,我们的tcp序号也是按照字节来编号的。
假设我们的一条数据长度是1000个字节,我们的数据是由1开始的,此时我们第一个字节编号就是1,第二个字节编号就是2依次,但是我们前1000个字节都是属于同一tcp报文,因此我们tcp报头里记录的序号就是第一个字节的序号,也就是1。
当我们发送第二条数据,序号就是1001了,我们的tcp字节序号是依次增加的,起始字节序号是上一个数据的最后一个字节,我们tcp报头只需写tcp头一个字节序号即可,在根据tcp报文长度即可知道tcp的每一个字节的序号。
我们应答报文里的确认序号中的1001就是刚才尾字节1000基础 1的结果,1001所表示的含义有两个:
1.小于1001的数据都已经确认收到了
2.发送方接下来应该从1001这个序号开始发送数据
结论: tcp可靠传输最主要是通过确认应答机制保证的,我们不仅可以让发送方清楚的知道是否传输成功,并且通过序号和确认序号对多组数据进行了详细的区分。
2.超时重传
我们刚才在讨论确认应答的时候,只是在一个理想的传输场景下讨论的,那如果在传输过程丢包了呢?
首先丢包有两种情况: 1. 发送的数据丢了 2.应答报文ack丢了,站在我峨嵋你发送方的角度,这两种情况都认为是丢包了,正在因为这种丢包的可能性发送,我们tcp就引入了重传机制,我们在丢包的时候就要重新发一遍相同的数据,那么如何判断是丢包还是ack正在返回路上?
我们tcp直接引入一个时间阈值,在发送数据后,就会开始倒计时等待ack,如果时间超过时间阈值也没收到ack,统一认为丢包。
超时重传: 接收方在一定时间没有响应,发送方就重新发送一份相同的数据。
时间阈值: 超时时间具体是多少,这个我们不具体讨论是多少,根据场景业务而定,是可配置的。
这种情况比较形象,发送方发送的数据丢包了,在一定的时间后重新传输一次,然后收到ack之后就视为一次成功的传输。
但是当ack丢包时,对于我们主机b而言,1 -1000的数据接收了两次,这种情况就比较麻烦了,但是我们tcp对于重复的数据传输,具有去重功能,我们tcp存在一个发送缓冲区和接收缓冲区接收缓冲区相当于是接收方操作系统内核的一段内存,我们在网络编程的时候学习的socket的对象中都有一个接收缓冲区,我们主机b的网卡接收到数据之后,将数据放到主机soekct对象对应的接收缓冲区中,我们可以将这个缓冲区想象成一个优先级阻塞队列,我们根据序号可以将数据进行去重排序,如果有相同的数据就将后到的数据丢弃,排序就可以应对我们的后发先至情况。
结论: 由于我们去重与排序机制的存在,发送方只要发现在一定时间阈值内ack没有到达,就会重新发一份数据,数据即使重复乱序,我们的接收方都可以处理,去重和排序机制都依赖tcp报头的序号。
相信有一些善于思考同学又会问了,要是我们重传的数据有丢包了呢?
首先我们丢包是一件概率比较低的事件,连续重传丢包的概率更低,因此我们在重传到一定次数后,就不会再死磕,就会认为网络出现故障,tcp就会尝试重置连接,如果重置还是无效,就会彻底断开连接。
tcp的可靠传输: tcp的可靠传输通过 确认应答 和 超时重传机制保证的,两者相互配合,共同保证了tcp的可靠性。
3.连接管理
连接在不同的场景表示不同的含义,在我们tcp协议这里,表示的是维护一些信息(四元组)的空间。
我们tcp建立连接并不是指通过一根线将两者连接起来,而是将这两部分信息维护好,简单来说a要能通过这部分信息找到b,b能通过这部分信息找到b,我们称保存好这部分信息的空间为连接,删除这部分信息,称之为断开。
三次握手
我们仍然拿出a同学和b同学,刚上大学a同学和b同学想通过游戏加深一下感情,但是又不知道对方玩什么游戏,于是有了以下的场景:
首先a向b发出了询问,然后b回应了a,此时双方都知道了b玩王者农药,但想要一起组队玩王者农药,b需要明确a是否也玩,于是b又发送了一次请求,这时候a知道了b玩,b知道了a也玩,这个时候他们才能一起上号打游戏,相当于建立连接成功。
我们把上述过程的每次通信形象的称为一次挥手,但实际上我们有两次是可以合并成一次的。
我们所谓的三次握手的本质上是四次交互,双方各自需要发送一个建立的请求,然后收到一个ack,实际上是有四次的信息交互,但是中间两次是可以合并的,因此就构成了三次握手。
如果不合并,可以吗?
不可以,如果我们从三次变成四次就会多封装分用一次,成本更高,就好比宿舍倒垃圾,本来可以一个大袋子一次性倒,一个是用两个小袋子分两次倒。
两次握手可以完成建立连接吗?
如果少了最后一次握手,站在a的角度,他已经知道了b玩王者农药,但是站在b的角度,他不知道a玩不玩王者农药。
有的同学可能会说,a能问你a肯定玩呀,虽然这个情况也有可能,但是tcp这里不适用,因为三次握手还有一个重要作用:验证通信双方各自的发送和接收能力是否正常
比如我们两个人在打王者农药,组队开麦,a和b能够顺利交流,需要有两个保证,就是a和b的麦和音响都是好的。
第一次通信,当b听到"你可以听到吗?" b知道了 a的麦是正常的,b的音响是正常的。 a什么都不知道。
第二次通信,当a听到"可以,你呢", b知道了a的麦是正常的,b的音响是正常的。a知道了a的麦和音响以及b的麦和音响都是正常的。
第二次通信之后,a以及知道了双方都满足了条件,但b知道的信息还不全,需要进行第三次通信。
第三次通信,当b听到"可以"时,证明我们a和b都知道了自己的麦和音响和对方的麦和音响是正常的,可以正常交流了。
三次握手的作用:
1.让通信双方建立相互的认可
2.验证双方的发送与接受能力是否正常
3.在握手的过程,双方协商一些参数。
我们客户端主动给服务器发送的建立连接请求称为syn同步报文段.
这些都是tcp的状态,不同的tcp状态主要体现我们的tcp在干什么,三次握手中我们主要学习两个重要状态:
1.listen服务器状态:表示ag真人游戏的服务器以及转杯就绪,随时可以与客户端建立连接。
2.established::这个状态我们客户端服务器都有,当我们进行两次握手后,我们的客户端就已经认为进行成功建立连接,于是进入了established状态,ag真人游戏的服务器只有当三次握手进行完毕后才认为成功的建立了连接,进入了established状态,当我们成功建立连接之后就可以进行通信了。
上述描述的三次握手一次性记住不太容易,大家需要记住主要的流程:
四次挥手
握手和挥手都是形象的叫法,指的是客户端服务器之间的交互,四次挥手指的是通信双方给对方发一个断开连接的请求,在各自给对方一个回应。
这里我们同样是a同学和b同学打了一下午王者农药输了一下午:
这里需要注意,我们在断开连接的过程中,中间两次通常是不能合并的。
我们三次握手之所以中间两次可以合并,是因为它两是属于同一时机的,具体来说,三次握手的三次交互过程是系统内核完成的,服务器内核收到了syn之后会立即发送ack也会立即发送syn。
这就好比于我们在同一家店铺买东西,我们的三次握手中间的两次就相当于是同一时间段在一家店铺买的两样东西,所以可以通过一个包裹发送这两样东西,而我们四次挥手中间两次,则是在不同的天数买的,比如2月9号买了个牙刷,2月11买了个牙膏,则不能够通过一个包裹,必须通过两个包裹发送。
我们的fin发起不是有内核控制的,而是我们客户端调用socket的close方法(进程退出)才会触发fin,我们服务器的ack是有内核控制的,收到fin之后立即返回ack,而我们服务器的fin是我们服务器执行到socket的close方法(或进程退出)才会触发fin。
因此大家注意到,我们服务器ack和fin之间只有一个时间差的,而这个时间差的大小由我们的代码所决定,我们可以发开我们之前写的tcp客户端服务器回显服务器看一看。
我们可以看到这里的break决定着循环结束,而这里的break能够执行到取决于我们的hasnext为false,因就是流对象读到了eof(文件结束标记),这里能读到eof是因为内核收到了客户端发送来的fin数据报,虽然我们的客户端程序没有显示的写close方法,但是当我们客户端进行退出的时候也会执行socket close会触发fin。
当我们的循环执行结束之后,会执行到finally里的close方法,相当于我们服务器给客户端发送的fin,我们上述代码里循环结束后立即fin,ack与fin之间的时间间隔相对就比较短,也就有可能包裹成一个,但是如果时间间隔比较长就不可以了,比如这样:
我们的代码在close之前进行了一系列操作,这样我们的fin和ack时间间隔就比较久了,也就无法合并成一个了。
同样的我们断开连接中的fin在报头中也有标志,fin为1证明是断开连接的请求报文。
我们tcp四次挥手当中也有许多状态,我们这里重点学习两个tcp状态:
1.close_wait: 出现在被动连接的一方,等待关闭(等待socket调用close方法),大家需要注意的是,建立连接的过程中一定是客户端主动发起请求,断开连接可能是客户端也可能是服务器。
time_wait: 出现在主动发起断开请求的一方法,这里我们客户端是主动断开的一方,当客户端进行time_wait状态时,客户端认为四次挥手已经挥完了,这里的time_wait要保持tcp状态保持一会不要立即释放。
为什么我们的time_wait要等待一会不要立即释放连接?
因为我们最后一个ack虽然已经发出去了,但是仅仅是发出去了,到没到我们得打一个问号?万一这个ack丢包了呢,我们time_wait会在发完ack之后等,如果没有接收到重传的fin,就认为最后一个ack没有丢包然后释放连接。
站在我们服务器角度来说,当我们ack发送了之后,进入time_wait状态时,相当于是四次挥手已经完成了一样,没有客户端的活了,虽然看起来完了,但是是建立在一切顺利的前提下,如果出现了丢包等情况,客户端就没有完成工作,我们的time_wait就是这些工作但的保证。
那么time_wait保持多久,才真正释放呢?
我们这里等待的时间为2msl ,如果time_wait维持了2msl都没用收到重传的fin,就认为我们的ack顺利到达了。
那么msl指的又是什么呢?
指的是,互联网上两个结点之间,消耗传输时间的最大时间。至于这个msl通常大概是60s,在这里大家需要注意,我们这里的msl无论如何定义都不可避免一些特殊情况,因此我们的msl相当于是一个经验值,绝大多数情况下数据包传输时间都不会超过msl。