搜档网
当前位置:搜档网 › ISE使用手册

ISE使用手册

ISE使用手册
ISE使用手册

ISE 使用手册
第 1 页
ISE 使用手册
郝新庚 (haoxingeng@https://www.sodocs.net/doc/4a7179029.html,) 最后更新:2013-05-30
一. ISE简介 ............................................................................................................. 3 1.1 ISE概述 ....................................................................................................... 3 1.2 ISE的主要特点 ............................................................................................... 3 1.3 为什么要跨平台 .............................................................................................. 3 1.4 编译与安装 .................................................................................................... 4 1.5 目录结构 ...................................................................................................... 5 1.6 Hello World .................................................................................................. 7 1.7 IseBusiness接口 ............................................................................................ 8 二. 使用ISE开发TCP服务端程序 ................................................................................... 12 2.1 详述ISE的TCP并发模型 .................................................................................. 12 2.1.1 IO模型 ............................................................................................. 12 2.1.2 Reactor与Proactor.............................................................................. 13 2.1.3 常见TCP并发模型................................................................................. 14 2.1.4 ISE的TCP并发模型............................................................................... 16 2.2 基于ISE开发TCP服务端程序 ............................................................................. 17 2.2.1 与TCP相关的几个角色简介 ...................................................................... 17 2.2.2 发送和接收数据 ................................................................................... 17 2.2.3 连接的断开与销毁 ................................................................................ 20 2.2.4 主动发起连接 ...................................................................................... 21 2.2.5 并发模型的选择 ................................................................................... 22 2.2.6 多个TCP服务器的实现 ........................................................................... 22 2.3 实现一个简单的echo服务 ................................................................................ 23 三. 使用ISE开发UDP服务端程序 .................................................................................. 27

ISE 使用手册
第 2 页
3.1 UDP服务端工作原理....................................................................................... 27 3.1.1 监听线程池 ........................................................................................ 27 3.1.2 生产者消费者队列 ................................................................................ 27 3.1.3 动态工作者线程池 ................................................................................ 28 3.2 高并发UDP服务程序设计 ................................................................................. 29 3.2.1 对UDP请求包进行分组(Request Grouping)............................................. 29 3.2.2 设置相关参数 ...................................................................................... 30 3.3 实现一个简单的UDP服务 ................................................................................. 31 四. 更进一步了解ISE ................................................................................................ 37 4.1 服务模块(Server Modules) .......................................................................... 37 4.1.1 服务模块工作原理 ................................................................................ 37 4.1.2 基于服务模块的编程方式 ........................................................................ 37 4.1.3 服务模块编程示例 ................................................................................ 37 4.2 辅助线程(Assistor Threads) ........................................................................ 38 4.3 服务状态监视(Server Inspector)................................................................... 38 4.4 多线程环境编程基础设施 ................................................................................. 39 4.5 ISE扩展(ISE Extensions) ........................................................................... 40 4.6 对数据库的支持 ............................................................................................ 41 4.6.1 ISE数据库接口(DBI) ......................................................................... 41 4.6.2 MySQL接口 ....................................................................................... 43 4.6.3 开发更多的数据库接口 ........................................................................... 45 五. ISE编程示例...................................................................................................... 46 5.1 四个简单的TCP协议 ....................................................................................... 46 5.2 简单的HTTP服务 ........................................................................................... 48 5.3 服务状态监视器 ............................................................................................ 48 5.4 工作者线程池 ............................................................................................... 48 5.5 简单的UDP服务端 ......................................................................................... 49 六. 附录 ................................................................................................................ 50 6.1 ISE参数配置 ................................................................................................ 50 6.2 参考资料 .................................................................................................... 52

ISE 使用手册
第 3 页
一. ISE简介
1.1 ISE概述
ISE(Iris Server Engine)是一个基于现代 C++的跨平台(Linux 和 Windows)的高性能多线程 并发网络服务器程序框架。它封装了琐碎的 socket 以及各种操作系统 APIs,以面向对象方式向开发者提 供稳定、高效、易扩展、易配置、易维护的程序框架。ISE 的用户只需遵循接口的约定,挂接自己的业务 程序,即可轻松开发出稳定、高效的网络服务器程序。
1.2 ISE的主要特点
? ? ? ? ? ? ? ? 跨平台。目前支持 Linux 和 Windows。 基于多线程并发,而非多进程。 支持 TCP 与 UDP 服务端程序开发。 基于 Proactor 模式的 TCP 服务接口。 基于请求分组与负载自适应的高并发 UDP 服务模型。 不支持 IPv6,只支持 IPv4。 提供通用数据库接口(DBI) ,并预置 MySQL 扩展。 ISE 是一个程序框架(Framework) ,而非一个程序库(Library) 。
1.3 为什么要跨平台
ISE 克服了许多平台差异带来的困难,在不牺牲性能的前提下,为开发者提供了一致的接口。ISE 跨 平台的目标是 Linux 和 Windows,短期内不考虑其它平台。在这两种平台下,多线程网络服务程序需要面 对的问题有: ? 多线程环境编程基础设施。比如线程实现方式、互斥锁、条件变量、信号量、原子整数等。两种 平台对这些基础设施的实现差异甚大,比如 Windows 平台下,在 Vista 之前的系统并不提供对 条件变量的支持,需要开发者通过多种同步机制自行模拟;再比如 Windows 中的事件对象 (Event)在 Linux 中并无对应的机制。 IO 多路复用机制(IO multiplexing) 。Linux::EPoll 和 Windows::IOCP 分别代表了两种平台 下最高效的 IO 多路复用机制,但是两者的设计理念完全不同,EPoll 以 Reactor 模式为基础,而 IOCP 则以 Proactor 模式为基础。在不同的平台下,要求开发者以不同的思维模式来设计自己的 程序。ISE 解决了这个问题。 数据库访问机制。虽然不是全部,但有相当一部分服务端程序存在访问数据库的需求。在不同的 平台下,访问数据库的途径五花八门,而如果要实现跨平台,则有必要实现一套统一的机制。 其它常用操作。比如文件读写、日期时间、内存管理、日志输出等等。平台的差异性将导致这些
?
? ?

ISE 使用手册
第 4 页
最常用操作的实现方式大相径庭。ISE 封装了这些差异,为开发者提供了一致的接口。 从这些差异来看,ISE 为跨平台做了不少工作。但好在付出的这些代价只局限在 ISE 本身的开发,对 于 ISE 的用户来说,跨平台并没有带来坏处。ISE 跨平台的前提是:不牺牲性能。
那么 ISE 为什么要跨平台?因为: ? ? 这是 ISE 的选择; 如果你正在做一个基于 Linux 的网络服务程序,而你的团队中有人并不擅长 Linux 编程,那么他 可以拿起他最擅长的 Visual Studio 工具,写出在两种平台下行为一致的程序(ISE 保证了这一 点) 。 你的产品不只面向一个客户,有些客户要求基于 Linux 系统,而有些要求 Windows 系统。没关 系,基于 ISE 的项目可以做到“一份代码,两处编译” 。
?
1.4 编译与安装
ISE 是一个开源项目,它在 GitHub 中的地址是: https://www.sodocs.net/doc/4a7179029.html,/haoxingeng/ise ISE 的主要开发环境: ? Linux 平台: Debian 6.0 Squeeze(内核版本:2.6.32) g++ 4.4 32 位和 64 位环境。 ? Windows 平台: Visual C++ 2005 SP1
ISE 依赖 Boost,但只用到了 boost::function/bind、shared_ptr、scoped_ptr、any 等这些无需 编译的部分。安装 Boost 很简单: Windows 平台下,可先下载 boost 压缩包后解压到某个目录下(比如 c:\boost) ,再在 Visual C++ 中“选项 -> 项目和解决方案 -> VC++目录 -> 包含文件”中添加 boost 目录(比如 c:\boost)即可。 Linux 平台下,可直接 apt-get 安装:
# sudo apt-get install libboost1.40-dev
在 Windows 平台下编译 ISE, 请先安装 Visual C++ 2005 SP1, 再打开 ise\build\windows\VC8\ 目录下的解决方案(*.sln)文件即可。 libise.sln libise_dbi_mysql.sln - ISE 主程序。 - ISE 数据库接口(DBI)的 MySQL 扩展。

ISE 使用手册
第 5 页
libise_utils.sln examples.sln
- ISE 的 utils 扩展。 - 各种示例程序。
在 Linux 平台下,ISE 采用 CMake 进行编译。如果系统未安装 CMake,可先安装它:
# sudo apt-get install cmake
编译 ISE 的方法很简单:
# cd ise/build/linux # ./build.sh # ./build.sh install
以上命令将 ISE 的头文件和生成的静态库文件安装到 ise/build/linux/debug-install/ 目录下。 如果要编译为 release 版,可执行:
# BUILD_TYPE=release ./build.sh # BUILD_TYPE=release ./build.sh install
以上命令将 ISE 的头文件和生成的静态库文件安装到 ise/build/linux/release-install/ 目录下。 编译完成之后,可运行 ise/build/linux/debug/bin 目录下的示例程序,比如 server_inspector。然 后在浏览器中输入 http://192.168.0.100:8080,其中 192.168.0.100 请替换成您的 Linux 机器的实 际 IP 地址。
1.5 目录结构
ISE 的目录结构如下: ISE 根目录 项目编译根目录 Linux 平台的编译目录 Windows 平台的编译目录 文档 示例程序 ISE 源码根目录 ISE 扩展 数据库扩展(DBI) 预置的 MySQL 扩展 实用程序(utils)扩展 丰富的加解密、散列算法库 跨平台的 XML 操作封装库 ISE 主程序
ise ├─ │ │ ├─ ├─ └─
build ├─ linux └─ windows doc examples ise ├─ ext │ ├─ dbi │ │ └─ mysql │ └─ utils │ ├─ cipher │ └─ xml └─ main

ISE 使用手册
第 6 页
编译目录说明: - 项目编译根目录 - Linux 平台的编译目录 - 存放 debug 模式下示例程序编译结果 - 存放 debug 模式下编译安装后的头文件 - 存放 debug 模式下编译安装后的静态库文件 - 存放 release 模式下示例程序编译结果 - 存放 release 模式下编译安装后的头文件 - 存放 release 模式下编译安装后的静态库文件 - Windows 平台的编译目录 - 存放示例程序编译结果 - 存放编译生成的静态库文件
build ├─ linux │ ├─ debug │ │ └─ bin │ ├─ debug-install │ │ ├─ include │ │ └─ lib │ ├─ release │ │ └─ bin │ └─ release-install │ ├─ include │ └─ lib │ └─ windows └─ VC8 ├─ bin └─ lib
ISE 主程序说明:
ise └─ main ├─ ├─ ├─ ├─ ├─ ├─ ├─ ├─ ├─ ├─ ├─ ├─ ├─ ├─ ├─ ├─ ├─ ├─ ├─ ├─ ├─ └─
ise.h ise_application.* ise_assert.* ise_classes.* ise_database.* ise_err_msgs.* ise_exceptions.* ise_global_defs.* ise_http.* ise_inspector.* ise_options.* ise_scheduler.* ise_server_assistor.* ise_server_tcp.* ise_server_udp.* ise_socket.* ise_stldefs.* ise_svr_mod.* ise_svr_mod_msgs.* ise_sys_threads.* ise_sys_utils.* ise_thread.*
-
ISE 主程序根目录 ISE 的总头文件 程序框架流程控制 断言支持 基础类库 数据库接口定义(DBI) 错误信息定义 各种异常定义 全局常量、类型定义 HTTP 协议的实现 服务监视 编译条件定义 简单的定时任务 辅助线程管理 TCP 服务的实现 UDP 服务的实现 基础 socket 封装 STL 相关的头文件 服务模块支持 服务模块消息支持 ISE 框架的后台线程 各种跨平台的实用函数 线程封装

ISE 使用手册
第 7 页
1.6 Hello World
为了快速了解 ISE 的基本使用方法,现在让我们以最快的速度写一个基于 ISE 的“Hello World”程 序。 这个程序是一个 TCP 服务器, 把它运行起来之后, 用 telnet 作为客户端去连接它, 会得到一行输出 “Hello World!”然后连接断开,就这么简单。在 ise/examples/hello_world 目录下可以查看这个示例程序的源 码。
hello_world.h 的源码如下:
hello_world.h
01 #include "ise/main/ise.h" 02 03 using namespace ise; 04 05 class AppBusiness : public IseBusiness 06 { 07 public: 08 09 10 11 12 };
virtual void initIseOptions(IseOptions& options); virtual void onTcpConnected(const TcpConnectionPtr& connection); virtual void onTcpSendComplete(const TcpConnectionPtr& connection, const Context& context);
hello_world.cpp 的源码如下:
hello_world.cpp
01 #include "hello_world.h" 02 03 IseBusiness* createIseBusinessObject() 04 { 05 06 } 07 08 // class AppBusiness 09 10 void AppBusiness::initIseOptions(IseOptions& options) 11 { 12 13 14 } 15 16 void AppBusiness::onTcpConnected(const TcpConnectionPtr& connection) 17 { 18 19 20 } 21 22 void AppBusiness::onTcpSendComplete(const TcpConnectionPtr& connection, 23 24 { 25 26 }
return new AppBusiness();
options.setServerType(ST_TCP); options.setTcpServerPort(12345);
string msg = "Hello World!\r\n"; connection->send(msg.c_str(), msg.length());
const Context& context) connection->disconnect();

