Android重学系列 Android网络编程 总览

2020-09-20  本文已影响0人  yjy239

前言

关于网络编程这一块的内容,其实很早就想写一块的内容。毕竟网络编程这一块的内容是Android开发中,除了ui和framework以外,最常接触的模块。这个部分的知识是横跨所有的编程的知识栈。因此,我们必须深入的掌握这部分的内容。

本系列本来想放在Android重学系列的,毕竟后面的篇章会进入Android的Linux内核中解析socket的源码,不过考虑到这是共有的知识栈,也就独立出一个专题来总结。

特别是做后端的开发们,对这一部分的内容应该是了然于心,甚至对socket的底层源码都十分熟悉。而我们作为Android开发,socket作为经常接触Android的核心模块一部分,有什么理由不去探索一二呢?

本系列将会以OkHttp为核心,从Http协议一路到底层看看网络通信的过程中Linux内核都做了什么?在来看看腾讯的开源库mars中究竟都做了什么优化,能做到跨平台高并发的网络吞吐量压力?

其实在去年,我已经对这一部分进行了铺垫,写了https://www.jianshu.com/p/5061860545ef关于OKio的源码解析,有兴趣可以去看看。

这里也算是对去年,专门学习研究socket和网络协议一次系列总结。

如果遇到什么问题欢迎来到本文探讨https://www.jianshu.com/p/ba60ff3c56e6

正文

我们先来对整个网络通信协议有一个大体的了解,才好继续下去。一聊到网络通信协议,就不得不提及网络通信的七层协议,以及TCP/IP模型的五层协议

下面是一幅图:


网络协议.png

这个基础模型就是网络通信协议的根本。如果不熟悉这个基础模型,去聊底层的代码那是不现实的。

我们先解释OSI七层协议的每一个意义:

物理层

物理层就是物理硬件层面上的接口,还记得在大学网络工程课程中做过的1-3,2-6交叉法做的水晶头接线。物理层就是这个物理层面上的链接。也就是在TCP/IP协议中标注的ethernet端口

数据链路层

当只是两个电脑进行通信还好说,两个电脑通过接线头进行链接即可。但是一旦是3个以上的电脑,就需要如路由器,交换机,,集线器。但是这样就出现了一个很大的问题,三台电脑需要发送到正确的电脑,就需要解决如下几个问题:

而这里就需要数据链路层,也就是常说的MAC(Medium Access Control也就是多媒体控制访问)层解决的问题。名字叫做多媒体控制访问,MAC层控制多媒体发送数据的时候,规定了什么数据先发,什么数据后发,防止出现了混乱,也就是解决了第二个问题,这个方式也叫多路访问。

多路访问分为三种方式:

聊到mac层,就肯定会聊到mac层的数据封包,而mac的数据封包就是为了解决第一个问题和第三个问题,数据发给谁怎么发,发给谁,怎么考验是否错误。

mac数据封包.png

在每一台机器的网卡中都存在一个物理地址,这个物理地址可以说在以太网中唯一的网卡机器标识(绝大部分情况),能通过这个标识找到需要通信的目标。

有没有想过如果此时,电脑是第一次接入到网络,它并不知道目标的mac地址怎么办?

ARP协议

此时就需要一个叫做ARP协议了。但是ARP协议也不是单独可以运行,还需要知道后面数据中的IP地址,两个联动起来才能正确的找到需要通信起来。

换句话说就是已知IP地址,求目标的mac地址。

整个流程如下图:


ARP寻址过程.png

只有经历了这个过程,第一次接入网络,才能正确的找到需要通信的ip地址对应的mac地址。

VLAN 协议

当出现更大规模的网络接入,如一个办公大楼接入同一个网络之后。如果都是接入同一个交换机,太多的消息都经过同一个交换机的局域网很容易被抓包,那么就没有什么安全性。

解决这个的方案有两种:

整个协议如图


VLAN协议.png

在原来的二层上新增一个TAG,这个TAG里面有一个VLAN ID,这个ID 有12位,也就是有4096个VLAN。如果交换机是支持VLAN,就会把这个TAG取出来获取其中的VLAN ID并识别。只有相同的VLAN ID包才能互相转发,不同的VLAN包是不能互相看到的。

交换机之间有一个Trunk口,互相链接交换机。

网络层

IP地址

有了mac地址,可能不少人就觉得,因为它是唯一的所以可以进行全局的通信了,就以寻找mac地址为基准。

