oohcode

$\bigodot\bigodot^H \rightarrow CODE$

chapter3:REST式服务有什么特别不同

前面讲了不少关于Rest的知识,但是现在大部分都是RPC或者REST-RPC混合的模式,这种并不是纯粹的Rest架构。本章就是为了介绍REST架构及面向资源的服务到底是什么样的。

资源

Amazon S3为例子进行介绍。S3提供SOAP和REST两种服务方式,一个桶就是一个放置资源的仓库,桶列表就是仓库的列表,一个对象就是放置到仓库里的一个资源。S3的资源及其方法如下:

GET HEAD PUT DELETE
桶列表 列出所有桶 - - -
一个桶 列出桶里的对象 - 创建桶 删除桶
一个对象 获取对象的值及元数据 获取对象的元数据 设置对象的值及元数据 删除对象

从表中可以看出每个方法的作用都是名副其实的。

HTTP相应代码

利用HTTP响应代码是REST架构的另一个标志象征。程序根据HTTP响应代码来判断响应的内容是正确的还是错误的。

一个S3客户端

下面我们根据S3客户端编写的代码来剖析REST服务。

对桶的操作

获取桶的列表(GET方法):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# 桶列表
class BucketList
inlcude Authorized

#获取该用户的所有桶
def get
buckets = []
#向桶列表的URI发送GET请求,并读取返回的XML文档
doc = REXML::Document.new(open(HOST).read)

#对于每个桶...
REXML::XPath.each(doc, "//Bucket/Name") do |e|
#...创建一个Bucket对象,并把它添加到列表中
bucket << Bucket.new(e.text) if e.text
end
return buckets
end
end

保存或更新桶(PUT方法)和删除桶(DELETE方法):
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
# 一个S3桶
class Bucket
include Authorized
attr_accessor :name

def initialize(name)
@name = name
end

#桶的URI等于服务的根URI加上桶名
def uri
HOST + URI.escape(name)
end

#在S3上保存这个桶
#类似于在数据库里保存对象的ActiveRecord::Base#save.
def put(acl_policy=nil)
#设置HTTP方法,作为open()的参数
#同时为该桶设置S3访问策略(如果有提供的话)
args = {:method=> :put}
args["x-amz-acl"] =acl_policy if acl_policy

#向该桶的URI发送PUT请求
open(uri, args)
return self
end

#删除该桶
#如果该桶不为空的话,改删除操作将失败
#并返回HTTP响应代码409("Conflict")
def delete
open(uri, :method => :delete)
end

对对象的操作

获取对象(GET方法)

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
# 获取桶里的全部或部分对象
def get(options={})
#获取该桶的基准URI,并把子集选项
#附加到查询字符串上
uri = uri()
suffix = '?'

#对于用户提供的每个选项...
options.each do |param, value|
#...如果属于某个S3子集选项...
if [:Prefix, :Maker, :Delimiter, :MaxKeys].member? :param
#...把它附加到URI上
uri << suffix << param.to_s << '=' << URI.escape(value)
suffix = '&'
end
end

#现在我们已经构造好了URI,向该URI发送GET请求,
#并读取含有S3对象信息的XML文档
doc = REXML::Document.new(open(uri).read)
there_are_more = REXML::XPath.first(doc, "//IsTruncated").text == "true"

#构建一个S3::Object对象的列表
objects = []
#对于桶里的每个S3对象...
REXML::XPath.each(doc, "//Contents/Key") do |e|
#...构建一个S3:Object对象,并把它添加到列表中
objects << Object.new(self, e.text) if e.text
end
return objects, there_are_more
end

获取对象数据(GET方法)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# 跟某个桶关联的一个具有值和元数据的S3对象
class Object
include Authorized

#客户端可以知道对象在哪个桶里
attr_reader :bucket

#客户端可以读写对象的名称
attr_accessor :name

#客户端可以写对象的元数据和值
attr_writer :metadata, :value

def initialize(bucket, name, value=nil, metadata=nil)
@bucket, @name, @value, @metadata = bucket, name, value, metadata
end

#对象的URI等于所在桶的URI加上该对象的名称
def uri
@bucket.uri + '/' + URI.escape(name)
end

获取对象元数据(HEAD方法)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# 获取对象的元数据hash
def metadata
#如果没有元数据...
unless @metadata
#向对象的URI发送一个HEAD请求,并从响应的HTTP报头里读取元数据
begin
store_metadata(open(uri, :method => :head).meta)
rescue OpenURI::HTTPError => e
if e.io.status == ["404", "Not Found"]
#假如没有元数据是因为对象不存在,这不算错误
@metadata = {}
else
#其他情况,做错误处理
raise e
end
end
end
return @metadata
end

