RFC3265 翻译 Session Initiation Protocol (SIP)-Specific Event Notification SIP-特定事件通

来源:互联网 发布:strict模式下的js 编辑:程序博客网 时间:2024/06/11 14:10
 

闲来无事,找篇文章翻译一下,复习下英文,也强迫自己好好读读协议文档。

从短的开始吧,呵呵,也希望自己能够坚持下去。

1 简介

在终端节点间通信过程中,发送异步通知消息的能力被证实在很多类型的SIP服务中是相当有用的。其中包括:自动会呼功能(基于终端状态事件)、好友列表(基于用户存在事件)、消息等待通知(基于邮箱状态变化事件)以及PSTN和Internet互通状态(基于呼叫状态事件)。

在本文档中描述的方法,提供了一个这些通知消息可以遵循的框架。

在这里定义的事件通知机制并不是一个对所有订阅和通知消息都有效的规则。对于某个单一协议,要想满足所有订阅和通知消息的需求是非常困难的。我们的目标只是提供一个针对SIP的框架,它对于一个特定领域不会过于复杂以至于无法实现,但能够提供足够灵活强大的服务。必须注意到,基于这个框架的事件包可能会定义一下强制的规则,它们定义的订阅和通知消息都必须遵守这些规则。

本文档没有没有定义可以直接使用的扩展功能;这些扩展功能必须在其他文档定义(这里是指“事件消息包”)。在一个基于对象的设计系统中,它可以被看作为一个抽象基类,可以被其他扩展的功能继承。在第4章中描述了进行这些扩展的一些基本原则。

1.1 概述

订阅和通知的基本理念是:在网络中的实体可以订阅在网络中各种不同资源和呼叫的资源和呼叫状态,并且那些实体当状态发生变化时会发送通知消息。

一个典型的呼叫流程如下图所示:

订阅者 通知者

|-----SUBSCRIBE---->| 请求状态订阅

|<---------200------------| 订阅请求响应

|<------NOTIFY---------| 返回当前状态通知

|--------200------------->|

|<------NOTIFY---------| 返回当前状态通知

|--------200------------->|

订阅消息是会过期的,必须由后续的SUBSCRIBE来刷新。

1.2 文档约定

2 定义

事件包(Event Package):事件包是指一个附加的规范,其中定义了由通知者发送给订阅者的一系列状态信息。消息包也定义了基于本文描述的框架语法和语义,用来描述这些特定的状态信息。

事件模板包(Event Template-Package):事件模板包是一种特殊的事件包,它定义了所有事件包可能用到的一系列状态,包括它自身。

通知(Notification):通知是值由通知者向订阅者发送某个资源的状态。

通知者(Notifier):通知者是一个用户代理(User Agent),它会产生一个NOTIFY请求给订阅者,来通知订阅者某个资源的状态。通常通知者可以接收一个SUBSCRIBE请求来创建订阅。

状态代理(State Agent):状态代理是一个通知者,它根据某个资源的行为来产生状态信息。为了做到这点,它可能需要汇总多个资源的这类状态信息。一个状态代理通常会拥有它所产生通知消息的资源的完整状态。

订阅者(Subscriber):订阅者是一个用户代理,它接收来自通知者的NOTIFY请求,这些NOTIFY请求中包含了订阅者关心的某个资源的状态。通常订阅者也会产生SUBSRCIBE请求,并将请求发送给通知者来创建订阅。

订阅(Subscribe):订阅是指与一个对话相关的一系列的应用程序状态。这个应用程序状态包括一个指向对应对话的指针,事件包的名字,以及一个可选的ID令牌。事件包会定义附加的订阅状态消息。根据定义,订阅在订阅者和通知者都存在。

订阅转移(Subscribe Migration):订阅转移是指讲某个订阅从一个通知者转到另一个通知者的一些列操作。

3 节点行为特性

3.1 SUBSCRIBE行为描述

SUBSCRIBE方法是向一个远端节点请求当前状态和获取状态更新。

3.1.1 订阅持续时间

每个SUBSCRIBE请求都应该包含“Expires”头(在SIP[1]中定义)。这个超时值表明了当前这个订阅的有效时间。为了保证订阅的有效性,使之不超过Expires头中定义的持续时间,订阅者需要定时在同一会话中发送一个新的SUBSCRIBE请求来刷新订阅。

如果在SUBSCRIBE请求中没有定义Expires头,那么默认值是在事件包中定义。

对于SUBSCRIBE请求的相应消息—200族响应消息中也必须包含Expires头。其中定义的时间可以比SUBSCRIBE请求中定义的有效时间短,但绝对不能较之更长。200应答消息中定义的超时时间就是这次订阅的超时时间。

在Contact头中的expires参数和SUBSRCIBE没有关系,很显然,它不等同与SUBSCRIBE请求和响应中的Expires头。

很容易联想到如果一个SUBSRCIBE请求中Expires头设为0,那么就表示取消该订阅。

除了表明取消订阅外,一个Expires头为0的SUBSCRIBE也引发一个状态取回操作,详见3.3.6。

通知者也可能想要取消某个订阅。这点非常有用,比方说,没有被订阅的资源已经不存在了,那就需要取消它的订阅。关于这点将在3.2.2中做更为详细的讨论。

3.1.2 订阅事件和事件类的描述

事件的描述由三部分信息组成:请求URI(Request URI)、事件类型、消息体(可选)。

Request URI是SUBSCRIBE请求中最重要的字段,它包含了足够的路由信息将请求依照每个路由流程送到目的端。它也包含了足够的信息来标示事件订阅的资源。但它不一定包含足够的信息来确定某个事件(比如:sip:adam@dynamicsoft.com可以订阅我现在的状态,也可以是订阅我语音信箱的状态)。

订阅者必须在SUBSCRIBE请求中包含一个Event头,且只能包含一个,来表明订阅的事件或是事件类。Event头也要包含一个标记来表明这次订阅的状态类型。这个标记是在IANA注册的,并且和某个事件包对应来描述这个事件或是事件类。Event头也可以包含一个Id参数。如果这个Id参数存在的话,它包含一个不透明的标记,用来标示对话中的特定订阅。Id参数仅在单个对话中有效。

如果事件标记对应的事件包定义了SUBSCRIBE请求中的消息体,那么就要遵从这些定义。

事件包也可以自己为Event头定义参数,那么,它也必须定义这些参数的语义。

3.1.3 其他SUBSCRIBE请求的头变量赋值

