paulwong

#

深度技术揭秘,支付宝,财付通,到底每天都是怎样工作的?

为了可以更好地解释支付结算系统对账过程,我们先把业务从头到尾串起来描述一下场景,帮助大家理解:一个可能得不能再可能的场景,请大家深刻理解里面每个角色做了什么,获取了哪些信息:  某日阳光灿烂,支付宝用户小明在淘宝上看中了暖脚器一只,价格100元。犹豫再三后小明使用支付宝网银完成了支付,支付宝显示支付成功,淘宝卖家通知他已发货,最近几日注意查收。
  小明:持卡人,消费者,淘宝和支付宝的注册会员,完成了支付动作,自己的银行账户资金减少,交易成功。
  银行:收单银行,接受来自支付宝的名为“支付宝BBB”的100元订单,并引导持卡人小明支付成功,扣除小明银行卡账户余额后返回给支付宝成功通知,并告诉支付宝这笔交易在银行流水号为“银行CCC”
  支付宝:支付公司,接收到淘宝发来的订单号为“淘宝AAA”的商户订单号,并形成支付系统唯一流水号:“支付宝BBB”发往银行系统。然后获得银行回复的支付成功信息,顺带银行流水号“银行CCC”
  淘宝:我们支付公司称淘宝这类电商为商户,是支付系统的客户。淘宝向支付系统发送了一笔交易收单请求,金额为100,订单号为:“淘宝AAA”,支付系统最后反馈给淘宝这笔支付流水号为“支付BBB”
  以上流程貌似大家都达到了预期效果,但实际上仍然还有一个问题:
  对支付公司(支付宝)而言,虽然银行通知了它支付成功,但资金实际还要T+1后结算到它银行账户,所以目前只是一个信息流,资金流还没过来。
  Tips:插一句话,对支付系统内部账务来说,由于资金没有能够实时到账,所以此时小明的这笔100元交易资金并没有直接记入到系统资产类科目下的“银行存款”科目中,而是挂在“应收账款”或者“待清算科目”中。大白话就是,这100元虽然答应给我了,我也记下来了,但还没收到,我挂在那里。
  对商户(淘宝)而言,虽然支付公司通知了它支付成功,他也发货了,但资金按照合同也是T+1到账。如果不对账确认一下,恐怕也会不安。
  倒是对消费者(小明)而言:反正钱付了,你们也显示成功了,等暖脚器呀等暖脚器~
  基于支付公司和商户的困惑,我们的支付结算系统需要进行两件事情:一是资金渠道对账,通称对银行帐;二是商户对账,俗称对客户帐。对客户帐又分为对公客户和对私客户,通常对公客户会对对账文件格式、对账周期、系统对接方案有特殊需求,而对私客户也就是我们一般的消费者只需要可以后台查询交易记录和支付历史流水就可以了。
  我们先聊银行资金渠道对账,由于支付公司的资金真正落地在商业银行,所以资金渠道的对账显得尤为重要。
  在一个银行会计日结束后,银行系统会先进行自己内部扎帐,完成无误后进行数据的清分和资金的结算,将支付公司当日应入账的资金结算到支付公司账户中。于此同时,目前多数银行已经支持直接系统对接的方式发送对账文件。于是,在某日临晨4点,支付宝系统收到来自银行发来的前一会计日对账文件。根据数据格式解析正确后和前日支付宝的所有交易数据进行匹配,理想情况是一一匹配成功无误,然后将这些交易的对账状态勾对为“已对账”。
  Tips:此时,对账完成的交易,会将该笔资金从“应收账款”或者“待清算账款”科目中移动到“银行存款”科目中,以示该交易真正资金到账。
  以上太理想了,都那么理想就不要对账了。所以通常都会出一些差错,那么我总结了一下常见的差错情况:
  1.支付时提交到银行后没有反馈,但对账时该交易状态为支付成功
  这种情况很正常,因为我们在信息传输过程中,难免会出现掉包和信息不通畅。消费者在银行端完成了支付行为,银行的通知信息却被堵塞了,如此支付公司也不知道结果,商户也不知道结果。如果信息一直没法通知到支付公司这边,那么这条支付结果就只能在日终对账文件中体现了。这时支付公司系统需要对这笔交易进行补单操作,将交易置为成功并完成记账规则,有必要还要通知到商户。
  此时的小明:估计急的跳起来了……付了钱怎么不给说支付成功呢!坑爹!
  TIPS:通常银行系统会开放一个支付结果查询接口。支付公司会对提交到银行但没有回复结果的交易进行间隔查询,以确保支付结果信息的实时传达。所以以上情况出现的概率已经很少了。
  2.我方支付系统记录银行反馈支付成功,金额为100,银行对账金额不为100
  这种情况已经不太常见了,差错不管是长款和短款都不是我们想要的结果。通常双方系统通讯都是可作为纠纷凭证的,如果银行在支付结果返回时确认是100元,对账时金额不一致,可以要求银行进行协调处理。而这笔账在支付系统中通常也会做对应的挂账处理,直到纠纷解决。
  3.我方支付系统记录银行反馈支付成功,但对账文件中压根不存在
  这种情况也经常见到,因为跨交易会计日的系统时间不同,所以会出现我们认为交易是23点59分完成的,银行认为是第二天凌晨0点1分完成。对于这种情况我们通常会继续挂账,直到再下一日的对账文件送达后进行对账匹配。
  如果这笔交易一直没有找到,那么就和第二种情况一样,是一种短款,需要和银行追究。
  以上情况针对的是一家银行资金渠道所作的流程,实际情况中,支付公司会在不同银行开立不同银行账户,用以收单结算(成本会降低),所以真实情况极有可能是:
  临晨1点,工行对账文件丢过来(支行A)
  临晨1点01分,工行又丢一个文件过来(支行B)
  临晨1点15分,农行对账文件丢过来
  。 。 。
  临晨5点,兴业银行文件丢过来
  。。。
  不管什么时候,中国银行都必须通过我方业务员下载对账文件再上传的方式进行对账,所以系统接收到中行文件一般都是早上9点05分……
  对系统来说,每天都要处理大量并发的对账数据,如果在交易高峰时段进行,会引起客户交互的延迟和交易的失败,这是万万行不得的所以通常支付公司不会用那么傻的方式处理数据,而是在一个会计日结束后,通常也是临晨时段,将前一日交易增量备份到专用对账服务器中,在物理隔绝环境下进行统一的对账行为,杜绝硬件资源的抢占。
  以上是银行资金渠道入款的对账,出款基本原理一致,不过出款渠道在实际业务过程中还有一些特别之处,由于大家不是要建设系统,我就不赘述了。
  谈完了资金渠道的对账,我们再来看看对客户帐。
  前面提到了,由于资金落在银行,所以对支付公司来说,对银行帐非常重要。那么同理,由于资金落在支付公司,所以对商户来说,对支付公司账也一样重要。能否提供高品质甚至定制化的服务,是目前支付公司走差异化路线的一个主要竞争点。
  之前说过,银行与支付公司之间的通讯都是可以作为纠纷凭证的。原理是对支付报文的关键信息进行密钥加签+md5处理,以确保往来报文“不可篡改,不可抵赖”。
  同理,支付公司和商户之间也会有类似机制保证报文的可追溯性,由此我们可以知道,一旦我方支付系统通知商户支付结果,那么我们就要为此承担责任。由此我们再推断一个结论:
  即便某支付订单银行方面出错导致资金未能到账,一旦我们支付系统通知商户该笔交易成功,那么根据协议该结算的资金还是要结算给这个商户。自然,我们回去追究银行的问题,把账款追回。
  没经过排版的小知识点---------------------------------------------------
  一、对支付系统而言,最基本的对账功能是供商户在其后台查询下载某一时间段内的支付数据文件,以供商户自己进行对账。
  这个功能应该是个支付公司就有,如果没有就别混了。
  二、对大多数支付系统而言,目前已经可以做到对账文件的主动投送功能。
  这个功能方便了商户系统和支付系统的对接,商户的结算人员无须登录支付平台后台下载文件进行对账,省去了人工操作的麻烦和风险。
  对大型支付系统而言,商户如果跨时间区域很大,反复查询该区域内的数据并下载,会对服务器造成比较大的压力。各位看官别笑,觉得查个数据怎么就有压力了。实际上为了这个查询,我最早就职的一家支付公司重新优化了所有SQL语句,并且因为查询压力过大服务器瘫痪过好几次。
  现在比较主流的做法是把商户短期内查询过、或者经常要查询的数据做缓存。实在不行就干脆实时备份,两分钟同步一次数据到专用数据库供商户查询,以避免硬件资源占用。甚至……大多数支付公司都会对查询范围跨度和历史事件进行限制,比如最多只能查一个月跨度内,不超过24个月前的数据……以避免服务嗝屁。
  对账这块大致就这样了,再往细的说就说不完了,
  风险控制,在各行各业都尤其重要。
  金融即风险,控制好风险,才有利润。
  虽然第三方支付严格意义上说并非属于金融行业,但由于涉及资金的清分和结算,业务主体又是资金的收付,所以风险控制一样非常重要。
  对支付公司来说,风控主要分为合规政策风控以及交易风控两种。
  前者主要针对特定业务开展,特定产品形态进行法规层面的风险规避,通常由公司法务和风控部门一起合作进行。例如,一家公司要开展第三方支付业务,现在要获得由人民银行颁发的“支付业务许可证”。遵守中国对于金融管制的所有条规,帮助人行监控洗钱行为……这些法规合规风险,虽然条条框框,甚至显得文绉绉,但如果没人解读没人公关,业务都会无法开展。
  当然,这块也不是本题所关注的重点,提问者关注的,应当是业务进行过程中的交易风控环节。
  现在随着各支付公司风险控制意识的加强,风控系统渐渐被重视起来。除了上述提到的合规风控相关功能,风控系统最讲技术含量,最讲业务水平,最考究数据分析的业务就是交易风控环节。
  对一笔支付交易而言,在它发生之前、发生过程中及发生过程后,都会被风控系统严密监控,以确保支付及客户资产安全。而所有的所有秘密,都归结到一个词头上:规则。风控系统是一系列规则的集合,任何再智能的风控方案,都绕不开规则二字。
  我们先看看,哪些情况是交易风控需要监控处理的:
  1.钓鱼网站
  什么是钓鱼呢?
  用我的说法,就是利用技术手段蒙蔽消费者,当消费者想付款给A的时候,替换掉A的支付页面,将钱付给B,以达成非法占用资金的目的。
  还有更低级的,直接就是发小广告,里面带一个类似http://tiaobao.com的网址,打开后和淘宝页面一摸一样,上当客户直接付款给假冒网站主。
  第一种情况风控系统是可以通过规则进行简单判定的,虽然有误杀,但不会多。
  通常使用的规则是判断提交订单的IP地址和银行实际支付完成的IP地址是否一致,如果不一致,则判断为钓鱼网站风险交易,进入待确认订单。
  但第二种情况,亲爹亲娘了,支付公司真的没办法。当然遇到客户投诉后可能会事后补救,但交易是无法阻止了。
  2.盗卡组织利用盗卡进行交易
  大家都知道,信用卡信息是不能随便公布给别人的,国内大多信用卡虽然都设置了密码,但银行仍然会开放无磁无密支付接口给到商户进行快速支付之用。
  所谓无磁无密,就是不需要磁道信息,不需要密码就可以进行支付的通道。只需要获取到客户的CVV,有效期,卡号三个核心信息,而这三个信息是在卡上直接有的,所以大家不要随便把卡交给别人哦~
  碰到类似的这种交易,风控系统就不会像钓鱼网站这样简单判断了。
  过去我们所有的历史交易,都会存库,不仅会存支付相关信息,更会利用网页上的控件(对,恶心的activex或者目前用的比较多的flash控件)抓取支付者的硬件信息,存储在数据库中。
  当一笔交易信息带着能够搜集到的硬件信息一同提交给风控系统时,风控系统会进行多种规则判定。
  例如:当天该卡是否交易超过3次
  当天该IP是否交易超过3次
  该主机CPU的序列号是否在黑名单之列
  等等等等,一批规则跑完后,风控系统会给该交易进行加权评分,标示其风险系数,然后根据评分给出处理结果。
  通过硬件信息采集以及历史交易记录回溯,我们可以建立很多交易风控规则来进行监控。所以规则样式的好坏,规则系数的调整,就是非常重要的用以区别风控系统档次高低的标准。
  例如,我听说著名的风控厂商RED,有一个“神经网络”机制,灰常牛逼。
   
  其中有一个规则我到现在还记忆犹新:
  某人早上八点在加利福尼亚进行了信用卡支付,到了下午一点,他在东亚某小国家发起了信用卡支付申请。系统判断两者距离过长,不是短短5小时内能够到达的,故判定交易无效,支付请求拒绝。
  规则非常重要,当然,数据也一样重要。我们不仅需要从自己的历史记录中整合数据,同时还要联合卡组织、银行、风控机构,购买他们的数据和风控服务用来增加自己的风控实力。
  SO,风控是一个不断积累数据、分析数据、运营数据、积累数据的循环过程。
  好的风控规则和参数,需要经过无数次的规则修改和调整,是一个漫长的过程。
  不知道大家做互联网,有没有利用GA做过AB测试,同样的,风控系统也需要反复地做类似AB测试的实验,以保证理论和实际的匹配。
  最后给大家说一个小小的概念:
  所谓风控,是指风险控制,不是风险杜绝。
  风控的目标一定不是把所有风险全部杜绝。
  合理的风控,目标一定是:利润最大化,而不是风险最小化
  过于严格的风控规则,反而会伤害公司利益(看看销售和风控经常打架就知道了)
  不光是交易的风控,我们日常制定规则,法规,公司流程,也一定要秉着这个出发点进行规划。

