目录

JavaEE-9.网络原理TCPIP

JavaEE—9.网络原理TCP/IP


应用层

自定义应用层协议

应用层是咱们日常开发中最常用到的一层

主要涉及到两种

1.使用大佬们已经创建好的应用层协议

2.自己定义应用层协议

自定义应用层协议具体做些什么:

1.明确前后端交互过程中需要传递哪些协议

        举个例子,打开一个外卖软件

        打开软件需要展示一个"商家列表"

        此处就是需要确定传输的信息是什么

        a)请求:用户是谁(用户的ID),用户所在地位置

        b)响应:商家列表,包括多个商家,每个商家中又有商家的名字图片,距离,评分等等

2.明确组织这些信息的格式

        针对信息组织的格式也可以有很多种,使用那种方式都是可以的

        但是得确保前后端是同一种方式

        举个例子:使用行文本的方式组织上述的数据

        a)请求

        用户id,用户位置\n

        1001,E45N60\n

        b)响应

        商家的id,商家的名称,商家的图片地址,商家的距离,商家的评分

        2001,肯德基,http://image1.com,1.5km,4.5;2002,麦当劳,http://image2.com,1.7km,4.9;\n

关于组织的格式,上述"行文本"的方案在实际开发中很少会这么高

1.xml(传统的方法)

通过"成对的标签"来表示"键值对"信息

https://i-blog.csdnimg.cn/direct/803bd28f40754f4c99b0e45e62680dba.png

xml进行网络传输的时候,有一个明显的缺点,在传输的时候,消耗大量的带宽

网络通信中,带宽是一个非常昂贵的硬件设备

可以使用xml来传输网络数据,也可以作为程序的配置文件

xml和html很相似,html也是成对的标签,夹住一些内容

xml的标签(键值对)都是程序员自定义的

html的标签都是固定的(已经有一套标准,约定好那些标签是合法标签,这些标签都是啥含义)

2.JSON     当前更主流的网络通信的数据格式

        相比html来说,jason的可读性是很好的,同时也能节省一定的带宽

https://i-blog.csdnimg.cn/direct/fd37e9ad5cb5421f82f114062c721293.png

json也是"键值对"的格式

键和值的使用:分割,

键值对之间使用,分割,

所有的键值对都使用{}括起来

但是可读性不一定完全好,也会存在一些极端情况

{“userId”:1001,“position”:“E60N45”}这种,全部放在一起

3.yml(yaml)   强制要求了数据组织的格式

https://i-blog.csdnimg.cn/direct/f09c39f6de894faf9b42ad7d2eab3e00.png

yml强制要求了键值对必须独占一行,并且"嵌套"结构,必须通过缩进来标识

强制要求我们写一个可读性非常高的方式

4.google protobuffer

前三个方案都是关注可读性

protobuffer关注性能,牺牲了可读性(通过二进制的方法组织数据的)

而protobuffer直接通过"位置"约定字段的含义,不需要传输key 的名字,也会针对传输的数据进行二进制编码,起到一些"压缩"效果

        极大的缩减了要传输的数据体积==>带宽消耗越小=>效率越高

二进制数据无法用肉眼读出来,调试相关程序的时候,就会比较麻烦

自定义应用层协议

1)确定传递的信息

2)确定传递的数据格式

传输层

UDP/TCP

端口号:

        端口号是一个整数,用来区分不同的进程,同一时刻,同一个机器上,统一协议,同一个端口只能被一个进程绑定,但是一个进程可以绑定多个端口

        端口号是通过2个字节的无符号整数来表示的,取值范围是0-65535

        实际上0这个端口比较特殊(随机设定空闲端口),不会去使用的

        1-1023端口属于已经被预定好了的知名服务区,已经提前被预定好了,这种端口被称为"知名端口号",日常开发中会避开这些端口

        例如 80=>http        443=>https        22=>ssh

对于刚刚提到的一个进程可以绑定多个端口

例如:

业务端口:编写服务器肯定需要先绑定至少一个端口和客户端进行交互

管理端口:服务器运行过程中,希望能对这个服务器的行为进行一些"控制",比如让服务器重新加载某个数据/某个配置/修改服务器的某个功能,通过这个端口编写一个客户端,给服务器发送一些控制类的请求

调试端口:需要针对服务器的运行状态进行检测和调试,需要查看服务器运行中的某个关键变量的数值,(千万不能使用调试器来调试,使用调试器一旦调试这个任务就会使服务器的一些线程被阻塞住了,无法给客户端提供服务了),让服务器绑定另一个端口,然后实现一些相关的,打印关键变量的逻辑,让客户端发送对应的调试请求

UDP

学习一个网络协议,主要就是学习"数据格式",“报文格式”

https://i-blog.csdnimg.cn/direct/a61225979e2b47979217370922fe0251.png

实际上的UDP的数据,并没有"换行"动作

https://i-blog.csdnimg.cn/direct/e39060afcb5544ba99a6e7f738e3889a.png

报头总共八个字节,固定长度,报头中的四个字段,没有指定分隔符

而是通过固定长度来进行区分的

使用两个字节的长度来表述端口号

网络通信中涉及到的四个关键信息

源IP,目的IP,源端口,目的端口(发件人地址,收件人地址,发件人电话,收件人电话)

报文长度

UDP报文长度就是通过报头长度+载荷长度

报文长度的基本单位是"字节"

比如报文长度1024 =>整个UDP数据包就是1024字节

由于是使用2个字节 来表示这个长度 =>最大值65535 =>64KB(65535/1024)

64KB在如今十分的小,如果UDP协议传输一个很大的数据,就会变得非常麻烦

如果传输的大小大于64KB,会被截断

2个解决办法:

1.把一个大的数据报拆成多个,分别进行传输