由于SUBSCRIBE请求会按照SIP[1]的定义创建一个对话,它就可能带有一个Accept头。如果存在的话,Accept头定义了在后续NOTIFY请求中消息体的结构。事件包必须定义在没有Accept头情况下SUBSCRIBE请求的行为。通常这种情况下,SUBCRIBE仅包含一个默认的消息体类型。

在本文档中没有定义的消息头的赋值参考SIP[1]中的定义。

3.1.4 订阅者SUBSCRIBE行为

3.1.4.1 请求订阅

SUBCRIBE如同在SIP[1]中的定义,是创建一个对话的方法。

当订阅者想要订阅某个资源的特定状态时,它会创建一个SUBSCRIBE消息。如果第一个SUBCRIBE是在某个对话之外的(通常是这种情况),它会按照SIP[1]中定义的UAC在对话外创建请求的流程一样创建SUBSCRIBE请求。

SUBSCRIBE请求会由一个最终的应答来确认。200族的应答消息表明此次订阅被确认,然后一个NOTIFY消息会立即发送。200应答表明此次订阅已经成功,用户有权订阅请求的资源。202应答仅仅表明已经收到订阅请求,但是否订阅成功还未确定。

200族的应答消息中的Expires头表明订阅的实际有效时间。

非200族的应答消息,表明没有订阅或是对话被创建,并且也没有后续的NOTIFY会被发送。初应答489以外,所有的非200族应答拥有同样的意义,如SIP[1]所述。

在一个SUBSCRIBE请求的Event头中可能包含一个Id参数,来区分同一个会话中的不同订阅。

3.1.4.2 刷新订阅

在订阅超时前的任何时候,订阅者都可以发送另一个SUBSCRIBE请求来刷新订阅超时定时器,这个刷新SUBSCRIBE请求与原先的订阅在同一个会话中,并且有相同的Event头和Id参数。通常情况下这个SUBSCRIBE请求和初始SUBSCRIBE处理流程一样,但如果是下面的情况则有所不同:

如果初始SUBSCRIBE请求在Event头中带有Id参数,那么刷新的SUBSCRIBE请求必须带有同样的Id参数,否则这个SUBSCRIBE会被当做在此对话中一个新的订阅。

如果一个刷新SUBSCRIBE请求收到一个481应答,则表明这个订阅已经结束,订阅者也不会再收到通知消息。此时,订阅者应该认为此订阅已经无效。如果订阅者想要重新订阅这个状态,它必须再创建一个新的初始SUBSCRIBE请求,包含新的Call-ID和新的唯一的From标记。

如果一个刷新SUBSCRIBE请求收到一个失败应答,但不是481,则表明这次订阅在未超时前仍然有效,这个超时时间可能是在最近一次SUBSCRIBE和应答之间协商确定,也可能是在NOTIFY消息中Subscription-State头的expires参数定义。

这种错误可能是由网络状况或是通知者问题引起的,以至于没有后续NOTIFY消息会被发送。

3.1.4.3 取消订阅

取消订阅和刷新订阅的处理流程相同,只是Expires头的值为0。取消订阅成功后,将会有一个最终的NOTIFY消息。

3.1.4.4 订阅创建确认

订阅者可以认为它将从所有成功处理订阅和订阅刷新的节点等到NOTIFY消息。在第一个NOTIF消息到达之前,订阅者应该认为被订阅资源处于不确定状态。定义事件包的文档中必须规定这种不确定状态,以方便应用程序处理(参见4.4.7)。

由于存在消息乱序和网络路径分叉的可能性,订阅者必须有在SUBSCRIBE事务完成前处理NOTIFY消息的能力。

3.1.5 代理订阅行为

代理需要在SIP[1]定义的功能外支持新的行为来支持SUBSCRIBE。如果代理希望看见某个对话中所有的SUBSCRIBE请求和NOTIFY请求,那么它必须改写初始SUBSCRIBE请求和用来建立对话的NOTIFY请求的路由信息,也应该改写其他所有SUBSCRIBE和NOTIFY的路由信息。

订阅者和通知者可以使用S/MIME来对SUBSCRIBE和NOTIFY进行编码。因此,代理不能获取那些在SIP[1]中明确定义代理可读的信息。

3.1.6 通知者SUBSCRIBE行为

3.1.6.1 初始SUBSCRIBE事务处理

SUBSCRIBE事务必须在第一时间做出应答,它持续的时间不能超过自动处理所需的时间,也就是说,通知者不能在等到用户响应后再发送SUBSCRIBE请求应答。

通知者需要确定在Event头中定义的事件包是否支持。如果不支持,通知者需要返回一个489-无效事件应答,来表明指定的事件不支持。

通知者也应根据自己需要来进行必要的鉴权和认证,参见3.1.6.3。

通知者也可以检查Expires头规定的超时值是否过短。仅当超时值大于0,小于1小时,并且小于通知者的最小配置值时,通知者可以返回一个423-时间间隔过小应答,并且者应答消息Min-Expires头中指明支持的最小超时值。Min-Expires头如SIP[1]中定义。

如果通知者能够马上确定它可以支持此事件包,鉴权也已经通过,并且没有没有其他问题,那么通知者就会创建订阅和一个与之对应的对话,并且返回200应答(除非这样做引起鉴权策略混淆,参见5.2)。

如果通知者不能立即创建订阅(例如,需要用户输入密码完成鉴权,或者需要联系另个暂时无法联系的节点),或是希望加密鉴权策略,通知者可以返回一个202-Accept应答。这个应答说明SUBSCRIBE请求已经被接受,但是由于还未鉴权,所以不能生效。

当通知者创建订阅时,它会将事件包名字和Event头Id参数作为订阅信息的一部分保存下来。在200族应答消息中的Expires值和REGISTER应答中的是一样的:服务端可以缩短时间,但不能延长超时时间。

如果SUBSCRIBE请求中的超时时间过短,那么通知者可以发送一个423应答,如前所述。

作为SUBSCRIBE请求的应答,200族应答在订阅有效期间不会产生任何有用信息,它们的目的仅是保证事务传输可靠有效。状态信息将会被包含在后续的NOTIFY消息中发送给订阅者。

SIP[1]中定义的其他应答代码也可以使用。

3.1.6.2 订阅创建/刷新确认

