Android 网络性能优化-概述和DNS优化

1. 移动App网络优化背景

对于Android来说,开发者可以轻松的打造一套 MVP + Retrofit + RxJava 的框架来处理所有的网络请求。因为 Retrofit下层封装的OkHttp是非常强大的网络库,而 RxJava又能够很好的帮我们处理线程切换的问题。

但是对于大型的App来说,仅仅是使用这些是不够的,它太机械,不能帮我们处理复杂多变的网络情况。

在我的上个公司,智能设备的网络连接是老大难问题,有时候设备连不上Wifi的情况下,开发人员去跟进,到最后总会丢出一句:“设备就是Ping不通网络啊” 以此把锅甩在了网络上。而这些客户,家里同时存在多种同类型的设备,竞品设备的连接却毫无问题…

所以无论是哪一端,当用户量变多时,网络的性能优化都是不可忽视的点,虽然网络确实有其独特的不可控性和复杂性,但是每一端都可以尽自己所能去优化到一个较好的情况。

这个“较好的情况”有下面几个标准:

  • 避免重复的上传、下载
  • 富媒体、JS/CSS/HTML 都有压缩
  • 对于选择压缩算法,优化到最适合业务的情况
  • 请求合并
  • 网络请求失败的重试必须有明显的结束条件
  • 有连接建立兜底的能力

2. 关于网络优化的主要问题

网络性能优化,是性能优化专题中不可忽视却又不可控的一点。

之所以不可控,是因为 网络 和CPU、磁盘、内存这些本地资源相比,网络是外部资源,它有自己独特的性能瓶颈,程序员突破不了这些瓶颈,令人困扰。

为了尽可能优化网络性能,总结出下面几大问题,可以从这些问题入手,来进行优化:

  • 业务成功率
  • 业务网络延时
  • 业务宽带成本
  • 业务安全性


2.1 业务成功率

下面有两个场景是用户真实可以遇到的:

  1. 当发消息的时候走进了电梯
  2. 在演唱会时分享朋友圈


上面两个场景就体验来说,是最有可能出现发送失败的地方。
而正好,这两个场景分别代表了两种典型的网络差的场景,进电梯代表了网络信号差,而演唱会则代表了拥塞网络,程序如果处理不当都会直接影响业务的成功率。

2.1.1 弱信号 / 弱网

可以简单的看成手机信号只有一格的时候,这时不仅仅是信令(无线网络其实通信的时候都是一个个信令)发出去困难,而且还有可能导致不断切换网络、切换基站。App能做的,就是在应用层做重试,因为很大概率这个弱信号只是一时的。

2.1.2 拥塞网络

这个我们熟悉,当我们把网络的消息传输看成是一个 生产消费者模型,就是一端在不断发消息,一端在处理消息,
拥塞网络的本质就是: 生产者生产消息的速度大于消费者处理消息速度,这时候网络状态就过载了。
如果我们App不断重试,只会导致拥塞更为严重。这个时候能做的就是让自己的非核心业务不要捣乱,不要排队,让核心业务的数据量更少,协议来回更少。

2.2 业务网络延时

成功率直接代表了业务的成功和失败。
而网络延时相比起来没那么直接,但是 慢 会带来不爽,按我们公司的话来说,就是“等待时焦虑”,也是会流失用户的。

这个慢,就必须从一个数据包的发送历程开始说起,可以参考下图:

在这里插入图片描述

2.2.1 DNS解释 

域名换ip。这一步看似简单却充满陷阱,10分钟的DNS的Cache过期时间,200~2000ms不等的DNS耗时,坑了无数应用。
解决无非有三个策略:

  • ip直连
  • 域名重用
  • HttpDNS(利用云服务器,通过自定义的协议获取域名对应的IP地址,甚至是列表)

2.2.2 建立连接

大多数应用都是基于TCP,所以就是三次握手建立TCP连接。这一步的耗时,如果是长连接的话就是一次消耗,但是短连接的话就是多次消耗(因为Http的无状态性)。要维护的化就需要心跳包,心跳包多,会耗电,特别是当心跳间隔等于移动网络状态机Active-Idle切换间隔时,简直就是悲剧。