不太可靠,实现分包,实现组包的过程中充满不确定性非常复杂

2.直接使用TCP

TCP对于长度是没有限制的

TCP自身也带有可靠传输这样的机制,对于整体的通信质量来说也是有利的

代码修改的成本比较低

校验和

前提:网络传输的过程中,非常容易出现错误,电信号/光信号/电磁波=>收到环境的干扰,使里面传输的信号发生改变,电厂和磁场等都是能够相互影响的

1 -> 0

0 -> 1

这种被称为比特翻转

校验和存在的目的就是为了能"发现"或者"纠正"这里出现的错误

就可以给传输的数据中引入额外的信息用来发现/纠正传输数据的错误

校验和具体是怎么工作的?

UDP中使用2个字节作为校验和

UDP使用简单有效的方案,CRC校验和冗余(循环冗余校验)

把UDP数据报整个数据都进行遍历,分别取出每个字节,往一个 两个字节的变量上进行累加

由于数据很多,加着加着会出现结果的溢出,溢出也没关系,重点不关心最终加和是多少,而是关系校验和的结果是否在传输中发生改变

https://i-blog.csdnimg.cn/direct/d990642abd3b4a979a20d607199bc1b8.png

在计算校验和的过程中,是否可能出现两个不同的数据生成的校验和相同呢?

可能是存在的,概率比较低

除了CRC之外,还可能会用到一些其他的算法,实现校验和,另外两个典型的算法:MD5和SHA1

MD5算法本质上是一个"字符串算法"

背后的实现过程是一个数学过程,就是套公式

相比于md5的计算细节,更关心特点

1.定长:无论输入的字符串长度多长,算出的md5的结果都是固定长度=>适合做校验和算法

2.分散:输入的内容哪怕只有一点点发生改变,得到的md5的值都会相差很多=>适合做hash算法

3.不可逆:根据输入的内容,计算md5,非常简单,如果已知md5的值,还原出原始的内容,理论上是不可行的=>适合作为加密算法

https://i-blog.csdnimg.cn/direct/2fc8a41fba2b47d7979e63ed84772108.png

得到的结果是整数,十六进制

md5的解密网站也并非是真的解密,而是他把大量常见的字符串md5值,通过提前哈希表的方式存储好了,输入md5值查表,找到原始的key

SHA1和md5非常相似,也是这三个特点

TCP协议

https://i-blog.csdnimg.cn/direct/d91f16e695f942e6823520af88760979.png

TCP报头的长度

UDP协议固定是8个字节,TCP来说报头长度是可变长的

4为首都长度:

4个比特位表述的数据范围是多少

0000->11111(0 -> 15)

此处的基本单位是4字节,不是字节

所以最大长度为4*15=60

保留位(6位)

这个是现在不用,但是给这个东西申请下来已备不时之需,未来某一天TCP需要新增属性,或者某个属性长度不够用了,就可以把保留位拿出来用作对应的作用

https://i-blog.csdnimg.cn/direct/98dc200f98d94851850788238368f853.pngTCP中核心的属性,这几个标志位和TCP的一些核心机制密切相关

TCP的相关机制

TCP的基本特点:有连接,可靠传输,面向字节流,全双工

TCP最核心的机制就是"可靠传输"

不能做到100%送达

尽可能感知到对方是否收到

1)能感知到对方是否收到

2)如果发现对方没收到就要进行重试

TCP的核心机制(1) 确认应答

感知对方是否能收到,就是对方告诉你一声收到了

应答报文acknowledge,缩写位ack

后发先至

如果我发送多个消息的时候,可能会存在歧义

也就是"后发先至"

后发先至是客观的情况无法改变,可以给传输的数据添加编号通过编号可以区分出数据的先后顺序

由于TCP是面向字节流的,实际上编号不是第一条第二条这种方式编排的

而是按照第一个字节,第一百个字节

每一个字节都有一个独立的编号,字节和字节之间编号是连续的,递增的

按照字节编号这样的机制,就成为TCP的序号

只需要知道数据部分的第一个字节序号,就知道后续所有字节的序号了

序号

序号只是针对TCP数据报携带的载荷来进行编号的(TCP报头不参与)

序号描述了载荷部分的第一个字节的序号是多少

TCP是以字节流的基本传输单位的

一个TCP数据报和下一个TCP数据报携带的数据天然就是"可拼装的"

比如要传输一个特别大的数据,传输的过程中,本身就会通过多个TCP数据报来进行携带

这些TCP数据报彼此之间携带的载荷都是可以在接收方自动被拼起来的

确认序号

确认序号的设定方式,和前面发短信的例子略有差别

如果A客户端向B服务器发送了1-1000序号的数据,那么B客户端的应答报文中的确认序号是什么?

实际上TCP确认序号是1001,接收方收到的数据最后一个字节序号的下一个序号

表示的含义是<1001的数据都收到了
对于应答报文来说,确认序号就会按照收到的数据的最后一个字节序号+1的方式来填写

应答报文的另外六个标志位中,第二为的ACK会设为1

普通报文的ack是0,应答报文的ack为1

如果是普通报文,序号是有效的,确认序号是无效的

如果是ack应答报文,序号和确认序号都是有效的

但是应答报文的序号这是另一套编号的体系,和传输的数据的序号不是一套的

应答报文默认情况下不携带数据

再谈后发先至(直到起始数据到了才会真正读取数据)

        TCP可以针对接收方收到的数据,进行重新排序,确保应用程序read的数据一定是和发送方法的数据顺序是一致的

