8/26 Java学习博客
继续TCP
一、四次挥手
建立连接,一般都是客户端主动发起的,而断开连接没客户端和服务器都可以主动发起。
建立连接我们用到的是TCP中的SYN,而断开连接则是用到FIN 也叫“结束报文段”
断开连接的过程

和三次握手不同的是,此时的四次挥手,中间的两次交互不一定可以合并。
原因就是ACK和第二个FIN的触发时机是不同的。 ACK是内核响应,B收到FIN立即返回ACK,而第二个FIN是应用程序的代码触发的,就比如B这里调用了close方法才会触发FIN
从服务器收到FIN(同时返回ACK),再到执行到close,发起FIN,这中间经历的时间是不确定的。FIN可能会在我们手动调用close方法时候发起,也有可能是进程结束。
像前面的三次握手,ACK和第二个syn都是内核出发的,统一个时机,可以合并。
这里的四次挥手,ACK是内核触发的,第二个FIN是应用程序执行close出发的,是时机不同不能合并!!!】
那这是是否意味着,代码没执行到close,第二个FIN就一直发不出去(也是有可能的)
如果是正常的四次挥手,"好聚好散”,正常的流程断开的连接
如果是不正常的挥手(没有挥完四次),异常的流程断开连接.(也是存在的)
二、滑动窗口
滑动窗口的存在就是为了提高效率。
TCP的可靠传输,实惠影响传输的效率。(多出了一些等待ack的时间,单位时间内能传输的数据就少了)
滑动窗口,就让可靠传输对性能的影响少一些
(TCP只要引入了可靠性,传输效率是不可能超过没有可靠性的UDP的,TCP这里的“效率机制”都是为了让 影响更小、缩短和UDP的差距)
正常我们发数据 如图

是每次收到一个应答报文,再发下一个数据这个过程中,等待时间比较长的
带有滑动窗口的话,如图