同时对于移动网络来说还会增加信令通道的负担,着也是当年那个轰动一时的微信信令风暴的部分原因。(信令风暴:就是发送心跳包过多或者过快,超过了服务器的处理速度,最终会导致大量占用网络资源、服务器不可用)。
而心跳包少了,会让连接在NAT中超时,导致长连接断开。在建立连接的过程中,TCP会进行一些商定,其中影响网络时延最明显的就是窗口。

2.2.3 接受窗口

用于拥塞控制。
客户端同一时间发送多少TCP数据包,当前的宽带有没有被充分利用,直接影响发送的速度。而让窗口太少的原因无非有几个:

  • 服务器的Receive Buffer较小
  • 因为慢启动,而包又太小,刚刚连接,慢启动会逐步放大窗口,没有等放大完,数据就发完了
  • Window size scaling factor失效,这里最有可能的原因是网络代理,失效的结果就窗口最大只有65536字节。

窗口本身就是Tcp拥塞控制的一部分,但有时App为了能自己控制,也是想尽了办法。利用应用层分片大小可以做更严格的拥塞策略;多连接和长连接一定程度上可以绕过拥塞策略中的慢启动。

在云时代,已经有各种各样的提升业务成功率和网络延时的优秀组件,比如 QQ的MSF,QZONE的WNS~

2.3 业务宽带成本

如果将流量列入到影响网络的原因中,那么就必须要思考应用的业务宽带成本了。

对于视频、图片这些富媒体业务,每天在宽带成本上的投入,跟烧钱没有什么区别。
如何在降低这些成本的同时,不会影响用户体验呢?策略有: 压缩、增量、去重复三种。

2.3.1 压缩

  • 图片用WebP压缩、PNG压缩,还可以使用像 progressive jpeg的不同程度压缩来替代大中小图
  • 视频用H264、H265压缩
  • 文本是用gzip和其他ZIP压缩
  • 传输数据使用 json、 protobuf等。

除此之外还有一些细节,例如

  • 图片的尺寸在不同分辨率的上要下载不同的尺寸,设计时要注意
  • WebP图片的编码和解码对于手机是有压力的,CPU消耗是JPEG的3倍以上,编码耗时也比JPEG长不少。所以不要在性能压力大的时候使用,建议解码后在本地保存成JPEG,以降低下次解码的压力。
  • 对于文本,也有通过改变编码方式来降低流量,UTF-8、UTF-16、GBK都有不同的编码方式,虽然现在Unicode流行于编码界,但是了解这些编码的发展历史、编码算法,可以利于我们对优化流量有一定的帮助。

2.3.2 增量

即增量更新。常见于列表上拉加载、下拉更新、左滑删除。

所以这明显是要看场景使用的,当你的RecyclerView怼满了2000个item时,一次性加载那就是灾难,而最多只有20个item时还做增量,就会导致我们的协议复杂度增加,得不偿失。

2.3.3 去重复

该问题很简单,而且很常见,比如地图SDK重复下载地图块、横竖屏幕切换WebView的内容,重复下载,这些都比较常见。
而像 压缩包里面的图片和没有压缩的内容重复、CSS里面的内嵌图片与压缩包里的图片重复这些问题则较为隐蔽,一般也没有这个精力去查问题。

2.4 业务安全性

怎样防止被第三方窃听/篡改或冒充,防止运营商劫持,同时又不影响性能?

现在已经普遍的使用 TLS协议来保证网络传输的安全性,HTTP + TLS = HTTPS
在 Android4.4开始,就已经支持非常完善的 TLS1.2版本,所以业务安全性问题在现在已经基本得到保障。

使用TLS安全协议,已经解决了两个问题:1. 保证安全 2.降低加密成本

在保证安全上:

  • 使用加密算法组合对传输数据加密,避免被窃听和篡改。
  • 认证对方身份,避免被第三方冒充。
  • 加密算法保持灵活可更新,防止定死算法被破解后无法更换,禁用已被破解的算法。