接收方这边操作系统内核里会有一段内存空间,作为"接受缓冲区",收到的数据就会先在接收缓冲区中排队等待,直到开头的数据到了,应用程序才会真正的开始读取数据

        接收方这边调用read的时候如果没有数据,就会阻塞等待(前面的代码写的是scanner读取,本质上就是调用InputStream.read)

        如果刚开始读到1001-2000,B客户端不会让read解除阻塞,读到1001-2000这个数据还是会继续等到,直到1-1000这个数据达到之后,read才会开始接触阻塞

        确保发送方的write的顺序和接收方的read的顺序是始终一致的

TCP的核心机制(2) 超时重传

在网络传输的过程中,并不会一帆风顺,而是可能会出现"丢包"情况

丢包的原因有很多种

1.数据传输过程中发生了bit翻转,收到这个数据的接收方/中间的路由器计算校验和发现校验和对不上————————–>把这个数据包就丢弃掉,不继续往后转发/不交给应用层使用

2.数据传输到某个节点(路由器/交换机),这个节点负载太高了

某个路由器,单位时间只能转发N个包,由于现在是网络高峰期,这个路由器单位时间需要转发的包超过了N个,已经发不过来了,后续传输过来的数据可能被这个路由器直接丢弃掉

TCP是如何对抗丢包的呢?

丢包是不可避免的,TCP能做的就是感知数据是否丢包,如果丢包就重新发一次

需要通过应答报文来区分 ,如果收到应答报文说明数据没有丢包,如果没有收到应答报文说明数据丢失了,所以这期间有一个限制时间(超时时间),如果在这个时间之内,ack没有收到,就视为数据丢包了

丢包之后,发送方设置了一个"超时时间"

在时间之内,没有收到反馈的ack,就认为数据丢包了

此时有两种情况:

1.数据确实丢包了

2.应答报文(ack)丢失了

发送方无法区分是哪个情况,都会在到达超时时间之后重传数据

但是如果是第二种情况下,接收方就收到了两份一样的数据

TCP对于上述的情况做处理,接收方有一个接受缓冲区,收到的数据先放到缓冲区中,如果后续再收到数据,就会根据序号,在缓冲区中找对应的位置(排序),如果发现序号已经在缓冲区存在了,就会把新收到的数据给丢弃了(去重)===>确保应用程序调用read读出来的数据是唯一的,不会重复

超时重传的时间设定

这里的时间不是固定值,而是动态变化的

发送方第一次重传,超时时间是t1,如果重传之后,仍然没有ack,就会继续重传,第二次重传,超时时间是t2                        t2>t1

每多重传一次,超时时间的间隔会变大/重传的频次会降低

经过一次重传之后,就能让数据到达对方的概率提升很多,再重传一次,又会提升很多

相反,如果重传几次都没用顺利到达,说明网络的丢包率已经到了一个非常高的程度,网络发生了严重的故障,大概率没办法使用了

重传也不会无休止的进行,当重传达到一定的次数之后,TCP就不会尝试重传了,就认为中国连接已经结束,会先尝试进行**“重置/复位 连接**",发送一个特殊的数据报”**复位报文".**如果网络这会儿恢复了,复位报文就会重置连接,使通信可以继续进行,如果网络还是有严重问题,复位报文也没得到回应,此时TCP就会单方面放弃连接

放弃连接也就是发送方释放掉之前保存的接收方的相关信息

针对上述的内容,只关注策略,不关注参数(比如超时时间的设置,重传次数的设定等等)

确认应答和超时重传相互补充,共同构建了TCP"可靠传输机制"

“三次握手和四次连接"也有作用,但是是辅助作用,不是主要的

TCP的核心机制(3):连接管理

建立连接                三次握手

断开连接                四次挥手

这些次数指的是网络通信的次数,握手/挥手是形象的比喻(handshake),计算机中的常见术语

网络中的握手,发送不携带业务数据(没有载荷,只有报头)的数据包,但是能起到"打招呼"的效果

https://i-blog.csdnimg.cn/direct/82a025371f10468fa9dc890a9d25827a.png

syn=>synchronized    互斥锁

这里的syn是同步的意思,计算机中一个术语经常有多重含义,需要结合上下文理解

此处的同步可以延伸成客户端希望服务器和他统一步调,完成后续的传输(同步数据包)

建立连接是一个双向操作

A需要给B说,我想和你建立连接(A想保存B的信息)

B也需要给A说,我也想和你建立连接(B想保存A的信息)

这里本来是四次交互的,但是中间两次合并了,就成了三次握手

三次握手的意义/作用

1.投石问路,初步验证通信的链路是否畅通

2.确认通信双发各自的发送能力接收能力是否都正常

3.让通信双方在进行通信之前对通信过程中需要用到的一些关键参数需要协商(起始数据的序号)

每一次建立连接TCP的其实序号都是不同的,并且不是从1开始的,

每次建立连接,tcp的起始数据都不同(并且故意差别很大),意义在于避免前朝的剑斩本朝的官

需要区分数据是之前的还是之后的,虽然主机还是双方,但是应用程序可能不是相同的

四次挥手:断开连接

https://i-blog.csdnimg.cn/direct/7507a71ca25f4d1bb255133207a6d829.png

双方各自把对端的信息删除掉

前面提到的超时重传是单方面释放连接断开连接不一定是客户端主动,服务器也可以主动断开

https://i-blog.csdnimg.cn/direct/e936744686a44c5791dc02c9d2c86251.png

通信双方,各自给对方发送"FIN”,各自给对方返回"ACK"

三次握手,三次是因为中间两次的交互合并在一起了

对于四次挥手来说,中间两次不一定能合并的(大概率是不能的)

对于三次握手来说,中间两次的ACK+SYN都是在内核中,由操作系统负责进行的,时机都是在收到SYN之后,此时同一时机,就可以合并了

对于四次挥手来说,ACK是内核控制的,但是FIN的触发则是通过应用层调用close/进程退出来触发的