ISE 使用手册
第 8 页
从示例程序可以看出,基于 ISE 的程序的开发步骤可简单归结为: 1) 继承 IseBusiness 类,写一个新的业务类 AppBusiness。 2) 写一个函数 createIseBusinessObject(),该函数创建了一个 AppBusiness 对象并返回。 3) 在 AppBusiness 类中根据需要覆写一些虚函数,实现自己的“业务逻辑” 。
下面详细解说 hello_world 程序。 在ISE中, IseBusiness类是一条与 “业务逻辑” 联接的通道, 任何程序都要首先继承它。 IseBusiness 1 , 比 如 对 TCP 而 言 , 有 四 个 虚 函 数 值 得 关 注 : 提 供 了 诸 多 虚 函 数 提 供 给 用 户 覆 写 ( override ) onTcpConnected, onTcpDisconnected, onTcpRecvComplete, onTcpSendComplete。当相应的事 件发生时,ISE会调用这些函数。对hello_world程序而言,它需要关注两个事件: a. 客户端连接建立时,即 onTcpConnected; (此事件发生时须发送字符串。 ) b. 字符串发送完毕时,即 onTcpSendComplete。 (此事件发生时须断开连接。 )
所以,hello_world.h 的第 09、10 行覆写了这两个虚函数。 hello_world.cpp 的第 19 行调用 connection->send() 发送了字符串“Hello World!” 。当发送完 毕时,第 25 行调用 connection->disconnect() 断开了连接。
如前文所述,hello_world 这个程序是一个 TCP 服务端,并且打算绑定本机端口 12345。这些信息需 要“告诉”ISE,途径是覆写 initIseOptions() 这个虚函数。 在 hello_world.cpp 的第 12、13 行,调用 options.setServerType(ST_TCP) 设定本程序是一个 TCP 服务端,调用 options.setTcpServerPort(12345) 指定绑定的端口号。
程序编译: 在 Linux 下,执行 build.sh 后,可在 ise/build/linux/debug/bin 中找到可执行文件 hello_world。 在 Windows 下,可在 VC++ 2005 中打开 ise/build/windows/VC8/examples.sln,编译完毕后, 可在 ise/build/windows/VC8/bin/debug 目录中找到可执行文件 hello_world.exe。
1.7 IseBusiness接口
ISE 提供了一个重要的接口类:IseBusiness。这个类是 ISE 与“业务逻辑”相联的通道。正如从 hello_world 示例程序中看到的,基于 ISE 开发任务程序,第一件事情便是继承 IseBusiness 类。开发者 可以认为 IseBusiness 是 ISE 的回调集合,ISE 通过 IseBusiness 将各种事件通知应用程序。 下面是 IseBusiness 的声明:
1 “面向对象”的怀疑论者可能不太赞成这种“继承+虚函数”的做法。本人不反对“基于对象” ,但更喜欢适度继承带来的 层次美感。 “方法论”的优劣见仁见智。