在降低加密成本上:

  • 用对称加密算法加密传输数据,解决非对称加密算法的性能低以及长度限制问题。
  • 缓存安全协议握手后的密钥等数据,加快第二次建连的速度。
  • (TLS1.3版本)加快握手过程:2RTT-> 0RTT。加快握手的思路,就是原本客户端和服务端需要协商使用什么算法后才能加密发送数据,变成通过内置的公钥和默认的算法,在握手的同时就把数据发出去,也就是不需要等待握手就开始发送数据,达到0RTT。

具体点可以查看该篇文章:TLS协议分析 与 现代加密通信协议设计

目前移动端在该问题上可以做的事情不是很多。 唯一可以做的就是升级 TLS版本,因为现在的主流版本还是 TLS1.2,它的优点是保证传输安全,缺点是建立连接速度。 而 TLS1.3版本进行了优化,提升连接速率。像微信就自行实现升级到了 TLS1.3.

TLS 1.2 和 1.3 的对比可以查看这篇文章: TLS1.3 VS TLS1.2,让你明白TLS1.3的强大

3. 工具集

说了这些,可见网络性能有许多坑,需要我们去优化,下面来介绍下工具,有助于平时测试。

来看下表:

工具 问题 能力
Wireshark 最专业的的网络分析工具,全部网络性能问题分析定位都可以查看它 发现 + 定位
Har+Pagespeed 把pacp转成har,上传到 http://stevesouders.com/flint/,然后会根据雅虎军规,发现很多性能问题 发现 + 定位
fiddler 主要针对Http,帮助发现Http总多性能问题, 还能模拟错误和延时的Http 发现 + 定位
tcpdump 抓包工具,要ROOT权限 发现 + 定位
traceroute 定位网络路由问题,包括就近接入、跨运营商问题 发现 + 定位
ARO 抓包工具,要ROOT权限 自动发现 + 定位
WebP/BPG 图片压缩方案,前者基于webm的帧内压缩 解决
SPDY/HTTP2.0/QUIC 网络协议,利用FastTcpOpen减少握手次数,利用UDP更好地适应网络抖动 解决
WebPageTest.org 如果要做Web应用的数据上报,建议参考之。它提供LoadTime、StartRender、SppedIndex、DOM Elements等耗时 发现 + 定位为
tPackageCaptrue 无ROOT抓包 发现
ATC 最专业的弱网络模拟工具,除能模拟窄宽、延时、丢包、损坏包外,最关键的还有包乱序的情况 发现 + 定位


这里就不对这些工具集的使用做过多的讲解,使用的时候再去学习。


网络性能优化——DNS优化

1.概述

在上面的文章中,我整理了部分网络会遭遇瓶颈的地方,并且提出了可以优化的做法或者思路。

而网络交互最初是的动作就是连接,所以我认为优化的方向先要从网络连接这个点切入。

而连接的最初的动作是DNS解释,在第一篇中,描述DNS的状况是:
域名换ip。这一步看似简单却充满陷阱,10分钟的DNS的Cache过期时间,200~2000ms不等的DNS耗时,坑了无数应用。
解决无非有三个策略:

  • ip直连
  • 域名重用(收敛)
  • HttpDNS

不过仅仅用一段话来描述DNS的疼点还是比较生硬的,所以在学习ip直连和HttpDNS之前,我需要先弄懂DNS出现问题的具体原因。

2. DNS

2.1 DNS是什么

在网络世界中,我们如果想要访问某一个服务器的东西,我们需要知道它的ip地址是多少,这样才能去连接走Http。
但是 ip地址32位二进制的数字,它的常用格式: XXX.XXX.XXX.XXX 显然,我就是为了访问一个服务器,你让我记住这么一长串的数字,这不是在为难人吗?我每天都要访问数十个网站,一秒钟几亿上下,每个网站的ip都要记在小本子上,而且一个服务器可能还有好几个ip,然后在访问时还要很细心一个个敲在输入框中…

在这里插入图片描述


所以为了解决这种体验问题,DNS(Domain Name System)就这么出现了。服务器可以申请得到域名,比如说我们熟知的百度的域名就是 : www.baidu.com。这样我们在浏览器的输入框只要输入这个域名,就能访问百度的服务器了。

 那我们看到DNS出现,不用去记那么一长串的ip地址了,只用记住域名就可以了,域名一般来说都是英文加数字格式,很方便人类进行记忆。