对于成功创建和刷新的订阅,通知者必须立刻发送一个NOTIFY消息给订阅者,其中包含了订阅资源的当前状态。这个NOTIFY消息是在有订阅者创建的对话中发送的。如果订阅发生时,资源处于一个非有效状态的话,通知者可以发送一个包含空消息体或是不确定状态的消息体的NOTIFY给订阅者。更多关于生成NOTIFY的细节请参考3.2.2。

值得注意的是,任何200族的应答被发送后,都要立刻发送一个NOTIFY消息,而不管是否经过鉴权。

3.1.6.3 SUBSCRIBE请求的认证和鉴权

由于一些隐私的原因,通知者可能需要通过某周鉴权策略来确定订阅者是否有权限来订阅某个事件。这种鉴权策略可以是查询某个允许访问的列表,也可以是和用户进行实时交互来查询。通常,鉴权在认证之前的不是什么常用。

SIP的认证机制在SIP[1]中有所讨论。这里,即使通知节点是一个代理,SUBSCRIBE请求的认证也是从401应答开始,而不是407。在创建订阅和发送通知时,通知者通常作为UA的角色。

当然,作为代理的情况,还是使用407应答。刚刚做的解释只是强调通知者是一个UAs,必须按照UA认证流程。

如果鉴权失败是由于允许列表中没有记录或是其他自动鉴权机制(也就是可以自动识别订阅者是否有权限订阅),通知者应该应答403-Forbidden或是603-Decline,除非这样会涉及到一些隐私信息。参见5.2。

如果通知者需要通过交互的方式来确定订阅是否有权限,它需要立即返回一个202-Accept应答。NOTIFY消息还是要立即发送,如前所述。

如果订阅鉴权被延误,通知者希望拒绝这次订阅请求,通知者可以发送一个包含Subscription-State头的NOTIFY的消息给订阅者来表明这次订阅结束,Subscription-State的值为terminated。

3.1.6.4 刷新订阅

当通知者收到刷新订阅消息时,假定订阅者仍然鉴权有效,通知者就需要更新订阅的超时时间。在初始订阅中服务端可以缩短超时时间,但不能延长。最终确定的超时时间在应答消息的Expires头中描述。如果订阅者确定的超时时间过短,通知者应该发送一个423-Subscription Too Brief应答。

如果在订阅超时前没有收到任何刷新订阅,那么这个订阅将被删除。当删除某个订阅时,通知者需要发送一个NOTIFY给订阅者来表明订阅已经被删除,这个NOTIFY的Subscription-State头被置为terminated,并且应该包含一个reason=timeout参数。

发送一个超时的NOTIFY可以导致对话结束。

3.2 NOTIFY行为描述

NOTIFY消息是通知订阅者它订阅的状态发生了变化。订阅通常是通过SUBSCRIBE方法完成,但是还有其他的方法。

如果有任何非SUBSCRIBE的方法来创建订阅,那么就必须保证相应的NOTIFY能够正常工作。这个机制的设计者必须能够区分两种NOTIFY消息,一种是发送给明确知道订阅的订阅者,另一种是发送无知的节点。后一种行为是无效的,肯定会收到481- Subscription does not exist应答(除非其他一些400族或500族更符合错误情况),参见3.2.4。也就是说订阅必须是对订阅者和通知者都有效的,无论是不是使用了SUBSCRIBE机制来完成订阅。

一条NOTIFY并不还意味者订阅结束,也就是说一条SUBSCRIBE请求可能会触发多条NOTIFY请求。

3.2.1 已报告时间、事件类和当前状态的识别

通知的事件类型识别和前面描述的订阅事件类型相似(参见3.1.2)。

和SUBSCRIBE请求一样,NOTIFY消息中Event头包含了创建的通知消息的事件包名。在NOTIFY中的事件包名必须和SUBSCRIBE中的事件包名相同。如果SUBSCRIBE消息中有Id参数,那么NOTIFY中也必须带有相同Id参数。

消息包会定义NOTIFY消息体的语义,如果确实定义了,那么就要在NOTIFY中实现这些语义。 NOTIFY的消息主要是用来提供一些发生事件和相关资源状态的一些详细信息。

如果有消息体的话,NOTIFY必须要符合的SUBSCRIBE请求中Accept头中规定的格式。消息体中可以包含订阅资源的状态,也可以包含指向此状态的URI(参见4.4.13)

3.2.2 通知者NOTIFY行为

当通知者以200族消息响应SUBSCRIBE请求后,必须马上构建并发送一个NOTIFY消息给订阅者。当订阅的状态发生变化时,也应该根据鉴权、本地策略或是实现难以程度的考虑来立即发送一个NOTIFY消息。

如果应答超时,那么这个NOTIFY被认为是失败了,或者收到一个非200族的应答消息,并且这个应答消息没有Retry-After头或其进一步要求重试的请求(比如401 Authorization Required)。

如果NOTIFY由于超时而失败,并且此订阅是由软件层次创建的(如 SUBSCRIBE),那么通知者需要删除这个订阅。

通过这种行为,就可以防止在订阅者崩溃或是离开网络的时候发送不必要的状态变化消息。由于这种传送是要发送多次的,重传算法在SIP[1]中有定义,持续向网络上不存在客户提供服务可能会引起网络阻塞。如果客户端重启或是重新加入网络的话,客户端应该重新发送SUBSCRIBE消息来继续订阅,这样的消息将会重启订阅流程。

如果由于错误导致NOTIFY失败,并且订阅是由软件层次触发的,通知者必须删除对应的订阅。

一个表明出错的应答通常表明订阅者或是路由上的某个代理出错。如果是订阅者出错,订阅者就可以在错误被解决的时候通过re-SUBSCRIBE来纠正这个状态。如果是代理出错,那么订阅者定期发送的刷新订阅消息就可以在网络问题解决时重新开始这次订阅。

如果通知者收到481应答,那么通知者必须删除这个订阅,即使这个订阅不是由SUBSCRIBE发动的。

如果没有定义上面的行为,那么订阅者在收到一个不可识别的订阅消息时需要先发送一个包含错误码的应答,再发送一个SUBSCRIBE取消订阅。这样就有可能让订阅者收到网络服务攻击,因此,我们选用错误码为481的应答来明确的说明订阅必须被全部取消。

NOTIFY必须包含一个Subscription-State头,这个消息头的取值包括active, pending和terminated。Active表明订阅已经被接收并且通过鉴权;pending表明订阅请求已经收到,但根据本地策略还不能确定是否被接收;terminate表明此次订阅已经结束。