获取对象数据(GET方法)
1
2
3
4
5
6
7
8
9
10
11
12
13
# 获取对象的值和元数据
def value
#如果没有值...
unless @value
#向对象的URI发送GET请求
response = opne(uri)
#从相应的HTTP报头里读取元数据
store_metadata(respons.meta) unless @metadata
#从实体主体里读取值
@value = response.read
end
return @value
end

保存对象数据(PUT方法)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# 在S3上保存对象
def put(acl_policy=nil)
#以原始元数据的副本开始,或者
#如果没有元数据的话,就以空hash开始。
args = @metadata ? @metadata.cloen :{}
#设置HTTP方法、实体主体及一些另外的HTTP报头
args[:method] = :put
args["x-amz-acl"] = acl_policy if acl_policy
if @value
args["Content-Lenght"] = @value.size.to_s
args[:body] = @value
end

#向对象的URI发送PUT请求
open(uri, args)
return self
end

删除对象数据(DELETE方法)
1
2
3
4
5
# 删除对象
def delete
#向对象的URI发送DELETE请求
open(uri,:method => :delete)
end

通过上面这些例子的介绍,我们对每个方法的使用都有了一个大致的印象,知道每个方法对应的应用场景。实际的使用过程中是如何把他们关联起来的呢?下面再对它们的应用通过一个例子做一个综合的介绍。

使用S3客户端

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
# !/usr/bin/ruby -w
# s3-sample-client.rb
require 'S3lib'

# 收集命令行参数
bucket_name, object_name, object_value = ARGV
unless bucket_name
puts "Usage: #{0} [bucket name] [object name] [object value]"
exit
end

# 找到或创建桶
buckets = S3::BucketList.new.get #GET /
bucket = buckets.detect{ |b| b.name == bucket_name }
if bucket
puts "Found bucket #(bucket_name)."
else
puts "Could not find bucket #{bucket_name}, creating it"
bucket = S3::Bucket.new(bucket_name)
bucket.put
end

# 创建对象
object = S3:Object.new(bucket, object_name)
object.metadata['content-type'] = 'text/plain'
object.value = object_value
object.put #PUT /{bucket}/{object}

# 对桶里的每个对象...
bucket.get[0].each do |o| #GET /{bucket}
#...打印出有关该对象的信息
puts "Name: #{o.name}"
puts "Value: #{o.value}" #GET /{bucket}/{object}
puts "Metadata hash: #{o.metadata.inspect}"
puts
end

用ActiveResource创建透明的客户端

安装ruby on rails,然后利用rails生成一个创建一个自己的app,根据书里的过程并参考《Ruby On Ralis 3 Toturial》中的例子就可以完成。建立完成后可以在页面中看到notes的操作了,可以增加,编辑,删除note,可以到在views里可以看到这么一段代码:

1
<td><%= link_to 'Destroy', note, method: :delete, data: { confirm: 'Are you sure?' } %></td>

ruby中前端是通过method来指定HTTP请求的发送类型的,但是我在web端看到的请求是:
1
2
3
Request Method:POST
Form Data:
_method:delete

从这里来看ruby并没有在HTTP请求的方法中就变为DELETE,而是在from里传了一个参数,这并不是符合标准的REST架构的。鉴于我对ROR还不了解,现在可能有些不太明白,先看着后面的章节,等了解后再完善本章内容。

chapter2:编写Web服务客户端

本章主要是介绍如何编写一个简单的Web Service客户端服务,但是其中涉及到一些知识是之前我没有注意和研究过的,所以这一章学习一下还是很有用的。

封装库、WADL和ActiveResource

web服务用的是HTTP请求,但是HTTP请求的过程却是大同小异的,所以如果每次请求都要自己写整个过程的话确实比较麻烦,而且容易出bug。最好的方法是对它们进行封装,封装库确实简化了web服务编程,但是还有一个问题就是这个封装库有很多种,没有一个统一的标准,于是又一个想法冒出来了:可不可以抽象服务间的差异,使一个封装库能够用于所有的REST式和混合式架构?答案当然是可以,了解SOAP的都知道有一个WSDL转本用来描述服务的格式。而REST社区就这个标准还没达成一致,这里作者推荐的是WADL作为面向资源的描述语言。

用HTTP库发送请求

不同的语言有不同的HTTP库来封装底层的HTTP请求,这里如果要构建一个完全通用的Web客户端,HTTP库需要具有以下特征:

  • 必须支持HTTPS和SSL证书验证。主要是为了安全传输。
  • 必须支持至少五个主要的HTTP方法:GET/HEAD/PUT/POST/DELETE。因为这是REST风格所必须的。
  • 必须允许定制PUT或POST请求的实体主体数据。
  • 必须允许定制请求的HTTP报头。
  • 必须能够获取HTTP响应的响应代码及报头。
  • 必须支持通过HTTP代理进行HTTP通信。