ISE 使用手册
第 9 页
01 class IseBusiness : 02 03 04 05 { 06 public: 07 08 09 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 public: 33 34 35 36 37 38 39 40 public: 41 42 43 44 45 46 47 48 49 50 51 52 public: 53 54 55
boost::noncopyable, public UdpCallbacks, public TcpCallbacks
IseBusiness() {} virtual ~IseBusiness() {} // 初始化 (失败则抛出异常) virtual void initialize() {} // 结束化 (无论初始化是否有异常,结束时都会执行) virtual void finalize() {} // 解释命令行参数,参数不正确则返回 false virtual bool parseArguments(int argc, char *argv[]) { return true; } // 返回程序的当前版本号 virtual string getAppVersion() { return "0.0.0.0"; } // 返回程序的帮助信息 virtual string getAppHelp() { return ""; } // 初始化 ISE 配置信息 virtual void initIseOptions(IseOptions& options) {} // 初始化之前 virtual void beforeInit() {} // 初始化成功之后 virtual void afterInit() {} // 初始化失败 (不应抛出异常) virtual void onInitFailed(Exception& e) {} /* interface UdpCallbacks */ // UDP 数据包分类 virtual void classifyUdpPacket(void *packetBuffer, int packetSize, int& groupIndex) { groupIndex = 0; } // 收到了 UDP 数据包 virtual void onRecvedUdpPacket(UdpWorkerThread& workerThread, int groupIndex, UdpPacket& packet) {} /* interface TcpCallbacks */ // 接受了一个新的 TCP 连接 virtual void onTcpConnected(const TcpConnectionPtr& connection) {} // 断开了一个 TCP 连接 virtual void onTcpDisconnected(const TcpConnectionPtr& connection) {} // TCP 连接上的一个接收任务已完成 virtual void onTcpRecvComplete(const TcpConnectionPtr& connection, void *packetBuffer, int packetSize, const Context& context) {} // TCP 连接上的一个发送任务已完成 virtual void onTcpSendComplete(const TcpConnectionPtr& connection, const Context& context) {}
// 辅助服务线程执行(assistorIndex: 0-based) virtual void assistorThreadExecute(AssistorThread& assistorThread, int assistorIndex) {}

ISE 使用手册 // 系统守护线程执行 (secondCount: 0-based) virtual void daemonThreadExecute(Thread& thread, int secondCount) {}
第 10 页
56 57 58 };
上的 IseBusiness 类的声明中, 每个接口的注释阐述了它的含义。 IseBusiness 由以下几大部分组成: ? 程序初始化和结束化。当程序启动时,ISE 会调用 initialize();程序退出时,会调用 finalize()。 应用程序可以将与业务逻辑相关的初始化和结束化在这两个函数中实现。
// 初始化 (失败则抛出异常) virtual void initialize() {} // 结束化 (无论初始化是否有异常,结束时都会执行) virtual void finalize() {}
?
命令行参数解析、程序版本、命令行帮助信息。
// 解释命令行参数,参数不正确则返回 false virtual bool parseArguments(int argc, char *argv[]) { return true; } // 返回程序的当前版本号 virtual string getAppVersion() { return "0.0.0.0"; } // 返回程序的帮助信息 virtual string getAppHelp() { return ""; }
?
ISE 配置接口。应用程序通过这个接口对 ISE 的所有参数进行配置。通过参数配置,可以设置服 务器类型(TCP/UDP) 、指定 TCP 监听端口号、选择 TCP 并发模型、进行 UDP 性能调优等等。
// 初始化 ISE 配置信息 virtual void initIseOptions(IseOptions& options) {}
?
程序初始化过程。应用程序一般会在这些回调中输出一些信息。
// 初始化之前 virtual void beforeInit() {} // 初始化成功之后 virtual void afterInit() {} // 初始化失败 (不应抛出异常) virtual void onInitFailed(Exception& e) {}
?
UDP 回调。classifyUdpPacket() 用于对收到的 UDP 包进行分组;每收到一个 UDP 包时,都会 调用 onRecvedUdpPacket()。详见「高并发 UDP 服务程序设计」 。
// UDP 数据包分类 virtual void classifyUdpPacket(void *packetBuffer, int packetSize, int& groupIndex) { groupIndex = 0; } // 收到了 UDP 数据包 virtual void onRecvedUdpPacket(UdpWorkerThread& workerThread, int groupIndex, UdpPacket& packet) {}
?
TCP 回调。详见「基于 ISE 开发 TCP 服务端程序」 。
// 接受了一个新的 TCP 连接 virtual void onTcpConnected(const TcpConnectionPtr& connection) {} // 断开了一个 TCP 连接 virtual void onTcpDisconnected(const TcpConnectionPtr& connection) {} // TCP 连接上的一个接收任务已完成 virtual void onTcpRecvComplete(const TcpConnectionPtr& connection, void *packetBuffer, int packetSize, const Context& context) {}

