科普贴 | 一文速览移动数据发送全过程

2,128 阅读19分钟

前言

作为一个移动开发的程序员,经常会被网络折磨的死去活来,比如各种网络库、处理HTTP请求、编写Socket。假设我们不去了解网络相关的知识,压根不知道这些参数为什么这么配置。于是乎,我痛定思痛,拜读了刘超老师的专栏《趣谈网络协议》以及上野宣的《图解HTTP》,今天就和各位好好聊一聊移动设备的数据发送过程。

图

# 工具学习

在进行下文之前,我先抛出一个问题,平时我们是如何抓取数据请求呢?
小王答曰:Charles

没错,Charles可以用来抓取请求和返回的数据包,这里需要注意,Android 7.0及以上只信任系统预装的CA证书,所以如果你想用Charles抓取其他App貌似没有办法,我们姑且以HTTP请求为例:

Charles截图
如果不熟悉Charles或者想抓取HTTPS包,建议阅读

《Charles 功能介绍和使用教程》
《听说安卓微信 7.0 不能抓 https?》

一、发送前准备

回归正文,一日,小王闲来无事,大好时光,岂不摸鱼?遂打开某App

摸鱼
数据请求就准备发送了。

1. 运营商网络准备

小王没有连接公司的无线网络,所以,数据走的运营商网络,这个过程是怎么样的呢?

运营商网络
我说明一下:

  1. 手机先发将信息发送给附近基站,跟基站E-Node B说我想发送数据。
  2. 基站跟手机说,别急,我去查查你的状态。当然,基站不是干这个的,这个时候它就把任务交给了MME。
  3. HSS用于存储用户信息的数据库,MME就是根据HSS判断当前手机号是否合法并且有效的,别当前手机已经欠费1000了,运营商还让你愉快的上网。
  4. MME通过HSS一看当前手机有效,先请求SGW为E-Node B建立E-NodeB-SGW-PGW之间的通信隧道,之后再跟E-Node B说,这手机有效,你去连接SGW把。
  5. 基站接收到HSS的通知以后,手机就可以通过E-Node B-SGW-PGW的通信隧道连接到IP网络了。

上述只是简化后的过程,实际要复杂的多。

如果想要对该过程有进一步了解可以阅读

《第一次有人把5G讲的这么简单明了》(跟本文关系不大,不过是一篇不错的科普文)
《走进通信:4G手机跟基站是怎么“交流”的》

2. 域名解析

设置Api接口的时候你是喜欢设置具体的IP还是域名呢?先不急回答,且看下文。

在有域名的前提下,手机需要将域名解析解析为IP地址,这不就是我们常常听到的DNS(域名解析解析)吗?众所周知,DNS是属于TCP/IP体系中应用层的协议,真实的域名解析过程中可不是你丢给它一个域名它扔给你一个IP地址这么简单,它需要多个DNS服务器的参与,它的结构如下:

DNS服务器结构
从上图可以看出DNS服务器是这么划分的:

  • 根DNS服务器:返回顶级域DNS服务器的IP地址
  • 顶级域DNS服务器:返回权威DNS服务器的IP地址
  • 权威DNS服务器:返回相应主机的IP地址

设计成这样的树形结构好处是什么呢?主要是为了应对高可用、高并发和分布式的场景。理解完DNS服务器的结构,我们再看具体的域名解析过程。

DNS请求流程
从上一个过程中我们得知小王的手机可以连接到IP网络,但此时的手机并不知道服务器的IP地址,DNS就其作用了,假设摸鱼的域名为www.vinsent.cn(因为上面的图片来自网络),手机就会向本地DNS服务器发起一轮询问:

  • 手机:本地DNS服务器你知道www.vinsent.cn的IP地址是什么吗?
  • 本地DNS服务器:不知道(如果缓存有,就直接返回了,过程就到此结束),我可以帮你问问根DNS服务器。
  • 本地DNS服务器:根DNS服务器啊,你知道www.vinsent.cn的IP地址吗?
  • 根DNS服务器:我不知道,不过我可以告诉你这个域名的顶级域.cn的服务器的IP地址,它是xxx.xxx.xxx.xxx。

