用于 PDF2JSON 的 RESTful Web 服务





5.00/5 (3投票s)
在 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
总结
使用 restify 为 pdf2json 暴露一个 REST 接口相当简单且功能强大。虽然本文全是关于在 RESTful Web 服务项目中运行 pdf2json,但其基于上下文和事件的异步模型也适用于其他基于 restify 的 Web 服务项目,希望您也觉得它有用。