代码中针对socket.close() => 系统内部,就是发送FIN

https://i-blog.csdnimg.cn/direct/33ebdaa5518b42d1a1532601cbe441b6.png

https://i-blog.csdnimg.cn/direct/dcbbb485a223457594237e1b1403d386.png

https://i-blog.csdnimg.cn/direct/8abefefce41f4ac6a6f9fd6329f99b17.png

这里可以发现TCP也是有状态

只需要记住几个关键的就行了

1.LISTEN

        服务器进入状态,服务器把端口号绑定好,相当于进入listen状态

        此时服务器就已经初始化完毕,准备好随时迎接客户端了

2.ESTABUSHED

        客户端和服务器都会进入的状态

        表示TCP连接建立完成(保存了对方的信息)

        接下来就可以进行业务数据的通信

3.CLOSED_WAIT

被动断开连接的一方会进入这个状态,先收到FIN的一方,等待代码执行close的方法

        如果发现服务器这端存在大量的CLOSED_WAIT状态的TCP连接说明服务器代码有BUG

        可能close没有写或者没有执行到位

4.TIME_WAIT

       主动断开连接的一方会进入这个状态,此处TIME_WAIT按照时间来等待,达到一定时间之后,连接也就释放了

        为什么一定要等待而不是直接释放?

        主要就是为了防止最后ACK丢包了

等待两个MSL,防止ACK对方没收到,超时重发数据

        MSL是数据包在网络传输中消耗的最大时间,不同的系统是不一样的,都是可配置的,比如Linux默认值是60s

TCP的核心机制(4):滑动窗口

可靠传输的代价是降低了传输的效率,所以TCP希望在可靠传输的基础上能提升效率

所以引入了滑动窗口

https://i-blog.csdnimg.cn/direct/1991cdab7fa84962acaec2a8b33a2cb3.png

https://i-blog.csdnimg.cn/direct/a7045ff83b234e47b9cf10414975e8b1.png

批量发送了这组收据,批量等待这组数据的ack

收到了第一个ack之后,就立即发送下一条数据

操作系统的内核为了维护这个滑动窗口,开辟了发送缓冲区来记录当前还有哪些数据没有应答;只有确认应答过的数据才能从缓冲区删掉

随之会带来两个情况

情况一:数据包已经到达,但是ACK丢了

**第一种情况不需要处理,**因为多个ACF只是丢了其中一部分不可能全部丢弃

需要理解确认序号:

确认序号表示的时候收到的数据的最后一个字节的下一个序号,进一步理解就是

确认序号之前的数据都已经收到了

接下来你要发送的数据都是从确认序号这里往后发的

虽然1001ACK丢失了,但是2001到达了,发送方收到2001,意味着2001之前的数据都已经收到

后一个ACK可以涵盖前一个ACK的意义

情况二:数据包直接丢失

当某一段报文丢失后,发送端会一直收到1001这样的ACK,就像是在提醒发送端"我想要的是1001"

如果发送端主机连续三次收到了同样一个1001这样的应答,就会将对应的数据1001-2000重新发送

这个时候接收端就收到了1001之后,再次返回ACK的就是7001,因为(2001-7000)接收端其实之前就已经收到了,被放到了接受端操作系统内核的接受缓冲区

这种机制被称为"高速重发机制"(快重传)

滑动窗口/快速重传

确认应答/超时重传

这两者彼此之间不冲突

如果通信双发单位时间发送的数据量比较少,就是按照之前的确认应答/超时重传

如果单位时间发送的数据比较多,会按照滑动窗口/快速重传

TCP的核心机制(5):流量控制(返回一个合适大小的窗口大小信息)

滑动窗口,提高速度(踩油门)

流量控制,制约速度(踩刹车)

滑动窗口大小并不是无限大的

如果发送速度过快,消费速度比较慢,就会使得接收缓冲区满了

接受缓冲区相当于一个阻塞队列(Blocking Queue),生产者消费者模型

https://i-blog.csdnimg.cn/direct/2fbbc208638e44439b96eadb79ad59a6.png

如果空闲空间越大,就可以认为是应用程序的处理速度比较快

就可以让发送方,发的快一点,设置一个更大的窗口大小

如果空闲空间越小,就认为是应用程序应用程序处理速度比较慢

就可以让发送方,发的,慢一点,设置一个更小的窗口大小

TCP中接收方收到数据的时候就会把接收缓冲区剩余空间大小通过ACK数据包反馈给发送发

发送方就可以依据这个数据来设置发送的窗口大小了

https://i-blog.csdnimg.cn/direct/02105d33761d4bc1a46324ba6f1a2bd4.png

https://i-blog.csdnimg.cn/direct/70fd549f5a654d4f9072621aa0b8628f.png

流量控制不是TCP独有的机制,其他的协议可能也会涉及到流量控制(比如,数据链路层中有的协议也支持流量控制)

TCP的核心机制(6):拥塞控制

这个和刚刚的流量控制有关联

流量控制,站在接收方的视角来限制发送方的速度

拥塞控制,站在数据链路层的视角来限制发送方的速度

流量控制就可以使用接收方的缓冲区剩余空间来痕量

但是考量的中间节点的情况就很复杂

1.中间节点非常多

2.每次传输的数据,走的路线还都不一样

3.中间哪个节点遇到了瓶颈就丢包了

4.中间节点传输的数据不止有你的数据,还有很多其他设备的数据

流量控制会限制发送窗口

拥塞控制也会限制发送窗口

这两个机制会同时起作用,最终实际发送的窗口大小取决于上述两个机制得到的发送窗口的较小值

拥塞控制中,窗口大小

https://i-blog.csdnimg.cn/direct/ba73e6da993b470d9e151f7db4033b83.png

