JavaEE初阶:网络原理之TCP/IP协议(一)

在前面的网络初识这一章,我们学习到了关于TCP/IP五层协议模型

在这片文章中,我将带大家具体学习TCP/IP五层协议模型的相关的知识.

一.应用层

我们之前编写完了基本的java socket ,要知道,我们之前所写的所有代码都在应用层,都是为了

完成某项业务,如翻译等。

应用层,是程序员最重要的一层,也是我们最关心的一层.下面四层,在系统内部已经实现好了的.

但是应用层是程序员自己写的.

在应用层中,程序员需要定义好数据的传输格式,调用传输层api(socket api)进行真正的网络通信.

什么叫做"定义好数据的传输格式"?

也就是相当于"自定义应用层协议",在工作中,更多的称为"约定好前后端交互接口"

例如通过QQ发送一个hello给对方

我们需要有发送者的id,接收者的id,时间,正文,对于这一块的格式和内容,都是由程序员自己定义的

可能是"1234;5678;2006-3-9 15:00 ;"你好"",也可能是别的一些格式,总之这些都是可以自己定义

自定义协议的过程,主要就是约定两件事:

1.通信的信息是啥

2.通信的数据格式是啥

通信的信息是啥???这个是根据需求来确定的

就好比我们在做外卖软件的时候,外卖程序的主页,就需要显示你附近的商家信息

当我们选定某个商家的时候,点击这个商家的图片,我们就可以进入到商家主页,然后进行更详细的信息介绍,此时的请求和响应又不用了

至于通信的数据格式是啥?这个是程序员自行定义

(1)行文本方式

什么是行文本方式???

举个例子,就是刚刚上面那种"1234;5678;2006-3-9 15:00 ;hello"

这种就是行文本,可读性很差,现在已经不怎么流行使用这种方式了

(2)XML

通过成对的标签进行内容解释说明

例如:

<request>

<from>1234</from>

<to>5678</to>

<time>2006-3-9 15:00</time>

<message>hello</message>

</request>

这种方式现在也不怎么多见了

XML现在更多是作为本地的配置文件进行使用(maven)

优点:可读性好了很多

缺点:引入了很多标签,需要占用更多的网络带宽

对于服务器硬件来说,CPU,内存,硬盘都没有网络带宽贵

(3)JSON

JSON是当前最流行的一种方式

{

"from":1234,

"to":5678,

"time":2006-3-9 15:00,

"message":hello

}

这种方式适用于不是那么缺网络带宽的情况,通过,:{}来分割

(4)google protobuffer

二进制的压缩方案,把数据按照一定的规则编码成二进制比特位,然后进行传输

可读性非常差,但是网络带宽消耗最小

除此之外,我们还可以使用一些现成的应用层协议,如HTTPS/HTTP协议,这一块是重点,会在后面章节详细介绍

二.传输层

我们在之前学过的UDP/TCP协议,就是传输层协议

虽然我们不需要再传输层实现代码,但是我们需要调用传输层的API

1.端口号

在我们学习网络编程那块,我们已经对端口号有了一定的了解

端口号标识了主机上进行通信的不同的应用程序

在TCP/IP协议中,我们使用"源IP","源端口","目的IP","目的端口","协议号"这样的一个五元组来标识一个通信

端口号划分:

0-1023:知名端口号,HTTP,FTP,SSH等这些广为使用的应用层协议,他们的端口号都是固定的

1024-65535:操作系统动态分配的端口号,客户端程序的端口号,就是由操作系统从这个范围分配

知名端口号:我们在自己写程序使用端口号的时候,要避开这些知名端口号

这里有两个问题可以思考一下:

学习网络协议,主要就是学习协议格式,接下来让我们了解UDP和TCP协议的格式

2.UDP协议格式

更加形象一点,UDP协议格式应该是这样:

源端口和目的端口不用过多解释,就是表示当前UDP数据报来自哪个端口和发往哪个端口