posted @ 2015-09-09 23:09 paulwong 阅读(878) | 评论 (0)编辑 收藏

Reactor模式详解

前记

第一次听到Reactor模式是三年前的某个晚上,一个室友突然跑过来问我什么是Reactor模式?我上网查了一下,很多人都是给出NIO中的 Selector的例子,而且就是NIO里Selector多路复用模型,只是给它起了一个比较fancy的名字而已,虽然它引入了EventLoop概 念,这对我来说是新的概念,但是代码实现却是一样的,因而我并没有很在意这个模式。然而最近开始读Netty源码,而Reactor模式是很多介绍Netty的文章中被大肆宣传的模式,因而我再次问自己,什么是Reactor模式?本文就是对这个问题关于我的一些理解和尝试着来解答。

什么是Reactor模式

要回答这个问题,首先当然是求助Google或Wikipedia,其中Wikipedia上说:“The reactor design pattern is an event handling pattern for handling service requests delivered concurrently by one or more inputs. The service handler then demultiplexes the incoming requests and dispatches them synchronously to associated request handlers.”。从这个描述中,我们知道Reactor模式首先是事件驱动的,有一个或多个并发输入源,有一个Service Handler,有多个Request Handlers;这个Service Handler会同步的将输入的请求(Event)多路复用的分发给相应的Request Handler。如果用图来表达:

从结构上,这有点类似生产者消费者模式,即有一个或多个生产者将事件放入一个Queue中,而一个或多个消费者主动的从这个Queue中Poll事件来处理;而Reactor模式则并没有Queue来做缓冲,每当一个Event输入到Service Handler之后,该Service Handler会主动的根据不同的Event类型将其分发给对应的Request Handler来处理。

更学术的,这篇文章(Reactor An Object Behavioral Pattern for Demultiplexing and Dispatching Handles for Synchronous Events)上说:“The Reactor design pattern handles service requests that are delivered concurrently to an application by one or more clients. Each service in an application may consistent of several methods and is represented by a separate event handler that is responsible for dispatching service-specific requests. Dispatching of event handlers is performed by an initiation dispatcher, which manages the registered event handlers. Demultiplexing of service requests is performed by a synchronous event demultiplexer. Also known as Dispatcher, Notifier”。这段描述和Wikipedia上的描述类似,有多个输入源,有多个不同的EventHandler(RequestHandler)来处理不同的请求,Initiation Dispatcher用于管理EventHander,EventHandler首先要注册到Initiation Dispatcher中,然后Initiation Dispatcher根据输入的Event分发给注册的EventHandler;然而Initiation Dispatcher并不监听Event的到来,这个工作交给Synchronous Event Demultiplexer来处理。

Reactor模式结构

在解决了什么是Reactor模式后,我们来看看Reactor模式是由什么模块构成。图是一种比较简洁形象的表现方式,因而先上一张图来表达各个模块的名称和他们之间的关系:

Handle:即操作系统中的句柄,是对资源在操作系统层面上的一种抽象,它可以是打开的文件、一个连接(Socket)、Timer等。由于Reactor模式一般使用在网络编程中,因而这里一般指Socket Handle,即一个网络连接(Connection,在Java NIO中的Channel)。这个Channel注册到Synchronous Event Demultiplexer中,以监听Handle中发生的事件,对ServerSocketChannnel可以是CONNECT事件,对SocketChannel可以是READ、WRITE、CLOSE事件等。
Synchronous Event Demultiplexer:阻塞等待一系列的Handle中的事件到来,如果阻塞等待返回,即表示在返回的Handle中可以不阻塞的执行返回的事件类型。这个模块一般使用操作系统的select来实现。在Java NIO中用Selector来封装,当Selector.select()返回时,可以调用Selector的selectedKeys()方法获取Set<SelectionKey>,一个SelectionKey表达一个有事件发生的Channel以及该Channel上的事件类型。上图的“Synchronous Event Demultiplexer ---notifies--> Handle”的流程如果是对的,那内部实现应该是select()方法在事件到来后会先设置Handle的状态,然后返回。不了解内部实现机制,因而保留原图。
Initiation Dispatcher:用于管理Event Handler,即EventHandler的容器,用以注册、移除EventHandler等;另外,它还作为Reactor模式的入口调用Synchronous Event Demultiplexer的select方法以阻塞等待事件返回,当阻塞等待返回时,根据事件发生的Handle将其分发给对应的Event Handler处理,即回调EventHandler中的handle_event()方法。
Event Handler:定义事件处理方法:handle_event(),以供InitiationDispatcher回调使用。
Concrete Event Handler:事件EventHandler接口,实现特定事件处理逻辑。

Reactor模式模块之间的交互

简单描述一下Reactor各个模块之间的交互流程,先从序列图开始:

1. 初始化InitiationDispatcher,并初始化一个Handle到EventHandler的Map。
2. 注册EventHandler到InitiationDispatcher中,每个EventHandler包含对相应Handle的引用,从而建立Handle到EventHandler的映射(Map)。
3. 调用InitiationDispatcher的handle_events()方法以启动Event Loop。在Event Loop中,调用select()方法(Synchronous Event Demultiplexer)阻塞等待Event发生。
4. 当某个或某些Handle的Event发生后,select()方法返回,InitiationDispatcher根据返回的Handle找到注册的EventHandler,并回调该EventHandler的handle_events()方法。
5. 在EventHandler的handle_events()方法中还可以向InitiationDispatcher中注册新的Eventhandler,比如对AcceptorEventHandler来,当有新的client连接时,它会产生新的EventHandler以处理新的连接,并注册到InitiationDispatcher中。

