老司机种菜


  • 首页

  • 分类

  • 关于

  • 归档

  • 标签

  • 公益404

  • 搜索

微信终端跨平台组件 mars 系列(二) - 信令传输超时设计

发表于 2019-03-12

前言

mars 是微信官方使用 C++ 编写的业务性无关、平台性无关的终端基础组件,目前在微信 Android、iOS、Windows、Mac、Windows Phone 等多个平台中使用,并正在筹备开源,它主要包含以下几个独立的部分:

  1. COMM:基础库,包括 socket、线程、消息队列、协程等基础工具;
  2. XLOG:通用日志模块,充分考虑移动终端的特点,提供高性能、高可用、安全性、容错性的日志功能;(详情点击:高性能日志模块xlog )
  3. SDT:网络诊断模块;
  4. STN:信令传输网络模块,负责终端与服务器的小数据信令通道。包含了微信终端在移动网络上的大量优化经验与成果,经历了微信海量用户的考验。

本篇文章将为大家介绍 STN(信令传输网络模块),由于 STN 的复杂性,该模块将被分解为多个篇章进行介绍,本文主要内容为微信中关于读写超时的思考与设计。

读写超时与设计目标

TCP/IP中的超时设计

微信信令通信主要使用 TCP/IP 协议,数据经过应用层、传输层、网络层、链路层(见图1)。其中,链路层与传输层,协议提供了超时重传的机制。 图1 使用 TCP/IP 协议

链路层的超时与重传

在链路层,一般使用混合自动重传请求(即 HARQ)。HARQ 是一种结合 FEC(前馈式错误修正)与 ARQ(自动重传请求)的技术,原理如图2所示。 图2 HARQ 原理

通过使用确认和超时这两个机制,链路层在不可靠物理设备的基础上实现可靠的信息传输。这个方案需要手机和 RNC 都支持,目前在 EDGE、HSDPA、HSUPA、UMTS和 LTE 上都已实现支持。

传输层的超时与重传

传输层(即 TCP 层)提供可靠的传输,然而,TCP 层依赖的链路本身是不可靠的,TCP 是如何在不可靠的环境中提供可靠服务的呢?答案是超时和重传。TCP 在发送数据时设置一个定时器,当定时器溢出还没有收到 ACK,则重传该数据。因此,超时与重传的关键之处在于如何决定定时器间隔与重传频率。

传统 Unix 实现中,定时器的间隔取决于数据的往返时间(即 RTT),根据 RTT 进行一定的计算得到重传超时间隔(即 RTO)。由于网络路由、流量等的变化,RTT 是经常发生变化的,RTT 的测量也极为复杂(平滑算法、Karn 算法、Jacbson 算法等)。在《TCP/IP详解》中,实际测量的重传机制如图3所示,重传的时间间隔,取整后分别为1、3、6、12、24、48和多个64秒。这个倍乘的关系被称为“指数退避”。

图3 实际测量的重传机制

在移动终端中,RTO 的设计以及重试频率的设计是否与传统实现一致呢?对此我们进行了实测,实测数据如下:

图4所示为OPPO手机TCP超时重传的间隔,依次为[ 0.25s,0.5s,1s,2s,4s,8s,16s,32s,64s,64s,64s …]: 图4 OPPO 手机 TCP 超时重传间隔

而 SamSung 中 TCP 超时重传的间隔依次为[0.42s, 0.9s, 1.8s, 3.7s, 7.5s, 15s, 30s, 60s, 120s, 120s …],见图5。 图5 三星手机 TCP 超时重传间隔

经过多次实际测试我们可以看出虽然由于不同厂商的 Android 系统实现,RTO 的值可能会有不同的设定,但都基本符合“指数退避”原则。

接下来再看 iOS 系统中,TCP RTO 的实验数据,图6所示为实验中第一次的数据[ 1s,1s,1s,2s,4.5s,9s,13.5s,26s,26s … ]。

图6 iOS 系统 TCP RTO 第一次实验数据

上面的数据看起来并不完全符合指数退避,开始阶段的重试会较为频繁且 RTO 最终固定在 26s 这一较小的值上。

进行第二次测试后发现数据有了新的变化[1s,1s,1s,2s,3.5s,8.5s,12.5s,24s,24s …],如图7所示。 图7 iOS 系统 TCP RTO 第二次实验数据

RTO 终值由26秒缩减至24秒,最终经过多次测试并未发现 iOS 中 TCP RTO 的规律,但可以看出 iOS 确实采用了较为激进的超时时间设定,对重试更为积极。

读写超时的目标

通过上述的调研与实验,可以发现在 TCP/IP 中,协议栈已经帮助我们进行了超时与重传的控制。并且在 Android、iOS 的移动操作系统中进行了优化,使用了更为积极的策略,以适应移动网络不稳定的特征。

那是否意味着我们的应用层已经不需要超时与重传的控制了呢?其实不然。在链路层,HARQ 提供的是节点之间每一数据帧的可靠传输;在传输层,TCP 超时重传机制提供的是端与端之间每个 TCP 数据包的可靠传输;同理,在微信所处的应用层中,我们仍然需要提供以“请求”为粒度的可靠传输。

那么,应用层的超时重传机制应该提供怎样的服务呢?

首先,我们来看一下应用层重传的做法。在应用层中,重传的做法是:断掉当前连接,重新建立连接并发送请求。这种重传方式能带来怎样的作用呢?回顾 TCP 层的超时重传机制可以发现,当发生超时重传时,重传的间隔以“指数退避”的规律急剧上升。在 Android 系统中,直到16分钟,TCP 才确认失败;在 iOS 系统中,直到1分半到3分半之间,TCP 才确认失败。这些数值在大部分应用中都是不为“用户体验”所接受的。因此,应用层的超时重传的目标首先应是: 在用户体验的接受范围内,尽可能地提高成功率 尽可能地增加成功率,是否意味着在有限的时间内,做尽可能多的重试呢?其实不然。当网络为高延迟/低速率的网络时,较快的应用层重传会导致“请求”在这种网络下很难成功。因此,应用层超时重传的目标二: 保障弱网络下的可用性 TCP连接是有固定物理线路的连接,当已 Connect 的线路中,如果中间设备出现较大波动或严重拥塞,即使在限定时间内该请求能成功,但带来的却是性能低下,反应迟钝的用户体验。通过应用层重连,期待的目标三是: 具有网络敏感性,快速的发现新的链路

我们总结应用层超时重传,可以带来以下作用:

  1. 减少无效等待时间,增加重试次数:当 TCP 层的重传间隔已经太大的时候,断连重连,使得 TCP 层保持积极的重连间隔,提高成功率;
  2. 切换链路:当链路存在较大波动或严重拥塞时,通过更换连接(一般会顺带更换IP&Port)获得更好的性能。

微信读写超时

方案一:总读写超时

在TCP层的超时重传设计中,超时间隔取决于RTT,RTT即TCP包往返的时间。同理,在微信的早期设计中,我们分析应用层“请求”的往返时间,将其RTT分解为:

  • 请求发送耗时 - 类比TCP包传输耗时;
  • 响应信令接收耗时 - 类比ACK传输耗时;
  • 服务器处理请求耗时 - TCP接收端接收和处理数据包的时间相对固定,而微信服务器由于信令所属业务的不同,逻辑处理的耗时会差异明显,所以无法类比;
  • 等待耗时 - 受应用中请求并发数影响。

因此,我们提出了应用层的总读写超时如图8所示,最低网速根据不同的网络取不同的值。 图8 应用层的总读写超时

方案二:分步的读写超时

在实际的使用过程中,我们发现这仅仅是一个可用的方案,并不是一个高性能的解决方案:超时时长的设置使用了差网络下、完整的完成单次信令交互的时间估值。这使得超时时间过长,在网络波动或拥塞时,无法敏感地发现问题并重试。进一步分析可以发现,我们无法预知服务器回包的大小,因此使用了最大的回包进行估算(微信中目前最大回包可到 128KB)。然而,TCP 传输中当发送数据大于 MSS 时,数据将被分段传输,分段到达接收端后重新组合。如果服务器的回包较大,客户端可能会收到多个数据段。因此,我们可以对首个数据分段的到达时间进行预期,从而提出首包超时,如图9所示。 图9 首包超时计算

首包超时缩短了发现问题的周期,但是我们发现如果首个数据分段按时到达,而后续数据包丢失的情况下,仍然要等待整个读写超时才能发现问题。为此我们引入了包包超时,即两个数据分段之间的超时时间。因为包包超时在首包超时之后,这个阶段已经确认服务器收到了请求,且完成了请求的处理,因此不需要计算等待耗时、请求传输耗时、服务器处理耗时,只需要估算网络的 RTT。

在目前方案中,使用了不同网络下的固定 RTT。由于有了“首包已收到”的上下文,使得包包超时的间隔大大缩短,从而提高了对网络突然波动、拥塞、突发故障的敏感性,使得应用获得较高的性能。

方案三:动态的读写超时

在上述的方案中,总读写超时、首包超时都使用了一些估值,使得这两个超时是较大的值。假如我们能获得实时的动态网速等,我们能获得更好的超时机制,如图10所示。 图10 实时动态网速下的超时估算

但是,理想是丰满的,现实是残酷的:

  • 动态网速需要通过工具方法测定,实时性要求高,并且要考虑网络波动的影响;
  • 服务器动态耗时需要服务器下发不同业务信令的处理耗时;
  • 真实回包大小则只能靠服务器通知。

上述的三种途径对客户端和服务器都是巨大的流量、性能的消耗,所以动态化这些变量看起来并不可行。

因此,这里需要换个角度思考动态优化,手机的网络状况可以大概地归为优质、正常、差三种情况,针对三种网络状况进行不同程度的调整,也是动态优化的一种手段。这里选择优质网络状况进行分析:

  • 如何判定网络状况好?网速快、稳定,网络模块中与之等价的是能够短时间完成信令收发,并且能够连续长时间地完成短时间内信令收发。
  • 即使出现网络波动,也可以预期会很快恢复。 图11 优质网络状况优化

根据对网络状况好的分析,我们可以做出这样的优化(如图11所示):

  • 将客户端网络环境区分为优良(Excellent)、评估(Evaluating)两种状态;
  • 网速快、稳定就是条件1,信令失败或网络类型切换是条件2。

进入Exc状态后,就缩短信令收发的预期,即减小首包超时时间,这样做的原因是我们认为用户的网络状况好,可以设置较短的超时时间,当遇到网络波动时预期它能够快速恢复,所以可以尽快超时然后进行重试,从而改善用户体验。

总结

虽然 TCP/IP 协议栈中的链路层、传输层都已经提供了超时重传,保障了传输的可靠性。但应用层有着不同的可靠性需求,从而需要额外的应用层超时重传机制来保障应用的高性能、高可用。应用层超时重传的设计目标,笔者从自身经验出发,总结为:

  • 在用户体验的接受范围内,尽可能地提高成功率;
  • 保障弱网络下的可用性;
  • 具有网络敏感性,快速地发现新的链路。

