目录
[4.google protobuffer](#4.google protobuffer)
网络原理站在具体协议的角度 进一步讨论,互联网中主流的是TCP/IP五层协议。
应用层是程序员日常开发最常用的一层,这一层的协议可以由我们自己实现也可以使用大佬创建好的。
其他层的协议,操作系统,硬件,驱动都是已经实现好的,程序员不能自定义。
传输属的几种格式
自定义协议并不是非常遥远的事情,很简单很朴素。主要分为两个步骤:
1.明确前后端交互过程中,需要传递哪些信息。
一个外卖界面要有商家列表,此处传递的信息就是用户的ID,用户所处的位置,用户的评分等等。
2.明确,组织这些信息的格式。(使用现有的格式)
当前的信息组织个是有很多,但是要确保前后端使用的是同一个格式。
使用文本行来组织上述数据:
请求:用户ID,用户位置,店铺评分\n;
响应:商家ID,商家位置,商家评分\n;
实际开发中很少使用文本行,有这么几种格式:
1.xml:通过成对的标签表示键值对信息。
<request>
<userid>1001<userid>
<position>E45N60<position>
<request>
每个标签都是成对存在的,这就非常消耗网络带宽。网络通信中,带宽是非贵的硬件设备。
xml也可以作为配置文件,xml与html相似,html也是通过成对的标签夹住一些内容的。
xml里的标签都是程序员自定义的。html的标签都是固定的(每个标签都有自己的含义)
2.json:当前更主流一点的,网络通信的数据格式
相比于xml,可读性更加好,同时也能够节省网络带宽
{
"user":1001,
"position": "E60S45"
}
json也是"键值对"格式,
键和值:分割
键值对之间使用,分割
所有键值对用 { } 包含起来。
可读性很好,但是没有明确要求数据的格式,就会有些极端的情况。
比如:{ "userid":2001,"position" : "S60E45" },这样也是不好看的。
3.yml(yaml)强制要求数据的组织格式
yml要求键值必须独占一行,而且"嵌套结构"必须通过缩进表示,强制要求成为可读性非常高的格式。
request:
userid:1001
posiiton: "E45N60"
4.google protobuffer
前三个方案都是关注可读性,protobuffer关注性能,牺牲可读性(通过二进制来组织数据)
而protobuffer直接通过 "位置"约定字段含义,不需要传输 key的名字,也会针对传输的数值进行二进制编码,起到一些压缩效果。
极大的缩减啦传输数据的体积,减少啦消耗的带宽,传输的效率更高。但是二进制数据无法肉眼阅读,调试程序时,就会比较麻烦。
传输层
1.端口号:
是一个整数用来区分不同的进程。同一个时刻同一个机器上的同一个协议,一个端口只能被一个进程绑定**。一个进程可以绑定多个端口号。**
服务器绑定多个端口控制端口,调试端口,监控端口等等。
端口号两个字节表示,0->65535,其中0->1023已经是知名端口。日常开发会避开这些端口.虽然是知名端口但是其中大部分已经使用不上了,现在一些知名的服务器使用的端口号反而是1024以上的了。
UDP协议
学习网络协议主要就是学习**"数据格式","报文格式"**
表格中端口号,长度这些属性换行了,但是实际中这些都是在同一行的元素。
前四个字段一共8字节,长度是固定的。报头的四个字段没有指定分隔符,而是通过固定的长度来进行区分的。
使用两个字节表示端口号范围是0->65535,如果端口号是10W,那么就会发生截断。
UDP报文长度就是指报头+荷载长度。单位是字节。
比如报文长度是1024,那么整个UDP长度就是1024字节。
最大是65535字节,约为64KB.
网络通信中四个关键信息:源IP(发件人地址),目的IP(收件人信息),源端口(发件人电话),目的端口(收件人电话)。
因为UDP的长度是有限制的,传输数据的时候可不可以把UDP长度单位改为4个字节?
不可以,因为数据传输的两端UDP要保持统一,网络协议是不可以随便修改的,即使我们修改的自己电脑的UDP实现,但是我们改变不了所有客户端上的UDP实现。相比于修改UDP协议,不如使用新的协议替代UDP协议,这种方式更简单一些。
关于校验和:
前提:网络传输中,非常容易出现错误。因为大气中的光信号/电信号/电磁波可能会影响到传输信号的改变。(电场与磁场会相互影响的)。比特位会翻转:0->1,1->0
校验和 存在的目的就是能够 "发现"或者 "纠正"这里出现的错误 ,就可以给传输的数据中引入额外的信息(checksum),用来发现/纠正 传输数据中的错误。
这里只是发现错误,携带额外信息很少。如果是发现错误并且修改那么要携带的额外的信息就要很多。
如果发现错误就会丢弃掉,并不会让对方重发。
校验和具体是怎么工作的呢?
根据每一段的数据内容片段生成校验和,
举例:
比如每个书名就是一段数据,校验和就根据这些数据的片段生成校验和 , 比如**取每个书名的第一个字作为校验和,**如果第一个字对上了那就说明这个书名是对的
也有书名第一个字是对的,但是中间的字出错,恰好没有校验出来这种情况也是有的,但是好的校验算法发生这个问题的可能性很低.
CRC校验和(循环冗余校验)
UDP中使用两个字节作为校验和的长度,把UDP数据报整个遍历一遍,分别取出每个字节 进行累加, 由于数据可能会很大, 就会使结果溢出,但是没有关系**,重点关心不是加的和最终是多少 ,而是关心校验和结果是否会在传输发生改变.**
计算校验和的过程中是否能会出现两个不同的数据生成校验和相同呢?
可能会,但是概率非常之低. 但是这个问题对于CRC来说相对高一点,实现校验和除了使用CRC,还会使用另外两个典型的算法:MD5,和SHA1
MD5算法本质上是一个**"字符串hash算法". 背后实现的是数学公式.**
MD5 优点:定长,分散,不可逆;
1.定长:无论输入的字符串多长,算出的md5结果都是固定长度.------>适合做校验算法
2.分散:输入的内容,哪怕只有一点点改变,得到的md5值都会相差很大 ------>适合做hash算法
3.不可逆:根据输入的内容,计算MD5, 非常简单,但是想要已知md5值,得到初始内容理论不行--->做加密算法
得到的是十六进制的数,仅仅是相差一个字母得到的加密结果却相差甚远.越分散,越不容易发生hash冲突.
TCP协议
相比于UDP更加复杂
4位首部长度
TCP的报头长度,是四个比特位 0000-->1111,此处的单位长度是4字节,也就是最大报头长度是2^8*4字节. 这个4字节单位长度是当年大佬发明TCP约定俗成的.
保留(6位)
这个是保留位,现在虽然不用,但是未来TCP需要新增属性或者某个属性的长度不够用了,就可以把保留位拿出来应对. 充分吸收啦UDP的教训: 报文长度没法扩展,
TCP的基本特点:有链接,可靠传输,面向字节流,全双工
可靠传输是在代码层面感知不到的, 是系统内核完成啦这份工作. TCP的核心机制就是可靠传输.
可靠传输不能做到 100%的送达,只能尽可能使数据送达到对方.
1.能感知到对方是否收到,
2.发现对方没有收到就会进行重传
可靠传输核心功能之一/TCP核心机制一:
确认应答,感知到对方是否收到
所谓感知就是要靠对方告诉你是否收到.
举例:
这个时候小明在收到应答报文的情况下知道小红同意的情况下会进行下一步的准备.
但是如果小明发送多个消息就可能产生歧义
如果应答报文按照上述的顺序进行传输 就没有问题.但是网络传输中的数据经常会发生先发后至的情况,因为传输两个数据报不一定都是走同一个路线,走不同的路线传输的到达的时间也不一样.
所以就有可能发生如下为情况
所以针对应答报文先发后至的情况 通过添加**"编号"** 可以区分出数据的先后顺序
这样即使收到的应答报文的顺序混乱 也可以区分出来针对那一条的数据报的应答报文
如何编号?
由于TCP是面向字节流的,实际上的编号不是按照 "第一条""第二条"这样的编号方式进行的.
而是按照"字节", 第一个字节,,,,,第100字节
每个字节都有独立的不同的编号, 编号之间是连续递增的.
按照字节编号这样的机制,就成为"TCP"序号, 在应答报文中针对之前收到的数据进行对应的编码,称为 "TCP确认序号 "
32位序号:表示的就是TCP荷载中第一个字节的序号开始到32位结束,由于序号是连续递增的,所以也就是知道了每一个`序号.
TCP传输数据:
32位序号/4字节 最大表示0->42亿九千万, 意味着TCP一次传输的最多4GB内容.
TCP是面向字节流的, 一个TCP数据报 和 下一个TCP 数据报 携带的数据是天然可拼装的. 如果要传输一个很大数据 传输中就会多个TCP 数据报来携带, 传输的过程中 这些TCP数据报彼此之间携带的载荷都是可以在 接受方 自动被拼接起来的.
不像使用UDP存在传输上限, 使用UDP就要考虑一次send的数据是否超过64KB, 使用TCP可以一次write, 也可以多次write,超过4GB也没有关系.
确认序号
确认序号的设定方式与前面发短信的例子有些不同.
确认序号也是按照字节来编排的.
这里确认序号是1001开始,确认序号是从接收方收到的最后一个字节的序号的下一位开始.表示的含义是<1000的序号数据都收到了.因为TCP序号是顺序增长的.
对于应答报文来说,确认序号就会按照收到数据最后一个字节号+1的方式来填写.另外六个标志位的ack会设为1, 对于普通报文序号是有效的,确认序号是无效的. 对于应答报文 序号和确认序号都是有效的.只不过这里的序号是采用另一种方式编写的.
应答报文默认不携带数据.
再谈先发后至
TCP针对接受方收到的数据会在"接收缓冲区"重新排序, 使read到的数据一定是和发送方的数据顺序是一致的.
B接收方这会在操作系统内核里留有一端内存空间,作为"接收缓冲区" ,收到的数据就会现在内存缓冲 区里排队等待,直到开头的数据到了,应用程序才能真正读取到里面的数据.
接收方这边调用read的时候 如果没有数据就会阻塞等待,scanner读取本质是InputStream.read.
**就算10001-2000 这个数据到了,但是1-1000这个数据没有到 B就不会让read读取这个数据,直到1-1001这个数据到了,才会解除阻塞,**读取数据.这个就是确保write的顺序 和 read的顺序始终是一致的
丢包
网络传输不是一帆风顺,可能会丢包。
丢包的原因有如下:
1.数据传输的过程之中,发生了比特位反转 ,这个数据的接收方/中间的路由器啥的 ,计算校验和发现对不上了。就会把这个数据报丢掉,不继续往后转发不交给应用层使用。
2.数据传输到某个节点发现 (路由器/交换机)发现这个节点负载太高啦 。单位时间内这个节点并不能转发这么多,那么多出来的就会丢弃掉。
丢包与否完全是随机的,不可预测的。
TCP如何让应对丢包?
能做的就是感知到是否丢包 ,如果丢包就重新再发一次。那么如何感知到丢包呢?
需要通过应答报文来区分,收到应到报文就说明没有丢包,没有收到就说明丢包了。 但是网络中的数据传输需要时间的,发送方会给出一个**"时间限制",如果暂时没有收到应答报文会等待一段时间,超过这个时间限制就会认为这个包丢了,此后在再到这个包已经无效了。**
丢包有如上两种情况:一种是数据压根没有传输到接收方 ,还有一种是接收方收到了数据在返回ack的时候这个ack丢了 ,并没有被发送方收到 。此时超过时间限制之后就会认为这个数据丢了 ,就会超时重传,也就是会把这个包重新传输一遍。但是此时接收方会受到两份一样的数据。
去重:TCP接收方会有一个缓冲区,先放入缓冲区里,会根据序号在缓冲区里找,如果找到两份一样的数据,就会把新收到的数据丢弃,确保应用程序调用read出来的数据是唯一不重复的。接收方会再次返回一个针对同一个数据的ack。
可靠传输核心功能之二/TCP核心机制二:超时重传
这个时间不是固定的,而是动态变化的。
发送方第一次重传的时间是t1,如果重传之后仍然没有ack,会继续重传。第二次重传的时间是t2
t2 > t1 。 每次重传超时间的间隔都会变大,重传的频次都会降低。
每经过一次重传,超时间的间隔都会变大,到达接收方的概率都会提升很多。如果很多次重传都都没有顺利到达,说明网络丢包率已经非常高了,不能再继续使用了。
重传不会无休止进行 当重传次数一定后 就会认为放弃这个连接已经挂了,会进行 "重复/复位连接",发送一个特殊的 "复位报文" 。如果网络恢复了,复位报文就会重置连接,使通信可以继续进行。如果网络还是有严重的问题,复位报文也没有回应,此时TCP就会单方面放弃这个连接。
连接就是通信双方各自保存对方的信息,有一方保存的信息释放掉,这个连接就无了。
总结:确认应答 与 超时重传 共同构建了TCP的 "可靠传输机制"。TCP的可靠传输,不是通过"三次握手,四次挥手"保证的。
TCP核心机制三:连接管理之三次握手
建立连接:
三次握手四次挥手,这里的握手和挥手指的是网络通信的次数 ,网络中的握手 就是**发送不携带业务数据的(没有载荷,只有报头的)数据报,**但是能起到打招呼的效果。
上述的连接就建立完成了,本来是四次交互但是中间两次合并了 ,就成为了三次握手。 合并很重要分两次发送与和并发送 效率是打折扣的。
这里的syn 与 synchronized 相似, synchronized 表示互斥,syn表示同步。这里意思是客户端希望服务器可以与他统一步调完成后续操作。建立连接断开连接都会有超时重传。
建立连接是一个双向操作,A给B说需要保存你的信息(syn),B也要给A说保存你的信息(syn)
为什么要进行三次握手?意义何在?
三个方面:
1.投石问路,初步验证通信链路是否畅通,这是进行可靠传输的"前提条件"。
2.确认通信双方的发送能力 与 接受能力 是否都正常。
3.让通信双方在通信之前,对通信过程中的一些关键参数进行协商。
(避免前朝的箭,斩本朝的官)
为什么要进行协商?避免历史遗留的数据干扰本次连接, 因为在传输数据时避免不了有的数据会迷路,以至于到下一次两个主机连接但是是不同的应用程序, 此时那个迷路的数据就传输到了接收方,但是这个数据对于本次的连接已经没用了,所以就要舍弃, 每次建立新的连接TCP的其实序号就会差距很大,**如果发现某个数据的数据号都与之前都收到数据差距很大,**那么它很有可能是之前遗留的数据,应该舍弃。
TCP的可靠传输是通过"确认应答""超时重传"体现的,三次握手只有一定的支持性,但不是关键因素。
连接管理之二,四次挥手
四次挥手的目的是把各自保留的对端信息删除掉
这里的断开连接不一定是客户端主动,服务器也可以主动。
通信双方各自给对方发送"FIN",各自返回对方的"ACK",三次握手中间两次可以合并在一起,因为中间两次的操作SYN,ACK都是在操作系统内核完成的,同一时机就可以合并。
对于四次挥手来说,中间的ACK是在操作系统内核完成的,但是FIN的触发是通过应用程序调用close/进程退出,来触发的。所以两个操作执行的时机不一样所以很难合并。
LISTEN:服务器进入的状态,服务器端口绑定好后,相当于进入listen状态了,此时服务器就已经完成初始化了,准备迎接客户端了。
ESTABLISHED:客户端和服务器都会进入的状态,TCP连接建立完成(保存了对方的信息),接下来就可以进行数据业务的通信了。
CLOSE_WAIT:被动断开连接的一方会进入这个状态,先收到FIN的一方,"等待执行close"
如果发现存在大量的CLOSE_WAIT状态的TCP连接,说明被断开连接的一方代码可能有bug,排查close是否写了/执行到了。
TMIE_WAIT:主动断开连接的一方会进入这个状态,此时的TIME_WAIT按照时间等待,到一定时间后,连接就释放了。
为什么不直接释放,而是要等待一段时间呢? 因为防止最后ACK丢包,被断开的一方发送FIN后再给被断开的一方返回一个ACK就知道了刚刚那个FIN没有丢 ,**如果直接收到FIN后断开,则发送FIN的一方不知道FIN有么有发送过去,而且如果最后一个ACK没有成功到达,也可以再次发送一个ACK。**TMIE_WAIT存在的最长时间是2ML(ML是网络传输中消耗的最大时间)。系统不一样M时常也不一样。
TCP核心机制四:滑动窗口
可靠传输 会降低传输的效率,为了能够尽可能弥补效率的损失引入了滑动窗口
即使引入滑动窗口但是效率依旧比不上UDP
没有滑动窗口传输如下
每次需要收到ACK后才发下一个数据。
有滑动窗口
改进:把一次发送一个变为 一次发送一批,把多次等待ACK的时间,合并成一份时间了,批量发送的数据越多,认为效率越高。数据的量就是"窗口大小"
一次发送四个数据,当收到了第一个ACK之后,只发送下一个字节流,收到了2001的ack,就说明1-1000,1001-2000的数据已经得到应答了,因为在接收缓冲区这个写数据必须按顺序读取,不然会发生阻塞。此时会再发送6001-7001,7001-8001的数据。
滑动窗口出现丢包的情况
丢包情况一:数据报已经过去,但是ACK丢了。
这种情况不用担心,批量发送数据,批量ACK。 丢了多个ACK只是其中的一部分不会全部丢失。
虽然1001丢了,但是5001返回了,意味着5001之前的数据都已经收到了。
情况二:数据包丢了。
此时1-1000,2001-3000数据都收了,但是1001-2000数据丢失了。此后无论A给B传输那个序号断的数据,B返回的ACK都是1001,B就是在向A索要1001-2000的数据。当主机收到多个1001ACK后,主机就知道了1001的数据丢了,主机就会重新发1001-2000的数据。
当1001-2000数据传输过来后,由于2001-7000都是发过的,A会立即发送7001以后的数据。
快速识别那个数据包并且针对性的重传,其他都不需要重传这个过程称为:"快速重传"
快速重传:是滑动窗口搭配的超时重传。
快速重传与超时重传并不冲突:通信双方发送的数据比较小就会 按照"确认应答""超时重传"规则
如果单位时间发送数据很多,就会按照"滑动窗口""快速重传"规则。
TCP核心机制五:流量控制
流量控制约束发送方的发送速度
滑动窗口的窗口大小 对于数据的传输性能是直接相关的,但是窗口不能无限大。
发送方发送的速度确保接受处能处理的过来 以及 中间的数据链路层也能应付得来。
A到B的传输不是直接给B,而是先将数据传输到B的接收缓冲区 中(内核中的内存空间,每个socket对象都有一个这样的空间),这个缓冲区类似于一个阻塞队列BlockingQueue,然后B再调用read相关方法读取数据。
也可以通过定量的方式来实现制约。看内存缓冲区的空间大小。
空闲空间越大 就可以认为应用程序处理数据的速度比较快,就可以让发送方发送的速度也快一点,设置一个更大的窗口。
如果空闲空间按比较小 ,就可以认为应用程序处理数据的速度比较慢,就可以让发送方发的慢一点。设置一个更小窗口。
TCP中接收方收到数据的时候 ,就会把接收缓冲区剩余空间的大小 通过ACK数据报 反馈给发送方
下一步发送方 就可以依据这个数据报 来设置发送的窗口大小。
这个标红的16位窗口大小 就是接收缓冲区的剩余空间的大小,这个属性只有在ACK报文中(ACK为一)才有效。
此处的16位表示的范围是64kb,但是这不意味着发送方的窗口就只有64kb 。因为选项中可以设置一个特殊的选项 "窗口扩展因子",发送方窗口大小 = 窗口大小 << 窗口扩展因子。
流量控制不是TCP独有的,其他协议也有流量控制(比如数据链路层也有协议,也支持流量控制)
TCP核心机制六:拥塞控制
这个操作也是和刚才的流量控制有关的。
流量控制 是在接收方的视角来限制发送方的速度的。
拥塞控制 是在传输链路的视角来限制发送方的速度的。
假设B的处理速度非常之快,难道A就能无限制速度的发送数据吗,不行 因为中间的链路上的设备可能承受不住。
如果此时标红的节点负载已经很高了,A以很快的速度发送,这个节点就有可能丢包。
流量控制就可以精准的 使用 接收缓冲区剩余空间 来衡量。
但是 拥塞控制 需要考虑的就很多:
1.中间结点非常多
2.每次传输走的路线不一定一样。
3.中将哪个节点遇到瓶颈都不好说
4.中间节点传输的数据不只有A的 还有其他设备的数据。
最终采用一种动态的模式来寻找最合适的发送速度
1.先拿找一个较小的速度进行发送
2.数据非常畅通没有丢包,说明传输数据整体比较畅通,就可以加快传输数据速度。
3.增达到一定值后,发现开始丢包,网络可能存在拥堵,就减慢传输速度。
4.减到一定程度后,不丢包就继续加速。
5.加速后又丢包了就继续减速。
拥塞控制中,窗口的大小具体变化过程:
1.刚开始传输数据,拥塞窗口非常小,用一个很小的速度来发送数据
2.发现并不丢包,就以指数增长窗口大小
3.增长到一定程度,达到某个阈值 就会停止指数增长,变为线性增长
4.线性增长,也会持续使发送的速度越来越快,达到某个阈值就会出现丢包。
此时就会减小窗口大小:
经典过时的方案,回归到开始非常小的初始值再重蹈覆辙,指数增长,线性增长
现在的方案:回归到新的阈值上,开始线性增长。(以后都不会指数增长了)
流量控制 会限制发送窗口,拥塞控制也会限制发送窗口,最终的窗口大小取决于这两个机制的得到的窗口较小值。流量控制与拥塞控制都是对可靠传输的补充
TCP核心机制七:延时应答
延时应答提高效率,是ACK返回时携带的窗口大小更大
如果此时ACK返回,那么返回的窗口大小就是4KB,但是要ACK延时返回 原有5KB又被接收方消耗掉3KB,此时再让ACK返回,那么ACK携带的窗口大小就是(4+3)KB,那么下一次又可以传入7KB的数据了。
但是延时返回ACK也不一定就会返回的窗口更大,因为接收方此时可能没有接收数据,延时的时候可能也会有新数据发过来。
TCP核心机制八:捎带应答
捎带应答在延时应答的基础之上 提高效率,把返回的业务数据 和 ACK 合二为一了。
网络通信中大部分是一问一答的形式
正常情况下,ACK 和 响应是不同时机的无法合并,但是ACK 涉及到" 延时应答"返回时间就回往后拖,就可以能赶上下一次发送的响应了,于是响应的的信息中ACK也带上了。
本身ACK 不需要荷载,报头中ACK这一位设置为1,设置窗口大小值,设置确认序号。
而响应数据主要是设置荷载和ACK不冲突 可以共存
延时应答,捎带应答都是TCP提升性能的机制。
TCP核心机制九:面向字节流
读写 100 个字节数据 可以有很多种方式,一次读100个,一次读50个,一次都10个等等。
无论怎么读 最终接收方收到的数据就是100个字节。但是所有字节都是紧挨在一起的,这就是粘包问题。
面向字节流的数据大多都会涉及到粘包问题,粘的是TCP携带的荷载
应用层数据包在TCP的接收缓冲区中,连成一片黏在一起,称为"粘包问题"
应用程序需要读取接收缓冲区中的数据,由于TCP面向字节流,读取可以很多种方式
一次读取一个a,a,a,b,b,b,c,,,,,
也可以 aaa,bbb,ccc, 还可以:aa,a,bb,b,cc,c,,,,,
但是实际上aaa,bbb,ccc才正确的读法。
可以采用以下几种数据格式:
方案一:指定分隔符
xml , yml , json 都可以采用这种方案
适用于文本类的数据,在之前模拟的回显客户端服务器TcpEchosever的时候,约定请求响应都是以\n结尾
发送请求响应的时候,专门使用println写数据。
读取请求响应的时候,专门使用scanner.next 按照\n进行解析
前提是:数据内容不能包括\n, 保险起见可以使用ASCII表中比较靠前的"控制字符".。
方案二:指定数据长度
protobuf 采用这种方案
比如 约定,每个应用层数据包,开头的2/4字节,表示 数据包的长度。
接下来应用程序读取的时候就按照 这个长度读取数据包,一个长度代表一个数据包。
UDP传输数据报不涉及以上问题。send/receive 得到的就是一个完整的数据报。