Reactor模式实现

Reactor An Object Behavioral Pattern for Demultiplexing and Dispatching Handles for Synchronous Events中,一直以Logging Server来分析Reactor模式,这个Logging Server的实现完全遵循这里对Reactor描述,因而放在这里以做参考。Logging Server中的Reactor模式实现分两个部分:Client连接到Logging Server和Client向Logging Server写Log。因而对它的描述分成这两个步骤。
Client连接到Logging Server

1. Logging Server注册LoggingAcceptor到InitiationDispatcher。
2. Logging Server调用InitiationDispatcher的handle_events()方法启动。
3. InitiationDispatcher内部调用select()方法(Synchronous Event Demultiplexer),阻塞等待Client连接。
4. Client连接到Logging Server。
5. InitiationDisptcher中的select()方法返回,并通知LoggingAcceptor有新的连接到来。 
6. LoggingAcceptor调用accept方法accept这个新连接。
7. LoggingAcceptor创建新的LoggingHandler。
8. 新的LoggingHandler注册到InitiationDispatcher中(同时也注册到Synchonous Event Demultiplexer中),等待Client发起写log请求。
Client向Logging Server写Log

1. Client发送log到Logging server。
2. InitiationDispatcher监测到相应的Handle中有事件发生,返回阻塞等待,根据返回的Handle找到LoggingHandler,并回调LoggingHandler中的handle_event()方法。
3. LoggingHandler中的handle_event()方法中读取Handle中的log信息。
4. 将接收到的log写入到日志文件、数据库等设备中。
3.4步骤循环直到当前日志处理完成。
5. 返回到InitiationDispatcher等待下一次日志写请求。

Reactor An Object Behavioral Pattern for Demultiplexing and Dispatching Handles for Synchronous Events有对Reactor模式的C++的实现版本,多年不用C++,因而略过。 

Java NIO对Reactor的实现

在Java的NIO中,对Reactor模式有无缝的支持,即使用Selector类封装了操作系统提供的Synchronous Event Demultiplexer功能。这个Doug Lea已经在Scalable IO In Java中有非常深入的解释了,因而不再赘述,另外这篇文章对Doug Lea的Scalable IO In Java有一些简单解释,至少它的代码格式比Doug Lea的PPT要整洁一些。

需要指出的是,不同这里使用InitiationDispatcher来管理EventHandler,在Doug Lea的版本中使用SelectionKey中的Attachment来存储对应的EventHandler,因而不需要注册EventHandler这个步骤,或者设置Attachment就是这里的注册。而且在这篇文章中,Doug Lea从单线程的Reactor、Acceptor、Handler实现这个模式出发;演化为将Handler中的处理逻辑多线程化,实现类似Proactor模式,此时所有的IO操作还是单线程的,因而再演化出一个Main Reactor来处理CONNECT事件(Acceptor),而多个Sub Reactor来处理READ、WRITE等事件(Handler),这些Sub Reactor可以分别再自己的线程中执行,从而IO操作也多线程化。这个最后一个模型正是Netty中使用的模型。并且在Reactor An Object Behavioral Pattern for Demultiplexing and Dispatching Handles for Synchronous Events的9.5 Determine the Number of Initiation Dispatchers in an Application中也有相应的描述。

EventHandler接口定义

对EventHandler的定义有两种设计思路:single-method设计和multi-method设计:
A single-method interface:它将Event封装成一个Event Object,EventHandler只定义一个handle_event(Event event)方法。这种设计的好处是有利于扩展,可以后来方便的添加新的Event类型,然而在子类的实现中,需要判断不同的Event类型而再次扩展成 不同的处理方法,从这个角度上来说,它又不利于扩展。另外在Netty3的使用过程中,由于它不停的创建ChannelEvent类,因而会引起GC的不稳定。
A multi-method interface:这种设计是将不同的Event类型在 EventHandler中定义相应的方法。这种设计就是Netty4中使用的策略,其中一个目的是避免ChannelEvent创建引起的GC不稳定, 另外一个好处是它可以避免在EventHandler实现时判断不同的Event类型而有不同的实现,然而这种设计会给扩展新的Event类型时带来非常 大的麻烦,因为它需要该接口。

关于Netty4对Netty3的改进可以参考这里
ChannelHandler with no event objectIn 3.x, every I/O operation created a ChannelEvent object. For each read / write, it additionally created a new ChannelBuffer. It simplified the internals of Netty quite a lot because it delegates resource management and buffer pooling to the JVM. However, it often was the root cause of GC pressure and uncertainty which are sometimes observed in a Netty-based application under high load.

4.0 removes event object creation almost completely by replacing the event objects with strongly typed method invocations. 3.x had catch-all event handler methods such as handleUpstream() andhandleDownstream(), but this is not the case anymore. Every event type has its own handler method now:

为什么使用Reactor模式

归功与Netty和Java NIO对Reactor的宣传,本文慕名而学习的Reactor模式,因而已经默认Reactor具有非常优秀的性能,然而慕名归慕名,到这里,我还是要不得不问自己Reactor模式的好处在哪里?即为什么要使用这个Reactor模式?在Reactor An Object Behavioral Pattern for Demultiplexing and Dispatching Handles for Synchronous Events中是这么说的:
Reactor Pattern优点

Separation of concerns: The Reactor pattern decouples application-independent demultiplexing and dispatching mechanisms from application-specific hook method functionality. The application-independent mechanisms become reusable components that know how to demultiplex events and dispatch the appropriate hook methods defined byEvent Handlers. In contrast, the application-specific functionality in a hook method knows how to perform a particular type of service.

Improve modularity, reusability, and configurability of event-driven applications: The pattern decouples application functionality into separate classes. For instance, there are two separate classes in the logging server: one for establishing connections and another for receiving and processing logging records. This decoupling enables the reuse of the connection establishment class for different types of connection-oriented services (such as file transfer, remote login, and video-on-demand). Therefore, modifying or extending the functionality of the logging server only affects the implementation of the logging handler class.

Improves application portability: The Initiation Dispatcher’s interface can be reused independently of the OS system calls that perform event demultiplexing. These system calls detect and report the occurrence of one or more events that may occur simultaneously on multiple sources of events. Common sources of events may in- clude I/O handles, timers, and synchronization objects. On UNIX platforms, the event demultiplexing system calls are calledselect and poll [1]. In the Win32 API [16], the WaitForMultipleObjects system call performs event demultiplexing.

Provides coarse-grained concurrency control: The Reactor pattern serializes the invocation of event handlers at the level of event demultiplexing and dispatching within a process or thread. Serialization at the Initiation Dispatcher level often eliminates the need for more complicated synchronization or locking within an application process.

这些貌似是很多模式的共性:解耦、提升复用性、模块化、可移植性、事件驱动、细力度的并发控制等,因而并不能很好的说明什么,特别是它鼓吹的对性能的提升,这里并没有体现出来。当然在这篇文章的开头有描述过另一种直观的实现:Thread-Per-Connection,即传统的实现,提到了这个传统实现的以下问题:
Thread Per Connection缺点

Efficiency: Threading may lead to poor performance due to context switching, synchronization, and data movement [2];

Programming simplicity: Threading may require complex concurrency control schemes;

Portability: Threading is not available on all OS platforms. 对于性能,它其实就是第一点关于Efficiency的描述,即线程的切换、同步、数据的移动会引起性能问题。也就是说从性能的角度上,它最大的提升就是减少了性能的使用,即不需要每个Client对应一个线程。我的理解,其他业务逻辑处理很多时候也会用到相同的线程,IO读写操作相对CPU的操作还是要慢很多,即使Reactor机制中每次读写已经能保证非阻塞读写,这里可以减少一些线程的使用,但是这减少的线程使用对性能有那么大的影响吗?答案貌似是肯定的,这篇论文(SEDA: Staged Event-Driven Architecture - An Architecture for Well-Conditioned, Scalable Internet Service)对随着线程的增长带来性能降低做了一个统计:

在这个统计中,每个线程从磁盘中读8KB数据,每个线程读同一个文件,因而数据本身是缓存在操作系统内部的,即减少IO的影响;所有线程是事先分配的,不会有线程启动的影响;所有任务在测试内部产生,因而不会有网络的影响。该统计数据运行环境:Linux 2.2.14,2GB内存,4-way 500MHz Pentium III。从图中可以看出,随着线程的增长,吞吐量在线程数为8个左右的时候开始线性下降,并且到64个以后而迅速下降,其相应事件也在线程达到256个后指数上升。即1+1<2,因为线程切换、同步、数据移动会有性能损失,线程数增加到一定数量时,这种性能影响效果会更加明显。

对于这点,还可以参考C10K Problem,用以描述同时有10K个Client发起连接的问题,到2010年的时候已经出现10M Problem了。

当然也有人说:Threads are expensive are no longer valid.在不久的将来可能又会发生不同的变化,或者这个变化正在、已经发生着?没有做过比较仔细的测试,因而不敢随便断言什么,然而本人观点,即使线程变的影响并没有以前那么大,使用Reactor模式,甚至时SEDA模式来减少线程的使用,再加上其他解耦、模块化、提升复用性等优点,还是值得使用的。

Reactor模式的缺点