如果Subscription-Stat头的值是active或是pending,通知者必须在Subscription-Stat头包含一个expires参数来表明订阅的有效时间。通知者可以通过这种方式来缩短超时时间,但不能延长订阅有效时间。

当SUBSCRIBE请求产生分叉时,订阅者无法收到应答,这种情况下在active和pending订阅中加入超时信息就十分有用。需要注意的是这个expires值是Subscription-Stat头的参数,而不是Expires头。

如果Subscription-Stat头的值是terminate,NOTIFY也应该包含一个reason参数。通知者也可以包含一个retry-after参数。详见3.2.4。

3.2.3 代理NOTIFY行为

代理不需要额外的行为来支持NOTIFY。如果代理希望看到在一个对话中所有的SUBSCRIBE和NOTIFY,那么它必须重写初始SUBSCRIBE消息和创建对话的NOTIFY消息的路由信息。也需要重写所有SUBSCRIBE和NOTIFY的路由信息。

订阅者和通知者可以使用S/MIME来对SUBSCRIBE和NOTIFY进行编码。因此,代理不能获取那些在SIP[1]中明确定义代理可读的信息。

3.2.4 订阅者NOTIFY行为

当收到一个NOTIFY请求时,订阅者必须判断它是否和某个未处理的订阅匹配。如果没有,那么订阅者必须返回一个481 -Subscription does not exist应答,除非有其他400族或500族的应答消息更适合。匹配的SUBSCRIBE和NOTIFY表明一个新的对话被创建,参见3.3.4。如果是在一个已经创建的对话中建立的SUBSCRIBE和NOTIFY,那么它们属于同一对话,并且Event头相同,在7.2.1中描述。

如果由于某些原因,订阅者不支持NOTIFY消息中的Event头表示的事件包,那么它必须返回一个489-Bad Request 应答。

NOTIFY请求应该认证,使用SIP定义的认证机制。

NOTIFY请求必须包含Subscription-State头来表明订阅的当前状态。

如果Subscription-State的值是active,那么表明订阅已经被接收和认证。如果消息头带有expires参数,那么订阅者应该将其当作认证订阅的有效时间,并作适当调整。retry-after和reason参数对active类型的NOTIFY没有意义。

如果Subscription-State的值是pending,表明通知者已经收到订阅请求,但是还不能确定是否接收此次订阅。如果消息头带有expires参数,那么订阅者应该将其当作认证订阅的有效时间,并作适当调整。retry-after和reason参数对pending类型的NOTIFY没有意义。

如果Subscription-State的值是terminated,那么订阅者应该认为此次订阅已经结束。Expires参数对terminated类型的NOTIFY没有意义。如果包含有原因码,那么客户端需要做如下处理:如果没有原因码或是原因码不认识,那么客户端在任何时候重新发起一个订阅(除非带有retry-after参数,表明再次订阅的间隔时间)。以及定义的原因码包括:

deactived :订阅已经结束,但是订阅者应该马上再次发起一个新的订阅。这样一个主要的作用是允许订阅在节点间转移。retry-after参数对于deactived没有意义。

Probation:订阅已经结束,但是订阅者可以在一段时间后重试订阅。如果存在retry-after参数,那么客户端应该在retry-after指明的时间后再发起新的订阅,时间单位为秒。

rejected:由于认证策略改变,订阅已经结束。客户端不应再发起订阅。retry-after参数对于rejected没有意义。

timeout:由于在有效时间内没有刷新订阅被结束。客户端可以马上重新发起订阅。retry-after参数对于timeout没有意义。

giveup:由于通知者在一段时间内没有获得鉴权信息而结束订阅。如果存在retry-after参数,那么客户端应该在retry-after指明的时间后再发起新的订阅,时间单位为秒。否则,客户端可以立刻重新发起订阅,这样订阅可能回到pending状态。

noresouce:由于所请求资源的状态已经不存在而结束订阅。客户端不应再发起订阅。retry-after参数对于noresouce没有意义。

如果订阅者认为通知是可以接受的,那么订阅者应该返回200应答。一般,NOTIFY的应答不带有消息体。如果NOTIFY带有Accept的话,应答也可能带有消息体。

如果适当的话,也可以返回其他在SIP[1]中定义的应答。任何情况下,NOTIFY事物的持续时间都不应该超过自动处理能够完成的时间。也就是说,不能等待用户响应才返回一个最终NOTIFY。

3.3 概况

3.3.1 判断是否支持SUBSCRIBE和NOTIFY

SUBSCRIBE和NOTIFY都不需要使用Requires或是Proxy-Requires头。同样在Support头中也没有定义。如果需要的话,客户端可以按照SIP[1]中定义的OPTIONS请求来表明支持SUBSCRIBE和NOTIFY。

可以添加Allow-Events头来表明支持SUBSCRIBE和NOTIFY。

当注册的时候,Contact头的methods参数也可以表明支持SUBSCRIBE和NOTIFY。

3.3.2 取消请求

取消SUBSCRIBE和NOTIFY没有意义。

3.3.3 分叉

根据SIP[1]中定义的非INVITE请求代理规则,一个成功的SUBSCRIBE请求应该只会收到一个200族的应答,但是由于分叉,订阅可能被多个节点接受。因此订阅者必须能够正确处理From字段与200族应答消息中To字段不同的NOTIFY消息。

如果对于某个SUBSCRIBE请求,收到了不同对话的多个NOTIFY消息,每个对话代表了由于分叉而到达的不同目的地。对于这种情况的处理,请看4.4.9

3.3.4 对话的创建和关闭

如果初始订阅请求不是在一个已经存在的对话中发送的,订阅者会等待SUBSCRIBE请求的应答和一个对应的NOTIFY请求。

如果一个应答消息和SUBSCRIBE请求包含相同的Call-ID,From头和相同的CSeq,那么这个应答就是SUBSCRIBE请求对应的应答。这些消息头如何进行比较在SIP[1]中有定义。如果一个200族的应答和某个SUBSCRIBE请求对应,那么就创建了一个新的订阅和对话(除非有一个相匹配的NOTIFY已经创建了会话)。

如果一个NOTIFY和SUBSCRIBE相匹配,那么它们应该拥有相同的Call-ID和Event头,并且NOTIFY的From头和SUBSCRIBE的To头相同。Event头的比较在7.2.1描述。如果NOTIFY的Subscription-State值为active或是pending,就创建了一个新的订阅和对话(除非已经有一个相匹配的应答创建了对话)。

