作者:~小明学编程
文章专栏:JavaEE
格言:热爱编程的,终将被编程所厚爱。
应用层
应用层是我们最为重要的一层,在应用层我们需要自己去设计一个合适的协议来供前后端的交互使用,其中比较知名的应用层协议就是HTTP协议。
下面我们介绍几种常见的应用层模板
XML
1.可读性好,但是运行效率不高,属于老的数据格式了。虽然也在用,但是用的越来越少了。
2.特点:由标签构成: <标签名>内容</标签名> 标签名就是 key,标签值就是 value 通过标签就更好的提升了可读性。
3.虽然提高了可读性,但是又引入了太多的辅助信息(标签名之类的) 就导致占用了更高的 网络带宽。
4.所以 xml 就很少作为 应用层协议的 模板了,更多的是用来做一些配置文件。
json
1.可读性好,但是运行效率不高。当下最流行的一种设计 应用层协议 的数据格式,通过 大括号 构成了键值对格式。
2.一个 大括号 中有很多键值对,键值对之间使用 逗号 分割,键 和 值 之间使用 冒号 分割。
3.要求 键 必须是字符串类型的 值允许有好多种(数字,字符串,布尔,数组,另一个 json)。
4.json 中表示字符串,单引号和双引号都可以,最后一个键值对,后面可以有逗号,也可以没有(标准要求是没有的,但是一般的 json 解释器都不会在意)。
5.json 要求 key 一定是字符串,因此 key 这里的引号可以忽略,除非 key 中包含了一些特殊符号(比如像 空格,-,就不能省略了)。
json 和 xml 对比:
相比于 xml 来说,json 也能保证可读性,同时又没有那么繁琐(占用的带宽小).
json 虽然传输效率比 xml 高,但是然然要多传递一些冗余信息,就是 key 的名字,尤其是数组的时候,更明显。当表示数组的时候,就会有很多 key 重复出现,也就占用了带宽。出问题的请求和响应,一目了然。
protobuffer
1.运行效率高,但是可读性不高。
2.是一种 二进制格式 的数据,在 protobuffer 的数据中,不再包含 key 的名字。而是通过顺序以及一些特殊符号,来区分每个字段的含义。同时再通过一个 LDL 文件。来描述这个数据格式(每个部分是啥意思)。
3.LDL 只是起到一个辅助开发的作用,并不会真正的进行传输,传输的只是二进制的纯粹的数据。
传输层
UDP的数据报文格式
这是我们的UDP的数据报的报文格式,在进入到传输层的时候我们会对其加上UDP的报头,这个报头一共占8个字节,这8个字节分别是分给我们的源端口也就是我们操作系统给我们的客户端分配的一个端口号,目的端口也就是我们服务端的端口号,报文长度就是我们想要发送内容所占的字节长度,校验和就是对我们所发送的数据进行一个校验。
TCP报文格式
TCP相比DCP要更加的复杂,同时TCP的应用也更加的广泛,下面就简单的给大家介绍一下这些格式的含义。
1.首先是16位的源端口号和16位的目的端口号也就是我们客户端和服务端的两个端口号16位也就是两个字节和DCP一样。
2.4位首部长度是来描述我们报头的大小的其单位是4个字节,因为我们报头的大小不是固定的,因为选项的存在,选项可有可无,可以有多个所以我们的大小不固定,我们在计算报头长度的时候就要根据4位首部长度来计算,4位所能表示的最大值为15,其大小也就是15*4=60字节。
3.保留位是用来未雨绸缪的,现在暂时还用不到但是未来用不用的到,升级用不用的到那就不一定了。
TCP的可靠传输
TCP的四大特点:可靠传输,有连接,字节流,双全工,其中最重要的就是我们的可靠传输,可靠传输也就是我们的发送端知道我们的接收端有没有接收到,并且数据能按照我们想要的方式传送到位那么这一功能是如何实现的呢?
这是AB二人在互相发消息,因为传输的问题就导致了B的回复发生了错乱,A先问的身高结果体重的消息先达到了,这就是典型的不可靠传输,当然我们上述案例是从宏观上来看的从微观上来看的话那就是你怎么保证你发的是你多高而不是高多你。
TCP的可靠传输基于两点来解决问题:1.确认应答 2.超时重连
确认应答(安全机制)
确认应答就是来解决前面的乱序问题的,想要解决这个问题的话我们可以采用对数据进行一个排序的方式。
TCP将每个字节的数据都进行了编号,即为序列号,每一个ACK都带有对应的确认序列号,意思是告诉发送者,我已经收到了哪些数据,下一次你从哪里开始发。
这样就解决了我们数据发送混乱的问题了。
超时重传(安全机制)
非一般的情况下总是会有意外的导致我们的数据不能正常的发送到位,就像上述的情况,A给B发了一个吃了吗?有可能中间会产生丢包的情况我们的B没有接收到消息,自然也就不会发送ack了,还有一种可能就是B接收到消息了,然后发送ack,但是ack在发送的时候发生了丢包的情况,这样也会导致我们的A接受不到ack也就是A会觉得自己的消息B没有接收到。
对于上述的情况TCP设定了超时重连当发送端发送数据后在指定的一段时间没有收到ack,就会触发重发机制,重新打包发送我们的数据,但是这又会产生一个问题,如果是我们发送的时候丢包没问题重发就行了,万一是我们的ack发生了丢包,重发岂不是会存在多份数据。
这个问题TCP也帮我们给解决了,当我们接收到数据的时候我们的数据是放在操作系统的接受缓存区的,如果收到新的数据我们的TCP就会去接受缓存区中去检索有没有这个消息,如果有的话就就丢弃如果没有的话就放进去,用来保证程序调用 socket API 拿到的这个数据一定是不可重复的。
重传如果失败,还会继续尝试,也不会无休止的重传,连续几次失败的话,就认为是网络遇到了严重的情况,再怎么重传也可能不行,就只能放弃(自动断开与 TCP 的连接)。重传的时间间隔会逐渐变大。
连接管理机制(安全机制)
我们知道我们的TCP是有连接的,在正常情况下要经历三次握手建立连接,四次挥手断开连接。
三次握手
1.客户端先给服务端发送syn想要知道服务端是否能够接收到消息,如果服务端接收到了消息那么此时的服务端就知道自己的接受能力没有问题,同时客户端的发送能力也没有问题。
2.服务端会发送ack,来告诉客户端我能接收到你的消息并且服务端也要知道客户端能不能知道服务端的消息,也会发送一个syn,通常情况下这个ack和syn会一起发送。
3.客户端此时接收到反馈,也就知道了服务端的接受和发送没有问题,同时客户端的发送和接受也没有问题,但是我们要服务端知道它的发送和我们客户端的接受没有问题,我们将会再次发送一个ack给服务端,服务端接收到后就知道双方的接受发送都没有问题了。
第一次握手的时候客户端给服务器发一个 SYN 同步报文段。意思就是客户端要和服务器建立连接。
如果 ACK 这一位为 1,就表示这个报文是一个确认报文段(确认应答报文也就是 ACK 为 1)。ACK 这一位为 1 的时候,起到一个应答效果,用来确认消息是已经送到了的。
三次握手的意义在于去检测将要通信的双方接受和发送的功能是否正常,保证后面通信的正常进行,如果不正常那就不能连接通信。
四次挥手
断开连接就是:四次挥手。三次握手,就让客户端和服务器之间建立好了连接。其实在建立好连接之后,操作系统内核当中,就需要使用一定的数据结构来保存连接的相关的信息,保存的信息其实最重要的就是前面说的五元组:源IP、源端口、目的IP、目的端口、TCP。有一天,连接断开了,那么五元组保存的连接信息就没意义了,对应的空间就可以释放了。就像攒钱买了手机,就和手机联系在了一起。如果有一天又买了新的手机,把旧手机卖掉之后,就和旧手机的所有联系都断开了。
1.首先我们的其中一个端会发出FIN请求中断连接,然后我们的接收端会接收到请求。
2.当我们的接收端接收到请求之后会发送个ack告诉发送端我们接收到了请求。
3.接收端在发送完ack之后同样也会发送FIN请求中断连接。
4.原发送端在接收到请求之后就会相应的也发送ack,告诉对放我们也接收到了消息。
重要状态:
CLOSE_WAIT:
1.这个状态是在挥手两次之后的状态,意思为等待关闭,这个状态就是在等待代码中调用 socket.close 方法,来进行后续的挥手过程。
2.正常情况下,一个服务器上面不应该存在大量的 CLOSE_WAIT 如果存在,说明大概率是代码 bug,close 没有被执行到。
TIME_WAIT:
1.为什么要有这个状态呢?一般情况下我们刚开始发送FIN的那一方在发送完并且收到ack之后我们就会觉得没用了可以撤销资源了,但是实际的过程中我们最后一个ack可能会发生丢包的情况。
2.丢包之后我们就会重发,这是如果原发送端不见了那么该发送给谁呢,所以需要这个等待的状态。
3.TIME_WAIT 持续时间:2*MSL MSL:表示网络上任意两点之间,传输需要的最大时间。这个时间也是系统上可以配置的参数,一个典型的设置就是:60s(经验值)。
三次握手和四次挥手的区别:
1.发送方不一样:三次挥手的发送方一定是客户端但是四次挥手的发送方可能是客户端也可能是服务端。
2.合并问题:三次握手是合并了中间了两次,但是四次握手一般不能合并,因为fin的时机是用户代码负责的而ack是操作系统负责的,二者发送时机不一样很难合并。
滑动窗口(效率机制)
刚才我们讨论了确认应答策略,对每一个发送的数据段,都要给一个ACK确认应答。收到ACK后再发送下一个数据段,这样做有一个比较大的缺点,就是性能较差,尤其是数据往返的时间较长的时候。
我们在不使用滑动窗口的时候:
这个时候我们发送一条消息就要接受一个ack来确认是否时候效率太低了。
使用滑动窗口:
在使用了滑动窗口之后我们一次性发送多条消息,同时也会接受多哥ack,但是每当接受一个ack的时候我们就可以接着发送下一条消息了,然后再接受一条就再发一条,如果我们接收到下一个是4001这个ack的话那就直接发送四条不用考虑前面的ack有没有到达了,因为4001收到以后我们就认为4001之前的数据全部都已经收到了。
滑动窗口让我们把原本一次一次的ack可以放在一起发送极大的节省了时间。
丢包问题:
我们一次性发送多条数据那么要是出现丢包问题该怎么解决呢?
ack丢了
ack丢了的话问题不大,我们还是会有下一个ack的,比如我们的3001这条ack丢了我们就不能保证3001之前的数据有没有安全的发送出去,但是5001这条ack安全的发送了出去那就代表5001之前的数据全部收到了。
数据丢失:
如果是我们的数据丢失的话就不会这么简单了。
现在我们这条1001-2000的数据没有发送到为中间发生了丢包的情况,也接收不到ack,这个时候我们的发送端会发送后面的数据,这个时候接收端就发现我们前面的数据还没有接收到,接着就察觉到了丢包问题然后就会发送ack告诉数据端下一个是1001,然后重复发送直到我们重发并接受到缺失的数据。
流量控制(安全机制)
接收端处理数据的速度是有限的。如果发送端发的太快,导致接收端的缓冲区被打满,这个时候如果发送端继续发送,就会造成丢包,继而引起丢包重传等等一系列连锁反应。
因此TCP支持根据接收端的处理能力,来决定发送端的发送速度。这个机制就叫做流量控制(Flow
Control)。
· 滑动窗口越大,传输速率越高,不光要考虑发送方,还得考虑接收方。
**· **发送方发的太快,接收方处理不过来,就会把新收的包给丢了,发送方还得重传。
· 流量控制的关键是能够衡量接收方的速度,直接使用接收方接受缓冲区的剩余空间大小,来衡量· 当前的处理能力。
**· **可以把接收缓冲区理解为一个阻塞队列。B 的应用程序,在调用 read 方法的时候,就是从接收缓冲区中来取数据。通过 ACK 报文来看缓冲区大小,如果接收方反馈的窗口大小是 0,那么发送方也会发一个信息,用来探测有没有空间。
可以把其当成一个蓄水池,一端负责进水一端负责排水,当我们进水过多的时候就予以相应的警告我们的进水量就会相应的减少当水位低的时候也会给出相应的提醒然后进水量增加。
拥塞控制(安全机制)
拥塞控制也是滑动窗口的延申,也是限制滑动窗口的发送速率。控制衡量的是:
1.发送方到接收方 整个链路之间的拥堵情况,都会影响传输速率。
2.开始的时候,以一个比较小的窗口来发送数据,如果数据很流畅的到达了,那么就加大窗口大小。
3.加大到一定程度之后,出现了丢包(就意味着链路出现拥堵了),然后再减小窗口。
4.通过反复的 增大/减小 最终达到”动态平衡“。通过拥塞窗口来限制滑动窗口的大小。
5.最终滑动窗口的大小 由 拥塞窗口 和 流量控制窗口共同决定,由小的一方决定。一旦丢包,就立刻让窗口变小,降低速度,然后继续去试探的增大。
6.如果把窗口一下变得很小,就是期望这次传输,一定能成功。
总结下来就是说我们在刚开始的时候不知道整个链路是个什么情况会不会导致阻塞拥堵的情况,然后我们的滑动窗口的大小就会很小,接着窗口大小以指数形式来增长以此试探我们所能接受的最大值,一旦超过最大值就会又变成最小值。
值得我们注意的是我们的窗口增长不是一直以指数形式来增长的,当达到我们的阈值的时候就改为线性增长了。
当TCP开始启动的时候,慢启动阈值等于窗口最大值;
在每次超时重发的时候,慢启动阈值会变成原来的一半,同时拥塞窗口置回1;
延迟应答(效率机制)
如果接收数据的主机立刻返回ACK应答,这时候返回的窗口可能比较小,当我们的窗口比较小的时候如果我们的ack很快的应答的话那么我们所的发送的也会很少此时的传输是一个比较阻塞的状态,这个时候我们的效率就会比较的低,那么我们如果等一会再应答的话此时我们的窗口可能就会相应的大一点,此时我们的吞吐量也会相应的大一些,提高了效率。
捎带应答(效率机制)
在延迟应答的基础上,我们发现,很多情况下,客户端服务器在应用层也是 "一发一收" 的。意味着客户端给服务器说了 "How are you",服务器也会给客户端回一个 "Fine, thank you";
那么这个时候ACK就可以搭顺风车,和服务器回应的 "Fine,thank you" 一起回给客户端。
面向字节流
因为我们的TCP传输数据是面向字节流的,这样就会导致一个粘包问题
如上所示我们客户端发送的数据在服务端会被接受然后拆分获取里面的主要的数据,最后放在缓存区等待socketAPI的拿取,但是问题来了,应用程序看到了这么一连串的字节数据,就不知道从哪个部分开始到哪个部分,是一个完整的应用层数据包。
解决:明确边界
对于定长的包,保证每次都按固定大小读取即可;例如上面的Request结构,是固定大小
的,那么就从缓冲区从头开始按sizeof(Request)依次读取即可。
对于变长的包,可以在包头的位置,约定一个包总长度的字段,从而就知道了包的结束位
置。
对于变长的包,还可以在包和包之间使用明确的分隔符(应用层协议,是程序猿自己来定
的,只要保证分隔符不和正文冲突即可)。
TCP异常处理
进程终止:进程终止会释放文件描述符,仍然可以发送FIN。和正常关闭没有什么区别,要经历四次挥手。
机器关机:和进程终止一样。
机器断电/网线断开:
1.如果是服务器那边出了问题的话,我们服务器会接受不到ack,然后就会进入超时重传,当重传几次时候仍然接收不到ack就会尝试重新连接,如果重新连接仍然失败那就放弃链接。
2.如果是客户端出了问题,服务端会以为客户端一会会继续发送先会等一会,等一会还不发送就会发送试探报文如果接收不到ack那就放弃连接。
网络层
IP协议
IP协议的主要作用就是完成两个方面的工作,一个是地址的管理一个是路由的选择。
下面就简单的介绍一下这个写个各个部分是干嘛用的:
1)4位版本号:当前的的版本号只有两种,一个是4一个是6,分别对应的是ipv4和ipv6。
2)4位首部长度:与tcp一样IP的报头也是可变的。
3)8位服务长度:TOS说是8位其实有效的就只有4位,这4位分别对应着最小延时,最大吞吐量,最高可靠性,最小成本,因为我们网络层的作用是规划一个最优的路线,所以需要根据情况在这四种状态下切换。
4)16位总长度:也就是64k,和UDP比较类似,也就约束了我们一次性传输的上限就是64k,那么如果我们想要传输较大的数据该怎么办呢?
5)16位标记:16位标记就是用来拆包的当我们想要传输较大的文件的时候我们可以把大的文件拆成一个一个小的文件
6)13位片偏移:刚刚我们对一个数据进行了拆包之后那么问题又来了,我们在进行组合的时候怎么知道谁在前谁在后呢,我们的13位片偏移就解决了这个问题,相当于给我们的每个包做了一个序号标记哪一个在前哪一个在后。
7)3位标志:3位标志的目的是给我们的最后一个包做标记告诉我们这是最后一个包了。
8)8位生存时间:我们的每个数据报在刚开始的时候都会有一个TTL(常见的128,64),然后当我们每经过一个路由的时候其值就-1,当其值减到0的时候所在的路由就会将其抛弃,这样做是为了将一些到达不了的地址还在网络中传播的数据报给抛弃减少网络的阻塞。
9)8位协议:告诉我们传输层使用的是哪一种协议,是UDP还是TCP协议。
10)16位首部校验和:校验我们传送的数据是否正确。
地址管理
源IP:发送人的地址。
目的IP:目的地的地址。
我们的IP地址占32位,我们用三个点将其分成4份,每一份所能表示的范围就是0-255。
其中IP地址又分为两个部分:网络号+主机号。
网络号就是我们所处的网络,而主机号就是我们所处网络的主机。
子网掩码
子网掩码格式和IP地址一样,也是一个32位的二进制数。其中左边是网络位,用二进制数字“1”表示,1的数目等于网络位的长度;右边是主机位,用二进制数字“0”表示,0的数目等于主机位的长度。
特殊的IP地址
1.如果主机号全是 0,那么该 IP 就表示网络号(局域网里的正常设备,主机号不能为 0)。
2.如果 IP 的主机号全是 1,该 IP 就表示”广播地址“,往这个广播地址上发的消息,整个局域网中都能收到。
3.IP 地址是 127 开头的,就表示”环回 IP“表示主机自己(127.0.0.1:环回 IP 中的典型代表)。
4.IP 地址是 10 开头 或者 192.168 开头 或者 172.16-172.31 开头,就表示该 IP 地址是一个局域网内部的 IP(内网 IP)。
5.除此之外,剩下的 IP 称为 外网IP(直接在广域网上使用的 IP)要求外网IP 一定是唯一的,每个外网 IP都会对应到唯一的一个设备,内网 IP 只是在当前局域网中是唯一的,不同局域网里,可以有相同的内网 ip 的设备。
IP地址不够用问题
ipv4协议下的地址是32位也就是能够分配42亿九千万台的机子,但是这个数量对于人类来说慢慢的就不够用了,那么该怎么解决这个问题呢?
1.动态分配 IP 地址:每个设备连上网的时候,才有 IP,不联网的时候就没 IP(这个IP就给别人用),通过 动态IP 来解决 IP地址不够用的问题。
2.NAT 机制:让多个设备去公用一个外网(IP),就是把网络分成外网和内网,外网是唯一的,但是内网不是唯一的,内网可以共用一个 外网的IP。也就是这里的外网 IP,会给所有接入这个运营商设备的局域网都来使用这个 外网IP。外网区分内网的数据,就是通过端口号来区分的。网络的连接,是一个 五元组,即使是 IP 相同也没事。
a)对于一个 外网IP 来说,可以在互联网的任意位置都能访问到,
b)对于一个 内网IP 来说,只能在当前局域网内部访问,就是 局域网1 的设备,不能使用内网访问 局域网2 的设备。
c)内网IP 是可以重复的,只有在局域网内才是唯一的。
d)不同的局域网访问的话,就需要一个带有 外网IP 的机器,然后 局域网1 把数据放到 外网服务器,然后 局域网2 就可以访问到外网服务器,就会修改 IP数据报,把内网发出的 源IP,改为 外网IP。
e)NAT 也是存在极限的,端口号个数是 65535,如果一个局域网内的连接数超过了 65535,这时候 NAT 就不一定好使了,因为端口号不够用了。NAT 相当于是续命了,并不是从根本解决。
IPV6庞大的地址数量可以彻底的解决当前的问题。
路由管理
路由选择也就是我们在网络层中路线的规划,我们想要把数据传到指定的位置那就需要去规划路线。
我们数据在网络中传播的时候是不能一下子就知道具体的路线的而是走一步再看一步,我们每走到一个路由器就会向其询问目的的路线,如果知道那就告诉我们该怎么走,如果不知道的话就会给我们一个大概的方向然后我们按照方向走到那个位置再去问新的路由器。
那么我们路由器是怎么知道的呢,因为路由器内部维护了一个数据结构(路由表),记录了一些网段信息(网络号,)以及每个网络号对于的网络接口。
数据链路层
数据链路层主要的协议是:以太网。每个网卡,都有 mac地址,是唯一的,在网卡出厂的时候就写死了,以太网数据帧。
其中目的IP和源IP是6个字节表示我们数据传输过程中相邻的两个节点的mac地址,这两个地址是不断的变化的。
MTU:一个以太网数据帧能够承载的数据范围,这个范围取决于硬件设备。以太网和硬件也是密切相关的,不同的硬件设备,对应的 数据链路层协议,可能又不一样,MTU 也不相同。MTU 对IP 的影响;主要影响就是分包的过程。
MSS:TCP 中在 IP 不分包的前提下,最多搭配多少载荷:
a)一方面取决于 MTU。另一方面取决于 TCP 和 IP 的报头。
b)当 TCP 传输的数据长度不高于 MSS 的时候,是处于最高效的状态。
c)ARP 报文并不是用来传输数据的,只是起到一个辅助的效果。
d)路由器拿到一个 IP地址(目的IP),通过 IP地址,来决定接下来这个数据咋走(从哪个端口出去,发到哪个设备上)。
d)所以就得决定,接下来封装的以太网数据帧,目的 mac 是啥,需要根据 ARP协议,建立起 IP -> mac 这样的映射关系。
e)当设备启动的时候,就会向局域网当中,广播 ARP报文,每个设备收到的时候,都会做一个应答。
f)应答信息中就包含了自己的 IP 和 mac,发起广播的那一方,就可以根据这些回应,建立起这个映射表了。
特殊协议
DNS:DNS是一个应用层的协议主要是为了对域名进行解析,对于一些难记的地址,就可以使用一串英文单词来表示这个 ip地址,这串英文单词,就叫做域名。域名和 IP地址 之间是一一对应的关系。
DNS 系统:DNS 系统,最开始的时候,只是一个普通文件,称为 hosts 文件。
现在 hosts 文件已经不使用了,因为域名太多了,不可能去一直修改 host 文件。于是有了一个机构,专门负责处理这些 域名 和 IP 的对应关系。如果想要对域名进行解析的话,就访问一下这个服务器(域名解析服务器)就好了,每个城市都会有镜像服务器,我们一般都是在访问镜像服务器。
当我们的主机,查询了 DNS 之后,主机就会把这个查询结果缓存一定的时间(浏览器来进行缓存)下次再访问到同一个域名的时候,就可以省略查询 DNS 的过程了。
NATP:这个就是当 端口号重复时的情况,它的处理机制就是 NAPT。
这种关联关系也是由NAT路由器自动维护的。
例如在TCP的情况下,建立连接时,就会生成这个表项;
在断开连接后,就会删除这个表项
版权归原作者 ~小明学编程 所有, 如有侵权,请联系我们删除。