就是批量传数据,不等ack回来,直接再发下一个数据,不过批量传输也不是无限传输。它也是存在上线的,达到上限之后,就会等待ack
在不等待的情况下,批量最多发多少数据,这个数据量,称为“窗口大小”
当然,我们肯定不是等所有的ack回来之后才继续往下发,而是回来一个ack,就继续发一份数据。 这时候从看起来的效果 我们这个窗口就往下移动了一份数据,就像在“滑动”
那我们从这里可以看出来,窗口越大,等待的ack越多,此时传输的效率也就越高。
TCP初心是 “可靠传输” 上面的滑动窗口中,确认应答是可以正常工作的,但是,如果出现了丢包问题 怎么解决?
这里还是用到重传,不过相比于超时重传,又有变化。
先来分析下丢包的两种情况
情况1:ack丢了
这时候不需要任何重传,在TCP里我们知道在传输时候有个序列号来区分个先后啥的。
这时候如果ack=1001丢了,但是ack=2001到了,这就说明2001之前的数据都已经确认传输成功了,涵盖了1001的情况
情况2:数据包丢了
主机A就需要知道是哪个数据丢了.主机B也就得告诉A是哪个数据丢了
此时假设1001-2000 这个数据丢失了,就算后面收到了2001-3000的数据,这个也不会处理而是先放到这个接受缓冲区,继续重发这个ack=1001.
*TCP 允许接收乱序报文,会缓存乱序数据,但 ACK 只报最早缺失的那个序号,哪怕后面一大段数据都收到了,只要前面有缺口,ACK 始终指向缺口位置。
如果发送发接收到了3次重复ack,就会触发 快速重传 ,不等待计时器,直接重传丢失报文。
就比如:
丢失:seq=1001(1001‑2000)
到达:seq=2001 → 接收方回复 ACK=1001(第 1 个重复 ACK)
到达:seq=3001 → 接收方回复 ACK=1001(第 2 个重复 ACK)
到达:seq=4001 → 接收方回复 ACK=1001(第 3 个重复 ACK)
如果通信双方,传输数据的量比较小,也不频繁,就仍然是普通的确认应答和普通的超时重传.如果通信双方,传输数据量更大,也比较频繁,就会进入到滑动窗口模式,按照快速重传的方式处理
通过滑动窗口的方式传输数据,效率是会提升的.
窗口越大,传输效率就越大.(一份时间,等待的ack更多了,总的等待时间更少了)
但是,如果传输的速度太快,就可能会使接收方,处理不过来了.此时,接收方也会出现丢包.发送方还得重传.......
三、流量控制
站在接收方的角度,反向制约发送方的传输速率.发送方发送的速率,不应该超过接收方的处理能力.
那这个处理能力如何量化衡量?
我们直接通过接收方,缓冲区的剩余空间大小,作为衡量处理能力的指标.
剩余空间越大,意味着消费速度越快,处理能力就越强.剩余空间越小,消费速度越慢,处理能力就越弱.
在TCP的报文中有一个16位窗口大小,他指的是接收缓冲区的剩余大小
接收缓冲区,总的空间是4000收到1000数据之后,还剩3000.
于是就把3000放到应答报文中,告诉发送方了.
假设在这个过程中,接收方的应用程序还没来得及处理任何数据呢,此时收到一个数据,接收缓冲区的剩余空间就缩小一分
反馈0,意味着告诉A我接收方这边已经满了.你暂时先别发数据了.此时,A确实就要暂停发送。
暂停多久呢?
此时,虽然不传输业务数据了务数据.
仍然会周期性的发送一个"窗口探测包”,并不携带具体的业务数据.
探测包就只是为了触发ack,为了查询当前接收方这边的接收缓冲区剩余空间.
流量控制,是考虑的接收方的处理能力.
影响这个传输速率不只是接受方,还有整个通信的路,我们就引入的拥塞控制
四、拥塞控制
考虑/衡量通信过程中中间节点的情况
这中间的转发过程中,任何一个节点,处理能力达到上限,都可能会对发送方产生影响,都可能会影响到可靠传输
关键问题:接收方的处理能力,很方便进行量化.
由于中间节点,结构更复杂,更难以直接的进行量化.
因此就可以使用"实验”的方式,来找到个合适的值.
让A先按照比较低的速度先发送数据(小的窗口)如果数据传输过程非常顺利,没有丢包,再尝试使用更大的窗口,更高的速度进行发送.(一点一点变化)
随着窗口大小不停的增大,达到一定程度,可能中间节点就会出现问题了.此时这个节点就可能会出现丢包.发送方发现丢包了,就把窗口大小调整小.此时如果发现还是继续丢包,继续缩小.如果不丢包了,就继续尝试变大,在这个过程中,发送方不停的调整窗口大小,逐渐达成"动态平衡"
这种做法,就相当于把中间节点,都视为"整体”,通过实验的方式,来找到中间节点的瓶颈在哪里。