依从这些目标,mars STN 的超时重传机制在使用中不断的精细化演进,使用了包含总读写超时、首包超时、包包超时、动态超时等多种方案的综合。即使如此,STN 的超时重传机制也有着不少的缺点与局限性,例如相对适用于小数据传输的信令通道、局限于一来一回的通信模式等。mars STN 也会不断发现新的问题持续演进,并且所有的演进都将在微信的海量用户中进行验证。同时也期待随着 mars STN 的开源,能收获更多、更广的经验交流、问题反馈、新想法的碰撞等。

转自微信终端跨平台组件 mars 系列(二) - 信令传输超时设计

tips-net-applicationlayer-diff

发表于 2019-03-12

I. 协议优化演进

1. 带宽与拥塞

现状

目前的网络基建越来越好,因此带宽的已经不再是瓶颈, 但是由于相关协议(如TCP)的拥塞窗口(CWND, congestion window)控制算法,很多时候并没有将带宽有效的利用,因此更有效的利用带宽是一个优化方向,特别针对视频、游戏等领域。

应对

  • QUIC: 基于UDP,QUIC可以支持无序的递交,因此通常单个丢包最多只会影响1个请求stream,并且QUIC中一定程度上拆分拥塞窗口来更好的适配多个多路复用的连接,来尽可能的利用带宽,目前已经在Youtube以及一些Google通用库(如字体库)上应用
  • HTTP: 通过同时建立多个连接通道,由于每个通道有单独的拥塞窗口保证一个丢包最多只拥塞一个连接通道
  • BBR: Google推出的全新的阻塞拥塞控制算法,从根本上解决该问题,通过交替测量带宽和激进的估算算法尽可能的占满带宽与降低延迟(此方式极大的提高了带宽利用率),目前已经在Youtube上应用

存在该缺陷的协议

  • TCP: 由于采用”加性增,乘性减”的拥塞控制算法,错误的将网络中的错误丢包也认为是拥塞丢包,导致拥塞窗口被收敛的很小,带宽无法有效利用
  • SPDY: 由于SPDY基于TCP,因此存在TCP相同的缺陷问题,并且虽然SPDY采用了多路复用,也做个各类优化,但是由于一个TCP连接只有一个拥塞窗口,因此一个请求stream丢包,就会导致整个通道被阻塞

2. 握手的N-RTT的开销

现状

目前TCP与SSL/TLS(1.0,1.1,1.2),每次建连需要TCP三次握手+安全握手需要: 4~5-RRT,导致建连效率低下,Google、Facebook、Tencent(Wechat)等公司推出了各类优化策略。

应对

  • TLS1.3: 安全握手提出了0-RTT草案
  • QUIC: 通过实现自己的安全模块,整个握手过程(TCP + TLS)采用全新的0-RTT方案,并计划当完成时适配到TLS1.3中
  • Proxygen: Facebook基于QUIC的0-RTT协议进行优化,保证安全握手最多只有1-RTT,并运用在TCP中 ,并将贡献各类优化成果给TLS1.3
  • mmtls: Wechat基于TLS1.3草案中的0-RTT,进行优化推出自己的mmtls,其对于长连接保障安全握手1-RTT,对于短连接安全握手尽可能使用0-RTT

存在该缺陷的协议

  • SSL、TLS1.3之前版本: 在TLS1.2中,需要2~1-RTT(全握手需要2-RTT)

3. 冗余数据

现状

通常的一般的HTTP请求,每次请求header基本上没什么变化;在一些情况下多个页面使用相同静态资源(js、logo等),却每次都重复下载。

应对

  • SPDY: 采用DEFLATE对请求头/响应头进行压缩
  • HTTP/2: 采用HPACK算法对请求头/响应头进行压缩,并且通讯双方各自cache一份header fields表,避免了重复header的传输
  • QUIC: 目前版本采用HPACK算法对请求头/响应头进行压缩
  • HTTP/1.1、HTTP/2: 支持Cache-Control用于控制资源有效时间,支持Last-Modified来控制资源是否可复用
  • Facebook geek方案: 将expiration time全部设置为1年,所有的资源请求链接,都采用概念性的连接(在请求链接后加上资源名的md5,再做mapping)(只要资源不变化链接就不变化),保证已下载资源能被有效利用的同时,避免重复检测资源有效性
  • 浏览器优化: Facebook联系Chrome与Firefox,针对复用资源可复用检测频率进行调整(如firefox支持在cache-control中的immutable关键字表示资源不可变不用重复检测)

存在该缺陷的协议

  • HTTP/1: 请求头未做压缩,不支持Cache-Control与Last-Modified因此存在冗余资源重复下载问题
  • HTTP/1.1: 请求头未做压缩

4. 预准备

  • Taobao: DNS-Prefetch、Preconnect、Prefetch、Flush HTML early、PreRender
  • SPDY、HTTP/2、QUIC:: 允许服务端主动推服务端认为客户端需要的静态资源

5. 负载均衡、超时策略优化与其他

  • 负载均衡: 收益较小的长连接,带来服务端没必要的性能开销
  • 超时策略: 策略性的调整建连与维连时的超时重连的频率、时间、IP/端口,来应对弱网状况,何时快速放弃节约资源(无网状态),何时找到可用资源快速恢复连接(被劫持、服务器某端口/IP故障、基站繁忙、连接信号弱、丢包率高)
  • 策略性阻塞: 根据网络情况、请求数目动态调整连接数来保证吞吐量与稳定性(如SPDY、HTTP/2、QUIC中的多路复用)
  • DNS: 结合TTL有效管理本地DNS缓存的有效时间、以及缓存大小来减少DNS查询的阻塞,以及可以通过HTTPDNS优化DNS请求的线路以及来避免DNS被篡改等问题(如果使用okhttp3,可以指定DNS,并且可以为请求设定缓存大小与时间,可以很轻易的实现自己的HTTPDNS)

II. 常见协议区分

1. TCP

关于TCP窗口的研究与学习,请移步TCP窗口

目前应用最广泛的可靠的、有序的、自带问题校验修复(error-checked)、传输协议,通常情况下发送端与接收端通过TCP协议来保障数据的可靠到达,中间层通过IP协议来路由数据的传递。

tips-net-applicationlayer-diff-2019312152553

  • 建连: 通过三次握手,保障连接已可靠连接
  • 超时重试: 通过连接超时重试、读写超时重试机制,来保障连接的稳定性
  • 拥塞控制: 通过”加性增,乘性减”算法,来保障尽量少的报文传输尽量多的数据的同时,减少丢包重传的概率
  • 校验和: 通过对TCP/IP头进行”校验和”检查,来保障传输数据与地址信息的可靠
  • 有序性: 通过”序列号”来鉴别每个字节数据,保证接收端能够有序的重建传输数据,以及校验数据完整性
  • 应答机制: 每次接收端会发送Acks(Acknowledgements)给发送端告知数据以被接收
  • 断连: 通过四次挥手,保障连接已可靠断开

2. HTTP

HTTP1.1 vs HTTP1.0

  • 更灵活缓存处理: 引入Etag(Entity tag)等目前常用的缓存相关策略
  • 优化带宽使用: 引入range头域,支持206(Partial Content),用于数据断点续传。
  • 错误机制更完善: 引入24个错误状态码,如409(Conflict)请求资源与当前状态冲突; 410(Gone)资源在服务器上被永久删除
  • Host头处理: 请求头中必须带上host,否则会报400 Bad Request,为了支持一台服务器上有多台虚拟主机,因此通常一个IP对应了多个域名
  • 长连接: 默认Connection: keep-alive,以复用已建连通道,不像http1.0每个请求都需要重新创建

3. HTTPS

1994年由 网景 提出,并应用在网景导航者浏览器中。最新的HTTPS协议在2000年5月公布的RFC 2818正式确定。

HTTPS协议是基于TLS(Transport Layer Security)/SSL(Secure Sockets Layer)对数据进行加密校验,保障了网络通信中的数据安全。

在当前大陆的网络环境而言,是有效避免运营商劫持的手段。

image_1b8ji5se91a1kvn431umcc2vk9.png-44.3kB

  • SSL与TLS: 早期HTTPS是通过SSL对数据验证加密,后SSL逐渐演变为现在的TLS,所以大多数为了有效的支持加密,都同时支持了SSL与STL
  • TLS提高了SSL: 虽然最早的TLS1.0与SSL3.0非常类似,但是TLS采用HMAC(keyed-Hashing for Message Authentication Code)算法对数据验证相比SSL的MAC(Message Authentication Code)算法会更难破解,并且在其他方面也有一些小的改进
  • 请求端口: 443

4. SPDY

读音speedy

是谷歌开发为了加快网页加载速度的网络协议。

SPDY兼容性: http://caniuse.com/#feat=spdy

image_1b8jj8l511lag13eslpm1al918krm.png-23.8kB

  • 采用多路复用(multiplexing): 多个请求stream共享一个tcp连接, 降低延时、提高带宽利用率
  • 请求优先级: 允许给每个请求设置优先级,使得重要的请求得到优先响应
  • TLS/SSL的加密传输: 强制要求使用TLS/SSL提高数据安全可靠性
  • 压缩请求头/响应头: 通过DEFLATE或gzip算法进行对请求头/响应头进行压缩
  • 支持Server Push: 允许服务端主动的推送资源(js、css)给客户端,当分析获知客户端将会需要时,以此利用起空闲带宽
  • 支持Server Hints: 允许服务端可以在客户端还没有发现将需要哪些资源的时候,主动通知客户端,以便于客户端实现准备好相关资源的缓存

5. HTTP/2

HTTP/2基于SPDY设计

image_1b90ik3e01di41tgr16hc12ks19uvp.png-129.5kB image_1b8jku3ol1rbveu4es1tp8rk61j.png-125kB

HTTP/2 vs SPDY

  • SSL/TLS: SPDY强制使用SSL/TLS,HTTP/2非强制(但是部分浏览器(如Chrome)不允许,所以目前如果使用HTTP/2最好都配置SSL/TLS)
  • 消息头压缩算法: HTTP/2消息头压缩算法采用HPACK,SPDY采用DEFLATE,一般情况下HPACK的压缩率会高于DEFLATE
  • 传输格式: HTTP/2传输采用二进制而非文本,因此HTTP/2中的基本单位是帧, 文本形式众多很难权衡健壮、性能与复杂度,二进制弥补了这个缺陷,并且是无序的帧,最终根据头帧重新组装
  • 继承与优化: HTTP/2继承并优化了SPDY的多路复用与Server Push