接着本地DNS服务器会不厌其烦的询问顶级域.cn服务器,得到管理www.vinsent.cn域名的权威服务器IP地址,最后通过权威服务器得到www.vinsent.cn的IP地址,缓存过后就返回给请求的客户端。这会儿,客户端就得知服务器的IP地址了。

这里有一点是需要注意的,一些大型应用为了缓解中心服务器的压力,实现了全局的负载均衡,通过在DNS服务器配置CNAME的方式可以为我们的客户端基于我们的位置和运营商等提供最优的服务器。就跟我们买KFC选择最近的是一个道理,也是为了当一个服务器宕机这时候可以有Plan B或者Plan C,这也是为什么设置IP接口的时候最好设置域名而非具体的IP地址。

3. TCP三次握手

有了IP地址这个门牌号,发送数据就容易多了,当然,发送数据之前,手机还得和服务器建立连接,就跟送快递一样,小哥不可能给你送快递的时候直接把快递扔在小区外的马路上,也不管你有没有收到通知,这是UDP的做法,显然我们要做的就是建立TCP连接。

手机客户端和服务器的对话如下:

TCP建立连接
大致意思就是:

  • 客户端:告诉你,我准备连接了
  • 服务器:好的,我知道你要连接了
  • 客户端:111,我收到你可以连接的消息了

仔细的同学可能发现了,图中有两个ACK,一个是大写,一个是小写,这是什么意思呢,先看一下TCP报文:

TCP报文
看一下做标记的即可:

  • seq对应的是序号,小写的ack是确认编号,它们的目的是为了给包排序和解决丢包问题
  • 大写的ACK和SYN,它们则是状态标记位,值是0或1,常用的是SYN-建立连接、ACK-应答、FIN-终止连接和RST-重置

至此,连接状态就建立了。

如果对TCP不是特别熟悉,强烈建议阅读,因为TCP的传输过程很重要

《计算机网络:这是一份全面 & 详细 的TCP协议攻略》

4. HTTPS加密

连接状态终于建立了,是不是意味着小王的手机可以发送数据请求了呢?如果当前的请求是HTTP,现在完全可以。不过,现在很少会有网站使用明文传输,并且,Android 9.0后会将明文的数据传输默认给禁掉,所以使用HTTPS(超文本传输安全协议)就显得尤其重要。

显然,摸鱼服务器的后台也是这么想的,它可不想随随便便就把自己的信息接口贡献出去,让谁都可以调用。HTTPS中实际起作用的是TLS/SSL协议:

HTTPS=HTTP+TLS/SSL

于是,TCP建立以后,又马不停蹄的搭建HTTPS:

HTTPS
过程是这样的,非对称加密+对称加密,简单来说,非对称加密有两把钥匙,公钥和私钥,公钥加密,私钥解密,对称加密只需一把钥匙进行加密解密,效率也就更高,所以非对称加密用来传输对称加密的秘钥相关内容,对称加密用来进行信息的交流,对话又开始了:

  • Client:Client Hello,告诉服务器一个随机数A、支持的加密套路和一些版本信息
  • Server:Server Hello,告诉客户端随机数B,确定加密套路,以及后续的加密算法
  • Server:Certificate,将证书下发给客户端
  • Server:Server Hello Done,Server Hello过程结束

客户端在收到证书之后,验证证书,确定证书里面的公钥是来自服务器,接着产生随机数C。

  • Client:Client Key ExChange,客户端使用证书里的公钥加密随机数C传给服务器,服务器在收到消息则利用自己的私钥解密得到随机数C.

至此,两边都拥有了随机数A+B+C,于是利用先前约定的算法生成对称加密的钥匙。客户端会继续发送消息过去:

  • Client:Change Cipher Spec,客户端告诉服务端我们以后都用对称加密进行交流了
  • Client:Encrypted Handshake Message,试着发出一条加密消息,服务器能解出来说明加密成立
  • Server:Change Cipher Spec,客户端告诉服务端我们以后都用对称加密进行交流了
  • Server:Encrypted Handshake Message,试着发出一条加密消息,服务器能解出来说明加密成立

如果不熟悉HTTPS,建议阅读:

