App线上网络问题优化策略

在我们App开发过程中,网络是必不可少的,几乎很难想到有哪些app是不需要网络传输的,所以网络问题一般都是线下难以复现,一旦到了用户手里就会碰到很多疑难杂症,所以对于网络的监控是必不可少的,针对用户常见的问题,我们在实际的项目中也需要添加优化策略。

1 网络的基础优化

对于一些主流的网络请求框架,像OkHttp、Retrofit等,其实就是对Http协议做了封装,我们在使用的时候常见的就是POST或者GET请求,如果我们是做客户端开发,知道这些基本的内容好像也可以写代码,但是真正碰到了线上网络问题,反而摸不到头脑,其实最大的问题还是对于网络方面的知识储备不足,所以文章的开始,我们先来点基础的网络知识。

1.1 网络连接的类型

其实对于网络的连接,我们常见的就是向服务端发起请求,服务端返回对应的响应,但是在同一时刻,只能有一个方向的数据传输,这种连接方式称为半双工通信。

类型 描述 举例
单工 在通信过程中,数据只能由一方发送到另一方 常见的例如UDP协议;Android广播
半双工 在通信过程中,数据可以由一方A发送到另一方B,也可以从一方B发送到另一方A,但是同一时刻只能存在一方的数据传输 常见的例如Http协议
全双工 在任意时刻,都会存在A到B和B到A的双向数据传输 常见的例如Socket协议,长连接通道

所以在Http1.0协议时,还是半双工的协议,因为默认是关闭长连接的,如果需要支持长连接,那么就需要在http头中添加字段:“Connection:Keep-Alive”;在Http 1.1协议时,默认是开启了长连接,如果需要关闭长连接,那么需要添加http请求头字段:“Connection:close”.

那么什么时候或者场景下,需要用到长连接呢?其实很简单,记住一点即可,如果业务场景中对于消息的即时性有要求时,就需要与服务端建立长连接,例如IM聊天,视频通话等场景。

1.2 DNS解析

如果伙伴们在项目中有对网络添加trace日志,除了net timeout这种超时错误,应该也看到过UnknowHostException这种异常,这是因为DNS解析失败,没有解析获取到服务器的ip地址。

像我们在家的时候,手机或者电脑都会连接路由器的wifi,而路由器是能够设置dns服务器地址的,

但是如果设置错误,或者被攻击篡改,就会导致DNS解析失败,那么我们app的网络请求都会出现异常,所以针对这种情况,我们需要加上自己的DNS解析策略。

首先我们先看一个例子,假设我们想要请求百度域名获取一个数据,例如:

object HttpUtil {

    private const val BASE_URL = "https://www.baidu.comxx"

    fun initHttp() {
        val client = OkHttpClient.Builder()
            .build()
        Request.Builder()
            .url(BASE_URL)
            .build().also {

                kotlin.runCatching {
                    client.newCall(it).execute()
                }.onFailure {
                    Log.e("OkHttp", "initHttp: error $it ")
                }

            }
    }
}

很明显,百度的域名是错误的,所以在执行网络请求的时候就会报错:

java.net.UnknownHostException: Unable to resolve host "www.baidu.comxx": No address associated with hostname

所以一旦我们的域名被劫持修改,那么整个服务就会处于宕机的状态,用户体感就会很差,因此我们可以通过OkHttp提供的自定义DNS解析器来做一个小的优化。

public interface Dns {
  /**
   * A DNS that uses {@link InetAddress#getAllByName} to ask the underlying operating system to
   * lookup IP addresses. Most custom {@link Dns} implementations should delegate to this instance.
   */
  Dns SYSTEM = hostname -> {
    if (hostname == null) throw new UnknownHostException("hostname == null");
    try {
      return Arrays.asList(InetAddress.getAllByName(hostname));
    } catch (NullPointerException e) {
      UnknownHostException unknownHostException =
          new UnknownHostException("Broken system behaviour for dns lookup of " + hostname);
      unknownHostException.initCause(e);
      throw unknownHostException;
    }
  };

  /**
   * Returns the IP addresses of {@code hostname}, in the order they will be attempted by OkHttp. If
   * a connection to an address fails, OkHttp will retry the connection with the next address until
   * either a connection is made, the set of IP addresses is exhausted, or a limit is exceeded.
   */
  List<InetAddress> lookup(String hostname) throws UnknownHostException;
}