6. QUIC

  • 发音quick
  • QUIC 参考了HTTP/2与SPDY
  • Google在2013年10月第一次在IETF展示QUIC, 2016年7月启动工作群
  • 可靠的,多路复用的基于UDP的网络协议,内置安全加密模块,低延迟、运行在用户空间、开源的新一代网络协议。Google计划在完成后将其服务于所有的Google服务。

  • 减少建连延迟: 从未访问过服务的情况下1-RTT,其他的可以立马开始传输数据(0-RTT)
  • 拥塞控制: 提升TCP Cubic拥塞控制
  • HOL阻塞: 消除多路复用中的HOL阻塞(head-of-line blocking)
  • 更少的帧消耗: Quic数据包包含更少的帧,因此更多的数据包可以携带数据
  • 提升丢包重试: 丢包重试时使用新的序列号以及采用重新加密
  • 安全加密: 内置的加密模块(支持SNI,因此支持一个IP部署多个证书),并且是默认打开的,相比TLS更高效的向前加密 - 完成以后,将计划适配到TLS 1.3中
  • 端口: 使用443端口来处理UDP协议数据 - Port 80/443 UDP Traffic to Google?
  • 其他: 更好的FEC(Forward error correction)机制、与Connection migration机制

  • 从tcp原理角度理解Broken pipe和Connection Reset by Peer的区别
  • 淘宝HTTPS探索
  • HTTP,HTTP/2,SPDY,HTTPS你应该知道的一些事
  • QUIC Geek FAQ
  • google/bbr
  • 滑动窗口和拥塞窗口简述
  • BBR算法原理 - 李博杰
  • QUIC - Next generation multiplexed transport over UDP
  • Building Zero protocol for fast, secure mobile connections
  • 基于TLS1.3的微信安全通信协议mmtls介绍
  • QUIC Wire Layout Specification
  • SPDY - Wiki
  • This browser tweak saved 60% of requests to Facebook
  • HTTP2学习(四)—HTTP2的新特性
  • Server Push and Server Hints
  • What is TLS/SSL?
  • QUIC - Google-peering
  • QUIC教材
  • QUIC视频介绍
  • Http2-test
  • Http2-debug

网络相关总结

发表于 2019-03-11

TCP和UDP是否可以绑定同一端口进行通信

TCP、UDP可以绑定同一端口来进行通信: 网络中可以被命名和寻址的通信端口,是操作系统可分配的一种资源。 按照OSI七层协议的描述,传输层与网络层在功能上的最大区别是传输层提供进程通信能力。从这个意义上讲,网络通信的最终地址就不仅仅是主机地址了,还包括可以描述进程的某种标识符。为此,TCP/IP协议提出了协议端口(protocol port,简称端口)的概念,用于标识通信的进程。 端口是一种抽象的软件结构(包括一些数据结构和I/O缓冲区)。应用程序(即进程)通过系统调用与某端口建立连接(binding)后,传输层传给该端口的数据都被相应进程所接收,相应进程发给传输层的数据都通过该端口输出。在TCP/IP协议的实现中,端口操作类似于一般的I/O操作,进程获取一个端口,相当于获取本地唯一的I/O文件,可以用一般的读写原语访问之。 类似于文件描述符,每个端口都拥有一个叫端口号(port number)的整数型标识符,用于区别不同端口。由于TCP/IP传输层的两个协议TCP和UDP是完全独立的两个软件模块,因此各自的端口号也相互独立,如TCP有一个255号端口,UDP也可以有一个255号端口,二者并不冲突。

端口号的分配是一个重要问题。有两种基本分配方式:第一种叫全局分配,这是一种集中控制方式,由一个公认的中央机构根据用户需要进行统一分配,并将结果公布于众。第二种是本地分配,又称动态连接,即进程需要访问传输层服务时,向本地操作系统提出申请,操作系统返回一个本地唯一的端口号,进程再通过合适的系统调用将自己与该端口号联系起来(绑扎)。TCP/IP端口号的分配中综合了上述两种方式。TCP/IP将端口号分为两部分,少量的作为保留端口,以全局方式分配给服务进程。因此,每一个标准服务器都拥有一个全局公认的端口(即周知口,well-known port),即使在不同机器上,其端口号也相同。剩余的为自由端口,以本地方式进行分配。TCP和UDP均规定,小于256的端口号才能作保留端口。

再讨论一下,一个服务器监控一个端口,比如80端口,它为什么可以建立上成千上万的连接? 首先, 一个TCP连接需要由四元组来形成,即(src_ip,src_port,dst_ip,dst_port)。当一个连接请求过来的时候,服务端调用accept函数,新生成一个socket,这个socket所占用的本地端口依然是80端口。由四元组就很容易分析到了,同一个(src_ip,src_port),它所对应的(dst_ip,dst_port)可以无穷变化,这样就可以建立很多个客户端的请求了。

tips-net-http

发表于 2019-03-11

tips-net-mars

发表于 2019-03-11

tips-net-tcp

发表于 2019-03-11

首部格式

tips-net-tcp-2019311171945

各个段位说明:

  • 源端口和目的端口:  各占 2 字节.端口是传输层与应用层的服务接口.传输层的复用和分用功能都要通过端口才能实现
  • 序号:  占 4 字节.TCP 连接中传送的数据流中的每一个字节都编上一个序号.序号字段的值则指的是本报文段所发送的数据的第一个字节的序号
  • 确认号:  占 4 字节,是期望收到对方的下一个报文段的数据的第一个字节的序号
  • 数据偏移/首部长度:  占 4 位,它指出 TCP 报文段的数据起始处距离 TCP 报文段的起始处有多远.“数据偏移”的单位是 32 位字(以 4 字节为计算单位)
  • 保留:  占 6 位,保留为今后使用,但目前应置为 0
  • 紧急URG:  当 URG=1 时,表明紧急指针字段有效.它告诉系统此报文段中有紧急数据,应尽快传送(相当于高优先级的数据)
  • 确认ACK:  只有当 ACK=1 时确认号字段才有效.当 ACK=0 时,确认号无效
  • PSH(PuSH):  接收 TCP 收到 PSH = 1 的报文段,就尽快地交付接收应用进程,而不再等到整个缓存都填满了后再向上交付
  • RST (ReSeT):  当 RST=1 时,表明 TCP 连接中出现严重差错(如由于主机崩溃或其他原因),必须释放连接,然后再重新建立运输连接
  • 同步 SYN:  同步 SYN = 1 表示这是一个连接请求或连接接受报文
  • 终止 FIN:  用来释放一个连接.FIN=1 表明此报文段的发送端的数据已发送完毕,并要求释放运输连接
  • 检验和:  占 2 字节.检验和字段检验的范围包括首部和数据这两部分.在计算检验和时,要在 TCP 报文段的前面加上 12 字节的伪首部
  • 紧急指针:  占 16 位,指出在本报文段中紧急数据共有多少个字节(紧急数据放在本报文段数据的最前面)
  • 选项:  长度可变.TCP 最初只规定了一种选项,即最大报文段长度 MSS.MSS 告诉对方 TCP:“我的缓存所能接收的报文段的数据字段的最大长度是 MSS 个字节.” [MSS(Maximum Segment Size)是 TCP 报文段中的数据字段的最大长度.数据字段加上 TCP 首部才等于整个的 TCP 报文段]
  • 填充:  这是为了使整个首部长度是 4 字节的整数倍
  • 其他选项:
    • 窗口扩大:  占 3 字节,其中有一个字节表示移位值 S.新的窗口值等于TCP 首部中的窗口位数增大到(16 + S),相当于把窗口值向左移动 S 位后获得实际的窗口大小
    • 时间戳:  占10 字节,其中最主要的字段时间戳值字段(4字节)和时间戳回送回答字段(4字节)
    • 选择确认:  接收方收到了和前面的字节流不连续的两2字节.如果这些字节的序号都在接收窗口之内,那么接收方就先收下这些数据,但要把这些信息准确地告诉发送方,使发送方不要再重复发送这些已收到的数据

数据单位

TCP 传送的数据单位协议是 TCP 报文段(segment)

特点

TCP 是面向连接的传输层协议 每一条 TCP 连接只能有两个端点(endpoint),每一条 TCP 连接只能是点对点的(一对一) TCP 提供可靠交付的服务 TCP 提供全双工通信 面向字节流

注意

TCP 对应用进程一次把多长的报文发送到TCP 的缓存中是不关心的 TCP 根据对方给出的窗口值和当前网络拥塞的程度来决定一个报文段应包含多少个字节(UDP 发送的报文长度是应用进程给出的) TCP 可把太长的数据块划分短一些再传送.TCP 也可等待积累有足够多的字节后再构成报文段发送出去 每一条 TCP 连接有两个端点 TCP 连接的端点不是主机,不是主机的IP 地址,不是应用进程,也不是传输层的协议端口.TCP 连接的端点叫做套接字(socket)或插口

自动重传请求ARQ

定义:

可靠传输协议常称为自动重传请求ARQ (Automatic Repeat reQuest)

累积确认:

  • 定义:  接收方一般采用累积确认的方式.即不必对收到的分组逐个发送确认,而是对按序到达的最后一个分组发送确认,这样就表示:到这个分组为止的所有分组都已正确收到了
  • 优点:  容易实现,即使确认丢失也不必重传
  • 缺点:  不能向发送方反映出接收方已经正确收到的所有分组的信息

    Go-back-N(回退N):

    如果发送方发送了前 5 个分组,而中间的第 3 个分组丢失了.这时接收方只能对前两个分组发出确认.发送方无法知道后面三个分组的下落,而只好把后面的三个分组都再重传一次

具体实现

说明:

  • TCP 连接的每一端都必须设有两个窗口 一个发送窗口和一个接收窗口
  • TCP 可靠传输机制用字节的序号进行控制.TCP 所有的确认都是基于序号而不是基于报文段
  • TCP 两端的四个窗口经常处于动态变化之中
  • TCP连接的往返时间 RTT 也不是固定不变的.需要使用特定的算法估算较为合理的重传时间

图释

tips-net-tcp-201931117259

发送缓存

发送缓存用来暂时存放:

  • 发送应用程序传送给发送方 TCP 准备发送的数据
  • TCP 已发送出但尚未收到确认的数据

    图释:

    tips-net-tcp-2019311172637

接收缓存

接收缓存用来暂时存放:

  • 按序到达的、但尚未被接收应用程序读取的数据;
  • 不按序到达的数据

    图释:

    tips-net-tcp-2019311172725

滑动窗口

滑动窗口(rwnd)是用于流控的动态缩放可靠滑动的接收与发送窗口,防止发送端发送过快接收端被淹没 对应的还有拥塞窗口(rwnd),是在一个RTT内可以最多一次可发送的报文段数 — 发送方的流量控制

TCP是以报文段(若干字节)为单位,每一个报文段需要一次ACK确认收到,但是其带来的问题很明显,频繁的发送确认等待导致用于确认与等待的时间太长。引入窗口后,发送端只要在窗口内,便不用每次都等待ACK才发送下一个报文段,可以在发送窗口内一次连续发送几个报文段而无需等待ACK

图释:

tips-net-tcp-2019311172750

特点:

  • 以字节为单位的滑动窗口
  • A 的发送窗口并不总是和 B 的接收窗口一样大(因为有一定的时间滞后)

    要求:

  • TCP 标准没有规定对不按序到达的数据应如何处理.通常是先临时存放在接收窗口中,等到字节流中所缺少的字节收到后,再按序交付上层的应用进程
  • TCP 要求接收方必须有累积确认的功能,这样可以减小传输开销

具体实现:

tips-net-tcp-201931117296 tips-net-tcp-2019311172934 tips-net-tcp-2019311172949 tips-net-tcp-201931117305

发送窗口与接收窗口的关系

TCP是双工协议,会话双方都可以同时接收与发送数据,因此双方都同时维护一个发送窗口与接收窗口。

  • 接收窗口大小取决于应用、系统、硬件等限制;
  • 发送窗口大小取决于对方接收窗口的大小

