1.前置知识
定义应用层协议
1.确定客户端和服务端要传递哪些信息
2.约定传输格式
网络上传输的一般是二进制数据/字符串
结构化数据转二进制/字符串 称为序列化
反之称之为反序列化
下面就是传输层了
在TCP/IP协议中,我们以 目的端口,目的IP 源端口 源IP 协议号这样一个五元组来表示一段通信
可以在cmd命令行窗口中使用
netstat -n
来查看计算机上正在通信的TCP协议应用程序
端口号划分
端口号标识了主机上通信的不同应用程序
0-1023 知名端口号
简单列几个知名服务器(这里的知名是站在当年的角度来说的)
•ssh服务器,使⽤22端⼝
• ftp服务器,使⽤21端⼝
• telnet服务器,使⽤23端⼝
• http服务器,使⽤80端⼝
• https服务器,使⽤4431024-65535 操作系统动态分配的端口号,客户端程序随机分配空闲的端口号就是在这个区间内
2.UDP报文
回顾UDP协议的特点
1.无连接
2.面向数据报
3.全双工
4.不可靠传输
含义:丢包了也不知道 我不管你接受方收没收到
以上是UDP报文的组成结构
16位源端口号 : 2字节
16位目的端口号: 2字节
16位UDP长度:2字节
也表示了UDP报文的大小,表示的范围就是 0 - 65535 也就是约等于64KB
16位UDP检验和:网络在传输的过程中可能会坏掉了,用来证伪
常见校验方式:
CRC 循环冗余算法
这里我可以取出数据包的数据逐字节的加等,最后判断传输接收方计算的数据和发送方计算的数据是否相等,相等就说明没有发生比特翻转,不相等就发生了比特翻转
但是多个发生翻转,最后结果一样就无法判断了,只能说这样的概率比较小,如果想更好的判断,就需要使用更精密的算法来计算了
md5算法
特点
1.定长 常见的有 2/4/6/8 字节版本
2.不可逆 因为用一个原来的字符串来计算md5值,其中有部分数据是丢失了的,这也导致了其的不可逆性
3.分散 一个比特不同可能导致最终结果相差很大 这样的特性也就决定了它可以来作为哈希表的一个实现
sha1算法:和上面类似,更安全
我们知道数据一般是使用光,磁,电磁波信号等方式传输,但是传输过程中高低电平可能发生翻转,称之为比特翻转,其实虽然现在的有了很多保护措施,但是还是有可能发生这样的事情的
这里一旦数据超过了64KB,就会导致数据被截断
这里的数据大小也认为是不超过64KB,因为前面的8字节相对来说可以忽略不计
如果我们要传输超过64KB的内容就需要应用层来手动分包了
这里的64KB相对现在来说已经算捉襟见肘了,但是为什么迟迟不升级其实并不是技术手段不到位,而是政治问题,这里面临着每个操作系统都得修改否则就无法通信了
3.TCP报文内容
TCP的报文结构相对来说就比UDP复杂的多了
16位源端口号:同UDP
16位目的端口号:同UDP
32位序号:用来保证发送报文顺序
32位确认序号:用来保证应答报文顺序
4位首部长度: 0 - 15 这里单位是4字节 也就是表示了0 - 60 字节的报头长度
保留位(六位):用于扩展TCP报头的长度(确保兼容性可以保证)
标记:URG/ACK/PSH/RST/SYN/FIN :后续介绍,他们分别代表不同的含义的报文
应答报文的ACK就是1,平时是0
请求连接报文是SYN为1
请求结束时FIN为1
RST:请求重新建立连接的时候为1
URG:紧急指针是否有效
PSH:催促接收端从缓冲区把数据读走
16位窗口大小:后续说明,是TCP的一个保证传输效率的策略,用于滑动窗口
16位检验和:和上述相同
16位紧急指针:标识哪部分数据是紧急数据
选项:选项可以有,也可以没有,可以有多个,也可以只有一个 前面固定 如果首部长度是15就表示了,前面固定20字节,选项部分40字节.
数据:要传输的一些数据
4.TCP的一些特性
1.确认应答机制
确保这里可靠性的最关键的机制
试想一下这样的消息
但是在网络上也会发生先发后至的效果,例如这样
为啥会产生这样的效果呢???
因为网络传输的路径错综复杂,可能两者的传输路径是不同的,就像同一个目的地和出发地,路径不同,到达的时间啊也就不同
为了解决这样的问题,就引入了序号和确认序号
也就能解决问题了
真实的情况是: TCP是面向字节流传输的
实际上TCP的序号和确认序号都是以字节来编号的
类似于这样的方式,由于字节的序号是连续的,报头只需要保存第一个字节的序号即可
比如 1-1000 保存1即可
应答报文的ACK标志位就是1
应答报文的序号是将发送的报文+1得到的,假如发送的数据是1-1000,接收方返回的应答报文就是确认序号就是1001
两种理解方式
1.小于1001的数据都收到了
2.你下面要发送1001开始以后的数据了
2.超时重传机制
网络上存在丢包的情况
这里发送方收到的ack报文可能会丢包,发送方发送的请求数据报也可能会丢包
为啥会存在丢包??
网络传输的路线错综复杂,一个设备不仅仅支持这一次通信,还要支持千千万万那主机之间的通信
假设中间某个网络设备突然负载很很高,网络上短时间可能有大量的数据包要经过这个设备转发,超出了这个设备的极限,多出来的数据包就被丢包了
TCP协议就是以防这种不可控的因素,期望是在丢包这种客观因素存在的背景下,能尽可能的把包给传过去.
数据丢包的情况
试想一个这样的场景:发送方发了一个数据之后,要等
等的时间里,收到了ack报文
这里假设超过了等待的阈值还没有收到ack报文的话,发送方就认为传输的数据丢包了
此时发送方就会把数据重传一次(没有问题)
ack丢包的情况
这样发送方其实也会没收到数据,重传(接收方收了两次)
试想这里万一是扣款请求呢???这就会有问题 .
TCP这里是有接收缓冲区的,还有发送缓冲区的
接收缓冲区是有去重机制的,如果已经有了就会直接丢弃
接收方如何判断是否重复?
根据数据的序号
1.数据还在接收缓冲区里,和缓冲区的所有数据序号比较
2.序号小于当前接收到的最新序号
UDP协议也是有接收缓冲区的
超时重传也是有限制的,到一定的程度还是没有ack就会尝试重置连接,重置也失败就会直接放弃连接
超时重传的时间阈值不是固定的,随着重传的次数增加,重传时间的阈值也会越来越长
超时重传的
超时重传的时间一般是秒或者是毫秒级别的数据,对于计算机来说是一个很大的数字
电流在网线中读取的速度是电场移动的速度,不是电子运动的速度,比如传输一个电影不是靠一两个电子光子就行的,而是需要持续不断的光信号才行,这个过程中编码解码也会消耗时间,经过路由器交换机也会消耗时间
3.连接管理机制(重点)
我们上文说到的TCP是有连接的
socket = new Socket(IP,Port)这个操作就是在建立连接
当服务器accept之后其实就是连接完成
这里的连接是在操作系统内核中完成的
三次握手
内核中建立连接的方式就是使用三次握手操作
这里的SYN实际上是一种特殊的报文段,不包含载荷部分,会有报头段(包含TCP/IP/以太网数据帧),对应的TCP数据报头的标志位SYN标志为1,不包含任何的业务逻辑,就形象的称之为"握手"
此时只要服务器空闲就能建立连接,高负载的情况下可能会无法响应
正常情况下就会直接返回ACK,并同时发送一个SYN
客户端又会返回一个ACK
为啥进行握手??意义何在??
1.三次握手去确定通信链路是否畅通
类似于地铁早上空车跑一趟看看是否正常
2.看看通信双方的发送接收能力是否正常
试想一个形象的场景
我和哥们打csgo在试麦和耳机
我:听得到不哥们?
哥们:能 你听得到我说话不?(我的麦克风是好的,他的耳机是好的)
我:okok,直接开吧. (他的麦克风是好的,我的耳机也是好的)
3.协商一些参数
对可靠性有一定的保证,但是核心还是靠超时重传和确认应答来保证
TCP也是有很多参数需要协商的,往往是在"选项"部分体现的
比如通信序号,往往不是从1开始的,背后有一系列分配策略
即使是同一个客户端和服务器,每次连接,开始的序号都不同
是为了避免"前朝的剑来斩本朝的官"
这里比如第一次连接传输的数据迟迟没到,接着双方就挥手离开了
而后面到达对端的时候已经改朝换代了
此时这份数据就应该被丢弃
数据报传输的时候一般是根据 ip+端口来识别的
对于客户端来说,端口相同的概率是很低的,服务器倒是有可能
怎么识别"前朝的"数据包?
正常的数据包都是从开始序号往后一次排的,偶尔丢包差异不会很大
此时前朝的包和本朝差异就很大
面试题:为啥一定是三次握手,四次行不行??两次行不行??
四次肯定行但是没必要,因为网络传输也需要消耗一定的网络带宽等硬件资源,这里四次多余了,两次是肯定不行,服务器无法确定自己的发送能力和对方的接收能力是否正常,就破坏了可靠传输的前提,所以我们还得来一个握手,让服务器知道自己有没有这个能力
四次挥手
连接其实就是让通信双方持有对方的一些信息,以一定的数据结构储存起来
所以这里断开连接的本质就是把对端的信息,从数据结构中删除/释放掉(逻辑上的删除)
两种方式(双方同意断开/单方面同意断开)
四次挥手这里是双方同意的场景,为啥是四次呢??不是跟刚刚的三次握手一样吗,中间两次为啥不合二为一呢??
四次挥手也可能可以合二为一,但不一定,三次握手是100%可以合并
因为三次握手第二个ack和syn是同步触发的,应用程序无法干预,是由内核操作的
四次挥手的第一个fin的触发是由于socket.close()
而服务器对于fin的返回也是socket.close()产生的
这里在close的产生时间可能比ack的产生晚的多,这就不能合并了,就要分成两次来传输了
但是如果当时时间很小,可能触发捎带应答,延迟应答机制,这里就可以合并了
对于互联网大厂,一般对于系统的可用性有很高的要求
使用冗余的连接
一个服务器有多个镜像,多个方式可以到达,避免单点故障
这里的客户端发给任何一个镜像服务器都是一样的结果
这里就可以面对高并发的场景了,降低每个服务器的负载,通过冗余的方式,确保任何一个机器出问题,都可以有机器能顶上
连接过程中也涉及TCP的状态转换
LISTEN 表示服务器这里创建好ServerSocket了,并且绑定端口号完成
ESTABLISHED(确立的) 表示连接建立完成,三次握手完毕
CLOSE_WAIT 表示接下来我的代码中需要调用close方法,来主动发起fin了,就是收到对方的fin之后
TIME_WAIT 表示本端给对方发起fin之后,对端也发送fin 了,本端会进入TIME_WAIT
给对端的ack重传留有时间
谁主动断开连接谁进入TIME_WAIT 确保重传
谁被动断开连接谁进入CLOSE_WAIT
正常情况下CLOSE_WAIT会瞬间转换成LAST_ACK
如果出现了CLOSE_WAIT意味着代码出问题了,比如忘记关闭socket了
状态转换详图
如果此时没有收到最后一个ACK,就会重传
此处的TIME_WAIT等待也不是无休止的等待
最多等待两倍的MSL,MSL表示客户端到服务器的最长时间
常见的是1min,一般是拍脑门设置出来的
4.滑动窗口机制
滑动窗口也是TCP的一个非常有特点的机制
前三个特点是来保证可靠性
确认应答,超时重传,连接管理 来保证可靠性
可靠性确实是好,但是也付出了一定的代价(单位时间能传输的数据量变少了)
确认应答的机制使得每发一个数据都得等一个ack然后才会发送下一个数据
此时的等待时间是比较低的
这里的滑动窗口就是希望在保证可靠传输的同时,希望效率能高点
本质上就是批量传输
在发送一个数据报之后不去等待ack而是继续发送第二份数据
这里就是先发若干条数据报(有大小限制,称之为窗口大小)再去等待ack,多次等待时间使用同一份时间,缩短了等待时间
下面我们来看图
先传输出四份数据之后
此时开始等待ack,回来一个ack就继续发送一个数据包
实际上ack返回的速度也很快,就会有一个滑动的效果
丢包情况
1.ack丢失
这种情况无需进行任何处理,不需要重传
因为ack报文中的确认序号会起作用,假设1001的ack丢失,但是3001收到,其实就表示3001之前的信息我都收到了
2.数据丢失
这里发送方在多次收到索要1001的ack的时候,就知道1001是丢包了,就开始重传一次1001
但是后面返回的确认序号是7001而不是2001
滑动窗口中也有确认应答,只是把等待策略稍作调整
批量的前提是短时间传输了很多数据
如果数据比较少就退化成确认应答机制
确认应答 对应 超时重传
滑动窗口 对应 快速重传
5.流量控制(流控)
通过滑动窗口来提高传输效率
滑动窗口越大,就要有更多的数据在同一块时间等待,效率就越高
窗口能无限大???
不能,因为窗口大了就无法保证可靠性了
发送的速度也不能无限快
因为发送的太快,接收方接受不过来(接收方缓冲区满了)就也会产生丢包的情况
让接收方缓冲区的剩余空间来影响发送方的发送速率
会通过TCP中的窗口大小来反馈发送方的速度
这个字段在普通报文中无意义,在ack报文中才有意义
比如现在缓冲区还有3000字节的大小,这里每个数据报是传输1000字节的数据,就反过来控制这个窗口大小还能放多少
此时再发送一个就变成2000,以此类推,当接收方缓冲区为为0时,将停止发送数据报
注:这里还有一个参数叫窗口扩展因子 假设为2
此时窗口就会变成 原大小*2^窗口扩展因子
'
如果这时候超过了超时重发的时间还没所有收到任何窗口更新的消息,就会发送一个窗口探测的包,一旦查询出来的结果已经是非0了就让发送方继续发送数据包
6.拥塞控制(传输途中)
不过这里是从接收方的角度来制约发送方的发送速率
发送方和接收方之间也是有很多通信路径的
假设这里接收方处理很快,但是中间出现了拥堵情况,就很容易丢包,这里发送方也不应该发的太快
这里的处理方式就是慢启动机制,先发送少量的数据,探探路,看看当前网络的拥堵情况,再决定按照多大的速度来传输数据
引入一个概念: 拥塞窗口
如果出现丢包的情况就视为中间路径出现拥堵,就需要减小窗口大小
如果没出现丢包,那么就视为中间路径不存在拥堵,就需要增大窗口大小
按照上述策略就可以让发送速率动态变化
拥塞控制和流量控制,使用哪个窗口呢???
哪个产生的窗口小就使用哪个
流程
1.慢启动,先以少量数据开始传输
2.如果没有出现丢包的情况,就说明网络是畅通的,就要增大窗口大小,一开始使用指数级别增长
3.指数增长不会一直保持,会到一个阈值停止(以防突然产生网络拥堵)
此时指数增长就成了线性增长,这样就可以让步窗口保持一个比较高的速率
如图所示
以前使用的Tahoe版本已经被废弃了
之所以有这个更迭,是因为网络环境的改变,TCP当年是发布于1981年,当时的网络无论是带宽,通信质量还是稳定性都是和现在无法同日而语的
当年是非常容易出现网络拥堵的,从而出现大规模的网络波动,一旦出现拥塞,此时网络带宽就非常捉襟见肘了,按照比较小的速率发送数据是更稳健的做法
现在之前的问题就不存在了,现在是出现三个重复ack的时候发现丢包窗口就缩小一半,然后开始重新线性增长,称之为"快恢复"
7.延迟应答
也是基于滑动窗口,对于效率做提升
结合滑动窗口以及流量控制,通过延时应答的方式,让反馈的窗口搞大一点
'
接收方收到数据的时候不会立即返回ack,而是稍等一下,等一会再返回ack
等了这一会也是相当于给接收方的应用程序这里腾出更多的时间来消费ack
如果是立即返回,这时候接收方的缓冲区大小就是非常小的,就可能不能直接发送相对应的数据包,但是如果等待一段时间,比如100ms,剩余的空间就会更大,返回的窗口也就是一个相对较大的值了
正常是每个数据都要有一个ack,但是ack丢包是无所谓的,只要能收到一部分其实就可以,所以这里我们就是隔几个数据返回一次ack,这样也节省了发送ack的时间
一般数据包取2,延时时间取200ms
8.捎带应答
基于延时应答引入的机制,也是用来提升传输效率的
尽可能得把能合并的数据包进行合并,从而起到提高效率的效果
试想一个这样的场景
客户端向服务端发送一个请求request
服务端返回一个ack
服务端返回一个response
客户端返回一个ack
这里服务端返回ack的时候由于延时应答策略,这里就可以将response和ack合二为一,也不会产生什么冲突
能否合并取决于服务器处理完的数据在延时时间之内,就可以合并
9.面向字节流粘包问题
这里的"包"指的是TCP载荷中的应用数据包
tcp传输的数据到了接收方之后,接收方就需要根据socket api来read出来
由于read很灵活,可能会使代码中无法区分当前的数据是从哪到哪是一个完整的应用数据包
假设我这里发送三个请求分别为
aaa bbb ccc
在缓冲区里面就变成了 aaabbbccc
此时应用程序调用read来读取数据,由于这里是基于字节流的,读取过程非常灵活
多个应用层数据包混淆不清了,称为粘包
粘包问题不是TCP专有的问题,而是所有面向字节流都是有这种同样的问题的
解决问题的关键就是"明确包之间的边界"
方案
1.特殊符号作为分隔符
2.指定出包的长度,比如在包的一开始腾出一个空间来保存整个数据的长度
注:上述问题都是应该在设计应用层协议的时候考虑进去的
对于UDP只需要约定一个数据报单独只承载一个应用层数据包,无需额外手段区分
我们之前谈到的 XML JSON Protobuffer都可以解决问题
10.异常情况处理
考虑比丢包更严重的情况,甚至说网络出现故障的情况如何处理
1.有一方出现了进程崩溃
不管是进程崩溃还是正常结束,都会触发回收文件资源,关闭文件的效果(系统自动完成)
这时候就会触发四次挥手(fin)
虽然进程崩溃了,但是TCP连接还在(连接时间可以长于进程的生命周期)
仍然可以进行四次挥手,和正常的没啥区别
2.有一方出现关机(正常流程关机)
关机的时候会强制结束所有进程(类似上述的强杀进程)
会触发四次挥手
点了关机之后四次挥手不一定能挥完
如果挥的快,能够顺利挥完
本端和对端都能删除保留的连接信息(核心任务)
挥的不快,也能把第一个fin发送给对端,告诉对端我要结束了
对端收到fin也要进入结束流程了
不过发送的fin就不会收到ack了,进行超时重传也发现没有ack
此时就会单方面释放连接信息
3.其中一方出现了断电
这个时候肯定是发送不了fin了
1.断电的是接收方
此时发送方就会发现没有ack了,触发重传机制,重传几次还是不行
触发TCP的复位连接,发送方发送复位报文段
RST报文
此时重置了也不行就直接单方面放弃连接
URG:带外数据
PSH:催促对方快点发消息
2.断电的是发送方
接收方无法区分发送方是g了还是暂时没发送数据
接收方没有收到对方的信息就会发送一个"心跳包"
也是不携带引用层数据的特殊数据包
心跳包的发送是周期性的
如果没有心跳了,就会直接尝试复位并且单方面释放连接了
4.网线断开
此时就是3中的两种情况结合了