1.刚开始传输数据,拥塞窗口会非常小,得用一个很小的速度来发送数据

当前网络是否拥堵,是未知的

2.不丢包,增大窗口大小,指数增长

        增长速度特别快,短时间内达到很大的窗口大小

3.增长到一定程度,达到某个指定的阈值,此时即使没有丢包也会停止指数增长,变成线性增长

        不至于太快的进入到丢报的节奏

4.线性增长也会持续发送,达到某个情况下就会出现丢包

        一旦出现丢包接下来就需要减少发送的速度,减小窗口大小

        两种处理办法

        1)经典的方案,回归慢开始非常小的初始值,指数增长,线性增长

        2)现在的方案,回归到新的阈值上,线性增长(以后都不会出现指数增长)

这个图不需要特意去背,记住一个原则:面多加水,水多加面,达到动态平衡

TCP的核心机制(7):延迟应答

提升效率的机制,尽可能降低可靠传输带来的性能的影响

https://i-blog.csdnimg.cn/direct/838a12c919f54e68946c06d3144d661a.png

如果我们返回的ack时间稍微晚一点,应用程序就有机会读取缓冲区的更多的数据

此时,延迟返回的ack的窗口大小,大概率是比立即返回ack的窗口大小更大

在这个时间里,会有一个消费数据的过程

TCP的核心机制(8):捎带应答

        延迟应答的基础上,引入的提升效率的机制

        把返回的业务和ack两者合二为一了

https://i-blog.csdnimg.cn/direct/447dafb012e04e2296d9920d927205ec.png

ACK是内核返回的,是收到请求之后立即返回ACK

响应则是应用程序返回的,代码中根据计算得到响应再把响应写回到客户端

正常情况ack和响应是不同的时机,无法合并,但是ack涉及到延迟应答,延迟应答就会使ack返回的时间被往后拖,这样一延迟,就可能赶上接下来发送响应数据的操作

于是就可以在发送相应的时候把刚才的ack的信息也带上

https://i-blog.csdnimg.cn/direct/23505a052f3e4b9cbd95f56c1cad5c03.png

TCP的核心机制(9):面向字节流(粘包问题)

读写100个字节的数据

1.可以一次读写一个字节,分100次

2.可以一次读写10个字节,分10次

3.可以一次读写50个字节,分2次

4.可以一次读写100个字节,一次搞定

粘包问题,通过面向字节流的方式传输数据都会涉及到粘包问题

粘的是TCP携带的载荷,应用层数据包

https://i-blog.csdnimg.cn/direct/94506f1d84244c5e8d158cc7ce1a31de.png

应用程序需要读取接收缓冲区中的数据

因为TCP是面向字节流的,此处的读操作,怎么读都是可以的

可以读一个a,aa,bb,b,c,cc

也可以读aaab,bb,c,cc

也可以读 aa,ab,bbc,cc

存在无数种读法

但是实际上 aaa,bbb,ccc才是正确的读法,aaa,bbb,ccc才是完整的应用层数据包

应用层数据包在TCP的缓冲区中连成一片,黏在一起,也就称为粘包问题

想要解决粘包问题,关键在于明确"包之间的边界"

方案一:指定分隔符

适用于文本类的数据

TCP echo server

当时我们的做法就是约定请求响应,都以\n结尾

发送请求响应的时候专门使用println来进行写数据

读取请求响应的时候专门使用后scanner.next进行解析

但是需要确认的是数据内容的正文中不能包含分隔符,如果传输的数据是纯文本的数据的话,此时使用\n或者;之类的可能都不合适,但是可以使用ASCII中靠前的"控制字符"

方案二:制定数据的长度

比如约定在每个应用层数据包,开头的2/4个字节,表示数据包的长度,如果传输的是二进制数据,这个方案就很有用

https://i-blog.csdnimg.cn/direct/85d69047f2fe47b7874232c3d9e5bf2e.png

UDP这种面向数据包的传输方式

不涉及到上述问题

send/receive 得到的就是一个完整的DatagramPacket

这里携带的二进制字节数组就是一个完整的应用层数据包

TCP的核心机制(10) 异常处理

1)进程的崩溃

Java中的体现就是抛出异常,但是没人catch,最终异常到了JVM这里,JVM进程就挂了

看起来是崩溃,但是操作系统会进行善后,当进程崩溃的时候,进程中的PCB就会被回收

PCB中的文件描述符表里的对应的所有文件,也都会被系统自动关闭

其中针对socket文件,也就能触发正常的关闭流程(TCP的四次挥手)

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

正常流程点击关机按钮,此时操作系统就会先干掉所有进程

干的过程中他同样会触发四次挥手

1)四次挥手非常快,已经完成了,关机动作才真正的完成

2)四次挥手还没来得及挥手完,关机就完成了

https://i-blog.csdnimg.cn/direct/154894d049ed4083a1a00f21a64f0409.png

B的重传有上限,重传到一定的此处还没得到响应就会单方面的放弃连接(B把保存的A的信息给删掉)

3)主机掉电(拔电源)

a)接收方掉电

https://i-blog.csdnimg.cn/direct/cbdef6ff82e74293a355284ed19d7521.png

A给B发送的数据,就得不到ACK了

A触发超时重传,重传的数据还是没有响应

反复多次之后,A尝试重置连接(rst)

重置操作也没得到ACK,A就会单方面释放(A把保存的B的信息删除掉)

b)发送方掉电

https://i-blog.csdnimg.cn/direct/c4475ae3718b4a99bc131bfd05934f10.png

A发着发着没声了

B的视角看起来不知道A是挂了还是发的慢

此时B就会给A发送一个数据报,不携带任务,只是为了触发ACK

如果发了探测报文之后,A返回了ACK说明A只是歇了一会,没挂掉