可选特性:

  • 为了节省带宽,需要支持压缩的报头(Encoding)
  • HTTP库能够自动把返回的响应缓存起来
  • HTTP库应该透明的支持常见的HTTP认证形式
  • HTTP库应该能够透明地作HTTP重定向
  • HTTP库能够解析并创建HTTP cookie字符串

用XML解析器处理相应

这里重点的内容是XML的解析器的几种不同的原理。
有两种XML的解析策略:

  • 基于文档的(document-based)策略(如DOM等树式解析器)
  • 基于事件的(event-based)策略(如SAX及“拖”式解析器)

基于文档的策略
这种策略是最简单的,先把整个文档加载进来,然后根据XPath等把整个文档转换为树式结构,然后就可以处理所需的节点了。这种方案的缺点就是必须要有完整的XML文档才行。
SAX式或拖式(pull)解析器
这种是把XML文档转换为事件流,而不是数据结构。首标签(staring tag)、尾标签(closing tag)、XML注释(comments)及实体声明(entity declaration)等都是事件(event)。
如果每个事件都要处理则拖式解析器是很有用的。拖式解析器允许一次处理一个事件,处理完一个事件后再从事件流拖出下一个事件,是一个类似先进先出的链表。
如果只关心部分事件,那么SAX则更有用。向SAX解析器注册一些回调方法。一旦定义好回调方法,解析器将按照既定的、跟文档无关的步骤执行下去:解析器将把XML文档转换为一系列事件,并连接处理文档中的每个事件;每当一个事件与回调方法所对应的事件匹配时,解析器就会触发该回调方法,执行你定义的代码;回调方法执行结束后,SAX解析器将接着继续处理事件序列。关于PHP的基于事件的XML解析器可以参考php文档中的说明。
基于文档的解析器优点是:可以随机访问文档中的内容;而对于基于事件的解析器,事件触发以后就没有机会再处理了。如果XML格式有问题,基于事件的解析器直到遇到错误的地方才会报错和崩溃。
实际使用过程中可以根据自己的需求选择不同的XML文档解析器。

chapter1:Programmable Web及其分类

本章首先对Programmable Web的定义进行了通俗的介绍,然后有讲了最基础的HTTP协议的相关知识,以及对各种web service架构与HTTP协议的密切关系进行了介绍,让读者对不同的web service架构有了一个初步的认识。

Programmable Web的分类

首先要明确是什么事Programmable Web。这里主要是与Human Web进行了对比,下面是对它们的定义:

  • Programmable Web: 面向计算机程序使用的Web
  • Human Web: 面向人类使用的web

这么说其实很不好理解,但是介绍它们之间的不同之处是就很好理解了:

  • Programmable Web:数据主要是供计算机进行读取分析的,对人类不友好的,比如XML,json等数据格式
  • Human Web: 主要是供人类读取的,比如HTML页面等

说道Programmable Web的分类,其实现在主要是根据实现的技术(URI, SOAP, XML-RPC等),或者背后的架构与设计思想进行分类的。根据技术进行分类其实存在缺陷,在某些场合容易混淆,只有根据其架构进行分类才是王道。

HTTP信封里的文档

HTTP协议作为web的基础,也作为Programmable Web分类的重要标志,需要在这里进行简要的介绍一下(关于HTTP协议的详细介绍请参考《HTTP协议详解》这本书)。

  • HTTP方法: 如同编程语言的方法名,表示客户端希望服务器如何处理该信封。
  • 路径(path):是URI主机后面的部分,表示信封的地址。
  • 请求报头(request headers):是一组k-v对,起到元数据的作用。
  • 实体主体(entity-body):是放在信封里的文档。

HTTP相应可分为三个部分:

  • HTTP相应代码
  • 响应报头
  • 实体主体

方法信息

客户端发起什么样的请求,对应的服务器端采取何种方式操作数据,这就是方法信息。按照REST的Web服务的做法,增删改查的方法信息都放在了HTTP方法里,分别对应的是POST/DELETE/PUT/GET。有些则是把这些信息放到URI信息里进行传递。还有些是放到实体主体和HTTP报头里,比如SOAP服务请求的方法细节都在WSDL文件里。

作用域信息

客户端如何告诉服务器对那些数据进行操作,这种信息就是作用域信息。作用域信息的放置也可以是URI或者SOAP服务的实体主体里。

相互竞争的服务架构

常见的web服务架构有三种:REST式架构、RPC式架构和RPC-REST式架构。

REST式面向资源的架构
REST结构意味着,方法信息都在HTTP方法里;面向资源的架构(SOA)意味着,作用域信息都在URI里。REST的具体细节将在后面进行详细的介绍。
RPC式架构
RPC式web服务通常从客户端收到一个充满数据的信封,然后发回一个同样充满数据的信封。RPC式架构意味着:方法信息和作用域信息都在信封里或报头里。HTTP是一种常见的信封格式,SOAP也是一种常见的信封格式,但是SOAP是通过HTTP进行传输的。
REST-RPC式架构
混合式的架构是指不是彻底的贯彻REST风格的架构,既有一部分是符合REST架构的要求,又有一部分是RPC架构的要求,这些或者是有意的,或者是无意的。