Reactor模式的缺点貌似也是显而易见的:
1. 相比传统的简单模型,Reactor增加了一定的复杂性,因而有一定的门槛,并且不易于调试。
2. Reactor模式需要底层的Synchronous Event Demultiplexer支持,比如Java中的Selector支持,操作系统的select系统调用支持,如果要自己实现Synchronous Event Demultiplexer可能不会有那么高效。
3. Reactor模式在IO读写数据时还是在同一个线程中实现的,即使使用多个Reactor机制的情况下,那些共享一个Reactor的Channel如果出现一个长时间的数据读写,会影响这个Reactor中其他Channel的相应时间,比如在大文件传输时,IO操作就会影响其他Client的相应时间,因而对这种操作,使用传统的Thread-Per-Connection或许是一个更好的选择,或则此时使用Proactor模式。

参考

Reactor Pattern WikiPedia
Reactor An Object Behavioral Pattern for Demultiplexing and Dispatching Handles for Synchronous Events
Scalable IO In Java
C10K Problem WikiPedia

posted @ 2015-09-08 12:28 paulwong 阅读(724) | 评论 (0)编辑 收藏

Netty3架构解析

前记

很早以前就有读Netty源码的打算了,然而第一次尝试的时候从Netty4开始,一直抓不到核心的框架流程,后来因为其他事情忙着就放下了。这次趁着休假重新捡起这个硬骨头,因为Netty3现在还在被很多项目使用,因而这次决定先从Netty3入手,瞬间发现Netty3的代码比Netty4中规中矩的多,很多概念在代码本身中都有清晰的表达,所以半天就把整个框架的骨架搞清楚了。再读Netty4对Netty3的改进总结,回去读Netty4的源码,反而觉得轻松了,一种豁然开朗的感觉。

记得去年读Jetty源码的时候,因为代码太庞大,并且自己的HTTP Server的了解太少,因而只能自底向上的一个一个模块的叠加,直到最后把所以的模块连接在一起而看清它的真正核心骨架。现在读源码,开始习惯先把骨架理清,然后延伸到不同的器官、血肉而看清整个人体。

本文从Reactor模式在Netty3中的应用,引出Netty3的整体架构以及控制流程;然而除了Reactor模式,Netty3还在ChannelPipeline中使用了Intercepting Filter模式,这个模式也在Servlet的Filter中成功使用,因而本文还会从Intercepting Filter模式出发详细介绍ChannelPipeline的设计理念。本文假设读者已经对Netty有一定的了解,因而不会包含过多入门介绍,以及帮Netty做宣传的文字。

Netty3中的Reactor模式

Reactor模式在Netty中应用非常成功,因而它也是在Netty中受大肆宣传的模式,关于Reactor模式可以详细参考本人的另一篇文章《Reactor模式详解》,对Reactor模式的实现是Netty3的基本骨架,因而本小节会详细介绍Reactor模式如何应用Netty3中。

如果读《Reactor模式详解》,我们知道Reactor模式由Handle、Synchronous Event Demultiplexer、Initiation Dispatcher、Event Handler、Concrete Event Handler构成,在Java的实现版本中,Channel对应Handle,Selector对应Synchronous Event Demultiplexer,并且Netty3还使用了两层Reactor:Main Reactor用于处理Client的连接请求,Sub Reactor用于处理和Client连接后的读写请求(关于这个概念还可以参考Doug Lea的这篇PPT:Scalable IO In Java)。所以我们先要解决Netty3中使用什么类实现所有的上述模块并把他们联系在一起的,以NIO实现方式为例:

模式是一种抽象,但是在实现中,经常会因为语言特性、框架和性能需要而做一些改变,因而Netty3对Reactor模式的实现有一套自己的设计:

1. ChannelEvent:Reactor是基于事件编程的,因而在Netty3中使用ChannelEvent抽象的表达Netty3内部可以产生的各种事件,所有这些事件对象在Channels帮助类中产生,并且由它将事件推入到ChannelPipeline中,ChannelPipeline构建ChannelHandler管道,ChannelEvent流经这个管道实现所有的业务逻辑处理。ChannelEvent对应的事件有:ChannelStateEvent表示Channel状态的变化事件,而如果当前Channel存在Parent Channel,则该事件还会传递到Parent Channel的ChannelPipeline中,如OPEN、BOUND、CONNECTED、INTEREST_OPS等,该事件可以在各种不同实现的Channel、ChannelSink中产生;MessageEvent表示从Socket中读取数据完成、需要向Socket写数据或ChannelHandler对当前Message解析(如Decoder、Encoder)后触发的事件,它由NioWorker、需要对Message做进一步处理的ChannelHandler产生;WriteCompletionEvent表示写完成而触发的事件,它由NioWorker产生;ExceptionEvent表示在处理过程中出现的Exception,它可以发生在各个构件中,如Channel、ChannelSink、NioWorker、ChannelHandler中;IdleStateEvent由IdleStateHandler触发,这也是一个ChannelEvent可以无缝扩展的例子。注:在Netty4后,已经没有ChannelEvent类,所有不同事件都用对应方法表达,这也意味这ChannelEvent不可扩展,Netty4采用在ChannelInboundHandler中加入userEventTriggered()方法来实现这种扩展,具体可以参考这里

2. ChannelHandler:在Netty3中,ChannelHandler用于表示Reactor模式中的EventHandler。ChannelHandler只是一个标记接口,它有两个子接口:ChannelDownstreamHandler和ChannelUpstreamHandler,其中ChannelDownstreamHandler表示从用户应用程序流向Netty3内部直到向Socket写数据的管道,在Netty4中改名为ChannelOutboundHandler;ChannelUpstreamHandler表示数据从Socket进入Netty3内部向用户应用程序做数据处理的管道,在Netty4中改名为ChannelInboundHandler。

3. ChannelPipeline:用于管理ChannelHandler的管道,每个Channel一个ChannelPipeline实例,可以运行过程中动态的向这个管道中添加、删除ChannelHandler(由于实现的限制,在最末端的ChannelHandler向后添加或删除ChannelHandler不一定在当前执行流程中起效,参考这里)。ChannelPipeline内部维护一个ChannelHandler的双向链表,它以Upstream(Inbound)方向为正向,Downstream(Outbound)方向为方向。ChannelPipeline采用Intercepting Filter模式实现,具体可以参考这里,这个模式的实现在后一节中还是详细介绍。

4. NioSelector:Netty3使用NioSelector来存放Selector(Synchronous Event Demultiplexer),每个新产生的NIO Channel都向这个Selector注册自己以让这个Selector监听这个NIO Channel中发生的事件,当事件发生时,调用帮助类Channels中的方法生成ChannelEvent实例,将该事件发送到这个Netty Channel对应的ChannelPipeline中,而交给各级ChannelHandler处理。其中在向Selector注册NIO Channel时,Netty Channel实例以Attachment的形式传入,该Netty Channel在其内部的NIO Channel事件发生时,会以Attachment的形式存在于SelectionKey中,因而每个事件可以直接从这个Attachment中获取相关链的Netty Channel,并从Netty Channel中获取与之相关联的ChannelPipeline,这个实现和Doug Lea的Scalable IO In Java一模一样。另外Netty3还采用了Scalable IO In Java中相同的Main Reactor和Sub Reactor设计,其中NioSelector的两个实现:Boss即为Main Reactor,NioWorker为Sub Reactor。Boss用来处理新连接加入的事件,NioWorker用来处理各个连接对Socket的读写事件,其中Boss通过NioWorkerPool获取NioWorker实例,Netty3模式使用RoundRobin方式放回NioWorker实例。更形象一点的,可以通过Scalable IO In Java的这张图表达:


若与Ractor模式对应,NioSelector中包含了Synchronous Event Demultiplexer,而ChannelPipeline中管理着所有EventHandler,因而NioSelector和ChannelPipeline共同构成了Initiation Dispatcher。

5. ChannelSink:在ChannelHandler处理完成所有逻辑需要向客户端写响应数据时,一般会调用Netty Channel中的write方法,然而在这个write方法实现中,它不是直接向其内部的Socket写数据,而是交给Channels帮助类,内部创建DownstreamMessageEvent,反向从ChannelPipeline的管道中流过去,直到第一个ChannelHandler处理完毕,最后交给ChannelSink处理,以避免阻塞写而影响程序的吞吐量。ChannelSink将这个MessageEvent提交给Netty Channel中的writeBufferQueue,最后NioWorker会等到这个NIO Channel已经可以处理写事件时无阻塞的向这个NIO Channel写数据。这就是上图的send是从SubReactor直接出发的原因。

6. Channel:Netty有自己的Channel抽象,它是一个资源的容器,包含了所有一个连接涉及到的所有资源的饮用,如封装NIO Channel、ChannelPipeline、Boss、NioWorkerPool等。另外它还提供了向内部NIO Channel写响应数据的接口write、连接/绑定到某个地址的connect/bind接口等,个人感觉虽然对Channel本身来说,因为它封装了NIO Channel,因而这些接口定义在这里是合理的,但是如果考虑到Netty的架构,它的Channel只是一个资源容器,有这个Channel实例就可以得到和它相关的基本所有资源,因而这种write、connect、bind动作不应该再由它负责,而是应该由其他类来负责,比如在Netty4中就在ChannelHandlerContext添加了write方法,虽然netty4并没有删除Channel中的write接口。

Netty3中的Intercepting Filter模式

如果说Reactor模式是Netty3的骨架,那么Intercepting Filter模式则是Netty的中枢。Reactor模式主要应用在Netty3的内部实现,它是Netty3具有良好性能的基础,而Intercepting Filter模式则是ChannelHandler组合实现一个应用程序逻辑的基础,只有很好的理解了这个模式才能使用好Netty,甚至能得心应手。