流量控制、拥塞控制
都是在限制发送方的发送窗口的大小.
最终时机发送的窗口大小,是取流量控制和拥塞控制中的窗口的较小值.
五、延时应答
A把数据传给B,B就会立即返回ack给A [正常]
也有的时候,A传输给B,此时B等一会再返回ack给A[延时应答]
本质上也是为了提升传输效率.
发送方的窗口大小,就是传输效率的关键.
流量控制这里,就是根据接收缓冲区的剩余空间,来决定发送速率的.如果能够有办法,让这个流量控制得到的窗口更大点,发送速率就更快点.(大点的前提,能够让接收方还是能处理过来的)
延时返回ack,给接收方更多的时间,来读取接收缓冲区的数据,此时接收方读了这个数据之后,缓冲区剩余空间,变大了~~返回的窗口大小也就更大了.
需要注意的是:设计延时 ACK 初衷:减少 ACK 包数量,希望捎带确认,或者凑两个报文再 ACK,并不是专门留给应用读数据的时间。
只是客观上,ACK 被推迟发送,给了应用一段窗口时间去 read 缓冲区,如果刚好读完,这一次 ACK 就可以顺带通告一个更大的 接收窗口。
六、捎带应答
在延时应答的基础上,进一步的提高效率.网络通信中,往往是这种"一问一答”这样通信模型.
由于tcp引入了延时应答.上面的ack,不一定是立即返回,可能要等一会.在等一会的过程中,服务端就正好把 响应 给计算好了.计算好了之后就会把 响应 返回,于此同时顺便就把刚才要返回的ack 也带上了.
这也是一开始我们说四次挥手不一定是可以合并的原因
本来是要传输两个tcp数据包(封装分用两遍)
目前通过上述操作,就可以把两个包合并成一个了.此时就可以得到更高效的效果
七、面向字节流
这里有一个最重要的问题,粘包问题(不是tcp独有的,而是面向字节流的机制都有类似的情况)
此处"包"应用层数据包.如果同时有多个应用层数据包被传输过去,此时就容易出现粘包问题.
例子 :
目前,接收缓冲区中aaabbbccc,这三个应用层数据包的数据,就是以字节的形式紧紧挨在一起的.
接收方的应用程序,读取数据的时候可以一次读一个字节,也可以读两个字节,也可以读N个字节...
但是最终的目标是为了得到完整的应用层数据包.
B应用程序,就不知道,缓冲区里的数据,从哪里到哪里是一个完整的应用数据包了.
相比之下,像UDP这样的面向数据报的通信方式,就没有上述问题,UDP的接受缓冲区就相当于一个一个的DatagramPacket对象,应用程序在读的时候,就能知道哪里到哪里是一个完整的连接。
【aaa】【bbb】【ccc】
如何解决粘包问题?
核心思路:通过定义好应用层协议,明确应用层数据包之间的边界.
1.引入分隔符.
前面写的回显服务器,就是这样的方式. 以\n为分隔符
2.引入长度.
3aaa4bbbb3ccc
在字节流数据前加上数据包的长度 ,程序根据这个数据报的长度,继续读取对应字节的个数
在应用层协议里,xml,json,protobuffer,本身都是明确了包的边界的
八、异常情况的处理
如果在使用tcp的过程中,出现意外,会如何处理?
(1)进程崩溃
进程没了,异常终止了.文件描述符表,也就释放了.相当于调用socket.close()
此时就会触发FIN,对方收到之后,自然就会返回FIN和ACK,这边再进行ACK
(正常的四次挥手断开连接的流程)
TCP的连接,可以独立于进程存在.(进程没了,TCP连接不一定没)
(2)主机关机(正常流程)
在进行关机的时候,就是会先触发强制终止进程操作.(相当于1)
此时就会触发FIN,对方收到之后,自然就会返回FIN和ACK.不仅仅是进程没了,整个系统也可能关闭了.
如果在系统关闭之前,对端返回的ACK和FIN到了,此时系统还是可以返回ACK,进行正常的四次挥手的.
如果系统已经关闭了,ACK和FIN迟到了,无法进行后续ACK的响应.站在对端的角度,对端以为是自己的FIN丢包了,重传FIN.重传几次都没有响应,自然就会放弃连接.(把持有的对端的信息就删了)
(3)主机掉电(非正常)
此时,是一瞬间的事情,来不及杀进程,也来不及发送FIN,主机直接就停机了.
站在对端的角度,对端不一定知道这个事情咋搞~~
- 如果对端是在发送数据(接收方掉电),那么发送方收不到 ACK,会触发 TCP 超时重传。连续多次重传仍没有响应后,TCP 最终认为连接已经不可用,放弃该连接,并向应用层返回超时/连接失败等错误。
2.如果对端是在接收数据(发送方掉电),对端还在等待数据到达....等了半天没消息,此时其实无法区分,是对端没法消息还是对方挂了
TCP中提供了心跳包机制.(形象的比喻)
接收方也会周期性的给发送方发起一个特殊的,不携带业务数据的数据包.并且期望对方返回一个应答.如果对方没有应答,并且重复了多次之后,仍然没有,就视为对方挂了.
至于RST 复位报文段
RST 什么情况下可能出现?
有一个很经典的情况:
A B
保持TCP连接 突然掉电
↓
重新启动
↓
A继续发送数据 ------>
↓
B已经没有这个TCP连接的信息
↓
B返回RST
(4)网线断开
网线断开,和刚才的主机掉电非常类似的.
当前假设,是A正在给B发送数据,一旦网线断开,A就相当于就会触发超时重传->连接重置->单方面释放连接.B就会触发心跳包->发现对端没响应->单方面释放连接.
TCP的心跳机制.
在日常开发中,经常见到这种心跳机制.(尤其是在分布式系统中)如何识别某个机器是否是挂了?一般都是通过心跳来检测.般后续用到的心跳,都是在应用程序中,自助实现的,而不是直接使用tcp的心跳
分布式系统中往往希望是 秒级 甚至毫秒级 就能检测出对端是否存活.
tcp的心跳机制,周期比较长.
九、TCP和UDP之间的对比
TCP优势可靠传输,UDP适用于绝大部分场景.
UDP优势更高效率 UDP更适合于,对于"可靠性不敏感”,”性能敏感”场景
(局域网内部(同一个机房)的主机之间通信同一个机房内部,网络结构简单,带宽充足,交换机/路由器网络设备负载程度也不是很高.出现丢包的概率就不大.
往往也希望机器之间数据传输能更快.)
如果要传输比较大的数据包,TCP更优先(UDP有64KB的限制)
如果要进行"广播传输”,优先考虑UDP.UDP天然支持广播,TCP不支持(应用程序额外写代码实现)
比如 投屏功能
手机和电视得在同一个局域网下,手机这边触发投屏功能,就会往局域网发起广播数据包,询问一下,你们谁是电视??谁能够接受投屏??电视此时就会回应了:俺是电视,并且把自己的ip端口啥的都告诉了手机. 手机就可以完成后续的投屏了
经典面试题:如何基于UDP实现可靠传输?
这个问题本质上是考察TCP!!
写代码,在应用程序这里把可靠传输给做出来.TCP是怎么可靠传输的,照抄。
1)确认应答
2)引|入序号确认序号
3)超时重传
4)滑动窗口
等等
十、传输层结束了 这时候来到网络层:
网络层要做的事情,主要是两方面,
(1)地址管理.
制定一系列的规则,通过地址,描述出网络上一个设备的位置.
(2)路由选择.
网络环境比较复杂的.从一个节点到另一个节点之间,存在很多条不同的路径,就需要通过这种方式,筛选/规划出更合适的路径进行数据传输
认识下ip协议格式