同步与异步,阻塞与非阻塞

同步异步及阻塞与非阻塞是在开发过程中经常遇到的名词,但是却经常容易混淆的名称,这里根据自己的理解对他们进行简要的介绍,以便自己能够清晰的记住他们之间的区别和联系

说道同步与异步,阻塞与非阻塞这两个概念,我一直都很混淆,相信很多人也和我一样对它们之间的单个记起来很容易,但是放到一起的时候就晕了,这其实是对它们了解不清楚导致的。这里我根据自己的理解说下他们的本质区别:
阻塞与非阻塞的区别就是一个进程在等待一个事情处理的时候能不能干其他的事情。
举个例子:假设一个进程为P,任务分为T1,T2,T3等。

  • 阻塞模式下
    P从T1开始执行,如果T1没有执行完返回则P一直在挂起状态,只有T1执行完了才重新唤醒P,执行T2任务。注意重点是:这期间P只能等待,不能干其他的事情。
  • 非阻塞模式下
    P从T1开始执行,这是不管T1有没有执行完毕都会返回一个结果,则P会继续执行其他任务,比如说T2,再执行T3等,也就是P不用关心所执行的任务是否执行完毕,只需要任务及时给出相应即可,这时如果T1执行完毕则会主动通知P,告诉P任务执行完了。可以看出P一直出去激活状态,不管是执行的一个任务还是多个任务,它不会因为一个任务没有执行完就等待。

异步与同步的区别就是进程在干多个事情的时候,这些事情是不是存在顺序关系。
举个例子:还是假设一个进程为P,任务分为T1,T2,T3等,每个任务又分为多个步骤,例如T1分为T1.1, T1.2, T1.3等步骤。

  • 同步模式下
    在同步模式下,任务的执行是有先后顺序之分的。比如T1,必须T1.1完了才能进行T1.2步骤,T1.2步骤完了才能进行T1.3步骤。所以当P执行T1的时候必须等T1.1执行完了才能执行T1.2,T1.3等任务。
  • 异步模式下
    异步模式是没有顺序之分的,P可以执行T1的步骤安装任何的顺序,因为这些步骤之间不存在依赖关系。

如果按照以上的理解(我得理解,如果有问题,恳请指出~),我觉得其实他们之间的关系就可以分得很清楚了~ 下面再来看看他们的混合情况:

  • 同步阻塞: 这个其实可以根据他们各自的定义来看,还以上面的为例。P执行T1三个阶段的任务以及T2,T3等任务,先执行T1.1,由于是同步所以,必须等T1.1执行完了才能继续往下进行,有由于是阻塞过程,所以在等待T1.1执行的过程中P是挂起的,T2,T3等任务必须处于等待状态。
  • 同步非阻塞: 这个过程还是同步的。前面和同步阻塞一样,必须等T1.1执行完了才能执行T1.2和T1.3,但是T2和T3和T1并没有顺序关系,所以当等待T1.1执行的过程中,P可以继续执行T2和T3,这就是非阻塞。
  • 异步阻塞:这个组合其实是不存在的。首先因为是异步的,也就是说P可以任意顺序执行T1,T2,T3,执行的过程中他们并不会相互等待,进程更不会挂起,所以没有这个组合。也有的说这个状态是存在的,因为执行T的过程虽然可以同时进行,但是等待消息的过程是可以是阻塞的,也就是任务同时发出,等待任务结果的这段时间进程是处于挂起状态的,我觉得这其实跟你执行的任务多少以及任务的执行时间是密切关联的,如果同时发出很多任务,而任务需要执行很长时间,那么应该有一段时间进程是挂起的,但是纵观整个过程来看,这样说并不合适,所以我认为不存在这种状态。
  • 异步非阻塞: 异步非阻塞是执行效率最高的模式,一个进程可以同时发出很多任务,比如说T1,T2,T3等,而它们之间并无顺序关系,那个任务先执行完就可以先进行回调,执行其它任务。这样P的利用率就可以达到最大。

csapp chapter2:信息的表示和处理

本章主要是对各种信息在计算机中是如何表示的进行了详细的介绍。特别是不同的数据类型,比如浮点数的表示方式,进行了非常深入的介绍,学习本章对科学计算以及程序中出现的莫名其妙的数据错乱会有很好的理解。

(由于本章对了解计算机系统的运行过程相关性不是太大可以先不阅读)

SOAP and REST

SOAP(Simple Object Access Protocol)和REST(Representational State Transfer)是现在Web Service开发中最流行的两个架构,这篇文章详细分析了他们的来源以及优缺点。

