oohcode

$\bigodot\bigodot^H \rightarrow CODE$

Think in UML

本书主要讲的是UML的设计方法及如何把UML运用到实际的工作当中

第一部分:准备篇——需要了解

1. 为什么需要UML

简单来说随着业务越来越复杂面向过程的方法已经不能满足对这种业务的分析需求了,需要使用新的方法,也就是面向对象的分析方法对业务进行分析,但是面向对象也存在一下问题,比如对象是如何抽象出来的,对象世界的不容易理解等,要想更好的解决这些问题,我们就需要:

  • 一种把现实世界映射到对象世界的方法。
  • 一种从对象世界描述现实世界的方法。
  • 一种验证对象世界行为是否正确反映了现实世界的方法。

而UML背后所代表的面向对象设计方法正式针对这些问题提出的。下面将详细介绍UML是如何解决这些问题的。

UML带来了什么

面向对象分析(OOA)方法中,其中最重要的就是UML的前身,后来发展成为UML这种统一建模语言,这种统一建模语言是为了让人们能够无障碍的交流,并且让人和机器都能够读懂,而且UML是可视化的,使用图形更能够形象的说明问题。为了便于建模,UML提供了一些元素来为现实世界建模:

  • actor 参与者:信息来源的提供者,同时也是第一驱动者。
  • use case用例:表示驱动者的业务目标。

UML通过被称为概念化的过程(Conceptual)来建立适合计算机理解和实现的模型,这个模型称为分析模型(Analysis Model),分析模型的主要元模型有:

  • 边界类(boundary):是面向对象分析的一个非常重要的观点,是事物内外交互的一个介质。
  • 实体类(entity):原始需求中领域模型中的业务实体映射了现实世界中参与者完成业务目标时所涉及的事物,UML采用实体类来重新表达业务实体。
  • 控制类(control):边界和实体都是静态的,UML采用控制类来表述原始需求中的动态信息,即业务或用例场景中的步骤和活动。

通过从现实世界 -> 业务模型 -> 概念模型 -> 设计模型的过程便可以解决上一节提到的三个问题了。

统一过程简介

RUP是统一过程,统一过程是在很多实践中总结出来的软件工程的最佳实践。RUP是一个理论的指导,UML则是具体的语言实现。

2. 建模基础

建模

建模(Modeling):是指通过对客观事物建立一种抽象的方法用以表征事物并获得对事物本身的理解,同时把这种理解概念化,将这些逻辑概念组织起来,构成一种对所观察的对象的内部结构和工作原理的便于理解的表达。
建模需要关注的两点是:这么建立?模是什么?
做需求的时候,首先目标不是弄清楚业务是如何一步一步完成的,而是要弄清楚有多少个业务的参与者,每个参与者的目标是什么,参与者的目标就是你的抽象角度。实际上,这就是用例。
业务建模是什么,这个问题可以参考下面的几个公式来理解:

  • \(问题领域= \sum_{1}^{n} 抽象问题 \)
  • 抽象角度 = 问题领域边界之外的参与者的业务目标 = 业务用例
  • \(业务用例= \sum_{1}^{n} 特定场景\)
  • 特定场景 = 静态的事物 + 特定的条件 + 特定的动作 或者
    特定的事 = 特定的事物 + 特定的规则 + 特定的人的行为

用例驱动

用例驱动是统一过程的重要概念,或者说整个软件的生产过程就是用例驱动的。如果我们找到的事物、规则和行为实现了所有必要的用例,那么问题领域就被解决了。统一过程中一个用例就是一个分析单元、设计单元、测试单元甚至部署单元。用例可以驱动的内容包括:

  • 逻辑视图: 一个系统只有一个,它以图形的方式说明关键的用例实现、子系统、包和类。即建模公式中的那些“人”、“事”、“物”、“规则”是如何分类组织的。
  • 进程视图: 一个系统只有一个,它以图形方式说明系统中进程的详细组织结构,其中包括类和子系统到进程和线程的映射。即建模公式中的那些“人”、“事”、“物”、“规则”是如何交互的,它们的关系如何。
  • 部署视图: 一个系统只有一个,它以图形的方式说明了处理活动在系统中各节点的分布,包括进程和线程的物理分布。即建模公式中的那些是如何部署在物理节点(主机、网络环境)上的。
  • 实施视图: 获取为实施指定的架构决策,即建模公式中那些是如何构系统的“零部件”,以及我们如何组织人力生产和组装这些“零部件”以建成最终系统。

抽象层次

抽象层次是面向对象方法中极其重要,但是又非常难以掌握的技巧。抽象层次越高,具体信息越少,越容易理解,反之亦然。抽象的方法有两种:

  • 自顶向下: 适用于让人们从头开始认识一个事物。
  • 自底向上: 适用于在实践中改进和提高认识。

软件开发中应该主体上采用自顶向下的方法,同时应当辅以自底向上的方法,通过总结在较低抽象层次的实践经验来改进较高层次的概念一提升软件质量。

视图

视图用于组织UML元素,表达出模型某一方面的含义。如何选择视图才是重点。视图另一个被人忽视重要概念是:视角。就是人们观察事物的角度。一方面,从信息的展示角度来说,恰当的视角可以让观察者更容易抓住信息的本质;另一方面,从观察者角度说,观察者只会关心信息中他感兴趣的那一部分视角。于是用很多个不同的视图去展示软件这些不同的方面——静态的、动态的、结构性的、逻辑性的等——才能够说建立了一个完整的模型。为了说明这些方面,UML定义了用例图、对象图、类图、包图、活动图等不同的视图。所以在实际工作过程中需要考虑的问题就是:

  • 应该为哪些软件信息绘制哪些视图?
  • 应给给哪些干系人展示哪些视角?

对象分析方法

使用好UML的前提条件就是掌握了面向对象的思想和方法:

  • 一切都是对象
  • 对象都是独立的
  • 对象具有原子性
  • 对象都是可以抽象的
  • 对象都有层次性
  • 对象分析方法总结

第二部分:基础篇——在学习中思考