如果初始订阅是在一个已经存在对话中发送的,那么对应的应答或是NOTIFY仅仅创建一个与此对话相关的新的订阅。

一个对话可以对应多个订阅。订阅可以存在于由INTIVE创建的对话或是其他文档定义的机制创建的对话。这些应用的状态不会超越对话描述的状态。

当通知者发送一个Subscription-State为terminated的NOTIFY消息是,订阅被关闭。

订阅者一个发送一个Expires为0的SUBSCRIBE消息来触发通知者发送这个关闭订阅的NOTIFY消息。然而考虑到对话和订阅的生存时间,在收到Subscription-State为terminated的NOTIFY之前,订阅者不应认为订阅已经结束。

如果关闭订阅后,对话没有其他应用的状态,那么对话也就结束了。如果订阅没有关闭,其他应用状态的关闭不会导致对话被关闭。

上面定义的这些行为表明一个由INVITE创建的对话不会仅因为收到BYE而关闭。同样当一个对话和多个订阅相关是,只有当所有的订阅全部被关闭,对话才会关闭。

3.3.5 状态代理和通知者转移

当使用状态代理时(参见4.4.11),可以在状态代理和提供状态信息的节点间转移转移订阅就十分有用。这种订阅转移可以是由一个Subscription-State为terminated并且reason为deactived的NOTIFY消息触发。参见3.2.2。

一旦收到这种通知消息,订阅者应该尝试重新订阅。这样订阅者会创建一个新的对话,并且不会再按照原先的路由信息来发送消息。

订阅转移可以是一个或多个服务端的策略发生变化,不再路由订阅信息,这个或这些服务端是原先订阅消息发送的目的端。也可能是通知者转变为代理或是重定向服务器后发生订阅转移。

通知者是否转移,何时转移,为何转移都会在每个事件包中定义,否则,这就取决与通知者的本地策略,属于个体行为。

3.3.6 查询资源状态

根据上面描述的行为,一个可能的结果就是:可以通过一个Expires为0的SUBSCRIBE消息来立刻获取资源的当前状态,而不需要一个长期有效的订阅。

当然,在一个有效地订阅过程中,可以将SUBSCRIBE中expires的值设为此次订阅的的有效剩余时间,以此来立即查询状态。

一旦收到这样的请求,通知者就会发送一个包含当前状态信息的NOTIFY请求。

如果这个NOTIFY消息是由一个expires值为0的SUBSCRIBE消息触发的,那么NOTIFY中Subscription-State的值为terminated,reason为timeout。

查询事件状态可能会增加网络和通知者的负荷,因此应该尽可能少的使用这种方式。当会产生比持续运行的订阅更多的网络消息时,这种方式更不应该使用。

当使用及时查询时,订阅者应该缓存认证信息以减少查询所产生的网络流量。

3.3.7 Allow-Events头的使用

如果Allow-Events头存在的话,则表明了客户端(发生请求)和服务端(发送应答)支持的事件列表。也就是说,一个发送Allow-Events的节点表明它可以产生支持所列所有事件的SUBSCRIBE和NOTIFY。

任何支持一个或多个事件包的节点,都应该在初始话对话和其响应消息中列出所支持的事件包类型。

这样的信息十分有用。比方说,它可以让UA通过节点是否支持事件要求的特性来获取特定的接口。

另外,代理不能自行插入Allow-Events头。

3.3.8 兼容PINT

本文所述的Event头是必须的。然而为了与PINT兼容(参看[2]),服务端可以认为一个没有Event的SUBSCRIBE是订阅一个PINT事件。如果服务端不支持PINT,则可以发送一个不带Event头的489 Bad Event应答。

4 事件包

本章包含了一些在SUBSCRIBE和NOTIFY中使用事件包时需要考虑的问题。

4.1 是否使用得当

当按照本文所述的通知事件方法设计事件包时,有几点很重要:SIP是否合适这个问题?是否由于SIP某些特性才选择SIP(比如用户的移动性),还是仅仅因为SIP可以完成这个工作。如果你是在设计诸如网络管理或是汽车引擎温度检测,那最好还是考虑换个协议。

另外,这套机制也不适合需要经常快速上报状态的情况,比如每秒好几次。一个SIP的网络通常只提供合理的网络流量,毫秒级的发送用户GPS位置通知信息会很容易是网络过载。

4.2 事件模板包

一般来说,事件包定义了某个资源的一系列的状态,比方说用户是否登录,呼叫状态以及明邮箱状态。

事件模板包是一中特殊类型的包,它定义了可以被其他包使用的一系列状态,比如统计、进入策略以及订阅者列表。事件模板包甚至可以被另一个事件模板包使用。

这是一种基于对象的算法,和C++中的模板类似,必须要有其他包才能使用。

一个使用了事件模板包的包的命名实在包本身的名字后加上.再加上事件模板包的名字。比如,一个模板包叫winfo,使用它的包的名字叫presence,那么在Event和Allow-Events的应该为presence.winfo。

事件模板包必须要定义,这样它就可以应用与任何无理由包。也就是说,事件模板包不能和某个货多个父类包绑定,这样有可能引起它们无法同时工作。

4.3 携带状态的数量

当设计事件包的时候,很重要的一点就是要考虑通知消息所携带状态的类型。

通常的做法是仅携带事件(比如,有一条新的语音信息)而不带相应的状态(比如,一共有7条语音信息)。这样通常会使订阅者的实现变得复杂,因为它们要来记录并维护所订阅资源的状态,而且也会有一些同步问题。

有两种可选择的方法来解决这些问题。

4.3.1 完全状态信息

对于那些携带状态信息量比较小(通常小于1k)的事件包,可以当事件发生时,发送这个状态信息。

在一些情况下,对于一些特定的事件仅携带当前状态可能还不够。这种情况下,消息包就要携带不仅是当前状态的完整信息。比如携带“当前没有客户代表服务可用”就没有“当前没有客户代表服务可用,客服代表sip:46@cs.xyz.int刚刚注销”来的有用。

4.3.2 状态增量变化

对于那些要携带大量状态信息的事件,可以在NOTIFY消息中携带增量信息,而不是全部信息。

具体流程如下:在响应SUBSCRIBE的立即发送的NOTIFY消息中包含状态的全部信息。由于状态变化而发送的NOTIFY仅仅包含变化的状态信息,订阅者就可以将变化信息和原来的全信息进行合并而得到当前的状态。