然而,这是不可能实现的。mac地址就像一个人的身份证一样,但是我们想要找到一个人除了知道找谁之外,还需要知道这个人住哪里才行。这就需要网络成的IP地址了。

有了IP地址才知道需要往哪里通信,通过IP地址找到对应的地方后,就需要通过mac地址找到具体的人在哪里

IP地址的组成往往分为5种类型:


IP地址类型.png

我们常用的A,B,C三类网络地址。前一部分位网络号,后一部分为主机号。相当于住在4单元406号房一个意思。主机号可以看成一个个主机接入的序列。如果是按照这种设计,很容出现过于浪费的情况。

特殊的D类网络地址,属于组播地址,一般是用于如邮件往某个邮件组发送是的这个邮件接受组都能接收到。

比如B类地址,就有65534个主机号,也就是能接入这么多电脑,不就浪费了这么多的位置吗?

所以诞生了一个无类型域间选路(CIDR)的概念。

一般的,一个CIDR的IP地址会写成如下格式:

192.168.0.1/24

24代表32位IP地址中前24位为网络号,最后9位代表主机号。伴随着CIDR,存在一个所有人都能监听到的广播地址,与一个子网掩码。

比如一个网络号10.100.122 那么发送一个数据包往10.100.122.255所有主机都能监听到。

子网掩码是用来计算一个CIDR 中IP地址的网络号。比如子网掩码为255.255.255.0,代表头24位就是网络号。

IP报文结构

有了IP地址还不够,还需要更多的信息才能正确的定位。当我们发送一个邮件,需要什么呢?发件人,发件人地址,收件人以及收件人地址,以及邮件内容。

转化过来就是需要源IP地址,目标IP地址,以及传输过来的内容,由于网络环境很复杂还需要一些版本号,校验码等数据。就有了下面这幅著名的IP报文结构图:


IP报文.png

所谓IP报文实际上就是在MAC层的封装基础上填写更多的内容。

有了IP报文之后,我们就可以往更广大的网络世界进行通信了。

IP 路由协议

路由协议,实际上可以和Android 开发中的Router的概念做对比,一个页面对应一个url路由。而这里则是不同局域网不同的路由。

当我们想要访问一个服务器网卡时候,会经过网关,就会尝试的判断是否在网关同一个网段(也就是网络号)。

路由协议分为两大类:

静态路由

实际上就是在路由器上记录一条条规则,比如想要访问A,需要从哪个端口出去,下一个IP地址是谁。

在这个过程中,会遇到两种网关:

因此就会出现两种情况:

首先每一个服务器在因特网内有一个ip地址,当然这个服务器在自己的局域网内有另一个面向局域网的ip地址。

当服务器A想要访问服务器B的时候,经历两种网关,一个是转发网关路由器A,一个是NAT网关路由器B:

静态路由发送流程.png

核心就是不断的切换MAC地址,抵达下一个网关。而在静态路由表中记录了国际IP地址和本地局域网IP地址的映射,在进出NAT网关的时候进行一次切换,可以直接找到。

核心是根据目的 IP 地址来配置路由。

静态路由表,NAT网关映射一般都是配置在IPTable中,进行查询的。


$ ip route list table main 
60.190.27.189/30 dev eth3  proto kernel  scope link  src 60.190.27.190
183.134.188.1 dev eth2  proto kernel  scope link  src 183.134.189.34
192.168.1.0/24 dev eth1  proto kernel  scope link  src 192.168.1.1
127.0.0.0/8 dev lo  scope link
default via 183.134.188.1 dev eth2

比如说运营商静态的写入给这个路由器写下了如上规则:

给网卡eth2 分配了国际IP地址为183.134.189.34,对应的网关是183.134.188.1

因此到了不同的网关,就对应了不同的全局IP地址。注意一个路由器能配置多个网关

动态路由

然而这种写死的路由表策略往往不足以应付现在的网络的复杂情况。

动态路由实际上是指动态的在整个网络中构建整个通信结构,而不是写在命令ip route中。

动态构建ip 网络关系图有两种算法:

1.距离矢量路由算法

每一个路由器中都包含了全局的路由表。每隔一段时间就同步一下局域网内路由表的链接状态。有了这个表就能知道如何通过最短的路程找到目标地址。这种的问题是,好消息传输快,坏消息传输慢。这么一个场景,比如说有某个主机失联了,此时需要访问所有路由器之后,拿到全局的路由表,才知道这个主机原来断开链接了。

