Blog


  • 首页

  • 关于

  • 标签

  • 分类

  • 归档

  • 搜索

数据库高性能

发表于 2019-04-29 | 分类于 架构

读写分离

读写分离的基本原理是将数据库读写操作分散到不同的节点上。

读写分离适用单机并发无法支撑并且读的请求更多的情形。在单机数据库情况下,表上加索引一般对查询有优化作用却影响写入速度,读写分离后可以单独对读库进行优化,写库上减少索引,对读写的能力都有提升,且读的提升更多一些。
不适用的情况:
1)如果并发写入特别高,单机写入无法支撑,就不适合这种模式。
2)通过缓存技术或者程序优化能够满足要求

读写分离的基本实现是:
1)数据库服务器搭建主从集群,一主一从,一主多从都可以
2)数据库主机负责写操作,从机负责读操作
3)数据库主机通过复制将数据同步到数据库从机,每台数据库服务器都存储了所有的业务数据
4)业务服务器将写操作发给数据库主机,将读操作发给数据库从机

但有两个细节点引入了复杂度:主从复制延时和分配机制,以下为解决方案

复制延时:
1)写操作后的读操作指定发给数据库主机
2)读从机失败后再度一次主机
3)关键业务读写操作全部指向主机,非关键业务采用读写分离

分配机制:
将读写操作区分开来,然后访问不同的数据库,一般有两种方式:程序代码封装和中间件封装

分库分表

分库分表会带来很多复杂度。在引入分库分表之前,应该是这些操作依次尝试:
1.做硬件优化,例如从机械硬盘改成使用固态硬盘,当然固态硬盘不适合服务器使用,只是举个例子
2.先做数据库服务器的调优操作,例如增加索引,oracle有很多的参数调整;
3.引入缓存技术,例如Redis,减少数据库压力
4.程序与数据库表优化,重构,例如根据业务逻辑对程序逻辑做优化,减少不必要的查询;
5.在这些操作都不能大幅度优化性能的情况下,不能满足将来的发展,再考虑分库分表,也要有预估性

分库

业务分库是指按照业务模块将数据分散到不同的数据库服务器。

存在问题:
1)join问题
2)事务问题
3)成本问题

分表

分两种方式:垂直分表、水平分表

水平分表:
水平分表后,某条数据具体属于哪个子表,需要增加路由算法进行计算,这个算法会引入一定的复杂性。
常见的路由算法有:
1)范围路由
2)hash路由
3)配置路由

其他常见的复杂性问题:join,count,order by等

架构设计流程

发表于 2019-04-28 | 分类于 架构

如何识别复杂度

架构设计由需求所驱动,本质目的是为了解决软件系统的复杂性;为此,我们在进行架构设计时,需要以理解需求为前提,首要进行系统复杂性的分析。具体做法是:

(1)构建复杂度的来源清单——高性能、可用性、扩展性、安全、低成本、规模等。

(2)结合需求、技术、团队、资源等对上述复杂度逐一分析是否需要?是否关键?

“高性能”主要从软件系统未来的TPS、响应时间、服务器资源利用率等客观指标,也可以从用户的主观感受方面去考虑。

“可用性”主要从服务不中断等质量属性,符合行业政策、国家法规等方面去考虑。

“扩展性”则主要从功能需求的未来变更幅度等方面去考虑。

(3)按照上述的分析结论,得到复杂度按照优先级的排序清单,越是排在前面的复杂度,就越关键,就越优先解决。

需要特别注意的是:随着所处的业务阶段不同、外部的技术条件和环境的不同,得到的复杂度问题的优先级排序就会有所不同。一切皆变化。

备选方案设计

经过架构设计流程第 1 步——识别复杂度,确定了系统面临的主要复杂度问题,进而明确了设计方案的目标,就可以开展架构设计流程第 2 步——设计备选方案。架构设计备选方案的工作更多的是从需求、团队、技术、资源等综合情况出发,对主流、成熟的架构模式进行选择、组合、调整、创新。

1.几种常见的架构设计误区

(1)设计最优秀的方案。不要面向“简历”进行架构设计,而是要根据“合适”、“简单”、“演进”的架构设计原则,决策出与需求、团队、技术能力相匹配的合适方案。

(2)只做一个方案。一个方案容易陷入思考问题片面、自我坚持的认知陷阱。

2.备选方案设计的注意事项

(1)备选方案不要过于详细。备选阶段解决的是技术选型问题,而不是技术细节。

(2)备选方案的数量以 3~5个为最佳。

(3)备选方案的技术差异要明显。

(4)备选方案不要只局限于已经熟悉的技术。

3.问题思考

可以从开源、自研的角度提出架构设计方案

如果是创业公司的业务早、中期阶段,可直接考虑采用阿里云/腾讯云,性能、HA、伸缩性都有保证。

最大的感悟是:做事情永远都要有B方案。

评估和选择备选方案

1 评估和选择备选方案的方法
列出我们需要关注的质量属性点,然后分别从这些质量属性的维度去评估每个方案,再综合挑选适合当时情况的最优方案。常见的质量属性点有:性能、可用性、硬件成本、项目投入、复杂度、安全性、可扩展性。
按优先级选择,即架构师综合当前的业务发展情况、团队人员规模和技能、业务发展预测等因素,将质量属性按照优先级排序,首先挑选满足第一优先级的,如果方案都满足,那就再看第二优先级……以此类推。

2 RocketMQ 和 Kafka 有什么区别?

(1) 适用场景
Kafka适合日志处理;RocketMQ适合业务处理。

(2) 性能
Kafka单机写入TPS号称在百万条/秒;RocketMQ大约在10万条/秒。Kafka单机性能更高。

(3) 可靠性
RocketMQ支持异步/同步刷盘;异步/同步Replication;Kafka使用异步刷盘方式,异步Replication。RocketMQ所支持的同步方式提升了数据的可靠性。

(4) 实时性
均支持pull长轮询,RocketMQ消息实时性更好

(5) 支持的队列数
Kafka单机超过64个队列/分区,消息发送性能降低严重;RocketMQ单机支持最高5万个队列,性能稳定(这也是适合业务处理的原因之一)

3 为什么阿里会自研RocketMQ?

(1) Kafka的业务应用场景主要定位于日志传输;对于复杂业务支持不够
(2) 阿里很多业务场景对数据可靠性、数据实时性、消息队列的个数等方面的要求很高
(3)当业务成长到一定规模,采用开源方案的技术成本会变高(开源方案无法满足业务的需要;旧版本、自开发代码与新版本的兼容等)
(4) 阿里在团队、成本、资源投入等方面约束性条件几乎没有

详细设计方案

完成备选方案的设计和选择后,接下来需要将确定的备选方案细化,使得备选方案变成一个可以落地的设计方案。

1)架构师不但要进行备选方案的设计和选型,还需要对备选方案的关键细节有较深入的理解。
2)通过分步骤、分阶段、分系统等方式,尽量减低方案复杂度,方案本身的复杂度越高,某个细节推翻整个方案的可能性就越大,适当降低复杂度,可以降低这种风险。
3)如果方案本身就很复杂,那就采取设计团队的方式来进行设计,博采众长,防止可能出现的盲点或经验误区。

Flutter APP体积为何比较大

发表于 2019-04-26 | 分类于 Hybrid Develop

flutter构建的App体积比native的大一些,是什么原因造成App体积大呢?

其实flutter 在release时App体积和native的大小差不多,而debug时体积通常会大。debug版本体积较大是为了Hot reload和快速编译。如果有flutter开发经验的朋友都体验过,如果您修改一下App的背景颜色,只需save一下就可以立刻看到修改后效果。我称之为“像艺术家一样在创造App”,因此为了实现这些目标,提高开发的效率,debug将占用全部资源。而当我们构建release版时,flutter又会采用AOT策略,提高App运行效率,release版只打包必需的资源,因而体积又会减少。

另外,flutter团队也一直在寻找减小程序大小的方法。

现在开发 App 的方式非常多,原生、ReactNative、Flutter 都是不错的选择。那你有没有关注过,使用不同的方式,编译生成的 Apk ,大小是否会有什么影响呢?本文就以一个最简单的 Hello World App,来看看不同的框架,编译出来的 Apk 大小,有什么区别。

Java(539 KB)

首先使用 Java 来开始这次实验,使用 Java 开发 Android 算是最常规也是最简单的一种方式。正如前面描述的那样,由于我们仅仅使用了 Java 和 Android 框架来创建这个应用程序,所以它将是最小的,唯一的依赖是 Android 支持库,它占用了整个 Apk 内相当多的空间。

Flutter(7.5MB)

由 Flutter 的 cli 生成的 Release 版本的应用程序中,包含 C / C ++ 引擎和 Dart VM,它们构成了 Apk 的几乎所有部分。该应用程序直接使用本机指令集运行,不涉及任何解释器。

本文里介绍的几种编写 App 的方法,都存在优缺点,在实际工作中,应该根据需求选择适合的方式。你还可以混合搭配这些框架,仅仅用它们的优点来开发 App 的部分功能。

React Native(7MB)

如果你有前端(Web)的开发经验,并希望使用 JavaScript 来开发 App,那么 React Native 是一个不错的选择。

如果你希望在已发布的 App 上,进行更快的功能迭代,使用 React Native 也可以让你不必为每个小改动都发布应用市场。

由 React Native 生成的 Release apk 在 classes.dex 文件中有几个类,这些类有 12193 个针对此应用程序的引用方法。
它还在 x86 和 armeabi-v7a 的 lib 目录中添加了一些 so 库。总共添加了大约 6.4 MB 的空间。


ReactNative 和 Flutter 因为其内部还需要包含一些解析器和引擎,本身就会有一些基础库在其内,所以变大也是符合预期的。

参考资料

https://www.jianshu.com/p/0e223b472f41
https://www.cnblogs.com/plokmju/p/release_apk.html

架构的概念

发表于 2019-04-25 | 分类于 架构

架构设计的主要目的是为了解决软件系统复杂度带来的问题。个人感悟是:架构及(重要)决策,是在一个有约束的盒子里去求解或接近最合适的解。这个有约束的盒子是团队经验、成本、资源、进度、业务所处阶段等所编织、掺杂在一起的综合体(人、财、时间等)。架构无优劣,但是存在恰当的架构用在合适的系统中,而这些就是决策的结果。不要过分设计。

架构概念

软件架构指软件系统的顶层结构;框架是面向编程或配置的半成品;组件是从技术维度上的复用;模块是从业务维度上职责的划分;系统是相互协同可运行的实体。

软件架构指软件系统的“基础结构”,创造这些基础结构的准则,以及对这些结构的描述。

软件模块(Module)是一套一致而互相有紧密关连的软件组织。它分别包含了程序和数据结构两部分。现代软件开发往往利用模块作为合成的单位。模块的接口表达了由该模块提供的功能和调用它时所需的元素。模块是可能分开被编写的单位。这使它们可再用和允许人员同时协作、编写及研究不同的模块。

软件组件定义为自包含的、可编程的、可重用的、与语言无关的软件单元,软件组件可以很容易被用于组装应用程序中。

高性能

性能是软件的一个重要质量属性。衡量软件性能包括了响应时间、TPS、服务器资源利用率等客观指标,也可以是用户的主观感受(从程序员、业务用户、终端用户/客户不同的视角,可能会得出不同的结论)。

在说性能的时候,有一个概念与之紧密相关—伸缩性,这是两个有区别的概念。性能更多的是衡量软件系统处理一个请求或执行一个任务需要耗费的时间长短;而伸缩性则更加关注软件系统在不影响用户体验的前提下,能够随着请求数量或执行任务数量的增加(减少)而相应地拥有相适应的处理能力。

但是,什么是“高”性能?这可能是一个动态概念,与当前的技术发展状况与业务所处的阶段紧密相关。比如,现在在行业/企业内部认为的高性能,站在5年后来看,未必是高性能。因此,站在架构师、设计师的角度,高性能需要和业务所处的阶段来衡量。高到什么程度才能与当前或可预见的未来业务增长相匹配。一味去追求绝对意义上的高,没有太大的实际意义。因为,伴随性能越来越高,相应的方法和系统复杂度也是越来越高,而这可能会与当前团队的人力、技术、资源等不相匹配。但是什么才合适的高性能了?这可能需要从国、内外的同行业规模相当、比自己强的竞争者、终端用户使用反馈中获取答案并不断迭代发展。

软件系统中高性能带来的复杂度主要体现在两方面,一方面是单台计算机内部为了高性能带来的复杂度;另一方面是多台计算机集群为了高性能带来的复杂度。

2 WHY 为什么需要高性能?
追求良好的用户体验;
满足业务增长的需要。

3 HOW 如何做好高性能?
可以从垂直与水平两个维度来考虑。垂直维度主要是针对单台计算机,通过升级软、硬件能力实现性能提升;水平维度则主要针对集群系统,利用合理的任务分配与任务分解实现性能的提升。

垂直维度可包括以下措施:
增大内存减少I/O操作
更换为固态硬盘(SSD)提升I/O访问速度
使用RAID增加I/O吞吐能力
置换服务器获得更多的处理器或分配更多的虚拟核
升级网络接口或增加网络接口

水平维度可包括以下措施:
功能分解:基于功能将系统分解为更小的子系统
多实例副本:同一组件重复部署到多台不同的服务器
数据分割:在每台机器上都只部署一部分数据

垂直维度方案比较适合业务阶段早期和成本可接受的阶段,该方案是提升性能最简单直接的方式,但是受成本与硬件能力天花板的限制。

水平维度方案所带来的好处要在业务发展的后期才能体现出来。起初,该方案会花费更多的硬件成本,另外一方面对技术团队也提出了更高的要求;但是,没有垂直方案的天花板问题。一旦达到一定的业务阶段,水平维度是技术发展的必由之路。因此,作为技术部门,需要提前布局 ,未雨绸缪,不要被业务抛的太远。