端口号占2个字节,一个字节8个比特位,所以端口号的范围是0-65535

UDP长度为2字节,表示数据的范围是0-65535,也就是64KB

这表示一个UDP数据报的最大长度就是64KB

如果需要使用UDP协议传输比较大的数据就可能装不下.


至于为什么不对UDP协议进行升级呢?

比如将2个字节改为4个字节,这个范围一下子扩大多少倍了

这是因为,升级之后,必须要通信双方的设备都一起进行升级,只有当双方的协议格式一样的时候,才能正确解析数据.

一但升级,就一定会出现一方是新版本,一方是旧版本,此时就会出现部分设备无法正常通讯,造成的后果就无法估量.

同时,协议标准很好升级,但是协议的具体实现,实在各种操作系统的厂商手里的,需要这些厂商同样配合你升级.

所以从短期来看,UDP协议的升级是难以实现的


校验和:验证UDP数据报在传输过程中是否出现错误的情况

数据传输会出错??这是肯定的!

让我们来了解比特翻转(1 -> 0,0 -> 1)

数据的传输,本质上来说是电信号/光信号/电磁波

这些信号的传输,都会受到周围环境的印象,周围的磁场,高能粒子流,都可能会是这些信号在传输过程中,由低电平变为高电平,这就是比特翻转

比特翻转这个情况是客观存在的,无法避免,所以我们只能通过一些手段判断传输过来的数据是否出错,如果出错,我们可以不去使用这个数据

校验和,就是甄别数据出错的一个有效方案

发送方,构造完UDP数据报之后,可以把数据报的每个字节的数据,都进行累加,将结果累加到一个16为的帧数上,如果溢出就溢出了,最后得到的结果check1,就是校验和

然后将校验和填到报头的校验和字段

当接收方收到UDP数据报之后,按照同样的算法,再计算一遍校验和check2,将check1和check2进行对比,如果相等,就可以视为传输的数据是正确的,如果不相等,就是错误的

极端情况,还会出现恰好是2个比特位发生翻转,导致翻转后的校验和和翻转前一样,这种情况也是存在的,但是概率非常小.如果是对数据非常敏感的场景,我们还有别的方法来判断数据是否传输正确.

3.TCP协议格式

TCP协议,相比UDP协议,是一个有连接,可靠传输,面向字节流的.

所以TCP的协议格式,也是和UDP协议格式有很大的区别

1.源端口和目的端口:

这个就不过多解释了,和UDP数据报的是一样的,表示通信双方的端口号

2.4位首部长度:

表示的是报头的长度,因为包含选项,所以说报头的长度其实是可变的

4位首部长度,这里的单位是4字节,0-15,每个单位表示4个字节,所以说TCP的报头长度最大是60字节.

3.保留位

在前面,我们学习了UDP协议的格式,知道UDP协议最大一次只能传输64KB,这是因为UDP的长度已经限制死了2字节

而在这里,保留位的作用就是,现在不使用这6个比特位,等到后面需要用的时候在使用

不过在2001年的RFC3168标准中,后面两个保留的比特位已经被征用了,具体的大家可以自行了解一下

4.16位校验和:

这个和UDP协议报头里面的校验和作用一样,都是用来判断当前数据是否在传输过程中出现错误

5.标志位:

这六个标志位,以及剩下的一些窗口大小,紧急指针,32位序号,32位确认序号就需要结合TCP的一些核心机制来讲解

我们都知道,TCP协议是一个可靠传输的协议.那么TCP是如何做到的呢?

首先,我们要知道TCP协议是不可能百分百保证数据到达对方的

发送的数据,能够知道对方是否收到,就可以认为是"可靠传输"

(1)如果知道对方收到了,传输成功!

(2)如果知道对方没收到(丢包),采取其他的补救措施!

那么,TCP为了来实现可靠传输,就引入了很多的机制,

核心机制一:确认应答

什么是确认应答?实现让发送方知道接收方是否收到数据.