我们看下源码,lookup方法相当于在做DNS寻址,一旦发生异常那么就会抛出UnknownHostException异常;同时内部还定义了一个SYSTEM方法,在这个方法中会通过系统提供的InetAddress类进行路由寻址,同样如果DNS解析失败,那么也会抛出UnknownHostException异常。

所以我们分两步走,首先使用系统能力进行路由寻址,如果失败,那么再走自定义的策略。

class MyDNS : Dns {


    override fun lookup(hostname: String): MutableList<InetAddress> {

        val result = mutableListOf<InetAddress>()
        var systemAddressList: MutableList<InetAddress>? = null
        //通过系统DNS解析
        kotlin.runCatching {
            systemAddressList = Dns.SYSTEM.lookup(hostname)
        }.onFailure {
            Log.e("MyDNS", "lookup: $it")
        }

        if (systemAddressList != null && systemAddressList!!.isNotEmpty()) {
            result.addAll(systemAddressList!!)
        } else {
            //系统DNS解析失败,走自定义路由
            result.add(InetAddress.getByName("www.baidu.com"))
        }

        return result
    }
}

这样在www.baidu.comxx 解析失败之后,就会使用www.baidu.com 域名替换,从而避免网络请求失败的问题。

1.3 接口数据适配策略

相信很多伙伴在和服务端调试接口的时候,经常会遇到这种情况:接口文档标明这个字段为int类型,结果返回的是字符串“”;或者在某些情况下,我需要服务端返回一个空数组,但是返回的是null,对于这种情况,我们在数据解析的时候,无论是使用Gson还是Moshi,都会解析失败,如果处理不得当,严重的会造成崩溃。

所以针对这种数据格式不匹配的问题,我们可以对Gson简单做一些适配处理,例如List类型:

class ListTypeAdapter : JsonDeserializer<List<*>> {
    override fun deserialize(
        json: JsonElement?,
        typeOfT: Type?,
        context: JsonDeserializationContext?
    ): List<*> {
        return try {
            if (json?.isJsonArray == true) {
                Gson().fromJson(json, typeOfT)
            } else {
                Collections.EMPTY_LIST
            }
        } catch (e: Exception) {
            //
            Collections.EMPTY_LIST
        }
    }
}

如果json是List数组类型数据,那么就正常将其转换为List数组;如果不是,那么就解析为空数组。

class StringTypeAdapter : JsonDeserializer<String> {

    override fun deserialize(
        json: JsonElement?,
        typeOfT: Type?,
        context: JsonDeserializationContext?
    ): String {
        return try {
            if (json?.isJsonPrimitive == true) {
                Gson().fromJson(json, typeOfT)
            } else {
                ""
            }
        } catch (e: Exception) {
            ""
        }
    }
}

对于String类型字段,首先会判断是否为基础类型(String,Number,Boolean),如果是基础类型那么就正常转换即可。

GsonBuilder()
    .registerTypeAdapter(Int::class.java, IntTypeAdapter())
    .registerTypeAdapter(String::class.java, StringTypeAdapter())
    .registerTypeAdapter(List::class.java, ListTypeAdapter())
    .create().also {
        GsonConverterFactory.create(it)
    }

这样在创建GsonConverterFactory时,就可以使用我们的策略来进行数据适配,但是在测试环境下,我们不建议这样使用,因为无法发现服务端的问题,在上线之后为了规避线上问题可以使用此策略。

2 HTTPS协议

http协议与https协议的区别,就是多了一个“s”,可别小看这一个“s”,它能够保证http数据传输的可靠性,那么这个“s”是什么呢,就是SSL/TLS协议。

从上图中看,在进入TCP协议之前会先走SSL/TLS协议.

2.1 对称加密和非对称加密

既然Https能保证传输的可靠性,说明它对数据进行了加密,以往http协议数据的传输都是明文传输,数据极容易被窃取和冒充,因此后续优化中,对于数据进行了加密传输,才有了Https协议诞生。

常见的加密手段有两种:对称加密和非对称加密。

2.1.1 对称加密

首先对称加密,从名字就能知道具体的原理,看下图:

对称加密和解密的密钥是一把钥匙,需要双方约定好,发送方通过秘钥加密数据,接收方使用同一把秘钥解密获取传递的数据。

所以使用对称加密非常简单,解析数据很快,但是安全性比较差,因为双方需要约定同一个key,key的传输有被劫持的风险,而统一存储则同样存在被攻击的风险。

所以针对这种情况,应运而生出现了非对称加密。

2.1.2 非对称加密