作为一个web后台开发人员我竟然拿分不清web service 和 web server!!!自觉面壁~~~
所以我做不住了,我睡不着了,听说了REST后我知道了SOAP,知道SOAP后我知道了WSDL,但是始终百思不得其解,原来是我最基本的东西都不知道,何谈了解他们!所以之能静下心来从头开始了!

web server与 web service

其实这两个差的太远了,如果不细看还以为是一个东西,因为写法差不多,但是仔细品味一下就知道了:

  • web server是指web服务器,如果apache、nginx等,主要是用来用来处理HTTP服务的底层软件(下次把学到的web server的发展说一下)。
  • web service是指基于web的一个服务,是web技术的一个应用。就像你开发一个网站就是一个service,提供一个HTTP的API接口也是service。

其实web service这个东西只要是做web开发或者移动端开发的人都会用到,只不过各有各的用法,各有各的标准。而这里的SOAP和REST只是在众多的应用中提取出来的通用的协议标准。当然拟开发的时候也可以都不用都没有任何关系。
再看看W3C对Web service的定义:

  1. Web Services 是应用程序组件
  2. Web Services 使用开放协议进行通信
  3. Web Services 是独立的(self-contained)并可自我描述
  4. Web Services 可通过使用UDDI来发现
  5. Web Services 可被其他应用程序使用
  6. XML 是 Web Services 的基础

其实SOAP和REST主要解决的是一个分布式应用的情况,什么是分布式应用呢?我的理解是比如一个MVC结构的网站。一般情况下都会把所有代码放到一台前端机器上,我们开发的时候也是C直接调用M的函数接口。这样对于一般应用是满足的,但是存在两个问题:

  1. 网站发展的一定程度后所有的服务压力都在前端机器不能满足业务需求
  2. 为了提高性能M采用的是更快速的语言开发的,C采用的是另一种语言,它们之间怎么通信?

针对第一个问题,我们知道M和C的作用不一样,M主要是底层服务,而C主要是业务逻辑相关的服务,网站开发过程中其性能瓶颈主要是集中在M这一层,所以有可能我们需要把M和C完全分离开,M放在单独的底层服务器,C放在前端机,这样就可以各司其职了。但是这样做就会面临第二个问题,它们之间如何通信?比如C需要调用M的某个函数,怎么实现?这就是SOAP和REST发挥作用的地方了,只要C和M都遵循这两个标准开发,整个世界就都清净了~
(详细的过程下面将具体的协议时会说道。)

RPC协议

说道这两个协议,不得不提一下RPC协议,其实他们都是在RPC的思想上发展起来的~
《TCP/IP协议详解卷一:协议》这本书的第29章网络文件系统介绍了RPC协议。
当终端用户编程的时候其实是不关心网络传输细节的,比如一个C/S架构的程序,Client只需要把网络的数据流当做是一个文件进行操作就可以了,不管是向服务器发送请求还是接受来之服务器的请求都只需调用系统函数就可以实现;同样Server端也是只需要调用系统函数获取客户端发出的请求并且队请求进行处理并把处理结果返回到客户端就行了。C和S之间的交互都是通过网络实现的,但是对于两端来说并无这个过程是透明的。中间的过程其实就是由RPC协议实现的,交互的过程中底层发生的细节入下:

  1. 当客户程序调用远程的过程时,它实际上只是掉哟呵过年了一个位于本机上的、有RPC程序包生成的函数。这个函数被称为客户残桩(stub)。客户残桩将过程的参数封装在一个网络报文,并且将这个报文发送给服务器程序。
  2. 服务器主机上的一个服务器残桩负责接收这个网络报文。它从网络报文中提取参数,然后调用应用程序员编写的服务器过程。
  3. 当服务器函数返回时,它返回到服务器残桩。服务器残桩提取返回值,把返回值封装成一个网络报文,然后将网络报文发送给客户残桩。
  4. 客户残桩从接收到的网络报文中取出返回值,将其返回给客户程序。

一个RPC程序包提供好很多好处:

  • 程序设计更加容易,因为很少或几乎没有涉及到网络编程。应用程序设计院只需要编写一个客户程序和客户程序调用的服务器过程。
  • 如果使用了一个不可靠的协议,如UDP,像超时和重传等细节一样就由RPC程序包来处理。这就简化了用户应用程序。
  • RPC库为参数和返回值的传输提供任何需要的数据转换。

下面这个图片可以展示一下在报文中RPC所处的位置:
RPC报文
RPC报文

SOAP协议

上节说道的RPC协议是应用于网络通信服务,但是HTTP却不是为此设计的,RPC 会产生兼容性以及安全问题;防火墙和代理服务器通常会阻止此类流量。对于web应用来说通过HTTP协议进行通信是更好的方法,因为 HTTP 得到了所有的因特网浏览器及服务器的支持。SOAP 就是被创造出来完成这个任务的。
SOAP 提供了一种标准的方法,使得运行在不同的操作系统并使用不同的技术和编程语言的应用程序可以互相进行通信。(以上来W3C)