一般应用于 外网之间的路由,又称为外网路由协议(简称 BGP)。为什么外部变化大的反而使用这种收敛坏消息速度慢的协议呢?

你可以想想在一个国家中有数量有限的大型自治系统成为AS

AS分为3种:

每一个AS 自治系统都有自己的边界路由器,使用这种路由器和外界交互。

说回来,现在运用的BGP分为两种eBGPiBGP。自治系统之间使用eBGP进行交互,而自治系统内部使用iBGP使得内部路由找到抵达外网目的地最好的边界路由器。

正因为这些大型的自治系统不容易变更,所以反而使用这种方式会更好。

2.链路状态路由算法

当前路由器把自己和邻居路由表的关系发送出去,路由器会接受每一个路由器邻居的关系构建出一个路由表。

常用的对应算法是OSPF(Open Shortest Path First,开放式最短路径优先).一般用于数据中心内部进行决策,因此也叫内部网关协议

传输层

从这里开始就是日常开发耳熟能详的协议都在这里了,比如TCP,UDP,用于ping的ICMP等。当然还有更加快速基于UDP开发的QUIC,已经经常用于流媒体的协议RTMP都在这一层。

这里我们先只讨论TCP和UDP,关于RTMP的我会放到后面音视频模块中讲解。

TCP和UDP之间有什么区别?

TCP会建立三次握手的链接,而UCP不会。所以就有说法说,TCP面向链接,UDP面向无链接。

所谓的链接,就是为了维护客户端和服务端之间的链接而建立的数据结构来维护双方的状态,用这样的数据结构来保证面向链接的特性。

更加仔细的区别大致有如下几点:

UDP

UDP.png

UDP的结构如上。

UDP的使用场景

一般来说UDP经常用于如下场景:

QUIC

正因为有这种特殊特性,所以诞生了QUIC协议。我记得这个协议还是2年前一个微信哥们和我提到过,说他们那边研究过这个觉得很不错。这是Google基于UDP协议上进一步开发的,目的是为了减少延时。

为什么QUIC能办到呢?其实原理很简单。这也是分场景的。因为移动App现在都是基于Http协议,而Http协议又是基于TCP的。那么就需要阻塞拥塞窗口进行流量控制。

那么问题就来了,因为是移动设备,和主机设备不一样。主机设备如果发现拥塞窗口阻塞的厉害那么可以认为当前的通信到服务器的网络环境比较拥挤,需要让一点资源让服务器那边反应过来。

但是移动设备往往是移动的,可能动着动着出现网络环境不好的情况,那么每一次断开TCP的握手,又重新握手就会出现延时十分厉害。

因此诞生了QUIC,QUIC在UDP快速的基础上,再加上一些校验的逻辑。这部分内容看看后面有没有想法,可以和大家聊聊底层设计。

RTMP

因为这种机制,在流媒体中也迅速普及UDP的使用,如RTMP协议。因为老得视屏帧数丢了也就丢了,在直播这些流媒体领域如何追求画面的实时同步才是更加重要的考察点。

当然还有游戏中传送包等情况。

TCP

先来看看TCP的数据结构:

TCP结构图.png

一聊到TCP就一定会聊到3次握手和4次挥手。在我们大学的网络编程的课本中经常出现下面2幅图:

TCP三次握手

TCP三次握手.png

三次握手中发生了几次状态的变化。其实也是保证了包的顺序以及应答之间的状态。

总的来说就是三个步骤:

实际上就是为了应付复杂的网络环境,而出现的一种保证机制。当然有人会稳为什么不是4次,5次呢?实际上确实可以这样下去,甚至40次都可以。但是只需要保证客户端A确保链接上了服务器B,B就会立即发送数据的流程即可。

图中seq就是代表了当前发送包的序列,每一个包的序列默认来说都是每次递增1.所以可以通过这个规律知道包那些的发送漏了,从而在底层进行排序,排序好后在发送到上层。

整个流程如下:

换句话说,就是通过拿到对方的ack来校验本次seq的序列是否正确。

TCP四次挥手

TCP四次挥手.png

在4次挥手整个流程如下:

这个过程的行为原因有2点:

如果这个过程中,B超过了2MSL的时间,都没有收到A发送的FIN的ACK包,就不等了,直接设置为RST关闭这个口