发送方发送数据之后,接收方一但收到了,就需要给发送方返回一个"应答报文",来告诉发送方收到了

应答报文:也就是在这六个标志位中的ACK(acknowledge)

类似于这种效果,在每次发送完数据之后,接收方都会回复信息但是有没有一种可能,数据收到的顺序不对呢?

我们知道,网络中存在"后发先至"的情况,这是因为一系列的数据包,要经过很多不同的路由器交换机,有的线路可能堵塞,有的线路可能通畅,所以说不一定先发送的数据一定先到达对方

就有可能出现下面这种情况:

为了解决这种情况,我们就引入了"序号和确认序号"

这里的1和2就相当于"序号",确认1和确认2就相当于"确认信号"

由于TCP是字节流传输,所以说序号和确认序号也是针对"字节"进行编号的

一个TCP数据包,包含多个字节,那么如何体现所有的编号呢??

编号是连续递增的,所以,我们只需要知道TCP载荷中的第一个字节编号是多少,后面的每一个字节的编号都知道了

TCP数据包载荷中的第一个字节编号不是从1开始的,客户端和服务器双方,都会随机生成一个数字当做第一个字节编号,这样是为了安全性(防止伪造旧链接的数据)避免旧包干扰新连接

就相当于是下面这种情况

在这六个标志位中,确认序号只在应答报文中生效,当ACK为1的时候,就表示确认序号字段有效,否则就属于无效字段

确认序号的含义:

1.所有小于确认序号的数据,都已经表示收到了

2.发送方接下来应该从确认序号的位置,继续发送数据

确认应答,就是TCP实现可靠传输的最关键的机制之一!!!

这里可能有小伙伴就会想到了,既然发送的数据包会丢失,那么ACK是不是也会丢失呢?

答案是会的,ack也是会丢失的!所以这里就需要TCP的另一个核心机制了

核心机制二:超时重传

超时重传机制是确认应答机制的重要补充!!!

什么是超时重传??当发送方发现,到达一定时间后,没有收到接收方发送来的ack,就可以认为是"丢包"了,就会再发一次

假设,一个数据包,在传输过程中,丢包的概率是百分之10,那么连续传输两次,至少到达对方的概率就变成了百分之99,随着传输次数的增加,数据包到达对方的概率会大幅度增加

那么,等待多久才会再次发送数据包?这个时间就成为"超时时间"

没有收到ack,有两种情况:

(1)发送的数据出现丢包情况,接收方没有收到数据包,自然不会发送ack

(2)发送的数据没有丢包,接收方收到数据包后发送ack,但是ack出现丢包情况

这种情况下,如果是情况2的话,那么接收方就会收到两份一模一样的数据,这样是否会有什么影响呢???

其实并不会有什么影响,在TCP协议中,已经对这种情况有过处理

TCP在接收数据的时候,会在操作系统内核中维护一个"接收缓冲区"(内存空间).

提到缓冲区,有些人就会想起了,我们在使用TCP网络编程的时候,好像有提到一个flush(),这个也是用来刷新缓存的

那么这两个缓存是一样的缓存吗?

答案很显然是不一样的.

在TCP协议中的"接收缓冲区",主要作用有三个:

(1)去重:

当由于接收方发送的ack出现丢包情况的时候,发送方就会发送来两份一模一样的数据.

这个时候,这两份数据都会放在这个"接收缓冲区"里面,系统就会根据数据的信号在"接收缓冲区"进行"去重"操作!

这样可以确保用户在使用read()操作的时候,不会读到重复的数据

(2)乱序重排

我们知道,在网络传输的过程中,会出现"先发后至"的情况,这个时候,就可能会出现序号为1-1000的数据是先发出的,1001-2000的数据是后发出的,但是1001-2000这个数据却比1-1000这个数据包先到接收方

这种情况我们上面也提及到了,是由于数据包在传输过程中,经历的路由器和交换机的线路是随机的,有些路由器和交换机会比较堵塞