ISE 使用手册 // TCP 连接上的一个发送任务已完成 virtual void onTcpSendComplete(const TcpConnectionPtr& connection, const Context& context) {}
第 11 页
?
辅助线程和系统守护线程回调。详见「辅助线程(Assistor Threads) 」 。
// 辅助服务线程执行(assistorIndex: 0-based) virtual void assistorThreadExecute(AssistorThread& assistorThread, int assistorIndex) {} // 系统守护线程执行 (secondCount: 0-based) virtual void daemonThreadExecute(Thread& thread, int secondCount) {}

ISE 使用手册
第 12 页
二. 使用ISE开发TCP服务端程序
基于 ISE,开发 TCP 服务端程序将变得非常简单和高效。ISE 封装了琐碎的 socket APIs,解决了网 络编程中的各种麻烦和问题,ISE 使开发者能从复杂易错的 socket 调用中解放出来,把精力集中在业务逻 辑的实现上。在 ISE 中,开发者只需关注四个事件: 1) 2) 3) 4) 连接的建立(IseBusiness::onTcpConnected) ; 连接的断开(IseBusiness::onTcpDisconnected) ; 读操作完成(IseBusiness::onTcpRecvComplete) ; 写操作完成(IseBusiness::onTcpSendComplete) 。
在 ISE 中,通过 TCP 连接发送和接收数据也很简单,发送和接收操作都以异步模式进行。向 ISE 提交 操作请求后,只需关注“读操作完成事件”和“写操作完成事件”即可,不用担心数据只发送了一半,或 者只接收到半个数据包。 ISE 非常重视系统性能。ISE 以 Linux::EPoll 和 Windows::IOCP 为基础,经过合理的封装后,结合 事件循环线程模型(每个线程轮循一个事件循环 + 多线程) ,并将基于 Reactor 的 Linux::EPoll 和基于 Proactor 的 Windows::IOCP 统一,最终向开发者提供 Proactor 模式的编程接口。
2.1 详述ISE的TCP并发模型
为了解释 ISE 的 TCP 并发机制, 先从 IO 模型、 Reactor 与 Proactor 两种模式的比较, 以及常见 TCP 并发服务设计模型说起。
2.1.1
IO模型
系统 IO 可分为三种: ? ? ? 阻塞型; 非阻塞同步型; 非阻塞异步型。
阻塞型 IO 意味着控制权直到调用操作结束后才会回到调用者手中,在等待 IO 结果的过程中,调用者 做不了其它任何事情。 相比之下,非阻塞同步型操作会立即返还控制权给调用者,调用者不需要等待,它从调用操作中获取 两种结果,要么成功了,要么还需继续重试。比如 read() 操作,如果当前 socket 无数据可读,则立即返 回 EWOULBLOCK/EAGAIN,告诉调用者“数据还未就绪,请稍后重试” 。 非阻塞异步型操作和前两者不同,调用操作除了会立即返回控制权,还告诉调用者: “此申请已提交给 系统, 系统会使用另外的资源或线程去完成这项任务, 请注意接收任务完成通知” 。 典型的例子是 Windows