关于Intercepting Filter模式的详细介绍可以参考这里,本节主要介绍Netty3中对Intercepting Filter模式的实现,其实就是DefaultChannelPipeline对Intercepting Filter模式的实现。在上文有提到Netty3的ChannelPipeline是ChannelHandler的容器,用于存储与管理ChannelHandler,同时它在Netty3中也起到桥梁的作用,即它是连接Netty3内部到所有ChannelHandler的桥梁。作为ChannelPipeline的实现者DefaultChannelPipeline,它使用一个ChannelHandler的双向链表来存储,以DefaultChannelPipelineContext作为节点:

public interface ChannelHandlerContext {
    Channel getChannel();
    ChannelPipeline getPipeline();
    String getName();
    ChannelHandler getHandler();
    boolean canHandleUpstream();
    boolean canHandleDownstream();
    void sendUpstream(ChannelEvent e);
    void sendDownstream(ChannelEvent e);
    Object getAttachment();
    void setAttachment(Object attachment);
}

private final class DefaultChannelHandlerContext implements ChannelHandlerContext {
    volatile DefaultChannelHandlerContext next;
    volatile DefaultChannelHandlerContext prev;
    private final String name;
    private final ChannelHandler handler;
    private final boolean canHandleUpstream;
    private final boolean canHandleDownstream;
    private volatile Object attachment;
..
}

在DefaultChannelPipeline中,它存储了和当前ChannelPipeline相关联的Channel、ChannelSink以及ChannelHandler链表的head、tail,所有ChannelEvent通过sendUpstream、sendDownstream为入口流经整个链表:

public class DefaultChannelPipeline implements ChannelPipeline {
    private volatile Channel channel;
    private volatile ChannelSink sink;
    private volatile DefaultChannelHandlerContext head;
    private volatile DefaultChannelHandlerContext tail;

    public void sendUpstream(ChannelEvent e) {
        DefaultChannelHandlerContext head = getActualUpstreamContext(this.head);
        if (head == null) {
            return;
        }
        sendUpstream(head, e);
    }

    void sendUpstream(DefaultChannelHandlerContext ctx, ChannelEvent e) {
        try {
            ((ChannelUpstreamHandler) ctx.getHandler()).handleUpstream(ctx, e);
        } catch (Throwable t) {
            notifyHandlerException(e, t);
        }
    }

    public void sendDownstream(ChannelEvent e) {
        DefaultChannelHandlerContext tail = getActualDownstreamContext(this.tail);
        if (tail == null) {
            try {
                getSink().eventSunk(this, e);
                return;
            } catch (Throwable t) {
                notifyHandlerException(e, t);
                return;
            }
        }
        sendDownstream(tail, e);
    }

    void sendDownstream(DefaultChannelHandlerContext ctx, ChannelEvent e) {
        if (e instanceof UpstreamMessageEvent) {
            throw new IllegalArgumentException("cannot send an upstream event to downstream");
        }
        try {
            ((ChannelDownstreamHandler) ctx.getHandler()).handleDownstream(ctx, e);
        } catch (Throwable t) {
            e.getFuture().setFailure(t);
            notifyHandlerException(e, t);
        }
    }

对Upstream事件,向后找到所有实现了ChannelUpstreamHandler接口的ChannelHandler组成链(
getActualUpstreamContext()),而对Downstream事件,向前找到所有实现了ChannelDownstreamHandler接口的ChannelHandler组成链(getActualDownstreamContext()):