每个支持增量变化的事件包都必须支持版本号字段,在订阅过程中,每发送一次NOTIFY,版本号就要加1。这个版本号字段是在消息体中的,而不是在SIP头中。

如果收到一个版本号增量大于1的NOTIFY,那么订阅者就知道缺少了增量信息。订阅者就会忽略这个NOTIFY,并且重新发送SUBSCRIBE来获取全部状态信息。

4.4 事件包职能

事件包不需要重复本文档所描述的这些行为,当然它也可以这样做来起到明确和强调的作用。通常情况下,消息包只要描述本文档没有定义或是和本文档有冲突的地方。

在本文中所有表明“必须”和“应该”的地方都不能在扩展文档中有所改变,其强制性都不能削弱,但是可以增强,比如将“应该”改为“必须”。

4.4.1 事件包命名

作为事件包的标示,事件包名字是必须存在的。它必须在INAN注册中出现的名字。更多关于INAN的信息,请看第6章。

4.4.2 事件包参数

如果在Event头中定义了其他参数来改变事件包的行为,那么其语法和语义就必须明确定义。

4.4.3 SUBSCRIBE消息体

事件包最主要的就是定义SUBSCRIBE消息体的语法和语义。这些消息体通常会调整、扩展、过滤、关闭,或是为请求设置起点。事件包的设计者最好尽可能利用已有的MIME类型来定义消息体。

这个强制的部分表明了SUBSCRIBE请求的事件类型。它的语法和语义必须要有详细的定义。

4.4.4 订阅有效时间

事件包最好能提供一个合理的有效时间作为参考。但必须定义一个Expires的默认值,以防止没有定义expires。

4.4.5 NOTIFY消息体

NOTIFY消息体是用来通知某个被订阅的资源发生了状态变化。事件包必须定义NOTIFY中可能出现的类型或事件类型。事件包必须定义或是应用这些消息体的语法和语义。

4.4.6 通知者处理SUBSCRIBE请求

这段定义了当通知者收到SUBSCRIBE请求时的处理流程。这段定义也是需要的。

这部分定义包含了如何认证和鉴权。其中鉴权可能包括是否对于所有的SUBSCRIBE消息都应答202(参见5.2)

4.4.7 通知者产生NOTIFY请求

这部分描述了通知者如何产生和发送一个NOTIFY请求。其细节包括:何种事件导致发送NOTIFY、如何计算NOTIFY要发送的状态信息、如何产生一些不确定或是假的状态来隐藏鉴权的细节、采用全状态通知还是增量状态通知,参见4.3。这部分定义也是需要的。

这部分可以会用来定义后续响应的行为。

4.4.8 订阅者处理NOTIFY请求

这部分定义了当订阅者收到NOTIFY请求后的处理过程,包括如何组成清晰的资源状态。

4.4.9 处理分叉请求

每个事件包都必须规定是否允许SUBSCRIBE请求分叉而形成得多点订阅。

如果是允许的,那么第一个可能创建对话的消息来创建对话。之后所有虽然和SUBSCRIBE对应(比如To头和From头、From头的tag参数,Call-ID,CSeq,Event以及Event头得Id参数都匹配)的却和此对话不相匹配NOTIFY消息都会通过一个481应答来拒绝。需要注意的是响应SUBSCRIBE的200族应答,可能会比一个匹配得NOTIFY要迟收到,那么这些200应答会和NOTIFY创建的对话不一致。除非要求完整的SUBSCRIBE事务,不然这些200族应答将会被忽略。

如何由于SUBSCRIBE分叉而引起的多点订阅是允许的,那么订阅者和每个发送NOTIFY的通知者之间都会创建一个对话,并且发送一个200应答来相应此NOTIFY。

如果多点订阅是被允许的,那么事件包就必须去确定是否需要合并某个状态得所有通知消息,并且要定义如何合并。某些事件包可能会这样定义:每个对话都和一个相互独立得状态绑定,并且不会收到其他对话影响,如何是这种情况,事件包必须描述清楚。

4.4.10 通知的频率

每个事件包都应该定义一个通知者产生通知消息的最大频率。

每个事件包可以定义一种修改机制来使得订阅者可以改变这最大频率。

4.4.11 状态代理

事件包得设计者必须考虑他设计的事件包是否能利用网络聚集点或是处理其他节点的节点。(比如,某个节点提供某个资源的状态,但是此资源自己却又不能提供这样的状态信息)。这种应用的一个例子就是一个跟踪用户是否存在和是否有效的节点。

如果可以使用状态代理,那么事件包必须定义它们如何收集信息以及如何进行认证和鉴权。

事件包也可以对通知者转移发生时的情况做规定。

4.4.12 例子

每个事件包都应该包含一些例子和消息流程图,它们包含了一些典型的,语法正确并且完整的例子。

文档最好也要写明这些例子是用来提供如何使用的信息的,而不是一些规定,从而引导实现者去参考特定协议的文档来实现。

4.4.13 通过URIs来获取状态

有些事件包定义的状态消息可能过大,而无法在一个SIP消息中发送。为了解决这个问题,事件包要求可以发送一个URI而不是发送状态消息,可以通过这个URI来获取状态信息。

如何发送这些URI不在本文你描述范围内。

5 安全方面考虑

5.1 进入控制

由于很多事件都关系到一些隐私问题,所以是否接受订阅应该在用户的直接控制下完成。同样,通知者应该根据订阅者的身份(基于准入控制列表),使用SIP定义的认证机制来有选择的拒绝一些订阅。创建和发布这样的准入控制列表不在本文的讨论范围之内。

5.2 通知者隐私机制

仅仅返回200应答或是某个4**和6**应答给SUBSCRIBE请求,在某些条件下,可能会引发一些关于隐私泄露的问题。这种情况下,通知者应该返回202应答。当后续的NOTIFY消息不能携带真实的状态时,它必须包含一个从订阅者角度来看是正确的数据,和一个有效的NOTIFY请求没有差别。这种情况下,关于此次状态订阅是否被授权的实际情况不会被返回给订阅者。

可以在其他文档中描述如何以及为什么产生这些看似正确的数据。比如,在RFC 2779[6]就描述了关于用户是否在线的实现。

5.3 拒绝服务攻击

这样一个由一个SUBSCRIBE请求触发一个应答和多个NOTIFY请求的模型,很容易就形成一个网络放大器以用来进行smurf攻击。

同样,通过大量创建SUBSCRIBE请求的应答,攻击者也可以耗尽订阅者终端的性能,使之无法使用。