但是即使出现了"先发后至"的这种情况,也不需要担心,TCP会针对这些数据在"内存缓冲区"根据序号进行重新排序.

(3)缓存数据

当发送方发送的数据过多的时候,接收方一下子处理不过来,就会将剩余的数据放在"接收缓冲区"里面,慢慢读


那么我们之前在网络编程中提到的flush()方法,那个刷新的缓存区又是什么呢?

那个是应用层缓冲区的概念,这是为了减少系统调用,会将数据先放在用户态缓存区

比如你发"hello",他可能就并没有发送到对方那边,而是存在了缓存区里面,等缓存区数据到一定数量的时候,再一次性发送过去,所以这时候需要使用flush()刷新缓存区.


那么我们知道超时重传是有个超时时间的,这个时间是固定的吗?

不是的,随着超时重传的次数增多,如果还是没有收到ack,超时重传还是要继续进行下去,但是超时时间会逐渐变长.

这是因为,正常情况下,超时重传之后,会大幅度提高数据到达对方的概率

如果重传之后还是没有收到ack,只能说明当前的丢包概率不止百分之10,这个时候,网络大概率出现问题了,如果再进行多次超时重传,不仅不能解决问题,还可能加重网络故障

重传次数和总的重传时间是有上限的,达到上限之后,重传还没有成功的话,TCP连接就会"重置".也就是相当于单方面断开连接

这里又涉及了一个标志位RST,触发RST(复位报文),就表示这个连接不要了,释放连接就相当于是删掉之前保存的对方的信息

确认应答是处理传输顺利的情况,超时重传是处理丢包的情况!!!

核心性质三:连接管理

我们知道TCP是有连接的,但是这个连接不是物理上的,而是虚拟的.

这个连接就相当于"双方都保存了对方的关键信息,如IP,端口号......"

那么如何建立这个连接呢?通过三次握手!!!如何断开连接呢?通过四次挥手!!!

这个内容相当重要!!!

1.三次握手:

在日常生活中,我们看见了一个熟人,都会过去"嗨"一下,这个"嗨"并没有传达什么信息,只是告诉对方我看见他了.

在TCP连接中也是如此,TCP中的握手,就是传输一个"打招呼"的数据包,这个数据包没有任何的"业务数据"(也就是没有应用层载荷),这个数据包只有报头,没有正文!!!

所谓三次握手,也就是说,在客户端和服务器建立连接的时候,需要有三次这样的"打招呼过程",连接才能建立好

关键流程:

(1)客户端先给服务器发送一个syn(同步报文段),告诉服务器:我要和你建立连接,请你保存我的信息

这个syn同步报文也是在那六个标志位中

(2)服务器返回给客户端ack,表示收到,我会保存你的信息

(3)同时服务器也会给客户端发送一个syn,表示让客户端保存服务器的信息

(4)客户端收到之后也会返回一个ack表示收到,我会保存你的信息

其实服务器和客户端之间是有四次交互流程,但是由于返回ack和发送syn是由系统内核控制,所以说,中间的两次可以合并为一个TCP数据包发送过去,这就是三次握手的流程

当然,在三次握手的这个过程中,也会出现某一次传输数据包的时候丢包,这个时候就需要靠超时重传机制了.

三次握手的意义是什么呢???能够解决什么问题呢?

(1)三次握手实际上是相当于"投石问路".验证通信链路是否畅通

(2)三次握手也是在验证通信双方的接收和发送能力是否正常

(3)三次握手,让通信双方协商关键信息.就比如我们之前提到的TCP数据包的第一个字节的序号,序号并不是从1开始的,需要在三次握手的过程中协商的.

每次连接产生的第一个字节的序号都是不同的,为了防止出现当前连接使用上一次连接因为延迟等原因晚到的数据包,这种上一次连接中晚到的数据包,通常会丢弃.

TCP有这么多状态,我们不需要全部都学习,只需要了解几个即可