Web services 平台的元素:

  1. SOAP (简易对象访问协议)
  2. UDDI (通用描述、发现及整合)
  3. WSDL (Web services 描述语言)

这三个元素是组成SOAP服务必须标准组件,一个完整的SOAP交互过程可以用下面这个图来表示(下图是基于自己的理解画的,如果不妥之处还请指出):
SOAP flow
为了更好地理解SOAP协议,这里举一个例子:用PHP大家SOAP server服务
首先要安装soap服务:

1
yum install php-soap

然后在服务端建立两个文件
user.wsdl
其内容如下:
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
<?xml version="1.0" encoding="ISO-8859-1"?>
<definitions xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:SOAP-ENC="http://schemas.xmlsoap.org/soap/encoding/" xmlns:tns="http://www.somelocation.com" xmlns:soap="http://schemas.xmlsoap.org/wsdl/soap/" xmlns:wsdl="http://schemas.xmlsoap.org/wsdl/" xmlns="http://schemas.xmlsoap.org/wsdl/" targetNamespace="http://www.somelocation.com">
<types>
<xsd:schema targetNamespace="http://www.somelocation.com">
<xsd:import namespace="http://schemas.xmlsoap.org/soap/encoding/" />
<xsd:import namespace="http://schemas.xmlsoap.org/wsdl/" />
</xsd:schema>
</types>
<message name="userDataRequest">
<part name="operation" type="xsd:string" />
<part name="statement" type="xsd:string" />
</message>
<message name="userDataResponse">
<part name="return" type="xsd:string" />
</message>
<portType name="userWsdlPortType">
<operation name="userData">
<documentation>Query User Data</documentation>
<input message="tns:userDataRequest" />
<output message="tns:userDataResponse" />
</operation>
</portType>
<binding name="userWsdlBinding" type="tns:userWsdlPortType">
<soap:binding style="rpc" transport="http://schemas.xmlsoap.org/soap/http" />
<operation name="userData">
<soap:operation soapAction="http://www.somelocation.com#feelbad" style="rpc" />
<input><soap:body use="encoded" namespace="http://www.somelocation.com" encodingStyle="http://schemas.xmlsoap.org/soap/encoding/" /></input>
<output><soap:body use="encoded" namespace="http://www.somelocation.com" encodingStyle="http://schemas.xmlsoap.org/soap/encoding/" /></output>
</operation>
</binding>
<service name="userWsdl">
<port name="userWsdlPort" binding="tns:userWsdlBinding">
<soap:address location="http://www.soap.com/service.php" />
</port>
</service>
</definitions>

然后在服务端建立两个文件

service.php
内容如下:

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
<?php

//设置不缓存wsdl
ini_set("soap.wsdl_cache_enabled","0");

//初始化wsdl服务
$server = new SoapServer("user.wsdl");

//主功能性的class,这里可以分离出来写各种复杂的逻辑
class USER {

function getInfo($userId) {
return json_encode(array('userId'=>$userId,'userName'=>'Zhang San'));
}

function getGroup($userId) {
return json_encode(array('userId'=>$userId,'userGroup'=>'111'));
}

}

//接口的主入口函数
function userData($operation,$statement){
return USER::$operation($statement);
}

//注册主函数
$server->AddFunction("userData");

//启动soap server
$server->handle();

?>

然后在客户端建立一个访问的文件
client.php

1
2
3
4
5
6
7
<?php

# $client = new SoapClient('http://www.soap.com/service.php?wsdl');
$client = new SoapClient('http://www.soap.com/service.php?wsdl');
$res1 = $client->__soapCall('userData',array('operation'=>'getInfo','statement'=>'111'));
print_r($res1);
?>

上面这个例子客户端和服务端都是PHP写的,其实他们可以是不同的语言,只需要这个语言支持SOAP协议即可。
根据以上的例子我相信你已经能对SOAP有个清晰的定义了。根据SOAP的特点可以知道SOAP对于分布式的服务和跨语言的通信提供了一个很好的方案,但是就不足的就是它需要额外定义的SOAP协议,WSDL描述性文件等,具有一定的复杂性。随着HTTP协议的发展,它本身其实已经具有了独立承担这个服务的能力了,利用HTTP协议本身我们能够做得更好,这就是下面要讲的REST协议。

REST协议

在读这个之前请先移步到理解本真的REST架构风格这篇文章

深入理解计算机操作系统之写在前面

这个系列的博客主要是对《深入理解计算机操作系统》(Computer Systems A Programmers’s Perspective 以下简称CSAPP)这本书的学习记录和感悟。