《用故事说透 HTTPS》(入门篇)
《SSL/TLS 握手过程详解》
《彻底搞懂HTTPS的加密机制》

5. 构建HTTP请求

做了这么多功,客户端的请求其实还没发出去。

???
客户端:我太难了

客户端构建了一个HTTP请求:

POST /getInfo HTTP/1.1
Host: www.moyu.com
Content-Type: application/json; charset=utf-8
Content-Length: nnn
 
{
 "info": {
  "date": "2019-12-15",
  "name": "沸点",
  "type": "摸鱼",
  "limit": 50
 }
}

它对应的结构是这样的:

请求报文结构

请求行
  • 请求方法:对应着POST,在Android中我们常用的是RESTful api,我们都知道,如果请求方法是POST,就需要我们在请求体放入一些我们需要请求的参数。
  • URL:这里的URL就是www.moyu.com/getInfo
  • 协议版本:对应着HTTP 1.1,最常见的版本
请求头

请求行可以定义很多东西,比如编码格式、缓存规则、日期、路由跳数等很多信息。

  • Host:服务器地址是www.moyu.com
  • Content-Type:编码格式是utf-8,数据格式是Json
  • Content-Length:内容长度是nn,这里应该是具体数字
请求体

请求体就是我们要请求的内容。

如果对HTTP不是特别清楚,建议阅读:

《计算机网络:这是一份全面& 详细 HTTP知识讲解》

二、发送

HTTP报文在应用层生成,接着会经过TCP/IP体系中的运输层、网际层和网络接口层,报文信息也会跟着发生相应的变化:

报文格式变化过程

1. 自上而下的发送

需要说明的是,这里的二层,三层和TCP层指的是网络的五层体系或者OSI体系的链路层、网络层和传输层,而在TCP/IP体系中,指的是网络接口层、网际层和运输层。

应用层生成完HTTP报文以后,消息会根据TCP/IP体系自上而下发送。

1.1 运输层

因为客户端用的是HTTP,它是基于TCP的,所以会在HTTP报文前面加上一层TCP报文,TCP报文会记录源端口号、目的端口号跟进程有关的信息,接收返回信息的时候,可以帮助客户端区分是哪个应用发出的信息。当HTTP的数据比较多的时候,会被拆分成一个个的数据块。TCP报文上文已经有了,这里不再放图。

1.2 网际层

再往下就是网际层,用是IP协议,需要加上一层IP报文,IP报文最主要的作用是记录源IP和目标IP,这里的源IP是运营商给我们手机分配的IP地址,目标IP是摸鱼服务器的IP地址。

MAC和IP报文

1.3 网络接口层

这一层似乎没有停止加报文的意思,继续加上一层MAC头信息,MAC头里面存放着:目标MAC地址和源MAC地址

我们都知道,MAC地址是计算机的唯一身份,相信对网络不熟悉的同学们肯定会有这样的疑惑,既然MAC地址唯一,直接使用MAC地址就行了,为什么还要存放IP地址呢?简单来说,就像人的身份证和住址,尽管身份证能够证明你的唯一性,却只能通过住址来定位,找到你家位置以后,发现你们家有你爸、你妈和你三个人,这个时候MAC地址就起作用了,谁的身份证是xxxxx呢?这个时候你回了一声,是你。

2. 进入网络

一个个的数据包终于被安排发送进网络了,如果服务器和客户端处于同一个局域网内,这种情况就比较简单了,直接发送给局域网里面的服务器,服务器就可以直接解析了。不过,客户端使用的移动网络,情况就变得复杂多了。为了便于理解,我用下方简化过后的图片进行说明:

网络发送过程

2.1 手机--PGW

小王的手机的IP地址和服务器的IP地址显然不在一个网段,所以他们不能够直接进行沟通,为什么不能沟通呢,你可以把小王手机IP地址所属的局域网看成一个国家,服务器所在的局域网属于另外一个国家,国家和国家之前进行交流靠什么呢?当然是边关啊,所以一个局域网里的设备想跟另外一个局域网里的设备通信,得通过网关,移动网络中分配IP地址的网关是PGW,网关的局域网内的IP地址一般是xxx.xxx.xxx.1或者xxx.xxx.xxx.2,已知网关IP地址,可以利用ARP(地址解析协议)获取MAC地址。出了手机以后,当前网络包的MAC头和IP头是这样的:

  • 源MAC:手机MAC
  • 目的MAC:PGW MAC
  • 源IP:手机IP
  • 目的IP:服务器S1 IP

