65.9K
CodeProject 正在变化。 阅读更多。
Home

用于 PDF2JSON 的 RESTful Web 服务

starIconstarIconstarIconstarIconstarIcon

5.00/5 (3投票s)

2013 年 4 月 5 日

CPOL

6分钟阅读

viewsIcon

49964

在 RESTful Web 服务中运行 pdf2json 模块,该服务使用 restify 和 nodejs 构建

引言

pdf2json 扩展了带有交互式表单元素的 pdf.js,并作为 node.js 模块运行。它将 PDF 文件作为输入,对其进行解析,然后在 node.js 中将其转换为内存对象。pdf2json 模块中包含的命令行工具会将内存中的解析结果写出为 JSON 文件,本文介绍了一种不同的运行时上下文:在 RESTful Web 服务中运行 pdf2json

当通过 Web 服务运行 pdf2json 时,可以在服务器端按需定位和解析 PDF 文件,而客户端应用程序(无论是 Web 客户端、桌面应用还是移动应用)接收的是 JSON 格式的 PDF 内容,而不是 PDF 二进制文件。这样,客户端可以更专注于表单呈现和数据绑定/集成,无需担心加载和解析 PDF 二进制文件。这种架构将数据解析与呈现分离,还将数据模板(服务返回的 JSON 内容)与用户数据(用户在表单中输入的内容)分离开来,从而可以显著减少应用服务器上的会话数据和缓存大小,并使无状态/无会话的服务成为可能,以实现更高的可扩展性和可用性。

该项目已在 Github 上开源,模块名为 p2jsvc,它是用 pdf2json v0.1.23、restify v2.3.5 和 node.js v0.10.1 构建的。

背景

为了在 REST Web 服务中运行 pdf2json,我们利用了 node.js 内置的 Web 服务器,并选择 restify 作为 REST API 框架。尽管 restify 大量借鉴了 express,但它通过严格的 RESTful 风格的服务 API 实现了对 HTTP 交互的完全控制。p2jsvc 的服务端点非常简单。

	HTTP GET:   http://[host_name]:8001/p2jsvc/[folderName]/[pdfId]
	HTTP POST:  http://[host_name]:8001/p2jsvc
		content-type: application/json
		body: {"folderName":"", "pdfId":""} 

响应体中的 JSON 格式有很好的文档说明,这里我就不再重复了,让我们深入了解一下这个服务是如何构建的。

上下文和响应类

在讨论实际的服务代码之前,我们可以简要看一下两个辅助类。第一个是响应类。

 
'use strict';
var SvcResponse = (function () {
    // private static
    var _svcStatusMsg = {200: "OK", 400: "Bad Request", 404: "Not Found"};

    // constructor
    var cls = function (code, message, fieldName, fieldValue) {
        // public, this instance copies
        this.status = {
            code: code,
            message: message || _svcStatusMsg[code],

            fieldName: fieldName,
            fieldValue: fieldValue
        };
    };

    cls.prototype.setStatus = function(code, message, fieldName, fieldValue) {
        this.status.code = code;
        this.status.message = message || _svcStatusMsg[code];
        this.status.fieldName = fieldName;
        this.status.fieldValue = fieldValue;
    };

    cls.prototype.destroy = function() {
        this.status = null;
    };

    return cls;
})();

module.exports = SvcResponse;

实际的响应类将继承自它,这样 `status` 无论是成功还是错误情况,始终是响应内容的一部分。客户端将始终在尝试读取其他属性之前检查 `status.code`,在发生应用程序错误(非网络异常)时,客户端代码可以根据 `status.message`、`status.fieldName` 和 `status.fieldValue` 构建用户友好的消息。一个例子是当用户登录失败时,来自 XHR 的 HTTP 状态是 200,而在响应体中,`status.code` 将是 401,因此客户端会显示一条“请重试”的消息。

第二个辅助类是上下文类,它封装了来自 restify 的 `request`、`response` 对象和 `next` 函数。

 
'use strict';
var SvcContext = (function () {
    // constructor
    var cls = function (req, res, next) {
        // public, this instance copies
        this.req = req;
        this.res = res;
        this.next = next;
    };

    cls.prototype.completeResponse = function(jsObj) {
        this.res.send(200, jsObj);
        this.next();
    };

    cls.prototype.destroy = function() {
        this.req = null;
        this.res = null;
        this.next = null;
    };

    return cls;
})();

module.exports = SvcContext;