3. UML核心元素

  • 版型(stereotype):是对UML元素基础定义的一个扩展,在同一元素基础定义的基础上赋予特别的含义,使得这个元素适用于特定的场合。
  • 参与者(actor):是系统之外与系统交互的某人或某事物。系统与参与者之间有一个明确的边界。如何确定系统之外和系统之内呢?主要通过下面两个问题来判断:

    • 谁对系统有着明确的目标和要求并且主动发出动作?
    • 系统是为谁服务的?

      参与者又叫主角,这个叫法更形象,因为参与者很容易与其他的混淆,比如后面会提到业务主角(是参与者的一个版型)、业务工人(被动参与业务的参与者)、涉众(要建设的这个系统有利益相关的一切人和事)、用户(系统的使用者,是参与者的代表)、角色(参与者的职责,是一个抽象的概念,从众多的参与者中抽象出相同的那一部分。)等。

  • 用例(use case):关于用例的一些知识点总结如下:
    • 用例是系统所执行的一系列操作。
    • 用例是一个相对独立的过程。
    • 用例的执行结果对参与者来说是可观测的和有意义的。
    • 这件事必须有一个参与者发起。
    • 用例必须是以动宾短语形式出现的。如:喝水、取钱等。
    • 一个用例就是一个需求单元、分析单元、设计单元、开发单元、测试单元,甚至部署单元。
    • 用例粒度的控制:一般在业务建模阶段,以一个用例能够说明一件完整的事情为宜;在概念建模阶段,用例粒度应以能够描述一个完整的事件流为宜,即完成一项完整业务的一个步骤;在系统建模阶段,用例视角是针对计算机的,因此粒度应以能够描述操作者与计算机的一次交互为宜。
    • 如何获得一个用例:主要应当确保:一个明确的目标才是一个用例的来源;一个真实的目标应当完备地表达主角的期望;一个有效的目标应当在系统边界内,由主角发动,并具有明确的后果。
    • 区分用例与功能之间的误区:功能是脱离使用者的愿望而存在的;功能是孤立的;用例是一系列完成一个特定目标的“功能”的组合。确定用例要从用例的角度查找,而不是从功能的角度。
    • 目标和步骤之间的误区:步骤是为了完成目标而做的,但是有时候步骤和目标却不是绝对的,主要是在不同的分析阶段如业务用例,概念用例,由于边界发生了变化,这些也都是可以转变的。
    • 用例粒度的误区:这个误区就是用例粒度把握的不好,用例的层次不够清晰,导致依据用例进行设计的系统架构不能够满足需求,或者对需求的变动有很差的应变能力。
    • 业务用例:专门用于需求阶段的建模,在为业务领域建模时应当使用这种版型。
    • 概念用例:用于概念建模。作为概念模型的核心元素,概念用例用来获取业务用例中的核心业务逻辑。
    • 系统用例:就是一般意义上的用例,是用来定义系统范围,获取功能性需求的。
  • 边界: 是参与者和用例之间的一个界限。边界的定义要考多需求的把握。
    • 边界决定视界。
    • 边界决定抽象层次。
    • 边界需要灵活使用。
  • 业务实体:是类(class)的一种版型,特别用于在业务建模阶段建立领域模型。官方定义是:业务实体代表业务角色执行业务用例是所处理或使用的“事物”。
  • 包:将某些信息分类,形成逻辑单元。包的目标是高內聚、低耦合。包的版型有:领域包、子系统、组织结构、层等。
  • 分析类:用于获取系统中主要的“职责簇”。两个主要的性质:
    - 分析类代表系统中主要的法“职责簇”,这意味着分析类是从功能性需求向计算机实现转化过程中的“第一个关口”
    - 分析累可以产生系统的设计类和子系统,这意味着计算机实现是可以通过某种途径“产生”出来的 ,而不是拍脑袋拍出来的。
    分析类有三个版型:边界类、控制类和实体类。

4. UML核心视图

静态视图

只描述事物的静态结构,而不描述其动态行为。

用例图

采用参与者和用例作为基本元素,用不同的视角展现系统的功能性需求。用例图是系统蓝图和开发的依据。主要包括:

  • 业务用例视图: 从不同的角度来对业务用例视图进行不同的划分,来保证业务目标是否齐全,业务主角和用例是否齐全等。
    业务主角视角:

    业务模块视角:

  • 业务用例实现视图:上面展现了业务的功能性需求,这个视图则是描述这些需求的实现途径的。也就是说一个用例有多少个实现途径,需要描述一下。

  • 概念用例视图:用于展现从业务用例中经过分析分解出来的关键概念用例,并表示概念用例和业务用例之间的关系。一般这些关系有扩展、包含和精化。

  • 系统用例视图:展现系统范围,将对业务用例进行分析后得到的系统用例展现出来,一般来说系统用例视图是以业务为单位展现的,即将视图名称命名为业务用例名称。下面是一个借阅图书系统用例视图:

  • 系统用例实现视图
    与业务用例实现视图类似,如果一个系统用例有多种实现方式,也应当为其绘制实现视图。

类图

是现实世界问题领域的抽象对象的结构化、概念化、逻辑化描述。

  • 概念层类图:概念层的观点认为,在这个层次的类图描述的是现实世界中问题领域的概念理解,即类图中表达的类与现实世界的问题领域有着明显的对应关系,类之间的关系也与问题领域中实际事物的关系有着明显的对应关系。说的通俗一点就是概念层类图与现实世界中的事物一一对应,类与类之间的关系也与现实世界中的事物之间的关系一一对应。
  • 说明层类图:说明层的观点认为,在这个层次的类图考察的是类的接口而不是实现,类图中表达的类和类之间的关系应当是对问题领域在接口层次抽象的描述。
  • 实现层类图:实现层观点认为,类是实现代码的描述,类图中的类直接映射到可执行代码。这个阶段必须明确用哪种语言,什么设计模式,什么通信标准,遵循什么规范等。

包图

一般用来展示高层次的观点,把繁多的元素通过包这个容器从大到小、从粗到细地建立关系。

动态视图

动态视图是描述视图动态行为的。

活动图

  • 用例活动图:活动图用来描述用例场景,也就是通常所说的业务流程,活动图有几个关键的元素:

    • 起始点:标记业务流程的开始,一个业务流程有且仅有一个起始点。
    • 活动:活动是业务流程中的一个执行单元。
    • 判断:判断根据某个条件进行决策,执行不同的流程分支。
    • 同步:分为同步启示和同步汇合。同步起始表示从它开始多个分流并行执行;同步汇合表示多个支流同时到达后再执行后续活动。
    • 结束点:表示业务流程的终止。
    • 基本流:表示最主要、最频繁使用的、默认的业务流程分支。
    • 支流
    • 异常流
    • 组合活动
  • 对象活动图

  • 泳道图
  • 业务场景建模
  • 用例场景建模

    状态图

    时序图

    协作图

5. UML核心模型

6. 统一过程核心工作简介

7. 迭代式软件生命周期

第三部分:进阶篇——在实践中思考

第四部分:高级篇——在提炼中思考

我使用的工具列表

在码农生涯中我喜欢不断的折腾,在这里我把我自己用的一些工具集中列一下,以便我自己进行总结,也希望能给看到的人一些帮助~

工具列表

  • VIM: 编辑器
  • Jekyll: 写博客
  • plantuml: 生成UML图
  • markdown: 文档写作格式

plantuml在Mac OS X系统下中文乱码问题解决

安装完plantuml使用plantuml.jar生成的uml图中文会出现乱码,这个问题困扰了我大半天,终于解决了~

首先编写uml程序的文件的编码方式是utf-8的,test.uml内容如下:

1
2
3
4
5
6
7
8
9
10
11
@startuml
|Brower|
start
:点击领取按钮;
|WebServer|
:领取勋章;
|Cache|
:更新缓存;
|DB|
:更新DB;
@enduml

但是利用java -jar plantuml.jar -tsvg test.uml或者利用plantuml的vim插件命令:make都可以正常生成,但是里面的中文都是乱码。实在是让我发愁啊,在网上找了好久,只有一篇博客提到了这个问题,就是ubuntu plantuml的中文问题 。但这并不是我想要的,首先我的系统是Mac OS X,跟博文中的系统不一样,其次我的系统对中文编码是支持的,其他的都没问题,只是这里有问题。最后还是自己帮了自己,使用java -jar plantuml.jar -h可以看到有个-charset选项,但是后面说的默认编码格式却是GBK2312,总有找到问题了,于是我在命令里加了下面的参数java -jar plantuml.jar -charset utf-8 -tsvg test.uml,在看结果终于可以了~中文显示正常了!