LISTEN:服务器存在的状态,服务器在 new ServerSocket之后,就会进入LISTEN,表示"在监听"这个端口,端口上过来客户端,就可以直接accept()

ESTABLISHED:表示客户端和服务器建立好连接,可以正常通信了

2.四次挥手

当TCP要断开连接的时候,就要经历四次挥手这个阶段

和建立连接的三次握手不同,三次握手一定是客户端主动发起

但是四次挥手可能是客户端主动发起,也可能是服务器主动发起,先发起的一方,定义为客户端

这里又涉及到了六个标志位中的一个FIN标志位

触发FIN的情况有两种:(1)进程退出(2)close()方法关闭连接,当FIN为1时,表示需要关闭连接

主要流程:

(1)客户端告诉服务器:我要和你断开连接,请你把我的信息删了

(2)服务器表示收到

(3)服务器也告诉客户端,我要和你断开连接,把我的信息删了

(4)客户端表示收到

那么有人就会有疑问了,为什么建立连接的时候,中间的两次通信可以合并为一个数据包,而在这里,FIN和ACK为什么不能一起发送呢?

这里我们要知道,ACK是由系统内核控制的,并不受程序员的代码控制,而FIN是由程序员的代码控制,或者是由客户端控制,FIN和ACK的触发时机不一定在一块,所以是可能合并,也可能不合并


经典面试题:建立连接的时候,必须握手三次吗?两次或者四次可以吗?

四次可以,但是没必要,中间两次传输时在同一时刻,合并成一次可以提高效率(封装分用2次 ->1次)

两次不可以,无法完成验证通信双方是否接收和发送能力正常的效果


四次挥手的意义:为了"释放空间"

建立连接需要保存对方的信息,连接不用了,自然需要释放这些信息

看上图,我们可以知道在四次挥手的时候,TCP的状态发生了改变

CLOSE_WAIT:表示被动断开连接的那一方进入的状态,收到对方发来的FIN就会进入TIME_WAIT状态同时返回ACK

TIME_WAIT:等待连接彻底释放.发送完ACK进入CLOSED状态

TIME_WAIT为什么需要等待呢?按理来说,我们传完ACK之后就可以释放连接了.

这是为了防止最后一个ACK出现丢包的情况,所以需要等待,如果最后一个ACK丢包,我们就会再次受到服务器发来的FIN,此时需要再次发送一个ACK

等待时间也是有限制的,2*MSL(网络上任意两点之间,传输消耗的最大时间),这个时间通常是一个很宽裕的值,比如Linux上是60s

TCP 协议中的几个关键状态:
LISTEN: 手机开机,信号良好,随时可以打电话
ESTABLISHED : 连接已经建立,电话接听,可以说话了.
CLOSE_WAIT: 等待代码调用 close
TIME_WAIT: 等待 FIN 重传,应对最后一个 ACK 丢包的情况

核心机制四:滑动窗口

前面三个核心机制,是为了实现"TCP的可靠性"

而滑动窗口,是为了提高效率.

实现可靠性,我们就需要牺牲部分传输效率,因为每次发送一个数据,都需要等待ack.单位时间内的传输数据的量就少了

于是滑动窗口这个机制就出现了

原本,我们需要每一次传输完数据都等一个ack,当要传输的数据量大了之后,现在我们可以改为传输好几份数据,再等ack

我们把批量传输多少份数据不需要等待的这个数据大小,称为"窗口大小"

那我们是收到4个ack再往后发4组数据,还是收到一个ack发送一组数据呢?

肯定是后者,收到一个ack就往后继续发一组数据.

滑动窗口的出现,是"亡羊补牢",再怎么提高,也不会超过UDP这种无可靠传输机制的效率

我们要知道,只要是使用TCP传输数据,在任何一个阶段都可能会出现丢包!!!

所以,滑动窗口的时候,丢包该怎么办呢???

情况1:数据包正常抵达,ack出现丢失:

这种情况一般不需要去管,因为ack表示的是当前确认序号前面的所有序号的数据全部接收成功,如果3001这个确认序号丢失,但是4001这个确认序号正常发出去,那么就可以认为4001之前的数据全部成功发送,这时候也就不需要再去重新发送2001-3000这个数据包了.

如果是最后一个ack丢失了,说明批量传输已经结束了,需要靠超时重传这个机制

我们要知道,TCP这些机制都是共存的,并不冲突.

如果TCP传输大量的数据的时候,就会自动触发滑动窗口 ,重传机制就采取快速重传(下讲)

如果TCP传输少量数据的时候,就会使用确认应答和超时重传这两个机制

可以想象成

java 复制代码
if(连续多次收到同一个ack||长时间没收到ack){

                ​​​​​​​        ​​​​​​​        ​​​​​​​        触发重传

}

情况2:数据包丢失:

当我们发送数据的过程中,比如1001-2000这个数据包丢失,然后我们又发了后面几组数据包

接收方在收到这些数据的时候,没有收到上一个ack对应的序号的数据,就会一直发送这个ack

当发送方收到三个一样的ack的时候,就会将这个数据包重传过去,这个在滑动窗口机制下的重传叫做快速重传,相当于超时重传的变种

当接收方收到1001之后,会发现自己的接收缓冲区里面已经有了2001-7000这些数据,下一个ack的确认序号就是7001了

核心机制五:流量控制

这个机制是搭配滑动窗口使用的,我们知道滑动窗口的窗口大小越大,传输效率就越高,但是滑动窗口不能无限的大下去,这样对可靠性就有影响了

所以,流量控制,就是根据接收方的处理能力,干预到发送方的发送速度(调整滑动窗口的大小)

那么,我们怎么衡量一个接收方对数据的处理能力呢?

所谓"接收方对数据的处理能力",就是接收方应用程序调用read()的速度(调用read()有多快,每次read读多少)

但是调用read()的速度是和应用程序的代码相关的,想直接衡量还不太方便.

所以,我们就可以通过接收缓冲区的剩余大小来衡量read()的速度.

我们都知道,发送方发送数据之后,会先进入接收方的接收缓冲区.如果接收缓冲区的剩余空间大小多的话,就相当于read的速度快,如果空间小的话,就认为read的速度慢.

这样,在接收方返回ack报文的时候,在TCP报头中,把接收缓冲区的剩余空间大小的数值放在ack报头中,等发送方收到ack报文之后,就可以知道接收方的处理速度了.

发送方收到ack的时候,就会根据当前窗口大小,重新设置下一轮滑动串口传输数据的窗口大小了

我们可以看到,在TCP协议格式中,窗口大小栈16位,也就是64KB,这是不是意味着窗口大小最多64KB???

不是这样的,TCP协议在设计的时候,充分吸收的UDP的教训,在选项中有一个"窗口扩展因子 ",当发送方收到ack后,就会设置窗口大小,窗口大小 = 16位窗口大小<<窗口扩展因子.左移一位相当于*2,所以这是指数级增长的,窗口大小的取值范围是非常大的.

当然,如果当前数据包不是ack,那么窗口大小字段无效(里面的值没有意义)

当接收方的接收缓冲区满了之后,发送方就会暂停发送.当发送方不给接收方发送数据了,接收方也就不会给发送方返回ack.

但是发送方只是不发业务数据了,还是会发一个"窗口探测包",这个"窗口探测包"的作用就是探测接收端的接收缓冲区是否还有空间,当接收方的接收缓冲区有空间之后,也会给发送方返回一个"窗口更新包",之后发送端就会继续发送数据

核心机制六:拥塞控制

流量控制,是根据接收端对数据的处理能力,调整窗口大小来控制发送速率

而拥塞控制,是根据在传输过程中,传输路径(路由器,交换机)是否堵塞,来调整发送速率

那么,我们怎么能知道发送数据包的路径中的交换机路由器是否堵塞呢?毕竟传输路径是随机的.

我们主要采用的方式就是"试".

