【Java网络原理】 五
本文主要介绍了TCP传输控制协议的报头字段意义以及TCP协议的十大核心特性。
一.TCP传输控制协议
1.TCP报文格式
>端口
范围是0-65535 ,只有确定了端口号,才知道把数据报交给哪个应用程序。
>4位首部长度
TCP报头是变长的, 4bit的范围是0-15
此处的单位是4字节,因此真正的长度要乘4得到
因此tcp报头最大长度是15*4=60字节
而前20个字节是固定的,因此tcp报头的最短长度是20字节
使用首部长度,来确认报头到哪里结束,载荷数据从哪里开始;
>选项
报头变长的体现,可以有,也可以没有,选项的长度也决定了报头最后的长度
>保留(6位)
目前未使用,先占个位置,后面可能需要使用,给未来留下扩展的空间
>32位序列号
>32位确认序列号
>16位校验和
>16位窗口大小
>16位紧急指针
二.TCP十大核心特性
1.确认应答
确认应答是保证可靠性最核心的机制
TCP是有连接,可靠传输,但是由于可靠传输是内核实现的,程序员写代码的时候,并不能很好的感知。
比如:我对女神发出提问,女神对我的问题返回了一个应答
当发送多条数据的时候,可能会出现后发先至,这会导致女神的应答乱套
解决方法是:对发送的报文进行编号,女神对我的应答也进行编号
比如:
TCP中,面向字节流传输
1.针对字节进行编号,而不是针对条
对于一串要传输的字节,只要知道这串字节的开始编号(报头的32位序号指明),以及数据的长度,每个字节的编号自然也就知道了
2.应答报文也是和收到的数据的序号相关联
比如:
报头中的32位序号:报文的第一个字节的序号
32位确认序号:收到的最后的一个序号+1
确认报文 ACK:
如果ACK为0 :表示这是一个普通报头,此时只有32位序号有效
如果AXK位1:表示i这是一个应答报文,此时 序号 和 确认序号 都有效
确认应答,是TCP保证可靠性的最核心机制!
注意:在TCP报文中,是无法知道它的载荷数据是多长的,只能结合IP协议才知道,TCP载荷长度=IP载荷长度 - TCP报头长度
丢包:网络上,很可能发送一个数据,然后丢了。
往往是因为网络繁忙,后面等的数据太久了,就可能被丢弃了,网络负载越高,越繁忙,就越容易丢包
如果真的丢了,发送方就会启动超时重传
2.超时重传
超过一定时间之后,就将报文进行重传
我们把丢包分两种:
1.发的消息本身丢了
>触发超时重传
2.应答报文丢包了
>也会触发超时重传,发送方一旦重传,接收方同一消息收到了两次,就会出现问题,因此接收方需要把重复的数据丢弃,调用inputStream.read的时候,读到的不会出现重复
去重
直接使用tcp的序号作为判定依据,tcp会在内核中,给每个socket都安排一个内存空间,相当于一个队列,也称为 接收缓冲区
收到的数据,都会被放到接收缓冲区里,并且按照序号排列好顺序 ;
收到数据,接收方的网卡,把数据放到对应的socket的接收缓冲区中,应用程序调用read,就相当于从这个接收缓冲区消费数据;
当被read走,就可以从队列中删除了
由于接收缓冲区是按照序号有序排列的,如果排列队首元素序号,已经超过新收到的这个数据的序号,表明新收到的数据对应的数据之前已经被读过
对方上述两种情况,发送方是无法区分的
注意:
1.由于接收缓冲区会对数据排列好顺序,就不必考虑数据传输的先后顺序了,因此也解决了后发先至的问题。
2.
正常情况下,丢包概率比较小,重传可以提高传输成功的可能性。
另外啊,超时时间不是固定的值,会随着轮次的增加,进一步增加
如果重传次数达到一定程度,就会放弃重传,此时就会尝试 重置TCP连接
TCP复位报文
RSI如果为1就是复位报文
如果网络中出现严重故障,复位操作也无法成功,最终就会删除保存的对端的信息
超时重传也是TCP可靠性机制的有效补充
3.连接管理
1.建立连接 -->三次握手
握手:发一个发招呼的数据,并不会携带业务信息
A和B完成建立连接的过程,就需要三次这样的打招呼的数据交互
注意:中间的两次可以合并成一次。
为什么合并?
合并之后,节省了封装和分用的过程,降低成本提高了效率,而且还能起到消息协商的效果。
消息协商
通信的时候涉及到一些参数,需要双方保持一致,通过协商,来确定参数。
比如双方的序号从几开始(一般不从0/1),这样做主要是保证两次连接,消息的序号能够有较大的差异,从而去判定出某个消息是否属于这个连接的
比如:某个消息知道直到连接断开才到达对端,此时的连接已经不是彼时的连接了,这个时候,就可以通过序号,明显判别出这个是上一个连接的消息,就可以直接丢弃。
总结:
三次握手的初心有两方面
1.投石问路,验证通信路径是否畅通,双方的发送/接收能力是否正常
2.协商必要的参数,使客户端和服务器使用相同的参数进行消息传输
ACK是应答报文
SYN申请建立连接的请求 同步报文段
三次握手的过程可以化简成如下:
2.断开连接 -->四次挥手
连接,通信双方在各自的内存中保存了对端的信息;如果不需要连接了,就得即使释放上述的空间
FIN 为1,就是结束报文段
经过上述四个步骤之后,连接就彻底不能使用了,双方就可以把各自保存的对端信息的空间释放了
四次挥手:
为什么是四次?能不能是合并?
FIN的触发,是应用程序代码控制的,调用socket.close()或者进程结束,就会触发FIN;ACK则是内核控制的收到FIN立即就会返回ACK
因此,如果close执行的快,也有可能立即触发FIN,导致ACK和FIN的合并,形成三次挥手。
问题:
1.如果服务器始终不close会怎样,客户端的连接就始终不关闭吗?
此时,服务器的TCP状态,就处于CLOSE_WAIT状态
站在服务器的角度,虽然这里的连接没有关闭,但是这个连接其实已经不能正常使用了
针对当前socket进行读操作,如果数据还没读完,能正常读到缓冲区的数据;如果已经读完了,此时就会读到EOF
针对当前socket进行写操作,直接就会触发异常;
无论无何,这个连接,已经没用了,关闭是唯一的选择。更极端情况,忘记写close了,等不到关闭,就会单方面放弃连接。
2.如果通信中,出现丢包了,怎么办?
尽可能重传,如果重传仍然失败,连续多次,此时仍然会单方面释放连接
丢包分两种情况:
1.第一组丢了
客户端直接重传FIN即可
2.第二组丢了
>FIN丢了 重传
>ACK丢失
如果最后一个ACK丢失,B就会重传一个FIN;如果此时A已经释放连接了,重传的FIN就无人进行ACK了;
因此,需要让A在发出去最后一个ACK之后让连接等一会;如果等了一会之后,对方没有重传FIN,就认为ACK已经被收到了
此时A才能正确释放连接。
A等待的这个时候叫做:MSL等待时间是网络上任意两点之间传输数据的最大空间*2
4.滑动窗口
提高传输效率,更准确的说是让TCP的可靠传输的前提下,效率不要太拉跨
如果滑动窗口发一次数据等待一个应答,就会导致消耗大量的时间等待ack,使用滑动窗口,就是为了缩短上述等待时间:
有滑动窗口:
批量发送:一次性发一组数据,发这一组数据的过程中,不需要等待ack,此时就相当于用一份等待时间等四个ack
窗口:一次性发多少数据,不用等ack这样的大小
窗口越大,批量发送的数据就越多,效率就越高;当然不能无限大,接收方能否处理过来,中间的网络设备能否承受住,都是未知数。
滑动窗口是一个形象的比喻,实质上就是批量发送数据。
问题:丢包
>数据丢了
>重传
比如:当1001-2000丢失,即使2001-3000数据到达,B返回的ACK确认序号仍然是1001
也就是索要1001数据,当A连续多次收到来自B的索要1001数据,A就会重传1001-2000.
>ACK丢了
不用做任何处理!
确认序号:该序号之前的数据都收到了!
比如,就算A没有收到1001确认序号,但是收到了2001序号,就足以表明1001已经收到了。
除非所有ACK都丢了,否则丢一部分,都是没有影响的!
注意:
滑动窗口不是使用TCP一定会涉及的,如果通信双方大规模传输数据,会有滑动窗口;如果数据规模小,那么就不会使用滑动窗口;
5.流量控制
作为滑动窗口的补充,给滑动窗口踩刹车
滑动窗口,窗口越大,传输效率越高
但是传输的窗口也不能无限大,如果太大,接收方/中间链路就可能处理不过来,这样一旦丢包,就得重传,窗口大反而影响了效率。
接收缓冲区
发送方发过来的数据会先放到接收方的接收缓冲区;如果这个缓冲区满了,继续发数据,就会丢包。
流量控制就是根据接收方的处理能力,来限制发送方的发送速度(窗口)
如何衡量接收方的处理速度
此处使用接收缓冲区剩余空间大小来作为衡量指标。
报头中的16位窗口大小:只对ack报文才有意义
这个数字返回给发送方,就可以作为下一轮发送的参考数据。
对于大小:
选项中有一个选项是窗口大小扩展因子
实际的窗口大小是16位窗口<<扩展因子(此时就很大了)
当接收缓冲区满的时候,发送方就会等待,周期性的发送窗口探测包(不会携带具体数据),只是为了触发ACK,查询当前接收缓冲区的情况,当不是0了,就可以继续发送了。
6.拥塞控制
总的传输效率,是一个木桶效应,取决于最短板
如果中间某个环节,转发能力比较差,此时发送方的发送速度就不应该超过这里的阈值
具体怎么去衡量中间设备的转发能力呢
采用实验的方式动态调整
1.使用一个较小的窗口传输,如果传输通畅,就调大窗口
2.使用一个较大的窗口,如果出现丢包/堵塞,就调小窗口
拥塞控制:
1.慢启动
刚开始通信的时候,先使用一个非常小的窗口
2.指数增长(*2)
3.线性增加(+n)
4.拥塞窗口回归小窗口
当窗口增长过程中,如果传输出现丢包,就把窗口调整为最初的小窗口,继续回到指数增长,线性增长,并根据丢包的窗口大小,调整阈值(丢包窗口大小/2)
这样的调整就可以很好的适应多变的网络环境。
实际发送的窗口=min(拥塞窗口,流量控制窗口)
拥塞控制和流量控制,共同限制了滑动窗口机制,可以使滑动窗口,能够在可靠性的前提下,提高传输效率。
7.延迟应答
提高传输效率的机制
在返回ack的时候,拖延一点时间,就可以给应用程序腾出更多的消费数据,接收缓冲区的剩余空间就更大了。
此处,到底通过延迟应答能提高多少速度,还是取决于接收方的实际出列能力
8.捎带应答
在延时应答的基础上,引入进一步提高效率的方式
延时应答是让ack传输的时机更慢
捎带应答是基于延时应答,让数据进行合并
比如:
由于ack会延时应答,响应就可以直接捎带上ack一起发送
数据报合并成一个,效率会有明显的提升
能合并的原因:
1.时机上是可以同时的
2.ack 数据本身不携带载荷,和正常的数据也不冲突,完全可以让这一个数据报,既可以携带在载荷数据又带有ack的信息
9.面向字节流
在面向字节流的情况下,会产生粘包问题(粘的是应用层数据报)
通过tcp read/write的数据,都是tcp报文的载荷,也就是应用层数据
发送方一次性是可以发送多个应用层数据报的,但是接收的时候,怎么区分一个完整的数据报
解决方法是,设计合理的应用层协议
1.应用层协议中,引入分隔符,区分包之间的边界
2.应用层协议中,引入包长度,也能区分包之间的边界
10.TCP异常情况处理
网络本身就会存在一些变数,导致tcp连接不能继续正常工作
(1)进程崩溃
进程没来-->PCB没了 --> 文件描述符表也就没了 -->相当于调用socket.close()
崩溃的这一方就会发出FIN,进一步触发四次挥手,此时连接就正常释放了
此时tcp的处理和进程正常退出没啥区别
(2)主机关机(正常关机)
正常关机,就会先强制终止所有的进程,就和上面的情况是一样的
主机关机会有一定的时间,在这个时间之内,四次挥手可能没挥完,但是也没关系
如果有包没送达,自然会单方面释放。
(3)主机掉电
瞬间掉电,没有任何可以操作的空间,此时无法发任何FIN
两种情况
a.如果A给B正在给B发消息,B掉电,A等不到ack就会单方面释放连接
b.如果A给B发消息,A掉电,B在等A的消息,B会阻塞等待
心跳包
B会周期性给对方发送一个不携带任何业务数据的tcp数据报,目的就是为了触发ack,确认一下A是否正常工作/确认网络是否畅通
和前面说的流量控制的窗口探测报文,是一样的。
(4)网线断开
和主机掉电一样~
最后再回顾一下TCP十大核心特性(当然TCP不止这十个特性!!)
关于TCP和UDP的对比
TCP优势在于可靠性,适用于绝大部分场景
UDP优势在于效率,适用于机房内部主机之间的通信