ISE 使用手册
第 13 页
的完成端口(IOCP) 。 对比这三种 IO 模型, “非阻塞异步型”是性能最优、伸缩性最好的。
2.1.2
Reactor与Proactor
在高性能 IO 设计中,有两个著名的模式:Reactor 和 Proactor。其中 Reactor 模式基于同步 IO, 而 Proactor 模式则基于异步 IO。 这两种模式都运用了IO多路复用(IO multiplexing)技术,而IO多路复用机制的核心是“多路事件 ” 。多路事件选择器的作用是分发事件处理者感兴趣的事件。开发者首先要在多 选择器 1(demultiplexor) 路事件选择器那里注册感兴趣的事件,并提供相应的事件处理器(event handlers) ;当事件发生时,多 路事件选择器会将这些事件分发给事件处理器。 在 Reactor 模式中,多路事件选择器等待某个事件发生(比如文件描述符可读写) ,当事件发生时,多 路事件选择器把这个事件传给事先注册的事件处理器,再由后者来做实际的读写操作。 而在 Proactor 模式中,程序发起一个异步操作请求,而实际的工作由操作系统来完成。发起请求时, 以读操作为例,需提供读数据大小、缓存区、以及这个请求完成后的事件处理器等信息。多路事件选择器 得知这个请求后,它等待这个请求的完成,然后发送完成事件给相应的事件处理器。 下面再以读操作为例,以步骤分解方式看看这两种模式的区别。 Reactor 模式的做法: ? ? ? ? 事件处理者宣称它对某个 socket 上的“可读”事件感兴趣; 多路事件选择器等待这个事件的发生; 当事件发生时,多路事件选择器通知事件处理者; 事件处理者收到了通知,于是去执行 socket 的读操作。如果需要,再次重复以上步骤。
Proactor 模式的做法: ? ? ? ? ? 事件处理者直接提交一个读操作,并只对该操何时完成感兴趣; 多路事件选择器等待这个读操作的完成; 在多路事件选择器等待期间,操作系统已经开始了实际的读操作,它从目标读取数据并放入用户 提供的缓存区,最后通知多路事件选择器: “任务完成” ; 多路事件选择器通知事件处理者: “读操作已完成” ; 事件处理者这时会发现,想要读的数据已经放在了缓存区中。如果有需要,再次重复以上步骤。
对比可以发现,Reactor 与 Proactor 的根本区别是:真正的读写操作是由谁来完成的。Reactor 模式 需要应用程序自己读写数据;而 Proactor 模式中,真正的读写操作由操作系统代劳。 如前文所述, Linux::EPoll 基于 Reactor 模式, 而 Windows::IOCP 基于 Proactor 模式, 各有特色。 因为模式的差异,在不同的平台下,开发者必须以不同的思维模式去设计程序。ISE 提供了一种融合了 Reactor 与 Proactor 两种模式的解决方案,对 Reactor 稍做演变,在“真正的读写操作是由谁来完成的” 这个核心问题上, 把真正读写操作任务由原本的应用程序交给 ISE, 即由 ISE 来担任 Proactor 模式中操作 系统的角色,这样 Reactor 便摇身变成了 Proactor。 通过这样的模式演变,ISE 便可以在两种平台下向开发者提供统一的编程模式,即 Proactor 模式。下 面列出了 ISE 执行一个读操作的流程:
1
又名“事件分离器” 。

ISE 使用手册
第 14 页
? ?
? ? 2.1.3
应用程序提交了一个读请求:connection->recv(...); ISE 内部执行实际的读操作。在 Linux 平台下,由事件循环线程驱动,当 EPoll 显示为可读状态 时,执行读操作,直到读取完用户期望的数据;在 Windows 平台下,直接向 IOCP 提交读操作 请求,并由事件循环线程驱动,接收完成通知并酌情重试,直到读取完用户期望的数据; ISE 读取完用户期望的数据后,通过 IseBusiness::onTcpRecvComplete 通知应用程序; 应用程序这时发现,缓存中已经有了期望的数据,并且是完整的。 常见TCP并发模型
在网络服务器程序的运行环境中,一般都是一个服务器对应多个客户端。为了处理客户端的请求,对 服务端程序的设计有很多要求。服务器程序为了能同时响应不同的客户端,产生了不同的并发策略,这些 策略被归纳总结之后,称为并发模型。下面列出几种常见的基于 TCP 的并发模型。 (0) 循环服务器 (1) 即时创建型多进程并发模型 (2) 即时创建型多线程并发模型 (3) 预创建型多进程并发模型 (4) 预创建型多线程并发模型 (5) 基于 select 的小规模 IO 多路复用并发模型 (6) 基于 IO 多路复用的单线程并发模型 (7) 基于 IO 多路复用的单线程 IO 加工作线程池并发模型 (8) 基于 IO 多路复用的单事件循环多线程并发模型 (9) 基于 IO 多路复用的多事件循环多线程并发模型 (10) (11) 基于 IO 多路复用的多事件循环多进程并发模型 基于 IO 多路复用的多事件循环多线程加工作线程池并发模型
模型(0)
循环服务器
该方案其实并不能实现并发,它只是一个循环服务器,一次只能服务于一个客户端。只有在这个客户 端的所有请求满足后,服务器才可以继续后面的请求。如果有一个客户端占住服务器不放,其它客户端就 都不能工作了,因此,TCP 服务器很少采用循环服务器模型,之所以在这儿列出来是为了与其它模型对比。 该模型的工作流程为:
01 socket(...); 02 bind(...); 03 listen(...); 04 while(true) 05 { 06 07 08 09 }
accept(...); process(...); close(...);