MySQL优化总结

这篇博客主要就mysql的优化进行问题从不同方面进行了总结。

基础知识

通常意义上,数据库也就是数据的集合,具体到计算机上数据库可以是存储器上一些文件的集合或者一些内存数据的集合。MySql数据库是开放源代码的关系型数据库。目前,它可以提供的功能有:支持sql语言、子查询、存储过程、触发器、视图、索引、事务、锁、外键约束和影像复制等。MySql也是客户/服务器系统并且是单进程多线程架构的数据库。MySql区别于其它数据库系统的一个重要特点是支持插入式存储引擎

存储引擎

根据存储数据及为数据建立索引和更新、查询技术的不同可以将mysql的存储分为不同的存储引擎,其中最主要的存储引擎有MyISAM、InnoDB、MEMORY等,其中最常用的有MyISAM和InnoDB两种,可以通过下面的命令查看自己的MySQL支持哪些存储引擎:

1
2
3
4
5
6
7
8
9
10
11
mysql> SHOW ENGINES;
+------------+---------+------------------------------------------------------------+--------------+------+------------+
| Engine | Support | Comment | Transactions | XA | Savepoints |
+------------+---------+------------------------------------------------------------+--------------+------+------------+
| MRG_MYISAM | YES | Collection of identical MyISAM tables | NO | NO | NO |
| CSV | YES | CSV storage engine | NO | NO | NO |
| MyISAM | DEFAULT | Default engine as of MySQL 3.23 with great performance | NO | NO | NO |
| InnoDB | YES | Supports transactions, row-level locking, and foreign keys | YES | YES | YES |
| MEMORY | YES | Hash based, stored in memory, useful for temporary tables | NO | NO | NO |
+------------+---------+------------------------------------------------------------+--------------+------+------------+
5 rows in set (0.00 sec)

  • MyISAM:这种引擎是mysql最早提供的。这种引擎又可以分为静态MyISAM、动态MyISAM 和压缩MyISAM三种:

    • 静态MyISAM:如果数据表中的各数据列的长度都是预先固定好的,服务器将自动选择这种表类型。因为数据表中每一条记录所占用的空间都是一样的,所以这种表存取和更新的效率非常高。当数据受损时,恢复工作也比较容易做。
    • 动态MyISAM:如果数据表中出现varchar、xxxtext或xxxBLOB字段时,服务器将自动选择这种表类型。相对于静态MyISAM,这种表存储空间比较小,但由于每条记录的长度不一,所以多次修改数据后,数据表中的数据就可能离散的存储在内存中,进而导致执行效率下降。同时,内存中也可能会出现很多碎片。因此,这种类型的表要经常用optimize table 命令或优化工具来进行碎片整理。
    • 压缩MyISAM:以上说到的两种类型的表都可以用myisamchk工具压缩。这种类型的表进一步减小了占用的存储,但是这种表压缩之后不能再被修改。另外,因为是压缩数据,所以这种表在读取的时候要先时行解压缩。

      但是,不管是何种MyISAM表,目前它都不支持事务,行级锁和外键约束的功能。

  • MyISAM Merge:这种类型是MyISAM类型的一种变种。合并表是将几个相同的MyISAM表合并为一个虚表。常应用于日志和数据仓库。
  • InnoDB:InnoDB表类型可以看作是对MyISAM的进一步更新产品,它提供了事务、行级锁机制和外键约束的功能。
  • memory(heap):这种类型的数据表只存在于内存中。它使用散列索引,所以数据的存取速度非常快。因为是存在于内存中,所以这种类型常应用于临时表中。
  • archive:这种类型只支持select 和 insert语句,而且不支持索引。常应用于日志记录和聚合分析方面。

索引

索引用到的数据结构

关于存储引擎用的数据结构其中最重要的就是B树与B+树,可以参考JULY的这篇从B 树、B+ 树、B* 树谈到R 树
可以看到,B树与B+树的最大的区别其实就是:B树的所有信息都存在字节点中,而B+树的所有信息都存储在叶子节点中。
B+树比B树更适合做文件索引和数据库索引,原因是:

  1. B+-tree的磁盘读写代价更低
    B+-tree的内部结点并没有指向关键字具体信息的指针。因此其内部结点相对B 树更小。如果把所有同一内部结点的关键字存放在同一盘块中,那么盘块所能容纳的关键字数量也越多。一次性读入内存中的需要查找的关键字也就越多。相对来说IO读写次数也就降低了。
    举个例子,假设磁盘中的一个盘块容纳16bytes,而一个关键字2bytes,一个关键字具体信息指针2bytes。一棵9阶B-tree(一个结点最多8个关键字)的内部结点需要2个盘快。而B+ 树内部结点只需要1个盘快。当需要把内部结点读入内存中的时候,B 树就比B+ 树多一次盘块查找时间(在磁盘中就是盘片旋转的时间)。
  2. B+-tree的查询效率更加稳定
    由于非终结点并不是最终指向文件内容的结点,而只是叶子结点中关键字的索引。所以任何关键字的查找必须走一条从根结点到叶子结点的路。所有关键字查询的路径长度相同,导致每一个数据的查询效率相当。
    读者点评

  3. 有人觉得这两个原因都不是主要原因。数据库索引采用B+树的主要原因是 B树在提高了磁盘IO性能的同时并没有解决元素遍历的效率低下的问题。正是为了解决这个问题,B+树应运而生。B+树只要遍历叶子节点就可以实现整棵树的遍历。而且在数据库中基于范围的查询是非常频繁的,而B树不支持这样的操作(或者说效率太低)。

索引的分类

MySQL数据库可以建立不同的数据库,主要类型有:

  • 普通索引:就是普通的INDEX。索引的列可以重复。
  • 唯一索引:UNIQUE INDEX ,索引的列值必须使唯一,可以有空值,如果是组合索引,则列值的组合必须使唯一的。主键索引是一种特殊的唯一索引,不允许为空值,一个表只能有一个主键。
  • 全文索引:FULLTEXT索引,把CHAR、VARCHAR或TEXT列作为索引,仅可以用于MyISAM表中。
  • 组合索引(最左前缀):对个列组合成为一个索引。这个索引其实只是上面几种的一个特殊情况。

存储引擎对索引的利用