我们可以先按照比较小的速率先去发送数据,如果在传输过程中不出现丢包情况,就增加窗口大小,增加传输速率,如果出现丢包情况,就见效窗口大小,见效速率.

发送方会自己围护一个变量作为拥塞控制的窗口.

发送方的发送窗口大小,同时取决于流量控制和拥塞控制.谁小听谁的

也就是说,发送方要根据接收方处理数据的能力和传输途径中的堵塞情况来调整自身窗口大小

下面图是窗口大小变化图

核心机制七:延迟应答

为了提高传输效率,我们可以尽可能让窗口大一点

那么,我们上面提到了,窗口大小是根据传输路径的堵塞程度和接收缓冲区的剩余空间大小决定的.

那么,我们不能改变传输路径的堵塞程度,但是我们可以让接收缓冲区的剩余空间大小更大

怎么做呢?那就是要依靠延迟应答.

当收到发送方发来的数据包的时候,我们不立刻返回ack,而是等一会,在这等的时候,接收方可能尽可能的多消费一些数据,这样接收缓冲区的剩余空间就会变大了.

假设接收端缓冲区为 1M. 一次收到了 500K 的数据;如果立刻应答,返回的窗口就是 500K;

但实际上可能处理端处理的速度很快,10ms 之内就把 500K 数据从缓冲区消费掉了;

在这种情况下,接收端处理还远没有达到自己的极限,即使窗口再放大一些,也能处理过来;

如果接收端稍微等一会再应答,比如等待 200ms 再应答,那么这个时候返回的窗口大小就是 1M;

一定要记得,窗口越大,网络吞吐量就越大,传输效率就越高。我们的目标是在保证网络不拥塞的情况下尽量提高传输效率.

核心机制八:捎带应答

网络通信中,经常是"一问一答"的模型.我发一个数据包,你回一个ack,然后计算出响应再回一个响应.

那么我们可以把ack和响应合并一起返回吗?

答案是可以的.

由于有延迟应答这样的机制,导致我们ack并不会在收到数据包的时候立刻返回,而是会等一会,这等的时候,可能服务器对请求计算出来的响应也完成了,此时就可以ack和响应一起返回.

ack只有报头,没有载荷,在报头中比较关键的是,ack为1,确认序号,窗口大小

而普通的响应,ack这一位是0,而且窗口大小,确认序号这些都是无效的.

所以,我们就可以把ack报文和响应数据做成一个TCP数据报返回给客户端

这就是捎带应答.但是捎带应答不是百分百触发的,得看代码怎么写的,如果说根据请求计算响应这一过程太慢了,就不会触发

核心机制九:面向字节流

TCP是面向字节流的.所以读写数据的方式有很多

如果有100个字节,我们可以一次读10字节,分十次;一次读20字节,分五次;还可以一次读50字节,分两次

这样,我们就会产生粘包问题

举个例子:

接收方收到数据之后,会去掉TCP报头,把载荷放在接收缓冲区里面.

如果我们一次读两个字节,就会出现aa;ab;bb;cc;c这种情况,如果我们一次读4个字节,就会出现aaab;bbcc;c这种情况,所以不能正确读到一个完整的数据包.

那么我们该怎么解决呢?怎么才能读到一个完整的数据包呢?

我们需要从应用层解决,设计合理的应用层协议,让包与包之间的边界清晰

解决方法:

(1)通过特殊的分隔符,来作为包的边界区分,比如以;结尾...或者用ASCII码表中的特殊字符来作为分隔符

(2)在应用层数据包开头的地方,通过固定长度,来约定整个应用层数据包的长度

比如,在数据包前面先固定读2个字节,看看这个字节里面的内容是啥,发现是3,我们就往后继续读3个字节,这样就是一个完整的数据包.

粘包问题,只要是针对字节流的传输,都有可能涉及到.对于UDP来说的话,就没有这个问题,因为UDP的接收缓冲区和TCP不一样.

