2014年3月7日星期五

[]wordpress添加rss订阅地址设置方法

本文自动转发自我的博客: http://www.haofengjing.org/2367.html

wordpress添加rss订阅的功能很方便,只需在后台添加rss地址,即可实时订阅到该feed的内容。

wordpress rss设置方法:

进入后台-外观-小工具,拖动"RSS"栏目至后侧的工具栏,在选项卡里面配置好相关的参数。

保存在,前台则显示该rss的标题以及发布日期。


[]Apache Thrift - 可伸缩的跨语言服务开发框架

本文自动转发自我的博客: http://www.haofengjing.org/2362.html

前言:

目前流行的服务调用方式有很多种,例如基于 SOAP 消息格式的 Web Service,基于 JSON 消息格式的 RESTful 服务等。其中所用到的数据传输方式包括 XML,JSON 等,然而 XML 相对体积太大,传输效率低,JSON 体积较小,新颖,但还不够完善。本文将介绍由 Facebook 开发的远程服务调用框架 Apache Thrift,它采用接口描述语言定义并创建服务,支持可扩展的跨语言服务开发,所包含的代码生成引擎可以在多种语言中,如 C++, Java, Python, PHP, Ruby, Erlang, Perl, Haskell, C#, Cocoa, Smalltalk 等创建高效的、无缝的服务,其传输数据采用二进制格式,相对 XML 和 JSON 体积更小,对于高并发、大数据量和多语言的环境更有优势。本文将详细介绍 Thrift 的使用,并且提供丰富的实例代码加以解释说明,帮助使用者快速构建服务。

一个简单的 Thrift 实例

本文首先介绍一个简单的 Thrift 实现实例,使读者能够快速直观地了解什么是 Thrift 以及如何使用 Thrift 构建服务。

创建一个简单的服务 Hello。首先根据 Thrift 的语法规范编写脚本文件 Hello.thrift,代码如下:


清单 1. Hello.thrift
				    namespace java service.demo    service Hello{     string helloString(1:string para)     i32 helloInt(1:i32 para)     bool helloBoolean(1:bool para)     void helloVoid()     string helloNull()    }
 

其中定义了服务 Hello 的五个方法,每个方法包含一个方法名,参数列表和返回类型。每个参数包括参数序号,参数类型以及参数名。 Thrift 是对 IDL(Interface Definition Language) 描述性语言的一种具体实现。因此,以上的服务描述文件使用 IDL 语法编写。使用 Thrift 工具编译 Hello.thrift,就会生成相应的 Hello.java 文件。该文件包含了在 Hello.thrift 文件中描述的服务 Hello 的接口定义,即 Hello.Iface 接口,以及服务调用的底层通信细节,包括客户端的调用逻辑 Hello.Client 以及服务器端的处理逻辑 Hello.Processor,用于构建客户端和服务器端的功能。

创建 HelloServiceImpl.java 文件并实现 Hello.java 文件中的 Hello.Iface 接口,代码如下:


清单 2. HelloServiceImpl.java
				    package service.demo;    import org.apache.thrift.TException;    public class HelloServiceImpl implements Hello.Iface {       @Override       public boolean helloBoolean(boolean para) throws TException {           return para;       }       @Override       public int helloInt(int para) throws TException {           try {               Thread.sleep(20000);           } catch (InterruptedException e) {               e.printStackTrace();           }           return para;       }       @Override       public String helloNull() throws TException {           return null;       }       @Override       public String helloString(String para) throws TException {           return para;       }       @Override       public void helloVoid() throws TException {           System.out.println("Hello World");       }    }
 

创建服务器端实现代码,将 HelloServiceImpl 作为具体的处理器传递给 Thrift 服务器,代码如下:


清单 3. HelloServiceServer.java
				    package service.server;    import org.apache.thrift.TProcessor;    import org.apache.thrift.protocol.TBinaryProtocol;    import org.apache.thrift.protocol.TBinaryProtocol.Factory;    import org.apache.thrift.server.TServer;    import org.apache.thrift.server.TThreadPoolServer;    import org.apache.thrift.transport.TServerSocket;    import org.apache.thrift.transport.TTransportException;    import service.demo.Hello;    import service.demo.HelloServiceImpl;      public class HelloServiceServer {       /**        * 启动 Thrift 服务器       * @param args        */       public static void main(String[] args) {           try {               // 设置服务端口为 7911               TServerSocket serverTransport = new TServerSocket(7911);               // 设置协议工厂为 TBinaryProtocol.Factory               Factory proFactory = new TBinaryProtocol.Factory();               // 关联处理器与 Hello 服务的实现              TProcessor processor = new Hello.Processor(new HelloServiceImpl());               TServer server = new TThreadPoolServer(processor, serverTransport,                       proFactory);               System.out.println("Start server on port 7911...");               server.serve();           } catch (TTransportException e) {               e.printStackTrace();           }       }    }
 

创建客户端实现代码,调用 Hello.client 访问服务端的逻辑实现,代码如下:


清单 4. HelloServiceClient.java
				    package service.client;    import org.apache.thrift.TException;    import org.apache.thrift.protocol.TBinaryProtocol;    import org.apache.thrift.protocol.TProtocol;    import org.apache.thrift.transport.TSocket;    import org.apache.thrift.transport.TTransport;    import org.apache.thrift.transport.TTransportException;    import service.demo.Hello;      public class HelloServiceClient {    /**        * 调用 Hello 服务       * @param args        */       public static void main(String[] args) {           try {               // 设置调用的服务地址为本地,端口为 7911               TTransport transport = new TSocket("localhost", 7911);               transport.open();               // 设置传输协议为 TBinaryProtocol               TProtocol protocol = new TBinaryProtocol(transport);               Hello.Client client = new Hello.Client(protocol);               // 调用服务的 helloVoid 方法              client.helloVoid();               transport.close();           } catch (TTransportException e) {               e.printStackTrace();           } catch (TException e) {               e.printStackTrace();           }       }    }
 

代码编写完后运行服务器,再启动客户端调用服务 Hello 的方法 helloVoid,在服务器端的控制台窗口输出"Hello World"(helloVoid 方法实现在控制台打印字符串,没有返回值,所以客户端调用方法后没有返回值输出,读者可以自己尝试其他有返回值方法的调用,其结果可以打印在客户端的控制台窗口 )。

Thrift 架构

Thrift 包含一个完整的堆栈结构用于构建客户端和服务器端。下图描绘了 Thrift 的整体架构。


图 1. 架构图
图 1. 架构图 

如图所示,图中黄色部分是用户实现的业务逻辑,褐色部分是根据 Thrift 定义的服务接口描述文件生成的客户端和服务器端代码框架,红色部分是根据 Thrift 文件生成代码实现数据的读写操作。红色部分以下是 Thrift 的传输体系、协议以及底层 I/O 通信,使用 Thrift 可以很方便的定义一个服务并且选择不同的传输协议和传输层而不用重新生成代码。

Thrift 服务器包含用于绑定协议和传输层的基础架构,它提供阻塞、非阻塞、单线程和多线程的模式运行在服务器上,可以配合服务器 / 容器一起运行,可以和现有的 J2EE 服务器 /Web 容器无缝的结合。

服务端和客户端具体的调用流程如下:


图 2. Server 端启动、服务时序图(查看大图
图 2. Server 端启动、服务时序图 

该图所示是 HelloServiceServer 启动的过程以及服务被客户端调用时,服务器的响应过程。从图中我们可以看到,程序调用了 TThreadPoolServer 的 serve 方法后,server 进入阻塞监听状态,其阻塞在 TServerSocket 的 accept 方法上。当接收到来自客户端的消息后,服务器发起一个新线程处理这个消息请求,原线程再次进入阻塞状态。在新线程中,服务器通过 TBinaryProtocol 协议读取消息内容,调用 HelloServiceImpl 的 helloVoid 方法,并将结果写入 helloVoid_result 中传回客户端。


图 3. Client 端调用服务时序图(查看大图
图 3. Client 端调用服务时序图 

该图所示是 HelloServiceClient 调用服务的过程以及接收到服务器端的返回值后处理结果的过程。从图中我们可以看到,程序调用了 Hello.Client 的 helloVoid 方法,在 helloVoid 方法中,通过 send_helloVoid 方法发送对服务的调用请求,通过 recv_helloVoid 方法接收服务处理请求后返回的结果。

数据类型

Thrift 脚本可定义的数据类型包括以下几种类型:

  • 基本类型:
    • bool:布尔值,true 或 false,对应 Java 的 boolean
    • byte:8 位有符号整数,对应 Java 的 byte
    • i16:16 位有符号整数,对应 Java 的 short
    • i32:32 位有符号整数,对应 Java 的 int
    • i64:64 位有符号整数,对应 Java 的 long
    • double:64 位浮点数,对应 Java 的 double
    • string:未知编码文本或二进制字符串,对应 Java 的 String
  • 结构体类型:
    • struct:定义公共的对象,类似于 C 语言中的结构体定义,在 Java 中是一个 JavaBean
  • 容器类型:
    • list:对应 Java 的 ArrayList
    • set:对应 Java 的 HashSet
    • map:对应 Java 的 HashMap
  • 异常类型:
    • exception:对应 Java 的 Exception
  • 服务类型:
    • service:对应服务的类

协议

Thrift 可以让用户选择客户端与服务端之间传输通信协议的类别,在传输协议上总体划分为文本 (text) 和二进制 (binary) 传输协议,为节约带宽,提高传输效率,一般情况下使用二进制类型的传输协议为多数,有时还会使用基于文本类型的协议,这需要根据项目 / 产品中的实际需求。常用协议有以下几种:

  • TBinaryProtocol —— 二进制编码格式进行数据传输

    使用方法如清单 3 和清单 4 所示。

  • TCompactProtocol —— 高效率的、密集的二进制编码格式进行数据传输

    构建 TCompactProtocol 协议的服务器和客户端只需替换清单 3 和清单 4 中 TBinaryProtocol 协议部分即可,替换成如下代码:

    清单 5. 使用 TCompactProtocol 协议构建的 HelloServiceServer.java
    				    TCompactProtocol.Factory proFactory = new TCompactProtocol.Factory();
    清单 6. 使用 TCompactProtocol 协议的 HelloServiceClient.java
    				    TCompactProtocol protocol = new TCompactProtocol(transport);
  • TJSONProtocol —— 使用 JSON 的数据编码协议进行数据传输

    构建 TJSONProtocol 协议的服务器和客户端只需替换清单 3 和清单 4 中 TBinaryProtocol 协议部分即可,替换成如下代码:

    清单 7. 使用 TJSONProtocol 协议构建的 HelloServiceServer.java
    				    TJSONProtocol.Factory proFactory = new TJSONProtocol.Factory();
    清单 8. 使用 TJSONProtocol 协议的 HelloServiceClient.java
    				    TJSONProtocol protocol = new TJSONProtocol(transport);
  • TSimpleJSONProtocol —— 只提供 JSON 只写的协议,适用于通过脚本语言解析

传输层

常用的传输层有以下几种:

  • TSocket —— 使用阻塞式 I/O 进行传输,是最常见的模式

    使用方法如清单 4 所示。

  • TFramedTransport —— 使用非阻塞方式,按块的大小进行传输,类似于 Java 中的 NIO

    若使用 TFramedTransport 传输层,其服务器必须修改为非阻塞的服务类型,客户端只需替换清单 4 中 TTransport 部分,代码如下,清单 9 中 TNonblockingServerTransport 类是构建非阻塞 socket 的抽象类,TNonblockingServerSocket 类继承 TNonblockingServerTransport

    清单 9. 使用 TFramedTransport 传输层构建的 HelloServiceServer.java
    				    TNonblockingServerTransport serverTransport;    serverTransport = new TNonblockingServerSocket(10005);    Hello.Processor processor = new Hello.Processor(new HelloServiceImpl());    TServer server = new TNonblockingServer(processor, serverTransport);    System.out.println("Start server on port 10005 ...");    server.serve();
    清单 10. 使用 TFramedTransport 传输层的 HelloServiceClient.java
    				    TTransport transport = new TFramedTransport(new TSocket("localhost", 10005));
  • TNonblockingTransport —— 使用非阻塞方式,用于构建异步客户端

    使用方法请参考 Thrift 异步客户端构建

服务端类型

常见的服务端类型有以下几种:

  • TSimpleServer —— 单线程服务器端使用标准的阻塞式 I/O

    代码如下:

    清单 11. 使用 TSimpleServer 服务端构建的 HelloServiceServer.java
    				    TServerSocket serverTransport = new TServerSocket(7911);    TProcessor processor = new Hello.Processor(new HelloServiceImpl());    TServer server = new TSimpleServer(processor, serverTransport);    System.out.println("Start server on port 7911...");    server.serve();

    客户端的构建方式可参考清单 4。

  • TThreadPoolServer —— 多线程服务器端使用标准的阻塞式 I/O

    使用方法如清单 3 所示。

  • TNonblockingServer —— 多线程服务器端使用非阻塞式 I/O

    使用方法请参考 Thrift 异步客户端构建

Thrift 异步客户端构建

Thrift 提供非阻塞的调用方式,可构建异步客户端。在这种方式中,Thrift 提供了新的类 TAsyncClientManager 用于管理客户端的请求,在一个线程上追踪请求和响应,同时通过接口 AsyncClient 传递标准的参数和 callback 对象,服务调用完成后,callback 提供了处理调用结果和异常的方法。

首先我们看 callback 的实现:


清单 12.CallBack 的实现:MethodCallback.java
				    package service.callback;    import org.apache.thrift.async.AsyncMethodCallback;      public class MethodCallback implements AsyncMethodCallback {       Object response = null;         public Object getResult() {           // 返回结果值          return this.response;       }         // 处理服务返回的结果值      @Override       public void onComplete(Object response) {           this.response = response;       }       // 处理调用服务过程中出现的异常      @Override       public void onError(Throwable throwable) {         }    }
 

如代码所示,onComplete 方法接收服务处理后的结果,此处我们将结果 response 直接赋值给 callback 的私有属性 response。onError 方法接收服务处理过程中抛出的异常,此处未对异常进行处理。

创建非阻塞服务器端实现代码,将 HelloServiceImpl 作为具体的处理器传递给异步 Thrift 服务器,代码如下:


清单 13.HelloServiceAsyncServer.java
				    package service.server;    import org.apache.thrift.server.TNonblockingServer;    import org.apache.thrift.server.TServer;    import org.apache.thrift.transport.TNonblockingServerSocket;    import org.apache.thrift.transport.TNonblockingServerTransport;    import org.apache.thrift.transport.TTransportException;    import service.demo.Hello;    import service.demo.HelloServiceImpl;      public class HelloServiceAsyncServer {       /**        * 启动 Thrift 异步服务器       * @param args        */       public static void main(String[] args) {           TNonblockingServerTransport serverTransport;           try {               serverTransport = new TNonblockingServerSocket(10005);               Hello.Processor processor = new Hello.Processor(                       new HelloServiceImpl());               TServer server = new TNonblockingServer(processor, serverTransport);               System.out.println("Start server on port 10005 ...");               server.serve();           } catch (TTransportException e) {               e.printStackTrace();           }       }    }
 

HelloServiceAsyncServer 通过 java.nio.channels.ServerSocketChannel 创建非阻塞的服务器端等待客户端的连接。

创建异步客户端实现代码,调用 Hello.AsyncClient 访问服务端的逻辑实现,将 MethodCallback 对象作为参数传入调用方法中,代码如下:


清单 14.HelloServiceAsyncClient.java
				    package service.client;    import java.io.IOException;    import org.apache.thrift.async.AsyncMethodCallback;    import org.apache.thrift.async.TAsyncClientManager;    import org.apache.thrift.protocol.TBinaryProtocol;    import org.apache.thrift.protocol.TProtocolFactory;    import org.apache.thrift.transport.TNonblockingSocket;    import org.apache.thrift.transport.TNonblockingTransport;    import service.callback.MethodCallback;    import service.demo.Hello;      public class HelloServiceAsyncClient {       /**        * 调用 Hello 服务       * @param args        */       public static void main(String[] args) throws Exception {           try {               TAsyncClientManager clientManager = new TAsyncClientManager();               TNonblockingTransport transport = new TNonblockingSocket(                       "localhost", 10005);               TProtocolFactory protocol = new TBinaryProtocol.Factory();               Hello.AsyncClient asyncClient = new Hello.AsyncClient(protocol,                       clientManager, transport);               System.out.println("Client calls .....");               MethodCallback callBack = new MethodCallback();               asyncClient.helloString("Hello World", callBack);               Object res = callBack.getResult();               while (res == null) {                   res = callBack.getResult();               }               System.out.println(((Hello.AsyncClient.helloString_call) res)                       .getResult());           } catch (IOException e) {               e.printStackTrace();           }     }    }
 

HelloServiceAsyncClient 通过 java.nio.channels.Socketchannel 创建异步客户端与服务器建立连接。在本文中异步客户端通过以下的循环代码实现了同步效果,读者可去除这部分代码后再运行对比。


清单 15. 异步客户端实现同步效果代码段
Object res = callBack.getResult();  // 等待服务调用后的返回结果  while (res == null) {     res = callBack.getResult();  }
 

通过与清单 9 和清单 10 的代码比较,我们可以构建一个 TNonblockingServer 服务类型的服务端,在客户端构建一个 TFramedTransport 传输层的同步客户端和一个 TNonblockingTransport 传输层的异步客户端,那么一个服务就可以通过一个 socket 端口提供两种不同的调用方式。有兴趣的读者可以尝试一下。

常见问题

NULL 问题

我们在对服务的某个方法调用时,有时会出现该方法返回 null 值的情况,在 Thrift 中,直接调用一个返回 null 值的方法会抛出 TApplicationException 异常。在清单 2 中,HelloServiceImpl 里实现了 helloNull 方法,返回 null 值,我们在 HelloServiceClient.java 中加入调用该方法的代码,出现如下图所示的异常:


图 4. TApplicationException 异常
图 4. TApplicationException 异常 

为了处理返回 null 值情况,我们要捕获该异常,并进行相应的处理,具体客户端代码实现如下:


清单 16. 处理服务返回值为 null 的代码
				    package service.client;    import org.apache.thrift.TApplicationException;    import org.apache.thrift.TException;    import org.apache.thrift.protocol.TBinaryProtocol;    import org.apache.thrift.protocol.TProtocol;    import org.apache.thrift.transport.TSocket;    import org.apache.thrift.transport.TTransport;    import org.apache.thrift.transport.TTransportException;    import service.demo.Hello;      public class HelloServiceClient {       /**        * 调用 Hello 服务,并处理 null 值问题       * @param args        */       public static void main(String[] args) {           try {               TTransport transport = new TSocket("localhost", 7911);               transport.open();               TProtocol protocol = new TBinaryProtocol(transport);               Hello.Client client = new Hello.Client(protocol);               System.out.println(client.helloNull());               transport.close();           } catch (TTransportException e) {               e.printStackTrace();           } catch (TException e) {               if (e instanceof TApplicationException                       && ((TApplicationException) e).getType() ==                                      TApplicationException.MISSING_RESULT) {                   System.out.println("The result of helloNull function is NULL");               }           }       }    }
 

调用 helloNull 方法后,会抛出 TApplicationException 异常,并且异常种类为 MISSING_RESULT,本段代码显示,捕获该异常后,直接在控制台打印"The result of helloNull function is NULL"信息。

安装部署

Apache Thrift 的官方网站为:http://thrift.apache.org/tutorial/ ,具体安装步骤如下:

  1. 下载 thrift 源文件(http://svn.apache.org/repos/asf/thrift/tags/thrift-0.6.1/ )
  2. 将 thrift 源文件导入 eclipse,进入 /lib/java 目录,使用 ant 编译 build.xml 获得 libthrift-0.6.1-snapshot.jar
  3. 将 libthrift-0.6.1-snapshot.jar、slf4j-api-1.5.8.jar、slf4j-log4j12-1.5.8.jar 和 log4j-1.2.14.jar 导入 eclipse 开发环境
  4. 下载 thrift 编译工具,该工具可将 thrift 脚本文件编译成 java 文件,下载地址:http://apache.etoak.com//thrift/0.6.0/thrift-0.6.1.exe
  5. 创建 Hello.thrift 脚本文件,具体代码如上一章节所述,进入 thrift-0.6.1.exe 所在目录,执行命令"thrift-0.6.1.exe -gen java x:\Hello.thrift",在当前运行盘符下,可看见 gen-java 目录,进入目录可看到生成的 Java 代码。更多 thrift 的命令内容,请参考 thrift 自带的 help 命令
  6. 编写服务端和客户端代码,完成 thrift 的安装和部署

基于 Apache Thrift 框架生成的服务包括客户端和服务器端,具体的部署模式如下所示:


图 5. 部署图
图 5. 部署图 

从图中我们可以看到,客户端和服务器端部署时,需要用到公共的 jar 包和 java 文件,如图"Common file"区域,其中 Hello.java 由 Hello.thrift 编译而来。在服务器端,服务必须实现 Hello.Iface 接口,同时要包括服务器的启动代码 HelloServiceServer.java。在客户端,包括客户端调用服务的代码 HelloServiceClient.java。客户端和服务器通过 Hello.java 提供的 API 实现远程服务调用。

总结

本文介绍了 Apache Thrift 的安装部署和架构,并通过大量实例介绍了在不同情况下如何使用 Apache Thrift 来构建服务,同时着重介绍了 Thrift 异步客户端的构建,希望能给读者带来一些帮助。

 

参考资料

学习


[] 经典软件设计模型 - 事件驱动模型

本文自动转发自我的博客: http://www.haofengjing.org/2360.html

模型说明

在UI编程中,常常要对鼠标点击进行相应,首先如何获得鼠标点击呢? 方式一:创建一个线程,该线程一直循环检测是否有鼠标点击,那么这个方式有以下几个缺点: 1. CPU资源浪费,可能鼠标点击的频率非常小,但是扫描线程还是会一直循环检测,这会造成很多的CPU资源浪费;如果扫描鼠标点击的接口是阻塞的呢? 2. 如果是堵塞的,又会出现下面这样的问题,如果我们不但要扫描鼠标点击,还要扫描键盘是否按下,由于扫描鼠标时被堵塞了,那么可能永远不会去扫描键盘; 3. 如果一个循环需要扫描的设备非常多,这又会引来响应时间的问题; 所以,该方式是非常不好的。 方式二:就是事件驱动模型 目前大部分的UI编程都是事件驱动模型,如很多UI平台都会提供onClick()事件,这个事件就代表鼠标按下事件。事件驱动模型大体思路如下: 1. 有一个事件(消息)队列; 2. 鼠标按下时,往这个队列中增加一个点击事件(消息); 3. 有个循环,不断从队列取出事件,根据不同的事件,调用不同的函数,如onClick()、onKeyDown()等; 4. 事件(消息)一般都各自保存各自的处理函数指针,这样,每个消息都有独立的处理函数; 如图:      

Android的Looper和Handler模型

在Android系统中,一般的事件驱动应用都可以使用Looper和Handler来实现,uml如下

UML

说明

Message是一个个的消息,每个消息代表一个事件,每个Message都有独立的处理者target; 多个消息利用Message里的next属性首尾相接组成一个MessageQueue队列; MessageQueue提供了插入Message接口enqueueMessage和读取Message接口next; Looper是一个循环类,它利用MessageQueue的next接口,不断从MessageQueue获得消息; 如果Looper获得了一个消息Message,那么就调用Message的target来处理该消息,这个target其实就是一个Handler; Handler的HandlerMessage函数就是处理Message的地方,这是一个接口函数,所以使用者根据业务需求重写这个接口; Handler除了处理Message外,还提供了插入Message的接口sendMessage,从这也可以看出,利用这四个类设计最简单的事件驱动模型只需操作Hanlder一个类即可,因为它既提供了插入消息的接口,也提供了处理消息的接口; 当然这四个类除了MessageQueue之外,其他三个用户都可以根据业务需求操作实现更复杂的模型,我们可以自己创建一个Message而不是使用Message的默认创建方法(如可以为每个Message设计不同的处理函数,不一定使用HandlerMessage),然后通过Handler插入到消息队列中;然后创建自己的Looper线程,自己独自使用一个Looper;之所以MessageQueue不能自行设计,是因为Android的设计问题,因为MessageQueue是在Looper的构造函数自己new的。

重点代码分析

  Handler的handleMessage优先级  
  1. /**
  2.  * Subclasses must implement this to receive messages.
  3.  */
  4. public void handleMessage(Message msg) {
  5. }
  6. /**
  7.  * Handle system messages here.
  8.  */
  9. public void dispatchMessage(Message msg) {
  10.     if (msg.callback != null) {
  11.         handleCallback(msg);
  12.     } else {
  13.         if (mCallback != null) {
  14.             if (mCallback.handleMessage(msg)) {
  15.                 return;
  16.             }
  17.         }
  18.         handleMessage(msg);
  19.     }
  20. }
looper读取一个消息后,首先执行的是dispatchMessage,然后dispatchMessage根据不同条件调用不同的handleMessage; 这里边有好多callback,是因为不一定非要使用Handler提供的handleMessage接口处理消息,也可以有很多其他方式设置回调函数; 优先级最高的是msg.callback,如果要处理的Message本身有处理回调接口,那么就使用Message本身的; 优先级次之的是mCallback,mCallback是在Handler的构造函数设置的,说明可以为Handler统一设置一个处理回调接口; 优先级最低的是handleMessage,这个才是handleMessage用户重写的接口; 设置这些优先级,主要为了在应用中需要处理的消息有很多种分类,但是它们共用一个Looper,如 1. 如果所有的消息属于一个分类,那么只需要一个Handler即一个handleMessage即可; 2. 如果消息分属于不同的分类,那么每个分类使用一个Handler管理,即每个分类使用一个handleMessage,即mCallback; 3. 如果所有的消息种类都不一样,如果使用一个Handler在逻辑上不伦不类,所以,这个时候不如为每个消息设置一个回调函数更好,即msg.callback;     MessageQueue的时间优先级when Handler是通过enqueueMessage接口将消息插入到MessageQueue的,在插入的时候就已经将消息按照时间排序了,其中时间0代表最高的优先级,时间越大,优先级越小,越延后处理 所以,Looper通过MessageQueue的next接口获得的消息就是优先级最高的消息,next不需要重新排序消息队列了;    

HandlerThread的作用

如果我们需要自己创建Looper,需要将创建的Looper和创建的Handler联系在一起。如果两个对象的创建是在一个线程里,并且先创建Looper后创建Handler是没有问题的;但是如果两个对象是在两个线程里创建,有可能在构造Handler时,Looper并没有真正的创建好,其实这之间只要通过一个同步就可以。HandlerThread就是Android提供给我们用的一个类,它已经做好了同步,所以可以安全使用。

[]使用事件驱动模型实现高效稳定的网络服务器程序

本文自动转发自我的博客: http://www.haofengjing.org/2354.html

前言

事件驱动为广大的程序员所熟悉,其最为人津津乐道的是在图形化界面编程中的应用;事实上,在网络编程中事件驱动也被广泛使用,并大规模部署在高连接数高吞吐量的服务器程序中,如 http 服务器程序、ftp 服务器程序等。相比于传统的网络编程方式,事件驱动能够极大的降低资源占用,增大服务接待能力,并提高网络传输效率。

关于本文提及的服务器模型,搜索网络可以查阅到很多的实现代码,所以,本文将不拘泥于源代码的陈列与分析,而侧重模型的介绍和比较。使用 libev 事件驱动库的服务器模型将给出实现代码。

本文涉及到线程 / 时间图例,只为表明线程在各个 IO 上确实存在阻塞时延,但并不保证时延比例的正确性和 IO 执行先后的正确性;另外,本文所提及到的接口也只是笔者熟悉的 Unix/Linux 接口,并未推荐 Windows 接口,读者可以自行查阅对应的 Windows 接口。

阻塞型的网络编程接口

几乎所有的程序员第一次接触到的网络编程都是从 listen()、send()、recv() 等接口开始的。使用这些接口可以很方便的构建服务器 / 客户机的模型。

我们假设希望建立一个简单的服务器程序,实现向单个客户机提供类似于"一问一答"的内容服务。

图 1. 简单的一问一答的服务器 / 客户机模型
图 1. 简单的一问一答的服务器 / 客户机模型

我们注意到,大部分的 socket 接口都是阻塞型的。所谓阻塞型接口是指系统调用(一般是 IO 接口)不返回调用结果并让当前线程一直阻塞,只有当该系统调用获得结果或者超时出错时才返回。

实际上,除非特别指定,几乎所有的 IO 接口 ( 包括 socket 接口 ) 都是阻塞型的。这给网络编程带来了一个很大的问题,如在调用 send() 的同时,线程将被阻塞,在此期间,线程将无法执行任何运算或响应任何的网络请求。这给多客户机、多业务逻辑的网络编程带来了挑战。这时,很多程序员可能会选择多线程的方式来解决这个问题。

多线程的服务器程序

应对多客户机的网络应用,最简单的解决方式是在服务器端使用多线程(或多进程)。多线程(或多进程)的目的是让每个连接都拥有独立的线程(或进程),这样任何一个连接的阻塞都不会影响其他的连接。

具体使用多进程还是多线程,并没有一个特定的模式。传统意义上,进程的开销要远远大于线程,所以,如果需要同时为较多的客户机提供服务,则不推荐使用多进程;如果单个服务执行体需要消耗较多的 CPU 资源,譬如需要进行大规模或长时间的数据运算或文件访问,则进程较为安全。通常,使用 pthread_create () 创建新线程,fork() 创建新进程。

我们假设对上述的服务器 / 客户机模型,提出更高的要求,即让服务器同时为多个客户机提供一问一答的服务。于是有了如下的模型。

图 2. 多线程的服务器模型
图 2. 多线程的服务器模型

在上述的线程 / 时间图例中,主线程持续等待客户端的连接请求,如果有连接,则创建新线程,并在新线程中提供为前例同样的问答服务。

很多初学者可能不明白为何一个 socket 可以 accept 多次。实际上,socket 的设计者可能特意为多客户机的情况留下了伏笔,让 accept() 能够返回一个新的 socket。下面是 accept 接口的原型:

	 int accept(int s, struct sockaddr *addr, socklen_t *addrlen);

输入参数 s 是从 socket(),bind() 和 listen() 中沿用下来的 socket 句柄值。执行完 bind() 和 listen() 后,操作系统已经开始在指定的端口处监听所有的连接请求,如果有请求,则将该连接请求加入请求队列。调用 accept() 接口正是从 socket s 的请求队列抽取第一个连接信息,创建一个与 s 同类的新的 socket 返回句柄。新的 socket 句柄即是后续 read() 和 recv() 的输入参数。如果请求队列当前没有请求,则 accept() 将进入阻塞状态直到有请求进入队列。

上述多线程的服务器模型似乎完美的解决了为多个客户机提供问答服务的要求,但其实并不尽然。如果要同时响应成百上千路的连接请求,则无论多线程还是多进程都会严重占据系统资源,降低系统对外界响应效率,而线程与进程本身也更容易进入假死状态。

很多程序员可能会考虑使用"线程池"或"连接池"。"线程池"旨在减少创建和销毁线程的频率,其维持一定合理数量的线程,并让空闲的线程重新承担新的执行任务。"连接池"维持连接的缓存池,尽量重用已有的连接、减少创建和关闭连接的频率。这两种技术都可以很好的降低系统开销,都被广泛应用很多大型系统,如 websphere、tomcat 和各种数据库等。

但是,"线程池"和"连接池"技术也只是在一定程度上缓解了频繁调用 IO 接口带来的资源占用。而且,所谓"池"始终有其上限,当请求大大超过上限时,"池"构成的系统对外界的响应并不比没有池的时候效果好多少。所以使用"池"必须考虑其面临的响应规模,并根据响应规模调整"池"的大小。

对应上例中的所面临的可能同时出现的上千甚至上万次的客户端请求,"线程池"或"连接池"或许可以缓解部分压力,但是不能解决所有问题。

总之,多线程模型可以方便高效的解决小规模的服务请求,但面对大规模的服务请求,多线程模型并不是最佳方案。下一章我们将讨论用非阻塞接口来尝试解决这个问题。

非阻塞的服务器程序

以上面临的很多问题,一定程度是 IO 接口的阻塞特性导致的。多线程是一个解决方案,还一个方案就是使用非阻塞的接口。

非阻塞的接口相比于阻塞型接口的显著差异在于,在被调用之后立即返回。使用如下的函数可以将某句柄 fd 设为非阻塞状态。

	 fcntl( fd, F_SETFL, O_NONBLOCK );

下面将给出只用一个线程,但能够同时从多个连接中检测数据是否送达,并且接受数据。

图 3. 使用非阻塞的接收数据模型
图 3. 使用非阻塞的接收数据模型

在非阻塞状态下,recv() 接口在被调用后立即返回,返回值代表了不同的含义。如在本例中,

  • recv() 返回值大于 0,表示接受数据完毕,返回值即是接受到的字节数;
  • recv() 返回 0,表示连接已经正常断开;
  • recv() 返回 -1,且 errno 等于 EAGAIN,表示 recv 操作还没执行完成;
  • recv() 返回 -1,且 errno 不等于 EAGAIN,表示 recv 操作遇到系统错误 errno。

可以看到服务器线程可以通过循环调用 recv() 接口,可以在单个线程内实现对所有连接的数据接收工作。

但是上述模型绝不被推荐。因为,循环调用 recv() 将大幅度推高 CPU 占用率;此外,在这个方案中,recv() 更多的是起到检测"操作是否完成"的作用,实际操作系统提供了更为高效的检测"操作是否完成"作用的接口,例如 select()。

使用 select() 接口的基于事件驱动的服务器模型

大部分 Unix/Linux 都支持 select 函数,该函数用于探测多个文件句柄的状态变化。下面给出 select 接口的原型:

 FD_ZERO(int fd, fd_set* fds)    FD_SET(int fd, fd_set* fds)    FD_ISSET(int fd, fd_set* fds)    FD_CLR(int fd, fd_set* fds)    int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds,           struct timeval *timeout)

这里,fd_set 类型可以简单的理解为按 bit 位标记句柄的队列,例如要在某 fd_set 中标记一个值为 16 的句柄,则该 fd_set 的第 16 个 bit 位被标记为 1。具体的置位、验证可使用 FD_SET、FD_ISSET 等宏实现。在 select() 函数中,readfds、writefds 和 exceptfds 同时作为输入参数和输出参数。如果输入的 readfds 标记了 16 号句柄,则 select() 将检测 16 号句柄是否可读。在 select() 返回后,可以通过检查 readfds 有否标记 16 号句柄,来判断该"可读"事件是否发生。另外,用户可以设置 timeout 时间。

下面将重新模拟上例中从多个客户端接收数据的模型。

图 4. 使用 select() 的接收数据模型
图 4. 使用 select() 的接收数据模型

上述模型只是描述了使用 select() 接口同时从多个客户端接收数据的过程;由于 select() 接口可以同时对多个句柄进行读状态、写状态和错误状态的探测,所以可以很容易构建为多个客户端提供独立问答服务的服务器系统。

图 5. 使用 select() 接口的基于事件驱动的服务器模型
图 5. 使用 select() 接口的基于事件驱动的服务器模型

这里需要指出的是,客户端的一个 connect() 操作,将在服务器端激发一个"可读事件",所以 select() 也能探测来自客户端的 connect() 行为。

上述模型中,最关键的地方是如何动态维护 select() 的三个参数 readfds、writefds 和 exceptfds。作为输入参数,readfds 应该标记所有的需要探测的"可读事件"的句柄,其中永远包括那个探测 connect() 的那个"母"句柄;同时,writefds 和 exceptfds 应该标记所有需要探测的"可写事件"和"错误事件"的句柄 ( 使用 FD_SET() 标记 )。

作为输出参数,readfds、writefds 和 exceptfds 中的保存了 select() 捕捉到的所有事件的句柄值。程序员需要检查的所有的标记位 ( 使用 FD_ISSET() 检查 ),以确定到底哪些句柄发生了事件。

上述模型主要模拟的是"一问一答"的服务流程,所以,如果 select() 发现某句柄捕捉到了"可读事件",服务器程序应及时做 recv() 操作,并根据接收到的数据准备好待发送数据,并将对应的句柄值加入 writefds,准备下一次的"可写事件"的 select() 探测。同样,如果 select() 发现某句柄捕捉到"可写事件",则程序应及时做 send() 操作,并准备好下一次的"可读事件"探测准备。下图描述的是上述模型中的一个执行周期。

图 6. 一个执行周期
图 6. 一个执行周期

这种模型的特征在于每一个执行周期都会探测一次或一组事件,一个特定的事件会触发某个特定的响应。我们可以将这种模型归类为"事件驱动模型"。

相比其他模型,使用 select() 的事件驱动模型只用单线程(进程)执行,占用资源少,不消耗太多 CPU,同时能够为多客户端提供服务。如果试图建立一个简单的事件驱动的服务器程序,这个模型有一定的参考价值。

但这个模型依旧有着很多问题。

首先,select() 接口并不是实现"事件驱动"的最好选择。因为当需要探测的句柄值较大时,select() 接口本身需要消耗大量时间去轮询各个句柄。很多操作系统提供了更为高效的接口,如 linux 提供了 epoll,BSD 提供了 kqueue,Solaris 提供了 /dev/poll …。如果需要实现更高效的服务器程序,类似 epoll 这样的接口更被推荐。遗憾的是不同的操作系统特供的 epoll 接口有很大差异,所以使用类似于 epoll 的接口实现具有较好跨平台能力的服务器会比较困难。

其次,该模型将事件探测和事件响应夹杂在一起,一旦事件响应的执行体庞大,则对整个模型是灾难性的。如下例,庞大的执行体 1 的将直接导致响应事件 2 的执行体迟迟得不到执行,并在很大程度上降低了事件探测的及时性。

图 7. 庞大的执行体对使用 select() 的事件驱动模型的影响
图 7. 庞大的执行体对使用 select() 的事件驱动模型的影响

幸运的是,有很多高效的事件驱动库可以屏蔽上述的困难,常见的事件驱动库有 libevent 库,还有作为 libevent 替代者的 libev 库。这些库会根据操作系统的特点选择最合适的事件探测接口,并且加入了信号 (signal) 等技术以支持异步响应,这使得这些库成为构建事件驱动模型的不二选择。下章将介绍如何使用 libev 库替换 select 或 epoll 接口,实现高效稳定的服务器模型。

使用事件驱动库 libev 的服务器模型

Libev 是一种高性能事件循环 / 事件驱动库。作为 libevent 的替代作品,其第一个版本发布与 2007 年 11 月。Libev 的设计者声称 libev 拥有更快的速度,更小的体积,更多功能等优势,这些优势在很多测评中得到了证明。正因为其良好的性能,很多系统开始使用 libev 库。本章将介绍如何使用 Libev 实现提供问答服务的服务器。

(事实上,现存的事件循环 / 事件驱动库有很多,作者也无意推荐读者一定使用 libev 库,而只是为了说明事件驱动模型给网络服务器编程带来的便利和好处。大部分的事件驱动库都有着与 libev 库相类似的接口,只要明白大致的原理,即可灵活挑选合适的库。)

与前章的模型类似,libev 同样需要循环探测事件是否产生。Libev 的循环体用 ev_loop 结构来表达,并用 ev_loop( ) 来启动。

	 void ev_loop( ev_loop* loop, int flags )

Libev 支持八种事件类型,其中包括 IO 事件。一个 IO 事件用 ev_io 来表征,并用 ev_io_init() 函数来初始化:

	 void ev_io_init(ev_io *io, callback, int fd, int events)

初始化内容包括回调函数 callback,被探测的句柄 fd 和需要探测的事件,EV_READ 表"可读事件",EV_WRITE 表"可写事件"。

现在,用户需要做的仅仅是在合适的时候,将某些 ev_io 从 ev_loop 加入或剔除。一旦加入,下个循环即会检查 ev_io 所指定的事件有否发生;如果该事件被探测到,则 ev_loop 会自动执行 ev_io 的回调函数 callback();如果 ev_io 被注销,则不再检测对应事件。

无论某 ev_loop 启动与否,都可以对其添加或删除一个或多个 ev_io,添加删除的接口是 ev_io_start() 和 ev_io_stop()。

	 void ev_io_start( ev_loop *loop, ev_io* io )   	 void ev_io_stop( EV_A_* )

由此,我们可以容易得出如下的"一问一答"的服务器模型。由于没有考虑服务器端主动终止连接机制,所以各个连接可以维持任意时间,客户端可以自由选择退出时机。

图 8. 使用 libev 库的服务器模型
图 8. 使用 libev 库的服务器模型

上述模型可以接受任意多个连接,且为各个连接提供完全独立的问答服务。借助 libev 提供的事件循环 / 事件驱动接口,上述模型有机会具备其他模型不能提供的高效率、低资源占用、稳定性好和编写简单等特点。

由于传统的 web 服务器,ftp 服务器及其他网络应用程序都具有"一问一答"的通讯逻辑,所以上述使用 libev 库的"一问一答"模型对构建类似的服务器程序具有参考价值;另外,对于需要实现远程监视或远程遥控的应用程序,上述模型同样提供了一个可行的实现方案。

总结

本文围绕如何构建一个提供"一问一答"的服务器程序,先后讨论了用阻塞型的 socket 接口实现的模型,使用多线程的模型,使用 select() 接口的基于事件驱动的服务器模型,直到使用 libev 事件驱动库的服务器模型。文章对各种模型的优缺点都做了比较,从比较中得出结论,即使用"事件驱动模型"可以的实现更为高效稳定的服务器程序。文中描述的多种模型可以为读者的网络编程提供参考价值。