ISE 使用手册
第 15 页
模型(1)
即时创建型多进程并发模型
这是传统的 Unix 网络编程并发模型。每当接收到一个客户端的连接后,便立即 fork 出一个新的子进 程来为这个客户端服务。服务完毕后,子进程退出。所以,这种模型也叫“process per connection” 。 这种模型并不适合并发量较大的场合,因为 fork 的开销太大。 模型(2) 即时创建型多线程并发模型
与即时创建进程相比,创建一个线程的开销要小得多。但因为受到操作系统对单个进程内线程数量的 限制,该模型仍然无法适用于大并发量的情况。另外,线程数量太多,会给操作系统带来严重的线程切换 开销。 模型(3) 预创建型多进程并发模型
这是对模型(1)的优化。为了避免不断地创建和销毁进程,该模型在程序初始化时预先创建好了一批子 进程。Apache httpd 使用了这种模型。 模型(4) 预创建型多线程并发模型
这是对模型(2)的优化。为了避免不断地创建和销毁线程,该模型在程序初始化时预先创建好了一批线 程。Apache httpd 除了使用模型(3)外,也使用了这种模型。 模型(5) 基于 select 的小规模 IO 多路复用并发模型
IO 多路复用 (IO multiplexing) 是为了避免进程或线程阻塞于某个特定的 IO 系统调用而产生的技术。 IO 多路复用技术使得单个进程或线程可以同时关注多个 socket 的变化。 Linux 平台下的相应机制有 select、poll、epoll;Windows 平台下则有 select、Overlapped IO、IOCP 等。该模型使用 select 作 为 IO 多路复用的基础。它之所以被冠以“小规模” ,是因为 select 调用能同时关注的 socket 的数量受到 操作系统的限制:在 Linux 平台下,这个限制一般是 1024;而在 Windows 平台下则更小,一般是 64。 该模型由于受到 select 的限制,所以并不实用。 模型(6) 基于 IO 多路复用的单线程并发模型
除模型(5)外, 其它模型都不使用 select, 而是采用伸缩性更佳的 Linux::EPoll 或 Windows::IOCP。 在这种模型中,只有一个线程,既负责 IO,又负责业务逻辑计算,虽然能同时应对很多并发连接,但较难 发挥多核威力。所以,这种模型适合 IO 密集型应用,而不太适合 CPU 密集型应用。 模型(7) 基于 IO 多路复用的单线程 IO 加工作线程池并发模型
在这个模型中,一个线程负责 IO,另增加一个固定大小的线程池(根据计算密集程度和 CPU 数量确 定线程池的大小) ,用于业务逻辑计算。IO 线程接收到客户端的任务后,将任务转移至工作线程池,线程 池完成任务后再交由 IO 线程完成最后的对客户端的应答。 这种模型很好地弥补了模型(6)的不足。既能利用 IO 多路复用处理并发连接,又能借助工作线程池充 分发挥多核优势。在 IO 压力不大时,此模型能很好地满足一般需求。当 IO 压力大时,可考虑采用“多事 件循环”方案,见模型(9)和模型(11)。 模型(8) 基于 IO 多路复用的单事件循环多线程并发模型
在 Reactor/Proactor 模式中,程序执行点由多路事件选择器(demultiplexor)开始, 运行至事件 处理器,再返回至多路事件选择器,这个循环称为“事件循环(event loop) ” 。在具体实现中,这个循环 中可能有事件发生,也可能没有。当没有事件发生时,实际上是一个什么也没做的空循环。事件循环的运 转要由线程(或进程)来驱动。一个事件循环可以由一个线程来驱动,也可以由多个线程来驱动(事件发 生时,多路事件选择器只将事件传递给一个线程) 。反过来,一个线程无法同时驱动多个事件循环。 在这种模型中,只使用单个事件循环(即只有一个多路事件选择器) ,但是有多个线程在驱动。这是典

ISE 使用手册
第 16 页
型的 Windows::IOCP 的做法。在 IOCP 中,一般只创建一个“完成端口句柄” ,同时会根据 CPU 核数创 建若干个线程。这些线程都参与事件循环(调用 API: GetQueuedCompletionStatus ) 。这种模型与 Windows::IOCP 非常匹配,由于 Windows 操作系统内置了高效的异步机制,在即使只有一个“完成端 口句柄”的情况下仍能高效地工作,而多线程可以充分利用 CPU 资源。 在多数场合下,这种模型能很好地工作。但由于单事件循环的特性(即单个事件循环中管理着所有并 发连接) ,使得应用层很难区分不同连接的优先级;另外,单个连接上的不同请求会被分配到不同的线程, 使得请求处理的顺序性很难得到保证。 模型(9) 基于 IO 多路复用的多事件循环多线程并发模型
基于目前操作系统的性能,对于一个网络程序,一个事件循环 1就足以应付千兆以太网的网络IO。在一 个程序使用多个事件循环更多的是基于其它方面的考虑,比如利用多事件循环来区别对待不同优先级的连 接。 这种模型就是这样,它使用了多个事件循环(即多个多路事件选择器) ,并为每个事件循环只分配一个 线程。事件循环数量与线程数量相等,数量大小一般由 CPU 核数确定。 这种模型中,多线程模式保证了对 CPU 资源的充分利用;并发 IO 请求由多个事件循环共同承担,避 免突发 IO 导致单个事件循环的负载饱和; 另外, 一个连接被分配到一个事件循环后, 完全由一个线程管理, 保证了连接上请求处理的顺序性。与模型(7)相比,由于此模型使用了多线程 IO,所以在处理突发 IO 时更 能应对自如,虽然少了工作线程池,但小规模计算仍可以在当前 IO 线程中进行。此外,由于使用了多个事 件循环,能够处理连接的优先级,可以将优先级高的连接分配到单独的事件循环中。 综上,这是一种适应性很强的并发模型。同时也是 ISE 的默认 TCP 并发模型。 模型(10) 模型(11) 基于 IO 多路复用的多事件循环多进程并发模型
与模型(9)相比,这个模型将线程换成了进程。Nginx 采用的正是这个模型。ISE 不支持此模型。 基于 IO 多路复用的多事件循环多线程加工作线程池并发模型
在模型(9)在基础上增加工作线程池,就形成了这种并发模型。这种模型适用于既有大规模 IO 并发请 求,又存在大量的计算任务的情形。如同模型(7)的做法一样,IO 线程并不参与计算,而是把计算任务转 移到工作线程池中。ISE 提供了对这一模型的支持。
2.1.4
ISE的TCP并发模型
在上节提出的 12 种并发模型中,实用的模型有以下四种: ? ? ? ? 模型(6) 基于 IO 多路复用的单线程并发模型; 模型(7) 基于 IO 多路复用的单线程 IO 加工作线程池并发模型; 模型(9) 基于 IO 多路复用的多事件循环多线程并发模型; 模型(11) 基于 IO 多路复用的多事件循环多线程加工作线程池并发模型。
这四种实用模型在 ISE 中都得到了完善的支持。具体使用方法,详见「并发模型的选择」 。
1
一个事件循环,在 Linux 平台下意味着只使用一个 EPoll fd;在 Windows 平台下意味着只创建一个 IOCP handle。