为了减少被这样攻击的机会,通知者应该实现认证。认证的相关问题在SIP[1]中讨论。

5.4 重复攻击

重复SUBSCRIBE或是NOTIFY同样会产生危害。

对于SUBSCRIBE消息,攻击者可以通过他所看见的过去发生的订阅来创建很多无理由订阅。重复NOTIFY可以拷贝一些以前的状态信息(尽管一个设计有版本信息的NOTIFY消息可以减少这种攻击)。值得注意的是,禁止向那些没有请求订阅的节点发送通知消息也有助于减少这中攻击。

为了减少这种攻击,需要实现带有重复保护的认证机制,在SIP[1]中讨论。

5.5 Man-in-the middle攻击

即使使用认证,main-in-the-middle攻击也会产生不确定订阅,它们通过拦截存在的订阅、关闭未处理的订阅或是改变订阅的资源来进行攻击。要防止这样的攻击,实现者至少要对Contact、Route、Expires、Event和To消息头进行统一保护。如何SUBSCRIBE的消息体还包含呼叫的其他状态,也要一同保护起来。

main-in-the-middle攻击也会利用NOTIFY消息来产生一个无故的状态信息或是终止未处理的订阅。为了防止这种类型的攻击,实现者需要对Call-ID、CSeq和Subscription-State消息体以及NOTIFY的消息体进行统一保护。

在SIP[1]中对关于统一保护的详细信息。

5.6 信任

NOTIFY消息中的状态信息可能会包含一些敏感的信息。实现者可以通过加密来保护这些信息。

还有一种较少的情况就是,SUBSCRIBE也包含一些用户不要被别人知道的信息,它们也可以被加密。

为了保证远端可以保护它们认为敏感的信息,实现者应该可以处理加过密的SUBSCRIBE和NOTIFY消息。

在SIP[1]中对如何保证可靠性有消息描述。

6 关于IANA

定义事件类型的名字空间需要一个统一的规划。确定这种统一性的就是Internet Assigned Numbers Authority (IANA)。

有两种不同的事件类型:普通的事件包和事件模板包,参见4.2。为了避免冲突,事件模板包和事件包的名字共享同一个名字空间,也就是事件模板包的名字决不能和事件包的名字相同。

根据“Guidelines for Writing an IANA Considerations Section in RFCs [4]"中描述,普通事件包的名字遵循先申请先有效的原则,而事件模板包的名字则需要IETF达成共识。

向IANA注册,必须包括注册的名字以及是普通事件包还是事件模板包。另外,还必须包含对这次注册负责的组织的联系方式,以及一个描述次包的文档。事件模板包的注册必须包含一个指向定义这个事件模板包的RFC文档的应用。

注册的事件包和事件模板包的名字中都不能含有“.”,这是用来分割包和模板包的。

6.1 注册信息

本文没有实际的事件包和模板包的名字,IANA开始时注册的事件类型也为空。下文描述了IANA维护的信息类型,它也描述了5个必要元素包类型、联系方式和参考。

下表描述了在SIP-Specific Event Notification[RFC3265]中定义的事件包和事件模板包。每一个名字都表示了一个事件包或是事件模板包,由type区分:

Package Name      Type         Contact      Reference
   ------------      ----         -------      ---------
   example1          package      [Roach]
   example2          package      [Roach]      [RFC3265]
   example3          package                   [RFC3265]
   example4          template     [Roach]      [RFC3265]
   example5          template                  [RFC3265]

   PEOPLE
   ------
   [Roach] Adam Roach <adam@dynamicsoft.com>

   REFERENCES
   ----------
   [RFC3265] Roach, A., "SIP-Specific Event Notification", RFC 3265,
             June 2002.

6.2 注册模板

To: ietf-sip-events@iana.org
   Subject: Registration of new SIP event package 

   Package Name:  包名字

(包的名字必须符合7.2.1定义的语法)

   Is this registration for a Template Package:   是否是事件模板包

       (是或否)

   Published Specification(s):

(模板包需要引用RFC,其他可根据需要列出参考)

  Person & email address to contact for further information:

6.3 头域名字

本文定义了三个新的头域名字,它们在其他地方描述。这三个头根据下面的信息定义,并且已经加入到消息头列表中http://www.iana.org/assignments/sip-parameters

Header Name:   Allow-Events
Compact Form:  u

Header Name:   Subscription-State
Compact Form:  (none)

Header Name:   Event
Compact Form:  o

6.4 应答代码

本文定义了两个新的应答代码,它们在其他地方描述。这两个应答根据下面的信息定义,并且已经加入到应答列表中http://www.iana.org/assignments/sip-parameters

Response Code Number:   202
Default Reason Phrase:  Accepted

Response Code Number:   489
Default Reason Phrase:  Bad Event

7 语法

本章定义了SIP中事件通知的语法。语义在第三章中已经定义。注意这里定义的语法与SIP[1]中ABNF格式相同,并且包含某些元素的引用。

7.1 新方法

本文描述了两种新的方法:SUBSCRIBE和NOTIFY。

下表是对SIP[1]中表2和表3的扩展。

   Header                    Where    SUB NOT
   ------                    -----    --- ---
   Accept                            o
   Accept                     2xx       -
   Accept                     415       o
   Accept-Encoding                   o
   Accept-Encoding            2xx       -
   Accept-Encoding            415       o
   Accept-Language                   o
   Accept-Language            2xx       -
   Accept-Language            415       o
   Alert-Info                        -
   Alert-Info                 180       -
   Allow                             o

   Allow                      2xx       o
   Allow                             o
   Allow                      405       m
   Authentication-Info        2xx       o
   Authorization                     o
   Call-ID                           m
   Contact                           m
   Contact                    1xx       o
   Contact                    2xx       o
   Contact                    3xx       m
   Contact                    485       o
   Content-Disposition                  o
   Content-Encoding                     o
   Content-Language                     o
   Content-Length                       t
   Content-Type                         *
   CSeq                              m
   Date                                 o
   Error-Info               300-699     o
   Expires                              -
   Expires                    2xx       -
   From                              m
   In-Reply-To                       -
   Max-Forwards                      m
   Min-Expires                423       -
   MIME-Version                         o
   Organization                         -
   Priority                          -
   Proxy-Authenticate         407       m
   Proxy-Authorization               o
   Proxy-Require                     o
   RAck                              -
   Record-Route                      o
   Record-Route           2xx,401,484   o
   Reply-To                             -
   Require                              o
   Retry-After        404,413,480,486   o
   Retry-After              500,503     o
   Retry-After              600,603     o
   Route                             c
   RSeq                       1xx       o
   Server                            o
   Subject                           -
   Supported                         o
   Supported                  2xx       o
   Timestamp                            o
   To                         c(1)      m
   Unsupported                420      

   User-Agent                           o
   Via                               m
   Warning                           o
   Warning                           o
   WWW-Authenticate           401       m