关于存储引擎及其使用的数据结构可以看下这个博客:浅谈mysql索引背后的数据结构及算法
这篇博客讲的很清楚了,但是这里我还是要总结一下重点,便于自己记忆。

  1. MyISAM与InnoDB使用的都是B+树作为索引
  2. MyISAM使用B+树的方式如下:

    • 对于主键索引:
      primary key
    • 对于辅助索引:
      primary key
      可见它主要是MyISAM的叶子节点存储的是数据的地址,索引文件与数据是分离的,当查询时先从索引文件中找到数据的地址,然后再根据地址去取出数据的值。主索引与普通索引的查询方法是一致的。
  3. InnoDB使用B+树的方式如下:

    • 对于主键索引:
      primary key
    • 对于辅助索引:
      primary key
      可以看出与MyISAM不同,InnoDB的数据文件同时也是索引文件,对于主键索引的使用,就是直接从索引文件的叶子节点中找出数据。但是普通索引所有的叶子节点存储的都是主键的值,对于普通索引只能先通过索引文件找出主键的值,然后再根据主键的值从主键索引文件中找出数据。
      ps:有点疑问:前面说选择B+Tree作为存储引擎数据结构的原因第一条是索引文件比较小,可以放到同一个磁盘上,减少磁盘的读取次数,所以效率比较高,但是这个把索引文件和数据融到了一起,是不是也会有这个问题呢?

存储引擎适用的场景

只有了解存储引擎的原理才能更好的进行优化,其实优化就是根据其原理把数据库的性能提升到尽可能的高。

markdown转pdf

现在已经对markdown痴迷了,一直在折腾,想以后把markdown 作为我不管是写博客还是写文档的标准文件格式,因为据说它可以转换成很多其他的格式,适应在不同的设备上使用,这就是一劳永逸啊。可以过程却是很坑爹啊~

工具选择

Google 了很久,终于发现一个强大的工具:pandoc。于是苦逼的旅程开始了~

linux下安装:

  1. pandoc下载安装页面竟然没有redhat?
  2. 查看一下发现有一个all-platforms
  3. 提示需要先安装Haskell platform
  4. 选择linux平台却发现没有redhat,只是看到不起眼的一行写着:See also: justhub, for RHEL, CentOS, Scientific Linux, and Fedora support.
  5. 进入下载页面总于看到希望了:
    wget http://sherkin.justhub.org/el6/RPMS/x86_64/justhub-release-2.0-4.0.el6.x86_64.rpm
    rpm -ivh justhub-release-2.0-4.0.el6.x86_64.rp
    yum install haskell
    尼玛,这个也太大了,要一个多G,我的usr都满了~
  6. 再回到pandoc的下载页面执行:cabal install --force pandoc pandoc-citeproc
  7. 果然又报错:pandoc-1.12.2.1 failed during the building phase.,找了好久,还是stackoverflow靠谱,原来是内存不够用了!果断把其它进程kill了,腾出空间再次运行~
  8. 终于看到安装成功的提示了,-_-#。来,运行一个pandoc命令看看:pandoc -V,竟然提示command no found!!!_| ̄|○
  9. 找找有没有pandoc吧:find / -name pandoc果然找到了,二进制文件在这里:/root/.cabal/bin/pandoc。把它们加到PATH里,退出再登录一把。
  10. Y(^_^)Y,终于好了!!!

csapp chapter3:程序的机器级表示

*本章是理解程序如何运行的关键所在,这一章将学到:

  1. 程序时如何从高级代码变为汇编代码;
  2. 程序编译后运行时在内存中是如何分布的;
    3…*

程序代码

首先介绍各种单片机、处理器,不同的处理器所使用的能够识别的汇编语言也是不一样的,如何根据不同处理器把同一段高级语言转换成对应的汇编语言,这个是编译器的事儿,本文所有的汇编语言我的机器就跟他不同,导致我学习的时候也为此很头疼,严重影响了我对本来就一知半解的汇编语言的理解。

程序编码

在深入了解程序代码之前需要简单的了解一下GCC编译器的使用方法。

首先编写一个简单的C文件:code.c

1
2
3
4
5
6
7
8
9
 //code.c
int accum = 0;

int sum(int x, int y)
{
int t = x + y;
accum += t;
return t;
}

然后利用gcc命令加上-S参数生产一个汇编代码文件:

1
2
3
4
gcc -O1 -S code.c
ll
code.c
code.s

可见生成了一个.s文件,打开这个汇编文件,内容如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
        .file"code.c"
.text
.globl sum
.type sum, @function
sum:
.LFB0:
.cfi_startproc
leal (%rsi,%rdi), %eax
addl %eax, accum(%rip)
ret
.cfi_endproc
.LFE0:
.size sum, .-sum
.globl accum
.bss
.align 4
.type accum, @object
.size accum, 4
accum:
.zero4
.ident "GCC: (GNU) 4.4.6 20110731 (Red Hat 4.4.6-3)"
.section .note.GNU-stack,"",@progbits

但是书中给出的生成的代码却是这样的:
1
2
3
4
5
6
7
8
sum:
pushl %ebp
movl %esp, %ebp
movl 12(%ebp), %eax
addl 8(%ebp), %eax
addl %eax, accum
popl %ebp
ret

可以看到这两段代码差别很大,这是因为所在的处理器环境不同,所以汇编命令格式会有差别;而且gcc的版本也不同,会导致优化的程度也不同。为了能够更好的理解课本,以后就按照书上的来理解。
如果在使用gcc的时候加-C参数,则汇编器会把汇编文件编译成二进制文件:
1
gcc -O1 -c code.c

这是生产了code.o文件,但是它的内容是二进制的,直接看不到。用vi打开文件,然后在normal模式下输入::%!xxd
1
2
3
4
5
6
7
8
9
10
11
0000000: 7f45 4c46 0201 0100 0000 0000 0000 0000  .ELF............
0000010: 0100 3e00 0100 0000 0000 0000 0000 0000 ..>.............
0000020: 0000 0000 0000 0000 1001 0000 0000 0000 ................
0000030: 0000 0000 4000 0000 0000 4000 0c00 0900 ....@.....@.....
0000040: 8d04 3e01 0500 0000 00c3 0000 0047 4343 ..>..........GCC
0000050: 3a20 2847 4e55 2920 342e 342e 3620 3230 : (GNU) 4.4.6 20
0000060: 3131 3037 3331 2028 5265 6420 4861 7420 110731 (Red Hat
0000070: 342e 342e 362d 3329 0000 0000 0000 0000 4.4.6-3)........
0000080: 1400 0000 0000 0000 017a 5200 0178 1001 .........zR..x..
0000090: 1b0c 0708 9001 0000 1400 0000 1c00 0000 ................
......

还可以通过gdb这个调试工具进行查看二进制文件:
1
2
3
4
$gbd code.o
(gdb) x/17xb sum
0x0 <sum>:0x8d0x040x3e0x010x050x000x000x 00
0x8 <sum+8>:0x000xc3Cannot access memory at address 0xa

但是要查看目标代码文件的内容,最有价值的还是反汇编器(disassembler),这玩意儿听上去就很NB有木有?使用方法入下:
1
2
3
4
5
6
7
8
9
10
objdump -d code.o
code.o: file format elf64-x86-64


Disassembly of section .text:

0000000000000000 <sum>:
0:8d 04 3e lea (%r si,%rdi,1),%eax
3:01 05 00 00 00 00 add %eax,0x0(%rip) # 9 <sum+0x9>
9:c3 retq

这个是我电脑上的结果,跟书上的有不一样,书上的是:
1
2
3
4
5
6
7
8
9
10
11
Disassembly of funciton sum in binary file code.o
00000000 <sum>
Offset Bytes Equivalent assembly language
0: 55 push %ebp
1: 89 e5 mov %esp,%ebp
3: 8b 45 0c mov 0xc(%ebp),%eax
6: 03 45 08 add 0x8(%ebp), %eax
9: 01 05 00 00 00 00 add %eax,0x0
f: 5d pop %ebp
10: c3 ret