那我们已经很清楚DNS的作用了,我们告诉DNS域名,DNS帮我们找到ip地址,即 域名换ip,来看下下图:

在这里插入图片描述

 这样人类就可以很哈批的去访问各种网站了。

注:DNS大部分都是基于UDP的,所以DNS本身具有UDP连接的不可靠性

2.2 DNS服务器结构

每天都有成万上亿的人在互联网冲浪,这说明DNS没有一刻不在被使用,所以DNS服务器一定要保证高可用、高并发和分布式服务器。所以有必要了解DNS的结构。

我们先来看下域名的空间结构:

在这里插入图片描述

 像 com、net、cn这些代表的就是顶级域名(也叫根域名),顶级域名前面的就是二级域名,二级域名前面的是三级域名。
比如 www.baidu.com , .com就是根域名, .baidu就是二级域名, www就是三级域名。根据这个我们再来看看DNS的结构。

从理论上说,任何形式的标准域名要想被实现解析,按照技术流程,都必须经过全球“层级式”域名解析体系的工作,才能完成。所以DNS的服务器结构是根据官方的层级式来分类的,有下面四类:

  • 根域名服务器(Root Name Server)
  • 顶级域名服务器(Top-level Name Server)
  • 权威域名服务器(Authoritative Name Server)
  • 本地 DNS 服务器(Local DNS Server)

2.2.1 根域名服务器

根域名服务器是互联网域名解析系统(DNS)中最高级别的域名服务器,负责返回顶级域的权威域名服务器地址。
“层级式”域名解析体系第一层就是根服务器,负责管理世界各国的域名信息,在根服务器下面是顶级域名服务器,即相关国家域名管理机构的数据库。

一个域名必须首先经过根数据库的解析后,才能转到顶级域名服务器进行解析。

在根域名服务器中虽然没有每个域名的具体信息,但储存了负责每个域(如.com,.xyz,.cn,.ren,.top等)的解析的域名服务器的地址信息。

全世界域名的最高管理机构,是ICANN,在美国加州,它制定了根域名的规范,管理所有域名的顶级域名。它就是根域名服务器。

当然了,域名这么多,来自各个国家各个区域,所以为了便于管理,所有域都会被一些组织、群体所托管,比如 .cn域名的托管商就是中国互联网络信息中心(CNNIC),它决定了.cn域名的各种政策。而像Verisign管理着 .com、.net等,而这些服务器就是顶级域名服务器了,如下图所示:

在这里插入图片描述
也就是说,当我们输入 www.example.cn时,会先去找根域名服务器ICANN,然后它会翻列表,发现 .cn的解析是被CNNIC管的,那么你就要去找CNNIC做顶级域名解析。 

一般来说,根域名列表很少变化,所以ISP会帮我们缓存这些列表,这样就可以省去找根域名服务器的时间了。

2.2.2 顶级域名服务器

所有顶级域名服务器的名称和IP地址是在根服务器注册的。它们的作用是 解析顶级域名
比如上一节中:

  • 用来解析 .cn 域名的服务器-------CNNIC
  • 用来解析 .com、.net域名的服务器------Verisign

而顶级域名服务器里面解析好域名后,会得到该域名所在权威域名服务器地址:

在这里插入图片描述
2.2.3 权威域名服务器

权威域名服务器, 本质上是一个 Map表: K -> 域名 V -> ip列表
这样,权威域名服务器就会在这个表里面根据域名找到最终的ip,返回请求者这个ip地址。

2.2.4 本地DNS服务器

显然,本地服务器并不具备寻址能力,因为不可能所有的机器都做为DNS服务器来存储地址,所以本地DNS服务器的作用是 缓存DNS域名与ip地址的映射
一般来说,Local DNS服务器在运营商那里,而DNS劫持一般的目标都会在本地DNS服务器上。

2.2.5 为什么要这样分级?