    private DefaultChannelHandlerContext getActualUpstreamContext(DefaultChannelHandlerContext ctx) {
        if (ctx == null) {
            return null;
        }
        DefaultChannelHandlerContext realCtx = ctx;
        while (!realCtx.canHandleUpstream()) {
            realCtx = realCtx.next;
            if (realCtx == null) {
                return null;
            }
        }
        return realCtx;
    }
    private DefaultChannelHandlerContext getActualDownstreamContext(DefaultChannelHandlerContext ctx) {
        if (ctx == null) {
            return null;
        }
        DefaultChannelHandlerContext realCtx = ctx;
        while (!realCtx.canHandleDownstream()) {
            realCtx = realCtx.prev;
            if (realCtx == null) {
                return null;
            }
        }
        return realCtx;
    }

在实际实现ChannelUpstreamHandler或ChannelDownstreamHandler时,调用 ChannelHandlerContext中的sendUpstream或sendDownstream方法将控制流程交给下一个 ChannelUpstreamHandler或下一个ChannelDownstreamHandler,或调用Channel中的write方法发送 响应消息。

public class MyChannelUpstreamHandler implements ChannelUpstreamHandler {
    public void handleUpstream(ChannelHandlerContext ctx, ChannelEvent e) throws Exception {
        // handle current logic, use Channel to write response if needed.
        
// ctx.getChannel().write(message);
        ctx.sendUpstream(e);
    }
}

public class MyChannelDownstreamHandler implements ChannelDownstreamHandler {
    public void handleDownstream(
            ChannelHandlerContext ctx, ChannelEvent e) throws Exception {
        // handle current logic
        ctx.sendDownstream(e);
    }
}

当ChannelHandler向ChannelPipelineContext发送事件时,其内部从当前ChannelPipelineContext节点出发找到下一个ChannelUpstreamHandler或ChannelDownstreamHandler实例,并向其发送ChannelEvent,对于Downstream链,如果到达链尾,则将ChannelEvent发送给ChannelSink:

public void sendDownstream(ChannelEvent e) {
    DefaultChannelHandlerContext prev = getActualDownstreamContext(this.prev);
    if (prev == null) {
        try {
            getSink().eventSunk(DefaultChannelPipeline.this, e);
        } catch (Throwable t) {
            notifyHandlerException(e, t);
        }
    } else {
        DefaultChannelPipeline.this.sendDownstream(prev, e);
    }
}

public void sendUpstream(ChannelEvent e) {
    DefaultChannelHandlerContext next = getActualUpstreamContext(this.next);
    if (next != null) {
        DefaultChannelPipeline.this.sendUpstream(next, e);
    }
}

正是因为这个实现,如果在一个末尾的ChannelUpstreamHandler中先移除自己,在向末尾添加一个新的ChannelUpstreamHandler,它是无效的,因为它的next已经在调用前就固定设置为null了。


ChannelPipeline作为ChannelHandler的容器,它还提供了各种增、删、改ChannelHandler链表中的方法,而且如果某个ChannelHandler还实现了LifeCycleAwareChannelHandler,则该ChannelHandler在被添加进ChannelPipeline或从中删除时都会得到同志:

public interface LifeCycleAwareChannelHandler extends ChannelHandler {
    void beforeAdd(ChannelHandlerContext ctx) throws Exception;
    void afterAdd(ChannelHandlerContext ctx) throws Exception;
    void beforeRemove(ChannelHandlerContext ctx) throws Exception;
    void afterRemove(ChannelHandlerContext ctx) throws Exception;
}

public interface ChannelPipeline {
    void addFirst(String name, ChannelHandler handler);
    void addLast(String name, ChannelHandler handler);
    void addBefore(String baseName, String name, ChannelHandler handler);
    void addAfter(String baseName, String name, ChannelHandler handler);
    void remove(ChannelHandler handler);
    ChannelHandler remove(String name);
    <T extends ChannelHandler> T remove(Class<T> handlerType);
    ChannelHandler removeFirst();
    ChannelHandler removeLast();
    void replace(ChannelHandler oldHandler, String newName, ChannelHandler newHandler);
    ChannelHandler replace(String oldName, String newName, ChannelHandler newHandler);
    <T extends ChannelHandler> T replace(Class<T> oldHandlerType, String newName, ChannelHandler newHandler);
    ChannelHandler getFirst();
    ChannelHandler getLast();
    ChannelHandler get(String name);
    <T extends ChannelHandler> T get(Class<T> handlerType);
    ChannelHandlerContext getContext(ChannelHandler handler);
    ChannelHandlerContext getContext(String name);
    ChannelHandlerContext getContext(Class<? extends ChannelHandler> handlerType);
    void sendUpstream(ChannelEvent e);
    void sendDownstream(ChannelEvent e);
    ChannelFuture execute(Runnable task);
    Channel getChannel();
    ChannelSink getSink();
    void attach(Channel channel, ChannelSink sink);
    boolean isAttached();
    List<String> getNames();
    Map<String, ChannelHandler> toMap();
}

在DefaultChannelPipeline的ChannelHandler链条的处理流程为:


http://www.blogjava.net/DLevin/archive/2015/09/04/427031.html


参考:

《Netty主页》
《Netty源码解读(四)Netty与Reactor模式》
《Netty代码分析》
Scalable IO In Java
Intercepting Filter Pattern

posted @ 2015-09-08 11:11 paulwong 阅读(550) | 评论 (0)编辑 收藏

最新版BOOTSTRAP 4.0教学


http://wiki.jikexueyuan.com/project/bootstrap4/

posted @ 2015-09-02 10:56 paulwong 阅读(520) | 评论 (0)编辑 收藏

张左峰的歪理邪说 之 Redmine 1.4.X 史上最全插件方案

原创作品,允许转载,转载时请务必以超链接形式标明文章 原始出处 、作者信息和本声明。否则将追究法律责任。http://salivaxiu.blog.51cto.com/330680/1613510

前面有一个0.9.X的插件推荐列表,但是太老了,更新一下!

 

PS.很多插件是我自己汉化,甚至付费的,并且进行小幅修改,所以仅供参考。。。。。

 

PPS.话说,搞Redmine这帮人,真是一群疯子,更新太快了。。。。。就不敢更新慢点么。。。。。。

 

Advanced roadmap & milestones plugin  0.7.0  高级路线图,里程碑,不解释
Author box  0.0.3  这个可以在Wiki侧边栏显示一个你的头像和信息,搞个人崇拜用的
CRM plugin 2.3.3-pro CRM插件,很好用,也很专业,付费的
Due Date Reminder plugin  0.2.1 超期提醒插件
Issue Hot Buttons Plugin  0.4.5 超级有用的插件,快速自定义功能按钮,很方便更新问题,尤其是一两项的时候
Redmine Add Subversion Links  0.0.5 为版本库增加直接的SVN链接
Redmine Assets plugin  0.0.1 软资产管理,各位亲们,如果你有几万个问题,里卖包含图片文档啥的,你要找一个特痛苦吧?这个插件帮你解决
Redmine Banner plugin 0.0.6 公告。。。超级实用。。。可以针对全局,也可以针对项目,发出公告
Redmine Better Gantt Chart plugin 0.6.5 更好的甘特图。。的确更好
Redmine CKEditor plugin 0.0.6 超级棒的Textile插件,替换Redmine那个简单的文本编辑器
Redmine Code Review plugin 0.4.8 代码评审,不解释
Redmine Default Version plugin 0.0.2 默认版本,给新建问题设置一个默认版本
Digest plugin 0.2.0 一个后台发送项目活动信息的插件,做日报,或者什么汇报用的DMSF 1.2.3 网页版的SVN,文档管家,支持HTML5,超级好用,但是界面稍显臃肿
Redmine Doodles plugin 0.5.1 投票,不解释
Redmine Glossary Plugin 0.7.0 名词解释插件,知识管理一部分
Redmine Good Job plugin 0.1.1 Goodjob....耍帅用的
Issue Importer 1.0 导入插件,超级实用,如果你有多个工作系统,切换来切换去,离不开这玩意,但是不建议新手使用,批量出错,特痛苦。
Redmine Information Plugin 0.2.5 这个也挺有用的,把系统的一些信息开放出来,比如宏,让大家参考
Redmine Attach Screenshot plugin 0.4.2 附件截图,好似用了Java
Redmine Issue Checklist plugin 1.0.3 Checklist,这个还解释么
Redmine Issue Extensions plugin 0.1.0 问题扩展插件,官方都用的,必须用啊
Redmine Issue Templates plugin 0.0.2.1 这是一个比较好用的插件,问题模板,尤其是一些重复性高,但是需要对过程管理的,配合
ChecklistKnowledgebase 1.0.0 知识库。。。我觉得也很实用的,不过有个BUG,需要你在数据库指定关联一下项目
List duplicate views plugin 0.0.5 好似可以检测到插件冲突。。。我就遇到过
Redmine Local Avatars plugin 0.1.1 本地头像,哈哈,我们断网的
Redmine Logs plugin 0.0.3 可以再后台直接看日志,不用登陆到服务器了
Redmine (Monitoring & Controlling | Monitoramento & Controle) 0.1.1 图形化的数据分析插件,装B用的(当然也有实际意义,宏观数据)
My Roadmaps plugin 0.1.12 我的路线图。。。。其实没啥用
Redmine News Balloon plugin 0.0.1 如果有新闻,会弹出一个气泡,提醒你
Niko-niko Calendar plugin 1.1.2 我最最最最喜欢的插件,感觉跟每天微博一样,挺有意思的
Redmine plugin views revisions plugin 0.0.1 忘了。。。。。
Preview attached files and attributes column plugin 0.1.7 附件的那个缩略图
Private Wiki 0.1.1 私有维基,Redmine最大的问题,在于权限管理不够细分,多这么一个插件,就相当增加一项特殊权限
Reorder links arbitrary 0.0.7 这个对管理员设置有帮助,可以快速设置排序
Smart issues sort plugin 0.3.1 智能排序,对经常用父任务的人,帮助很大
My Page Blocks plugin 1.2 (20120610) 自定义我的工作台,很好使!
Redmine Wiki Extensions plugin 0.4.1 维基扩展,不解释,官方都用
Wiki sidebar toc plugin 0.0.1 维基侧边栏吧?想不起来了
Redmine Wiki table of contents plugin 0.0.3 维基快速位置排序
Issues XLS export 0.2.1  问题列表XLS导出

 

我还没用的,但是考虑装上试试的列表,都是1.4.X可以用的

 

用户属性 http://www.redmine.org/plugins/userprofile
会议室预定 http://www.redmine.org/plugins/mmqb
费用、分票 http://www.redmine.org/plugins/invoices       需付费,可以考虑
免费发票 http://www.redmine.org/plugins/haltr
我的技能 http://www.redmine.org/plugins/redmine_coderwall
甘特图日期 http://www.redmine.org/plugins/redmine_gantt_with_date
登陆后跳转 http://www.redmine.org/plugins/landing_page
Wiki加密 http://www.redmine.org/plugins/redmine_wikicipher     可以考虑
Email过滤 http://www.redmine.org/plugins/redmine_email_notification_content_filter  可以考虑
另一个截图 http://www.redmine.org/plugins/javasript_screenshot
项目侧边栏 http://www.redmine.org/plugins/sidebar       可以考虑
匿名观察 http://www.redmine.org/plugins/anonymous-watchers     可以考虑
你的意思 http://www.redmine.org/plugins/didyoumean      可以考虑
隐身模式 http://www.redmine.org/plugins/redmine_stealth      可以考虑
发行说明 http://www.redmine.org/plugins/redmine_release_notes

论坛主题 http://www.redmine.org/plugins/redmine_boards
自定义邮件 http://www.redmine.org/plugins/notify_custom_users
日历假期 http://www.redmine.org/plugins/redmine_multi_calendar
多种上传 http://www.redmine.org/plugins/redmine_multiple_files_upload

Wiki
维基按钮 http://www.redmine.org/plugins/redmine_wiki_files_toolbar

Time
时间联系 http://www.redmine.org/plugins/linked_time_entries

版本库
SCM扩展  http://www.redmine.org/plugins/redmine_scm_extensions
编辑维基 http://www.redmine.org/plugins/redmine_require_wiki_comment

posted @ 2015-08-19 10:26 paulwong 阅读(2239) | 评论 (0)编辑 收藏

20 个有用的 Angular.js 工具

喜欢 Angular.js?我们为开发者编写了一份最佳 angular.js 工具和资源清单,这可让使用 angular 开发应用程序变得高效。

对于大多数想要设计动态 web 应用的开发者而言,Angular.js 成为了一个可以选择的框架。angularjs 开发者如果想开始一个 AngularJS 工程,为了采取成熟的方式开发网页,他们或许需要很多工具。

在开始使用 angular.js 之前,Angular.js 新手或许也想读一些优秀的 angular.js 书籍。

我们同样也编写了一份庞大的在线 angular.js 教程清单。

为了减轻使用 AngularJS 开发 web 应用的负担,这里列出了几个出色的工具,包括测试、前端开发、编辑、函数库、扩展、模块、代码生成器、网格工具。

shirleywong
shirleywong
翻译于 1周前

0人顶

 翻译的不错哦!

Angular.js 开发最佳 IDE

Angular.js 需要的大部分集成开发环境(IDE)和轻量级的编辑器列在了下面。

Webstorm

Webstorm是一个出色的代码编辑器,它完全理解工程,可以为任何类型的网站提供高质量的代码。它支持所有最流行最新的网站开发技术。使用这个工具开发网站,可以很好地集成单点或流程。 

Aptana

Aptana 是一具集成开发环境,可以帮助你优雅地创建网站应用。使用它做为 AngularJS 的集成开发工具,你需要在 Eclipse 商店激活 AngularJS Eclipse 扩展。

wancheng
wancheng
翻译于 1周前

0人顶

 翻译的不错哦!

文本编辑器 Sublime

对 AngularJS Web 开发者来说,方便的文本编辑工具是 Sublime。它帮助程序员使用快捷方式或几个按键实现编码。它具有很强的适应性,可以定制任何类型的编程环境。它也可以按照你的意愿分块编辑。它还可以轻松地在项目之间切换,所有的修改都将自动保存在各自的项目中。

Angular.js 的专用测试工具

测试是开发的重要组成部分,无论对于使用 Angular.js 还是任何其他软件都是如此。下面的工具可以帮助你简化采用 Angular.js 开发的应用程序的测试。

这些工具都是在线 JavaScript 测试工具的好伴侣。

Protractor

Protractor 是一个端到端的测试框架,贯穿于 AngularJS,是一个完全自动化的测试工具。它可以运行在真实的浏览器中测试你的应用程序。它使用了 WebDriver,Mocha, jasmine, Node.js, selenium 和cucumber 等都使用的伟大的技术。

当所有的待处理网页任务完成时,它会自动使用AngularJS的应用进行通信。所以,你在测试时不需要使用等待或睡眠命令。

Iam魔方
Iam魔方
翻译于 1周前

0人顶

 翻译的不错哦!

Jasmine

Jasmine 是一个行为驱动开发框架,专为 Javascript 用户创建。它提供了基本的测试框架,并且可以持续维护。所有测试都可以使用一个 all-in-one 的包完成,这是 Jasmine 的主要特点,高效测试你的应用。

这个框架的一个缺点是它不能感知运行平台(浏览器)。如果配合Karma使用这个问题很容易避免。

Karma 可以做为测试运行者配合 Jasmine 使用。它是一个测试框架帮且你高效地测试应用。 

Code Orchestra

它是一个绝对意义上的前端开发工具帮助你创建和测试网站应用。你可以实时写代码,根据建议修改代码,以同样的格式保存代码。通过这个工具修改后的代码会自动布署到运行中的应用。

wancheng
wancheng
翻译于 1周前

0人顶