高可用

高可用基础是“状态决策”。本质上是通过“冗余”来实现高可用。

高可用保证的原则是“集群化”,或者叫“冗余”:只有一个单点,挂了服务会受影响;如果有冗余备份,挂了还有其他backup能够顶上。保证系统高可用,架构设计的核心准则是:冗余。有了冗余之后,还不够,每次出现故障需要人工介入恢复势必会增加系统的不可服务实践。所以,又往往是通过“自动故障转移”来实现系统的高可用。

可扩展性

核心是:封装变化,隔离可变性。

应对变化方案:
1)将“变化”封装在一个“变化层”,将不变的部分封装在一个独立的“稳定层”
2)提炼出一个“抽象层”和一个“实现层”。抽象层是稳定的,而实现层是根据业务进行定制的,当加入新功能时,只需要更改实现层,无须修改抽象层。

可伸缩性

当前大型互联网网站需要面对大量用户高并发访问、存储更多数据、处理更高频次的用户交互。网站系统一般通过多种分布式技术将多台服务器组成集群对外提供服务。伸缩性一般是系统可以根据需求和成本调整自身处理能力的一种能力。伸缩性常意味着系统可以通过低成本并能够快速改变自身的处理能力以满足更多用户访问、处理更多数据而不会对用户体验造成任何影响。

伸缩性度量指标包括(1)处理更高并发;(2)处理更多数据;(3)处理更高频次的用户交互。

其复杂度体现在(1)伸——增强系统在上述三个方面的处理能力;(2)缩——缩减系统处理能力;(3)上述伸缩过程还必须相对低成本和快速。

成本、安全、规模

低成本是架构设计中需要考虑一个约束条件,但不会是首要目标。低成本本质上是与高性能和高可用冲突的,当无法设计出满足成本要求的方案,就只能协调并调整成本目标。
往往只有“创新”才能达到低成本目标。1)引入新技术。主要复杂度在于需要去熟悉新技术,并且将新技术与已有技术结合;一般中小型公司基本采用该方式达到目标。2)开创一个全新技术领域。主要复杂度在于需要去创造全新的理念和技术,并且与旧技术相比,需要有质的飞跃,复杂度更高;一般大公司拥有更多的资源、技术实力会采用该方式来达到低成本的目标。

安全在技术角度上将包括功能安全和架构安全。1)功能安全-“防小偷”,减少系统潜在的缺陷(是一个逐步完善的过程,而且往往都是在问题出现后才能有针对性的提出解决方案,与编码实现有关),阻止黑客的破坏行为。2)架构安全-“防强盗”,保护系统不受恶意访问与攻击,保护系统的重要数据不被窃取(传统企业主要通过防火墙实现不同区域的访问控制,功能强大、性能一般,但是成本更高;互联网企业更多的是依靠运营商或者云服务商强大的带宽和流量清洗的能力,较少自己来设计和实现)。

规模带来复杂度的主要原因是“量变引起质变”。1)功能越来越多,调用逻辑越来越复杂,会导致系统复杂度指数级上升。2)数据容量、类型、关联关系越来越多。
规模问题需要与高性能、高可用、高扩展、高伸缩性统一考虑。常采用“分而治之,各个击破”的方法策略。

架构设计三原则

不断演化是架构发展的主旋律,而满足适合、追求简单是架构决策的重要依据。需求驱动技术的创新演化;技术反哺业务的发展升级。
1)合适原则
合适原则宣言:合适优于业界领先
失败原因:没有那么多人,却想干那么多活;没有那么多积累,却想一步登天;没有卓越的业务场景,却幻想灵光一闪成为天才。设计的目的不是为了证明自己,而是更快更好的满足业务需求。

2)简单原则
简单原则宣言:简单优于复杂
定位一个复杂系统中的问题总是比简单系统更为复杂

3)演化原则
演化原则宣言:演化优于一步到位
对于软件来说,变化才是主题。罗马不是一天建成的,架构也不是一开始就设计成完美的样子,然后可以一劳永逸的用下去。

各个公司的架构都是逐渐演进成当前的样子,在达到同样目的的过程中实现手段确并不完全相同,蚂蚁和阿里都进行了多地多中心部署的架构改造,但二者在诸如配置中心、跨ldc访问管控等方面都不尽相同,即使在蚂蚁内部也出现了后续实现推翻原始规划的情况。在多地多中心部署架构改造完成后,为进一步降低成本,避免大促活动中机器的浪费,又开始了弹性部署的改造,希望能够在大促高峰来临的前几个小时再临时增加服务器,等活动结束服务器就立即回收。等这个搞定,又开始在线离线混布的改造,进一步降低整体成本。
这些改造之所以一个接一个的能够实现,也在于使用的主要中间件和框架都是自研的,知根知底,可以快速迭代修改,如果是使用第三方的或者购买的,一方面可能非常贵,另一方面可能根本不支持,要重新设计改造部署所需的时间要远远大于自研的成本。

软件活动中没有“银弹”

在古代的狼人传说中,只有用银质子弹(银弹)才能制服这些异常凶残的怪兽。在软件开发活动中,“银弹”特指人们渴望找到用于制服软件项目这头难缠的“怪兽”的“万能钥匙”。

软件开发过程包括了分析、设计、实现、测试、验证、部署、运维等多个环节。从IT技术的发展历程来看,先辈们在上述不同的环节中提出过很多在当时看来很先进的方法与理念。但是,这些方法、理念在摩尔定律、业务创新、技术发展面前都被一一验证了以下观点:我们可以通过诸多方式去接近“银弹”,但很遗憾,软件活动中没有“银弹”。

布鲁克斯发表《人月神话》三十年后,又写了《设计原本》。他认为一个成功的软件项目的最重要因素就是设计,架构师、设计师需要在业务需求和IT技术中寻找到一个平衡点。个人觉得,对这个平衡点的把握,就是架构设计中的取舍问题。而这种决策大部分是靠技术,但是一定程度上也依赖于架构师的“艺术”,技术可以依靠新工具、方法论、管理模式去提升,但是“艺术”无法量化 ,是一种权衡。

软件设计过程中,模块、对象、组件本质上是对一定规模软件在不同粒度和层次上的“拆分”方法论,软件架构是一种对软件的“组织”方法论。一分一合,其目的是为了软件研发过程中的成本、进度、质量得到有效控制。但是,一个成功的软件设计是要适应并满足业务需求,同时不断“演化”的。设计需要根据业务的变化、技术的发展不断进行“演进”,这就决定了这是一个动态活动,出现新问题,解决新问题,没有所谓的“一招鲜”。

以上只是针对设计领域的银弹讨论,放眼到软件全生命周期,银弹问题会更加突出。

小到一个软件开发团队,大到一个行业,没有银弹,但是“行业最佳实践”可以作为指路明灯,这个可以有。

Android Retrofit 2详解

发表于 2019-04-11 | 分类于 HTTP

基础使用

以下就是实现一个登录Login接口的小功能 ,先了解一下Retrofit的基本用法:

private  void getLogin() {
Retrofit retrofit = new Retrofit.Builder()
        .baseUrl("//localhost:8080/")
        .addCallAdapterFactory(RxJava2CallAdapterFactory.create())
        .addConverterFactory(GsonConverterFactory.create())
        .build();
ApiManager apiService = retrofit.create(ApiManager.class);

Call<LoginResult> call = apiService.getData("lyk", "1234");
call.enqueue(new Callback<LoginResult>() {
   @Override
   public void onResponse(Call<LoginResult> call, Response<LoginResult> response) {
       if (response.isSuccess()) {
           // 请求成功
       } else {
          //直接操作UI 或弹框提示请求失败
       }
   }

   @Override
   public void onFailure(Call<LoginResult> call, Throwable t) {
       //错误处理代码
   }
   });
}

ApiManager接口:

public interface ApiManager {
 @GET("login/")
 Call<LoginResult> getData(@Query("name") String name, @Query("password") String pw);
}

Retrofit支持异步和同步

call.enqueue(new Callback)采用异步请求;
call.execute() 采用同步方式。

call.cancel() 取消请求

CallAdapterFactory

.addCallAdapterFactory(RxJava2CallAdapterFactory.create())这个是用来决定你的返回值是Observable还是Call。

// 使用call的情况
Call<String> login();  
// 使用Observable的情况
Observable<String> login();  

如果返回为Call那么可以不添加这个配置。如果使用Observable那就必须添加这个配置。否则就会请求的时候就会报错!

Retrofit中使用RxJava:由于Retrofit设计的扩展性非常强,你只需要添加一个 CallAdapter 就可以了

ConverterFactory

addConverterFactory 制定数据解析器,上面添加依赖的gson就是用在这里做默认数据返回的, 之后通过build()创建出来。

Retrofit内部自带如下格式:

Gson: com.squareup.retrofit2:converter-gson
Jackson: com.squareup.retrofit2:converter-jackson
Moshi: com.squareup.retrofit2:converter-moshi
Protobuf: com.squareup.retrofit2:converter-protobuf
Wire: com.squareup.retrofit2:converter-wire
Simple XML: com.squareup.retrofit2:converter-simplexml
Scalars (primitives, boxed, and String): com.squareup.retrofit2:converter-scalars

网络请求参数

@Path:所有在网址中的参数(URL的问号前面),如://192.168.1.1/api/Accounts/{accountId}

@Query:URL问号后面的参数,如://192.168.1.1/api/Comments?access_token={access_token}

@QueryMap:相当于多个@Query

@Field:用于POST请求,提交单个数据

@FieldMap:以map形式提交多个Field(Retrofit2.0之后添加)

@Body:相当于多个@Field,以对象的形式提交

注意:

  1. 使用@Field时记得添加@FormUrlEncoded

  2. 若需要重新定义接口地址,可以使用@Url,将地址以参数的形式传入即可。

  3. @Path 和@Query的区别
    相同点:都是请求头中的带有的数据
    不同点:前者是请求头中问号之前用于替换URL中变量的字段,后者是请求头问号之后用于查询数据的字段,作用和应用场景都不同

进阶功能

开启Log

用拦截器实现, retrofit已经提供了 HttpLoggingInterceptor 里面有四种级别,输出的格式,可以看下面介绍:

public enum Level {
/** No logs. */
NONE,
/**
 * Logs request and response lines.
 *
 * <p>Example:
 * <pre>{@code
 * --> POST /greeting 
 * 
 * 
 * /1.1 (3-byte body)
 *
 * <-- 200 OK (22ms, 6-byte body)
 * }</pre>
 */
BASIC,
/**
 * Logs request and response lines and their respective headers.
 *
 * <p>Example:
 * <pre>{@code
 * --> POST /greeting http/1.1
 * Host: example.com
 * Content-Type: plain/text
 * Content-Length: 3
 * --> END POST
 *
 * <-- 200 OK (22ms)
 * Content-Type: plain/text
 * Content-Length: 6
 * <-- END HTTP
 * }</pre>
 */
HEADERS,
/**
 * Logs request and response lines and their respective headers and bodies (if present).
 *
 * <p>Example:
 * <pre>{@code
 * --> POST /greeting http/1.1
 * Host: example.com
 * Content-Type: plain/text
 * Content-Length: 3
 *
 * Hi?
 * --> END GET
 *
 * <-- 200 OK (22ms)
 * Content-Type: plain/text
 * Content-Length: 6
 *
 * Hello!
 * <-- END HTTP
 * }</pre>
 */
BODY
}

例如,开启请求头添加拦截器:

Retrofit retrofit = new Retrofit.Builder().client(new OkHttpClient.Builder()
                         .addNetworkInterceptor(new  HttpLoggingInterceptor().setLevel(HttpLoggingInterceptor.Level.HEADERS))       
                         .build())

增加头部信息