生成实际可执行的代码需要对一组目标代码文件运行链接器,而这一组目标代码文件中必须含有一个main函数,假设文件main.c中有下面这样一个函数:
1
2
3
4
int main()
{
return sum(1,3)
}

然后用下面的方法生成可执行文件prog:
1
gcc -O1 -o prog code.o main.c

看到生成了一个prog可执行文件,对其进行反编译
1
objdump -d prog

可看到一大坨的汇编代码,但是其中有一段跟上面的代码一样

1
2
3
4
5
6
7
8
9
10
objdump -d code.o
code.o: file format elf64-x86-64


Disassembly of section .text:

0000000000000000 <sum>:
0:8d 04 3e lea (%rsi,%rdi,1),%eax
3:01 05 00 00 00 00 add %eax,0x0(%rip) # 9 <sum+0x9>
9:c3 retq

这个是我电脑上的结果,跟书上的有不一样,书上的是:

1
2
3
4
5
6
7
8
9
10
11
Disassembly of funciton sum in binary file code.o
00000000 <sum>
Offset Bytes Equivalent assembly language
0: 55 push %ebp
1: 89 e5 mov %esp,%ebp
3: 8b 45 0c mov 0xc(%ebp),%eax
6: 03 45 08 add 0x8(%ebp), %eax
9: 01 05 00 00 00 00 add %eax,0x0
f: 5d pop %ebp
10: c3 ret


生成实际可执行的代码需要对一组目标代码文件运行链接器,而这一组目标代码文件中必须含有一个main函数,假设文件main.c中有下面这样一个函数:
1
2
3
4
int main()
{
return sum(1,3)
}

然后用下面的方法生成可执行文件prog:
1
gcc -O1 -o prog code.o main.c

看到生成了一个prog可执行文件,对其进行反编译
1
objdump -d prog

可看到一大坨的汇编代码,但是其中有一段跟上面的代码一样
1
2
3
4
5
6
0000000000400474 <sum>:
400474: 8d 04 3e lea (%rsi,%rdi,1),%eax
400477: 01 05 d3 03 20 00 add %eax,0x2003d3(%rip) # 600850 <accum>
40047d: c3 retq
40047e: 90 nop
40047f: 90 nop

与前面的代码对比差不多,但是其地址有明显的改变,应为第二段代码编译后说有的地址都是相对程序的地址指定了的。

访问信息

访问信息分为:操作数指示符、数据传送指令等操作。通过一个练习题来巩固一下操作数指示符:

例题3.1 假设下面的值存放在指明的存储器地址和寄存器中:

地址
0x100 0xFF
0x104 0xAB
0x108 0x13
0x10C 0x11
寄存器
%eax 0x100
%ecx 0x1
%edx 0x3

填写下表,给出所示操作数的值。

操作数 思路
%eax 0x100 直接从上面获取寄存器的初始值
0x104 0xAB 从上面表中获取0x104地址的值
$0x108 0x108 立即数
(%eax) 0xFF 先获取寄存器%eax的值0x100,然后在获取地址0x100对应的值,间接寻址
4(%eax) 0xAB (基址+偏移量)寻址:(4 + 0x100) = (0x104) = 0xAB
9(%eax, %edx) 0x11 变址寻址:(9 + %eax + %edx) = (0x10C) = 0x11
260(%ecx, %edx) 0x13 变址寻址:(260 + %ecx + %edx) = (0x108) = 0x13
0xFC(, %ecx, 4) 0xFF 比例变址寻址:(0xFC + 0 + %ecx * 4) = (0xFC+0x4) = (0x100)=0xFF
(%eax, %edx, 4) 0x11 比例变址寻址:(%eax + %edx * 4)=(0x10C)=0x11

栈操作说明:

通过书中图3.5可知,当push一个数值时,栈指针减小,向下移动;当pop一个数据时栈指针向上移动。一般用 %esp来存储栈指针的地址。

下面通过一个例子说明C语言中指针使用的方式,函数exchange代码如下:

1
2
3
4
5
6
7
8
9
10
int exchange(int *xp, int y)
{
int x = *xp;
*xp = y;
return x;
}

int a = 4;
int b = exchange(&a, 3);
printf("a = %d, b = %d\n", a, b);

这段代码会打印出:
a = 3, b = 4
关键部分的汇编代码如下:
1
2
3
4
5
# xp地址的值存储在8(%ebp), y的值存储在12(%ebp)
movl 8(%ebp) %edx #获取xp地址的值,存储在%edx
movl (%edx), %eax #获取xp地址所指向的值赋予变量x,函数结束时返回这个值
movl 12(%ebp), %ecx #获取y的值,存储在%ecx
movl %ecx, (%edx) #%ecx的值存储在%edx所指向的值, 这时候*xp的值为y,xp地址的值没有变化

局部变量比如x,通常时保存在寄存器中。

算术和逻辑操作

加载有效地址

加载有效地址(load effective address)指令leal实际上是movl指令的变形。通过下面一个例子来说明它的含义:

1
2
3
# 假设%edx的值为x
movl 7(%edx, %edx, 4), %eax #计算7 + x + 4*x = 5x +7 那么%eax的值就是地址5x+7地址处所存储的值.
leal 7(%edx, %edx, 4), %eax #计算7 + x + 4*x = 5x +7 那么%eax的值就是地址的值5x+7,而不是这个地址存储的值.

一元操作和二元操作

如果一个操作只有一个操作数,既是源又是目的,这个操作就是一元操作。
如果一个操作有两个操作数,第二个操作数既是源又是目的,这个操作就是二元操作。
如下:

指令 效果 操作
INC D D <- D + 1 一元操作
ADD S, D D <- D + S 二元操作

移位操作

类型 操作 命令 示意图
非循环移位 逻辑左移/算术左移 SHL/SAL
非循环移位 逻辑右移 SHR
非循环移位 算术右移 SAR
循环移位 不含进位位的循环左移 ROL
循环移位 不含进位位的循环右移 ROR
循环移位 含进位位的循环左移 RCL
循环移位 含进位位的循环右移 RCR

控制

条件码

除了整数寄存器,CPU还维护着一组单个位的条件码(codition code)寄存器,最常用的条件码有:

  • CF:进位标志。最近的操作使最高位产生了进位。可以用来检测无符号操作数的溢出。
  • ZF:零标志。最近的操作得出的结果为0。
  • SF:符号标志。最近的操作得到的结果为负。
  • OF:溢出标志。最近的操作导致一个补码溢出——正溢出或负溢出。

访问条件码

条件码通常不会直接读取常用的使用方法有三种:

  1. 可以根据条件码的某个组合,将一个字节设置为0或者1;
  2. 可以条件跳转到程序的某个其它部分;
  3. 可以有条件地传送数据.

对于第一种情况,举例说明一下:

指令 同义名 效果 设置条件
sete D setz D<-ZF 相等/零
setne D setnz D<-~ZF 不等/非零
sets D D<-SF 负数
setns D D<-~SF 非负数
setg D setnle D<-~(SF ^ DF)&~ZF 大于(有符号>)

SET指令。每条指令根据条件码的某个组合将一个字节设置为0或者1,而不是直接访问条件码寄存器。

跳转指令及其编码

正常情况下,指令按照它们出现的顺序一条一条地执行。跳转(jump)指令会导致执行切换到程序中一个全新的位置。在汇编代码中,这些跳转的目的地通常用一个标号(label)指明。当执行与PC(程序计数器)相关的寻址时,程序计数器的值是跳转指令后面的那条指令的地址,而不是跳转指令本身的地址。例如一段汇编代码及其反汇编代码:

1
2
3
4
5
6
7
8
9
10
    jle .L2                         #if <=, goto dest2
.l5: #dest1:
movl %edx, %eax
sar1 %eax
subl %eax, %edx
leal (%edx, %edx, 2), %edx
testl %edx, %edx
jg .L5 #if >, goto dest1
.L2: #dest2
movl %edx, %eax

1
2
3
4
5
6
7
8
8:   7e 0d        jle     17 <silly+0x17>
a: 89 d0 mov %edx, %eax
c: d1 f8 sar %eax
e: 29 c2 sub %eax, %edx
10: 8d 14 52 lea (%edx, %edx, 2), %edx
13: 85 d2 test %edx, %edx
15: 7f f3 jg a <silly+0xa>
17: 89 d0 mov %edx, %eax

我们看到在反汇编代码里的第一行,前面地址为0x8的地方,0x7e代表jle, 0xd代表要跳转的地址,但是对应的汇编地址却是0x17, 其实0x17是通过0x8处的0xd与下一行0xa的地址(正是PC程序计数器的值)相加得出的。

翻译条件分支

如何将条件表达式和语句从C语言翻译成机器代码,最常用的方式是结合有条件和无条件跳转。通过一个例子来说明:
a) 原始的C语言代码

1
2
3
4
5
6
int absdiff(int x , int y) {
if (x < y)
return y - x;
else
return x - y;
}

b) 与之等价的goto版本
1
2
3
4
5
6
7
8
9
10
11
int absdiff(int x , int y) {
int result;
if (x >= y)
goto x_ge_y;
result = y - x;
goto done;
x_ge_y:
result = x - y;
done:
return result;
}

1
2
3
4
5
6
7
8
9
10
11
# x at %ebp + 8, y at %ebp + 12
movl 8(%ebp), %edx #get x
movl 12(%ebp), %eax #get y
cmpl %eax, %edx #compare y:x 即用x-y的值更新标记位
jge .L2 # x > y
subl %edx, %eax # y = y - x
jmp .L3 #goto L3
.L2:
subl %eax, %edx #x = x - y
movl %edx, %eax #y = x
.L3: #done return x

循环

其实循环和条件分支所利用的都是jump指令和标记寄存器的值,这里就不再说明了。

条件传送指令

实现条件操作的传统方法是利用控制的条件转移。但是在现代处理器上,它可能会非常的低效率。数据的条件转移是一种替代的策略。这种方法先计算一个条件操作的两种结果,然后再根据条件是否满足选取一个。通过一个例子来说明:

1
2
3
int absdiff(int x , int y) {
return x < y ? y-x:x-y;
}

产生的汇编代码:
1
2
3
4
5
6
7
8
9
# x at %ebp + 8, y at %ebp + 12
movl 8(%ebp), %ecx #get x
movl 12(%ebp), %edx #get y
movl %edx, %ebx #copy y
subl %ecx, %ebx #compute y - x
movl %ecx, %eax #copy x
subl %edx, %eax #compute x - y, set as return value
cmpl %edx, %ecx #compare x:y
cmovl %ebx, %eax #if <, replace return value with y - x

注意这个与上面的翻译条件分支一节的汇编做比较。这种方式效率会更好。原因是跟第4章和第5章讲的处理器的结构有关系,处理器通过流水线(pipelining)来获得高性能。第二种方式可以把所有的指令都放入到流水线中,而第一种方式则需要根据条件判断哪个指令放入到流水线,从这点可以看出第二种方式总是能把指令都放入流水线中,保证了执行的效率。

switch语句

switch(开关)语句可以根据一个整数索引值进行多重分支(multi-way branching)。处理具有多种可能结果的测试时。这种语句特别有用。它们不仅提高了C代码的可读性,而且通过使用跳转表(jump table)这种数据结构使得实现更加高效。

过程

一个过程调用包括将数据(以过程参数和返回值的形式)和控制从代码的一部分传递到另一部分。另外,它还必须在进入时为过程的局部变量分配空间,并在退出时释放这些空间。数据传递、局部变量的分配和释放通过操纵程序栈来实现。

栈桢结构

为单个过程分配的那部分栈称为栈帧(stack frame)。寄存器%ebp为帧指针,而寄存器%esp为栈指针。栈帧结构(栈用来传递参数、存储返回信息、保存寄存器,以及本地存储)

转移控制

支持过程调用和返回的指令:

指令 描述
call Label 过程调用
call *Operand 过程调用
leave 为返回准备栈
ret 从过程调用中返回

call指令的效果是将返回地址入栈,并跳转到被调用过程的起始处。返回地址是在程序中紧跟在call后面的那条指令地址。
例如:

执行call的之后栈顶入栈的地址为call命令后那条命令的地址,然后ret执行后把这个地址弹出到%eip,开始执行%eip处的命令。%eip应该是程序计数器。还有一个要注意的是保存的%ebp,这个起始是上一个帧的起始地址,当前过程执行完后,这个保存的%ebp就会返回到%ebp中,更新当前帧的开始地址为上一帧的地址。

寄存器使用惯例

程序寄存器组是唯一能被所有过程共享的资源。虽然在给定时刻只能有一个过程是活动的,但是我们必须保证当一个过程(调用者)调用另一个过程(被调用者)时,被调用者不会覆盖某个调用者稍后会使用的寄存器的值。根据惯例,寄存器%eax、%edx和%ecx被划分为调用者保存寄存器。当过程P调用Q时,Q可以覆盖这些寄存器,而不会破任何P所需要的数据。另一方面,寄存器%ebx、%esi和%edi被划分为被调用者保存寄存器。这意味着Q必须在覆盖这些寄存器之前,先把它们保存到栈中,并在返回前恢复它们。

过程示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
int swap_add(int *xp, int *yp)
{
int x = *xp;
int y = *yp;

*xp = y;
*yp = x;
return x + y;
}

int caller()
{
int arg1 = 534;
int arg2 = 1057;
int sum = swap_add(&arg1, &arg2);
int diff = arg1 - arg2;

return sum * diff;
}


1
2
3
4
5
6
7
8
9
10
11
12
caller:
pushl %ebp #save old %ebp, 保存上一个帧的开始地址
movl %esp, %ebp #set %ebp as frame pointer,更新%ebp地址为当前帧的地址
subl $24, %esp #alllocate 24 bytes on stack, 申请栈空间,指针头向下移动
movl $534, -4(%ebp) #set arg1 to 534
movl $1057, -8(%ebp) #set arg2 to 1057
leal -8(%ebp), %eax #compute &arg2
movl %eax, 4(%esp) #store on stack
leal -4(%ebp), %eax #compute &arg1
movl %eax, (%esp) #store to stack
call swap_add #call the swap_add function

