最近在开发一个支持Android和iOS的库,发现当开启VPN后,Android创建连接的速度比iOS快很多,最后了解到这是因为Android和iOS的Socket行为策略不同导致的。下面来探究为什么会出现这种差异。

内核

Android的内核是Linux,而iOS的内核是XNU。Linux的网络栈高度灵活,XNU的网络栈主要源自BSD内核。

Linux

Linux的网络栈会维护一个全局的、动态的转发表,也就是我们常说的路由表。当socket需要发送数据包时,内核协议栈会去查询这个转发表。这种设计天然就倾向于实时决策。

BSD

在BSD的传统实现中,一个套接字的路由信息在连接建立的早期阶段可能就被缓存下来了,后续的通信都会优先使用这个缓存的路由,而不是像Linux一样,每次都会去查询一个 全局的 路由表。这种设计倾向于减少决策次数。

当VPN打开后,发生了什么

###背景 启动APP会立刻进行网络连接,但是链接需要VPN才能成功。如果打开APP后再打开VPN,Android比iOS反应更快,iOS需要等待连接超时重连才能正确通过VPN访问服务器,Android却能在很短的时间内(体感2s内)通过新配置的VPN访问到服务器。

Android

  1. 程序调用了Connect,之后内核发送SYN包,此时会使用默认的路由表规则发出。因为VPN没有被激活,路由表没有被更改,数据包自然也无法被顺利发送到服务器;
  2. 此时VPN被打开,VPN软件修改了内核的路由表,添加了更高优先级的规则,所以之后的包都会往VPN发送;
  3. 这时候SYN包超时了,协议栈准备重传。由于每次发送都会查询 全局的、动态的 路由表,重传的包会经过VPN顺利到达服务器。

iOS

  1. 和Android一样,调用connect之后,也会查询路由表。区别在于,内核会将查询到的结果与套接字关联起来;
  2. 此时打开VPN,全局路由表同样被修改了;
  3. SYN包超时,协议栈准备重传,但会优先检查与Socket关联的配置,发现已经有缓存好的路由决策,于是直接使用这个决策重传,自然会以失败告终;
  4. 这个重传会一直进行下去,直到上层的connect调用失败并返回错误,进入代码中的reconnect流程,才会重新查询全局路由表,使用VPN的配置进行发送。这也是为什么iOS会更慢,因为要等到第一次connect调用彻底失败,返回超时错误,才会使用更新的配置进行通信。

总结

核心的区别在于路由决策的不同,Linux Kernel会在每次发包时进行动态查询,而BSD-based Kernel会在连接建立时候就进行路径选择。 这种路由决策的差异,造成了对网络变化适应性的不同。