窗口滑动协定

  • 发送窗口只有在收到窗口内字节的ACK确认,才会滑动其左边界
  • 接收窗口只有在窗口中所有的段都正确收到的情况下,才会滑动其左边界;当有字节未接收,但收到后面的字节的情况下,也会滑动,也不对后续字节确认,确保对方重传未接收字节

    哪些允许变化

  • 最大报文段大小在握手中,就确定了
  • 窗口缩放因子在握手中,就确定了
  • 接收窗口大小在根据本地的处理能力与缓存剩余空间动态调整,通过ACK带给对方当前剩余的接收窗口大小

确认丢失和确认迟到

tips-net-tcp-2019311173223 RTT = 传播时间+接收端处理时间+路由器的排队时间(变化较大反应当前网络拥塞情况)

超时重传时间选择

具体实现:

TCP 每发送一个报文段,就对这个报文段设置一次计时器.只要计时器设置的重传时间到但还没有收到确认,就要重传这一报文段

加权平均往返时间:

做法:

TCP 保留了 RTT 的一个加权平均往返时间 RTTS(这又称为平滑的往返时间),第一次测量到 RTT 样本时,RTTS 值就取为所测量到的 RTT 样本值.以后每测量到一个新的 RTT 样本,就按下式重新计算一次 RTTS: R

公式:

新的 RTTS = ( 1 - α)×(旧的 RTTS)+α(新的 RTT 样本)

说明:

式中,0 ≤ α< 1.若α很接近于零,表示 RTT 值更新较慢若选择 α 接近于1,则表示 RTT 值更新较快 RFC 2988 推荐的 α 值为 1/8,即 0.125

超时重传时间RTO:

RTO 应略大于上面得出的加权平均往返时间 RTTS. RFC 2988 建议使用下式计算 RTO:

1
RTO=RTTS + 4×RTTD

RTTD 是 RTT 的偏差的加权平均值 RFC 2988 建议这样计算 RTTD.第一次测量时,RTTD 值取为测量到的 RTT 样本值的一半.在以后的测量中,则使用下式计算加权平均的 RTTD:

新的 RTTD = (1-β)×(旧的RTTD)+β×|RTTS﹣新的 RTT 样本| β是个小于 1 的系数,其推荐值是 1/4,即 0.25 在计算平均往返时间 RTT 时,只要报文段重传了,就不采用其往返时间样本

修正的Karn算法:

报文段每重传一次,就把 RTO 增大一些:

1
新的 RTO= γ×(旧的 RTO)

系数γ 的典型值是 2 当不再发生报文段的重传时,才根据报文段的往返时延更新平均往返时延 RTT 和超时重传时间 RTO 的数值

持续计时器

  • TCP 为每一个连接设有一个持续计时器
  • 只要 TCP 连接的一方收到对方的零窗口通知,就启动持续计时器
  • 若持续计时器设置的时间到期,就发送一个零窗口探测报文段(仅携带 1 字节的数据),而对方就在确认这个探测报文段时给出了现在的窗口值
  • 若窗口仍然是零,则收到这个报文段的一方就重新设置持续计时器
  • 若窗口不是零,则死锁的僵局就可以打破了

报文段的发送时机

TCP 维持一个变量,它等于最大报文段长度 MSS.只要缓存中存放的数据达到 MSS 字节时,就组装成一个 TCP 报文段发送出去 由发送方的应用进程指明要求发送报文段,即 TCP 支持的推送(push)操作 发送方的一个计时器期限到了,这时就把当前已有的缓存数据装入报文段(但长度不能超过 MSS)发送出去

运输连接

三个阶段:

连接建立:

图释:

tips-net-tcp-2019311174443

步骤:
  • A 的 TCP 向 B 发出连接请求报文段,其首部中的同步位 SYN = 1,并选择序号 seq = x,表明传送数据时的第一个数据字节的序号是 x
  • B 的 TCP 收到连接请求报文段后,如同意,则发回确认(B 在确认报文段中应使 SYN = 1,使 ACK = 1,其确认号ack = x﹢1,自己选择的序号 seq = y)
  • A 收到此报文段后向 B 给出确认,其 ACK = 1,确认号 ack = y﹢1(A 的 TCP 通知上层应用进程,连接已经建立,B 的 TCP 收到主机 A 的确认后,也通知其上层应用进程:TCP 连接已经建立)

数据传送

连接释放:

图释

tips-net-tcp-2019311174722

步骤:
  • 数据传输结束后,通信的双方都可释放连接.现在 A 的应用进程先向其 TCP 发出连接释放报文段,并停止再发送数据,主动关闭 TCP 连接(A 把连接释放报文段首部的 FIN = 1,其序号seq = u,等待 B 的确认)
  • B 发出确认,确认号 ack = u+1,而这个报文段自己的序号 seq = v(TCP 服务器进程通知高层应用进程.从 A 到 B 这个方向的连接就释放了,TCP 连接处于半关闭状态.B 若发送数据,A 仍要接收)
  • 若 B 已经没有要向 A 发送的数据,其应用进程就通知 TCP 释放连接
  • A 收到连接释放报文段后,必须发出确认,在确认报文段中 ACK = 1,确认号 ack=w﹢1,自己的序号 seq = u + 1
注意:

TCP 连接必须经过时间 2MSL 后才真正释放掉(2MSL 的时间的用意 — 为了保证 A 发送的最后一个 ACK 报文段能够到达 B.防止 “已失效的连接请求报文段”出现在本连接中.A 在发送完最后一个 ACK 报文段后,再经过时间 2MSL,就可以使本连接持续的时间内所产生的所有报文段,都从网络中消失.这样就可以使下一个新的连接中不会出现这种旧的连接请求报文段)

发现丢失确认时候的处理

tips-net-tcp-2019311174940

三个问题:

  • 要使每一方能够确知对方的存在
  • 要允许双方协商一些参数(如最大报文段长度,最大窗口大小,服务质量等)
  • 能够对运输实体资源(如缓存大小,连接表中的项目等)进行分配

发送TCP请求客户端

tips-net-tcp-2019311175058

拥塞处理相关概念

拥塞窗口:

  • 含义:拥塞窗口的大小取决于网络的拥塞程度,并且动态地在变化.发送方让自己的发送窗口等于拥塞窗口.如再考虑到接收方的接收能力,则发送窗口还可能小于拥塞窗口
  • 发送方控制拥塞窗口的原则:只要网络没有出现拥塞,拥塞窗口就再增大一些,以便把更多的分组发送出去.但只要网络出现拥塞,拥塞窗口就减小一些,以减少注入到网络中的分组数

乘法减小:

是指不论在慢开始阶段还是拥塞避免阶段,只要出现一次超时(即出现一次网络拥塞),就把慢开始门限值 ssthresh 设置为当前的拥塞窗口值乘以 0.5

加法增大:

是指执行拥塞避免算法后,在收到对所有报文段的确认后(即经过一个往返时间),就把拥塞窗口 cwnd增加一个 MSS 大小,使拥塞窗口缓慢增大,以防止网络过早出现拥塞

快重传:

快重传算法首先要求接收方每收到一个失序的报文段后就立即发出重复确认.这样做可以让发送方及早知道有报文段没有到达接收方,发送方只要一连收到三个重复确认就应当立即重传对方尚未收到的报文段 tips-net-tcp-2019311175358

快恢复:

当发送端收到连续三个重复的确认时,就执行“乘法减小”算法,把慢开始门限 ssthresh 减半.但接下去不执行慢开始算法

发送窗口的上限值:

发送方的发送窗口的上限值应当取为接收方窗口 rwnd 和拥塞窗口 cwnd 这两个变量中较小的一个,即应按以下公式确定: 发送窗口的上限值 Min [rwnd, cwnd]

当 rwnd < cwnd 时,是接收方的接收能力限制发送窗口的最大值 当 cwnd < rwnd 时,则是网络的拥塞限制发送窗口的最大值

避免拥塞具体实现

有滑动窗口了,为什么还要拥塞窗口

发送方与接收方之间存在多个路由器和速率较慢的链路时,一些中间路由器就必须缓存分组,并可能耗尽缓存,此时便会出现拥塞,这将严重降低了TCP连接的吞吐量,拥塞窗口就是为了防止过多的数据注入到网络中,中间路由无法消化的问题。

TCP的做法是引入拥塞窗口(cwnd)并策略性的调整其大小,如上文提到的发送窗口大小是取滑动窗口大小与拥塞窗口大小的最小值,这个正是用来缓解该问题,下面是拥塞窗口大小变化的策略:

1. 慢开始、拥塞控制

tips-net-tcp-2019312133854 其目的是: 拥塞发生时循序减少主机发送到网络的报文数,使得这时路由器有足够的时间消化积压的报文。

  • 当主机开发发送数据时,拥塞窗口(cwnd)被初始化为1个报文段,试探性的发送1个字节的报文
  • 每收到一个ACK,拥塞窗口大小就指数的增加报文段数目(1,2,4,16…)
  • 最终到达提前预设的慢开始阀值(ssthresh),停止使用慢开始算法,改用拥塞避免算法
  • 拥塞避免算法是每经过一个RTT,拥塞窗口就增加一个报文段,即改为线性的增加报文段
  • 最终会出现网络拥塞,比如丢包等情况,停止拥塞避免算法,将慢开始阀值设置为目前拥塞时拥塞窗口大小的一半(但不能小于2),并重置拥塞窗口大小为1个报文段,开始新的一轮慢开始

慢开始门限 ssthresh 的用法:

  • 当 cwnd < ssthresh 时,使用慢开始算法
  • 当 cwnd > ssthresh 时,停止使用慢开始算法而改用拥塞避免算法
  • 当 cwnd = ssthresh 时,既可使用慢开始算法,也可使用拥塞避免算法

2. 快重传,快恢复

tips-net-tcp-2019312134328 其目的是: 减少因为拥塞导致的数据包丢失的重传时间,避免无用的数据到网络

接收方: 如果一个包丢失,后续的包继续发送针对该包的重传请求

发送方: 一旦收到三个一样的确认,判定为拥塞:

  • 立即重传该包
  • 开始执行快恢复算法
  • 快恢复是慢开始阀值设置为目前拥塞时拥塞窗口大小的一半;拥塞窗口大小设置为目前设置后的慢开始阀值的大小;执行拥塞避免算法

TCP窗口特殊情况

1. Persistence timer

tips-net-tcp-2019312134856 防止丢包导致发送端停留在上次收到的接收窗口大小为0的情况:

  • 接收端B: 我的缓存已满,接收窗口为0
  • 发送端A: 停止发送数据, 并启动持续计时器(Persistence timer)
  • 接收端B: 消化完缓存,发送报文给发送端A,我的接收窗口大小为400,但是 这个报文丢了
  • 发送端A: 计时器时间到,发送一个1字节的探测报文
  • 接收端B: 重新发送,接收窗口大小为400
  • 发送端A: 继续发送数据

    2. 应用层每次单字节发送

    单个发送字节,然后等待一个确认,再发送一个字节,这样为一个字节添加40个字节头的做法,无疑增加了网络中许多不必要的报文,该问题TCP层的解决方案:

