Ack保证消息可达
每一条消息发送成功以后都会返回一条Ack
来告知发送方已送达。我重新定义了一遍可靠性:能够明确告知发送方发送结果
1.接收方如果接收消息成功,返回Ack告知发送方已送达
2.就算没有发送成功不会有Ack消息,我们也可以`设置超时时间来认定发送失败`
TCP和UDP报文的格式
数据报文非常大,多个有序数据报在网络中传递发生乱序,丢包
拥塞避免,快速重传
TCP能够自主应对大部分数据传输过程中出现的问题,包括但不限于丢包,乱序等等
数据校验
无论你需要计算机传输的是什么样的资源文件,最后都会变成一串01000101010101...的比特流,回想一下描述文件大小的单位,就算一个简单的文件实际也是一串非常长的数据。要想保证它在层层传递的过程中不出现差错,这几乎是不可能实现的
多层协议上都引入了数据校验这一部分
在数据链路层有FCS;
网络层IP协议和运输层的TCP/UDP都提供了Checksum。
目的就是为了检测出因为网卡软硬件Bug、电缆不可靠、信号干扰而造成信号失真造成的数据错误。
为什么数据校验同一件事情几层同时去做
第一,在于链路层CRC不能完全检测出错误;
第二,每一层校验覆盖的数据范围是不一样的:
IP协议Checksum只覆盖IP首部
而传输层只针对自己的数据包
IP协议Checknum只覆盖IP首部的原因在于IP首部的信息传输过程中会被多次修改的,包括TTL以及某些情况下对源地址的修改。
其次,我们需要理解分而治之是网络中的一个重要概念。
有了校验可以认为数据更加安全了吗
有了校验可以认为数据更加安全了吗
这其实是把安全性和可靠性混淆在了一起。所有的校验操作是防君子而防不了小人,它可以检测硬件故障、软件bug、信号干扰、线路差、人为误操作这些非主观原因造成的错误,但是却无法防止别人恶意去篡改你的数据内容。道理很简单,篡改了数据的同时,可以将重新校验的结果覆盖上去。可靠性提供的是:避免非主观因素造成的数据错误。
但是如何避免被别人攻击篡改数据内容,这是安全性相关的内容,需要别的机制来提供相关的服务。以我们熟悉的TCP举例说明,它提供的是一个可靠但不安全的传输服务。
分片
MTU
链路层的数据帧称为MTU
为什么需要分片
1.链路层对于数据帧的长度都有一个限制,我们称之为MTU。如果需要传输的数据报长度大于链路层的MTU,那么我们就需要将数据报分成若干长度小于MTU的数据报,这个过程叫做分片
2.因为数据报在发送的过程中需要经过不同的网络,链路层的MTU不尽相同(不同网络的链路层的MTU不相同),所以分片不仅发生在源主机端,也可能会发生在中间路由上
上述所说的中间路由发生分片,是IPV4的环境下;在IPV6中只在源端分片重复,如果数据报长度大于中间路由的MTU,路由会直接返回一条ICMPv6 too big 给源主机端。这一部分我们后面会再说到
为什么要把分片放在IP层
分片的根源是MTU的限制
发展至今,IP协议几乎和网络层划上了等号。虽然网络层仍有其他协议的存在,但是可以说能走到最终用户面前的协议,网络层都是选择IP协议。所以分片操作放在IP协议上,可以为所有的上层协议服务,而不必再依次去运输层的协议中实现,相关的逻辑代码复用效率很高。
另外,IP协议比起上层协议还有一个很大的优势就在于它更贴近链路层,这让IP协议可以感知到底层的MTU。而上层协议既不关心,也无法直接关心到底层MTU。
总结来说,IP层实现分片是一个成本最低的选项。
因为物理层去做分片,需要和硬件打交道,如果处理的复杂或者不符合大众标准很可能就是路越走越窄最后把自己走死了;
而上层处理和我之前说的一样,运输层去各自实现成本很高,
应用层自己去做对于应用开发者来说非常痛苦。
其他层分包机制:
链路层中有类似Atm协议;
在TCP协议中,MSS(Maximum Segment Size,最大报文长度)实际就是一个分片机制。
IP层分包产生的乱序和丢包问题
TCP协议中的MSS和分片有一些简单的不同:分片是因为数据报长度大于MTU所以被动去分割数据报;而MSS是三次握手过程中双方协商的结果,提前分割数据避免分片发送。TCP之所以提供这样一个option来避免IP层分片,相信有部分原因在于IP分片并不如我们所假设的那么完美。
我们之前提到IP协议去做分片确实复用率非常高,成本非常低,但实际情况中这些操作给负责分片和重组的主机和路由的cpu带来了非常大的压力。负责分组的终端的IP层需要去拆分数据报,用ID值相同的IP Fragmented Packet将数据发送出去;而重组终端的IP协议IP层根据ID,MF位,Fragment Offset信息进行重组,得到完整数据提交给上层。需要着重强调的是我们无法保证分片后的数据按照顺序依次到达目的终端,并且IP层没有类似超时重传的可靠性支持,其中任一一个分片数据丢失都会引起整个数据报的丢失!!!!
乱序和丢包的问题确实非常的麻烦,这里的麻烦更多的在于出现问题需要解决的成本过高。类似UDP只依靠IP协议分片处理大数据报的,实际是非常不推荐的。
如果上层提前分割了数据,IP层只要为每一个数据报加上IP头发出即可。每一个数据相互独立,由上层提供了可靠性支持。相比IP层分片的苦苦挣扎,这样做确实简单了很多。
但是这并不能完全的避免分片的发生。如RFC 879所说:TCP provides an option that may be used at the time a connection is established (only) to indicate the maximum size TCP segment that can be accepted on that connection.。这是一个在三次握手过程中协商的结果,不保证通讯过程中一定不会发生变化。
DF
是否分片标识
在这一段的开始我们提到分片也可以发生在中间路由。举一个简单的例子:我们从源主机发生了一个IP包 = 1500,在互联网上逐跳传递,在中间的某台路由发送出去的时候,因为该接口MTU只有1000 < 1500,那么这个包就被分成两个IP分片发出去了。但是作为源主机对此毫不知情的,后续它仍然会按1500的大小发送IP包,那么每一个大于1000的IP包都会被分片。
为此我们可以在IP头中设置DF(Don‘t Fragement)为1来让中间路由不要分片,如果IP包的大小大于中间路由的mtu,那么直接丢弃并通过ICMP告知源主机。源主机再根据ICMP中提供的信息修改IP包的大小。
PMTUD
Path MTU Discovery(传输路径MTU发现)
说起PMTUD,我们必须在此回到上面讲到的ICMP需要分片但DF位置一差错报文,还记得那个ICMP差错报文中有一个字段是告知下一跳的MTU值的吗?PMTUD正是利用ICMP需要分片但DF位置一差错报文的这一特性来实现的。
发送方在接收到该差错报文后,会根据该报文给出的下一跳的MTU值计算适合传输的最大段长度,从而在后续的发送报文过程中,避免在中间路径被分片的情况产生。
这在端系统主要是通过在路由表里临时添加目的主机路由并将ICMP差错报文告知的下一跳MTU值跟该主机路由关联起来来实现。
PMTUD的确是个非常不错的机制,但是在复杂的实际网络环境中,有时候会失效,因为为了安全起见,有些网络管理员会在路由器、防火墙等中间设备上设置过滤ICMP报文的安全策略,这将导致ICMP差错报文被这些中间设备丢弃,无法达到发送方,从而引起PMTUD的失效,网上有个宫一鸣前辈共享的案例——《错误的网络访问控制策略导致PMTUD 实现故障一例》,该案例正是说明这种情况绝好的例子,大家可以自行百度此文档学习参考。
值得一提的是PMTUD仅TCP支持,UDP并不支持PMTUD
为什么应用层需要设置心跳
上面所说的情况在实际情况中经常会发生。我们假设通信的双方是服务器-客户端,提供服务的一端我们认为是服务器,服务器通常会一直保持运行状态并同时为多个客户端服务;发出请求的一方是客户端。通常来说客户端关闭程序或是手动关机的时候系统会为我们主动断开连接,发送一个FIN包给服务器。但是如果客户端突然崩溃或客户直接强制关闭了计算机,这个时候系统来不及发送FIN包,就会留下一个半开放连接在这里。如果服务器再次向这个已经非正常关闭的客户端发送消息,那么它会收到一个RST的回复。但是如果恰巧服务器正处在等待客户端回应的状态,那么它会一直等待下去
这样的情况显然是不可以接受的。因为服务器打开一个链接的同时,会以一个客户的身份占用着某一部分的资源,这样的一个半开放链接会导致这一部分的资源永远得不到释放。为了应对解决这一个问题,TCP设计了保活功能,也就是SO_KEEPALIVE选项。
TCP的心跳包是一个有争议的选项,这只是一个option,而不是一个必须实现的标准。通常情况下我们是在服务器打开这个选项,客户端被动响应,默认间隔时间7200s。但这不是绝对的,如果有需求客户端同样可以打开。之所以在服务器设置这个选项是因为它需要长时间保持在工作状态,并且同时为多个客户端服务,而客户端作为个人使用会经常(异常)关闭。检测出半开放的连接并删除它,释放资源对于服务器是非常必要的。
TCP保活机制的缺陷
保活机制是非常有必要的,但关于是否应该在TCP中提供一直争论不休。在Host Requirement中提供了3个不使用保活定时器的理由
1.在出现短暂差错的情况下,可能会使一个运行正常的连接释放掉。(比如中间路由崩溃并重新启动时会发送一个保活探查,会让TCP误认客户端已经崩溃)
2.耗费不必要的带宽
3.在按分组计费的情况下会在互联网下花费更多的金钱
抛开历史的包袱来看,带宽和计费相关的问题已经不再需要我们担心,但TCP的保活机制仍然是一个不被推荐的选项。
首先我们需要明确的是,TCP的保活只能够检测连接是否存活,但是否可用是未知的。做一个简单的比方,TCP的保活就像水道工,它只关心水管是否畅通能否通水,但水厂能否供水它无法保证。回到TCP上来解释就是进程可能死锁或者拥塞,操作系统是正常收发TCP消息的,但服务端繁忙无法提供服务。
其次,默认检测的时间是7200s=120min=2h。这个间隔实际上是非常长的,这么迟缓的响应能力在大部分的应用场景下是无法接受的。当然我们可以手动去修改SO_KEEPLIVE选项的参数,但这是系统级的变量,修改意味着会影响所有运行的程序。
有朋友提到这个参数在socket中可以为pre socket单独设置,类似Buffer。
除此之外,还有一个比较棘手的问题是keep alive的数据包有可能会被运营商拦截。如果仅仅依赖TCP的保活机制,那么在这种情况服务器可能会释放掉一个运行正常的连接
由此来说,**需要应用层来设置心跳,而不应该依靠TCP的SO_KEEPLIVE保活机制**
原文地址https://www.jianshu.com/p/7c3...
PMTUD拓展参考http://www.vants.org/?post=109
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。