目录
1)xmlxml)
2),JSON,JSON)
3),yml,yml)
4),google protobuffer,google protobuffer)
一,自定义应用层协议:
1,明确前后端网络交互过程中,需要传达的信息,
2,明确组织这些信息的格式.(哪种方式都可以,但是需要前后端保持一致)
1)xml
xml(传统的方案):通过"成对的标签"表示"键值对"信息
例如:<request><userID>1001</userID><position>E45N60</position></request>
xml既可以用来网络传输,也可以用来表示配置文件,但是由于网络传输中有一个明显的缺点,消耗大量的宽带,传输标签的时候,所以现在基本上只用来用作配置文件了
xml里面的标签都是程序员自定义的,HTML的标签,都是固定的(已经有一套标准)
2),JSON
JSON 当前更主流,网络通信的数据格式,相比于xml来说,可读性更好,同时能够节省一定的宽带
{"userID":1001,"position":"E45N60"}JSON也是"键值对"格式,键和值使用':'分割,键值对之间,使用','分割,所有的键值对都是用'{ }' 括起来
3),yml
(yaml)强制要求了数据组织格式,yml强制要求了键值对必须独占一行,而且"嵌套"结构,必须通过缩进来表示(强制写成可读性更高的格式)
4),google protobuffer
前三种格式都是关注可读性,protobuffer关注性能,牺牲了可读性(通过二进制的方式组织数据的),而protobuffer直接通过"位置"约定字段的含义,不需要输入可以的名字,也会针对传输的数据,进行二进制的编码,起到一些"压缩"效果(极大地压缩了传输的数据的体积=>宽带消耗越少,效率越高)
二,传输层UDP/TCP:
端口号:端口号是一个整数,用来区分不同的进程的,同一时刻,同一个机器上,同一个协议,一个端口只能被一个进程绑定,而一个进程可以绑定多个端口号,端口号是同过2个无符号整数表示,取值范围是0-65535实际上0这个端口号比较特殊,不会使用(0就相当于随机设置一个空闲的端口号,1-1023属于已经预定好的(有一些知名服务器,已经提前预定了这个端口)这样的端口称为"知名端口号",咱们日常开发的时候,也会避开这些端口)例如:80=>http,443=>https,22=>ssh
编写服务器,肯定至少需要绑定一个端口,和客户进行交互,这样的端口称之为"业务端口",在服务器运行的过程中,希望能够对这个服务器进行一些控制,例如服务器加载某个数据/某个配置/或者修改服务器的某个功能.也可以通过网络通信完成上述功能,就可以让服务器绑定另一个端口,通过这个端口,编写一个客户端,给服务器发送一些"控制类"请求,这样的端口就称之为"管理端口",需要针对服务器的运行状态进行检测和调试,需要查看服务器运行中的某个相关的变量,就可以让服务器绑定另一个端口,然后实现一些相关的打印关键变量的逻辑,客户端发送调试请求,这个端口就称之为"调试端口"
UDP协议:
报文长度由于是由两个字节来记录的,所以所记录的最大值是65535=>64KB,所以使用UDP来传输一个比较的数据,就会比较捉襟见肘,当前需要传输的数据较大时,
1,将一个大的包拆分成多个包进行传输,但是在传输过程中充满了不确定性,非常复杂,网络传输,本身就是一种不确定
2,直接使用TCP,对于TCP没有长度限制,TCP自身也带有可靠传输这样的机制,对于整体通讯质量 来说是有利的,且代码的修改成本比较低.
检验和(2个字节):网络传输过程中是很容易出错的,,电信号/光信号/电磁波,是会受到环境影响,使里面的传输的信号发生改变,检验和是为了能够"发现"和"纠正"这里出现的错误,就可以给传输的数据中,引入额外信息,用来发现/纠正传输的数据(如果只是发现错误,携带的额外信息比较少,但是如果想要纠正出错,携带的额外信息就会更多了,同时也会消耗更多的宽带),UDP使用的是一个比较简单的方案CRC校验和,把UDP数据报,都进行遍历,分别取出每一个字节,往一个两字节的变量上进行累加,由于整个数据可能很多,加着加着数据有可能会溢出,但是溢出了也没关系,重点不关心结果到底累加了多少,而是关心校验和结果是否会在传输中发生变化
具体流程:传输一个UDP数据报,基于这些数据计算出一个校验和checksum1,通过网络通信,接收方将得到的数据内容和校验和checksum1,这时,接收方会根据数据内容按照同样的方法再算一遍校验和,得到checksum2,如果传输过程中,数据没有发生任何变化,则checksum1==checksum2,反之,如果发现两者不相等,则说明数据传输中出错了.
出除了上述CRC之外,还有一些其他算法MD5和SHA1,MD5,本质上是一个"字符串hash算法",哈希表HashMap,key显然可以存String,通过Hash函数将String转化成一个数组下标,特点:
1,定长:无论输入的字符串长度,长度有多长,算出的MD5的结果就是固定长度的,因此=>适合做检验和算法
2,分散:输入的内容,哪怕只有一点点的改变,得到的MD5的值都会相差很大,因此=>适合做Hash算法
3,不可逆:根据输入内容,计算MD5,这个过程非常简单,但是如果已知MD5的值,想要推出来字符串,,这个过程理论上是不可能的,因此=>适合用来加密算法
TCP协议:
4位首部长度:4位代表4个bit,也就是可以存储0-15个数字,此处的长度单位是4字节(也就是1个数字代表4字节的长度),也就是可以存储0-4*15的报头长度
**保留(6位)**现在虽然不用,但是先把这个东西申请下来,以备不时之需,如果为了某一天想要增加TCP的某个属性或者某个属性的长度不够,就可以把保留位拿出来,用作应对的方案,这时,TCP不需要做出很大改变,这样就可以完成升级(主要是充分考虑到未来的扩展性)
6个标志位(最核心的属性)
TCP:有连接,可靠传输,面向字节流,全双工
TCP的核心机制一:确认应答
感知对方说是否收到,应答报文acknowledge(缩写为ack),网络中存在"后发先至"的情况,这种情况产生的原因??
答:传输两个数据包,两个数据包不一定走同一条路径,而每一个数据包传输的过程中,遇到的情况也各不相同,所以最终到达的时间就会存在差异了
"后发先至"是一个客观情况,但是传输的时候,可以给数据包编一个编号,来区分先后顺序,由于TCP是面向字节流的,所以并不是按照"第一条""第二条"这样进行编号,而是针对字节进行编号,每一个字节都有一个独立的编号,字节和字节之间的编号是连续,递增的.按照字节编号这样的机制就称之为"TCP的序号 ".在应答报文中,针对之前收到的数据进行对应的编号,就称之为"TCP的确认序号"
上者,表示的就是TCP数据包的载荷中的第一个字节的序号,由于序号是连续递增的,知道了第一个字节的序号,后续的每一个序号也就知道了.32位bit=>42亿9千万,就是4GB.由于TCP是面向字节流的,所以一个TCP数据包和下一个TCP数据包之间是可以拼接的.也就是说,如果要传输一个较大的数据,传输过程中就会采用多个TCP数据报来进行携带,这些TCP数据包彼此之间携带的载荷都是可以在接收端自动进行拼接的,这区别于UDP数据包,UDP在数据传输上存在上限,使用UDP传输大数据的时候就需要考虑调用一次send操作,参数是否超过64KB,超过64KB,就会传输失败,但是TCP不需要考虑这个问题,它可以调用一次write,也可以调用多次write(但是无论调用几次write,从接收放的角度来看,没有差别)如果数据量超过了4GB也没关系,数据序号是可以从0重新设置的
确认序号:
对于应答报文来说,确认序号就会按照收到数据的最后一个字节的序号+1的方式来填写,此外,六个标志位中的第二个(ack)会设置为1,在普通报文中,ack是0,应答报文中,ack为1.对于普通报文序号是有效的,但是确认序号是无效的,在应答报文中,确认序号是有效的,此时的序号(由来与另一个体系,与普通报文的序号不是一个体系下的),一般情况下应答报文是不带数据的(含有特殊情况)
由于一个较大的数据是由多个数据包分别进行携带的,所以也会有后发先至的情况发生,这时,接收方会将数据根据序号重新进行排序,从而确保read的数据和发送方的数据顺序保持一致,当接收方调用read的时候,没有数据会进行阻塞等待(前面是Scanner进行读取数据,实际上就是调用InputStream.read),当1001-2000的数据到了,并不会解除阻塞等待,还是继续保持阻塞等待的状态,直到1-1000这个数据到达之后,read才会解除阻塞的状态,才会读取到1-1000,1001-2000,2001-3000的数据,在接收方这边,操作系统内核会有一段内存空间,作为"接收缓冲区",收到数据,就会先在接收缓冲区中排队等待,直到开头的数据到了,应用程序才会真正的读取里面的数据
"丢包"情况的产生原因:
1,数据传输过程中,发生了bit翻转,收到数据的接收方/中间方路由器,计算校验和发现校验对不上了,就会进行丢包处理
2,数据传输过程中,传输到了某个节点,这个节点负载太高了,例如:某个路由器只能转发N个包,但是现在是网络高峰期,这个路由器单位时间需要转的包超过了N个包,后面的传输过来的数据就有可能会被这个路由器直接丢弃掉
TCP是如何对抗丢包呢:感知数据是否丢包,如果丢包,及重新再发一次.此处需要应答报文来进行确认,如果收到了应答报文就说明没有丢包,但是如果没有收到就说明丢包了.发送方会给出一个"时间限制"",如果在这个时间限制内,没有收到ack,那就说明丢包了.
分为以下两种情况:
如果是第二种情况,这时发送方再次发送数据,接收方就会收到两份一样的数据,,TCP会针对上述这种情况进行处理,接收方有一个接收缓冲区,收到的数据先放到缓冲区里,后序再收到数据,就会根据序号在缓冲区里找到对应的位置(排序),如果发现收到的数据在缓冲区里已经有一份了,就会把新收到的数据丢掉
TCP核心机制二:超时重传
超时重传的时间并不是固定的,而是动态变化的.例如,发送第一次重传,超时时间是t1,任然没有ack,还会继续重传,第二次重传,超时间是t2,这时t2>t1,每多重传一次,超时时间间隔就会变大/重传频率就会降低.如果多次重传都没有顺利到达,那么网络丢包率已经到达一个很高的程度,网络发生了严重故障,大概率没办法继续使用了.当重传到达一定次数时,TCP就不会进行重传操作,这时,会尝试进行"重置/复位 链接",发送一个特殊的数据包"复位报文",网络这会恢复了,复位报文就会重置链接,是通信可以继续进行,如果网络还是有严重问题,复位报文也没有回应,此时TCP就会单方面放弃连接
确认应答和超时重传,共同构建了TCP的"可靠传输机制"
TCP核心机制三:连接管理
三次握手/四次挥手()
建立连接:三次握手,handshack,计算机的常见术语.发送不携带业务数据的数据包(没有载荷,只有报头),但能起到"打招呼"的效果
六个标志位中的第5个syn,当syn为1的时候,这就是一个同步报文
完成上述步骤,连接就建立完毕了,本来是4次交互,但是中间有两次合并了,就成了三次握手,合并提高了效率(因为每个数据包都需要一系列封装复用)
意义:
1,初步验证通信的链路是否畅通
2,确认双方各自发送能力/接收能力是否正常
3,让双方通信之前,对通信过程中需要用的的一些关键参数,进行协商->TCP通信时,起始数据的序号就是通过三次握手协商确定的,每次建立连接,每次的序号都是不同的,而且故意差别很大,原因:如果收到的数据与起始序号差别很大,那么就说明这是一个"前朝数据"(不是本次两主机连接,发送方传来的数据,有可能是上次连接时未送达的遗留数据),该数据就会被丢弃掉
断开连接:四次挥手
6个标志位的最后一位,为1是,代表是结束报文
三次握手,是中间两次的交互,合并到一起.但是四次挥手,中间两次大概率是不能合并到一起的,三次握手是的中间两步,是在操作系统内核中进行操作的,在收到SYN的时候,就会立刻由操作系统内核回复ACK/SYN,这两个操作是同时进行的.但是在四次挥手的时候,ACK依旧是由操作系统内核进行操作,但是FIN是由应用层程序调用socket.close来触发的(针对socket.close操作,对应操作系统的操作就是回复FIN)
不同的状态:
LISTEN:服务器进入状态,服务器把端口绑定好,相当于进入了listen状态,此时服务器已经初始化完毕,随时准备好迎接客户端了
ESTABLISHED:客户端和服务器都进入了状态,TCP链接建立完成(保存了对方信息)接下来就可以进入业务数据通信了
CLOSE_WAIT:被动断开连接的一方,会进入这个状态(等待代码去执行close方法)
如果发现服务器这端,存在大量的CLOSE_WAIT状态的TCP连接,此时说明服务器可能有bug,排查close是否写了,以及是否及时执行到
TIME_WAIT:主动断开连接的一方的状态,这个状态是按照一个时间来等待,,等到达一定时间,连接也就释放了,这时为了防止最后一个ACK丢包.TIME_WAIT存在的时间,称之为2MSL(MSL=>数据报在网络传输中消耗的多大时间[不同系统时间是不一样,都是可配置的,例如Linux默认时间是60s])
TCP核心机制四:滑动窗口
可靠传输,会影响传输的效率,TCP希望有可靠传输的基础上,也有一个不错的效率,所以引入了"滑动窗口"
改进方案,将"发送一个等待一个"改进为"发送一批等待一批",多次等待ack的时间合并成了一份,批量发送的数量越多,可以说效率越高,批量发送的数据之间不需要等待,数据的量称之为"窗口大小"(批量发送的单位是字节,而发条数)
在收到2001的ACK,说明1001-2000的数据传到了,然后立即发送5001-6000的数据,此时等待ack的范围就是2001-6000.窗口大小还是4000,大小不变,窗口所处的位置改变了
滑动窗口丢包:
针对以上情况,不需要任何处理!!
批量发数据,多个ack,最多只丢其中一部分,理解确认序号的含义,表示表示收到数据的最后一个字节的下一个序号,进一步理解成,确认序号之前的数据都已经收到了,接下来要发送的数据就是从这个序号之后开始发送
此时,虽然1001的ack丢了,但是2001到达,对方收到2001代表2001之前的数据均已经收到,也就是说,后一个ack可以覆盖掉前一个ack的含义
此时,B收到的数据是1001之前的,,其中1001-2000丢包了,此时2001-3000传过来的时候,返回的ack依旧是1001,三次重复的应答(B在向A索要1001的数据),当1001-2000传过来之后,由于2001-7000的数据都传过来了,于是接下来返回的ack的就是7001.
在这个过程中,快速识别出是哪个数据包丢失了,然后针对性的进行重传,其他顺利到达的不需要进行重传,这个过程称之为"快速重传",快速重传可以视为滑动窗口下搭配的超时重传
滑动窗口/快速重传;确认应答/超时重传.两种模式并不冲突,如果通信双方发送的的数据较少,就是按照确认应答/超时重传,但是如果发送的数据较多,就是按照滑动窗口/快速重传的方式
TCP核心机制五:流量控制
接收端处理数据的速度是有限的. 如果发送端发的太快, 导致接收端的缓冲区被打满, 这个时候如果发送端继续发送, 就会造成丢包, 继⽽引起丢包重传等等⼀系列连锁反应. 因此TCP⽀持根据接收端的处理能⼒, 来决定发送端的发送速度. 这个机制就叫做流量控制
接收缓冲区,如果空闲空间比较大,就可以认为应用程序处理比较快,就可以让发送方发送快一点,设置一个更大的窗口大小.如果空闲空间比较小,就可以认为应用程序处理比较慢,可以让发送方发送数据慢一些,设置一个比较小的窗口大小.
TCP中,接收方收到数据的时候,就会将接收缓冲区还剩的空间大小通过ack数据包反馈给发送方,下一步,发送方就可以通过这个数据来决定发送的窗口的大小了
刚才谈到了,接收方接收缓冲区的剩余容量,这个属性只有在ack报文中(ack为一的时候,才有效),此处的"窗口大小"是16位,表示的范围就是64kb,但是在选项中, 可以设置一个"窗口扩展因子",因此,发送方的窗口大小=窗口大小<<窗口扩展因子
具体流程:
首先发送方发送一个数据报,接收方的ack里面的,储存的剩余容量的数据为300,所以发送方就设置了一个大小为3000的窗口(假设接收方没有对数据进行处理),接收方每次返回的ack中的值不断减小,减小至0,则说明缓冲区满了,这时发送方就不在的发送数据了.[发送方]当过了重发超时时间,就会发送一个窗口探测包,这个包不携带任何业务数据(载荷是空的),只是为了触发ack,如果接收方返回的ack依旧为0就会继续等待.[接收方]接收方也会在缓冲不为0的时候主动触发一个"窗口更新通知"这样的数据包
TCP核心机制六:拥塞控制
流量控制是站在接收方视角来控制发送方的速度,拥塞控制是站在传输链路的视角来限制发送方的速度
这里可以采用一个做实验的方法,找到一个合适的速度
1,首先先按照一个比较小的速度发送数据包
2,数据非常畅通没有丢包,书名网上传输数据整体是比较通畅的,就可以加快数据传输的速度
3,增大到一定速度后,发现出现丢包的现象,说明网络存在拥堵了,减慢数据传输速度
4,减速之后,发现不了丢包,继续在加速
5,加速之后又发现丢包了,继续减速
流量控制/拥塞控制,都会限制窗口的大小,这两个机制会同时起作用,最终发送窗口的大小,取决于上述两个机制得到的窗口的最小值
1,刚开始的时候,拥塞的窗口会非常小,用一个很慢的速度来传输数据,我们我们称之为慢启动
2,不丢包,增大窗口
3,到达某个阈值,即使不丢包,也会停止指数增长,变成线性增长(不至于太快的进入丢包的节奏)
4,线性增长,窗口大小持续增大,发送数据越来越大,直到某个时候出现丢包
5,一旦出现丢包,就需要啊减小发送数据的速度,两种处理方式
a)经典方案:回归慢启动非常小的初始点,指数增长,线性增长
b)现在方案:回到新的阈值上,线性增长
TCP核心机制七:延时应答
提升效率的机制,尽可能减少可靠传输带来的性能问题.如果我们返回ack的时间稍微晚一点,再晚一点的时间内,应用程序就要机会读取更多的缓冲区的数据,此时,延迟返回的ack大概率要比立刻返回的ack的窗口大小要更大,因为在这段时间里有一个消费数据的过程,但是这只是大概率,而非"一定",关键取决于1,代码是如何写的,是否是不停地读取数据2,在延迟时间内,是否发送方会有新的数据发送过来
TCP核心机制八:捎带应答
在延时应答的基础上,引入提升效率的机制,把返回的业务数据和ack两者合二为一
正常情况下,ack是由操作系统内核操作的,会立刻返回,但是响应是应用程序返回的,两者不是统一时机的,无法重合,但是ack涉及到"延迟应答",延迟应答就会使ack返回的时间往后拖,这样一延时,就有可能会赶上接下来发送的响应数据的操作了,于是在响应数据发回去的同时,将ack传回去
四次挥手也是有可能会出现三次挥手的情况的,原则上来说,ack和fin不是统一时机的,但是如果ack出发了延时应答的机制,就有可能会与fin合并,于是,三次挥手是客观存在的
TCP核心机制九:面向字节流
面向字节流就会面对到"粘包问题"(包指应用层的数据包)
那么应⽤程序看到了这么⼀连串的字节数据, 就不知道从哪个部分开始到哪个部分, 是⼀个完整的应 ⽤层数据包.想要解决"粘包问题"就要解决"包之间的边界的操作"
方案一:制定分隔符(文本)
约定请求响应均已\n结尾,需要确认数据的正文中不包含分隔符,
如果数据是纯文本的话,此时使用\n就不太合适了,但是可以用ASCII中靠前的"控制字符"
方案二:指定数据包的长度(二进制)
例如:约定每个应用层数据包,开头的2/4个字节,表示数据包的长度
TCP核心机制十:异常情况处理
(1)进程崩溃
Java体现,抛出异常,没有catch,最终到达JVM,JVM就会直接崩溃.看起来是崩溃,但是操作系统会完成善后,当程序崩溃的时候,进程中的PCB要被回收,PCB中的文件描述符里对应的所有文件,也会被自动关闭,其中针对socket文件,也会触发正常的关闭流程(四次挥手)
(2)主机关机
操作系统会关闭所有进程,关闭过程中同样会触发"四次挥手".
a)四次挥手已经完成了,关机动作才真正完成
b)四次挥手还没有完成,关机就完成了
关机后,B给A传fin,但是没有回应,就会触发超时重传,当重传达到上限的时候,还没有响应,就会主动放弃连接,同时B把A的信息删除掉
(3)主机掉电(拔电源)
a)接收方掉电:
A给B发送消息,但是B断电,所以A不会收到ack,所以会触发超时重传,多次之后,就会单方面断开连接,然后A删除B的信息
b)发送方掉电:
A发着发着没声了,这时候B给A发送一个数据包(不带有业务数据,只是为了触发ack),这样的探测报文是周期性的,同时这个报文是用来探测对方的"生死"的,所以也将这样的报文称之为"心跳包".
TCP内置了心脏包,但是TCP的心脏包的周期较长,通常是秒级,分钟级.应用程序这一层通常也会实现心脏包,达到更快速的"保活机制"
(4)网线断开
和主机掉电相似,相互认为对方失联
补充:
六个标志位中的URG:复位报文,超时重传,多次失败后,就会传URG
六个标志位中的URG:是配合紧急指针使用的,当URG为一的时候,紧急指针生效,紧急指针里面保存的是一个偏移量.TCP正常情况下都是按照顺序来传输数据的,紧急指针就是让后面的数据插队,根据紧急指针的偏移量,把指定位置的数据优先发送出去(特殊场景的一个特殊方案,日常开发中很少用到)
六个标志位中的PSH 催促标志位,带有这个标志位的数据,催促接收方尽快处理这个数据(特殊场景的一个特殊方案)
用UDP来实现可靠传输的方案:参考TCP
推荐图书:《图解TCP/IP》《图解HTTP》