发送方采用Nagle算法:

  • 若应用层是逐个字节把数据送到TCP,那么TCP不会逐个的发送,而是先发送第一个数据字节,然后缓存剩余的
  • 在收到第一个字节的ACK获知网络情况与对方的接收窗口大小后,把缓存的剩余字节组成合适的报文发送出去
  • 到达的数据达到发送窗口大小的一半或者报文段的最大长度时,立即发送

接收方的做法:

  • 等待本地有足够的缓存空间容纳一个报文段,或者等到本地的缓存空间有一半空闲的时候,再通知发送端发送数据。

TCP 的有限状态机

说明:

  • TCP 有限状态机的图中每一个方框都是 TCP 可能具有的状态
  • 每个方框中的大写英文字符串是 TCP 标准所使用的 TCP 连接状态名.状态之间的箭头表示可能发生的状态变迁
  • 箭头旁边的字,表明引起这种变迁的原因,或表明发生状态变迁后又出现什么动作
  • 图中有三种不同的箭头
    • 粗实线箭头表示对客户进程的正常变迁
    • 粗虚线箭头表示对服务器进程的正常变迁
    • 另一种细线箭头表示异常变迁

tips-net-tcp-2019311175949

其他概念

MTU

什么是MTU(Maximum Transmit Unit)

由于以太网传输的限制,每个以太网网数据帧的大小都是落在在区间[64Bytes,1518Bytes]中的,不在区间内的一般会被视为错误的数据帧,以太网转发设备直接丢弃。而根据以太网每帧的数据构成,除去固定的部分,留给上层协议的只有Data域的1500Bytes,我们将它称为MTU。

以太网(Ethernet II)每帧的数据构成: 目的Mac地址(DMAC)+源Mac地址(SMAC)+类型(Type)+数据(Data)+校验(CRC) = 6Bytes(48bit)DMAC + 6Bytes(48bit)SMAC + 2Bytes(16bit)Type + 1500BytesData + 4Bytes(24bit)CRC

MTU造成什么影响

由于一个帧放不下,如IP协议,就会对数据包进行分片处理,这就导致了原本一次可以搞定的,被分为多次,降低传输性能,不过我们可以通过在数据包包头加上DF(DonotFragment)标签来强制不被分片处理。

UDP协议不用关心数据的到达的有序以及正确,因此对分片无特殊要求 TCP协议相反,因此TCP协议本身的最大报文段大小MSS也受MTU影响,通常MSS是: MTU - 20Bytes(IP Header) - 20Bytes(TCP Header) 不过好在绝大多数的网络链路都是1500Bytes的MTU或者更大

什么是MSS(Maximum Segment Size)

TCP的最大报文段大小,只包含TCP Payload(不包含TCP Header与TCP Option)的TCP每次能够传输的最大数据分段的大小,可以用来限制每次发送的字节数。通常大小为1460Bytes(1500BytesMTU - 20Bytes(IP Header) - 20Bytes(TCP Header))

MSS是在TCP建连时确定的,通讯双方会根据双方提供的MSS值,取最小的MSS作为该次连接数据传输的MSS

什么是WS(Window Scaling)

TCP首部中表示Window Size的字段只有16位,因此按照协议,能表示的最大窗口大小是2^16-1=65535Bytes(64Kb),因此TCP的选项字段中包含了窗口扩大因子(WS)分别用option-kind、option-length、option-data来表示,这个参数可带可不带,只有在双方都支持的情况下,才会生效。如双方的WS都是256,而后我们ACK Window size value是5,那么此时就可以表示我们的接收窗口是1280Bytes(5*256=1280)。

参考

TCP的滑动窗口与拥塞窗口 计算机网络【七】:可靠传输的实现 TCP窗口控制、流控制、拥塞控制 也谈一下TCP segment of a reassembled PDU TCP流量控制中的滑动窗口大小 TCP 滑动窗口(发送窗口和接收窗口) TCP协议的滑动窗口具体是怎样控制流量的?

tips-net-nat

发表于 2019-03-11

1.NAT

NAT(Network Address Translation,网络地址转换)是1994年提出的。当在专用网内部的一些主机本来已经分配到了本地IP地址(即仅在本专用网内使用的专用地址),但现在又想和因特网上的主机通信(并不需要加密)时,可使用NAT方法。 这种方法需要在专用网连接到因特网的路由器上安装NAT软件。装有NAT软件的路由器叫做NAT路由器,它至少有一个有效的外部全球IP地址。这样,所有使用本地地址的主机在和外界通信时,都要在NAT路由器上将其本地地址转换成全球IP地址,才能和因特网连接。 另外,这种通过使用少量的公有IP 地址代表较多的私有IP 地址的方式,将有助于减缓可用的IP地址空间的枯竭。在RFC 2663中有对NAT的说明。

NAT的实现方式有三种,即静态转换Static Nat、动态转换Dynamic Nat和端口多路复用OverLoad。

静态转换

是指将内部网络的私有IP地址转换为公有IP地址,IP地址对是一对一的,是一成不变的,某个私有IP地址只转换为某个公有IP地址。借助于静态转换,可以实现外部网络对内部网络中某些特定设备(如服务器)的访问。

动态转换

是指将内部网络的私有IP地址转换为公用IP地址时,IP地址是不确定的,是随机的,所有被授权访问上Internet的私有IP地址可随机转换为任何指定的合法IP地址。也就是说,只要指定哪些内部地址可以进行转换,以及用哪些合法地址作为外部地址时,就可以进行动态转换。动态转换可以使用多个合法外部地址集。当ISP提供的合法IP地址略少于网络内部的计算机数量时。可以采用动态转换的方式。

端口多路复用(Port address Translation,PAT)

是指改变外出数据包的源端口并进行端口转换,即端口地址转换(PAT,Port Address Translation).采用端口多路复用方式。内部网络的所有主机均可共享一个合法外部IP地址实现对Internet的访问,从而可以最大限度地节约IP地址资源。同时,又可隐藏网络内部的所有主机,有效避免来自internet的攻击。因此,目前网络中应用最多的就是端口多路复用方式。 ALG(Application Level Gateway),即应用程序级网关技术:传统的NAT技术只对IP层和传输层头部进行转换处理,但是一些应用层协议,在协议数据报文中包含了地址信息。为了使得这些应用也能透明地完成NAT转换,NAT使用一种称作ALG的技术,它能对这些应用程序在通信时所包含的地址信息也进行相应的NAT转换。例如:对于FTP协议的PORT/PASV命令、DNS协议的 “A” 和 “PTR” queries命令和部分ICMP消息类型等都需要相应的ALG来支持。 如果协议数据报文中不包含地址信息,则很容易利用传统的NAT技术来完成透明的地址转换功能,通常我们使用的如下应用就可以直接利用传统的NAT技术:HTTP、TELNET、FINGER、NTP、NFS、ARCHIE、RLOGIN、RSH、RCP等。

2.TCP长连接

TCP连接建立后只要不明确关闭,逻辑上连接一直存在。 TCP是有保活定时器的,可以打开保活定时器来维持长连接,设置SO_KEEPALIVE才会开启,时间间隔默认7200s,也就是2h,这个默认是关闭的。

注意:HTTP的keepalive和TCP的用处不大一样tcp。

3.NAT超时

因为 IP v4 的 IP 量有限,运营商分配给手机终端的 IP 是运营商内网的 IP,手机要连接 Internet,就需要通过运营商的网关做一个网络地址转换(Network Address Translation,NAT)。简单的说运营商的网关需要维护一个外网 IP、端口到内网 IP、端口的对应关系,以确保内网的手机可以跟 Internet 的服务器通讯。 大部分移动无线网络运营商都在链路一段时间没有数据通讯时,会淘汰 NAT 表中的对应项,造成链路中断。 长连接心跳间隔必须要小于NAT超时时间(aging-time),如果超过aging-time不做心跳,TCP长连接链路就会中断,Server就无法发送Push给手机,只能等到客户端下次心跳失败后,重建连接才能取到消息。

因为IPv4地址不足, 或者我们想通过无线路由器上网, 我们的设备可能会处在一个NAT设备的后面, 生活中最常见的NAT设备是家用路由器. NAT设备会在IP封包通过设备时修改源/目的IP地址. 对于家用路由器来说, 使用的是网络地址端口转换(NAPT), 它不仅改IP, 还修改TCP和UDP协议的端口号, 这样就能让内网中的设备共用同一个外网IP. 举个例子, NAPT维护一个类似下表的NAT表 |内网地址| 外网地址| |—|—| |192.168.0.2:5566| 120.132.92.21:9200| |192.168.0.3:7788| 120.132.92.21:9201| |192.168.0.3:8888| 120.132.92.21:9202|

NAT设备会根据NAT表对出去和进来的数据做修改, 比如将192.168.0.3:8888发出去的封包改成120.132.92.21:9202, 外部就认为他们是在和120.132.92.21:9202通信. 同时NAT设备会将120.132.92.21:9202收到的封包的IP和端口改成192.168.0.3:8888, 再发给内网的主机, 这样内部和外部就能双向通信了, 但如果其中192.168.0.3:8888 == 120.132.92.21:9202这一映射因为某些原因被NAT设备淘汰了, 那么外部设备就无法直接与192.168.0.3:8888通信了.

国内移动无线网络运营商在链路上一段时间内没有数据通讯后, 会淘汰NAT表中的对应项, 造成链路中断.

4.心跳包

  • 心跳的原因:虽然理论tcp连接后一直不断,但实际上会断网。见:比如 NAT超时,更多 影响TCP连接寿命的因素
  • 心跳包的主要作用是告知对方连接端,我还活着,心还在跳。
  • 心跳时长多少?    现实是残酷的, 根据网上的一些说法, 中移动2/3G下, NAT超时时间为5分钟, 中国电信3G则大于28分钟, 理想的情况下, 客户端应当以略小于NAT超时时间的间隔来发送心跳包.
    地区/网络 NAT超时时间
    中国移动3G和2G 5分钟
    中国联通2G 5分钟
    中国电信3G 大于28分钟
    美国3G 大于28分钟
    台湾3G 大于28分钟

wifi下, NAT超时时间都会比较长, 据说宽带的网关一般没有空闲释放机制, GCM有些时候在wifi下的心跳比在移动网络下的心跳要快, 可能是因为wifi下联网通信耗费的电量比移动网络下小

5.心跳包和轮询的区别

心跳包和轮询看起来类似, 都是客户端主动联系服务器, 但是区别很大.

  • 轮询是为了获取数据, 而心跳是为了保活TCP连接.
  • 轮询得越频繁, 获取数据就越及时, 心跳的频繁与否和数据是否及时没有直接关系
  • 轮询比心跳能耗更高, 因为一次轮询需要经过TCP三次握手, 四次挥手, 单次心跳不需要建立和拆除TCP连接.

Android架构之网络优化

发表于 2019-03-08 | 分类于 Android

常规的网络框架设计和常用的网络优化方案。

  1. 网络框架OkHttp
  • 简洁易用的接口
  • 拦截器机制,网络重试与跳转
  • 连接池复用
  1. 网络加速
  • HttpDNS与IP直连
  • 连接加速:短连接复用、Http2多路复用、长连接
  1. 数据压缩与序列化
  • Json vs ProtoBuf
  • 压缩算法
  • 序列化
  1. 长连接技术与Mars架构
  • 智能心跳机制
  • 自动重连
  • Android跨进程实现
  • 智能唤醒
  1. 如何应对复杂网络
  • 弱网
  • 网络超时、振荡
  • 404与DNS劫持
  1. 如何保证网络数据安全
  • TLS协议,握手与证书
  • 数据签名及校验