接着会进入利用GTP(GPRS隧道协议)搭建的隧道,隧道这里的包我们就不深入了,感兴趣的同学可以在本小结结尾处查看链接。

PGW收到网路包以后,做了如下处理:

  1. 先取下当前的MAC头,查看一下目标MAC是否跟自己一样
  2. MAC一致,继续取下IP头,看看目标IP是否跟自己一致,一致则直接处理
  3. IP不一致,则把IP头重新装上
  4. PGW也有着路由功能,并且知道网络包的目标IP,于是从自己的路由表取出下一跳路由器RA的IP地址,再根据ARP获取其MAC地址,获取成功之后装上新的MAC头。(真实的环境不止这么简单,即使是内部网络,中间也间隔着N个路由器,于是就需要寻找最短路径,内部网络寻找最短路径是通过OSPF(开放式最短路径优先))

从PGW发出后,网络包结构变成:

  • 源MAC地址:PGW MAC
  • 目的MAC:RA MAC
  • 源IP:手机IP
  • 目的IP:服务器S1 IP

2.2 PGW--外网

公网IP资源稀少,运营商如果为每个人分配一个,岂不是血亏,为了解决公网IP少的问题,运营商使用了NAT(网络地址转换),常用的NAT有PAT(端口地址映射)

PAT,原理即公网IP+端口=内网IP,公网IP、端口、内网IP将这些信息生成一张PAT表。一个公网IP理论上可以支持65535个手机终端。

在移动网络中,NAT一般是PGW的边界路由器做的,所以边界路由器 RA实质是一个NAT路由器。NAT路由器跟普通路由器有点不一样,它可以修改IP头的内容,为什么要修改IP头呢?我们都知道,网络包进了公网,必须使用公网IP,而此时路由器RA收到的网络包的目标IP仍是内网IP,于是路由器RA将他改成分配好的公网IP,再将公网IP和端口存进PAT表中。

IP头组装好了,下一步就是组装MAC头,在外部网络中,使用BGP(外网路由协议)确定最短路径,在这里我们假设RA-RB-RD就是最短路径,中间可能间隔着N个路由设备。知道这些信息以后,路由器RA的下一跳就确定了,是路由器 RB,同样的,从路由表取IP,通过ARP获取MAC,一气呵成,从路由器 RA出来的包结构:

  • 源MAC:RA MAC
  • 目的MAC:RB MAC
  • 源IP:运营商公网IP
  • 目的IP:服务器S1 IP

下面的过程大家也就知道了,不停的更换MAC头,经过路由器 RB、路由器 RD,最终到达了服务器 S1。

如果对上面的某些概念不是特别清楚,推荐阅读:

《在LTE移动网络中为什么要使用GTP隧道/协议?》 《IPSec协议》 《BGP漫谈》

3. 复杂的网络环境

一个大的HTTP报文可能被分成很个数据包进行发送,网络环境这么复杂,怎么可能每个数据包都能按序按时到达,可能会出现的情况:数据包丢失、重复发送、乱序等。还好,我们有负责可靠的TCP,它的可靠在哪里呢?

  1. 面向连接:就是我们上文谈及的TCP三次握手,对方不在的时候解决不发送。
  2. 面向字节流:数据太大会被拆分成块,根据网络状况调整发送包的数量。
  3. 拆分成的块不丢失、无差错、不乱序、不重复。

关注一下报文中标记部分:

TCP报文
TCP内容要讲清楚的话可能又得花上一半篇幅,这里就讲一下应该关注的点:

  • 滑动窗口:发送者的发送包的速度和接受者的处理能力需要滑动窗口来平衡,除此以外,滑动窗口还能处理发送包的乱序、丢失问题。
  • 拥塞控制:除了收发速度的平衡,还需要适应网络带宽,与之相关的是慢开始 & 拥塞避免、快重传 & 快恢复两个解决方案,主要的原理就是在堵塞的边缘疯狂试探,从而找到合适的发送包的速度,而不会导致网络阻塞。