ps:为什么GCC分配从不使用的空间
GCC为caller分配了24个字节,但是却只是用了16个字节,这是因为GCC坚持一个x86编程指导方针,也就是一个函数使用的所有栈空间必须是16字节的整数倍。包括%ebp值的4个字节和返回值的4个字节,caller一共使用了32个字节。采用这个规则是为了保证访问数据的严格对齐(alignment)。

swap_add开始代码:

1
2
3
4
swap_add:
pushl %ebp
movl %esp, %ebp
pushl %ebx #需要寄存器%ebp做为临时存储。因为这是一个被调用者保存的寄存器。

swap_add主体代码:
1
2
3
4
5
6
7
movl    8(%ebp), %edx   #get xp
movl 12(%ebp), %ecx #get yp
movl (%edx), %ebx #get x
movl (%ecx), %eax #get y
movl %eax, (%edx) #store y at xp
movl %ebx, (%ecx) #store x at yp
addl %ebx, %eax #return value = x+y

这段代码从caller的栈帧中取出它的参数,这点需要注意。
swap_add结束代码:
1
2
3
popl    %ebx    #restore %ebx, 从栈帧中弹出之前存储的%ebx的值到%ebx,恢复%ebx的值
popl %ebp #restore %ebp, 从栈帧中弹出之前存储的%ebp的值到%ebp,恢复%ebp的值
ret #return, 弹出地址到%eip, 并且从上个帧调用call下一行的地址开始执行。

递归过程

上面描述的栈链接惯例使得过程能够递归地调用它们自身。因为每个调用过程都有它自己的私有空间,多个未完成调用的局部变量不会相互影响。

  • 递归的阶乘程序的C代码:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    int rfact(int n)
    {
    int result;
    if (n <= 1)
    result = 1;
    else
    result = n * rfact(n-1);
    return result;
    }
  • 对应的汇编代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# argument: n at %ebp + 8
# registes: n in %ebx, result in %eax
rfact:
pushl %ebp #save old %ebp
movl %esp, %ebp #set %ebp as frame pointer
pushl %ebx #save callee save register %ebx
subl $4, %esp #allocate 4 byte on stack
movl 8(%ebp), %ebx #get n
movl $1, %eax #result = 1
cmpl $1, $ebx # 比较参数 n 与 1
jle .L53 #if n <= 1, 跳到L53
leal -1(%ebx), %eax #%eax <- n-1
movl %eax, (%esp) #n-1保存到栈顶
call rfact #调用rfact(n-1)
imull %ebx, $eax #result = return value * n
.L53:
addl $4, %esp
popl %ebx
popl %ebp
ret

当n=1时,调用L53,被调用过程返回,然后继续执行imull指令 %eax 值 1


递归的阶乘函数的栈帧,这是递归调用之前的帧状态。

数组分配和访问

C语言中数组是一种将标量数据聚集成为更大数据类型的方式。

基本原则

对于数据类型T和整型常数N,声明如下:
T A[N]
它有两个效果:

  1. 它在存储器中分配一个\( L \bullet N \)字节的连续区域;这里L是数据类型T的大小(单位为字节), 用 \( x_{A} \) 来表示起始位置。
  2. 它引入了标符A;可以用A作为指向数组开头的指针,这个指针的值就是 \(x_{A}\) 。数组元素i被存放在地址为 \(x_{A} + L \bullet i\)的地方。
    通过例子加深理解:
    1
    2
    3
    4
    char    A[12];
    char *B[8]; //指针数组,数组的值是(char *)类型,指针的地址是int型的。
    double C[6];
    double *D[5];
数组 元素大小 总的大小 起始地址 元素i
A 1 12 \(x_{A}\) \(x_{A} + i\)
B 4 32 \(x_{B}\) \(x_{B} + 4i\)
C 8 48 \(x_{C}\) \(x_{C} + 8i\)
D 4 20 \(x_{D}\) \(x_{D} + 4i\)

异质的数据结构

C语言提供了两种结合不同类型的对象来创建数据类型的机制:结构(structure),用关键字struct声明, 将多个对象集合到一个单位中;联合(union), 用关键字union声明,允许用集中不同的类型来引用一个对象。

数据对齐

许多计算机系统对基本数据类型合法地址做出了一些限制,要求某种类型对象的地址必须是某个值k(通常是2,4或8)的倍数。这种对齐限制简化了形成处理器和存储器系统之间接口的硬件设计。
例如:

1
2
3
4
5
struct S1 {
int i;
char c;
int j;
};

按照最小字节分配应该是这样的:

但是要遵循字节对齐,真实的分配情况是这样的:

综合:理解指针

指针和它们映射到机器代码的关键原则:

  • 每个指针都对应一个类型。void * 类型代表通用指针。
  • 每个指针都有一个值。这个值是某个指定类型对象的地址。
  • 指针用&运算符创建。常用leal指令来计算表达式的值。int * p = &x
  • 操作符用于指针的间接引用。
  • 数组与指针紧密联系。
  • 将指针从一种类型强制转换为另一种类型,只改变它的类型,而不改变它的值。
  • 指针也可以指向函数。

存储器的越界引用和缓冲区溢出

C对于数组引用不进行任何边界检查,而且局部变量和状态信息(例如保存的寄存器值和返回地址),都存放在栈中。这两种情况结合到一起就可能导致严重的程序错误,对越界的数组元素的写操作会破坏存储在栈中的状态信息。一种常见的状态破坏称为缓冲区溢出(buffer overflow)。入下图所示:

为buf分配的空间只有8个字节元素,上面地址保存的是%ebx, %ebp和返回地址的值,如果buf的长度超过分配的长度,就会把多出来的元素写入到保存这些寄存器地址的值,破坏%ebx,%ebp和返回地址的值,这样程序就不能正确的返回%ebx的值,不能正确的返回上以栈帧的地址,不能正确返回下一条要执行的命令地址。如果这些输入的元素包含一些可执行代码的字节编码,称为攻击代码(exploit code), 比如串改返回地址的值,使程序返回到恶意程序的代码地址进行执行,就会成为我们所说的蠕虫和病毒
对抗缓冲区溢出攻击的方法:(详细的原理不再赘述,可以自己google)

  1. 栈随机化。
  2. 栈破坏检测。
  3. 限制可执行代码区域。

chapter7-12:REST章节总结

第七章:一个服务实现

本章主要是根据前面的给出的一些介绍和原则进行,用ROR对其进行一个简单的实现。其中最重要的是作者分析需求的过程以及如何根据这些需求与ROA架构进行融合。本章是设计ROA服务步骤的具体实践过程,是对第六章的一个继承。

第八章:REST和ROA最佳实践