看到这里,应该已经有很多同学把DNS服务器结构和 HTTPS的证书信任链联想在一起了…(可以看这篇:Android 深入Http(3)Https的工作机制

那大家就知道为什么一个简简单单的 域名换ip的事情,要分成这么多级了。

因为根服务器不可能管理者成千上万个域名-ip映射表。所以它会把这个根据顶级域名交给下层,如果你是 .net你就交给谁管,你是 .cn你就交给谁管。
而 一个顶级域名服务器也不可能管理着所有的 域名-ip映射表,因为并发量太高了,所以它会在解析好后,交由下层处理。当到了由大量权威服务器支撑的映射表后,就能保证高并发量、高可用了。

2.3 DNS寻址过程

这里总结一下DNS的寻址过程,可以看下图:

在这里插入图片描述

  1. 客户端输入一个网址即域名,到本地DNS服务器
  2. 本地服务器DNS查找域名缓存,如果命中对应的ip地址,则返回该ip,如果没有命中,则发请求给根域名服务器请求解析
  3. 根域名解析好后,返回 顶级域名服务器的地址
  4. 本地DNS 向顶级域名服务器发送请求。
  5. 顶级域名查看缓存,有没有对应ip,如果有直接返回,如果没有,则解析该域名,查找到目标 权威域名服务器的地址
  6. 本地DNS 向 权威域名服务器发送请求
  7. 权威域名服务器在数据库中查表,返回对应的ip地址
  8. 本地DNS服务器 把 ip地址 返回给 客户端,并且缓存该地址。

2.4 DNS坑点

2.4.1 DNS劫持

在这里插入图片描述

 DNS劫持又叫域名劫持,是指攻击者通过某种手段篡改了某个域名的解析结果,使得指向该域名的IP变成了另一个IP。

这里不过多讲解,因为内容很多,而且和本章没啥关联。

PS:你在看这篇文章时,左下角、右下角如果出现了广告,这很有可能能使DNS被劫持了,当然了,这也可能就是官方自己的广告= =。

2.4.2 DNS 服务器故障

每年都会经常出现 一些DNS服务器的故障,如果你恰好遇上,体验就很不好。这是因为DNS寻址是我们访问服务器的必要过程,DNS服务器故障直接导致寻不到址,或者寻址速度降低。

2.4.3 DNS 调度不准确

说不好一些DNS服务器里面的算法写的有点水,调度性能低下,这样会也会直接导致我们寻址速度变慢。

2.5 小结

总的来说,DNS寻址有下面几个缺点:

  • 不稳定 

        遭遇DNS劫持或DNS服务器宕机

  • 不准确

        某些小运营商没有 DNS 服务器,直接调用其他运营商的 DNS 服务器,最终直接跨网传输

  • 不及时

        运营商可能会修改 DNS 的 TTL(Time-To-Live,DNS 缓存时间),导致 DNS 的修改,延迟生效。

        除了运营商,DNS服务器本身的索引算法也可能会有调度的问题(不过概率蛮小的)

所以看到DNS其实也有蛮多缺点的,那么我们该怎么去优化呢?

首先我们是开发App的,我们面向的问题是App在进行网络请求时,更快更稳定。那么最暴力的方法就是绕过DNS,我们使用DNS的原因是我们记不住冗长的ip地址,但在App开发中,App程序本身是可以记得住这些ip地址的,那就让它去记就行了,让它直接访问ip地址,就减少了DNS的寻址过程。

而 ip直连 就是以此为基础的实现方案。

3. ip直连

3.1 原理

客户端从服务器拉取配置文件,配置文件包含了域名和ip地址的映射,客户端在之后的网络请求中,在配置中根据域名找到ip,直接连接这个ip地址进行请求。

而最开始拉取配置文件这个动作是经过DNS的,而之后都是绕过DNS的。

ip直连有以下的优势:

  • 在之后的连接中,都会绕过DNS,这样可以提高连接速率
  • 可以降低DNS劫持的风险
  • 一般都能拿到多个ip地址,所以可以进跑马比较,就快连接,这样可以提高连接质量

3.2 一个Http的直连demo

举一个最简单的直连的例子。假如我们需要直连百度,那我们先在cmd中,直接获取到百度的ip:

>ping www.baidu.com

正在 Ping www.a.shifen.com [14.215.177.39] 具有 32 字节的数据:
来自 14.215.177.39 的回复: 字节=32 时间=6ms TTL=50
来自 14.215.177.39 的回复: 字节=32 时间=6ms TTL=50
来自 14.215.177.39 的回复: 字节=32 时间=6ms TTL=50
来自 14.215.177.39 的回复: 字节=32 时间=6ms TTL=50

14.215.177.39 的 Ping 统计信息:
    数据包: 已发送 = 4,已接收 = 4,丢失 = 0 (0% 丢失),
往返行程的估计时间(以毫秒为单位):
    最短 = 6ms,最长 = 6ms,平均 = 6ms

上面就已经拿到了百度的ip。接着在我们App中运用:

class MainActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        // 直接加载这个页面
        webview.loadUrl("http://14.215.177.39/")
    }
}