不熟悉TCP的同学,强烈建议阅读:

《计算机网络:这是一份全面 & 详细 的TCP协议攻略》

三、返回请求

服务器一层层地解析网络包,终于得到客户端发送来的HTTP报文,得知小王要获取一些数据,于是就返回了一份响应报文给他:

HTTP/1.1   200   ok
Date: Tue,10 Dec 2019 12:12:12 GMT 
Content-Type: application/json; charset=utf-8
Content-Length: nnn

{
    "total":19,
    "result":[
        {
            "year":1996,
            "month":12,
            "day":18,
            "title":"小王成功摸了第一次鱼",
            "type":1
        },
        {
            "year":1995,
            "month":12,
            "day":18,
            "title":"联合国宣布1996年为国际消除贫困年",
            "type":1
        },
        {
            "year":1987,
            "month":12,
            "day":18,
            "title":"我国制成第一部完全国产化机器人",
            "type":1
        },
        {
            "year":1981,
            "month":12,
            "day":18,
            "title":"阿尔巴尼亚劳动政治局委员谢胡“自杀身亡”",
            "type":1
        },
        {
            "year":1979,
            "month":12,
            "day":18,
            "title":"消除对妇女一切形式歧视公约通过",
            "type":1
        }
    ],
    "error_code":0,
    "reason":"Succes"
}

响应报文也有自己的格式:

响应报文结构

1.响应行

  • HTTP版本:请求报文里面也有,不再赘述。
  • 状态码和状态描述:常用的状态码有200-正常,404-服务器无法处理
类别 原因短语
1XX Infromational(信息状态码) 接收的请求正在处理
2XX Success(成功状态码) 请求正常处理完毕
3XX Redirection(重定向状态码) 需要进行附加操作以完成请求
4XX Client Error(客户端错误状态码) 服务器无法处理请求
5XX Server Error(服务端错误状态码) 服务端处理请求出错

2.响应头

返回头也可以存放一些额外信息,缓存、日期、格式、编码等: date:日期信息

3. 响应体

服务器返回给小王的数据信息,使用的json格式。

更多状态码和状态信息说明可以查看我的整理:

《HTTP笔记》

四、关闭连接

收到响应报文以后,数据就成功的显示在小王的手机上,摸鱼成功。

这个时候,小王已经不需要进行HTTP请求了,但是TCP还处在连接中,总不能一直占用着网络资源吧,于是就出现了TCP的四次挥手来关闭连接。请看最后的深蓝色部分:

untitled@2x.png
名词:

  • FIN、ACK:ACK已介绍,FIN是断开连接的标记位,标记位的值都是0或1。
  • MSL:报文在网络中生存的最大周期,为什么要等待2MSL,因为进行客户端最后一次挥手,服务器如果不同意,可以立马发一个信息过来,一来一回就是两个网络包的最大生命周期,就是2MSL。

挥手过程大致什么意思呢?就跟流水线的上线和下线的工人即将下班的场景一样:

流水线

上线员工A:快要下班了,我现在不发东西过去了
下线员工B:好的,收到消息,我把流水线剩下的工作处理完
下线员工B:我处理完了,下班吧
上线员工A:收到,我要关电源了

上线员工A过了一会儿,看B没有再回消息,没有,就把流水线的电源给关了。四次挥手的过程大概就是这样的。

五、总结

本文到这里就结束了,网络的发送过程之大,绝不是一篇文章能够概括的,需要同学们系统的学习理论知识,本文的目的也正是如此,抛砖引玉,简单描述数据包在移动网络的生命周期,让同学们对此有更加宏观的认识,如果想要深入学习,则需要同学们自行努力啦。

本文篇幅较长,难免会有错误,如有异议,欢迎在下方留言讨论。再闲聊一下自己,写这篇文章大概花了两周的时间,主要是最近的事也太多了了,又被拉去写前端,哎,生活太难了。

分享不易,如果各位觉得不错,还望手动点赞~