由于我们的 Web 服务层位于 pdf2json 之上,而 pdf2json 不应该也不必了解 Web 服务的请求和响应,因此这两层之间的通信将依赖于 nodejs 事件进行异步操作。我们将为每个请求实例化一个新的 pdf2json 和 `SvcContext` 实例,并将新的 `SvcContext` 实例注入到 pdf2json 的实例中。当解析完成事件触发时,服务层中的事件处理程序可以使用来自事件数据的 `SvcContext` 实例,以 nodejs 的非阻塞异步方式完成响应,这样服务实例就可以在等待先前请求的事件时,继续为其他请求提供服务。

有了 `SvcReponse` 和 `SvcContext`,为 pdf2json 编写 REST 服务就成了一项简单而有趣的任务。

创建和配置服务器

restify 负责创建和配置服务器的繁重工作: 

var server = restify.createServer({
	name: self.get_name(),
	version: self.get_version()
});

server.use(restify.acceptParser(server.acceptable));
server.use(restify.authorizationParser());
server.use(restify.dateParser());
server.use(restify.queryParser());
server.use(restify.bodyParser());
server.use(restify.jsonp());
server.use(restify.gzipResponse());
server.pre(restify.pre.userAgentConnection());

一些 restify 内置的处理程序被配置用来处理请求,包括:

  • Accept 头解析
  • Authorization 头解析
  • Date 头解析
  • JSONP 支持
  • Gzip 响应
  • 查询字符串解析
  • 请求体解析(JSON/URL-encoded/multipart form)

由于我使用 `curl` 来测试服务 API,因此配置了 `pre.userAgentConnection()` 来检查用户代理是否为 `curl`。如果是,它会将 Connection 头设置为 "close" 并移除 "Content-Length" 头。否则,`curl` 将默认使用 `Connection: keep-alive`。

路由请求和启动服务器

如前所述,我们希望同时支持 `GET` 和 `POST` 请求 PDF 资源,并且我们希望为每个请求实例化新的 `SvcContext` 实例,然后调用 pdf2json 异步解析 PDF,这将使我们的服务器在处理早期请求时不会被阻塞。

	server.get('/p2jsvc/:folderName/:pdfId', function(req, res, next) {
		_gfilter(new SvcContext(req, res, next));
	});

	server.post('/p2jsvc', function(req, res, next) {
		_gfilter(new SvcContext(req, res, next));
	});

	server.get('/p2jsvc/status', function(req, res, next) {
		var jsObj = new SvcResponse(200, "OK", server.name, server.version);
		res.send(200, jsObj);
		return next();
	});

	server.listen(8001, function() {
		nodeUtil.log(nodeUtil.format('%s listening at %s', server.name, server.url));
	});

对于每个 `GET` 或 `POST` 请求,它都会被路由到同一个 `_gfilter` 函数,并带有一个新的 `SvcContext` 实例。`'/p2jsvc/status'` 路由只是简单地返回一个 HTTP 200 响应,而不解析 PDF,它可以用于服务监控工具的健康检查调用。

处理请求

所有 PDF 解析请求都由 pdf2json 的一个新实例来处理,类名为 `PDFParser`。

 
	var _gfilter = function(svcContext) {
		var req = svcContext.req;
		var folderName = req.params.folderName;
		var pdfId = req.params.pdfId;
		nodeUtil.log(self.get_name() + " resceived request:" + req.method + ":" + folderName + "/" + pdfId);

		var pdfParser = new PFParser(svcContext);

		_customizeHeaders(svcContext.res);

		pdfParser.on("pdfParser_dataReady", _.bind(_onPFBinDataReady, self));
		pdfParser.on("pdfParser_dataError", _.bind(_onPFBinDataError, self));

		pdfParser.loadPDF(_pdfPathBase + folderName + "/" + pdfId + ".pdf");
	};

当创建一个新的 `PDFParser` 实例时,`svcContext` 实例也会被传入。当 "pdfParser_dataReady" 或 "pdfParser_dataError" 事件触发时,事件处理程序可以访问原始的请求和响应对象来完成响应。这种基于新实例、上下文和事件的设置对于我们服务的吞吐量和性能至关重要。

完成响应