如果发了探测报文之后,A没有ACK,甚至发了多个之后也没得到ACK,可以视为A挂了

这样的报文是周期性的,同时这个报文是用来探测对方的"生死"的,也就把这样的报文成为"心跳包"

计算机中非常广泛的使用心跳包的思想,TCP内置了心跳包,由于TCP内置的心跳包周期比较长

秒级-分钟级

应用程序这一层通常也会自行实现一些心跳包,达到更快速的"保活机制"

4)网线断开

和主机掉电一样

https://i-blog.csdnimg.cn/direct/1f0b6ab92d3f4d69b692b362c8b8b5ce.png

以上所讲的是TCP的十个重要的机制

并不是TCP一共只有十个机制

TCP更多机制的详情可以参考rfc标准文档

再谈TCP报头

https://i-blog.csdnimg.cn/direct/7f7f4a1ad82740aeaed4207c245fefb5.png

大多数我们都学过了,剩下的两个标志位中URG和PSH还有紧急指针没有提到过

URG是配合紧急指针来使用的

当URG为1时,紧急指针生效,紧急指针里保存的是一个偏移量

TCP正常情况来说都是按照顺序来传输数据的

紧急指针就是让后面的数据插队,根据紧急指针的偏移量,把指定的位置的数据优先发送出去

特殊场景的特殊方案,不是一个通用的方案,日常开发中很少直接涉及到

PSH 催促标志位,带有这个标志位的数据,就相当于提示接收方,要尽快来处理这个数据

(也是特殊场景下的用法)

Java程序员日常开发中用的更多的是HTTP,但确实有时候一些场景会使用TCP

TCP

UDP          这两协议并无优劣

TCP对于数据可靠传输的场景是首选

UDP对于可靠性要求不高,对于性能要求很高的场景,比如分布式系统中主机之间通信

用UDP实现可靠性传输(比较经典的题目)

参考TCP

编写应用层代码段时候首先引入确认应答,接受放返回ack

为了区分ack响应谁,引入序号和确认序号,约定确认序号的规则

考虑到丢包的情况,引入超时重传

为了进一步提高传输效率,引入滑动窗口,引入快速重传

为了限制滑动窗口,引入了流量控制/拥塞控制

等等

网络层         IP协议

1.地址管理 ==> IP地址

2.路由选择 ==> 数据报传输的路径进行规划

IP协议报头结构

https://i-blog.csdnimg.cn/direct/92b19b1ad774425fa94d0c80a7088d55.png

https://i-blog.csdnimg.cn/direct/c58e8f9f0b914264ae7c0e1479a95a48.png

4位版本:实际上只有两个取值

4 -> IPV4(主流)

6 -> IPV6

https://i-blog.csdnimg.cn/direct/cb88c3d9b79c4c6a896070b586b55db5.png

IP协议报头也是变长的

0->15

实际上报头长度*4(基本单位为4个字节)

当15(1111)的时候,报头长度位60字节

https://i-blog.csdnimg.cn/direct/42a3469b5c01433f90419408a412d324.png

type of service

8种服务类型(其中3位已经弃用,1个保留字段)

4位TOS:最小延时(A到B的时间消耗最短),最大吞吐量(数据从A到B单位时间内传输的数量更多),最高可靠性,最小成本(设备上消耗中资源较小)

https://i-blog.csdnimg.cn/direct/39dbf75cc1774610aa52aa4d6c75db73.png

16位总长度IP数据报的长度,UDP也是16位(2个字节,64KB)

单个IP数据报确实没法超过64KB,但是不代表IP协议不能传输超过64KB的数据

IP协议会自动把大的数据报拆成多个IP数据报携带传输,在接收方再进行拼装

https://i-blog.csdnimg.cn/direct/60fa65814933420b90e0fb7e3a3c861d.png

IP协议会自动拆包

同一个载荷数据会被分成多份,交给多个IP数据报来携带

多个IP数据报,16位标识是相同数值

13位片偏移决定组包的时候数据包的位置(后发先至问题)

3位标志位只有2位有效(剩下一个保留位)

其中一个表示这个包是否需要组包(是否是拆包的一部分),如果是位1

另一个表示当前的包是否是组包中的最后一个单位

https://i-blog.csdnimg.cn/direct/e5e11230b58d49b5a998972695dc820d.png

应该数据包在网络中最多可以存在多长时间

假设有一个IP数据包,目的IP不存在,如果让这样的数据报无线传输,会消耗很多网络资源

TTL约定了传输时间的上线,达到上线之后,数据包就会自动被丢弃掉

TTL的单位不是s或者min,而是次数

指的是经过路由器转发的次数

发送一个IP数据报的时候会有一个初始的TTL的值(32,64,128)

数据报每次经过一个路由器转发,就会**-1(经过交换机不减的,因为交换机只涉及到数据链路层)**

路由器涉及到网络层

一旦TTL减少到0了,此时这个数据包就会被当前的路由器直接丢弃掉

https://i-blog.csdnimg.cn/direct/3bd7c1a4cf184fe2ba0f302c04afa98f.png

比如这里,数据报初始TTL64,中间经历了10个路由器转发,最终到达了百度

https://i-blog.csdnimg.cn/direct/f287b5de8d6f4a32b9631c4c406d921d.png

IP数据报中携带的载荷属于哪种传输层协议的数据报

通过这里不同的值,感知到接下来要把数据交给TCP解析还是UDP解析还是其他协议解析

https://i-blog.csdnimg.cn/direct/f674b78ff3064248a8f012f03db80dcc.png

验证数据在传输中是否出错(只针对首部,IP报头)

载荷部分TCP/UDP都有自己的校验和

https://i-blog.csdnimg.cn/direct/187c7a4f5efe45649b5825b7de4f55c5.png