 翻译的不错哦!

最好的 Angular.js 函数库

下面是一些有用的库,它们可以增强 angular.js 框架的能力,对开发者有所帮助。没有必要从头构建那些可能已经成为开放源码的函数库。

CodePen

对于所有 HTML,CSS 和 JavaScript 的前端开发者,CodePen 是一款完美的编辑工具。这个工具可以最大限度地减少对网站的创建、测试和完善的繁琐的网页开发工作。它是一个协作的在线编程环境。

Web 开发人员可以清晰地跨平台实时查看。它有一个浏览器中的代码编辑器,可以自动地迅速地上传多个文件。这个功能可协助 Web 开发人员在几秒的时间内创建一个新的代码。

Angular Fire

使用 Angular Fire,可以轻松地帮助你开发 AngularJS 的应用后台。AngularJS 绑定的 Firebase 已经正式被 Angular Fire 支持。Firebase 是一个基于云计算的平台,可以很容易地集成实时应用和快速创建后台。

当 Firebase 和 Angular Fire 组合在一起,它们有助于以更快的速度同步数据和提供良好的用户管理服务。它还提供了一个三向的数据绑定、用户身份验证和静态托管。

Iam魔方
Iam魔方
翻译于 7天前

0人顶

 翻译的不错哦!

AngularUI

AngularJS 以高效率创建单页面应用而出名. 创建这些单页面应用时候,我们需要一个灵活的路由,这个优秀的AngularJS框架是构建一个全面的UI组件俗称ui-router。 它能根据应用程序的状态提供一个简单的导航和改变视图,而不仅仅是基于URL。
AngularUI还包含非常多的UI组件,这些组件是使用原生指令像ui—maps,ui-calendar,ui-Bootstrap创建的。这些UI组件和指令可以更快建设Angular网站

UI Bootstrap

UI Bootstrap是一个不同寻常的AngularUI组件,它能帮助你创建基于智能手机的web应用程序,而且用户体验不错。这个UI组件提供的AngularJS原生指令完全兼容Twitte Bootstrap

成熟的毛毛虫
成熟的毛毛虫
翻译于 5天前

1人顶

 翻译的不错哦!

Angular.js 有用的扩展和工具

下面是一些 Angular.js 扩展,可以满足一些特殊应用之需。

Ng-Inspector

Ng-Inspector 是一个优秀的浏览器插件,支持 Firefox,Chrome 和 Safari,复用它可以创建一个探测控制面板,方便开发,调试 AngularJS 应用,它提供了完整的辅助功能。

使用它可以更方便的和你的应用交互,还可以实时更新。它还可以看到全部范围内的层次结构,模型,类型和值。点击你关注的一个范围,它会高亮显示相应的 DOM 结点。

AngularJS Batarang

你可以使用 AngularJS Batarang 来调试你的 AngularJ S应用,它是一个专为 Chrome 提供的插件。它帮助你改善应用性能。还可能衡量调节性能的进度。

wancheng
wancheng
翻译于 4天前

0人顶

 翻译的不错哦!

Restangular

AngularJS 独有的一个服务是 Restangular,它可以帮助您轻松应对各种要求,例如获取、发送、删除以及把数据存入数据库。它对于所有从 RESTful API 中大规模存取数据的 AngularJS 应用都很有必要。

Generator Angular - 一个有用的工具

Yeomen Generator

你可以很容易地开始一个具有合理的默认值和最好的用例的项目。建立这样的 Angular 应用,这款 Yeomen generator 工具是非常有用的。它只需几条终端的命令,便加速了 AngularJS 应用的开发过程。这个工具是非常有用的。这些专用的生成工具将有助于应用了解项目的有关信息,并有助于开发和测试应用程序。

Iam魔方
Iam魔方
翻译于 3天前

0人顶

 翻译的不错哦!

Angular Deckgrid

Angular Deckgrid 可以为你提供响应度和颜值俱高的应用,可以适配不同的移动终端。轻量级类砖石结构易于创建灵活的表格,高效创建图片展示。 

Radian

Radian 是一个优秀的框架,使用它只需要少量的设置就可以开启 AngularJS 项目。在多人开发项目中它是一个理想的选择。

Lumx

Lumx 以快速简单的方式帮助你创建简单而优雅的应用。 这个可响应式前端框架是基于AngularJS 和Google 材料设计规范。这个工具可嵌入最新的技术,如 Sass 预处理器,AngularJS 和 JQuery,能极大地提高 web 应用的性能。

wancheng
wancheng
翻译于 1天前

0人顶

 翻译的不错哦!

Angular Gettext

你可以用英语编码,在编码需要被翻译的地方加上注解。Angular Gettext 工具就会自动翻译那些独立的部分。这是 AngularJS 非常简单而强大的翻译支持工具。

NgDocs

AngularJS 框架内置 ngDocs 工具可以简化你项目文档和参考手册的相关工作。这款基于 Android 的工具也能提供给所有新手一些容易跟进的教程。

叶秀兰
叶秀兰
翻译于 18小时前

0人顶

 翻译的不错哦!

NgTables

无论是简单还是复杂的 Web 应用,在 AngularJS 框架中很容易创建一个表格,然后通过实用的 ngTables 工具进行高效的管理。ngTable 是 AngularJS 表格指令,支持排序,过滤和分页,在编译步骤中自动生成带有标题和过滤器的标题行。

ngTable 支持定制过滤选项,表格分组,表格外部数据控制 等等功能。

总的来说,这些都是创建任意 AngularJS Web 应用的,最有用的工具集合。用好这些工具可以帮助你轻松高效的创建 AngularJS 项目。

posted @ 2015-08-12 13:32 paulwong 阅读(610) | 评论 (0)编辑 收藏

排版六原则

作者: 阮一峰

日期: 2010年10月16日

上个月,我贴了《图形化简历》

几天后,就收到了秋叶老师的来信,希望与我探讨一些设计问题。他写过一本畅销书《说服力-让你的PPT会说话》,眼下正在写续集。

我看了新书的样章,觉得很不错,有些内容很值得分享。

====================================

首先,我们先看一个例子。良好的设计如何使得一个平庸的文档脱胎换骨。下面是一张大学生的求职简历,再普通不过了,想要引起招聘经理的注意,恐怕很难。

秋叶老师对它进行了简单的排版,还是一张表格,还是黑白配色,没有使用任何图形元素,效果却完全不一样了。

真是令人眼前一亮,不由自主地想多看几眼。这就是优秀设计的作用:它让你脱颖而出。

====================================

秋叶老师把他的排版心得,总结为六个原则:对齐,聚拢,重复,对比,强调,留白。我是这样理解的:

一、对齐原则

  相关内容必须对齐,次级标题必须缩进,方便读者视线快速移动,一眼看到最重要的信息。

二、聚拢原则

  将内容分成几个区域,相关内容都聚在一个区域中。段间距应该大于段内的行距。

三、留白原则

  千万不要把页面排得密密麻麻,要留出一定的空白,这本身就是对页面的分隔。这样既减少了页面的压迫感,又可以引导读者视线,突出重点内容。

四、降噪原则

  颜色过多、字数过多、图形过繁,都是分散读者注意力的"噪音"。

五、重复原则

  多页面排版时,注意各个页面设计上的一致性和连贯性。另外,在内容上,重要信息值得重复出现。

六、对比原则

