互联网中,主流的是 TCP/IP 五层协议
- 5G/4G 上网,是有自己的协议栈,要比 TCP/IP 更复杂(能够把 TCP/IP 的一部分内容给包含进去了)
应用层
可以代表我们所编写的应用程序,只要应用程序里面用到了网络通信,就可以认为这个代码就是属于应用层的代码
日常开发中最常用到的一层:
- 使用大佬们已经创建好的应用层协议
- 应用层知名的协议有很多,其中的佼佼者就是
HTTP
- 自己定义应用层协议
- 另外四层都是操作系统/硬件/驱动已经实现好了的,我们不可能“自定义”,只能使用人家的
协议就是约定
- 按照自己的规则,约定通讯方式——>自定义应用层协议
自定义应用层协议
自定义应用层协议,具体要做什么事情:
明确要传递的信息
- 明确前后端交货过程中,要传递哪些信息
举个例子:开发一个外卖软件,打开软件后,首先需要展示一个“商家列表”
此处就需要先确定传递的信息是什么
- 请求:用户是谁(用户的 ID),用户所处的位置
- 响应:商家列表,包含多个商家,每个商家信息中,又有商家的名字、图片、距离、评分
这里的信息如何确定,都是根据当前的需求来产生的
明确组织信息的格式
- 明确组织这些信息的格式
针对信息组织格式,也有很多种方式,使用哪种方方式都可以,只要确定前段和后端是同一种方式就可以了
举个例子:使用行文本的方式来组织上述数据
- 请求:
用户id,用户位置\n
- 响应:
商家的id,商家名称,商家的图片地址,商家的距离,商家的评分\n
关于组织数据的格式,还有一些说法,上述“行文本”简单粗暴的方案,在实际开发中,很少会这样做
XML 方案
Maven 中就会见到,通过“成对的标签”表示“键值对”信息
<request> <userid>1001</userid> <postion>E45N60</postion> </request>
- 可以通过
XML
来传输网络数据,也可以作为程序的配置文件- 不过
XML
进行网络传输的时候,又有一个明显的缺点——会消耗大量的带宽
- 网络通信中,带宽是一个非常贵的硬件设备
- 在传输标签的时候,都得传输成对的标签,传入的信息更多- 所以现在
XMl
一般都是在配置文件,不进行网络传输了XMl
里面的标签(键值对)都是程序员固定的,而HTMl
里面的标签都是固定的(已经有一套标准,约定好哪些标签是合法标签,这些标签都是什么含义)
JSON 方案
当前主流的网络通信的数据格式,相比 xml
来说,可读性是很好的,同时能节省一定的带宽
{ "userid":1001, "postion":""E45N60" }
JSON
也是“键值对”格式,
- 键和值之间用
:
分割- 键值对之间用
,
分割- 所有的键值对,都使用
{}
括起来
- 这里的标签都只有一份,不需要结束标签了,节省了传递开销
YML(YAML)
强制要求了数据组织的格式,强制要求写成“可读性非常高”的格式
- 键值对必须独占一行
- “嵌套”结构必须通过缩进来表示
Google protobuffer
前三个方案,都是关注可读性,而 protobuffer 关注性能,牺牲了可读性(通过二进制的方式组织数据)
- protobuffer 直接通过“位置”约定字段的含义,不需要传输 key 的名字,也会针对传输的数值,进行二进制的编码,起到一些“压缩”的效果
- 极大地缩减了要传输的数据的体积——>带宽消耗就越小——>效率越高
- 但二进制数据无法肉眼阅读,调试相关程序的时候,就会比较麻烦
传输层
主要的两个协议 UDP 和 TCP
常见端口号
端口号是一个整数,用来区分不同的进程。
- 同一时刻,同一个机器上,同一个协议,一个端口号只能被一个进绑定
- 一个进程可以绑定多个端口号
- 端口号是通过两个字节的无符号整数表示的,取值范围 0~65535,但实际上 0 比较特殊,一般不会使用
- 1~1023 属于已经被预定好的(有一些知名的服务器,已经提前预定了这个端口),这样的端口称为“知名端口号”(其实里面的大部分服务器已经不再使用了,在 80、90 年代是知名的)
- 我们日常开发的时候,会避开这些端口
业务端口和管理端口
什么时候会涉及到一个进程(服务器)绑定多个端口?
- 编写服务器,肯定需要先绑定至少一个端口号,和客户端进行交互(称为“业务端口”)
- 服务器运行过程中,希望能够对这个服务器的行为,进行一些“控制”
- 比如让服务器重新加载某个数据/某个配置/修改服务器的某个功能
- 也可以通过网络通信完成上述功能
- 就可以让服务器绑定另一个端口,通过这个端口,编写一个客户端,给服务器发送一些“控制类“请求
- 上面的“另一个端口”就是“管理端口“
调试端口
当需要针对服务器运行状态进行检测和调试,需要查看服务器运行中某个关键变量的数值的时候,千万不能用调试器来进行调试,一旦使用调试器调试这个服务,就会使服务器的一些线程被阻塞住,无法给客户端正确提供服务了
- 虽然可以通过日志进行打印,但是不方便,需要修改代码并重启服务器
- 可以让服务器绑定另一个端口,然后实现一些相关的打印关键变量的逻辑,客户端发送对应的调试请求
- 这里的“另一个端口”就是“调试端口”
- 在
TCP/IP
协议中,⽤,“源IP”,“源端⼝号”,“⽬的IP”,“⽬的端⼝号”,"协议号"这样⼀个五元组来标识⼀个通信
UDP 协议报头
学习一个网络协议,主要就是学习“数据格式”/“报文格式”
源端口/目的端口
- 端口号是属于传输层的概念
- UDP 报头使用两个自己的长度来表示端口号
- 之所以端口号的范围是 0~65535,是因为底层网络协议做出了强制要求
- 如果使用一个 10 w 这样的端口,就会在系统底层被“截断”
- UDP 并不关心后面的正文里面是什么数据,只需要关心报头里面是怎么组织的
- 报头里面分为四个部分,每个部分固定是两个字节(16 bit),里面没有分隔符,就是通过固定长度来进行区分的
网络通信中,涉及到四个关键信息:源 IP/目的 IP,源端口/目的端口
- 源端口:发出数据报那个程序使用的端口号——>发件人电话
- 目的端口:接受这个数据报的程序使用的端口号——>收件人电话
- 源 IP:发出数据报那个程序的 IP——>发件人地址
- 目的 IP:接受这个数据报的程序的 IP——>收件人地址
UDP报文长度
UDP报文长度:报头长度 + 载荷长度
- 长度单位是字节,
- 比如,报文长度 1024,——>整个 UDP 数据报就是 1024 字节;由于是两个字节来表示这个长度,所以最大值 65535——64 KB(65536/1024)
- 64 KB 放在今天,是个很小的数字,所以如果使用 UDP 协议传输一个很大的数据,就会变得很麻烦
UDP 用了好多年,一直挺好用,但随着业务的发展,广告越来越多,越来越复杂,导致一个网络响应数据报的体积越来越大,逐渐逼近 64 KB。一旦数据超过了 64 KB,就可能到值数据被截断,这样广告可能就无法正常显示了。对于这样的情况,有两个解决方案:
- 把一个大的数据报,拆分成多个,分别进行传输
- 很快就被否决了;因为实现分包、组包的过程非常复杂,充满了不确定性
- 直接使用 TCP
- TCP 对于长度没有限制,其自身也带有可靠传输这样的机制,对于整体的通信质量来说也是有利的
- 代码的修改成本比较低
校验和
前提:网络传输过程中,非常容易出现错误
- 电信号/光信号/电磁波——>收到环境的干扰,使里面传输的信号发生改变
校验和存在的目的,就是为了能够“发现”或者“纠正”这里的错误。就可以给传输的数据中,引入“额外信息”,用来发现/纠正传输数据的错误
- 这里的额外信息就是
checksum
- 如果只是发现错误,需要携带的额外信息,就可以少一些(发现就会丢弃掉,不会让对方重发)
- 如果是想要纠正错误,携带的额外信息就要更多(消耗更多带宽)
举个例子:你妈让你去买菜,西红柿、鸡蛋、茄子、晃过,最后补充一句“一共四样”
- 这里的“一共四样”起到的作用就相当于是“校验和”。通过“一共四样”你可以对手里的菜进行检查,有没有买多、买少
但这样的“校验和”并不能准确的识别出问题,而且容易误判。所以我们希望校验和可以更严格地检查数据的内容,可以结合内容/内容的片段来生成校验和
- 比如你在默写金庸先生的十五部作品的名称,写完后,你可以通过“飞雪连天射白鹿,笑书神侠倚碧鸳”这一幅对联和你写的书名的第一个字对一下,若能对象,就说名此处的名字都是正确的
- 这样的校验和就是基于内容来进行校验的
- 虽然出错的数据恰好没有被校验出来,这可情况也是可能会发生的
- 但是一个良好的校验和算法,可以让上述问题发生的可能性非常低
CRC 校验和(循环冗余校验)
把 UDP 数据报整个数据都进行遍历,分别取出每个字节,往一个两个字节的变量上进行累加
- 由于整个数据可能会很多,所以加着加着可能就结果溢出了,但溢出也没关系
- 我们重点关心的不是最终加和是多少,而是校验和结果是否会在传输中发生改变
例如:我们去传输一个
UDP
数据报
- 发送方整合整个
UDP
数据,基于这些数据,计算得到一个checksum1
- 接收方收到的数据:
- 数据的内容
- 校验和
checksum1
- 接收方就可以根据数据的内容,按照同样的算法,再算一遍校验和,得到
checksum2
- 如果传输的数据在网络通信过程中,没有发生任何改变,则一定有
checksum1 == checksum2
MD5 算法
本质上是一个“字符串 hash 算法”
特点:
- 定长:无论输入的字符串长度多长,算出的
MD5
的结果都是固定长度——>适合做校验和算法 - 分散:输入的字符串哪怕只有一点点发生改变,得到的
MD5
的值都会相差很大——>适合做 hash 算法 - 不可逆:根据输入内容,计算
MD5
非常简单,但是如果想通过MD5
值还原出原始的内容,理论上是不可行——>适合作为加密算法
TCP 协议
TCP 相关机制
TCP 基本特点:有连接、可靠传输、面向字节流、全双工
- 有连接、面向字节流和全双工都能在前面的代码中体现
- 有连接:必须要先调用
accept
建立联系才能处理 - 面向字节流:会拿到
clientSocket
对象的InputStream
和OutputStream
,再来去读写数据 - 全双工:一个
Socket
对象,既可以读,也可以写
而此处的可靠传输在代码层面感知不到,它是在系统内核完成了这里的工作
TCP
最核心的资质就是“可靠传输”,不能做到 100%
送达,只能尽可能的是数据能到达对方方
- 能感知到对方是否收到
- 如果发现对方没有收到,就要进行重试
后发先至
- 如果数据按照左边的传输次序来的,那就不会有歧义
- 虽然女神先发的“好啊好啊”,后发的“滚”,但是在网络传输中,可能存在“后发先至”,对于我们接收方来说,可能会先收到“滚”,后收到“好啊好啊”
- 此时,歧义就产生了,我将会误以为女神答应我了
为什么会出现“后发先至”?
互联网最初是用来防御核弹打击的,即使是遭受到了核弹打击,但是 A~B 之间的通信路径有很多很多条,不会全军覆没,所以仍然可以确保数据能正常传输
- 在正常传输数据的时候,传输数据包不一定走同一条路线
- 因为是不同的路线,每个数据包传输过程中,遇到的状况也都有差别,最终达到目标的时序就可能存在差异了
举个例子:一个婚车队伍,在触发的时候都是按照顺序,一辆一辆跟着走的,但一到大路上,车队就可能会走散
- 本来跟着头车走,结果遇到了红灯,一旦跟不上,就各凭本事了
- 每辆车,各个导航,可能走的路线都不一样,可能有的车还会堵车
- 最后到达目的地的先后顺序改变的情况就是很常见的
1. 确认应答
后发先至的解决方法,TCP 核心机制,感知对方是否收到,就是要靠对方告诉你一声“收到了”
- 后发先至是客观存在的情况,无法改变
若要解决,可以给传输的数据添加“编号”,通过编号,可以区分出数据的先后顺序
- 我收到的应答报文,即使顺序出现错乱,也能识别出来原意
- 由于
TCP
是面向字节流的,是加上这里的编号并非是按照“第一条,第二条”这样的方式来编排的,而是按照“字节“,“第一个字节,第一百个字节”,这样来编排的 - 每个字节都有一个独立的编号,字节和字节之间,编号是连续的、递增的
- 这种按照字节编号的机制,就成为“
TCP
的序号”,在应答报文中,针对之前收到的数据进行对应的编号,称为“TCP
的确认序号”
之后,TCP 就可以针对接收方收到的信息,进行重新排序,确保应用程序 read 到的数据一定是和发送方的数据顺序是一致的
接收方这边调用 read 的时候如果没有数据,就会阻塞等待(前面回显服务器文章中写的是 scanner 读取,本质上就是调用 InputStream.read
)
- 接收方收到的数据信息顺序可能和发送发传输时的顺序不一样
- 此时接收方收到
1001-2000
这个数据到了,但是接收方不会让read
接触阻塞,因为这个数据的起始部分还没到,2001-3000
到了之后也进行阻塞 - 直到
1-1000
这个数据到达之后,read
才会接触阻塞,才会读取到1-1000
,1001-2000
,2001-3000
- 接收方这边,操作系统内核里面,有一段空间,作为“接收缓冲区”,收到的数据就会先在缓冲区中排队等待,直到开头的数据到了,应用程序才能真正读取到里面的数据
前面接亲的例子:
等婚车到了女方门口,因为头车还没来,所以不能直接开到新娘门口去接人
- 因此这样的车就得在外等待,必须等头车到
- 等车到了之后,再重新排好队,再一起开到新娘家门口
丢包
丢包的原因有很多种:
- 数据传输的过程中,发生了 bit 翻转,收到这个数据的接收方/中间的路由器什么的,计算校验和,但是发现校验和对不上
发现错误,要及时止损,不能将错就错
- 所以就会把这个数据丢弃掉,不继续往后转发/不交给应用层使用
- 数据传输到某个节点(路由器/交换机),但这个节点的负载太高了,后续传输过来的数据就可能被这个路由器直接丢弃掉
- 负载太高:某个路由器,单位时间只能转发
N
个包,但现在是网络高峰期,这个路由器单位时间需要转发的包超过N
个了,发不过来了
2. 超时重传
发生丢包是完全随机,不可预测的,TCP
再怎么厉害,也不可能避免数据发生丢包。TCP
能做的是:感知到数据是否丢失,如果丢包,就重新再发一次
此时需要通过应答报文来区分
- 收到应答报文,说明数据没丢包
- 没收到应答报文,就说明数据丢包了
- 网络传输是需要消耗时间的,这里的“没收到”是暂时没收到,还是永远都收不到?
- 发送方发送数据之后,会给出一个“超时时间”,如果在这个时间限制之内,没有收到反馈的 ACK(应答报文 ACK 由 0 变为 1),就视为数据丢包了
不管是因为数据丢了导致的丢包还是因为 ACK 丢了导致的,都会重发
- 但如果是因为 ACK 丢了导致的重发,接收方就会收到两份一样的数据,这样是很不好的(一次扣款请求扣两次)
- 所以为了确保应用程序调用
read
读出来的数据是唯一、不重复的,TCP 就会对这种情况进行处理——去重
- 接收方有一个“接收缓冲区”,收到的数据会先进入到缓冲区里,后续再督导数据,就会根据序号,在缓冲区中找到对应的位置(排序)。如果发现当前序号 1-1000 这个数据已经在缓冲区中存在了,就会直接把新收到的这个数据丢弃掉
超时时间的设定
这里的时间不是固定不动的,而是动态变化的
发送方第一次重传,超时时间是 t1
,如果重传之后,仍然没有 ACK
,就会继续重传,第二次重传的超时时间是 t2
,t2>t1
- 每多重传一次,超时时间的间隔就会变大,重传的频次会降低
- 经过一次重传之后,就能让数据到达的概率提升很多
- 反之,如果重传了几次,都没有顺利到达,说明网络的丢包率已经达到了一个非常高的程度——>网络发生了严重故障,大概率没法继续使用了
重传也不会无休止的进行,当重传达到一定次数之后,TCP
不会再重传,就认为这个连接已经挂了 - 先尝试进行“重置/复位连接”,发送一个特殊的数据包“复位报文”,尝试和对方重新进行连接
- 如果网络这会恢复了,复位报文就会重置连接,使通信可以继续进行
- 如果网络还有严重问题,复位报文也没有得到回应,此时
TCP
就会单方面放弃连接(发送方释放掉之前接收方的相关信息,这个连接诶也就没了)
确认应答和超时重传相互补充,共同构建了 TCP 的“可靠传输机制”
- 可靠传输机制不是靠“三次握手和四次挥手保证的”
3. 连接管理
- 建立连接:三次握手
- 断开连接:四次挥手
网络中的握手/挥手,就是发送不携带业务数据(没有载荷,只有报头)的数据包,但是能起到“打招呼”这样的效果。次数就是指网络通信的次数。
建立连接:三次握手
此处建立连接发送的 SYN
是同步的意思,可以延伸成:客户端希望服务器和它统一步调,来完成手续的传输
建立连接是一个“双向的操作”,连接是“抽象的连接”,代表通信双方各自保存对方的信息
- A 需要给 B 说:我想和你建立连接(A 想保存 B 的信息),之后返回应答(
ACK
) - B 也需要给 A 说:我也想和你建立连接(B 想保存 A 的信息),之后返回应答(
ACK
)
本来是四次握手,但是中间两次合并了,就成了三次握手,合并是很重要、很有意义的,要是分两次发送,效率就会打折扣,因为每个数据包都需要进行一系列封装分用
- 收到
ACK/SYN
都会使报头中的对应值由 0 变为 1
三次握手要解决什么问题?为什么要三次握手?意义何在?
三个方面:
- 投石问路的效果,初步验证通信的链路是否畅通
先通过一些没什么业务意义的报文,来验证一下这个路是不是通的,这是进行可靠传输的“前提条件”
例如:地铁需要在每天开始第一班车之前,空跑一趟,进行“链路畅通的验证”
- 可以确认通信双方的发送能力和接受能力是否都正常
除了关注中间的路线,还要关注两头的双方的接法能力是否正常
例如:你要和朋友一起玩游戏,事先你们先要确保双方的耳机和麦克风都是正常的
- 第一次挥手:
- 你这边仍然都不了解双方的能力
- 朋友那边听到“嘿”,意味着他知道了你的麦克风正常,他的耳机正常。但他不知道我的耳机和他的麦克风是否正常
- 第二次挥手:
- 朋友说“喵喵”,他仍不确定他自己的麦克风和我的耳机是否正常
- 你听到了“嘿嘿”,你就知道了你的耳机正常,朋友的麦克风正常。由于你们事先约定好了,他一定要在听到你说“嘿”之后再回复,所以你也就知道了你的麦克风正常,他的耳机正常。此时,你就知道了双方的耳机和麦克风都是正常的
- 你的验证完成
- 第三次挥手:
- 我再说“嘿嘿嘿”,把我掌握的信息同步给他
- 朋友听到“嘿嘿嘿”,由于我们实现约定好我一定是在听到他说 "嘿嘿"之后我再回复。因此,他听到了“嘿嘿嘿”,意味着他刚才说的“嘿嘿”被我收到了,他也就知道了他的麦克风正常,你的耳机正常
- 朋友的验证完成
- 针对 TCP 来说,必须要通过三次握手来验证双方的发送和接收能力。但其他协议就不一定是三次了
- 让通信双方在进行通信之前,对通信过程中需要用到的一些关键参数进行协商
TCP 通信时,起始数据的序号,就是通过三次握手协商确定的(换而言之,TCP 序号并不是从 1 开始的)
- 每次建立连接,TCP 的起始序号都不同,而且故意差别很大
- 避免出现“前朝的剑,斩本朝的官”
- A 和 B 后来又建立连接,虽然还是 A B 两个主机之间的连接,但可能变成不同的应用程序了
- 在传输业务数据的过程中,可能有某个数据包“迷路”了,饶了一大圈,最终才到达对端。当他到达的时候,已经“改朝换代了”,已经不是原来那个连接了
- 针对这样的“迟到”的数据包,就要将其丢弃掉,不能按照正常的流程处理这里的数据了,避免其在新的程序中产生负面的影响
- 对于 B 来说,就要区分当前收到的数据是“本朝”还是“前朝”的
这样,就可以给每个连接都协商不同的起始序号,如果发现收到的数据和起始序号以及和最近收到的数据序号差别都很大的话,就视为这个数据就是“前朝”的数据
网上有些资料说“TCP 的可靠性是通过三次握手体现的”,这句话是有些问题的
- 三次握手对于可靠性,是有一定的支持的
- 但是不能说可靠性就是三次握手体现的
- 因为三次握手只是建立连接的时候进行的,一旦连接建立好了之后,数据开始传输了,就和三次握手没关系了
- 传输数据过程中的可靠性是通过“确认应答”和“超时重传”来体现的
断开连接:四次挥手
优雅地断开连接,双方各自删除掉保存的对方的信息
断开连接不一定是“客户端主动”,服务器也可以主动断开
- 通信双方各自给对方发送
FIN
,各自给对方返回ACK
- A:“B 兄,我要把你删了“==>
FIN
- B:“好的“==>
ACK
- B:“A 兄,那我也把你给删了哦”==>
FIN
- A:“好的”==>
ACK
三次握手,只有三次是因为中间两次的交互合并在一起了。但对于四次握手来说,中间两次却不一定能合并(大概率是不能)
- 因为对于三次握手来说,中间的两次(
ACK+SYN
)都是在内核中由操作系统负责进行的,时间都是在收到SYN
之后,此时同一时机,就可以合并了 - 对于四次挥手来说,
ACK
是由内核控制的(就是说系统只要收到FIN
就会立刻返回一个ACK
),但是FIN
的触发则是通过应用层程序调用close
/进程退出
来触发的
- 代码中:
socket.close()
==>系统内部:发送FIN
- 如果代码没有执行
close()
,系统内部就不会发送FIN
,所以中间发送ACK
和FIN
之间是有时间间隔的
TCP
的状态
LISTEN
:服务器进入的状态,服务器把端口绑定好,就相当于进入了LISTEN
状态,此时服务器就已经初始化完毕,准备好随时迎接客户端了(手机开机,信号良好,随时可以有人打电话)ESTABLISHED
:TCP
连接建立完成(保存好了对方的信息了),接下来就可以进行业务数据的通信了(电话接通,可以说话)CLOSE_WAIT
:被动断开连接的一方会进入的状态,先收到FIN
的一方。等待代码执行close
方法。
- 如果发现服务器这边存在大量的
CLOSE_WAIT
状态的TCP
连接,说明了什么?- 说明此时服务器代码可能有
bug
,排查close
是否写了或者是否执行到了
TIME_WAIT
:主动断开连接的一方会进入的状态,此处的TIME_WAIT
按照时间来等待,达到一定时间后,连接也就释放了
- 为什么不直接释放呢?
TCP
传输过程中,任何一个数据都会丢包,但丢包重传就好了,只要此处的连接还在,就能很好的处理这里的重传操作。保留TIME_WAIT
状态就是为防止最后一个ACK
丢包,这样即使丢包了也能进行重传- 如果最后一个
ACK
丢包了,此时B
这边就会重传一次FIN
,需要A
这边再发一次ACK
,但能够再发一次ACK
的前提是A
这边的连接还没有释放(如果连接释放了,就不知道对方的信息,无法返回任何数据了)- 如果
TIME_WAIT
状态保留了一段时间后,也没有收到重传的FIN
,就说明刚才的ACK
应该就是到了,就可以释放这里的连接了
TIME_WAIT
存在的时间称为2MSL
(MXL
==>数据包在网络传输中消耗的最大时间))
4. 滑动窗口
有一类算法题,就是通过滑动窗口的思想来解决的,算法中的“滑动窗口”借鉴自 TCP 的滑动窗口
TCP 是要保证可靠传输的==>代价,降低了传输的效率(重传,确认重传等操作)
TCP 希望能在可靠传输的基础上,也有一个不错的效率,就引入了“滑动窗口”
- 这里的提高效率,只是“亡羊补牢”,使传输效率的损失,尽可能降低
- 引入滑动窗口,也不能使传输效率比 UDP 还高
窗口
A 每次都需要收到 ACK 之后,再发下一个数据
- 低效,有大量的时间都消耗在等待 ACK 上
改进方案:
- 把“发送一个等待一个”改为“发送一批等待一批”
- 把多次等待 ACK 的时间合并时一份了
- 批量发送的数据越多,效率就越高
- 批量发送的数据中,不需要等待的数据的量,称为“窗口大小”
- 批量发送的是数据的字节数,而不是条
- 窗口就是一次能发多少数据,具体的数据量就是窗口大小
滑动
当收到了第一个 ACK
之后,不会继续等待剩下的 3 个 ACK
到了之后再发下一组,而是收到这个 ACK
之后,就立即发送下一条数据
- 收到
2001 ACK
,说明1001-2000
数据得到应答了 - 然后立即发送
5001-6000
这个数据,此时等待的ACK
范围就是2001-6000
(四份数据),窗口大小还是 4000 - 窗口大小不变,只是窗口所处的位置改变了
每收到一个 ACK,窗口就往后挪,因为 ACK 是接连不断的发送的,所以窗口就往后挪动了,就滑起来了
滑动窗口就是批量传输数据的一种实现方式
滑动窗口丢包
情况一:
不需要任何处理;批量发数据,批量 ACK,多个 ACK 只是丢其中的一部分,不可能全丢
确认序号的含义:表示的是收到的数据最后一个字节的下一个序号。进一步理解成,确认序号之前的数据,都已经收到了,接下来你要发的数据就从确认序号这里往后发
- 虽然 1001 ACK 丢了,但是 2001 ACK 到达了。发送方收到 2001 ACK 之后,意味着 2001 之前的数据都已经收到了
- 后一个
ACK
能涵盖前一个ACK
的意义
情况二:
1001-2000
丢包之后:
- 在
A
发过去2001-3000
之后,此时,B
收到的数据为:1-1000
,2001-3000
- 此时
B
收到2001-3000
的时候,返回的ACK
确认序号不是3001
,而是1001
B
就是在向A
索要1001
的数据- 接下来,
B
和搜到的3001-4000
,4001-5000
,5001-6000.
… 对应的ACK
确认序号都是1001
- 主机
A
连续收到1001
这样的ACK
之后,主机A
意识到1001
数据包丢了,于是主机重传1001-2000
- 当
1001-2000
重传过来之后,由于执勤啊2001-7000
数据都是已经发过了,此时的1001-2000
相当于是补全了之前的空缺,此时就意味着1-7000
的数据都齐了,于是接下来索要7001
开头的数据即可
上述过程,没有任何拖泥带水的操作,快速的识别出了是哪个数据丢包,并且针对性的进行了重传,其他顺利到达的数据都无需重传,这个过程称为“快速重传”
快速重传可以视为是“滑动窗口”下搭配的“超时重传”
TCP 报头
首部长度
TCP
报头的长度
UDP
协议报头固定就是8
个字节- 对于
TCP
来说,报头长度是可变的
4 个比特位可表示的范围: 0000~1111
——>0x0~0xF
——>0~15
- 此处的长度单位是 4 字节,不是字节(所以范围是
0~60
字节)
保留(6 位)
虽然现在不用,但是先把这个东西申请下来,以备不时之需。用于考虑未来的可扩展性
- 充分吸取了
UDP
的教训,UDP
的报文长度字段,是没法扩展的 - 如果未来某一天,
TCP
需要新增属性或者谋和属性的长度不够用,就可以把保留位拿出来,进行使用 TCP
的结构不需要发生太大的改变,这样的升级就会容易很多
关于“可扩展性”也是属于编程的时候需要考虑到的一点,毕竟写的代码不可能写一份就能持续地使用。对代码做出调整,做出修改,是非常普遍、常见的情况
但是,
选项
TCP 报头边长的主要原因。四个字节为一个单位
- 可以有, 也可以没有
- 可有一个,也可有多个
通过“首部长度”确定报头有多长,如果是两个四个字节长度就是两个选项,三个四个字节长度就是三个选项,以此类推
序号
由于会出现“后发先至”的情况,所以需要通过编号,区分出数据的先后顺序
序号:表示的就是 TCP
数据报载荷中的第一个字节的序号,由于序号是连续递增,知道了第一个字节的序号,后续每个字节的序号也就知道了
- 32 位/四字节,表示的范围是
0~42亿9千万
(0~4G
) - 因为
TCP
是面向字节流的,所以一个TCP
数据报和下一个TCP
数据报携带的数据,是可以直接进行拼装的 - 比如要传输一个特别大的数据,传输过程中,本身就会通过多个
TCP
数据报来进行携带,这些TCP
数据报彼此之间携带的载荷都是可以在接受方自动拼起来的
- 这样就不像
UDP
存在传输的上限,使用UDP
传输大数据,就需要考虑调用这一次send
操作,参数是否超过了64KB
,超过了就不行 - 使用
TCP
的话就没关系,可以调用一次write
,也可以调用多次write
。无论怎么进行write
,在网络传输和对端接收的角度来看是没有任何差别的 - 如果多次
write
,传输的总数据量超过上述的4G
也没关系,这里的数据序号是可以再从 0 开始重新设置的
确认序号
确认序号的设定方式,和后发先至中发短信的例子,略有差别
- TCP 序号不是按照“一条两条”来编排的,而是按照“字节”来编排的
TCP 的确认序号这里,填写的是 1001
,接收方收到的数据的最后一个字节序号的下一个序号
- 表示的含义是
<1001
的序号的数据都收到了(TCP
序号是连续增长的) - 对于应答报文来说,“确认序号”就会按照收到的数据的最后一个字节序号
+1
的方式来填写 - 并且六个标志位中,第二个标志位(
ACK
)会设为1
- 普通报文的
ACK
为0
,应答报文的ACK
为1
- 如果是普通报文,序号是有效的,确认序号是无效的;如果是应答报文,序号和确认序号都是有效的
- 应答报文的序号是另一套编号体系,和传输数据的序号是不一样的
- 应答报文默认情况下是不携带数据的