网络原理
TCP/IP协议
应用层(后端开发必知必会)
这一层,也是有很多的现成的协议(比如HTTP),也有很多时候,是需要程序员自己定义协议.
举个例子,假如说我们现在要去设计一个外卖软件,我们打开外卖软件,显示商家列表,列表中有很多项,每一项都包含了一些信息:商家的名称,图片,好评率,距离你的位置,评分...
我们就会做出如下的设计:
1.明确当前请求和响应中包含哪些信息(这个是根据需求来的,不需要我们去想)
请求可能会有:用户身份,用户当前的位置,商家的名称,图片,好评率,距离你的位置,评分...
2.明确具体的请求和响应的格式本质上就是一个字符串,所以网络上传输的数据其实就是"字符串"(准确的说是二进制形式的字符串)无法直接传输一个"Java对象"(结构化的数据)这样的内容
序列化
Java写代码,都是各种对象,但是最后在发送数据的时候,就需要把对象转成字符串(二进制的格式)
相当于不管你这个对象几维,都是变成一维.
反序列化
在收到数据的时候,也需要把(二进制)字符串转换回对象.
把一维的字符串还原回N维
上述这样的格式约定,怎么做都可以,都是任意进行约定的,只要保证客户端和服务器遵守同一个约定即可!
总的来说就是:
1.明确传递的信息是有什么?
2.约定好信息按照什么样的格式来组织成(二进制)字符串
一些通用的协议格式
1.XML
是以成对的标签来表示"键值对"信息。同时标签支持嵌套,就可以构成一些更复杂的树形结构数据.
还是以外卖平台获取商家列表为例
请求:
<request> (开始标签)
<userid>1234</userid>
//(表示的是键值对结构)
// key:userid value:1234
<position>100 80</position>
</request> (结束标签:特征:有'/')
注意,这里的格式虽然与html比较像,但是xml里的标签都是程序员自定义的,而html里的标签,都是有一套标准规定好的(html可以视为是xml的特化版本)
响应:
<response>
<shops>
<shop>
<name>魏家凉皮</name>
<image>1.jpg</image>
<distance>1km</distance>
<rate>96%</rate>
<star>4.7</star>
</shop>
</shops>
</response>
优点
xml非常清晰的把结构化数据表示出来
缺点
表示数据需要引入大量的标签,看起来繁琐,同时也会占用不少的网络带宽.(国内最贵的硬件资源就是网络带宽(个人观点))
2.json
本质上也是键值对,看起来要比xml要干净不少(依旧已外卖平台为例)
请求:
{
useid:1234,
position:"100 80"
}
(注意:在json中,使用{}表示键值对,使用[]表示数组,数组里的每个元素,可以是数字,可以是字符串,还可以是其他的{}或者[])
响应:
[
{
name:'魏家凉皮',
image:'1.jpg',
distance:'1km',
rate:96%,
star:4.7
},
{
name:'杨国福麻辣烫',
image:'2.jpg'
distance:'1km'
rate:96%,
star:4.7
}
]
json对于换行不敏感,如果这些内容全都放在同一行,也是完全合法的.一般网络传输的时候,会对json进行压缩(去掉不必要的换行和空格),同时把所有数据都放到一行去.整体占用就更降低了.(会影响到可读性,不过也有很多现成的json格式化工具)
优势
相比于xml而言,表示的数据简洁的多.可读性非常好的,方便程序员观察中间结果,方便调试问题.
劣势
终究需要花费一定的带宽来传输key的名字的.
3.protobuffer
这个主要适用于,对性能要求比较高的场景.
(牺牲了开发效率,换来了运行效率)
(而对于软件而言,通常情况下开发效率重要)
谷歌提出的一套二进制的数据序列化方式.使用二进制的方式,约定某几个字节,表示哪个属性...
最大程度的节省空间(不必传输key,根据位置和长度,区分每个属性)
优点
节省带宽,最大化效率
缺点
由于是二进制数据,无法肉眼直接观察,不方便调试,使用起来比较复杂,需要专门编写一个proto文件(有一系列的语法规则,有点像Java/C++,但又不一样,再进一步的通过人家提供的工具,把proto文件转换成一些代码,再嵌入到程序中使用),描述数据格式是啥样的.
除了上述的这三种之外,业界还存在很多其他的序列化方式(数据组织格式),Java标准库中就提供了方式,其他的第三方库,提供的方式就更丰富了.
传输层
UDP
udp基本特点:无连接,不可靠传输,面向数据报,全双工
报文格式
所以在这里有效端口号的范围是0-65535,但其实0这个端口号我们通常是不会使用的,而且1-1024这个范围的端口号(知名端口号(known port)给一些名气比较大的服务器提前安排好了)系统通常都赋予了特定的含义,一般也不建议咱们使用
实际的校验和不仅仅是一个“长度”,而是根据数据内容来生成的,当内容发生改变的时候,就能够感知到出错,而UDP的校验和用的是CRC校验法,
具体做法是将UDP数据报中的每个字节都进行累加,把累加结果保存到两个字节的变量当中,加着加着可能就溢出了,但是溢出也无所谓,将所有字节都加了一遍最后得到了校验和,传输数据的时候将原始数据和校验和一起传递过去,接收方收到数据
,同时也收到了接收端发来的校验和(旧的校验和),接收方按照同样的方式在算一遍,就得到了一个新的校验和,如果旧的校验和和新的校验和相同就可以视为数据在传输过程中是正确的,如果不同,则视为数据在传输过程中是错误的。
数据相同=>校验和相同
校验和不同=>数据不同
校验和相同!=>数据相同(正好巧了,某个字节多了1,后面一个字节少了1,两者相加正好抵消)
确实不是那么严谨,但是在工程上也是足够用的(这种现象出现的概率太小了)。也有一些其他的算法进行校验,可以达到更高的精确度,但是要付出更多的代价。
TCP
报文格式无论是TCP还是UDP,端口号都是其中一个重要的部分,知道了端口号,才能进一步的确认这个数据报交给哪个应用程序。
注意:TCP的报头是"变长"的.4bit表示的范围:0-15,此处的单位是"4字节",把这里的数值*4,才是真正的报头长度,所以TCP的报头最大是15*4=60个字节.
TCP报头的前20个字节,是固定的(TCP报头的最短长度,20字节)选项部分可以有,也可以没有,可以有一个选项,也可以有多个选项.需要使用首部长度来确认报头到哪里结束了,载荷数据是从哪里开始?
保留位:现在虽然不用先占个位置,后面可能需要用(目前 tcp 也这么多年,大概率是不用的),也就是说,保留位是给未来留下了可以升级扩展的空间.
这里是6个标志位,每个标志位是1bit,表示非常重要的含义.
TCP原理
确认应答 (保证可靠性的核心机制)
为什么会有后发先置?
网络上从A->B中间的路径由很多(互联网初心,冗余,才不怕核弹),两个包,从A->B走的路线不一定相同!!另外,每个节点(路由器/交换机)繁忙程度不一样,此时,这样的转发过程,就也会存在差异,就和等红灯一样.
对于后发先置这种情况怎么办?
针对数据进行编号
由于TCP是面向字节流的,不是按照"条"为单位来传输,而是按照字节为单位来传输的.
和刚才例子中不一致的地方有两个
1.针对字节进行编号,而不是针对"条"
2.应答报文也是要和收到的数据的序号相关联的,但不是"相等"
只需要知道这一串字节的开始编号,以及数据的长度,每个字节的编号自然也就知道了,只需要在TCP报头中,把这一串字节的编号,表示出来,再结合报文长度,此时每个字节的编号就确定了
32为序号就是这一串字节第一个字节的编号.
确认序号的数值就是收到的最后一个字节的编号再加一,这个字段就是给应答报文使用的
那么现在就需要有办法可以区分出,当前这个报文是普通报文还是一个确认应答报文,此时就需要我们6个标志位了.
6个标志位
ACK为0表示这是一个普通报文,此时只有32为序号是有效的
ACK为1表示这是一个应答报文,这个报文的序号和确认序号都是有效的,确认报文的序号和正常报文的序号之间没有关联关系.
序号,是你自己这个主机发送的数据进行的编号.
也就是说,普通报文和确认报文的序号没关系,都是各自发各自的
核心:确认应答,是TCP保证可靠性的最核心的机制.
超时重传,也是TCP可靠性机制的有效补充
开始序号+长度-1=>最后一个序号.
而这个长度,在IP协议的报头中,是可以知道载荷多长的.IP的载荷长度-TCP报头长度=>TCP载荷长度.
上述的情况都是非常理想的情况,现实中不会出现这种理想情况
丢包
在网络上很可能出现,发一个数据,然后丢了.
如果设备太繁忙了,后面新来的数据等太久了,就可能被丢弃了.网络负载越高,越繁忙,就越容易丢包.
真的出现丢包,那怎么办?
超时重传
如果丢包了,收不到应答(等待一定时间,超过一定的时间之后),那么就需要进行重传.
超时重传,相当于针对确认应答进行的重要补充.
第一种情况:发的消息本身丢包了
第二种情况:应答报文丢包了
而这两种情况,发送方是无法区分的.既然无法区分,那就都重传.
但是如果这样做,对于第二种情况而言,B,同一条消息(1-1000)收到了两次!!,如果是聊天的消息,重复收到两条一样的消息,好像问题不大,但是假如我发的是钱呢?明明我这边充10元,结果由于重传,成了充20,那还得了?!,所以在这里,接收方收到数据之后,就需要对数据进行去重.把重复的数据丢掉,保证应用程序,调用inputStream.read的时候,读到的数据不会出现重复.
如何去重?如何高效的判定当前收到的数据是否是重复的?
其实并没有必要去比较TCP报文中的每一个字节,直接使用TCP的序号来作为判定依据.TCP会在内核中,给每个socket对象都安排一个内存空间,相当于一个队列,也称为"接受缓冲区",收到的数据,都会被放到接收缓冲区里,并且按照序号排列好顺序(而有序排列在这里是很有意义的,因为这个时候就可以保证就算出现后发先至的情况应用程序从这边读到的数据仍然是有序的,因此应用程序就不用考虑数据传输先后顺序的问题).
读到数据后,这个数据还会在队列里面吗?
这里又是一个生产者消费者模型,收到数据,接收方的网卡,把数据放到对应socket的接受缓冲区中(内核中),应用程序,调用read,就是从这个接受缓冲区,消费数据,当数据被read走了,就可以从队列中删除掉了.(read的时候,有两种固定的模式,可以读到就删除(默认情况),也可以是读到不删除)丢包本质上是一个"概率性"问题,假设丢包的概率是10%,传输成功的概率是90%,那么连续两次传输,都丢包的概率是多少?10%*10%=>1%,所以随着你重传次数的增加,总体都能传输成功的概率,是更大的.但是当重传次数达到一定的程度,也就放弃重传了,此时就会尝试"重置"TCP连接.此处就涉及到TCP复位报文
就是它
但如果网络已经出现严重故障,复位操作/重置操作,也无法成功,最终就只能放弃
连接管理
1.建立连接(三次握手)
(下面是已经自学过Netty的自己)
握手:handshake
发一个打招呼的数据(这个数据并不会携带业务信息),使用"打招呼"来触发"特定场景"
A和B完成建立连接的过程,就需要"三次"这样的打招呼的数据交互.
这样的四次交互完毕之后,连接就算建立好了。此时双方都已经保存了对端的信息了(看起来是四次,实际上中间这两次,能够合并成一次)
为什么要合并呢?其实就是为了节省资源,合并之后就节省了封装和分用的过程.降低了成本,提高了效率,原则:能合并就合并
ACK是应答报文.
SYN申请建立连接的请求."同步报文段"
如果这一位位1,就是"同步报文段",就是一台主机就要尝试和另一台建立连接
这个代码就是开始进行三次握手.这个new操作就完成了,三次握手也就完成了,三次握手,这是内核完成的工作,应用程序这里无法干预.同时,服务器这边针对三次握手进行配合,是不需要涉及到任何的应用层代码的,这要你这个进程是绑定了对应tcp端口就可以在内核中自动的配合完成三次握手,无论你服务器代码是怎么样写的
三次握手完成之后,客户端和服务器就都形成了"连接",此时accept就能够返回,从连接队列中取出队首元素,进一步的获取到其中的socket对象,来和对端通信.
三次握手也是保证可靠性的核心机制.tcp想要保证可靠传输,前提是你的网络得畅通,tcp的三次握手,就是要验证2网络通信是否畅通,以及验证每个主机的发送能力和接受能力是否正常
综上,三次握手的初心主要就是两方面:
1.投石问路,验证通信路径是否畅通,双方的发送/接收能力是否正常.
2.协商必要的参数,是客户端和服务器使用相同的参数进行消息传输.
2.断开连接(四次挥手)
四次挥手,有的时候是可以三次完成的,但是有的时候不行,中间这两次不一定能合并.
FIN的触发是由应用程序代码来控制的.调用socket.close(),或者进程结束,就会触发FIN,相比之下,ACK则是内核控制的.收到FIN就会立即马上返回ACK如果服务器,始终不进行close,会怎么样?客户端的连接就始终不关闭吗?此时,TCP状态,就处于CLOSE_WAIT状态
站在服务器的角度,虽然这个连接没有关闭,但是,这个连接其实已经不能正常使用了.针对当前的socket进行读操作,如果数据还没读完(缓冲区还有数据),是可以正常读到的.
如果数据已经读完了,此时就会读到EOF操作(对于字节路来说,返回-1.如果是scanner.hasNext就会为false)
针对当前socket进行写操作,就会直接触发异常.
无论如何,这个连接,已经是一个废人了,关闭是唯一的选择了.
更极端的情况,比如代码写出BUG,close忘记写了,此时客户端迟迟收不到FIN,也会进行等待,如果一直等,都等不到,此时就会单方面放弃连接(客户端直接把自己保存的对端的信息就删了,释放了)
目的:释放资源,能双方都顺利释放固然是最好的,但如果条件不允许,哪页不影响咱们单方面释放
如果通信过程中有出现丢包了,怎么处理
这里也是涉及到超时重传的.但如果重传仍然失败,而且连续多次,此时就会单方面断开连接
如果是第一组FIN/ACK丢失,A直接重传FIN即可.如果是第二组FIN/ACK丢失了,是FIN哪页好办,直接重新发一个FIN即可,但如果是最后一个ACK丢包了,那就比较难办了,此时有两种情况.
.如果最后一个ACK丢失,B就会自动传过来一个FIN.
.此时如果A已经把连接释放了,重传的FIN就没有人已可以进行ACK了.
因此,就需要让A在发出去最后一个ACK之后,让连接在等一会.(主要就是看等的过程中会不会收到对方重传的FIN).如果等了一段时间后,对方还是没有重传FIN,就认为ACK已经被对方收到了.此时A才能正确的释放连接.
那A在这里等多久才能释放连接呢?
等待时间(MSL)就是网络上任意两点之间传输数据的最大时间*2.
还有极端情况,比如A在等2MSL时间的过程中,B在反复重传FIN多次,如果这些FIN都丢了(理论上存在).如果真出现这个情况,当前网络一定是出现严重故障了=>这个时候,是不具备"可靠传输"前提条件的.英寸,A就单方面释放资源,也无所谓了.
TCP是如何实现可靠传输的
1.确认应答
2.超时重传
3.连接管理(三次握手,四次挥手)
以上这些机制都起到了作用,但是真正起决定性作用的,还得是确认应答.
三次握手,四次挥手是用来探路的,一旦路探完了,后续就没他什么事了.网络环境是多变的,这会畅通,不代表后面一直畅通.而确认应答,则是保证每次传输数据都是可靠的.
滑动窗口
提高传输效率(更准确的说,是让TCP在可靠传输的前提下,效率不要太拉胯)
使用滑动窗口,不能是TCP变得比UDP快,但是可以缩小差距.
一次性发出一组数据,发这一组数据的过程中,不需要等待ACK就直接往前发.此时,就相当于使用"一份等待时间"等四个ACK.
一次发多少数据不用等ACK这样的大小,称为"窗口"
窗口越大,此时批量发送的数据就越多,效率就越高,但是窗口不能无限大,如果是无限大,相当于完全不必等ACK,此时就和不可靠传输差不多了.
如果你无限大,接收方能不能处理过来?中间的网络设备能否承受住?都是未知数
收到2001这个ACK之后,A就立即发送5001-6000这个数据
此时A等待的ACK
3001
4001
5001
6001
还是等4条ACK.(窗口大小还是一样大,但是往后挪了一个格子),直观上看起来,就是"窗口"往后滑动了一步.滑动窗口,是一个"形象的比喻",实际上本质就是批量发送数据.
现在按照这种批量的方式传输,中间都报了怎么办?
对于TCP来说,提高效率,必然不应该影响到可靠性!!
现在我们考虑两种情况
<1>.数据丢了
虽然A后续给B的数据都顺利传过去了,但是只要是1001这个数据没有,B始终都会向A所要这个1001的数据.(返回的ACK确认序号,都是1001)
当A连续几次都收到了来自于B的索要1001的素具,A就明白了,1001是丢了,A就会重新传输1001-2000这个数据.
当重传的1001到达B之后,B返回的ACK就是7001了
如果接受缓冲区,这一块是少了的,返回的ACK就会始终所要1001这个数据报,一旦1001这个数据报被补上了,此时,1001-2000后面的数据报都不必重新传输了(都在缓冲区里待着呢)
接下来就看后面的数据哪里是否还有缺失.如果有缺失,所要对应的数据,如果没缺失,直接索要缓冲区最后一条数据的下一个即可,此时,就相当于是使用最小的成本,来完成了这个重传数据的操作(只是把丢的数据重传了,其他数据都没有重复操作)
我们把这种传输方式称为:快速重传.
快速重传=超时重传+滑动窗口(其本质还是超时重传)
滑动窗口也不是说,使用TCP就一定会涉及,如果你通信双方大规模传输数据,肯定是会用到滑动窗口(此时按照快速重传来工作).如果你通信双方传输的数据规模比较少,这个时候就不会滑动窗口了(仍然按照之前的超时重传来进行工作).
<2>.ACK丢了
如果ACK丢了,不用做任何处理,也是正确的!!!
就比如上图,1001这个ACK丢了,但是后面的2001的ACK收到了(2001确认序号,表示2001之前的数据都收到了!!!也包含1-1000,也就是说,虽然A没有收到1001这个ACK,但是2001这个ACK涵盖了1001的含义)
流量控制
滑动窗口,窗口越大,传输效率越高.但是窗口也不能无限大.如果窗口太大了,就可能是接收方处理不过来了.或者是使传输的中间链路出现处理不过来的情况,那么这样就会出现丢包,就得重传了,窗口大并没有提高效率,反而还影响效率.
流量控制,就是给滑动窗口踩踩刹车.避免让窗口太大导致接收方处理不过来.
因此,流量控制,就是根据接收方的处理能力,来限制发送方的发送速度(窗口大小)
如何衡量接收方的处理速度?
此处就使用接收缓冲区剩余空间大小来作为衡量指标.
此处,就会直接把接受缓冲区的剩余空间的大小,通过ACK报文反馈给发送方,作为发送方下一次发送数据,窗口大小参考依据
虽然A不再发送数据了,但是也不知道B这边啥时候能腾出来空间.就会周期性的发送"窗口探测包"(不会携带具体的数据),知识为了触发ACK(查询当前接收缓冲区的情况),一旦发现不是0了,皆可以继续发送了.
接收方就可以根据窗口大小,来反向限制发送方传输速度了
拥塞控制
说到底,我们在这里的总的传输效率,是一个木桶效应,取决于最短板
具体怎样去衡量中间设备的转发能力呢?
此处并不会针对中间设备的转发能力进行量化,把中间的设备都看成一个整体,采取"实验"的方式,动态调整
线性增长,也是增长,就会使发送速度,越来越快.快到一定程度,接近网络传输的极限,就可能会出现丢包了.
实际发送方的窗口 = min(拥塞窗口,流量控制窗口).
拥塞控制和流量控制,共同限制了滑动窗口机制.就可以是滑动窗口,能够在可靠性的前提下,提高传输效率了.(保证可靠性的机制)
延迟应答
捎带应答
四次挥手为什么说可以变成三次,那是因为由于延时应答以及捎带应答所造成的合并
面向字节流
粘包问题

粘包问题,不仅仅是TCP才有的,只要是面向字节流的机制(比如:文件)也有同样的问题,解决方案也都是一样,要么使用分隔符,要么使用长度.
尤其是在自定义应用层协议的时候,就可以使用这样的思想来解决问题了.
TCP异常处理
网络本身就会存在一些变数,导致TCP连接不能正常工作了.
1.进程崩溃
进程没了=>PCB没了=>文件表述符表被释放了=>相当于调用socket.close()=>崩溃的一方就会发出FIN,进一步的触发,四次挥手,此时连接就可以正常释放了.(socket在系统内核中也是一个文件,也会被放到文件描述符表当中)
2.主机关机(正常步骤的关机)
3.主机掉电(拔电源,没任何反应的空间)
"心跳包"和咱们前面说的"流量控制"窗口探测报文,是一个东西.
4.网线断开
总结TCP特性