核心机制十:异常情况

1.进程崩溃

进程崩溃,意味着对应的文件描述符表就被关闭了(调用close()关闭进程,只要进程退出,都会释放PCB,释放文件描述符表)

这时候,就会触发FIN.TCP的连接并没有因为进程的结束立即结束,而是会等一会,等四次挥手结束.

2.主机关机(正常流程)

正常情况下,主机关机,会杀死所有进程,如果关机速度较慢,可能会完成四次挥手,如果关机速度过快,就完成不了四次挥手

不过这也没什么问题.没有挥完手,我们可以把自己这边保存的信息删掉,对端的就不管了.

对端这时候会尝试返回ack,也会继续发送FIN,但是由于我们这一端已经关机.所以不会有任何回应.

这时候对端没有收到我们这边对于它发送的FIN应该返回的ack,就会持续发送FIN,还是不会收到ack,这个时候就会直接断开连接(RST标志位).

3.主机掉电(直接拔电源)

主机掉电,台式机的话,啥也没了,都来不及发起FIN

如果掉电的一段是接收方,对方会持续发送数据,然后没收到ack,然后超时重传.达到一定程度,对方还是没有ack,就发送一个复位报文(RST),然后放弃当前连接

然后掉电的一方是发送端,对方是接收端

对方就会感受到,发送端不发数据了.正常情况下,接收方每隔一段时间,都会和发送方交换"心跳包"

什么是心跳包?就是A给B发送一个无业务数据的报文,B给A返回一个ack

如果对方有应答,就可以认为对方是在正产工作,如果心跳包也没有应答,就可以认为对方挂了

当然,心跳包也会出现丢包,所以说不是一次心跳包没有回应就断开连接.会有一定的容错次数.

4.网线断开

本质上和主机掉电是一样的,对于发送端来说,接收端掉线,就会没有ack,然后超时重传,还是没有ack,就会发送复位报文,断开连接

对于接收端来说,没有发送端发过来的心跳包,就会发现对方下线,然后断开连接.

讲完这些机制,我们会发现,TCP协议中还有一个内容没有了解到,那就是16位紧急指针(URG)

**URG为1表示紧急指针有效.**正常情况下,TCP数据都是按照顺序传输,1-1000,1001-2000......

紧急指针意味着后面有些数据需要优先传输(插队).紧急指针的值表示的是,从当前位置往后多少个字节位置的部分,要进行插队的.URG使用的很少

还有一个PSH,这个的作用就是催促接收方,尽快把缓冲区的数据交给应用程序,使用的也很少


本篇文章主要是对传输层做了一些相关介绍,尤其是TCP协议和UDP协议.

我们需要重点了解TCP协议的三次握手和四次挥手,以及TCP协议的这些机制.

TCP协议并不是只有这十个机制,而是有很多很多,本篇文章只提及了这十个重要的机制.

本篇文章还对应用层做了一些相关了解,后面还会学习更多有关应用层的知识,例如HTTP/HTTPS协议相关内容.

相关推荐
the sun342 小时前
Linux上位机开发中的串口termios库函数使用
linux·运维·服务器
XerCis2 小时前
Linux内网环境无法访问外网的情况下安装程序
linux·运维·服务器
文刀竹肃2 小时前
Upload-Labs 第1至第10关通关教程(更新中。。。)
网络·安全·web安全·网络安全
GIOTTO情2 小时前
游戏行业舆情处置技术实战:Infoseek 字节探索系统架构与 Python 落地脚本
网络·安全
流水迢迢lst2 小时前
靶场练习day12--SSRF
服务器·网络·安全
艾文-你好2 小时前
深信服SSL aTrust设备密码重置及管理密码重置
linux·服务器·ssl
WHD3062 小时前
苏州华为/联想/浪潮 国产服务器 硬件维修
运维·服务器·git
百结2142 小时前
Linux系统安全
linux·运维·服务器
Reuuse2 小时前
【网络基础概念】
开发语言·网络·php