这就是最简单的ip直连,绕过了DNS的域名寻址。 但是实际上,我们也不会这样简单的用,有三点:

  1. ip地址是会变的。比如人家服务器重启一下,就变了
  2. 一个服务器下面是允许有多个子服务器的,所以这些子服务器对外的ip是一样的。在访问时,如果不带上Host,可能会找不到想要的服务器。
  3. 现在基本都是使用 Https来访问了,而Https需要验证验证书,所以步骤会更加复杂。

3.3 Https的ip直连

Https应用到ip直连上,需要面临一个问题:证书的校验。

在Https建立连接中,有这么一步,客户端需要拿着操作系统的根证书列表去验证的域名的,而一般来说,这个动作都是由运营商DNS来做的。而ip直连没有了DNS,代表着我们需要手动去验证。

举个例子:当你打开Chrome,使用ip直连的方式访问某个网站,并且使用Https的形式,即: https://xxx.xxx.xx/,你会看到下面这个页面:

在这里插入图片描述

你需要在界面输入 "thisisunsafe"来绕过证书验证访问该界面。

3.3.1 使用HttpsURLConnection实现ip直连

为了解决上述问题,我们需要手动验证,所幸的是Android SDK提供api供我们去验证,看了下别的语言的情况,好像Dart就没有。

       Thread {
            val url = "https://140.205.160.59/"
            val hostName = "baidu.com"
            val connection = URL(url).openConnection() as HttpsURLConnection
            // 1
            connection.hostnameVerifier = HostnameVerifier { host, session ->
                HttpsURLConnection.getDefaultHostnameVerifier().verify(hostname, session)
            }
            
            connection.connect()
        }


来看看上面代码。

注释1:
SDK提供了 HttpsURLConnection.getDefaultHostnameVerifier().verify() 的接口来验证,我们将我们要访问的域名代入,走低层校验逻辑。这部分我没阅读过源代码,我的猜测就是使用操作系统的根证书来验证。

3.3.2 解决多域名ip直连问题(SNI技术)

在3.2节中,ip直连还面临了一个问题,即一个服务器下面是允许有多个子服务器的,所以这些子服务器对外的ip是一样的。在访问时,如果不带上Host,可能会找不到想要的服务器。

但是Https中存在一个问题:即TLS握手的建立是在Http请求之前的,这说明就算你在请求行中带上Host也没用,因为顺序先后的问题,服务器根本不知道你要访问的是哪个域名。

而SNI(Server Name Indication)技术则解决了这个问题:

SNI通过让客户端发送虚拟域名的名称作为TLS协商的一部分来解决此问题。这使服务器能够提前选择正确的虚拟域名,并向浏览器提供包含正确名称的证书。

而这个动作一般由运营商DNS来做的,所以绕过DNS,就需要自己手动连接。而SDK也提供了这些Api给我们。

下面是高于Api24的做法:

     // 拿到SSLSocket
     val ssl = SSLSocketFactory.getDefault().createSocket() as SSLSocket
     // 使用 SSLParameters来设置SNI技术
     val sslParameters = ssl.sslParameters
     // 设置SNI,该Api要高于24
     if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
          sslParameters.serverNames = listOf(SNIServerName(...))
     }

低于24的代码,则需要继承 SSLSocketFactory,重写 createScoekt来返回一个实现SNI的SSLSocket,大致代码如下:

/**
     * 创建一个SSLSocket,就是建立握手前协商通道,这个时候利用SNI,为服务器传入一个虚拟域名
     * 这样服务器根据可以根据虚拟域名,返回该域名的证书
     */
    override fun createSocket(s: Socket?, host: String?, port: Int, autoClose: Boolean): Socket {
        // 拿到设置的Host
        var peerHost = conn.getRequestProperty("Host")
        // 获取Socket连接的ip地址
        val address = s?.inetAddress
        if(peerHost == null){
            // 如果没有,则拿默认的
            peerHost = host
        }
        if(autoClose){
            // 原来的Socket可以不需要了
            s?.close()
        }

        // 获取一个 SSLCertificateSocketFactory
        val sslSocketFactory = SSLCertificateSocketFactory.getDefault(0) as SSLCertificateSocketFactory
        // 建立一个SSLSocket, 但是这时还没有带证书验证
        val ssl = sslSocketFactory.createSocket(address, port) as SSLSocket
        
        // 支持 TLS 1.1/1.2 版本
        ssl.enabledProtocols = ssl.enabledProtocols
        
        // 为该域名设置SNI
        sslSocketFactory.setHostname(ssl, peerHost)
        
        val session = ssl.session
        //获取低层默认证书验证器
        val hostnameVerifier = HttpsURLConnection.getDefaultHostnameVerifier()
        if(!hostnameVerifier.verify(peerHost, session)){
            // 如果验证失败,抛出错误
            throw SSLPeerUnverifiedException("Cannot verify hostname: $peerHost")
        }
        
        return ssl
    }


然后为 HttpsConnectionURL设置这个Factory:

connection.sslSocketFactory = SNISocketFactory(connection)

这大概就是ip直连的思路,当然了,在实际实战时,还是会有一些坑的,但是Android网络这方面的坑都被前人踩完了,所以大胆使用,不懂的可以直接百度、google就行了。

4. HttpDNS

4.1 原理

传统的DNS基本上都是基于UDP协议的,加上咱运营商参差不齐的管理Local DNS服务器,导致我们发出域名解析的那一瞬间,一切就变得不可控了。

而优化DNS的本质是让域名解析的过程变得可控,ip直连的本质做法是绕过DNS,直接不走DNS解析了,从根本上上解决问题。
而 Https另辟蹊径:既然udp不可靠,那么就换成可靠的tcp,既然运营商Local DNS容易被劫持,那就不用运营商的,用自己的服务器。

HttpDNS顾名思义,使用Http,去发出域名解析的请求,而请求的服务器是HttpDNS服务器集群,是云服务器,效率更优于运营商DNS,它利用算法优势查询最优域名,我们不需要了解其内部实现,只需要知道,它比传统Local DNS要强大就可以了。如下图所示:

在这里插入图片描述

 而HttpDNS服务器集群,可以自己开发, 也可以使用现成的,比如腾讯云、阿里云、微信都有他们自己研发的HttpDNS,收费都不高,接入方便。

4.2 使用HttpDNS的优势

请看下表: 

功能 HTTPDNS 传统DNS
防劫持 Http基于Tcp,具有一定防劫持功能,传输可靠 大部分基于Udp,容易被黑客篡改,防劫持能力垃圾的一批
调度算法 根据来源ip,就近、就快的接入业务节点 黑匣子,每个运营商都有自己的接入策略,调度策略参差不齐
解析延迟 对热点域名解析、缓存DNS解析结果、解析结果懒更新策略等方式实现0ms更新 无差别多重服务器查询,解析时长200~2000ms不等
快速生效 避免Local DNS不遵循权威TTL,解析结果长时间无法更新的问题 会自行修改DNS 的 TTL(Time-To-Live,DNS 缓存时间),导致 DNS 的修改,延迟生效。
业务成功率优化 有效降低无线场景下解析失败的比率 未知


4.3 OkHttp接入HttpDNS

OkHttp组件允许我们接入HttpDNS,所以实现起来不是很难。 OkHttp默认是使用Android SDK的服务 InetAddress来解析域名,所以我们需要替换这个就行了。