7.1.1 SUBSCRIBE 方法

SUBSCRIBE被加入SIP消息中Method元素中。

和所有SIP方法名字一样,SUBSCRIBE方法名字是大小写敏感的。SUBSCRIBE方式是请求一个事件或是一组事件状态的异步通知。

7.1.2 NOTIFY方法

NOTIFY被加入SIP消息中Method元素中。

NOTIFY是用来某个SIP节点发生了状态变化,这个节点就是原先发送SUBSCRIBE请求来订阅次状态的。它也可以提供进一步的状态信息。

7.2 新的消息头

下表是对SIP[1]中表2和表3的扩展,对7.1稍作修改。

Header field      where proxy ACK BYE CAN INV OPT REG PRA SUB NOT
   -----------------------------------------------------------------
   Allow-Events                        o
   Allow-Events       2xx                 o
   Allow-Events       489                 m
   Event                               m
   Subscription-State                  m

7.2.1 Event头

Event被加入message-header元素定义中。

为了让SUBSCRIBE消息和应答消息以及NOTIFY消息匹配,Event头的事件类型部分是按字节比较的,id也是按自己比较的。一个有id参数的Event头和一个没有id参数的Event头是不会相同的。比较的时候其他参数不予考虑。

需要注意,以上描述表示Event: foo; id=1234和Event: foo; param=abcd; id=1234相同;但是Event: foo不相同(id参数不同);和Event: Foo; id=1234也不同(事件类型不同)。

本文没有定义的事件类型的值。这些值将在单独的文档定义,比且必须在IANA中注册。

每个事件头只能有一个事件类型,每个消息有多个事件类型是不允许的。

7.2.2 Allow-Events头

Allow-Events被加入SIP消息中general-header元素中。它的使用方法在3.3.7

7.2.3 Subscription-State头

Subscription-State被加入SIP消息中request-header元素中。它的使用方法在3.2.4

7.3 新应答代码

7.3.1 202 Accepted

202被加入Success头域中。202 Accepted和HTTP/1.1[3]中定义的相同。

7.3.2 489 Bad Event

489被加入Client-Error头域中。489 Bad Event表示不支持Event头中列出的事件包。

7.4 增加的BNF定义

增加的BNF定义允许新的下列新的元素,或是修改下列元素。

SUBSCRIBEm        = %x53.55.42.53.43.52.49.42.45 ; SUBSCRIBE in caps
   NOTIFYm           = %x4E.4F.54.49.46.59 ; NOTIFY in caps
   extension-method  = SUBSCRIBEm / NOTIFYm / token

   Event             ( "Event" / "o" ) HCOLON event-type
                        *( SEMI event-param )
   event-type        event-package *( "." event-template )
   event-package     token-nodot
   event-template    token-nodot
   token-nodot       1*( alphanum / "-"  / "!" / "%" / "*"
                            / "_" / "+" / "`" / "'" / "~" )
   event-param       generic-param / ( "id" EQUAL token )

   Allow-Events =  ( "Allow-Events" / "u" ) HCOLON event-type
                   *(COMMA event-type)

   Subscription-State   = "Subscription-State" HCOLON substate-value
                          *( SEMI subexp-params )
   substate-value       = "active" / "pending" / "terminated"
                          / extension-substate
   extension-substate   = token
   subexp-params         ("reason" EQUAL event-reason-value)
                          / ("expires" EQUAL delta-seconds)
                          / ("retry-after" EQUAL delta-seconds)
                          / generic-param
   event-reason-value    "deactivated"
                          / "probation"
                          / "rejected"
                          / "timeout"
                          / "giveup"
                          / "noresource"
                          / event-reason-extension
   event-reason-extension = token

8 参考目录

[1]   Rosenberg, J., Schulzrinne, H., Camarillo, G., Johnston, A.,
         Peterson, J., Sparks, R., Handley, M. and E. Schooler, "SIP:
         Session Initiation Protocol", RFC 3261, June 2002.

   [2]   Petrack, S. and L. Conroy, "The PINT Service Protocol", RFC
         2848, June 2000.

   [3]   Fielding, R., Gettys, J., Mogul, J., Frystyk, H., Masinter, L.,
         Leach, P. and T. Berners-Lee, "Hypertext Transfer Protocol --
         HTTP/1.1", RFC 2616, June 1999.

   [4]   Narten, T. and H. Alvestrand, "Guidelines for Writing an IANA
         Considerations Section in RFCs", BCP 26, RFC 2434, October
         1998.

   [5]   Bradner, S., "Key Words for Use in RFCs to Indicate Requirement
         Levels", BCP 14, RFC 2119, March 1997.

   [6]   Day, M., Aggarwal, S., Mohr, G. and J. Vincent, "Instant
         Messaging/Presence Protocol Requirements", RFC 2779, February

9 信息参考目录

[7]   Rosenberg, J. and H. Schulzrinne, "Guidelines for Authors of
         SIP Extensions", Work in Progress.

   [8]   Schulzrinne, H. and J. Rosenberg, "SIP Caller Preferences and
         Callee Capabilities", Work in Progress.

 

以下略。

 

翻译后记

花了2个工作日再加上周末晚上的一部分时间中翻译完毕了。

有些是我自己也没看懂,有些是看懂了但是表达不清楚。

都被我强行翻译了,估计别人看是会雨里雾里的,但至少通过翻译我是完整的把这篇文档一字一句的看了遍,懂了八九十吧,还算有收获的。

于是决定把RFC 3261-SIP: Session Initiation Protocol 翻译一下。一直在做这个,但是仅仅依靠对H323的了解和对通信协议的普遍认知在工作着,很多细节都不甚了解。

这个工程浩大了,可能要花上革半年时间,还是希望能够坚持吧。目的还是一样,强迫阅读和练习英文。当然内心深处还是希望能对别人有所帮助,这也是我Live Space最根本的目的之一。但愿我的翻译不要误人子弟。阿弥陀佛。

感谢周末在家翻译时老婆悠扬及令人振奋的练习曲声。