这本书让我学到了很多东西,特别是对程序运行的过程和程序在计算机系统中的组织结构。以前也学过单片机原理,但是跟这个比起来就有点小打小闹了,这本书在豆瓣也是超高人气的。希望自己能通过写博客的方式对它进行更加深入的研究,让自己有些提高~

csapp chapter1:计算机系统漫游

第一章是对本书的概括性介绍,虽然是概括介绍但是仍然有不少猛料,比如一个c语言是如何编程可执行文件的,计算机的存储器是如何构成的,一个进程在计算机虚拟空间中的结构等等,这些问题能够理解了我感觉自己在写程序的时候有种豁然开朗的感觉,没写一行代码仿佛都看到它们是怎么进入计算机并执行的,实在是爽啊~

信息就是位+上下文

这里说的位其实就是信息其实就是一些字符串组成的,但是不同的信息有着不同的作用,这其实是跟这些信息所处的上下文环境有关系,同样的一串字符在不同的环境中发挥不同的作用~

程序的翻译过程

一个C编写的程序需要经过这怎么样的过程才能让计算机识别并执行?话不多说直接上图
程序翻译过程
从图中可以看到一个c语言源程序需要经过预处理器处理生成.i文件,然后在经过编译器的处理编程.s文件,最后经过汇编器处理编程.o的二进制文件,你以为这样就可以了?no,一个源程序需要其他库文件的支持,还要经过链接器才能生成可执行文件。
预处理阶段
这个阶段会对你写的源代码进行预处理,首先是找到#开头的行,比如说#include 这时候就需要加载需要的文件。最后形成了一个完整的源文件内容:以.i结尾的文件。
编译阶段
编辑器在这个阶段会发挥作用把C语言转换为汇编语言。(由于没有学过编译原理,这里只能囫囵吞枣了,下一步准备研究一下龙书)。
汇编阶段
这个阶段汇编器把.s结尾的汇编文件转化为以.o结尾的二进制文件
链接阶段
经过上面的几个步骤后程序还不能真正的运行起来,需要将程序用到的系统类库等于这个二进制文件进行组合,最后生成一个可执行的目标文件,这个目标文件就可以执行了~
这里还提到了解编译系统的工作原理是大有益处的,其中主要的几点就是可以优化程序性能、理解链接是出现的错误、避免安全漏洞等,这些在以后的章节中会介绍到。

计算机系统的硬件组成

还是直接上图:
计算机系统硬件组成
从图中可以看到一个计算机系统的组成主要包括一下几个重要部分:总线(bus)、IO设备、主存(Main memory)、处理器(CPU)。下面对它们的作用一一介绍:
总线
总线是贯穿整个系统的一组电子管道,它携带信息并在各个部件之间进行传递。总线通常被设计成传送定长的字节块,也就是字(word)。总线宽度决定了计算机系统的寻址能力,因为每根总线代表的是一位bit,我们所说的32bit系统其实就是说总线的宽度为32,也就是4字节,而现在的64bit就是说总线宽度为64位。可见总线的宽度决定了计算机的寻址能力。
IO设备
IO设备这个更好理解了,就是我们能够看到的键盘、鼠标、显示器以及看不到的磁盘,这些都是进行与终端用户进行交互的或者是永久存储数据的。
主存
主存是一个临时的存储设备,当程序运行时程序本身的以及程序运行时需要处理的数据。其实就是我们经常说的内存,服务器内存不够用了很有可能是打开的程序太多或者程序处理的数据太大导致内存被占用过多。从物理上说主存就是一个动态随机存储器(DRAM),从逻辑上说主存其实就是一个线性的数组,每个字节都对应一个唯一的地址。
处理器
处理器全称是中央处理单元(CPU),它是解释或执行存储在主存中指令的引擎。处理器的核心是一个字长的存储设备(或寄存器),称为程序计数器(PC),在任何时刻程序计数器都是指向主存中某条机器语言指令的(即程序计数器存储的是主存中某条指令的地址)。
处理器是整个计算机系统的核心,CPU的操作主要是围绕主存、寄存器文件(register file)和算术/逻辑单元(ALU)进行的。寄存器文件是1字长的寄存器组成的存储设备,没个寄存器都有一个唯一的名字。ALU计算新的数据和地址值。下面简要说一下CPU执行的一些操作:

  • 加载: 把一个字节或一个字从主存复制到寄存器,已覆盖寄存器原来的内容。
  • 存储: 把一个字节或一个字从寄存器复制到主存的某个位置,已覆盖这个位置上原来的内容。
  • 操作: 把两个寄存器的内容复制到ALU,ALU对这两个字做算术操作,并将结果存放到一个寄存器中,已覆盖该寄存器中原来的内容。
  • 跳转: 从指令本身中抽取一个字,并将这个字复制到程序计数器(PC)中,以覆盖PC中原来的值。