new Retrofit.Builder()
       .addConverterFactory(GsonConverterFactory.create())
       .client(new OkHttpClient.Builder()
               .addInterceptor(new Interceptor() {
                   @Override
                   public Response intercept(Chain chain) throws IOException {
                       Request request = chain.request()
                               .newBuilder()
                               .addHeader("mac", "f8:00:ea:10:45")
                               .addHeader("uuid", "gdeflatfgfg5454545e")
                               .addHeader("userId", "Fea2405144")
                               .addHeader("netWork", "wifi")
                               .build();
                       return chain.proceed(request);
                   }
               })
               .build()

特殊API接口单独加入,方法上注释@Headers:

@Headers({ "Accept: application/vnd.github.v3.full+json", "User-Agent: Retrofit-your-App"})
@get("users/{username}")
Call<User>   getUser(@Path("username") String username);

添加证书Pinning

证书可以在自定义的OkHttpClient加入certificatePinner 实现:

OkHttpClient client = new OkHttpClient.Builder()
.certificatePinner(new CertificatePinner.Builder()
        .add("YOU API.com", "sha1/DmxUShsZuNiqPQsX2Oi9uv2sCnw=")
        .add("YOU API..com", "sha1/SXxoaOSEzPC6BgGmxAt/EAcsajw=")
        .add("YOU API..com", "sha1/blhOM3W9V/bVQhsWAcLYwPU6n24=")
        .add("YOU API..com", "sha1/T5x9IXmcrQ7YuQxXnxoCmeeQ84c=")
        .build())

支持https

加密和普通http客户端请求支持https一样,证书同样可以设置到okhttpclient中.详细可以参考我之前的文章:android中使用https

常见问题

url被转义

https://api.myapi.com/http%3A%2F%2Fapi.mysite.com%2Fuser%2Flist

请将@path改成@url

public interface APIService { 
@GET Call<Users> getUsers(@Url String url);}

或者:

public interface APIService {
    @GET("{fullUrl}")
    Call<Users> getUsers(@Path(value = "fullUrl", encoded = true) String fullUrl);
}

Method方法找不到

java.lang.IllegalArgumentException: Method must not be null

请指定具体请求类型@get @post等

public interface APIService { 
   @GET Call<Users> getUsers(@Url String url);
}

Url编码不对,@fieldMap parameters must be use FormUrlEncoded

如果用fieldMap加上FormUrlEncoded编码

@POST()
@FormUrlEncoded
Observable<ResponseBody> executePost(@FieldMap Map<String, Object> maps);

上层需要转换将自己的map转换为FieldMap

@FieldMap(encoded = true) Map<String, Object> parameters,

path和url一起使用

Using @Path and @Url paramers together with retrofit2 
java.lang.IllegalArgumentException: @Path parameters may not be used with @Url. (parameter #4

如果你是这样的:

@GET
Call<DataResponse> getOrder(@Url String url, @Path("id") int id);

请在你的url指定占位符.url:

www.myAPi.com/{Id}

原理

Retrofit就像一个适配器(Adapter)的角色,将一个Java接口转换成一个Http请求并返回一个Call对象,简单的调用接口方法就可以发送API请求,Retrofit完全隐藏了Request 的请求体,并使用okhttp执行请求。

Retrofit 是怎么实现的呢?答案就是:Java的动态代理。Java动态代理,是一种结构性设计模式,可以在要调用的Class方法前或后,插入想要执行的代码进行改造。

案例中关键两行代码:

ApiManager apiService = retrofit.create(ApiManager.class); //2、retrofit对象创建一个API接口对象

Call<LoginResult> call = apiService.getData("lyk", "1234"); //返回响应接口回调

这简短的两行代码,隐藏了Request请求体并拿到Response返回Call对象。看下源码,这几行代码才是 Retrofit 精妙之处:

/** Create an implementation of the API defined by the {@code service} interface. */
public <T> T create(final Class<T> service) {
  Utils.validateServiceInterface(service);
  if (validateEagerly) {
     eagerlyValidateMethods(service);
  }
  return (T) Proxy.newProxyInstance(service.getClassLoader(), new Class<?>[] { service },
    new InvocationHandler() {
      private final Platform platform = Platform.get();
      @Override public Object invoke(Object proxy, Method method, Object... args)
          throws Throwable {
        // If the method is a method from Object then defer to normal invocation.
        if (method.getDeclaringClass() == Object.class) {
          return method.invoke(this, args);
        }
        if (platform.isDefaultMethod(method)) {
          return platform.invokeDefaultMethod(method, service, proxy, args);
        }
        ServiceMethod serviceMethod = loadServiceMethod(method);
        OkHttpCall okHttpCall = new OkHttpCall<>(serviceMethod, args);
        return serviceMethod.callAdapter.adapt(okHttpCall);
      }
    });
}

源码分析:
当 apiService 对象调用 getData方法时,就会被这个动态代理拦截并在内部做些小动作,它会调用 Proxy.newProxyInstance方法 中的 InvocationHandler 对象,它的 invoke方法 会传入3个参数:

Object proxy :代理对象 ,即APIManner.class
Method method :调用方法,即getData方法
Object… args : 参数对象,即 “lyk”,”1234”

Retrofit 得到了 method 和 参数args 。接下去 Retrofit 就会用 Java反射 获取到 getData方法 的注解信息,配合args参数,创建一个ServiceMethod对象。

ServiceMethod 是服务于请求方法的,服务于传入Retrofit的proxy对象的method方法,即getData方法。如何服务呢?它可以将method通过各种内部接口解析器进行组装拼凑,最终生成一个Request请求体。这个Request 包含 api域名、path、http请求方法、请求头、是否有body、是否是multipart等等。最后返回一个Call对象,Retrofit2中Call接口的默认实现是OkHttpCall,它默认使用OkHttp3作为底层http请求client。一句话就是:Retrofit 使用Java动态代理就是要拦截被调用的Java方法,然后解析这个Java方法的注解,最后生成Request由OkHttp发送Http请求。

想要弄清楚Retrofit的细节,先来简单了解一下Retrofit源码组成结构:


一个retrofit2.http包,里面全部是定义HTTP请求的Java注解,比如GET、POST、PUT、DELETE、Headers、Path、Query等;

余下的retrofit2包中,几个类和接口retrofit的代码真的很少很简单,因为retrofit把网络请求这部分功能全部交给了OkHttp。

Retrofit接口

Retrofit的设计使用插件化而且轻量级,高内聚而且低耦合,这都和它的接口设计有关。Retrofit中定义了四个接口:

  • Callback
  • Converter<F, T>
  • Call
  • CallAdapter

1、Callback
这个接口就是retrofit请求数据返回的接口,只有两个方法:

void onResponse(Response<T> response);
void onFailure(Throwable t);

2、Converter<F, T>
这个接口主要的作用就是将HTTP返回的数据解析成Java对象,主要有Xml、Gson、protobuf等。你可以在创建Retrofit对象时添加你需要使用的Converter实现。

3、Call
这个接口主要的作用就是发送一个HTTP请求,Retrofit默认的实现是OkHttpCall,你可以根据实际情况实现你自己的Call类。这个设计和Volley的HttpStack接口设计的思想非常相似,子类可以实现基于HttpClient或HttpUrlConnetction的HTTP请求工具。

4、CallAdapter
这个借口的属性只有responseType一个;这个接口的实现类也只有DefaultCallAdapter一个。这个方法的主要作用就是将Call对象转换成另一个对象,为了支持RxJava才设计这个类的吧。

Retrofit的运行过程

上面讲的案例代码,返回了一个动态代理对象。而执行这段代码时,返回了一个OkHttpCall对象,拿到这个 Call 对象才能真正执行 HTTP 请求。

ApiManager apiService = retrofit.create(ApiManager.class); //2、retrofit对象创建一个API接口对象
Call<LoginResult> call = apiService.getData("lyk", "1234"); //返回响应接口回调

上面代码中 apiService 对象其实是一个动态代理对象。当 apiService 对象调用 getData方法 时会被动态代理拦截,然后调用 Proxy.newProxyInstance 方法中的 InvocationHandler 对象, 创建一个 ServiceMethod对象:

ServiceMethod serviceMethod = loadServiceMethod(method);
OkHttpCall okHttpCall = new OkHttpCall<>(serviceMethod, args);
return serviceMethod.callAdapter.adapt(okHttpCall);

创建ServiceMethod

刚才说到 ServiceMethod 是服务于方法的,具体来看一下创建这个ServiceMethod的过程是怎么样的:
首先,获取到上面说到的 Retrofit的接口:

callAdapter = createCallAdapter();
responseType = callAdapter.responseType();
responseConverter = createResponseConverter();

然后,解析Method方法的注解,其实就是想获取Http请求的方法。比如请求方法是GET还是POST形式,如果没有程序就会报错。还会做一系列的检查,比如在方法上注解了@Multipart,但是Http请求方法是GET,同样也会报错。

for (Annotation annotation : methodAnnotations) {
    parseMethodAnnotation(annotation);
}

if (httpMethod == null) {
   throw methodError("HTTP method annotation is required (e.g., @GET, @POST, etc.).");
}

其次,比如上面 apiService 接口的方法中带有参数{name,password},这都占位符,而参数值是在Java方法调用中传入的。那么 Retrofit 会使用一个 ParameterHandler 来进行替换:

int parameterCount = parameterAnnotationsArray.length;
parameterHandlers = new ParameterHandler<?>[parameterCount];

最后,ServiceMethod 还会做其他的检查。比如用了 @FormUrlEncoded 注解,那么方法参数中必须至少有一个 @Field 或 @FieldMap。

执行Http请求

之前讲到,OkHttpCall是实现了Call接口的,并且是真正调用 OkHttp3 发送Http请求的类。OkHttp3发送一个Http请求需要一个Request对象,而这个Request对象就是从 ServiceMethod 的 toRequest 返回的。

总之,OkHttpCall 就是调用 ServiceMethod 获得一个可以执行的 Request 对象,然后等到 Http 请求返回后,再将 response body 传入 ServiceMethod 中,ServiceMethod 就可以调用 Converter 接口将 response body 转成一个Java对象。

综上所述,ServiceMethod 中几乎保存了一个api请求所有需要的数据,OkHttpCall需要从ServiceMethod中获得一个Request对象,然后得到response后,还需要传入 ServiceMethod 用 Converter 转换成Java对象。

你可能会觉得我只要发送一个HTTP请求,你要做这么多事情不会很“慢”吗?不会很浪费性能吗?
我觉得,首先现在手机处理器主频非常高了,解析这个接口可能就花1ms可能更少的时间(我没有测试过),面对一个HTTP本来就需要几百ms,甚至几千ms来说不值得一提;而且Retrofit会对解析过的请求进行缓存,就在Map<Method, ServiceMethod> serviceMethodCache = new LinkedHashMap<>()这个对象中

总结

Retrofit非常巧妙的用注解来描述一个HTTP请求,将一个HTTP请求抽象成一个Java接口,然后用了Java动态代理的方式,动态的将这个接口的注解“翻译”成一个HTTP请求,最后再执行这个HTTP请求

Retrofit的功能非常多的依赖Java反射,代码中其实还有很多细节,比如异常的捕获、抛出和处理,大量的Factory设计模式(为什么要这么多使用Factory模式?)

Retrofit中接口设计的恰到好处,在你创建Retrofit对象时,让你有更多更灵活的方式去处理你的需求,比如使用不同的Converter、使用不同的CallAdapter,这也就提供了你使用RxJava来调用Retrofit的可能

参考资料

Android Retrofit 2.0
https://blog.csdn.net/jiankeufo/article/details/73186929
https://www.jianshu.com/p/2e8b400909b7

JAVA中的CAS

发表于 2019-03-29 | 分类于 同步

无锁的概念

加锁是一种悲观策略,无锁是一种乐观策略,因为对于加锁的并发程序来说,它们总是认为每次访问共享资源时总会发生冲突,因此必须对每一次数据操作实施加锁策略。而无锁则总是假设对共享资源的访问没有冲突,线程可以不停执行,无需加锁,无需等待,一旦发现冲突,无锁策略则采用一种称为CAS的技术来保证线程执行的安全性,这项CAS技术就是无锁策略实现的关键。

CAS

CAS的全称是Compare And Swap 即比较交换,其算法核心思想如下:

执行函数:CAS(V,E,N)

其包含3个参数:
V表示要更新的变量
E表示预期值
N表示新值
如果V值等于E值,则将V的值设为N。若V值和E值不同,则说明已经有其他线程做了更新,则当前线程什么都不做。通俗的理解就是CAS操作需要我们提供一个期望值,当期望值与当前线程的变量值相同时,说明还没线程修改该值,当前线程可以进行修改,也就是执行CAS操作,但如果期望值与当前线程不符,则说明该值已被其他线程修改,此时不执行更新操作,但可以选择重新读取该变量再尝试再次修改该变量,也可以放弃操作,原理图如下:

示例如下:

//加一并返回值
public final int incrementAndGet() {
        for (;;) {
            int current = get();
            int next = current + 1;
            if (compareAndSet(current, next))
                return next;
        }
   }

//返回CAS操作成功与否
public final boolean compareAndSet(int expect, int update) {
        //根据变量在内存中的偏移地址valueOffset获取原值,然后和预期值except进行比,如果符合,用update值进行更新,这个过程是原子操作
        return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
    }

如果此时有两个线程,线程A得到current值为1,线程B得到current值也为2,此时线程A执行CAS操作,成功将值改为2,而此时线程B执行CAS操作,发现此时内存中的值并不是读到current值1,所以返回false,此时线程B继续进行循环,最后成功加1

CAS的原子性

或许我们可能会有这样的疑问,假设存在多个线程执行CAS操作并且CAS的步骤很多,有没有可能在判断V和E相同后,正要赋值时,切换了线程,更改了值。造成了数据不一致呢?答案是否定的,因为CAS是一种系统原语,原语属于操作系统用语范畴,是由若干条指令组成的,用于完成某个功能的一个过程,并且原语的执行必须是连续的,在执行过程中不允许被中断,也就是说CAS是一条CPU的原子指令,不会造成所谓的数据不一致问题。

Unsafe类

Unsafe类存在于sun.misc包中,其内部方法操作可以像C的指针一样直接操作内存,单从名称看来就可以知道该类是非安全的,毕竟Unsafe拥有着类似于C的指针操作,因此总是不应该首先使用Unsafe类,Java官方也不建议直接使用的Unsafe类,但我们还是很有必要了解该类,因为Java中CAS操作的执行依赖于Unsafe类的方法,注意Unsafe类中的所有方法都是native修饰的,也就是说Unsafe类中的方法都直接调用操作系统底层资源执行相应任务
CAS是一些CPU直接支持的指令,也就是我们前面分析的无锁操作,在Java中无锁操作CAS基于以下3个方法实现:

//第一个参数o为给定对象,offset为对象内存的偏移量,通过这个偏移量迅速定位字段并设置或获取该字段的值,
//expected表示期望值,x表示要设置的值,下面3个方法都通过CAS原子指令执行操作。
public final native boolean compareAndSwapObject(Object o, long offset,Object expected, Object x);                                                                                                  

public final native boolean compareAndSwapInt(Object o, long offset,int expected,int x);

public final native boolean compareAndSwapLong(Object o, long offset,long expected,long x);

参考资料

JAVA中的CAS

Java中使用RSA/AES加解密

发表于 2019-03-29 | 分类于 HTTP

RSA加密明文最大长度245字节,解密要求密文最大长度为256字节,所以在加密和解密的过程中需要分块进行。(RSA密钥长度随着保密级别提高,增加很快)
RSA加密对明文的长度是有限制的,如果加密数据过大会抛出如下异常:

Exception in thread "main" javax.crypto.IllegalBlockSizeException: Data must not be longer than 117 bytes  
at com.sun.crypto.provider.RSACipher.a(DashoA13*..)  
at com.sun.crypto.provider.RSACipher.engineDoFinal(DashoA13*..)  
at javax.crypto.Cipher.doFinal(DashoA13*..) 

1.密钥长度
rsa算法初始化的时候一般要填入密钥长度,在96-2048bits间
(1)为啥下限是96bits(12bytes)?因为加密1byte的明文,需要至少1+11=12bytes的密钥(不懂?看下面的明文长度),低于下限96bits时,一个byte都加密不了,当然没意义啦
(2)为啥上限是2048(256bytes)?这是算法本身决定的。另RSA密钥长度随着保密级别提高,增加很快

2.明文长度
明文长度(bytes) <= 密钥长度(bytes)-11.这样的话,对于上限密钥长度1024bits能加密的明文上限就是117bytes了.
所以就出现了分片加密,网上很流行这个版本.很简单,如果明文长度大于那个最大明文长度了,我就分片吧,保证每片都别超过那个值就是了.
片数=(明文长度(bytes)/(密钥长度(bytes)-11))的整数部分+1,就是不满一片的按一片算

3.密文长度
密文长度等于密钥长度.当然这是不分片情况下的.
分片后,密文长度=密钥长度*片数

例如96bits的密钥,明文4bytes
每片明文长度=96/8-11=1byte,片数=4,密文长度=96/8*4=48bytes

又例如128bits的密钥,明文8bytes
每片明文长度=128/8-11=5bytes,片数=8/5取整+1=2,密文长度=128/8*2=32

注意,对于指定长度的明文,其密文长度与密钥长度非正比关系.如4bytes的明文,在最短密钥96bites是,密文长度48bytes,128bits米密钥时,密文长度为16bytes,1024bits密钥时,密文长度128bytes.
因为分片越多,密文长度显然会变大,所以有人说,那就一直用1024bits的密钥吧…拜托,现在的机器算1024bits的密钥还是要点时间滴,别以为你的cpu很牛逼…那么选个什么值比较合适呢?个人认为是600bits,因为我们对于一个字符串的加密,一般不是直接加密,而是将字符串hash 后,对hash值加密.现在的hash值一般都是4bytes,很少有8bytes,几十年内应该也不会超过64bytes.那就用64bytes算吧, 密钥长度就是(64+11)*8=600bits了.

RSAUtils.java

package security;

import java.io.ByteArrayOutputStream;
import java.security.Key;
import java.security.KeyFactory;
import java.security.KeyPair;
import java.security.KeyPairGenerator;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.security.Signature;
import java.security.interfaces.RSAPrivateKey;
import java.security.interfaces.RSAPublicKey;
import java.security.spec.PKCS8EncodedKeySpec;
import java.security.spec.X509EncodedKeySpec;
import java.util.HashMap;
import java.util.Map;

import javax.crypto.Cipher;

/** *//**
 * RSA公钥/私钥/签名工具包
 * 
 * 罗纳德·李维斯特(Ron [R]ivest)、阿迪·萨莫尔(Adi [S]hamir)和伦纳德·阿德曼(Leonard [A]dleman)
 * 
 * 字符串格式的密钥在未在特殊说明情况下都为BASE64编码格式<br/>
 * 由于非对称加密速度极其缓慢,一般文件不使用它来加密而是使用对称加密,<br/>
 * 非对称加密算法可以用来对对称加密的密钥加密,这样保证密钥的安全也就保证了数据的安全
 * 
 * @author IceWee
 * @date 2012-4-26
 * @version 1.0
 */
public class RSAUtils {

    /** *//**
     * 加密算法RSA
     */
    public static final String KEY_ALGORITHM = "RSA";

    /** *//**
     * 签名算法
     */
    public static final String SIGNATURE_ALGORITHM = "MD5withRSA";

    /** *//**
     * 获取公钥的key
     */
    private static final String PUBLIC_KEY = "RSAPublicKey";

    /** *//**
     * 获取私钥的key
     */
    private static final String PRIVATE_KEY = "RSAPrivateKey";

    /** *//**
     * RSA最大加密明文大小
     */
    private static final int MAX_ENCRYPT_BLOCK = 117;

    /** *//**
     * RSA最大解密密文大小
     */
    private static final int MAX_DECRYPT_BLOCK = 128;

    /** *//**
     * 生成密钥对(公钥和私钥)
     * 
     * @return
     * @throws Exception
     */
    public static Map<String, Object> genKeyPair() throws Exception {
        KeyPairGenerator keyPairGen = KeyPairGenerator.getInstance(KEY_ALGORITHM);
        keyPairGen.initialize(1024);
        KeyPair keyPair = keyPairGen.generateKeyPair();
        RSAPublicKey publicKey = (RSAPublicKey) keyPair.getPublic();
        RSAPrivateKey privateKey = (RSAPrivateKey) keyPair.getPrivate();
        Map<String, Object> keyMap = new HashMap<String, Object>(2);
        keyMap.put(PUBLIC_KEY, publicKey);
        keyMap.put(PRIVATE_KEY, privateKey);
        return keyMap;
    }

    /** *//**
     * 用私钥对信息生成数字签名
     * 
     * @param data 已加密数据
     * @param privateKey 私钥(BASE64编码)
     * 
     * @return
     * @throws Exception
     */
    public static String sign(byte[] data, String privateKey) throws Exception {
        byte[] keyBytes = Base64Utils.decode(privateKey);
        PKCS8EncodedKeySpec pkcs8KeySpec = new PKCS8EncodedKeySpec(keyBytes);
        KeyFactory keyFactory = KeyFactory.getInstance(KEY_ALGORITHM);
        PrivateKey privateK = keyFactory.generatePrivate(pkcs8KeySpec);
        Signature signature = Signature.getInstance(SIGNATURE_ALGORITHM);
        signature.initSign(privateK);
        signature.update(data);
        return Base64Utils.encode(signature.sign());
    }

    /** *//**
     * 校验数字签名
     * 
     * @param data 已加密数据
     * @param publicKey 公钥(BASE64编码)
     * @param sign 数字签名
     * 
     * @return
     * @throws Exception
     * 
     */
    public static boolean verify(byte[] data, String publicKey, String sign)
            throws Exception {
        byte[] keyBytes = Base64Utils.decode(publicKey);
        X509EncodedKeySpec keySpec = new X509EncodedKeySpec(keyBytes);
        KeyFactory keyFactory = KeyFactory.getInstance(KEY_ALGORITHM);
        PublicKey publicK = keyFactory.generatePublic(keySpec);
        Signature signature = Signature.getInstance(SIGNATURE_ALGORITHM);
        signature.initVerify(publicK);
        signature.update(data);
        return signature.verify(Base64Utils.decode(sign));
    }

    /** *//**
     * <P>
     * 私钥解密
     * </p>
     * 
     * @param encryptedData 已加密数据
     * @param privateKey 私钥(BASE64编码)
     * @return
     * @throws Exception
     */
    public static byte[] decryptByPrivateKey(byte[] encryptedData, String privateKey)
            throws Exception {
        byte[] keyBytes = Base64Utils.decode(privateKey);
        PKCS8EncodedKeySpec pkcs8KeySpec = new PKCS8EncodedKeySpec(keyBytes);
        KeyFactory keyFactory = KeyFactory.getInstance(KEY_ALGORITHM);
        Key privateK = keyFactory.generatePrivate(pkcs8KeySpec);
        Cipher cipher = Cipher.getInstance(keyFactory.getAlgorithm());
        cipher.init(Cipher.DECRYPT_MODE, privateK);
        int inputLen = encryptedData.length;
        ByteArrayOutputStream out = new ByteArrayOutputStream();
        int offSet = 0;
        byte[] cache;
        int i = 0;
        // 对数据分段解密
        while (inputLen - offSet > 0) {
            if (inputLen - offSet > MAX_DECRYPT_BLOCK) {
                cache = cipher.doFinal(encryptedData, offSet, MAX_DECRYPT_BLOCK);
            } else {
                cache = cipher.doFinal(encryptedData, offSet, inputLen - offSet);
            }
            out.write(cache, 0, cache.length);
            i++;
            offSet = i * MAX_DECRYPT_BLOCK;
        }
        byte[] decryptedData = out.toByteArray();
        out.close();
        return decryptedData;
    }

    /** *//**
     * <p>
     * 公钥解密
     * </p>
     * 
     * @param encryptedData 已加密数据
     * @param publicKey 公钥(BASE64编码)
     * @return
     * @throws Exception
     */
    public static byte[] decryptByPublicKey(byte[] encryptedData, String publicKey)
            throws Exception {
        byte[] keyBytes = Base64Utils.decode(publicKey);
        X509EncodedKeySpec x509KeySpec = new X509EncodedKeySpec(keyBytes);
        KeyFactory keyFactory = KeyFactory.getInstance(KEY_ALGORITHM);
        Key publicK = keyFactory.generatePublic(x509KeySpec);
        Cipher cipher = Cipher.getInstance(keyFactory.getAlgorithm());
        cipher.init(Cipher.DECRYPT_MODE, publicK);
        int inputLen = encryptedData.length;
        ByteArrayOutputStream out = new ByteArrayOutputStream();
        int offSet = 0;
        byte[] cache;
        int i = 0;
        // 对数据分段解密
        while (inputLen - offSet > 0) {
            if (inputLen - offSet > MAX_DECRYPT_BLOCK) {
                cache = cipher.doFinal(encryptedData, offSet, MAX_DECRYPT_BLOCK);
            } else {
                cache = cipher.doFinal(encryptedData, offSet, inputLen - offSet);
            }
            out.write(cache, 0, cache.length);
            i++;
            offSet = i * MAX_DECRYPT_BLOCK;
        }
        byte[] decryptedData = out.toByteArray();
        out.close();
        return decryptedData;
    }

    /** *//**
     * <p>
     * 公钥加密
     * </p>
     * 
     * @param data 源数据
     * @param publicKey 公钥(BASE64编码)
     * @return
     * @throws Exception
     */
    public static byte[] encryptByPublicKey(byte[] data, String publicKey)
            throws Exception {
        byte[] keyBytes = Base64Utils.decode(publicKey);
        X509EncodedKeySpec x509KeySpec = new X509EncodedKeySpec(keyBytes);
        KeyFactory keyFactory = KeyFactory.getInstance(KEY_ALGORITHM);
        Key publicK = keyFactory.generatePublic(x509KeySpec);
        // 对数据加密
        Cipher cipher = Cipher.getInstance(keyFactory.getAlgorithm());
        cipher.init(Cipher.ENCRYPT_MODE, publicK);
        int inputLen = data.length;
        ByteArrayOutputStream out = new ByteArrayOutputStream();
        int offSet = 0;
        byte[] cache;
        int i = 0;
        // 对数据分段加密
        while (inputLen - offSet > 0) {
            if (inputLen - offSet > MAX_ENCRYPT_BLOCK) {
                cache = cipher.doFinal(data, offSet, MAX_ENCRYPT_BLOCK);
            } else {
                cache = cipher.doFinal(data, offSet, inputLen - offSet);
            }
            out.write(cache, 0, cache.length);
            i++;
            offSet = i * MAX_ENCRYPT_BLOCK;
        }
        byte[] encryptedData = out.toByteArray();
        out.close();
        return encryptedData;
    }

    /** *//**
     * <p>
     * 私钥加密
     * </p>
     * 
     * @param data 源数据
     * @param privateKey 私钥(BASE64编码)
     * @return
     * @throws Exception
     */
    public static byte[] encryptByPrivateKey(byte[] data, String privateKey)
            throws Exception {
        byte[] keyBytes = Base64Utils.decode(privateKey);
        PKCS8EncodedKeySpec pkcs8KeySpec = new PKCS8EncodedKeySpec(keyBytes);
        KeyFactory keyFactory = KeyFactory.getInstance(KEY_ALGORITHM);
        Key privateK = keyFactory.generatePrivate(pkcs8KeySpec);
        Cipher cipher = Cipher.getInstance(keyFactory.getAlgorithm());
        cipher.init(Cipher.ENCRYPT_MODE, privateK);
        int inputLen = data.length;
        ByteArrayOutputStream out = new ByteArrayOutputStream();
        int offSet = 0;
        byte[] cache;
        int i = 0;
        // 对数据分段加密
        while (inputLen - offSet > 0) {
            if (inputLen - offSet > MAX_ENCRYPT_BLOCK) {
                cache = cipher.doFinal(data, offSet, MAX_ENCRYPT_BLOCK);
            } else {
                cache = cipher.doFinal(data, offSet, inputLen - offSet);
            }
            out.write(cache, 0, cache.length);
            i++;
            offSet = i * MAX_ENCRYPT_BLOCK;
        }
        byte[] encryptedData = out.toByteArray();
        out.close();
        return encryptedData;
    }

    /** *//**
     * <p>
     * 获取私钥
     * </p>
     * 
     * @param keyMap 密钥对
     * @return
     * @throws Exception
     */
    public static String getPrivateKey(Map<String, Object> keyMap)
            throws Exception {
        Key key = (Key) keyMap.get(PRIVATE_KEY);
        return Base64Utils.encode(key.getEncoded());
    }

    /** *//**
     * <p>
     * 获取公钥
     * </p>
     * 
     * @param keyMap 密钥对
     * @return
     * @throws Exception
     */
    public static String getPublicKey(Map<String, Object> keyMap)
            throws Exception {
        Key key = (Key) keyMap.get(PUBLIC_KEY);
        return Base64Utils.encode(key.getEncoded());
    }

}

Base64Utils.java

package security;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.InputStream;
import java.io.OutputStream;

import it.sauronsoftware.base64.Base64;

/** *//**
 * <p>
 * BASE64编码解码工具包
 * </p>
 * <p>
 * 依赖javabase64-1.3.1.jar
 * </p>
 * 
 * @author IceWee
 * @date 2012-5-19
 * @version 1.0
 */
public class Base64Utils {

    /** *//**
     * 文件读取缓冲区大小
     */
    private static final int CACHE_SIZE = 1024;

    /** *//**
     * <p>
     * BASE64字符串解码为二进制数据
     * </p>
     * 
     * @param base64
     * @return
     * @throws Exception
     */
    public static byte[] decode(String base64) throws Exception {
        return Base64.decode(base64.getBytes());
    }

    /** *//**
     * <p>
     * 二进制数据编码为BASE64字符串
     * </p>
     * 
     * @param bytes
     * @return
     * @throws Exception
     */
    public static String encode(byte[] bytes) throws Exception {
        return new String(Base64.encode(bytes));
    }

    /** *//**
     * <p>
     * 将文件编码为BASE64字符串
     * </p>
     * <p>
     * 大文件慎用,可能会导致内存溢出
     * </p>
     * 
     * @param filePath 文件绝对路径
     * @return
     * @throws Exception
     */
    public static String encodeFile(String filePath) throws Exception {
        byte[] bytes = fileToByte(filePath);
        return encode(bytes);
    }

    /** *//**
     * <p>
     * BASE64字符串转回文件
     * </p>
     * 
     * @param filePath 文件绝对路径
     * @param base64 编码字符串
     * @throws Exception
     */
    public static void decodeToFile(String filePath, String base64) throws Exception {
        byte[] bytes = decode(base64);
        byteArrayToFile(bytes, filePath);
    }

    /** *//**
     * <p>
     * 文件转换为二进制数组
     * </p>
     * 
     * @param filePath 文件路径
     * @return
     * @throws Exception
     */
    public static byte[] fileToByte(String filePath) throws Exception {
        byte[] data = new byte[0];
        File file = new File(filePath);
        if (file.exists()) {
            FileInputStream in = new FileInputStream(file);
            ByteArrayOutputStream out = new ByteArrayOutputStream(2048);
            byte[] cache = new byte[CACHE_SIZE];
            int nRead = 0;
            while ((nRead = in.read(cache)) != -1) {
                out.write(cache, 0, nRead);
                out.flush();
            }
            out.close();
            in.close();
            data = out.toByteArray();
         }
        return data;
    }

    /** *//**
     * <p>
     * 二进制数据写文件
     * </p>
     * 
     * @param bytes 二进制数据
     * @param filePath 文件生成目录
     */
    public static void byteArrayToFile(byte[] bytes, String filePath) throws Exception {
        InputStream in = new ByteArrayInputStream(bytes);   
        File destFile = new File(filePath);
        if (!destFile.getParentFile().exists()) {
            destFile.getParentFile().mkdirs();
        }
        destFile.createNewFile();
        OutputStream out = new FileOutputStream(destFile);
        byte[] cache = new byte[CACHE_SIZE];
        int nRead = 0;
        while ((nRead = in.read(cache)) != -1) {   
            out.write(cache, 0, nRead);
            out.flush();
        }
        out.close();
        in.close();
    }


}

RSATester.java

package security;

import java.util.Map;

public class RSATester {

    static String publicKey;
    static String privateKey;

    static {
        try {
            Map<String, Object> keyMap = RSAUtils.genKeyPair();
            publicKey = RSAUtils.getPublicKey(keyMap);
            privateKey = RSAUtils.getPrivateKey(keyMap);
            System.err.println("公钥: \n\r" + publicKey);
            System.err.println("私钥: \n\r" + privateKey);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    public static void main(String[] args) throws Exception {
        test();
        testSign();
    }

    static void test() throws Exception {
        System.err.println("公钥加密——私钥解密");
        String source = "这是一行没有任何意义的文字,你看完了等于没看,不是吗?";
        System.out.println("\r加密前文字:\r\n" + source);
        byte[] data = source.getBytes();
        byte[] encodedData = RSAUtils.encryptByPublicKey(data, publicKey);
        System.out.println("加密后文字:\r\n" + new String(encodedData));
        byte[] decodedData = RSAUtils.decryptByPrivateKey(encodedData, privateKey);
        String target = new String(decodedData);
        System.out.println("解密后文字: \r\n" + target);
    }

    static void testSign() throws Exception {
        System.err.println("私钥加密——公钥解密");
        String source = "这是一行测试RSA数字签名的无意义文字";
        System.out.println("原文字:\r\n" + source);
        byte[] data = source.getBytes();
        byte[] encodedData = RSAUtils.encryptByPrivateKey(data, privateKey);
        System.out.println("加密后:\r\n" + new String(encodedData));
        byte[] decodedData = RSAUtils.decryptByPublicKey(encodedData, publicKey);
        String target = new String(decodedData);
        System.out.println("解密后: \r\n" + target);
        System.err.println("私钥签名——公钥验证签名");
        String sign = RSAUtils.sign(encodedData, privateKey);
        System.err.println("签名:\r" + sign);
        boolean status = RSAUtils.verify(encodedData, publicKey, sign);
        System.err.println("验证结果:\r" + status);
    }

}

AES对称加密和解密

package demo.security;

import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import java.util.Base64;
import java.util.Scanner;

import javax.crypto.BadPaddingException;
import javax.crypto.Cipher;
import javax.crypto.IllegalBlockSizeException;
import javax.crypto.KeyGenerator;
import javax.crypto.NoSuchPaddingException;
import javax.crypto.SecretKey;
import javax.crypto.spec.SecretKeySpec;

import sun.misc.BASE64Decoder;
import sun.misc.BASE64Encoder;

/*
 * AES对称加密和解密
 */
public class SymmetricEncoder {
  /*
   * 加密
   * 1.构造密钥生成器
   * 2.根据ecnodeRules规则初始化密钥生成器
   * 3.产生密钥
   * 4.创建和初始化密码器
   * 5.内容加密
   * 6.返回字符串
   */
    public static String AESEncode(String encodeRules,String content){
        try {
            //1.构造密钥生成器,指定为AES算法,不区分大小写
            KeyGenerator keygen=KeyGenerator.getInstance("AES");
            //2.根据ecnodeRules规则初始化密钥生成器
            //生成一个128位的随机源,根据传入的字节数组
            keygen.init(128, new SecureRandom(encodeRules.getBytes()));
              //3.产生原始对称密钥
            SecretKey original_key=keygen.generateKey();
              //4.获得原始对称密钥的字节数组
            byte [] raw=original_key.getEncoded();
            //5.根据字节数组生成AES密钥
            SecretKey key=new SecretKeySpec(raw, "AES");
              //6.根据指定算法AES自成密码器
            Cipher cipher=Cipher.getInstance("AES");
              //7.初始化密码器,第一个参数为加密(Encrypt_mode)或者解密解密(Decrypt_mode)操作,第二个参数为使用的KEY
            cipher.init(Cipher.ENCRYPT_MODE, key);
            //8.获取加密内容的字节数组(这里要设置为utf-8)不然内容中如果有中文和英文混合中文就会解密为乱码
            byte [] byte_encode=content.getBytes("utf-8");
            //9.根据密码器的初始化方式--加密:将数据加密
            byte [] byte_AES=cipher.doFinal(byte_encode);
          //10.将加密后的数据转换为字符串
            //这里用Base64Encoder中会找不到包
            //解决办法:
            //在项目的Build path中先移除JRE System Library,再添加库JRE System Library,重新编译后就一切正常了。
            String AES_encode=new String(new BASE64Encoder().encode(byte_AES));
          //11.将字符串返回
            return AES_encode;
        } catch (NoSuchAlgorithmException e) {
            e.printStackTrace();
        } catch (NoSuchPaddingException e) {
            e.printStackTrace();
        } catch (InvalidKeyException e) {
            e.printStackTrace();
        } catch (IllegalBlockSizeException e) {
            e.printStackTrace();
        } catch (BadPaddingException e) {
            e.printStackTrace();
        } catch (UnsupportedEncodingException e) {
            e.printStackTrace();
        }

        //如果有错就返加nulll
        return null;         
    }
    /*
     * 解密
     * 解密过程:
     * 1.同加密1-4步
     * 2.将加密后的字符串反纺成byte[]数组
     * 3.将加密内容解密
     */
    public static String AESDncode(String encodeRules,String content){
        try {
            //1.构造密钥生成器,指定为AES算法,不区分大小写
            KeyGenerator keygen=KeyGenerator.getInstance("AES");
            //2.根据ecnodeRules规则初始化密钥生成器
            //生成一个128位的随机源,根据传入的字节数组
            keygen.init(128, new SecureRandom(encodeRules.getBytes()));
              //3.产生原始对称密钥
            SecretKey original_key=keygen.generateKey();
              //4.获得原始对称密钥的字节数组
            byte [] raw=original_key.getEncoded();
            //5.根据字节数组生成AES密钥
            SecretKey key=new SecretKeySpec(raw, "AES");
              //6.根据指定算法AES自成密码器
            Cipher cipher=Cipher.getInstance("AES");
              //7.初始化密码器,第一个参数为加密(Encrypt_mode)或者解密(Decrypt_mode)操作,第二个参数为使用的KEY
            cipher.init(Cipher.DECRYPT_MODE, key);
            //8.将加密并编码后的内容解码成字节数组
            byte [] byte_content= new BASE64Decoder().decodeBuffer(content);
            /*
             * 解密
             */
            byte [] byte_decode=cipher.doFinal(byte_content);
            String AES_decode=new String(byte_decode,"utf-8");
            return AES_decode;
        } catch (NoSuchAlgorithmException e) {
            e.printStackTrace();
        } catch (NoSuchPaddingException e) {
            e.printStackTrace();
        } catch (InvalidKeyException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        } catch (IllegalBlockSizeException e) {
            e.printStackTrace();
        } catch (BadPaddingException e) {
            e.printStackTrace();
        }

        //如果有错就返加nulll
        return null;         
    }

    public static void main(String[] args) {
        SymmetricEncoder se=new SymmetricEncoder();
        Scanner scanner=new Scanner(System.in);
        /*
         * 加密
         */
        System.out.println("使用AES对称加密,请输入加密的规则");
        String encodeRules=scanner.next();
        System.out.println("请输入要加密的内容:");
        String content = scanner.next();
        System.out.println("根据输入的规则"+encodeRules+"加密后的密文是:"+se.AESEncode(encodeRules, content));

        /*
         * 解密
         */
        System.out.println("使用AES对称解密,请输入加密的规则:(须与加密相同)");
         encodeRules=scanner.next();
        System.out.println("请输入要解密的内容(密文):");
         content = scanner.next();
        System.out.println("根据输入的规则"+encodeRules+"解密后的明文是:"+se.AESDncode(encodeRules, content));
    }

}

测试结果:

使用AES对称加密,请输入加密的规则
使用AES对称加密
请输入要加密的内容:
使用AES对称加密
根据输入的规则使用AES对称加密加密后的密文是:Z0NwrNPHghgXHN0CqjLS58YCjhMcBfeR33RWs7Lw+AY=
使用AES对称解密,请输入加密的规则:(须与加密相同)
使用AES对称加密
请输入要解密的内容(密文):
Z0NwrNPHghgXHN0CqjLS58YCjhMcBfeR33RWs7Lw+AY=
根据输入的规则使用AES对称加密解密后的明文是:使用AES对称加密

参考资料

java RSA加密解密实现(含分段加密)
AES对称加密和解密
Android: AndroidKeyStore 非对称RSA加密解密
https://www.cnblogs.com/zuge/p/5430362.html

android dagger2使用心得

发表于 2019-03-22 | 分类于 Android知识点

Dagger 2 是 Java 和 Android 下的一个完全静态、编译时生成代码的依赖注入框架,由 Google 维护,早期的版本 Dagger 是由 Square 创建的。

Dagger 2 是基于 Java Specification Request(JSR) 330标准。利用 JSR 注解在编译时生成代码,来注入实例完成依赖注入。

Dagger2 几个注解

  • @Inject 带有此注解的属性或构造方法将参与到依赖注入中,Dagger2会实例化有此注解的类。标注的成员属性不能是private

  • @Module 带有此注解的类,用来提供依赖,里面定义一些用@Provides注解的以provide开头的方法,这些方法就是所提供的依赖,Dagger2会在该类中寻找实例化某个类所需要的依赖。

  • @Component 用来将@Inject和@Module联系起来的桥梁,从@Module中获取依赖并将依赖注入给@Inject。只能标注接口或抽象类,声明的注入接口的参数类型必须和目标类一致。

约定俗成的是@Provides方法一般以provide为前缀,Moudle 类以Module为后缀,一个 Module 类中可以有多个@Provides方法。

模块与模块之间的联系

1
2
3
4
5
6
7
@Module (includes = {BModule.class})// includes 引入)
public class AModule {
@Provides
A providerA() {
return new A();
}
}

一个Component 应用多个 module

1
2
3
4
@Component(modules = {AModule.class,BModule.class})
public interface MainComponent {
void inject(MainActivity activity);
}

dependencies 依赖其他Component

1
2
3
4
@Component(modules = {MainModule.class}, dependencies = AppConponent.class)
public interface MainConponent {
void inject(MainActivity activity);
}

Provider 注入

有时候不仅仅是注入单个实例,我们需要多个实例,这时可以使用注入Provider,每次调用它的 get() 方法都会调用到 @Inject 构造函数创建新实例或者 Module 的 provide 方法返回实例。

1
2
3
4
5
6
7
8
9
10
11
12
13
public class CarFactory {
@Inject
Provider<Car> carProvider;

public List<Car> makeCar(int num) {
...
List<Car> carList = new ArrayList<Car>(num);
for (int i = 0; i < num; i ++) {
carList.add(carProvider.get());
}
return carList;
}
}

Lazy 延迟注入

有时我们想注入的依赖在使用时再完成初始化,加快加载速度,就可以使用注入Lazy。只有在调用 Lazy 的 get() 方法时才会初始化依赖实例注入依赖。

1
2
3
4
5
6
7
8
9
10
public class Man {
@Inject
Lazy<Car> lazyCar;

public void goWork() {
...
lazyCar.get().go(); // lazyCar.get() 返回 Car 实例
...
}
}

Qualifier(限定符-别名)

试想这样一种情况:沿用之前的 Man 和 Car 的例子,如果 CarModule 提供了两个生成 Car 实例的 provide 方法,Dagger 2 在注入 Car 实例到 Man 中时应该选择哪一个方法呢?

1
2
3
4
5
6
7
8
9
10
11
12
@Module
public class CarModule {
@Provides
static Car provideCar1() {
return new Car1();
}
@Provides
static Car provideCar2() {
return new Car2();
}
// Car1 和 Car2 是 Car 的两个子类
}

这时 Dagger 2 不知道使用provideCar1还是provideCar2提供的实例,在编译时就会报错,这种情况也可以叫依赖迷失(网上看到的叫法)。而@Qualifier注解就是用来解决这个问题,使用注解来确定使用哪种 provide 方法。

下面是自定义的@Named注解,你也可以用自定义的其他 Qualifier 注解:

1
2
3
4
5
6
@Qualifier
@Documented
@Retention(RUNTIME)
public @interface Named {
String value() default "";
}

在 provide 方法上加上@Named注解,用来区分

1
2
3
4
5
6
7
8
9
10
11
12
13
@Module
public class CarModule {
@Provides
@Named("car1")
static Car provideCar1() {
return new Car1();
}
@Provides
@Named("car2")
static Car provideCar2() {
return new Car2();
}
}

还需要在 Inject 注入的地方加上@Named注解,表明需要注入的是哪一种 Car:

1
2
3
4
5
6
public class Man {
@Inject
@Named("car1")
Car car;
...
}

这样在依赖注入时,Dagger 2 就会使用provideCar1方法提供的实例,所以Qualifier(限定符)的作用相当于起了个区分的别名。

Scope

Scope 是用来确定注入的实例的生命周期的,如果没有使用 Scope 注解,Component 每次调用 Module 中的 provide 方法或 Inject 构造函数生成的工厂时都会创建一个新的实例,而使用 Scope 后可以复用之前的依赖实例。

在Dagger 2中
1、@Singleton可以保持类的单例。
2、@ApplicationScope注解的Component类与Applicaiton对象的生命周期一致。
3、@ActivityScope注解的Component类与Activity的生命周期一致
scope可以给我们带来“局部单例”,生命周期取决于scope自己。

在 Dagger 2 官方文档中我找到一句话,非常清楚地描述了@Scope的原理:
When a binding uses a scope annotation, that means that the component object holds a reference to the bound object until the component object itself is garbage-collected.
Scope 作用域的本质:Component 间接持有依赖实例的引用,把实例的作用域与 Component 绑定,它们不是同年同月同日生,但是同年同月死。

自定义@Scope

对于Android,我们通常会定义一个针对整个Activity的注解,通过仿照@Singleton

@Scope
@Documented
@Retention(RUNTIME)
public @interface ActivityScope {}

你可能会发现,这个自定义的@Scope和@Singleton代码完全一样,具有实现单例模式的功能。那干嘛还自定义@Scope,好处如下:

更好的管理ApplicationComponent和Module之间的关系,Component和Component之间的依赖和继承关系。如果关系不匹配,在编译期间会报错,详细下面会介绍。
代码可读性,让程序猿更好的了解Module中创建的类实例的使用范围。

Singleton Scope

@Singleton顾名思义保证单例

Reusable Scope

只单纯缓存依赖的实例,可以复用之前的实例,不关心与之绑定是什么 Component,Reusable 作用域只需要标记目标类或 provide 方法,不用标记 Component。

Releasable references(可释放引用)

使用 Scope 注解时,Component 会间接持有绑定的依赖实例的引用,也就是说实例在 Component 还存活时无法被回收。而在 Android 中,应该尽量减少内存占用,把没有使用的对象释放,这时可以用@CanReleaseReferences标记 Scope 注解。

然后在 Application 中注入ReleasableReferenceManager对象,在内存不足时调用releaseStrongReferences()方法把 Component 间接持有的强引用变为弱引用。

1
2
3
4
5
6
7
8
9
10
11
12
public class MyApplication extends Application {
@Inject
@ForReleasableReferences(MyScope.class)
ReleasableReferenceManager myScopeReferences;

@Override
public void onLowMemory() {
super.onLowMemory();
myScopeReferences.releaseStrongReferences();
}
...
}

这样在内存不足时,Component间接持有的实例为弱引用,如果没有其他对象使用的话就可以被回收。

使用@Scope的一些经验:

1、@Component关联的@Module中的任何一个@Provides有@scope,则该整个@Component要加上这个scope。否则在暴露或者注入时(不暴露且不注入时,既不使用它构造对象时,不报错),会有如下错误:

Error:(13, 1) 错误: cn.xuexuan.newui.di.component.ActivityComponent (unscoped) may not reference scoped bindings:
@Singleton @Provides android.app.Activity cn.xuexuan.newui.di.module.ActivityModule.getActivity()

2、 @Component的dependencies与@Component自身的scope不能相同;
@Singleton的组件不能依赖其他scope的组件,但是其他scope的组件可以依赖@Singleton组件;
没有scope的不能依赖有scope的组件。
否则出现下面错误:

Error:(21, 1) 错误: com.android.example.devsummit.archdemo.di.component.MyTestComponent (unscoped) cannot depend on scoped components:
@com.android.example.devsummit.archdemo.di.scope.ActivityScope com.android.example.devsummit.archdemo.di.component.MyTestComponentX

3、一个component不能同时有多个scope(Subcomponent除外),否则出现下面的错误

Error:Execution failed for task ‘:app:compileDebugJavaWithJavac’.
java.lang.IllegalArgumentException: com.android.example.devsummit.archdemo.di.component.MyTestComponent was annotated with more than one @Scope annotation

@BindsInstance

Component 可以在创建 Component 的时候绑定依赖实例,用以注入。这就是@BindsInstance注解的作用,只能在 Component.Builder 中使用。

@Module
public final class HomeActivityModule {
    private final HomeActivity activity;

    public HomeActivityModule(HomeActivity activity) {
        this.activity = activity;
    }

    @Provides
    @ActivityScope  // 自定义作用域
    Activity provideActivity() {
        return activity;
    }
}

而使用@BindsInstance的话会更加简单:

1
2
3
4
5
6
7
8
9
10
11
12
13
@ActivityScope
@Component
public interface HomeActivityComponent {

@Component.Builder
interface Builder {

@BindsInstance
Builder activity(Activity activity);

HomeActivityComponent build();
}
}

注意在调用build()创建 Component 之前,所有@BindsInstance方法必须先调用。上面例子中 HomeActivityComponent 还可以注入 Activity 类型的依赖,但是不能注入 HomeActivity,因为 Dagger 2 是使用具体类型作为依据的(也就是只能使用@Inject Activity activity而不是@Inject HomeActivity activity)。

如果@BindsInstance方法的参数可能为 null,需要再用@Nullable标记,同时标注 Inject 的地方也需要用@Nullable标记。这时 Builder 也可以不调用@BindsInstance方法,这样 Component 会默认设置 instance 为 null

注意:dagger.android 扩展库可以极大地简化在 Android 项目中使用 Dagger 2 的过程,但是还是有些限制,SubComponent.Builder 不能自定义 @BindsInstance 方法,SubCompoennt 的 Module 不能有含参数的构造函数,否则AndroidInjection.inject(this)在创建 SubComponent 时无法成功。

Component 的组织关系

Component 管理着依赖实例,根据依赖实例之间的关系就能确定 Component 的关系。这些关系可以用object graph描述,我称之为依赖关系图。在 Dagger 2 中 Component 的组织关系分为两种:

  • 依赖关系,一个 Component 依赖其他 Compoent 公开的依赖实例,用 Component 中的dependencies声明。

  • 继承关系,一个 Component 继承(也可以叫扩展)某 Component 提供更多的依赖,SubComponent 就是继承关系的体现。

依赖关系

示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
@ManScope
@Component(modules = CarModule.class)
public interface ManComponent {
void inject(Man man);

Car car(); //必须向外提供 car 依赖实例的接口,表明 Man 可以借 car 给别人
}

@FriendScope
@Component(dependencies = ManComponent.class)
public interface FriendComponent {
void inject(Friend friend);
}

ManComponent manComponent = DaggerManComponent.builder()
.build();

FriendComponent friendComponent = DaggerFriendComponent.builder()
.manComponent(manComponent)
.build();
friendComponent.inject(friend);

依赖关系就跟生活中的朋友关系相当,注意事项如下:

  1. 被依赖的 Component 需要把暴露的依赖实例用显式的接口声明,如上面的Car car(),我们只能使用朋友愿意分享的东西。

  2. 依赖关系中的 Component 的 Scope 不能相同,因为它们的生命周期不同。

继承关系

示例如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
@Module(subcomponents = SonComponent.class)
public class CarModule {
@Provides
@ManScope
static Car provideCar() {
return new Car();
}
}

@ManScope
@Component(modules = CarModule.class)
public interface ManComponent {
//void inject(Man man); // 继承关系中不用显式地提供暴露依赖实例的接口

SonComponent.Builder sonComponent(); // 用来创建 Subcomponent
}

@SonScope
@SubComponent(modules = BikeModule.class)
public interface SonComponent {
void inject(Son son);

@Subcomponent.Builder
interface Builder { // SubComponent 必须显式地声明 Subcomponent.Builder,parent Component 需要用 Builder 来创建 SubComponent
SonComponent build();
}
}

ManComponent manComponent = DaggerManComponent.builder()
.build();

SonComponent sonComponent = manComponent.sonComponent()
.build();
sonComponent.inject(son);

在 parent Component 依赖的 Module 中的subcomponents加上 SubComponent 的 class,然后就可以在 parent Component 中请求 SubComponent.Builder。但却无法访问 SubComponent 中的依赖。

继承关系和依赖关系最大的区别就是:继承关系中不用显式地提供依赖实例的接口,SubComponent 继承 parent Component 的所有依赖。

依赖关系 vs 继承关系

相同点:

  • 两者都能复用其他 Component 的依赖

  • 有依赖关系和继承关系的 Component 不能有相同的 Scope

区别:

  • 依赖关系中被依赖的 Component 必须显式地提供公开依赖实例的接口,而 SubComponent 默认继承 parent Component 的依赖。

  • 依赖关系会生成两个独立的 DaggerXXComponent 类,而 SubComponent 不会生成 独立的 DaggerXXComponent 类。

在 Android 开发中,Activity 是 App 运行中组件,Fragment 又是 Activity 一部分,这种组件化思想适合继承关系,所以在 Android 中一般使用 SubComponent。

重复的 Module

当相同的 Module 注入到 parent Component 和它的 SubComponent 中时,则每个 Component 都将自动使用这个 Module 的同一实例。也就是如果在 SubComponent.Builder 中调用相同的 Module 或者在返回 SubComponent 的抽象工厂方法中以重复 Module 作为参数时,会出现错误。(前者在编译时不能检测出,是运行时错误)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
@Component(modules = {RepeatedModule.class, ...})
interface ComponentOne {
ComponentTwo componentTwo(RepeatedModule repeatedModule); // 编译时报错
ComponentThree.Builder componentThreeBuilder();
}

@Subcomponent(modules = {RepeatedModule.class, ...})
interface ComponentTwo { ... }

@Subcomponent(modules = {RepeatedModule.class, ...})
interface ComponentThree {
@Subcomponent.Builder
interface Builder {
Builder repeatedModule(RepeatedModule repeatedModule);
ComponentThree build();
}
}

DaggerComponentOne.create().componentThreeBuilder()
.repeatedModule(new RepeatedModule()) // 运行时报错 UnsupportedOperationException!
.build();

dagger.android 扩展库的使用

dagger.android 扩展库是为了简化 Dagger 2 在 Android 的使用。

引入 dagger.android 扩展库

1
2
3
4
5
6
7
8
// dagger 2
implementation "com.google.dagger:dagger:$dagger_version"
kapt "com.google.dagger:dagger-compiler:$dagger_version"

// dagger.android
implementation "com.google.dagger:dagger-android:$dagger_version"
implementation "com.google.dagger:dagger-android-support:$dagger_version"
kapt "com.google.dagger:dagger-android-processor:$dagger_version"

从上面可以看出 dagger.android 扩展库有单独的注解处理器 dagger-android-processor。

注入 Activity 中的依赖

以 SearchActivity 为例,说明 dagger.android 的使用:

1.在 AppComponent 中安装 AndroidInjectionModule,确保包含四大组件和 Fragment 的注入器类型。

1
2
3
@Singleton
@Component(modules = [AppModule::class, AndroidInjectionModule::class])
interface AppComponent { ... }

2.Activity 对应的 SubComponent 实现 AndroidInjector 接口,对应的 @Subcomponent.Builder 继承 AndroidInjector.Builder。

1
2
3
4
5
6
@ActivityScope
@Subcomponent
interface SearchActivitySubcomponent : AndroidInjector<SearchActivity> {
@Subcomponent.Builder
abstract class Builder : AndroidInjector.Builder<SearchActivity>()
}

3.在定义 SubComponent 后,添加一个 ActivityBindModule 用来绑定 subcomponent builder,并把该 module 安装到 AppComponent 中。

1
2
3
4
5
6
7
8
9
10
11
12
@Module(subcomponents = [SearchActivitySubcomponent::class])
abstract class ActivityBindModule {
@Binds
@IntoMap
@ActivityKey(SearchActivity::class)
abstract fun bindAndroidInjectorFactory(
builder: SearchActivitySubcomponent.Builder): AndroidInjector.Factory<out Activity>
}

@Singleton
@Component(modules = [AppModule::class, AndroidInjectionModule::class, ActivityBindModule::class])
interface AppComponent { ... }

如果 SubComponent 和其 Builder 没有其他方法或没有继承其他类型,可以使用 @ContributesAndroidInjector 注解简化第二步和第三步,在一个抽象 Module 中添加一个使用 @ContributesAndroidInjector 注解标记的返回具体的 Activity 类型的抽象方法,还可以在 ContributesAndroidInjector 注解中标明 SubComponent 需要安装的 module。如果 SubComponent 需要作用域,只需要标记在该方法上即可。

1
2
3
4
5
6
7
8
9
10
@Module
abstract class ActivityBindModule {
@ActivityScope
@ContributesAndroidInjector
abstract fun searchActivityInjector(): SearchActivity
}

@Singleton
@Component(modules = [AppModule::class, AndroidInjectionModule::class, ActivityBindModule::class])
interface AppComponent { ... }

4.Application 类实现 HasActivityInjector 接口,并且注入一个 DispatchingAndroidInjector 类型的依赖作为 activityInjector() 方法的返回值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class GankApp : Application(), HasActivityInjector {

@Inject
lateinit var dispatchingActivityInjector: DispatchingAndroidInjector<Activity>

override fun onCreate() {
super.onCreate()
DaggerAppComponent.builder()
.appModule(AppModule(this))
.build()
.inject(this)
}

override fun activityInjector() = dispatchingActivityInjector
}

5.最后在 onCreate)() 方法中,在 super.onCreate() 之前调用 AndroidInjection.inject(this)。

1
2
3
4
5
6
7
8
9
class SearchActivity : BaseActivity() {

override fun onCreate(savedInstanceState: Bundle?) {
AndroidInjection.inject(this)
super.onCreate(savedInstanceState)

...
}
}

使用 dagger.android 后 Activity 对应的 SubComponent 的定义简化如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Module
abstract class ActivityBindModule {
@ActivityScope
@ContributesAndroidInjector(modules = [FragmentBindModule::class])
abstract fun mainActivityInjector(): MainActivity

@ActivityScope
@ContributesAndroidInjector
abstract fun pictureActivityInjector(): PictureActivity

@ActivityScope
@ContributesAndroidInjector
abstract fun searchActivityInjector(): SearchActivity
}

其中 @ContributesAndroidInjector 注解可以解决之前的两个问题,不需要手动创建每一个 SubComponent,也不用在 parent componenet 声明对应的返回对应的 SubComponent.Builder 的接口,以后添加 SubComponent 只需要添加一个 ContributesAndroidInjector 抽象方法。

然后使用AndroidInjection.inject(this) 来简化注入依赖的过程,隐藏注入依赖的细节。

注入 Fragment 和其他三大组件也是类似,只是 ContributesAndroidInjector 抽象方法的返回值变了,HasActivityInjector 接口换做 HasFragmentInjector 等接口。

小结

dagger.android 扩展库可以极大地简化在 Android 项目中使用 Dagger 2 的过程,但是还是有些限制,SubComponent.Builder 不能自定义 @BindsInstance 方法,SubCompoennt 的 Module 不能有含参数的构造函数,否则AndroidInjection.inject(this)在创建 SubComponent 时无法成功。

在使用过 dagger.android 扩展库一段时间后,个人认为其设计非常优雅,简化了 SubComponent 的定义过程和依赖注入的过程,使得开发者可以专注于 Module 管理依赖对象,所以建议大家在 Android 项目中使用。

@Binds

@Binds:可以理解为关联,首先它是跟@Provides使用地方是一样的,不同的在于@Provides 注解的方法都是有具体实现的,而@Binds修饰的只有方法定义,并没有具体的实现的,在方法定义中方法参数必须是 返回值的实现类。这样创建实体类的地方就不用在Modules 中实现了,例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
@Module
public abstract class AppModule {

@Binds
@Named("AppManager")
abstract IAppManager bindAppManager(AppManager appManager);

@Provides
static Retrofit provideRetrofit() {
return new Retrofit.Builder().build();
}

}

@Component(modules = AppModule.class)
public interface AppComponent {
Application application();

Retrofit retrofit();

void inject(Application application);

@Component.Builder
interface Builder {

@BindsInstance
Builder application(Application application);

AppComponent build();
}
}

public class AppManager implements IAppManager {

@Inject
Application application;

@Inject
public AppManager() {

}

}

public class MyApplication extends Application {

@Override
public void onCreate() {
super.onCreate();

// 注入
DaggerAppComponent.builder().application(this).build().inject(this);
}
}

Module 中不一定要具体实现,可以用@Binds关联实体,这样在编译过程中会自动创建Fractory 以及实现的,AccountManagerDelegate中还可以使用该Module中 @Provides 提供的实体类

这里使用 provideXXX() 方式提供接口实例,说明几点:

  • 方法中必须有参数且只能有一个,是接口的实现类
  • 实现类必须提供@Inject的构造或 Module中@Provides形式提供
  • 方法是抽象方法,不必写方法体
  • 使用@Binds代替@Provides
  • 由于方法是抽象的,所以类也要抽象或接口

@IntoSet

使用注入形式初始化 Set 集合时,可以在 Module 中多次定义一系列返回值类型相同的方法:

1
2
3
4
5
6
7
8
9
10
@Module
class AnimalModule {
@IntoSet
@Provides
fun provideElephant() = Animal("大象")

@Provides
@IntoSet
fun provideMonkey() = Animal("猴子")
}

Animal.kt 代码:

1
class Animal(var name: String)

上面的 provideElephant() 和 provideMonkey() 返回值类型都是 Animal,都使用了@IntoSet注释,多说一句,@Provides当然也要有,如果是接口用@Binds。
初始化并使用:

1
2
3
4
5
6
7
8
@Inject
lateinit var set: MutableSet<Animal>

...//代码省略

set.forEach {
Log.i("Main", "--- ${it.name} ---")
}

kotlin 里面 MutableSet 代替 Java 中的 Set,上面代码运行可以输出:

1
2
--- 大象 ---
--- 猴子 ---

@IntoMap

与@IntoSet区别不大,Map 多了一个 key

1
2
3
4
5
6
7
8
9
@Provides
@IntoMap
@IntKey(0)
fun provideFish() = Animal("鱼")

@Provides
@IntoMap
@IntKey(1)
fun provideHuman() = Animal("人")

@IntKey里面就是 Map 中的 key,providesXXX() 返回值是 key 对应的 value,如果 key 是 String 类型的,则使用@StringKey()输入 key,此外,还可以自定义 key:

1
2
3
4
5
@Documented
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@MapKey
annotation class ZhuangBiKey(val f: Float)

初始化 map 并使用:

1
2
3
4
5
6
7
8
@Inject
lateinit var map: Map<Int, Animal>

...//代码省略

map.forEach {
Log.i("Main", "--- key:${it.key}\nvalue:${it.value.name} ---")
}

参考资料

Dagger 2 完全解析
打破Dagger2使用窘境:Dagger-Android详解(https://github.com/qingmei2/Sample_dagger2)
dagger组件化

HTTP基础

发表于 2019-03-20 | 分类于 HTTP

HTTP(HyperText Transfer Protocol)超文本传输协议是互联网上应用最为广泛的一种网络协议。由于信息是明文传输,所以被认为是不安全的。

为了理解HTTP,我们有必要事先了解一下TCP/IP协议族。其是互联网相关联的协议集合的总称,通常使用的网络就是在TCP/IP协议族的基础上运作的,而HTTP属于它内部的一个子集,除此之外,还包括大家所熟知的FTP,DNS,TCP,UDP,IP等等协议。

OSI的七层协议


其核心思想就是把数据信息包装起来,即封装:发送端在层与层之间传输数据时,每经过一层时必定会被打上一个该层所属的首部信息。反之,接收端在层与层传输数据时,每经过一层时会把对应的首部消去。值得一提的是,层次化之后,设计也变得相对简单了。处于应用层上的应用可以只考虑分派给自己的任务,而不需要弄清对方在地球上哪个地方、对方的传输线路是怎样的、是否能确保传输送达等问题。

TPC/IP协议是传输层协议,主要解决数据如何在网络中传输,而HTTP是应用层协议,主要解决如何包装数据。WEB使用HTTP协议作应用层协议,以封装HTTP 文本信息,然后使用TCP/IP做传输层协议将它发到网络上。
下面的图表试图显示不同的TCP/IP和其他的协议在最初OSI(Open System Interconnect)模型中的位置:

HTTP 方法

下面的表格比较了两种 HTTP 方法:GET 和 POST

HTTP 请求方法

持久连接

HTTP 协议的初始版本中,每进行一个 HTTP 通信都要断开一次 TCP 连接。比如使用浏览器浏览一个包含多张图片的 HTML 页面时,在发送请求访问 HTML 页面资源的同时,也会请求该 HTML 页面里包含的其他资源。因此,每次的请求都会造成无畏的 TCP 连接建立和断开,增加通信量的开销。
为了解决上述 TCP 连接的问题,HTTP/1.1 和部分 HTTP/1.0 想出了持久连接的方法。其特点是,只要任意一端没有明确提出断开连接,则保持 TCP 连接状态。旨在建立一次 TCP 连接后进行多次请求和响应的交互。在 HTTP/1.1 中,所有的连接默认都是持久连接。

管线化

持久连接使得多数请求以管线化方式发送成为可能。以前发送请求后需等待并接收到响应,才能发送下一个请求。管线化技术出现后,不用等待亦可发送下一个请求。这样就能做到同时并行发送多个请求,而不需要一个接一个地等待响应了。
比如,当请求一个包含多张图片的 HTML 页面时,与挨个连接相比,用持久连接可以让请求更快结束。而管线化技术要比持久连接速度更快。请求数越多,时间差就越明显。

Cookie

HTTP 是一种无状态协议。协议自身不对请求和响应之间的通信状态进行保存。也就是说在 HTTP 这个级别,协议对于发送过的请求或响应都不做持久化处理。这是为了更快地处理大量事务,确保协议的可伸缩性,而特意把 HTTP 协议设计成如此简单的。
可是随着 Web 的不断发展,我们的很多业务都需要对通信状态进行保存。于是我们引入了 Cookie 技术。有了 Cookie 再用 HTTP 协议通信,就可以管理状态了。
Cookie 技术通过在请求和响应报文中写入 Cookie 信息来控制客户端的状态。Cookie 会根据从服务器端发送的响应报文内的一个叫做 Set-Cookie 的首部字段信息,通知客户端保存Cookie。当下次客户端再往该服务器发送请求时,客户端会自动在请求报文中加入 Cookie 值后发送出去。服务器端发现客户端发送过来的 Cookie 后,会去检查究竟是从哪一个客户端发来的连接请求,然后对比服务器上的记录,最后得到之前的状态信息。
Cookie是服务器保存在浏览器的一小段文本信息,每个 Cookie 的大小一般不能超过4KB。浏览器每次向服务器发出请求,就会自动附上这段信息。

cookie的用途
  1. 会话管理
    1.1 记录用户的登录状态是cookie最常用的用途。通常web服务器会在用户登录成功后下发一个签名来标记session的有效性,这样免去了用户多次认证和登录网站。
    1.2 记录用户的访问状态,例如导航啊,用户的注册流程啊。

  2. 个性化信息
    2.1 Cookie也经常用来记忆用户相关的信息,以方便用户在使用和自己相关的站点服务。例如:ptlogin会记忆上一次登录的用户的QQ号码,这样在下次登录的时候会默认填写好这个QQ号码。
    2.2 Cookie也被用来记忆用户自定义的一些功能。用户在设置自定义特征的时候,仅仅是保存在用户的浏览器中,在下一次访问的时候服务器会根据用户本地的cookie来表现用户的设置。例如google将搜索设置(使用语言、每页的条数,以及打开搜索结果的方式等等)保存在一个COOKIE里。

  3. 记录用户的行为
    最典型的是公司的TCSS系统。它使用Cookie来记录用户的点击流和某个产品或商业行为的操作率和流失率。当然功能可以通过IP或http header中的referrer实现,但是Cookie更精准一些。

WebView中的Cookie机制

WebView是基于webkit内核的UI控件,相当于一个浏览器客户端。它会在本地维护每次会话的cookie(保存在data/data/package_name/app_WebView/Cookies)
数据就保存在Cookies那个文件里,其实是个数据库,把后缀改成.db用数据库打开可以看到里面的表结构,主要有host_key, name, value, path等,host_key其实就是domain.
当WebView加载URL的时候,WebView会从本地读取该URL对应的cookie,并携带该cookie与服务器进行通信。WebView通过android.webkit.CookieManager类来维护cookie。CookieManager是 WebView的cookie管理类。

okhttp中的cookie

详见之前的文章:OKHttp深入理解

Cookie的缺陷

cookie会被附加在每个HTTP请求中,所以无形中增加了流量。
由于在HTTP请求中的cookie是明文传递的,所以安全性成问题。(除非用HTTPS)
Cookie的大小限制在4KB左右。对于复杂的存储需求来说是不够用的。

HTTP报文简介

HTTP 报文本身是由多行(用 CR+LF 作换行符)数据构成的字符串文本。HTTP 报文大致可分为报文首部和报文主体两部分。两者由最初出现的空行(CR+LF)来划分。通常,并不一定有报文主体。

请求报文结构


请求报文的首部内容由以下数据组成:

请求行 —— 包含用于请求的方法、请求 URI 和 HTTP 版本。
首部字段 —— 包含表示请求的各种条件和属性的各类首部。(通用首部、请求首部、实体首部以及RFC里未定义的首部如 Cookie 等)

请求报文的示例,如下:

响应报文结构


响应报文的首部内容由以下数据组成:

状态行 —— 包含表明响应结果的状态码、原因短语和 HTTP 版本。
首部字段 —— 包含表示请求的各种条件和属性的各类首部。(通用首部、响应首部、实体首部以及RFC里未定义的首部如 Cookie 等)

响应报文的示例,如下:

HTTP状态码

HTTP 状态码的职责是当客户端向服务端发送请求时,描述返回的请求结果。
状态码类型:

1XX:Informational(信息性状态码),接收的请求正在处理
2XX:Success(成功状态码),请求正常处理完毕
3XX:Redirection(重定向状态码),需要进行附加操作以完成请求
4XX:Client Error(客户端错误状态码),服务器无法处理请求
5XX:Server Error(服务器错误状态码),服务器处理请求出错

TCP三次握手四次挥手

在TCP/IP协议中,TCP协议提供可靠的连接服务,采用三次握手建立一个连接。

如下图所示,SYN(synchronous)是TCP/IP建立连接时使用的握手信号、Sequence number(序列号)、Acknowledge number(确认号码),三个箭头指向就代表三次握手,完成三次握手,客户端与服务器开始传送数据。


第一次握手:客户端发送syn包(syn=j)到服务器,并进入SYN_SEND状态,等待服务器确认;

第二次握手:服务器收到syn包,必须确认客户的SYN(ack=j+1),同时自己也发送一个SYN包(syn=k),即SYN+ACK包,此时服务器进入SYN_RECV状态;

第三次握手:客户端收到服务器的SYN+ACK包,向服务器发送确认包ACK(ack=k+1),此包发送完毕,客户端和服务器进入ESTABLISHED状态,完成三次握手。

四次挥手:

第一次挥手:客户端A发送一个FIN.用来关闭客户A到服务器B的数据传送

第二次挥手:服务器B收到这个FIN. 它发回一个ACK,确认序号为收到的序号+1。和SYN一样,一个FIN将占用一个序号

第三次挥手:服务器B关闭与客户端A的连接,发送一个FIN给客户端A

第四次挥手:客户端A发回ACK报文确认,并将确认序号设置为序号加1

TCP和UDP的区别

我这里简单列举几个:
1、基于连接与无连接;UDP是无连接的,即发送数据之前不需要建立连接

2、TCP保证数据正确性,UDP可能丢包,TCP保证数据顺序,UDP不保证。也就是说,通过TCP连接传送的数据,无差错,不丢失,不重复,且按序到达;UDP尽最大努力交付 ,即不保证可靠交付Tcp通过校验和,重传控制,序号标识,滑动窗口、确认应答实现可靠传输。如丢包时的重发控制,还可以对次序乱掉的分包进行顺序控制。

3、UDP具有较好的实时性,工作效率比TCP高,适用于对高速传输和实时性有较高的通信或广播通信。

4、每一条TCP连接只能是点到点的;UDP支持一对一,一对多,多对一和多对多的交互通信。

5、TCP对系统资源要求较多,UDP对系统资源要求较少。

HTTP 2.0

目标是改善用户在Web时的速度体验。可以说HTTP 2.0是SPDY的升级版(其实也是基于SPDY设计的)。

参考资料

HTTP基础
Cookie介绍及在Android中的使用
TCP三次握手和四次挥手

https加密解析

发表于 2019-03-19 | 分类于 HTTP

HTTPS全称为Hypertext Transfer Protocol over Secure Socket Layer,中文含义为“超文本传输安全协议”。

HTTP协议是没有加密无状态的明文传输协议,如果APP采用HTTP传输数据,则会泄露传输内容,可能被中间人劫持,修改传输的内容。HTTPS相当于HTTP的安全版本,作用如下:

认证用户和服务器,确保数据发送到正确的客户机和服务器;(身份认证)
加密数据以防止数据中途被窃取;(内容加密)
维护数据的完整性,确保数据在传输过程中不被改变。(数据完整性)

Https通讯原理

HTTPS是HTTP over SSL/TLS,HTTP是应用层协议,TCP是传输层协议,在应用层和传输层之间,增加了一个安全套接层SSL/TLS:

TLS协议主要有五部分:应用数据层协议,握手协议,报警协议,加密消息确认协议,心跳协议。TLS协议本身又是有record协议传输的,record协议的格式如上图最右所示。
SSL/TLS层负责客户端和服务器之间的加解密算法协商、密钥交换、通信连接的建立,安全连接的建立过程如下所示:

简单描述如下:

  1. 浏览器将自己支持的一套加密算法、HASH算法发送给网站。
  2. 网站从中选出一组加密算法与HASH算法,并将自己的身份信息以证书的形式发回给浏览器。证书里面包含了网站地址,加密公钥,以及证书的颁发机构等信息。
  3. 浏览器获得网站证书之后,开始验证证书的合法性,如果证书信任,则生成一串随机数字作为通讯过程中对称加密的秘钥。然后取出证书中的公钥,将这串数字以及HASH的结果进行加密,然后发给网站。
  4. 网站接收浏览器发来的数据之后,通过私钥进行解密,然后HASH校验,如果一致,则使用浏览器发来的数字串使加密一段握手消息发给浏览器。
  5. 浏览器解密,并HASH校验,没有问题,则握手结束。接下来的传输过程将由之前浏览器生成的随机密码并利用对称加密算法进行加密。

数字证书、CA

信息安全的基础依赖密码学,密码学涉及算法和密钥,算法一般是公开的,而密钥需要得到妥善的保护,密钥如何产生、分配、使用和回收,这涉及公钥基础设施。

公钥基础设施(PKI)是一组由硬件、软件、参与者、管理政策与流程组成的基础架构,其目的在于创造、管理、分配、使用、存储以及撤销数字证书。公钥存储在数字证书中,标准的数字证书一般由可信数字证书认证机构(CA,根证书颁发机构)签发,此证书将用户的身份跟公钥链接在一起。CA必须保证其签发的每个证书的用户身份是唯一的。

链接关系(证书链)通过注册和发布过程创建,取决于担保级别,链接关系可能由CA的各种软件或在人为监督下完成。PKI的确定链接关系的这一角色称为注册管理中心(RA,也称中级证书颁发机构或者中间机构)。RA确保公钥和个人身份链接,可以防抵赖。如果没有RA,CA的Root 证书遭到破坏或者泄露,由此CA颁发的其他证书就全部失去了安全性,所以现在主流的商业数字证书机构CA一般都是提供三级证书,Root 证书签发中级RA证书,由RA证书签发用户使用的证书。

X509证书链,左边的是CA根证书,中间的是RA中间机构,右边的是用户:

.pfx格式和.cer格式的区别

购买的证书,格式为.pfx,带有公钥和私钥,附带一个密码。还有一种格式为.cer的证书,这种证书是没有私钥的。

  1. 带有私钥的证书
      由Public Key Cryptography Standards #12,PKCS#12标准定义,包含了公钥和私钥的二进制格式的证书形式,以pfx作为证书文件后缀名(导出私钥,是需要输入密码的)。

  2. 二进制编码的证书
      证书中没有私钥,DER 编码二进制格式的证书文件,以cer作为证书文件后缀名。

  3. Base64编码的证书
    证书中没有私钥,BASE64 编码格式的证书文件,也是以cer作为证书文件后缀名。

https加密

加密算法一般分为对称加密与非对称加密。HTTPS一般使用的加密与HASH算法如下:

非对称加密算法:RSA,DSA/DSS
对称加密算法:AES,RC4,3DES
HASH算法:MD5,SHA1,SHA256

对称加密

客户端与服务器使用相同的密钥对消息进行加密
优点:1.加密强度高,很难被破解 2.计算量小,仅为非对称加密计算量的 0.1%
缺点:1.无法安全的生成和管理密钥 2.服务器管理大量客户端密钥复杂

非对称加密

非对称指加密与解密的密钥为两种密钥。服务器提供公钥,客户端通过公钥对消息进行加密,并由服务器端的私钥对密文进行解密。
优点:安全
缺点: 1. 性能低下,CPU 计算资源消耗巨大,一次完全的 TLS 握手,密钥交换时的非对称加密解密占了整个握手过程的 90% 以上。而对称加密的计算量只相当于非对称加密的 0.1%,因此如果对应用层使用非对称加密,性能开销过大,无法承受。2. 非对称加密对加密内容长度有限制,不能超过公钥的长度。比如现在常用的公钥长度是 2048 位,意味着被加密消息内容不能超过 256 字节。

其中非对称加密算法用于在握手过程中加密生成的密码,对称加密算法用于对真正传输的数据进行加密,而HASH算法用于验证数据的完整性。

非对称密钥加密最大的一个问题,就是无法证明公钥本身就是货真价实的公钥。比如,正准备和某台服务器建立非对称密钥加密方式下的通信时,如何证明收到的公开密钥就是原本预想的那台服务器发行的公开密钥。或许在公开密钥传输途中,真正的公开密钥已经被攻击者替换掉了。
为了解决上述问题,可以使用由数字证书认证机构(CA,Certificate Authority)和其相关机关颁发的公开密钥证书。

Hash算法(摘要算法)

Hash算法特别的地方在于它是一种单向算法,用户可以通过hash算法对目标信息生成一段特定长度的唯一hash值,却不能通过这个hash值重新获得目标信息。因此Hash算法常用在不可还原的密码存储、信息完整性校验等。

常见的Hash算法有MD2、MD4、MD5、HAVAL、SHA

HTTPS采用混合加密机制

HTTPS采用对称密钥加密和非对称密钥加密两者并用的混合加密机制,在交换密钥环节使用非对称密钥加密方式(安全地交换在稍后的对称密钥加密中要使用的密钥),之后的建立通信交换报文阶段则使用对称密钥加密方式。


所以,AES+RSA结合才更好,AES加密数据,且密钥随机生成,RSA用对方(服务器)的公钥加密随机生成的AES密钥。传输时要把密文,加密的AES密钥和自己的公钥传给对方(服务器)。对方(服务器)接到数据后,用自己的私钥解密AES密钥,再拿AES密钥解密数据得到明文。这样就综合了两种加密体系的优点。下面代码展示OkHttp添加拦截器实现(要对response.code()做处理,只有在和后台约定好的返回码下才走解密的逻辑,具体看自己的需求):

public class DataEncryptInterceptor implements Interceptor {
    @Override
    public Response intercept(Chain chain) throws IOException {
        //请求
        Request request = chain.request();
        RequestBody oldRequestBody = request.body();
        Buffer requestBuffer = new Buffer();
        oldRequestBody.writeTo(requestBuffer);
        String oldBodyStr = requestBuffer.readUtf8();
        requestBuffer.close();
        MediaType mediaType = MediaType.parse("text/plain; charset=utf-8");
        //生成随机AES密钥并用serverPublicKey进行RSA加密
        SecretKeySpec appAESKeySpec = EncryptUtils.generateAESKey(256);
        String appAESKeyStr = EncryptUtils.covertAESKey2String(appAESKeySpec);
        String appEncryptedKey = RSAUtils.encryptDataString(appAESKeyStr, serverPublicKey);
        //计算body 哈希 并使用app私钥RSA签名
        String appSignature = RSAUtils.signature(oldBodyStr, appPrivateKey);
        //随机AES密钥加密oldBodyStr
        String newBodyStr = EncryptUtils.encryptAES(appAESKeySpec, oldBodyStr);
        RequestBody newBody = RequestBody.create(mediaType, newBodyStr);
        //构造新的request
        request = request.newBuilder()
                .header("Content-Type", newBody.contentType().toString())
                .header("Content-Length", String.valueOf(newBody.contentLength()))
                .method(request.method(), newBody)
                .header("appEncryptedKey", appEncryptedKey)
                .header("appSignature", appSignature)
                .header("appPublicKey", appPublicKeyStr)
                .build();
        //响应
        Response response = chain.proceed(request);
        if (response.code() == 200) {//只有约定的返回码才经过加密,才需要走解密的逻辑
            //获取响应头
            String serverEncryptedKey = response.header("serverEncryptedKey");
            //用app的RSA私钥解密AES加密密钥
            String serverDecryptedKey = RSAUtils.decryptDataString(serverEncryptedKey, appPrivateKey);
            SecretKeySpec serverAESKeySpec = EncryptUtils.covertString2AESKey(serverDecryptedKey);
            //用AES密钥解密oldResponseBodyStr
            ResponseBody oldResponseBody = response.body();
            String oldResponseBodyStr = oldResponseBody.string();
            String newResponseBodyStr = EncryptUtils.decryptAES(serverAESKeySpec, oldResponseBodyStr);
            oldResponseBody.close();
            //构造新的response
            ResponseBody newResponseBody = ResponseBody.create(mediaType, newResponseBodyStr);
            response = response.newBuilder().body(newResponseBody).build();
        }
        response.close();
        //返回
        return response;
    }
}

参考资料

https://www.cnblogs.com/alisecurity/p/5939336.html
Https原理和实现
Android Okhttp网络请求加解密实现方案
java CA证书制作和代码中使用

1…678
Shuming Zhao

Shuming Zhao

78 日志
14 分类
31 标签

© 2021 Shuming Zhao
访客数人, 访问量次 |
由 Hexo 强力驱动
|
主题 — NexT.Pisces v5.1.4