IP数据报中的最关键的信息,数据从哪里来,到哪里去

IP地址是32位的整数,是一个很大的数字,不方便人进行阅读

所以把32位(4字节),通过三个圆点分隔开,每一个部分都是一个字节,范围是0-255

这种写法称为:点分十进制

https://i-blog.csdnimg.cn/direct/8c675bae734b48d2bf3478a526e62561.png

比如这里我的IP地址为:192.168.1.9

IP地址用来标识网络上的一个设备

期望IP地址是唯一的

32位标识的数据是0 -> 42亿9千万     2^32-1

解决IP不够用的问题

方案一:动态分配IP地址

一个设备上网就分配IP,不上网就先不分配

方案二:NAT网络地址转换

使用一个IP代表一大波设备(以一当千)

把IP地址分成两个大类

1.内网IP/私网IP        (10.*        172.16-172.31.*        192,168.*)

2,外网IP/公网IP

要求公网IP必须是唯一的,但是私网IP是允许重复的(在不同的局域网中是允许重复的)

NAT网络地址转换

一个设备在进行上网的时候,IP数据报中的IP地址就会被NAT设备(通常是路由器)进行自动的修改

1.同一个局域网内,主机A访问主机B(不涉及)

不会涉及到NAT

2.公网上的设备A访问公网上的设备B(不涉及)

不会涉及到NAT

3.一个局域网中的主机A访问另一个局域网的主机B(不允许)

NAT机制中不允许的

4.局域网内部的设备A访问公网上的设备B(生效)

NAT机制主要就是针对这种情况进行生效

数据从我的电脑发送到软件的服务器

https://i-blog.csdnimg.cn/direct/aabef0f4afe341848bf9c99091232bc9.png

日常上网的设备绝大部分都是在不同的局域网中的

就相当于一个公网IP,就可以代表一大批设备

数据从服务器到我的电脑

https://i-blog.csdnimg.cn/direct/c18f433368da4af6b82e3311df85c5fe.png

https://i-blog.csdnimg.cn/direct/29f0ed97421c4c4681be2c28d02fbf4f.png

如果此刻同局域网内,存在一个另一个设备进行访问同一个软件

这个时候可以凭借映射关系中的端口号找到对应的设备

https://i-blog.csdnimg.cn/direct/14d2786c0075412599fdde1d0d7d8ff5.png

https://i-blog.csdnimg.cn/direct/0cfbfdfa08be42c3817905673b1b0e2b.png

如果出现同一个局域网下的端口号相同该如何处理(这个情况的可能性很低)

https://i-blog.csdnimg.cn/direct/92521118102e42fc912394b6d523e168.png

虽然端口号相同,但是路由器建立映射关系的时候可以把端口号替换成不重复的其他端口

在网络通信中,不仅只有IP信息,还有一个关键的是端口号

端口号本来就区分同一个主机上不同的应用程序的

在NAT中就可以用于区分不同主机上不同的应用程序

NAT机制的特点:

网络环境太复杂

替换过程中,每一层路由器都需要维护映射关系

每次转发数据都要查询映射关系,都是开销

方案三 IPV6

从根本上解决IP地址不够用的问题

IPV4 使用32位    4个字节表示IP地址

IPV6 使用128位   8个字节表示IP地址

2^32=42亿多

2^128 = 2^32 * 2^32 * 2^32 * 2^32

IPV6的地址空间非常巨大

IP地址中的网段划分   组网(组建网络)

组网的时候,就需要我们针对每个上网设备IP地址(包括路由器的IP)进行设置

日常上网我们用的路由器都是家庭网络比较简单的网络结构,路由器都有"自动分配IP"的功能(dhcp)

复杂的场景下就需要手动进行设置

IP地址   32位的整数

一分为二

左边位网络号,右边位主机号

https://i-blog.csdnimg.cn/direct/e79d8c50a1624a998a457529bdab3648.png

如何区分网络号和主机号呢?

这个时候需要引入子网掩码来区分主机号和网络号,

子网掩码也是32位的整数,左半边都为1,右半边都为0不能0和1 穿插出现

255.255.255.0    也就是  1111 1111 1111 0000      左边的为网络号,右边位主机号

所有我 设备的网络号是192.168.1   主机号为9

网络中规定

1.同一个局域网内的设备,网络号必须相同,主机号必须不同

2.两个相邻的局域网,网络号必须不同

https://i-blog.csdnimg.cn/direct/15c661f0eaed45e9b0e705841ff2e39f.png

在这个局域网中,某个设备网络号不同的情况无法上网

某个设备网络号虽然相同,主机号和别的设备重复也无法上网

这个路由器连接了两个局域网,IP网络号分别为192.168.0和192.168.1,不能重复

路由器中的LAN口和WAN口,LAN口接你的电脑,WAN口接上层路由器的

除了子网掩码的方案,还有一种方案,ABCDE五类网络

https://i-blog.csdnimg.cn/direct/6d1c48f6bf3046a58cf90ddbd384fb03.png

特殊的IP地址

1.主机号为全0  (二进制)

此时这个IP就表示当前网段(相当于网络号)

因此,给局域网中的某个设备分配IP地址的时候,不能把主机号设为全0

2.主机号为全1 (二进制)        广播IP

比如子网掩码为:255.255.255.0

主机号为:192.168.0.255   ->(这里的255=1111 1111)

往这个IP上发送数据包就相当于给整个局域网中的所有设备都发了一次数据包

例如:电视/手机的投屏,按下按钮,弹出来一个设备列表,选择投屏到哪个设备上(要求在同一个局域网下)

手机是如何知道局域网中有多少个设备呢?