https://github.com/dhhAndroid/RxWebSocket

网络错误

ECONNABORTED

该错误被描述为“software caused connection abort”,即“软件引起的连接中止”。原因在于当服务和客户进程在完成用于 TCP 连接的“三次握手”后,客户 TCP 却发送了一个 RST (复位)分节,在服务进程看来,就在该连接已由 TCP 排队,等着服务进程调用 accept 的时候 RST 却到达了。POSIX 规定此时的 errno 值必须 ECONNABORTED。源自 Berkeley 的实现完全在内核中处理中止的连接,服务进程将永远不知道该中止的发生。服务器进程一般可以忽略该错误,直接再次调用accept。 SocketException: Software caused connection abort: recv failed

1
2
3
4
5
6
7
8
/* Linux system */  

include/asm-alpha/errno.h:#define ECONNABORTED 53 /* Software caused connection
abort */
include/asm-generic/errno.h:#define ECONNABORTED 103 /* Software caused
connection abort */
include/asm-mips/errno.h:#define ECONNABORTED 130 /* Software caused connection
abort */

导致这个异常出现的根本原因可能有多个, 在服务端/客户端单方面关闭连接的情况下,另一方依然以为 tcp连接仍然建立,试图读取对方的响应数据,导致出现 Software caused connection abort: recv failed的异常. 可能是是防火墙的原因。

ECONNRESET

该错误被描述为“connection reset by peer”,即“对方复位连接”,这种情况一般发生在服务进程较客户进程提前终止。当服务进程终止时会向客户 TCP 发送 FIN 分节,客户 TCP 回应 ACK,服务 TCP 将转入 FIN_WAIT2 状态。此时如果客户进程没有处理该 FIN (如阻塞在其它调用上而没有关闭 Socket 时),则客户 TCP 将处于 CLOSE_WAIT 状态。当客户进程再次向 FIN_WAIT2 状态的服务 TCP 发送数据时,则服务 TCP 将立刻响应 RST。一般来说,这种情况还可以会引发另外的应用程序异常,客户进程在发送完数据后,往往会等待从网络IO接收数据,很典型的如 read 或 readline 调用,此时由于执行时序的原因,如果该调用发生在 RST 分节收到前执行的话,那么结果是客户进程会得到一个非预期的 EOF 错误。此时一般会输出“server terminated prematurely”-“服务器过早终止”错误。

EPIPE

错误被描述为“broken pipe”,即“管道破裂”,这种情况一般发生在客户进程不理会(或未及时处理)Socket 错误,继续向服务 TCP 写入更多数据时,内核将向客户进程发送 SIGPIPE 信号,该信号默认会使进程终止(此时该前台进程未进行 core dump)。结合上边的 ECONNRESET 错误可知,向一个 FIN_WAIT2 状态的服务 TCP(已 ACK 响应 FIN 分节)写入数据不成问题,但是写一个已接收了 RST 的 Socket 则是一个错误。

ETIMEDOUT

错误被描述为“connect time out”,即“连接超时”,这种情况一般发生在服务器主机崩溃。此时客户 TCP 将在一定时间内(依具体实现)持续重发数据分节,试图从服务 TCP 获得一个 ACK 分节。当最终放弃尝试后(此时服务器未重新启动),内核将会向客户进程返回 ETIMEDOUT 错误。如果某个中间路由器判定该服务器主机已经不可达,则一般会响应“destination unreachable”-“目的地不可达”的ICMP消息,相应的客户进程返回的错误是 EHOSTUNREACH 或ENETUNREACH。当服务器重新启动后,由于 TCP 状态丢失,之前所有的连接信息也不存在了,此时对于客户端发来请求将回应 RST。如果客户进程对检测服务器主机是否崩溃很有必要,要求即使客户进程不主动发送数据也能检测出来,那么需要使用其它技术,如配置 SO_KEEPALIVE Socket 选项,或实现某些心跳函数。

ENOPROTOOPT

该错误不是一个 Socket 连接相关的错误。errno 给出该值可能由于,通过 getsockopt 系统调用来获得一个套接字的当前选项状态时,如果发现了系统不支持的选项参数就会引发该错误。 getsockopt/setsockopt(2) man page 写道

1
2
3
4
5
6
7
8
9
10
11
getsockopt, setsockopt -- get and set options on sockets.

#include <sys/socket.h>

int getsockopt(int socket, int level, int option_name,
void *restrict option_value, socklen_t *restrict option_len);

int setsockopt(int socket, int level, int option_name,
const void *option_value, socklen_t option_len);

Getsockopt() and setsockopt() manipulate the options associated with a socket. Options may exist at multiple protocol levels; they are always present at the uppermost "socket" level.

此外,getsockopt 和 setsockopt 还可能引发以下错误:

getsockopt/setsockopt(2) man page 写道

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
ERRORS

The getsockopt() and setsockopt() system calls will succeed unless:

[EBADF] The argument socket is not a valid file descriptor.
[EFAULT] The address pointed to by option_value is not in a valid part of the process dress space. For getsockopt(), this error may also be returned if option_len is not in a valid part of the process address space.
[EINVAL] The option is invalid at the level indicated.
[ENOBUFS]Insufficient memory buffers are available.
[ENOPROTOOPT] The option is unknown at the level indicated.
[ENOTSOCK] The argument socket is not a socket (e.g., a plain file).

The setsockopt() system call will succeed unless:

[EDOM] The argument option_value is out of bounds.
[EISCONN]socket is already connected and a specified option cannot be set while this is the case.

ECONNEREFUSED

A “connect failed: ECONNREFUSED (Connection refused)” most likely means that there is nothing listening on that port AND that IP address. Possible explanations include:

  • the service has crashed or hasn’t been started,
  • your client is trying to connect using the wrong IP address or port, or
  • server access is being blocked by a firewall that is “refusing” on the server/service’s behalf. This is pretty unlikely given that normal practice (these days) is for firewalls to “blackhole” all unwanted connection attempts.
  • The server couldn’t send a response: Ensure that the backend is working properly at IP and port mentioned.
  • SSL connections are being blocked: Fix this by importing SSL certificates
  • Cookies not being sent
  • Request timeout: Change request timeout

The java.net.SocketException is thrown when there is an error creating or accessing a socket (such as TCP). This usually can be caused when the server has terminated the connection (without properly closing it), so before getting the full response. In most cases this can be caused either by the timeout issue (e.g. the response takes too much time or server is overloaded with the requests), or the client sent the SYN, but it didn’t receive ACK (acknowledgment of the connection termination). For timeout issues, you can consider increasing the timeout value.

The Socket Exception usually comes with the specified detail message about the issue.

Example of detailed messages:

Software caused connection abort: recv failed.

The error indicates an attempt to send the message and the connection has been aborted by your server. If this happened while connecting to the database, this can be related to using not compatible Connector/J JDBC driver.

Possible solution: Make sure you’ve proper libraries/drivers in your CLASSPATH.

Software caused connection abort: connect.

This can happen when there is a problem to connect to the remote. For example due to virus-checker rejecting the remote mail requests.

Possible solution: Check Virus scan service whether it’s blocking the port for the outgoing requests for connections.

Software caused connection abort: socket write error.

Possible solution: Make sure you’re writing the correct length of bytes to the stream. So double check what you’re sending. See this thread.

Connection reset by peer: socket write error / Connection aborted by peer: socket write error

The application did not check whether keep-alive connection had been timed out on the server side.

Possible solution: Ensure that the HttpClient is non-null before reading from the connection.E13222_01

Connection reset by peer.

The connection has been terminated by the peer (server).

Connection reset.

The connection has been either terminated by the client or closed by the server end of the connection due to request with the request.

What’s causing my java.net.SocketException: Connection reset?

Android SharedPreference详解

发表于 2019-03-07 | 分类于 Android

SharedPreferences作为一种数据持久化的方式,是处理简单的key-value类型数据时的首选。

一般用法:

1
2
3
4
5
6
7
8
9
//demo是该sharedpreference对应文件名,对应的是一个xml文件,里面存放key-value格式的数据.
SharedPreferences sharedPreferences = context.getSharedPreferences("demo", MODE_WORLD_WRITEABLE);
//提供了getXXX的读取数据方法
boolean xxx = sharedPreferences.getBoolean("xxx", false);
//通过Editor提供了putXXX系列的存储方法,调用完需要使用apply()或commit()使之生效,不同点后面介绍
SharedPreferences.Editor edit = sharedPreferences.edit();
edit.putBoolean("xxx", true);
edit.apply();//使存储生效
//edit.commit();//使存储生效

每个SharedPreferences都对应了当前package的data/data/package_name/share_prefs/目录下的一个文件

源码解析

Context.java中getSharedPreferences接口说明:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
/**
* Retrieve and hold the contents of the preferences file 'name', returning
* a SharedPreferences through which you can retrieve and modify its
* values. Only one instance of the SharedPreferences object is returned
* to any callers for the same name, meaning they will see each other's
* edits as soon as they are made.
*
* @param name Desired preferences file. If a preferences file by this name
* does not exist, it will be created when you retrieve an
* editor (SharedPreferences.edit()) and then commit changes (Editor.commit()).
* @param mode Operating mode. Use 0 or {@link #MODE_PRIVATE} for the
* default operation, {@link #MODE_WORLD_READABLE}
* and {@link #MODE_WORLD_WRITEABLE} to control permissions.
*
* @return The single {@link SharedPreferences} instance that can be used
* to retrieve and modify the preference values.
*
* @see #MODE_PRIVATE
* @see #MODE_WORLD_READABLE
* @see #MODE_WORLD_WRITEABLE
*/
public abstract SharedPreferences getSharedPreferences(String name,
int mode);

ContextImpl中getSharedPreferences实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
@Override
public SharedPreferences getSharedPreferences(String name, int mode) {
SharedPreferencesImpl sp;
synchronized (ContextImpl.class) {
if (sSharedPrefs == null) {
sSharedPrefs = new ArrayMap<String, ArrayMap<String, SharedPreferencesImpl>>();
}

final String packageName = getPackageName();
ArrayMap<String, SharedPreferencesImpl> packagePrefs = sSharedPrefs.get(packageName);
if (packagePrefs == null) {
packagePrefs = new ArrayMap<String, SharedPreferencesImpl>();
sSharedPrefs.put(packageName, packagePrefs);
}

// At least one application in the world actually passes in a null
// name. This happened to work because when we generated the file name
// we would stringify it to "null.xml". Nice.
if (mPackageInfo.getApplicationInfo().targetSdkVersion <
Build.VERSION_CODES.KITKAT) {
if (name == null) {
name = "null";
}
}

sp = packagePrefs.get(name);
if (sp == null) {
File prefsFile = getSharedPrefsFile(name);
sp = new SharedPreferencesImpl(prefsFile, mode);
packagePrefs.put(name, sp);
return sp;
}
}
if ((mode & Context.MODE_MULTI_PROCESS) != 0 ||
getApplicationInfo().targetSdkVersion < android.os.Build.VERSION_CODES.HONEYCOMB) {
// If somebody else (some other process) changed the prefs
// file behind our back, we reload it. This has been the
// historical (if undocumented) behavior.
sp.startReloadIfChangedUnexpectedly();
}
return sp;
}