当从 pdf2json 实例触发 "pdfParser_dataReady" 或 "pdfParser_dataError" 事件时,响应将被完成,这是通过一个新的 `SvcReponse` 实例来完成的。

 
    var _onPFBinDataReady = function(evtData) {
        var resData = new SvcResponse(200, "OK", evtData.pdfFilePath, "FormImage JSON");
        resData.formImage = evtData.data;
        evtData.context.completeResponse(resData);
    };

    var _onPFBinDataError = function(evtData){
        nodeUtil.log(this.get_name() + " 500 Error: " +  JSON.stringify(evtData.data));
        evtData.context.completeResponse(new SvcResponse(500, JSON.stringify(evtData.data)));
    };

如果解析成功,调用 `context.completeResponse(resData)` 时会创建 JSON 格式的 PDF 解析结果。服务层代码处理所有与服务相关的任务,包括服务器、请求、响应、异步调用 PDFParser 以及将解析结果序列化为 JSON,而 pdf2json 实例以一种与上下文无关的方式工作,因此它既可以在 Web 服务项目中使用,也可以作为命令行工具重用。

跨域支持

在我的项目中,Web 服务器和应用服务器运行在具有不同主机名和子域的独立虚拟机上,这个 p2jsvc 部署在应用服务器上,而我基于 Backbone 的 Web 客户端部署在 Web 服务器上,并通过 Ajax 与应用服务器通信。为了支持这种跨域(或跨子域)的服务器配置,我在 Web 服务器的 httpd.conf 文件中配置了 Apache 代理。

<IfModule proxy_module>
	proxyrequests off

  ProxyPass /p2jsvc/ http://app.server.host.ip:8001/ retry=0
	ProxyPassReverse /p2jsvc/ http://app.server.host.ip:8001/ retry=0
</IfModule>

此外,p2jsvc 还支持 JSONP(在服务器配置中)和跨源资源共享 (CORS)

    var _customizeHeaders = function(res) {
        // This headers comply with CORS and allow us to server our response to any origin
        res.header("Access-Control-Allow-Origin", "*");
        res.header("Access-Control-Allow-Headers", "X-Requested-With");
        res.header("Cache-Control", "no-cache, must-revalidate");
    };

运行和测试服务

以下是一些运行和测试服务的快速命令参考。关于安装:

    git clone https://github.com/modesty/p2jsvc
    cd p2jsvc
    npm install

用于开发时启动服务器:

    cd p2jsvc
    node index

如果服务器启动成功,您应该在控制台中看到提示信息:

    [time_stamp] - PDFFORMServer1 listening at http://0.0.0.0:8001

在生产服务器上启动服务时,我使用 forever 将其作为后台进程运行。

    cd p2jsvc
    forever start index.js	

使用 HTTP GET 运行测试:

curl -isv http://0.0.0.0:8001/p2jsvc/data/xfa_1040ez
curl -isv http://0.0.0.0:8001/p2jsvc/data/xfa_1040a
curl -isv http://0.0.0.0:8001/p2jsvc/data/xfa_1040

这些 `xfa_xxx.pdf` 是测试用的 PDF 文件,您可以用您自己的文件替换它们,并放在 `data` 目录下。同样,您也可以用 POST 来测试:

curl -isv -H "Content-Type: application/json" -X POST -d '{"folderName":"data", "pdfId":"xfa_1040ez"}' http://0.0.0.0:8001/p2jsvc
curl -isv -H "Content-Type: application/json" -X POST -d '{"folderName":"data", "pdfId":"xfa_1040a"}' http://0.0.0.0:8001/p2jsvc
curl -isv -H "Content-Type: application/json" -X POST -d '{"folderName":"data", "pdfId":"xfa_1040"}' http://0.0.0.0:8001/p2jsvc

最后,这是检查服务状态的 `curl` 命令:

 curl -isv http://0.0.0.0:8001/p2jsvc/status

当服务正常运行时,响应的 JSON 体应该是:

 {"status":{"code":200,"message":"OK","fieldName":"PDFFORMServer1"}}

以下命令将发送 10 个并发请求来解析 PDF,用于并发基准测试:

 ab -n 10 -c 10 http://0.0.0.0:8001/p2jsvc/data/xfa_1040ez
 ab -n 10 -c 10 http://0.0.0.0:8001/p2jsvc/data/xfa_1040a
 ab -n 10 -c 10 http://0.0.0.0:8001/p2jsvc/data/xfa_1040

总结

使用 restifypdf2json 暴露一个 REST 接口相当简单且功能强大。虽然本文全是关于在 RESTful Web 服务项目中运行 pdf2json,但其基于上下文和事件的异步模型也适用于其他基于 restify 的 Web 服务项目,希望您也觉得它有用。

© . All rights reserved.