指令集结构描述的是每条机器代码指令的效果,微体系结构描述的是处理器实际上是如何实现的。

一个hello world的自白:
我是一个hello world,我使用C语言写的,我被翻译后变成了可执行文件,下面是我如何从可执行文件显示到显示器的过程,过程晦涩,大牛勿入:
下面是执行的过程:

1
2
./hello.out
hello world!

下面是计算机系统所做的工作:
读取用书的输入-->在当前目录下勋章可执行文件-->把可执行文件从磁盘复制到主存-->处理器执行main函数-->把要输出的hello world!从主存复制到寄存器文件-->寄存器内容复制到显示器
前面的工作其实是由外壳程序(其实就是shell)控制的

存储设备的层次结构

根据机械原理,较大的存储设备要比较小的存储设备运行的慢,但是快速设备的成本却别低速设备高。根据这个原理我们知道处理速度方面寄存器文件>主存>磁盘。而成本也是这样,为了平衡成本和速度,以及适应不同的设备,需要在它们之间再加一个缓存设备,叫做高速缓存存储器。它能够存储更多的信息同时也有更快的速度。这就形成了存储设备的层次结构。直接上图:
存储设备的层次结构
了解存储设备的层次结构可以利用这种特点提高程序的性能,这点在第六章会做介绍。

操作系统管理硬件

操作系统有两个基本功能:

  1. 防止硬件被失控的应用程序滥用
  2. 向应用程序提供简单一致的机制来控制复杂而又通常大相径庭的低级硬件设备

操作系统是通过进程、虚拟存储器和文件等抽象概念来实现这两个功能的,这种抽象表示如图所示:
进程的上下文切换
下面对他们进行简要的介绍:
进程是操作系统对一个正在运行的程序的一种抽象,一般来说一个CPU同时只能够运行一个进程,一个CPU运行多个进程是需要通过时间片轮转进行上下文切换。下面这幅图展示了一个CPU如何运行多个进程的:
进程的上下文切换
虚拟存储器是一个抽象概念,它为每个进程提供了一个奸相,即每个进程都在独占地使用主存,每个进程看到的是一致的存储器,称为虚拟地址空间。一个进程的虚拟地址空间如下图所示:
进程的上下文切换
可以看到虚拟地址空间分为几部分,每个区都有专门的功能。下面从地址空间的最下方向上进行一一介绍:

  • 程序代码和数据: 对于所有的进程来说,代码是从同一固定地址开始。紧接着是全局变量和静态变量相对应的数据位置。代码区和数据区是直接按照可执行目标文件的内容进行初始化的。这两个区域是从程序一开始运行是就规定好了,不会再做改变了。
  • 堆: 代码区和数据区后紧随着的是运行时堆。这个区的空间是动态的伸缩的,通过malloc和free这样的C标准库函数进行申请和释放空间。
  • 共享库: 在地址空间的中间部分,是一块用来存放像C标准库和数学库这样共享库的代码和数据的区域。这部分会再第七章介绍是如何工作的。
  • 栈: 位于虚拟地址空间顶部的是用户栈,编译器用它来实现函数调用,比如保持局部变量,保存函数调用的现场等等。
  • 内核虚拟存储器: 内核总是驻留在内存中,是操作系统的一部分。这部分不允许应用程序读写。

ps:注意到堆的地址空间是向上增长的,而栈的地址空间是向下增长的。这样设计是为了最大限度的利用地址空间。可以根据堆和栈的需求动态分配地址空间。
文件
文件其实就是字节序列。每个IO设备都可以看做是文件。系统的所有输入输出都是通过使用一小组成为Unix I/O的系统函数调用读写文件来实现的。第十章会进行详细的介绍。

系统之间利用网络通信

不同的系统之间通过网络进行通信。详细过程会在第十一章进行介绍。

其它

下面几个概念贯穿了计算机系统的所有方面。我们需要铭记在心:

  • 并发(concurrency): 是一个通用的概念,指一个同时具有多个活动的系统;
  • 并行(parallellism): 指的是用并发使一个系统运行的更快。

并发和并行的却别是一个很容易混淆的概念,下面通过一个简单的图像来表述他们的区别:
并发与并行
可以看出并发是在一个时间段内执行不同的任务,但是同一个时间点只能执行一个任务,通过时间片轮转来进行任务的切换。
并行是在同一个时间点不同的任务同时被执行,但是每个任务都有一个对应的执行者。
关于并发和并行其实有很多更加复杂的方式,一个go语言的例子可以学习一下:并发不是并行

总结

本章虽然是一个概括性的介绍,但其实已经包含了本书很多重要的核心内容,以后章节是对每个重要的部分进行一个详细的介绍。了解本章的内容对了解计算机系统有一个整体性的印象,让人有一种把握全局的感觉~