这段代码里,我们可以看出,

  1. SharedPreferencesImpl是保存在全局个map cache里的,只会创建一次。
  2. MODE_MULTI_PROCESS模式下,每次获取都会尝试去读取文件reload。当然会有一些逻辑尽量减少读取次数,比如当前是否有正在进行的读取操作,文件的修改时间和大小与上次有没有变化等。

Context.java中提供了以下四种mode:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
//这是默认模式,仅caller uid的进程可访问
/**
* File creation mode: the default mode, where the created file can only
* be accessed by the calling application (or all applications sharing the
* same user ID).
* @see #MODE_WORLD_READABLE
* @see #MODE_WORLD_WRITEABLE
*/
int MODE_PRIVATE = 0x0000;

//所有人可写,也就是任何应用都可修改它,这是极其危险的,因此改选项已被Deprected
/**
* @deprecated Creating world-readable files is very dangerous, and likely
* to cause security holes in applications. It is strongly discouraged;
* instead, applications should use more formal mechanism for interactions
* such as {@link ContentProvider}, {@link BroadcastReceiver}, and
* {@link android.app.Service}. There are no guarantees that this
* access mode will remain on a file, such as when it goes through a
* backup and restore.
* File creation mode: allow all other applications to have read access
* to the created file.
* @see #MODE_PRIVATE
* @see #MODE_WORLD_WRITEABLE
*/
int MODE_WORLD_READABLE = 0x0001;

//所有人可读,这个参数同样非常危险,可能导致隐私数据泄漏
/**
* @deprecated Creating world-writable files is very dangerous, and likely
* to cause security holes in applications. It is strongly discouraged;
* instead, applications should use more formal mechanism for interactions
* such as {@link ContentProvider}, {@link BroadcastReceiver}, and
* {@link android.app.Service}. There are no guarantees that this
* access mode will remain on a file, such as when it goes through a
* backup and restore.
* File creation mode: allow all other applications to have write access
* to the created file.
* @see #MODE_PRIVATE
* @see #MODE_WORLD_READABLE
*/
int MODE_WORLD_READABLE = 0x0002

//设置该参数后,每次获取对应的SharedPreferences时都会尝试从磁盘中读取修改过的文件
/**
* SharedPreference loading flag: when set, the file on disk will
* be checked for modification even if the shared preferences
* instance is already loaded in this process. This behavior is
* sometimes desired in cases where the application has multiple
* processes, all writing to the same SharedPreferences file.
* Generally there are better forms of communication between
* processes, though.
*
* <p>This was the legacy (but undocumented) behavior in and
* before Gingerbread (Android 2.3) and this flag is implied when
* targetting such releases. For applications targetting SDK
* versions <em>greater than</em> Android 2.3, this flag must be
* explicitly set if desired.
*
* @see #getSharedPreferences
*
* @deprecated MODE_MULTI_PROCESS does not work reliably in
* some versions of Android, and furthermore does not provide any
* mechanism for reconciling concurrent modifications across
* processes. Applications should not attempt to use it. Instead,
* they should use an explicit cross-process data management
* approach such as {@link android.content.ContentProvider ContentProvider}.
*/
int MODE_MULTI_PROCESS = 0x0004;

MODE_MULTI_PROCESS

当设置MODE_MULTI_PROCESS这个参数的时候,即使当前进程内已经创建了该SharedPreferences,仍然在每次获取的时候都会尝试从本地文件中刷新。在同一个进程中,同一个文件只有一个实例。MODE_MULTI_PROCESS的作用如上getSharedPreferences实现.这个方法先判断是否已创建SharedPreferences实例,若未创建,则先创建。之后判断mode如果为MODE_MULTI_PROCESS, 则调用startReloadIfChangeUnexpectedly(),看下其实现: SharedPreferencesImpl.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
void startReloadIfChangedUnexpectedly() {
synchronized (this) {
// TODO: wait for any pending writes to disk?
if (!hasFileChangedUnexpectedly()) {
return;
}
startLoadFromDisk();
}
}

private void startLoadFromDisk() {
synchronized (this) {
mLoaded = false;
}
new Thread("SharedPreferencesImpl-load") {
public void run() {
synchronized (SharedPreferencesImpl.this) {
loadFromDiskLocked();
}
}
}.start();
}

可以看出MODE_MULTI_PROCESS的作用就是在每次获取SharedPreferences实例的时候尝试从磁盘中加载修改过的数据,并且读取是在异步线程中,因此一个线程的修改最终会反映到另一个线程,但不能立即反映到另一个进程,所以通过SharedPreferences无法实现多进程同步。 综合: 如果仅仅让多进程可访问同一个SharedPref文件,不需要设置MODE_MULTI_PROCESS, 如果需要实现多进程同步,必须设置这个参数,但也只能实现最终一致,无法即时同步。

由于SharedPreference内容都会在内存里存一份,所以不要使用SharedPreference保存较大的内容,避免不必要的内存浪费。

注意有一个锁mLoaded ,在对SharedPreference做其他操作时,都必须等待该锁释放:

1
2
3
4
5
6
7
8
@Nullable
public String getString(String key, @Nullable String defValue) {
synchronized (this) {
awaitLoadedLocked();
String v = (String)mMap.get(key);
return v != null ? v : defValue;
}
}

写操作有两个commit apply 。 commit 是同步的,写入内存的同时会等待写入文件完成,apply是异步的,先写入内存,在异步线程里再写入文件。apply肯定要快一些,优先推荐使用apply:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
/**
* Commit your preferences changes back from this Editor to the
* {@link SharedPreferences} object it is editing. This atomically
* performs the requested modifications, replacing whatever is currently
* in the SharedPreferences.
*
* <p>Note that when two editors are modifying preferences at the same
* time, the last one to call commit wins.
*
* <p>If you don't care about the return value and you're
* using this from your application's main thread, consider
* using {@link #apply} instead.
*
* @return Returns true if the new values were successfully written
* to persistent storage.
*/
boolean commit();
/**
* Commit your preferences changes back from this Editor to the
* {@link SharedPreferences} object it is editing. This atomically
* performs the requested modifications, replacing whatever is currently
* in the SharedPreferences.
*
* <p>Note that when two editors are modifying preferences at the same
* time, the last one to call apply wins.
*
* <p>Unlike {@link #commit}, which writes its preferences out
* to persistent storage synchronously, {@link #apply}
* commits its changes to the in-memory
* {@link SharedPreferences} immediately but starts an
* asynchronous commit to disk and you won't be notified of
* any failures. If another editor on this
* {@link SharedPreferences} does a regular {@link #commit}
* while a {@link #apply} is still outstanding, the
* {@link #commit} will block until all async commits are
* completed as well as the commit itself.
*
* <p>As {@link SharedPreferences} instances are singletons within
* a process, it's safe to replace any instance of {@link #commit} with
* {@link #apply} if you were already ignoring the return value.
*
* <p>You don't need to worry about Android component
* lifecycles and their interaction with <code>apply()</code>
* writing to disk. The framework makes sure in-flight disk
* writes from <code>apply()</code> complete before switching
* states.
*
* <p class='note'>The SharedPreferences.Editor interface
* isn't expected to be implemented directly. However, if you
* previously did implement it and are now getting errors
* about missing <code>apply()</code>, you can simply call
* {@link #commit} from <code>apply()</code>.
*/
void apply();

注册/解注册sharedpreference变动监听:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
/**
* Registers a callback to be invoked when a change happens to a preference.
*
* <p class="caution"><strong>Caution:</strong> The preference manager does
* not currently store a strong reference to the listener. You must store a
* strong reference to the listener, or it will be susceptible to garbage
* collection. We recommend you keep a reference to the listener in the
* instance data of an object that will exist as long as you need the
* listener.</p>
*
* @param listener The callback that will run.
* @see #unregisterOnSharedPreferenceChangeListener
*/
void registerOnSharedPreferenceChangeListener(OnSharedPreferenceChangeListener listener);

/**
* Unregisters a previous callback.
*
* @param listener The callback that should be unregistered.
* @see #registerOnSharedPreferenceChangeListener
*/
void unregisterOnSharedPreferenceChangeListener(OnSharedPreferenceChangeListener listener);

为什么不推荐使用MODE_MULTI_PROCESS?

android文档已经Deprected了这个flag,并且说明不应该通过SharedPreference做进程间数据共享?这是为啥呢?从前面但分析可看到当设置这个flag后,每次获取(获取而不是初次创建)SharedPreferences实例的时候,会判断shared_pref文件是否修改过:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
private boolean hasFileChangedUnexpectedly() {
synchronized (this) {
if (mDiskWritesInFlight > 0) {
// If we know we caused it, it's not unexpected.
if (DEBUG) Log.d(TAG, "disk write in flight, not unexpected.");
return false;
}
}

final StructStat stat;
try {
/*
* Metadata operations don't usually count as a block guard
* violation, but we explicitly want this one.
*/
BlockGuard.getThreadPolicy().onReadFromDisk();
stat = Os.stat(mFile.getPath());
} catch (ErrnoException e) {
return true;
}

synchronized (this) {
return mStatTimestamp != stat.st_mtime || mStatSize != stat.st_size;
}
}

这里先判断mDiskWritesInFlight>0,如果成立,说明是当前进程修改了文件,不需要重新读取。然后通过文件最后修改时间,判断文件是否修改过。如果修改了,则重新读取:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
private void startLoadFromDisk() {
synchronized (this) {
mLoaded = false;
}
new Thread("SharedPreferencesImpl-load") {
public void run() {
synchronized (SharedPreferencesImpl.this) {
loadFromDiskLocked();
}
}
}.start();
}

private void loadFromDiskLocked() {
if (mLoaded) {
return;
}
if (mBackupFile.exists()) {
mFile.delete();
mBackupFile.renameTo(mFile);
}
Map map = null;
StructStat stat = null;
try {
stat = Os.stat(mFile.getPath());
if (mFile.canRead()) {
BufferedInputStream str = null;
try {
str = new BufferedInputStream(
new FileInputStream(mFile), 16*1024);
map = XmlUtils.readMapXml(str);
} finally {
IoUtils.closeQuietly(str);
}
}
} catch (ErrnoException e) {
}
mLoaded = true;
if (map != null) {
mMap = map;
mStatTimestamp = stat.st_mtime;
mStatSize = stat.st_size;
} else {
mMap = new HashMap<String, Object>();
}
notifyAll();
}

这里起码有3个坑!

  1. 使用MODE_MULTI_PROCESS时,不要保存SharedPreference变量,必须每次都从context.getSharedPreferences 获取。如果你图方便使用变量存了下来,那么无法触发reload,有可能两个进程数据不同步。
  2. 前面提到过,load数据是耗时的,并且其他操作会等待该锁。这意味着很多时候获取SharedPreference数据都不得不从文件再读一遍,大大降低了内存缓存的作用。文件读写耗时也影响了性能。
  3. 修改数据时得用commit,保证修改时写入了文件,这样其他进程才能通过文件大小或修改时间感知到。