非对称加密会有两把钥匙:私钥 + 公钥,对于公钥任何人都可以知道,发送方可以使用公钥加密数据,而接收方可以用只有自己知道的私钥解密拿到数据。

那么既然公钥所有人都知道,那么能够通过公钥直接推算出私钥吗?答案是目前不可能,未来可能会,得看全世界的密码学高手或者黑客能否解决这个问题。

总结一下两种加密方式的优缺点:

加密类型 优点 缺点
对称加密 流程简单,解密速度快 不安全,秘钥管理有风险
非对称加密 私钥只有自己知道 流程繁琐,解密速度慢

2.2 公钥的安全保障

通过2.1小节对于非对称加密的介绍,虽然看起来安全性更高了一些,但是对于公钥的传递有点儿太理想化,我们看下面的场景。

如果公钥在传输的过程中被劫持,那么发送方拿到的是黑客的公钥,后续所有的数据传输都被劫持了,所以问题来了,如何保证发送方拿到的公钥一定是接收方的?

举个简单的例子:我们在马路上捡到了一张银行卡,想把里面的钱取出来,那么银行柜台其实就是接收方,银行卡就是公钥,那么银行就会直接把钱给我们了吗?肯定不可以,要么需要身份证,要么需要密码,能够证明这个银行卡是我们自己的,所以公钥的安全性保证就是CA证书(可以理解为我们的身份证)。

那么首先接收方需要办一张身份证,需要通过CA机构生成一个数字签名,具体生成的规则如下:

那么最终发送给接收方的就是如下一张数字证书,包含的内容有:数字签名 + 公钥 + 接收方的个人信息等。

那么发送方接收到数字证书之后,就会检查数字证书是否合法,检测方式如下:

如果不是办的假证,这种可能性几乎为0,因为想要伪造一个域名的数字签名,根本不可能,CA机构也不是吃干饭的,所以只要通过证书认证了,那么就能保证公钥的安全性。

2.3 Https的传输流程

其实一个Https请求,中间包含了2次Http传输,假如我们请求 www.baidu.com 具体流程如下:

(1)客户端向服务端发起请求,要访问百度,那么此时与百度的服务器建立连接;

(2)此时服务端有公钥和私钥,公钥可以发送给客户端,然后给客户端发送了一个SSL证书,其中包括:CA签名、公钥、百度的一些信息,详情可见2.2小节最后的图;

(3)客户端在接收到SSL证书后,对CA签名解密,判断证书是否合法,如果不合法,那么就断开此次连接;如果合法,那么就生成一个随机数,作为数据对称加密的密钥,通过公钥加密发送到服务端。

(4)服务端接收到了客户端加密数据后,通过私钥解密,拿到了对称加密的密钥,然后将百度相关数据通过对称加密秘钥加密,发送到客户端。

(5)客户端通过解密拿到了服务端的数据,此次请求结束。

其实Https请求并不是完全是非对称加密,而是集各家之所长,因为对称加密密钥传递有风险,因此前期通过非对称加密传递对称加密密钥,后续数据传递都是通过对称加密,提高了数据解析的效率。

但是我们需要了解的是,Https保障的只是通信双方当事人的安全,像测试伙伴通过Charles抓包这种中间人攻击方式,还是会导致数据泄露的风险,因为通过伪造证书或者不受信任的CA就可以实现。

Android 学习笔录

OkHttp 源码解析笔记:https://qr18.cn/Cw0pBD
Android 性能优化篇:https://qr18.cn/FVlo89
Android 车载篇:https://qr18.cn/F05ZCM
Android 逆向安全学习笔记:https://qr18.cn/CQ5TcL
Android Framework底层原理篇:https://qr18.cn/AQpN4J
Android 音视频篇:https://qr18.cn/Ei3VPD
Jetpack全家桶篇(内含Compose):https://qr18.cn/A0gajp
Kotlin 篇:https://qr18.cn/CdjtAF
Gradle 篇:https://qr18.cn/DzrmMB
Flutter 篇:https://qr18.cn/DIvKma
Android 八大知识体:https://qr18.cn/CyxarU
Android 核心笔记:https://qr21.cn/CaZQLo
Android 往年面试题锦:https://qr18.cn/CKV8OZ
2023年最新Android 面试题集:https://qr18.cn/CgxrRy
Android 车载开发岗位面试习题:https://qr18.cn/FTlyCJ
音视频面试题锦:https://qr18.cn/AcV6Ap

猜你喜欢

转载自blog.csdn.net/weixin_61845324/article/details/132782128