本章主要讲REST和ROA的实现过程中遇到的一些问题,以及如何应对这些问题:

  • GET请求与HEAD请求是安全的,它们不应导致服务器状态发生改变。
  • GET、HEAD、PUT与DELETE请求应该是幂等的,想一个URL发送多次应该与只做过一次请求的效果一样。
  • POST既不是安全的也不是幂等的。
  • 遇到不支持PUT或DELETE请求的的情况,要用重载的POST请求来实现。
  • HTTP异步操作可以通过返回状态码202告诉客户端已接受到请求,正在等待处理
  • HTTP如何实现事务操作的过程比较复杂。
  • 复杂请求遵循的原则是:如果无法用统一接口适应多个动作,那就把它本身暴露为资源。
  • URL的设计要考虑到新旧版本的更迭。

其它涉及到HTTP缓存和认真等是HTTP本身的特性,不在这里介绍了。

第九章:服务的技术构件

本章第一部分是介绍在使用rest 服务是数据的表示方式,这不再说了,第二部分是如何定义请求与返回状态码。第三部分结束了WADL这个web应用描述语言,类似于SOAP的WSDL语言但它是基于REST的,虽然用法上感觉差不多,但其实是有着本质的区别的,我的理解是:WSDL和WADL都是为了简化编程,但是WSDL是根据SOAP的风格规定了调用的函数,传递的参数等,而WSDL则是遵循REST风格,规定了请求的方法、地址及请求的参数。

第十章:面向资源的架构VS大Web服务

chapter5:设计只读的面向资源的服务

本章主要是针对只读类型的ROA架构进行设计的,只读的HTTP方法包括GET和HEAD。通过一个地图的例子对具体的设计过程进行了介绍。

资源设计

RPC式架构设计方法是把系统分解为一个个的动作。REST式则是参考面向对象的程序设计,面向对象设计主要有类和方法,这里的资源设计策略可以称为:“极限面向对象”的策略,一个资源只暴露一个统一接口,最多支持六种HTTP方法。

创建只读资源

本章提出了一些列步骤来说明如何设计一个ROA的只读资源服务,它们是:

  1. 规划数据集
  2. 把数据集划分为资源
    对其中的每种资源:
  3. 用URI为该资源命名
  4. 设计发给客户端的表示
  5. 用超链接和表单把该资源与已有资源联系起来
  6. 考虑有哪些典型的事件经过
  7. 考虑可能出现哪些错误情况

这些步骤都是显而易见的,不需要花篇幅进行再描述了。关键是要在实践中去运用这些思想。

chapter6:设计可读写的面向资源的服务

本章主要是讲的如何设计一个可以读写资源的服务。需要写服务的话则需要PUT,POST操作。由于其中涉及到很多具体的例子,本博就不再赘述了,只把看到的重点总结一下。

  • 创建一个资源是用POST还是用PUT要根据上一章的原则进行选择。
  • HTML5以前FORM的method只支持GET和POST两种方式,没有PUT。为了能使用PUT可以使用发送WADL片段的方法,还可以通过第八章介绍的“重载POST发送的PUT请求”
  • 成功的HTTP的相应代码有可能是201、205等。

chapter4:面向资源的架构

这一章说的是一个具体的REST式架构——面向资源的架构(Resource-Oriented Architecture, ROA)。ROA是一种把实际问题转换为REST式Web服务的方法:它令URI、HTTP和XML具有跟其他Web应用一样的工作方式,令程序员们容易是用它们。
这一章将面向资源的架构(ROA)的功能分成:资源、资源名称、资源的表示、资源间的链接。还将介绍ROA的特性:可寻址性(addressability)、无状态性(statelessness)、连通性(connectedness)和统一接口(uniform interface)。

这里有一个容易混淆的地方:为什么有REST了还要用一个ROA呢?REST就是一个指导原则,只要满足REST原则,任何具体的设计都可以成为REST式架构。而ROA就是REST的一个具体的设计架构,可以称之为具体的架构。关于REST的设计原则请参考Roy Fielding的博士论文:《架构风格与基于网络的软件架构设计》及博客:理解本真的REST架构风格。其中总结REST的架构的6个主要的特性如下:

  • 客户-服务器(Client-Server)
    通信只能由客户端单方面发起,表现为请求-响应的形式。

  • 无状态(Stateless)
    通信的会话状态(Session State)应该全部由客户端负责维护。

  • 缓存(Cache)
    响应内容可以在通信链的某处被缓存,以改善网络效率。

  • 统一接口(Uniform Interface)
    通信链的组件之间通过统一的接口相互通信,以提高交互的可见性。

  • 分层系统(Layered System)
    通过限制组件的行为(即,每个组件只能“看到”与其交互的紧邻层),将架构分解为若干等级的层。

  • 按需代码(Code-On-Demand,可选)
    支持通过下载并执行一些代码(例如Java Applet、Flash或JavaScript),对客户端的功能进行扩展。

这些特性在介绍ROA的时候也会具体详细的介绍。
REST并不依赖于HTTP机制或URI结构,对于特定的服务来说比如Web服务来说明REST,则其具体的表现形式就是HTTP与URI,这些具体的架构形式就称之为ROA架构。如果一个REST式的服务不是基于Web的则可能不叫ROA,但是其遵循的原则都是一致的,只不过表现形式会有一些差别。

什么是资源

这里的定义是:任何事物,只要具有被引用的必要,它就是一个资源(resource)。
资源的具体形式在计算机里就是体现为比特流的事物,如文档、数据库的记录、某程序的运行结果。

URIs

在web里是什么令资源称得上为一个资源呢?那就是它必须至少有一个URI。URI既是资源的地址又是资源的名称,如果一则信息没有URI,那就不能称之为一个资源,而只能算是描述另一个资源的一些数据。
资源与URI的关系式:
URI:资源——> N:1

可寻址性

ROA的第一个特性.这个很好理解,就是每个资源都有自己的URI,这个URI在一定时间内是固定不变的,在有效的时间内用户可以通过URI来找到该资源。

无状态性

ROA的第二个特性.意味着每个HTTP请求都是完全孤立的。这个应该很好理解。
这里还有两个概念:应用状态资源状态。应用状态是要保存到客户端的,资源状态是保存在服务端的。其实这两个状态是资源与客户端所拥有的资源的状态,每个资源之间是孤立的。

连通性

这个特性是要求资源之间通过它们的表示彼此链接起来。这个特性主要是体现web的易用性。

统一接口

统一接口的意思就是要求对资源的操作要按照REST的要求进行,也就是特定的操作要使用对应的操作方法,而不是混乱的使用。
常用的HTTP方法已经介绍过了,但是有几点需要注意的是

  • PUT与POST该用哪个?这个是很容易混淆的地方,一般来说POST表示的是增加新的资源,PUT表示修改已有的资源。但是还有下面几个原则:

    • 客户端负责决定新资源采用什么URI,就用PUT;服务器负责新资源采用什么URI,那就用POST。
    • 对现有资源进行追加信息的时候采用POST。
  • 重载的POST不符合统一接口。
    这点并不好理解。其实大部分的时候我们见到的都是这种:它们用POST向一个数据加工处理程序提供数据块(a block of data )。其中一个最常见的例子就是form表单的提交。这种方法是通过单个HTTP方法表达无数个非HTTP方法。这种重载的POST不应该被用于掩饰拙略的资源设计,这个时候可以通过调整资源设计来做到统一接口。具体如何做,还要等了解再补充。