ISE 使用手册
第 17 页
2.2 基于ISE开发TCP服务端程序
在了解 ISE 的 TCP 并发模型后,本节将介绍基于 ISE 开发 TCP 服务端程序的技术细节。ISE 作为一 个程序框架,为开发者提供便利的同时,也提出了一些合理的“规定” ,比如 TCP 连接的管理方式、发送和 接收数据的方式等。开发者必须遵循这些规则,才能写出正确的程序。
2.2.1
与TCP相关的几个角色简介
在 ISE 中,与 TCP 相关的角色有: ? TcpConnection 封装了一个 TCP 连接。开发者的主要工作是和它打交道,在连接上可以进行数据的发送和接收操 作。它的对象依靠 boost::shared_ptr 管理(TcpConnectionPtr) 。ISE 和开发者共同控制 TCP 连接对象的生命期。 TcpServer TCP 的服务端,由 ISE 内部创建和管理,开发者可以无视它。 TcpClient TCP 的客户端,应用程序主动发起连接时会用到它,由 ISE 内部使用,开发者同样可以无视它。 TcpConnector TCP 连 接 器 , 用 于 以 客 户 端 身 份 主 动 发 起 连 接 。 开 发 者 在 应 用 程 序 中 发 起 连 接 时 可 调 用 iseApp().tcpConnector().connect(…)。 连接成功后, 自动创建的 TCP 连接 (TcpConnection) 会自动转移到事件循环中。 EventLoop 事件循环。ISE 内部管理,开发者可以无视它。 InetAddress 封 装 了 基 于 IPv4 的 IP 和 端 口 号 。 值 语 义 。 程 序 中 会 经 常 用 到 它 , 比 如 开 发 者 可 通 过 TcpConnection::getPeerAddr() 来取得此连接中对方的地址。
? ? ?
? ?
总之,虽然与 TCP 相关的角色有好几个,但开发者只需关注 3 个,那就是: TcpConnection 、 TcpConnector 和 InetAddress。
2.2.2
发送和接收数据
对于网络程序而言,最重要的事情莫过于发送和接收数据了。基于传统 socket 网络编程,开发者常常 要为看上去简单的发送和接收花费不少心思,非阻塞(non-blocking IO)模式下数据读写并不是一件简 单的事情。 TCP 是基于字节流的协议, 而在字节流协议上做应用层分包常常是网络编程的基本需求。 所谓 “分包” , 指的是在发送一个消息时,通过一定的处理,让接收方能从字节流中识别并截取出一个完整的消息。TCP 协议本身并没有“应用层分包”的概念和机制,这些工作一定是由应用层来完成的。所以,所谓的“TCP 粘包”问题并不能称作一个问题。 前文已提到,在 ISE 中,应用程序首先通过下面的接口获得了一个连接:

ISE 使用手册
第 18 页
void IseBusiness:onTcpConnected(const TcpConnectionPtr& connection);
现在程序要在连接(connection)上发送或接收数据了,发送数据的函数原型如下:
void TcpConnection::send( const void *buffer, size_t size, const Context& context = EMPTY_CONTEXT, int timeout = TIMEOUT_INFINITE );
函数说明: 发送操作是一个异步任务。send() 调用会立即返回,真正的发送操作由 ISE 完成。当数据发送完成 时,ISE 通过 IseBusiness::onTcpSendComplete() 通知应用程序。如果发送过程遇到错误(比如网络 中断) ,ISE 会断开连接,调用 IseBusiness::onTcpDisconnected(),并最终销毁连接对象。对应用程 序而言,不需要处理“数据只发了一半,还需要继续发送下一半”的情况。在 ISE 中,一个发送任务要么 成功(发送完成) ,要么失败(连接会被断开) 。 参数说明: ? ? ? buffer 要发送数据的缓存区。 size 要发送数据的字节数。 context 由于发送操作是一个异步任务, 发送完成通知需要等到 IseBusiness::onTcpSendComplete(), 为了能在 onTcpSendComplete() 中能知道是哪个任务完成了,可以在 send() 时给这个任务 标记一个“上下文” 。Context 对象是一个 boost::any,它可以存入任何值语义的数据。如果不 需要上下文,要以传入 EMPTY_CONTEXT,这是一个“空上下文对象” 。 timeout 通过此参数指定该发送任务的超时时间(单位为毫秒) 。如果在指定时限内数据还没有发送完成, ISE 会 主 动 断 开 此 连 接 ( IseBusiness::onTcpDisconnect() 会 被 调 用 ) 。此参数缺省为 TIMEOUT_INFINITE,表示无限时。
?
注意事项: 调 用 send() 提 交 一 个 发 送 任 务 后 , 在 此 任 务 完 成 之 前 , 如 果 主 动 断 开 了 连 接 , 比 如 调 用 了 connection->disconnect(),数据并不会完整发送,这时连接会被马上断开,剩余未发数据会被丢弃。
下面是接收数据的函数原型:
void TcpConnection::recv( const PacketSplitter& packetSplitter = ANY_PACKET_SPLITTER, const Context& context = EMPTY_CONTEXT, int timeout = TIMEOUT_INFINITE );
函数说明: 和发送操作一样, 接收操作也是一个异步任务。 recv() 调用会立即返回, 真正的接收操作由 ISE 完成。