这种功能基于广播IP实现,手机触发投屏按钮的时候,往对应的广播IP上发一个数据报(UDP),(TCP不支持广播,只能一对一),如果收到这个数据报的设备不具备投屏功能就没有回应,如果收到的数据报有投屏功能,则返回一个响应,告诉我是什么设备,IP号为多少

3.127.        环回IP(loopback)*

自发自收,给这个IP发一个数据,设备就会从这个IP上再收到同一个数据,自己给自己发

使用环回IP一般进行测试

写网络程序大多数情况都是为了跨主机通信

往往需要先自行测试,一台主机测试客户端和服务器之间能否正常交互

一般使用的环回IP就是  127.0.0.1        虽然其他的127开头的IP也是可以的,但是少见

路由选择

网络是复杂的网状结构,从一个节点到另一个节点有很多条路

每个路由器无法知道整个网络结构的全貌,只能知道其中的一小部分(每个路由器只知道他附近的设备是什么样子的情况)

路由表(路由器内部维护的重要的数据结构)

路由表类似于hash,key就相当于IP地址(网络号)

value就是对应的网络接口(往哪个方向走)

IP数据报到达路由器就要进行路由表查询

IP数据报中的目的IP,查这个目的IP在路由表中是否存在

如果查到了,按照路由表指向的方向继续转发,如果没查到路由表会有一个"默认的表项"(下一跳)

下一条就是指向了一个更高层次的路由器(认识的设备范围更广)

这个路由器也可能查不到,也会选一跳,再去指向更高层次的路由器(认识的范围比刚刚的还广)

数据链路层

以太网(插网线)

802.11(无线wifi)

https://i-blog.csdnimg.cn/direct/f6cd0a35b50e43fb94d31f9a9cb05193.png

源地址和目的地址指的是网卡的硬件地址(也称为MAC地址),是物理地址,长度为48位,6个字节,是在网卡出厂的时候固化的

帧协议类型描述了载荷中是什么样子的数据

0800携带的就是正常的IP数据报

0806和8035不传输业务数据,而是给转发功能提供辅助的措施

帧尾是校验和

MTU和相当于发快递时,对包裹尺寸的限制,这个限制是不同的数据链路层对应的物理层产生的限制的

以太网十分的短,最小46字节,最大1500字节,1.6KB    IP/UDP至少还是64KB

如果一个数据报从以太网由到拨号链路上,长度大于拨号链路的MTU了,需要对数据报进行分片

MAC地址和IP地址的区别

1.MAC地址使用6个字节表示,IP地址使用4个字节

        MAC地址的空间范围比IP地址大了6万倍,MAC地址当前仍然可以给每个设备都分配一个唯一的mac地址值,并且在出厂的时候mac地址是被分配好的写死的(不能修改),所以说mac地址可以作为设备的身份标识

2.MAC地址使用在数据链路层,用来实现两个香菱设备之间的数据转发            —微观的

IP地址使用在网络层,立足于转发整个流程,进行路径规划                 –宏观的

举个例子:

从西安=>吉林省白城市安广镇

https://i-blog.csdnimg.cn/direct/5dc8dfb00c9f4317ad298f8e6f78f8bd.png

ARP数据报/ARP协议 可以视为是让当前设备获取到周围设备IP地址和mac地址之间的映射关系

IP协议路由转发的过程

这个过程都是拿IP地址来进行查路由表的=>得到的是**“网络接口**"(抽象的概念)

具体点就是传输给那个mac地址的设备,这个环节就涉及到 IP -> mac

ARP协议就是用来建立上述的映射关系的

当前设备接入网络的时候,就会往广播地址发送ARP报文

收到ARP请求的设备就会返回ARP响应,响应中就会告诉自己的IP和mac

DNS应用层协议

DNS应用层协议,也可以认为是一套系统

域名解析系统

域名   =>   网站

域名代表了IP地址https://i-blog.csdnimg.cn/direct/ccbe5ed172144ff19acb2c4035ec18e0.png

域名和IP地址 存在对应关系

一般是一个域名对应一个或者多个IP

也可能多个域名对应一个IP

https://i-blog.csdnimg.cn/direct/f459d26c26394830b5cd189e6ae881ff.png

虽然把IP写作点分十进制, 但是仍然对于人类是不友好的

所以出现了用字符串表示IP地址,就是我们常用的网址(域名)

把域名转换成IP地址,这样一套系统就被称为域名解析系统

早起的域名解析系统非常简单,通过一个文件夹来实现的(host)

https://i-blog.csdnimg.cn/direct/dfcd8ff606d641a98eb7881c4287aa14.png

文本文件就记录了IP地址和域名对应的关系

虽然现在不使用了(仍然有效)

网站太多了,域名也有很多,IP地址也有很多,靠文件来维护不现实不方便

为了解决上述问题,搭建了DNS服务器

把hosts文件放在DNS服务器里

当某个电脑需要进行域名解析,访问DNS服务器

全世界有这么多设备上网,每时每刻都在访问DNS服务器,DNS服务器能顶住这么大的访问量吗?

多搞一些DNS服务器就好了,多搞出来的DNS服务器成为**“镜像服务器”**

事实上全世界有数不清的镜像服务器,往往是一些运营商/互联网公司维护的

每个人上网的时候就会就近的去访问DNS服务器

**一旦有数据变更,针对这种变更,需要约定以某个服务器的数据为基准==>根服务器(**绝大多数在美国人手里,一旦美国人把某个国家的域名里的数据从根服务器中删除掉,意味着这个国家的网络就会陷入瘫痪,曾经真实发生过)

一旦有变更,就修改这个基准服务器,其他服务器从基准服务器同步数据

某个地区DNS的镜像可能会出现故障(偶尔出现)

QQ/WX能使用,但是网页打不开

这个时候只需要换其他的DNS服务器就好了