这个过程seq的转化,首先是A往B通信,所以第一次为p,第二次B回应了一次ACK之后就是p+1.

其次,是B往A通信,所以seq重新设置为q,ack代表应答的是p+1。此时A收到后不需要B处理所以seq不需要设置,设置为p+1告诉B这个p+1对应序列号的消息已经应答了。

拥塞窗口

拥塞窗口算法.png

.一开始窗口只有一个mss大小叫做慢启动。接着翻倍的增长窗口大小,直到ssthresh临界值,之后就变成线性增长。这个过程我们成为拥塞避免。当出现丢包的时候,就说明网络环境开始变得紧张,就会开始调整窗口大小。

拥塞窗口有两种调整窗口大小的逻辑:

通过这种方式调整客户端的发送包的速度。

滑动窗口

滑动窗口中发送方的缓存.png

在TCP中,控制包的发送主要是通过拥塞窗口分为如下几个部分进行管理,从而得知缓冲队列哪些包发送且确认回收了,哪些包发送了在等待服务器回收,哪些包准备发送,哪些包不能发送。

在这里面有一个滑动窗口的概念,控制哪些发送的包行为。具体在后文会聊到。

Socket

而Socket 套字节就是面向开发者最常用的api,而这个api实际上是四层协议也就是传输层的api封装。我们可以在socket中选择对应的协议去执行不同的传输层TCP还是UDP的协议。不过更多的还是关注TCP和UDP相关的内容。更加详细的内容会在之后解析源码中放出。

应用层

Http协议与Https协议

对于Http协议还是Https协议,都是我们应用开发接触频率最高的。简单的来说Https协议就是在Http协议的基础上进行了加密安全保护。

无论是哪种协议,一旦聊起来,我们必定会聊到的下面这两幅Http协议结构图:

Http请求的结构:

Http请求协议结构.png

都是很熟悉的内容:
大致分为三个部分:

Http响应的结构:

Http响应协议结构.png

整个结构和Http的请求结构很像。实际上变化的是从请求行变成了状态行,其他都是类似的,不过是从客户端设置的内容变成服务端设置的内容。

一般来的,我们开发最重点关注的还是状态码,这里列一下简单的例子:

当然还有其他的,如重定向等。这部分内容放在OkHttp的解析,来看看这个库是怎么处理的。

Http 1.1

Http 1.1是基于Http 1.0的基础上发展过来的。

Http 2.0

Http 2.0是基于Http 1.1的基础上发展过来的。

Http 1.1是以纯文本的形式进行传输,每一次都会带上完整的Http头部不考虑pipeline模式,每一次都是完整的一来一回不断的发出了重复的部分,这样对实时性上存在不少问题。

因此Http 2.0在1.1之上做了如下改进:

通过上面三点,Http 2.0把多个请求划分在不同的流中,把内容拆分成为帧进行二进制传输。这些帧可以打乱顺序传输,最后根据帧的首部流标识符。

下面是一个示意图:


Http1.1和2.0之间的传输区别.png

在这个过程中服务器和客户端不需要再对请求一一对应,可以同时处理多个请求和应答。

实际上是在把三次串行的请求转化成三个流,将数据分成帧乱序发送。

Http2.0流的切割.png

每一个数据帧的数据格式为:


Http2 数据帧.png

注意Type代表当前的数据帧是什么类型,Flag代表当前数据帧是什么状态,StreamID当前数据帧是属于哪一个复用流的,Frame PayLoad就是当前数据帧所荷载的内容。

所有的这些都能先通过长度获取到后整个数据帧的内存范围后再逐步解析。

Https 协议

再聊Https协议之前需要明白下面两个知识点:

加密
数字证书

不对称加密如何把公钥发送出去呢?这又牵扯到另一个概念,数字证书CA。

公钥发送出去要么就是放在公网的某个地址让人下载,要么就是请求的时候下载。

现在公认一套流程是借助一个权威网站,从这个权威网站中下载公钥。而这个权威网站下发的东西就是我们常说的证书证书中包含了证书所有者,发布机构以及日期。

而发布证书的权威网站就是我们常说的CA( Certificate Authority)。

证书请求可以通过这个命令生成:

openssl req -key cliu8siteprivate.key -new -out cliu8sitecertificate.req

将这个请求发给权威机构,权威机构会给它盖一个章也就是使用签名算法生成一个签名:

openssl x509 -req -in cliu8sitecertificate.req -CA cacertificate.pem -CAkey caprivate.key -out cliu8sitecertificate.pem

而 cliu8sitecertificate.pem 就是签过名的证书.

里面有个 Issuer(谁颁发的这个证书);Subject(证书发给谁的);Validity(证书有效期限);Public-Key(公钥内容);Signature Algorithm 是签名算法。

怎么保证这个权威机构是没问题的呢?那么就需要更加上层的权威机构给这个权威机构添加一个CA证书。在这一层层的嵌套上,就有一个Root CA作为最全球最为权威的机构。通过这种层层授权,才让非对称模式在互联网上流行。

所以说,看见有的网站需要你添加Root CA到自己的电脑时候请注意了,一旦根CA出现了问题,之后Https也会变得不安全。

Https通信模型

Https通信模型.png

DNS

当网络的七层协议都了解之后,可以聊聊其他常用的概念,如DNS。

一般来说我们平时访问网站的时候不是通过ip地址访问,而是通过一个url域名访问的。而映射ip地址和url的地址薄是通过一个名为DNS的转化的。所以DNS很重要通常设置为高并发,高可用,分布式的。

DNS分为三种:

解析流程:

HttpDNS

传统的DNS访问会遇到如下几个问题:

为了解决这个问题,就诞生了HttpDns。HttpDns本质上就是在自己客户端搭建一个基于Http协议本地的DNS服务器集群,自己做映射缓存。一般都是手机端自己使用,手机端想要使用就需要手机添加HttpDNS的SDK。

工作流程也很简单,如果访问一个地址,则先从自己的缓存访问是否有缓存,有则返回。至于什么时候过时,那就是自己进行设置。如果本地没有,则需要访问HttpDNS的服务器,在本地HttpDNS服务器中IP列表中,选择一个发出Http请求,访问UP地址。

手机客户端自然知道手机在哪个运营商、哪个地址。由于是直接的 HTTP 通信,HttpDNS 服务器能够准确知道这些信息,因而可以做精准的全局负载均衡。

总结起来就是解决亮点问题:

Okhttp的设计

Okhttp可以分为如下七层协议:

OkHttp设计基础框架.png

头三层协议专门用于处理状态码重试缓存几种情况:

Http响应状态码大致上可以分为如下几种情况:

2XX 代表请求成功

200,203,204 代表请求成功,可以对响应数据进行缓存

30X 代表资源发生变动或者没有变动

4XX 客户端异常或者客户端需要特殊处理

5XX 服务端异常

retryAndFollowUpInterceptor

主要处理了如下几个方向的问题:

BridgeInterceptor

主要是把Cookie,Content-type设置到头部中。很多时候,初学者会疑惑为什么自己加的头部会失效,就是因为在Application拦截器中处理后,又被BridgeInterceptor 覆盖了。需要使用networkInterceptor

CacheInterceptor

主要是处理304等响应体的缓存。通过DiskLruCache缓存起来。

到这里前三层属于对Http协议处理的拦截器就完成了,接下来几层就是okhttp如何管理链接的。

ConnectInterceptor

ConnectInterceptor 链接拦截器在okhttp中做了如下几件事情:

到这里就完成了整个ConnectInterceptor 的工作。比起http 2.0的协议初始化,我们更需要关注的是,这个过程中由如下两个核心的方法:

CallServerInterceptor

先来看看Okhttp的管理活跃链接


Okhttp链接管理.png

实际上是由一个RealConnectionPool 缓存所有的RealConnection。实际上对应上层来说每一个RealConnection就是代表每一个网络链接的抽象门面。

而实际上真正工作的是其中的Socket对象。整个socket链接大致可以分为如下几个步骤:

这四个步骤都是在ConnectionInterceptor 拦截器中完成。

虽然都是RealConnection对象,但是分发到CallServerInterceptor之前会生成一个Exchange对象,其中这个对象就会根据Http1.0/1.1 或者Http2.0 协议 对应生成不同的Http1ExchangeCodec 以及 Http2ExchangeCodec. 这两个对象就是根据协议类型对数据流进行解析。

无论这两个协议做了什么,都可以抽象成如下几个方法:

后话

有了这些基础后,我们再从OkHttp开始阅读源码,看看这个手机端最出名的网络请求库是怎么设计的。

本文不是最终版本,之后会陆续更新详细。

上一篇 下一篇

猜你喜欢

热点阅读