ISE 使用手册
第 19 页
当接收完成时,ISE 通过 IseBusiness::onTcpRecvComplete() 通知应用程序。如果接收过程遇到错误 (比如网络中断) ,ISE 会断开连接,调用 IseBusiness::onTcpDisconnected(),并最终销毁连接对象。 应用程序在调用 recv() 时,已经通过 packetSplitter 参数告知了期望的分包规则。只有接收到了完整的 数据包,才能算是“接收完成” 。所以,对于应用程序而言,不需要处理“只接收到数据包的一半,还需要 继续接收下一半”的情况。在 ISE 中,一个接收任务要么成功(接收完成) ,要么失败(连接会被断开) 。 参数说明: ? packetSplitter 在 ISE 中, PacketSplitter 被称为 “分包器” 。 如前所述, 应用层分包是网络编程的基本需求。 ISE 支持自动分包机制,这使得开发者的在接收数据方面的思维负担大大减轻。关于分包,由于篇幅 较大,详见后文。 context 由于接收操作是一个异步任务,接收完成通知需要等到 IseBusiness::onTcpRecvComplete(), 为了能在 onTcpRecvComplete() 中能知道是哪个任务完成了,可以在 recv() 时给这个任务 标记一个“上下文” 。Context 对象是一个 boost::any,它可以存入任何值语义的数据。如果不 需要上下文,要以传入 EMPTY_CONTEXT,这是一个“空上下文对象” 。 timeout 通过此参数指定该接收任务的超时时间(单位为毫秒) 。如果在指定时限内数据还没有接收完成, ISE 会 主 动 断 开 此 连 接 ( IseBusiness::onTcpDisconnect() 会 被 调 用 ) 。此参数缺省为 TIMEOUT_INFINITE,表示无限时。
?
?
注意事项: 在 ISE 内部,无论应用程序有没有提交接收任务,都会尝试接收数据,并将收到数据存入接收缓存中。 这种做法可以提高网络收发效率, 并且在对方主动断开连接情况下, 我方能尽快察觉 (read 返回 0) 。 但是, ISE 并非无休止地接收数据,在当前没有任何接收任务的情况下,ISE 会给每个连接的接收缓存设定最大 容量。最大容量值可由 IseBusiness::initIseOptions() 配置,缺省为 1M Bytes.
下面说说分包。分包器(PacketSplitter)的定义如下:
typedef boost::function PacketSplitter;
正确的分包需要发送方和接收方配合进行,双方约定一个一致的规则。发送方按照约定的规则对数据 进行“打包” ,而接收方遵循同样的规则进行“分包” 。PacketSplitter 便代表了这样的规则。从定义可以 看出,PacketSplitter 其实是一个 boost::function,它可以是一个纯函数、成员函数或仿函数。 在执行接收任务时,ISE 每收到一点数据,都会调用 packetSplitter(recv() 事先已传入) ,如果 packetSplitter 的 retrieveBytes 参数返回了一个大于 0 的值,说明缓存中已经有了一个完整的数据包, 它的大小是 retrieveBytes。ISE 正是根据这样简单的规则来进行分包。 ISE 只对 PacketSplitter 做了定义, 具体的分包规则需由应用程序来实现。 这么做的原因是很显然的: ISE 并不懂“业务逻辑” 。当然,作为程序框架,ISE 内置实现了几个常用的分包器: ? BYTE_PACKET_SPLITTER 这个分包器认为每个字节都是一个数据包。

ISE 使用手册
第 20 页
? ? ?
LINE_PACKET_SPLITTER 这个分包器识别换行符(CR 或 LF 或其组合) ,认为每一行文本都是一个数据包。 NULL_TERMINATED_PACKET_SPLITTER 这个分包器以 ‘\0’ 为分界进行分包。 ANY_PACKET_SPLITTER 这个分包器最特别,它只要一收到数据便认为是一个数据包。
应用程序要实现自己的分包器非常简单,只需要实现一个符合 PacketSplitter 定义的函数(或成员函 数、或仿函数) ,再用 boost::bind 传给 TcpConnection::recv() 即可。 作为一个示例,下面列出了 NULL_TERMINATED_PACKET_SPLITTER 的实现:
01 void nullTerminatedPacketSplitter(const char *data, int bytes, int& retrieveBytes) 02 { 03 04 05 06 07 08 09 10 11 12 13 14 }
const char DELIMITER = '\0'; retrieveBytes = 0; for (int i = 0; i < bytes; ++i) { if (data[i] == DELIMITER) { retrieveBytes = i + 1; break; } }
2.2.3
连接的断开与销毁
连接的断开分两种:主动断开和被动断开。主动断开指应用程序主动调用了断开指令(socket API: close/shutdown) ;而被动断开指对方先断开了连接,被我方检测到(socket API: read 返回 0) 。 在 ISE 中,如果要主动断开连接,可以用下面的方法: ? TcpConnection::disconnect(); 此函数会立即关闭连接的“发送”通道。由于“接收”通道仍未关闭,所以此时程序仍可以接收 数据。正常情况下,对方在检测到我方关闭发送后(read 返回 0) ,应关闭连接。这样我方接着会 read 到 0,从而关闭“接收”通道。这是“优雅关闭”的常见做法,完整流程是: 我方发完了数据 -> 关闭发送 -> 对方 read 返回 0 -> 对方关闭连接 -> 我方 read 返回 0 -> 我方关闭接收 -> 连接完全关闭然后销毁。 TcpConnection::shutdown(bool closeSend, bool closeRecv); 应用程序如果希望自行控制连接的关闭过程,可以调用这个函数。它提供的两个参数 closeSend 和 closeRecv 分别代表是否关闭发送、和是否关闭接收。
?
在 ISE 中,有两种情况会导致 ISE 主动断开连接并双向关闭: a) 发送或接收超时(即在调用 send()/recv() 时使用了 timeout 参数) ; b) 程序退出前关闭剩余的连接。 需要注意的是,无论是 disconnect() 还是 shutdown(),都会立即执行相应操作,而不管该连接上 有没有未完成的发送任务,未完成的任务会被丢弃。如果希望在发送任务完成后再主动断开连接,可以在

相关主题