  加大不同元素的视觉差异。这样既增加了页面的活泼,又方便读者集中注意力阅读某一个子区域。

====================================

下面用一个PPT的例子,演示排版六原则。

上面这张ppt有两个毛病。一是字数太多,抓不住重点;二是右边没有对齐,使得读者的视线只能一行行地从行首到行尾移动,不能直上直下。

现在进行修改。

第一步,根据"聚拢原则",将六点分成六个区域。

第二步,根据"降噪原则",将每一点分成"小标题"和"说明文字"两部分。

第三步,根据"对齐原则",将每一个部分、每一种元素对齐。

第四步,根据"对比原则",加大"小标题"和"说明文字"在字体和颜色上的差异。

第五步,根据"留白原则",留出一定的空白。

页面的可读性大大增加。

(完)

posted @ 2015-08-07 15:49 paulwong 阅读(718) | 评论 (0)编辑 收藏

关于颜色理论

作者: 阮一峰

日期: 2008年7月21日

制作网页的过程中,我一直不知道应该如何配色。

我的意思是,我不知道应该选择哪些颜色放在一起,完全凭感觉。于是昨天,我在网上找了一些资料,希望找到理论指导。

结果很失望。颜色理论研究的都是颜色的本质,至于颜色搭配,最终靠的还是个人感觉。说到底,Choosing colors is art, not science。不过,我还是记录一下吧,其中一些东西还是很有趣的。

=================

1. Color Wheel

所谓Color Wheel,就是将一系列颜色,有次序地通过一个圆盘的形式,展现出来。

它的产生方式是,首先列出三原色(PRIMARY COLORS):红、黄、蓝。

Photobucket

然后,二二混合,产生二级颜色(SECONDARY COLORS):绿、橙、紫。

接着,继续二二混合,又产生6种三级颜色(TERTIARY COLORS):黄橙、红橙、红紫、蓝紫、黄绿、蓝紫。

通过不断混合相邻颜色,产生新的颜色,最终形成一个全域的Color Wheel。

2. 类似色和互补色

12色的Color Wheel上任意三个相邻的颜色,被称为类似色(analogous colors)。通常认为,它们放在一起会很和谐。

Color Wheel对角线上的两种颜色,被称为互补色(complementary colors)。通常认为,它们放在一起,会形成对比效果。

此外,如果要寻找三种互相平衡的颜色,可以选择12色的Color Wheel上任意三个三角对立的颜色(Triad)。

如果要寻找三种颜色,其中二种互相类似,另一种与它们形成对比,则可以选取互补色两侧相邻的颜色。(split-complementary colors

3. 颜色模型

常用的颜色模型有三种,分别是RGB、CMYK、HSV模型。

4. RGB模型

RGB是Red、Green和Blue的缩写,任意颜色都可以由红、绿、蓝这三种颜色不同比例混合后产生。这个模型主要用于电子显示屏的颜色显示。

RGB模型通常用三个十六进制数来表示颜色,FFFFFF代表100%的红色、100%的绿色和100%蓝色混合,产生白色;000000代表0%的红色、0%的绿色和0%蓝色混合,产生黑色。

5. HSV模型

H指的是Hue(色调),它是"颜色"的同义词。

S指的是Saturation(饱和度),它指的是颜色的纯度,即颜色中含有灰色(gray)的程度。饱和度越高,颜色越纯;饱和度越低,颜色中灰色成分越大。任何颜色,饱和度变成最小值时,都会变成灰色。

V指的是Value,即颜色中白色的成分。这个值越大,颜色就越白越亮,这个值最小,颜色就越黑越暗。最大值时,所有颜色都变成白色,最小值时,所有颜色都变成黑色。

HSV模型是通过调节这三个值来标识颜色。它通常是一个color wheel的形式,所有边缘的颜色都是饱和度最高的颜色,越向圆心饱和度越小。Hue通过角度值选取,另有一个亮度轴,来选取Value值。

6.CMYK模型

这个模型主要用于印刷业,就是指用cyan, magenta, yellow, black这四种颜料混合,产生其他各种颜色。

印刷品上的图案,仔细看其实都是由一个个小点构成,而每个小点又都是采用四色套印,重合叠加后产生各种颜色的效果。

(完)

posted @ 2015-08-07 15:47 paulwong 阅读(633) | 评论 (0)编辑 收藏

RESTful API 设计指南

作者: 阮一峰

日期: 2014年5月22日

网络应用程序,分为前端和后端两个部分。当前的发展趋势,就是前端设备层出不穷(手机、平板、桌面电脑、其他专用设备......)。

因此,必须有一种统一的机制,方便不同的前端设备与后端进行通信。这导致API构架的流行,甚至出现"API First"的设计思想。RESTful API是目前比较成熟的一套互联网应用程序的API设计理论。我以前写过一篇《理解RESTful架构》,探讨如何理解这个概念。

今天,我将介绍RESTful API的设计细节,探讨如何设计一套合理、好用的API。我的主要参考了两篇文章(12)。

RESTful API

一、协议

API与用户的通信协议,总是使用HTTPs协议

二、域名

应该尽量将API部署在专用域名之下。

 https://api.example.com 

如果确定API很简单,不会有进一步扩展,可以考虑放在主域名下。

 https://example.org/api/ 

三、版本(Versioning)

应该将API的版本号放入URL。

 https://api.example.com/v1/ 

另一种做法是,将版本号放在HTTP头信息中,但不如放入URL方便和直观。Github采用这种做法。

四、路径(Endpoint)

路径又称"终点"(endpoint),表示API的具体网址。

在RESTful架构中,每个网址代表一种资源(resource),所以网址中不能有动词,只能有名词,而且所用的名词往往与数据库的表格名对应。一般来说,数据库中的表都是同种记录的"集合"(collection),所以API中的名词也应该使用复数。

举例来说,有一个API提供动物园(zoo)的信息,还包括各种动物和雇员的信息,则它的路径应该设计成下面这样。

  • https://api.example.com/v1/zoos
  • https://api.example.com/v1/animals
  • https://api.example.com/v1/employees

五、HTTP动词

对于资源的具体操作类型,由HTTP动词表示。

常用的HTTP动词有下面五个(括号里是对应的SQL命令)。

  • GET(SELECT):从服务器取出资源(一项或多项)。
  • POST(CREATE):在服务器新建一个资源。
  • PUT(UPDATE):在服务器更新资源(客户端提供改变后的完整资源)。
  • PATCH(UPDATE):在服务器更新资源(客户端提供改变的属性)。
  • DELETE(DELETE):从服务器删除资源。

还有两个不常用的HTTP动词。

  • HEAD:获取资源的元数据。
  • OPTIONS:获取信息,关于资源的哪些属性是客户端可以改变的。

下面是一些例子。

  • GET /zoos:列出所有动物园
  • POST /zoos:新建一个动物园
  • GET /zoos/ID:获取某个指定动物园的信息
  • PUT /zoos/ID:更新某个指定动物园的信息(提供该动物园的全部信息)
  • PATCH /zoos/ID:更新某个指定动物园的信息(提供该动物园的部分信息)
  • DELETE /zoos/ID:删除某个动物园
  • GET /zoos/ID/animals:列出某个指定动物园的所有动物
  • DELETE /zoos/ID/animals/ID:删除某个指定动物园的指定动物

六、过滤信息(Filtering)

如果记录数量很多,服务器不可能都将它们返回给用户。API应该提供参数,过滤返回结果。

下面是一些常见的参数。

  • ?limit=10:指定返回记录的数量
  • ?offset=10:指定返回记录的开始位置。
  • ?page=2&per_page=100:指定第几页,以及每页的记录数。
  • ?sortby=name&order=asc:指定返回结果按照哪个属性排序,以及排序顺序。
  • ?animal_type_id=1:指定筛选条件

参数的设计允许存在冗余,即允许API路径和URL参数偶尔有重复。比如,GET /zoo/ID/animals 与 GET /animals?zoo_id=ID 的含义是相同的。

七、状态码(Status Codes)

服务器向用户返回的状态码和提示信息,常见的有以下一些(方括号中是该状态码对应的HTTP动词)。

  • 200 OK - [GET]:服务器成功返回用户请求的数据,该操作是幂等的(Idempotent)。
  • 201 CREATED - [POST/PUT/PATCH]:用户新建或修改数据成功。
  • 202 Accepted - [*]:表示一个请求已经进入后台排队(异步任务)
  • 204 NO CONTENT - [DELETE]:用户删除数据成功。
  • 400 INVALID REQUEST - [POST/PUT/PATCH]:用户发出的请求有错误,服务器没有进行新建或修改数据的操作,该操作是幂等的。
  • 401 Unauthorized - [*]:表示用户没有权限(令牌、用户名、密码错误)。
  • 403 Forbidden - [*] 表示用户得到授权(与401错误相对),但是访问是被禁止的。
  • 404 NOT FOUND - [*]:用户发出的请求针对的是不存在的记录,服务器没有进行操作,该操作是幂等的。
  • 406 Not Acceptable - [GET]:用户请求的格式不可得(比如用户请求JSON格式,但是只有XML格式)。
  • 410 Gone -[GET]:用户请求的资源被永久删除,且不会再得到的。
  • 422 Unprocesable entity - [POST/PUT/PATCH] 当创建一个对象时,发生一个验证错误。
  • 500 INTERNAL SERVER ERROR - [*]:服务器发生错误,用户将无法判断发出的请求是否成功。

状态码的完全列表参见这里

八、错误处理(Error handling)

如果状态码是4xx,就应该向用户返回出错信息。一般来说,返回的信息中将error作为键名,出错信息作为键值即可。

 {     error: "Invalid API key" } 

九、返回结果

针对不同操作,服务器向用户返回的结果应该符合以下规范。

  • GET /collection:返回资源对象的列表(数组)
  • GET /collection/resource:返回单个资源对象
  • POST /collection:返回新生成的资源对象
  • PUT /collection/resource:返回完整的资源对象
  • PATCH /collection/resource:返回完整的资源对象
  • DELETE /collection/resource:返回一个空文档

十、Hypermedia API

RESTful API最好做到Hypermedia,即返回结果中提供链接,连向其他API方法,使得用户不查文档,也知道下一步应该做什么。

比如,当用户向api.example.com的根目录发出请求,会得到这样一个文档。

 {"link": {   "rel":   "collection https://www.example.com/zoos",   "href":  "https://api.example.com/zoos",   "title": "List of zoos",   "type":  "application/vnd.yourformat+json" }} 

上面代码表示,文档中有一个link属性,用户读取这个属性就知道下一步该调用什么API了。rel表示这个API与当前网址的关系(collection关系,并给出该collection的网址),href表示API的路径,title表示API的标题,type表示返回类型。

Hypermedia API的设计被称为HATEOAS。Github的API就是这种设计,访问api.github.com会得到一个所有可用API的网址列表。

 {   "current_user_url": "https://api.github.com/user",   "authorizations_url": "https://api.github.com/authorizations",   // ... } 

从上面可以看到,如果想获取当前用户的信息,应该去访问api.github.com/user,然后就得到了下面结果。

 {   "message": "Requires authentication",   "documentation_url": "https://developer.github.com/v3" } 

上面代码表示,服务器给出了提示信息,以及文档的网址。

十一、其他

(1)API的身份认证应该使用OAuth 2.0框架。

(2)服务器返回的数据格式,应该尽量使用JSON,避免使用XML。

(完)

posted @ 2015-08-07 14:13 paulwong 阅读(764) | 评论 (0)编辑 收藏

理解OAuth 2.0

https://www.ruanyifeng.com/blog/2019/04/oauth-grant-types.html

https://aaronparecki.com/oauth-2-simplified

posted @ 2015-08-07 14:12 paulwong 阅读(515) | 评论 (0)编辑 收藏

仅列出标题
共115页: First 上一页 34 35 36 37 38 39 40 41 42 下一页 Last