步骤如下:

  1. 在App init的时候根据域名去HttpDNS拉取ip地址,将这些ip地址存储在本地DnsCache列表中
  2. 实现 DNS接口,重写lookup(),当OkHttp使用网络请求时,会走这个方法解析域名,那么让它去DnsCache列表中取就行了,如果没有的话,就走系统默认的DNS解析
  3. 调用OkHttp的 .dns()方法,使用第二步中的对象,替换OkHttp的域名解析实现。

我们需要自己实现DNS接口,并实现 lookup方法:

class HttpDNS : Dns {

    // DNS cache
    val dnsCache: MutableMap<String, MutableList<InetAddress>>? = null

    /**
     * 寻址方法, 必须重写
     */
    override fun lookup(hostname: String): MutableList<InetAddress> {
        val ip = getIpByHost(hostname)
        // 如果读不到ip,则使用系统默认的 InetAdaaress 来解析地址
        return ip ?: Dns.SYSTEM.lookup(hostname)
    }

    /**
     * 查找缓存
     */
    private fun getIpByHost(hostname: String): MutableList<InetAddress>? {
        return dnsCache?.get(hostname)
    }

    /**
     * 获取dns方法,一般在打开App的时候根据域名去获取ip列表,放在缓存里面
     */
    private fun refresh(hostName: String): MutableList<String>? {
        val ipAddress: MutableList<String>? = null
        // 做网络请求, 这里是HttpDNS的服务器url, 拿着HostName去解析
        val reqUrl: String = HTTPDNS_SERVER_URL;
        val request = Request.Builder().url(reqUrl)
            .addHeader("domain", hostName)  // 在请求中代入hostname
            .build()

        OkHttpClient().newCall(request).enqueue(object : okhttp3.Callback {
            override fun onFailure(call: Call, e: IOException) {
                Log.w("Rikka", "解析失败")
            }

            override fun onResponse(call: Call, response: Response) {
                Log.d("Rikka", "解析成功")
                // 反正这里就是把解析的数据丢进cache中
                dnsCache?.put(hostName, call.xxxxx)
            }
        })
        return ipAddress
    }
}

最后在 OkHttp中加入这个dns:

        okHttpClient = new OkHttpClient().newBuilder()
                .dns(new HttpDNS())  // 加入这个DNS
                ...
                .build();


使用HttpDNS + OkHttp的话,通用性更强,因为OkHttp组件是可以加拦截器的,在SNI、Https的场景下都适用,而且规避了证书校验、域名检查环节。最重要的是使用非常的简单。

5. 域名收敛

在讲解域名收敛之前,需要先了解域名发散。

5.1 域名发散

PC时代,为了突破突破浏览器对于同一域名并发请求数的限制,使用域名发散为同一个服务申请多个域名,从而可以一定程度上提高并发量。简单的来说就是:

http 静态资源采用多个子域名

为什么浏览器会有并发限制呢?主要有两个原因:

  • 服务器本身承载能力差,所以在高并发的场景下,一些服务器都会崩,所以为了克服,只能由浏览器进行限制了。
  • 防止 DDOS 攻击。最基本的 DoS 攻击就是利用合理的服务请求来占用过多的服务资源,从而使合法用户无法得到服务的响应。如果最大并发量不受限制,那么DoS攻击就可以使用大量的请求,直至服务器崩溃

5.2 域名收敛

emmm显然,在上面介绍了DNS和域名发散后,我们显然发现:如果同时请求多个域名,那么DNS的解析速度肯定严重影响着整个请求过程。

移动端时代,响应速度是App赖以生存的根本,如果因为并发请求导致DNS并发,会产生 速度、开销的严重消耗。

所以解决域名发散带来的问题的方案有是: 域名收敛

移动端的网络请求模式和PC浏览器不同,移动端一般一个页面请求1~3个接口,所以很少场景达到一个端的高并发,所以可以接口收拢,比如一个页面只用一个接口。减少DNS寻址

5.3 SPDY / Http2

我们都知道 spdy、http2,或者 http3,都会使用到多路复用技术:即以最少的连接做最多的请求。
这很符合域名收敛的观念,所以支持这些技术的场景下可以尝试的去进行优化~

猜你喜欢

转载自blog.csdn.net/jdsjlzx/article/details/123761340
今日推荐