重点是这段:

1
2
3
4
if (mBackupFile.exists()) {
mFile.delete();
mBackupFile.renameTo(mFile);
}

重新读取时,如果发现存在mBackupFile,则将原文件mFile删除,并将mBackupFile重命名为mFile。mBackupFile又是如何创建的呢?答案是在修改SharedPreferences时将内存中的数据写会磁盘时创建的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
private void writeToFile(MemoryCommitResult mcr) {
// Rename the current file so it may be used as a backup during the next read
if (mFile.exists()) {
if (!mBackupFile.exists()) {
if (!mFile.renameTo(mBackupFile)) {
mcr.setDiskWriteResult(false);
return;
}
} else {
mFile.delete();
}
}
FileOutputStream str = createFileOutputStream(mFile);
XmlUtils.writeMapXml(mcr.mapToWriteToDisk, str);
FileUtils.sync(str);
str.close();
ContextImpl.setFilePermissionsFromMode(mFile.getPath(), mMode, 0);
final StructStat stat = Os.stat(mFile.getPath());
synchronized (this) {
mStatTimestamp = stat.st_mtime;
mStatSize = stat.st_size;
}
// Writing was successful, delete the backup file if there is one.
mBackupFile.delete();
mcr.setDiskWriteResult(true);
return;
}

这段代码只保留了核心流程,忽略了错误处理流程。可以看到,写文件的步骤大致是:

  1. 将原文件重命名为mBackupFile
  2. 重新创建原文件mFile, 并将内容写入其中
  3. 删除mBackupFile

所以,只有当一个进程正处于写文件的过程中的时候,如果另一个进程读文件,才会看到mBackupFile, 这时候读进程会将mBackupFile重命名为mFile, 这样读结果是,读进程只能读到修改前的文件,同时,由于mBackupFile重命名为了mFile, 所以写进程写那个文件就没有文件名引用了,因此其写入的内容无法再被任何进程访问到。所以其内容丢失了,可认为写入失败了,而SharedPreferences对这种失败情况没有任何重试机制,所以就可能出现数据丢失的情况。 回到这段的重点:为什么不推荐用MODE_MULTI_PROCESS?从前面分析可知,这种模式下,每次获取SharedPreferences都会检测文件是否改变,只要读的时候另一进程在写,就会导致写丢失。这样失败概率就会大幅度提高。反之,若不设置这个模式,则只在第一次创建SharedPreferences的时候读取,导致写失败的概率就会大幅度降低,当然,仍然存在失败的可能。

为什么不做写失败重试?

为什么android不做写失败重试呢?原因是写进程并不能发现写失败的情况。难道写的过程中,目标文件被删不会抛异常吗?答案是不会。删除文件只是从文件系统中删除了一个节点信息而已,重命名也是新建了一个具有相同名称的节点信息,并把文件地址指向另一个磁盘地址而已,原来,之前的写过程仍然会成功写到原来的磁盘地址。所以目前的实现方案并不能检测到失败。

有没有办法解决写失败呢?

个人觉得是可以做到的,读里面读那段关键操作:

1
2
3
4
if (mBackupFile.exists()) {
mFile.delete();
mBackupFile.renameTo(mFile);
}

mBackupFile存在,意味着当前正处于写读过程中,这时候是不是可以考虑直接读mBackupFile文件,而不删除mFile呢?这样读话,读取效果一样,都是读的mBackupFile,同时写进程写的mFile也不会被mBacupFile覆盖,写也就能成功了。即使通过这段代码重命名,写进程写完后发现mBackupFile不存在了,其实也能认为发生了读重命名,大可以重试一次。

多进程使用SharedPreference方案

说简单也简单,就是依据google的建议使用ContentProvider了。我看过网上很多的例子,但总是觉得少了点什么

有的方案里将所有读取操作都写作静态方法,没有继承SharedPreference 。 这样做需要强制改变调用者的使用习惯,不怎么好。 大部分方案做成ContentProvider后,所有的调用都走的ContentProvider。但如果调用进程与SharedPreference 本身就是同一个进程,只用走原生的流程就行了,不用拐个弯去访问ContentProvider,减少不必要的性能损耗。

我这里也写了一个跨进程方案,简单介绍如下 SharedPreferenceProxy 继承SharedPreferences。其所有操作都是通过ContentProvider完成。简要代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
public class SharedPreferenceProxy implements SharedPreferences {
@Nullable
@Override
public String getString(String key, @Nullable String defValue) {
OpEntry result = getResult(OpEntry.obtainGetOperation(key).setStringValue(defValue));
return result == null ? defValue : result.getStringValue(defValue);
}

@Override
public Editor edit() {
return new EditorImpl();
}
private OpEntry getResult(@NonNull OpEntry input) {
try {
Bundle res = ctx.getContentResolver().call(PreferenceUtil.URI
, PreferenceUtil.METHOD_QUERY_VALUE
, preferName
, input.getBundle());
return new OpEntry(res);
} catch (Exception e) {
e.printStackTrace();
return null;
}
...

public class EditorImpl implements Editor {
private ArrayList<OpEntry> mModified = new ArrayList<>();
@Override
public Editor putString(String key, @Nullable String value) {
OpEntry entry = OpEntry.obtainPutOperation(key).setStringValue(value);
return addOps(entry);
}
@Override
public void apply() {
Bundle intput = new Bundle();
intput.putParcelableArrayList(PreferenceUtil.KEY_VALUES, convertBundleList());
intput.putInt(OpEntry.KEY_OP_TYPE, OpEntry.OP_TYPE_APPLY);
try {
ctx.getContentResolver().call(PreferenceUtil.URI, PreferenceUtil.METHOD_EIDIT_VALUE, preferName, intput);
} catch (Exception e) {
e.printStackTrace();
}
...
}
...
}

OpEntry只是一个对Bundle操作封装的类。 所有跨进程的操作都是通过SharedPreferenceProvider的call方法完成。SharedPreferenceProvider里会访问真正的SharedPreference

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class SharedPreferenceProvider extends ContentProvider{

private Map<String, MethodProcess> processerMap = new ArrayMap<>();
@Override
public boolean onCreate() {
processerMap.put(PreferenceUtil.METHOD_QUERY_VALUE, methodQueryValues);
processerMap.put(PreferenceUtil.METHOD_CONTAIN_KEY, methodContainKey);
processerMap.put(PreferenceUtil.METHOD_EIDIT_VALUE, methodEditor);
processerMap.put(PreferenceUtil.METHOD_QUERY_PID, methodQueryPid);
return true;
}
@Nullable
@Override
public Bundle call(@NonNull String method, @Nullable String arg, @Nullable Bundle extras) {
MethodProcess processer = processerMap.get(method);
return processer == null?null:processer.process(arg, extras);
}
...
}

重要差别的地方在这里:在调用getSharedPreferences时,会先判断caller的进程pid是否与SharedPreferenceProvider相同。如果不同,则返回SharedPreferenceProxy。如果相同,则返回ctx.getSharedPreferences。只会在第一次调用时进行判断,结果会保存起来。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
public static SharedPreferences getSharedPreferences(@NonNull Context ctx, String preferName) {
//First check if the same process
if (processFlag.get() == 0) {
Bundle bundle = ctx.getContentResolver().call(PreferenceUtil.URI, PreferenceUtil.METHOD_QUERY_PID, "", null);
int pid = 0;
if (bundle != null) {
pid = bundle.getInt(PreferenceUtil.KEY_VALUES);
}
//Can not get the pid, something wrong!
if (pid == 0) {
return getFromLocalProcess(ctx, preferName);
}
processFlag.set(Process.myPid() == pid ? 1 : -1);
return getSharedPreferences(ctx, preferName);
} else if (processFlag.get() > 0) {
return getFromLocalProcess(ctx, preferName);
} else {
return getFromRemoteProcess(ctx, preferName);
}
}


private static SharedPreferences getFromRemoteProcess(@NonNull Context ctx, String preferName) {
synchronized (SharedPreferenceProxy.class) {
if (sharedPreferenceProxyMap == null) {
sharedPreferenceProxyMap = new ArrayMap<>();
}
SharedPreferenceProxy preferenceProxy = sharedPreferenceProxyMap.get(preferName);
if (preferenceProxy == null) {
preferenceProxy = new SharedPreferenceProxy(ctx.getApplicationContext(), preferName);
sharedPreferenceProxyMap.put(preferName, preferenceProxy);
}
return preferenceProxy;
}
}

private static SharedPreferences getFromLocalProcess(@NonNull Context ctx, String preferName) {
return ctx.getSharedPreferences(preferName, Context.MODE_PRIVATE);
}

这样,只有当调用者是正真跨进程时才走的contentProvider。对于同进程的情况,就没有必要走contentProvider了。对调用者来说,这都是透明的,只需要获取SharedPreferences就行了,不用关心获得的是SharedPreferenceProxy,还是SharedPreferenceImpl。即使你当前没有涉及到多进程使用,将所有获取SharedPreference的地方封装并替换后,对当前逻辑也没有任何影响。

Flutter介绍

发表于 2019-03-07 | 分类于 flutter

Flutter是一款由Google开发的开源、跨平台的移动端开发框架,使用Flutter开发出的应用符合不同平台的原生体验,可以让应用看起来跟系统更加协调。 Flutter是一个全新的移动UI框架,它允许使用同一个代码库构建高性能的Android和iOS应用,同时它也是Google即将推出的Fuchsia操作系统的开发平台。通过自定义的Flutter引擎可以将其嵌入到其他平台,旨在帮助开发者使用一套代码开发高性能、高保真的Android和iOS应用。

Flutter优点

原生性能

Flutter会以原生的性能提供给开发者,它的开发性能非常接近传统的Native,包括渲染方式、AOT的编译方式和其他优化。

Flutter开发的页面跟Native没有差距。在安卓中低端机型里,基于Flutter开发出来的APP在帧率上会有更流畅的体现,内存占用也会有更低的消耗。

渲染方式,AOT,无锁GC

快速开发

Flutter因其本身的跨端性,大幅提升了传统的安卓开发速度。一般认为,前端开发的速度较快,基于Flutter,开发速度比前端更快。 压秒级,有状态的热重载

统一的应用开发体验

在跨端层面上,由于Flutter把两端的渲染机制下沉到更低的渲染层,基于统一的C++层的渲染引擎来搭建底层的UI框架,因此,Flutter会让跨端体验得到更一致的效果。 两端一致的开发方式,MD和IOS风格

问题

  • 内存的问题。随着Flutter页面的堆栈变得越来越深,内存的释放并没有得到及时的释放。
  • 字体的问题。不同的字体在不同的机器里渲染的效果非常不一致。
  • 截图会出现黑屏的问题。
  • 图片缓存的问题。跟安卓端的图片缓存是完全不同的体系。
  • 它的暗黑区、适配问题,
  • 私有库、中间件的适配,
  • 不支持反射和序列化,
  • 集成问题,怎么把Native的组件集成到Flutter体系
1…345…19
轻口味

轻口味

190 日志
27 分类
63 标签
RSS
GitHub 微博 豆瓣 知乎
友情链接
  • SRS
© 2015 - 2019 轻口味
京ICP备17018543号
本站访客数 人次 本站总访问量 次