4位版本 : 4=>ipv4 6=>ipv6
8位服务类型(TOS) :
能够让IP协议,切换形态
3位优先权字段(已经弃用),4位TOS字段,和1位保留字段(必须置为0)
这四个位,彼此之间是冲突的.只有一位设为1.不同的位设为1,表示IP协议不同的形态.
最小延时,最大吞吐量,最高可靠性,最小成本
4位首部长度:
IP协议的报头,也是变长的,0-0xf=>*4=>0-60字节
16位总长度(字节数): 描述了IP数据包最长是多长
IP协议,确实也存在64KB这样的限制,但是IP协议自身支持"拆包组包”功能
通过 16位标识:如果一个大的IP数据包需要拆成多个小的,此时拆出来的这多个小包,16标识就是相同的数值.
3位标志:有一位表示是否允许拆包.还有一位,表示是否是最后一个包.(类似于单链表的结束标记)
13位片偏移描述:当前每个小的数据包(分片)相对位置
通过这三个属性,来支持IP协议的拆包和组包
8位生存时间(TTL):描述了这个IP数据包,在网络上还能继续存活多久~~
TTL的单位,是次数.
数据包构造出来的时候,TTL会被设置成一个初始值(32,64,128...)数据包在转发过程中,每次经过一个路由器转发,TTL就会-1
如果这个数据包,已经把TTL耗尽了,还没有顺利到达对方,就会被丢弃掉
这个机制,还是非常有用的.给网络能够进行兜底~~
假设构造一个数据包,目的IP写作不存在的IP.这个数据包不可能到达目标.
显然这样的包,也不可能允许在网络上一直存在.
如果把TTL设置成32,会不会不够用?
会不会明明数据包是合法的目的ip,还没到达tt就耗尽了??
这个情况也是可能会存在的,概率不大.64,128....
一般来说,TTL还是会比较充裕的~~
8位协议
描述的是,IP数据包的载荷部分,是一个UDP数据包还是TCP数据包(传输层是哪个协议)
16位首部检验和
这个校验和,只是校验IP首部,不管IP数据的载荷.
(
UDP/TCP这样的数据
UDP和TCP自身都是有校验和的.
)
32位源IP地址/32位目的IP地址 IP数据包中,最关键的内容了!!!
IP地址,本质上就是一个32位的整数为了方便人来理解,写作点分十进制方式.
十一、地址管理.
IP地址,是一个32位的整数.2^32=> 42亿9千万.
地址,理论上来说,是不应该重复的!!
互联网发展到今天,能上网的设备,非常非常多的.早就超过了42亿9千万这个数字
解决方法:
方案一:动态分配IP
这个方案,治标不治本,提高了IP地址的利用率,并没有增加IP地址的数目.(虽然这是一个过度 方案,这个方案目前仍然是广泛存在的)
方案二:NAT机制(网络地址转换)本质上让一个IP地址,代表一批设备.
把IP地址分成两个大类
1)内网IP(局域网IP)
如果一个IP地址,是以10.或者172.16.-172.31.或者192.168.
(复合上述条件之一,IP就是内网IP)
在同一个局域网内部,内网IP之间,不能重复。
在不同的局域网中,内网IP之间,可以重复
2)外网IP(广域网IP)
剩下的IP就都是外网IP
外网IP则始终都不允许重复,务必唯一.
假设家里有两台电脑,都访问同一个网站:
电脑A:192.168.1.10
电脑B:192.168.1.20
路由器公网IP:1.2.3.4
网站服务器:8.8.8.8:443
两台电脑都可能恰好使用相同的客户端端口,例如:
电脑A:192.168.1.10:50000
电脑B:192.168.1.20:50000
这在内网里完全没问题,因为虽然端口都是 50000,但 IP 不同:
192.168.1.10:50000
192.168.1.20:50000
它们是两个不同的 Socket 地址。
经过路由器 NAT 后发生什么?
问题来了:路由器只有一个公网 IP:1.2.3.4
如果它把两个连接都直接变成:
1.2.3.4:50000 → 8.8.8.8:443
1.2.3.4:50000 → 8.8.8.8:443
那就冲突了。
所以 NAT 路由器除了转换 IP,通常还会根据需要转换源端口,这个机制经常叫:NAPT / PAT(端口地址转换)
例如路由器可能建立:
内网 公网
192.168.1.10:50000 → 1.2.3.4:50000
192.168.1.20:50000 → 1.2.3.4:50001
于是服务器实际看到的是:
1.2.3.4:50000 → 8.8.8.8:443
1.2.3.4:50001 → 8.8.8.8:443
路由器会保存一张 NAT 映射表
所以服务器返回:
8.8.8.8:443
↓
1.2.3.4:50000
路由器一查 NAT 表:
1.2.3.4:50000
↓
192.168.1.10:50000
于是交给电脑 A。
如果服务器返回:
8.8.8.8:443
↓
1.2.3.4:50001
路由器查表:
1.2.3.4:50001
↓
192.168.1.20:50000
于是交给电脑 B。
一句话
多个内网设备共享一个公网 IP 时,NAT 路由器通过“公网 IP + 转换后的端口号”区分不同连接,并维护 NAT 映射表;返回的数据到达后,再根据映射表还原成对应的内网 IP 和端口。
再往 TCP 那边联系一下
就能理解为什么我们说一个 TCP 连接通常可以通过 源 IP、源端口、目的 IP、目的端口 四元组唯一确定。

浙公网安备 33010602011771号