restify构建REST服务

从零开始nodejs系列文章,将介绍如何利Javascript做为服务端脚本,通过Nodejs框架web开发。Nodejs框架是基于V8的引擎,是目前速度最快的Javascript引擎。chrome浏览器就基于V8,同时打开20-30个网页都很流畅。Nodejs标准的web开发框架Express,可以帮助我们迅速建立web站点,比起PHP的开发效率更高,而且学习曲线更低。非常适合小型网站,个性化网站,我们自己的Geek网站!!

关于作者

  • 张丹(Conan), 程序员Java,R,PHP,Javascript
  • weibo:@Conan_Z
  • blog: http://blog.fens.me
  • email: bsspirit@gmail.com

转载请注明出处:
http://blog.fens.me/nodejs-restify/

nodejs-restify

前言

随着互联网应用的兴起,web2.0时代的到来,越来越多的人,选择用REST编程来代替原来的页面渲染。REST以资源为中心的web服务,分离了展现层和服务层,让前端和后端程序员能更专注于自己擅长的领域。

restify让REST变得如此简单!

目录

  1. 什么是REST?
  2. restify介绍
  3. restify安装
  4. restify服务端API
  5. restify客户端API

1. 什么是REST?

REST(Representational State Transfer表述性状态转移)是一种针对网络应用的设计和开发方式,可以降低开发的复杂性,提高系统的可伸缩性。

REST 定义了一组体系架构原则,您可以根据这些原则设计以系统资源为中心的 Web 服务,包括使用不同语言编写的客户端如何通过 HTTP 处理和传输资源状态。 如果考虑使用它的 Web 服务的数量,REST 近年来已经成为最主要的 Web 服务设计模式。 事实上,REST 对 Web 的影响非常大,由于其使用相当方便,已经普遍地取代了基于 SOAP 和 WSDL 的接口设计。

REST中的资源所指的不是数据,而是数据和表现形式的组合,比如“最新访问的10位会员”和“最活跃的10位会员”在数据上可能有重叠或者完全相同,而由于他们的表现形式不同,所以被归为不同的资源,这也就是为什么REST的全名是Representational State Transfer的原因。资源标识符就是URI(Uniform Resource Identifier),不管是图片,Word还是视频文件,甚至只是一种虚拟的服务,也不管你是xml格式、txt文件格式还是其它文件格式,全部通过 URI对资源进行唯一的标识。

文字介绍,摘自: http://baike.baidu.com/view/1077487.htm

2. restify介绍

restify是一个基于Nodejs的REST应用框架,支持服务器端和客户端。restify比起express更专注于REST服务,去掉了express中的template, render等功能,同时强化了REST协议使用,版本化支持,HTTP的异常处理。

restify提供了DTrace功能,为程序调式带来新的便利!

restifty的发布页:http://mcavage.me/node-restify/

3. restify安装

系统环境

  • Linux: Ubuntu 12.04 LTS 64bit
  • node: v0.6.12
  • npm: 1.1.4

创建项目


~ cd /home/conan/nodejs
~ mkdir nodejs-restify && cd nodejs-restify

~ sudo npm install restify
restify@2.6.1 node_modules/restify
├── assert-plus@0.1.4
├── once@1.3.0
├── deep-equal@0.0.0
├── escape-regexp-component@1.0.2
├── qs@0.6.5
├── tunnel-agent@0.3.0
├── keep-alive-agent@0.0.1
├── lru-cache@2.3.1
├── node-uuid@1.4.0
├── negotiator@0.3.0
├── mime@1.2.11
├── semver@2.2.1
├── spdy@1.14.12
├── backoff@2.3.0
├── formidable@1.0.14
├── csv@0.3.6
├── dtrace-provider@0.2.8
├── verror@1.3.6 (extsprintf@1.0.2)
├── bunyan@0.22.0 (mv@0.0.5)
└── http-signature@0.10.0 (assert-plus@0.1.2, asn1@0.1.11,ctype@0.5.2)

创建一个简单的rest服务

新建文件:app.js


~ vi app.js

var restify = require('restify');

function respond(req, res, next) {
  res.send('hello ' + req.params.name);
}

var server = restify.createServer();
server.get('/hello/:name', respond);
server.head('/hello/:name', respond);

server.listen(3900, function() {
  console.log('%s listening at %s', server.name, server.url);
});

运行程序


~ node app.js
restify listening at http://0.0.0.0:3900

使用curl访问测试


#空路径
~ curl localhost:3900
{"code":"ResourceNotFound","message":"/ does not exist"

#正常GET请求
~ curl localhost:3900/hello/conan
"hello conan"

#设置content-type, 返回包括header信息
~ curl -i localhost:3900/hello/conan -H 'accept:text/plain'
HTTP/1.1 200 OK
Content-Type: text/plain
Content-Length: 11
Date: Mon, 13 Jan 2014 08:31:30 GMT
Connection: keep-alive

hello conan

#设置connection:close, 返回包括header信息
~  curl -i localhost:3900/hello/conan -H 'connection:close'
HTTP/1.1 200 OK
Content-Type: application/json
Content-Length: 13
Date: Mon, 13 Jan 2014 08:29:38 GMT
Connection: close

hello conan

我们看到用restify创建REST服务非常的容易,接下来我们将深入学习restify的API。

4. restify服务端API

  • restify.createServer(): 创建服务器
  • server.use(): 注册handler
  • server.get(): 路由控制
  • server.formatters: 设置content-type
  • 异常处理
  • socket.IO集成/li>
  • Server API
  • 组件管理
  • Request API
  • Response API

1). restify.createServer(): 创建服务器

创建服务器代码


var restify = require('restify');

var server = restify.createServer({
  certificate: ...,
  key: ...,
  name: 'MyApp',
});

server.listen(8080);

服务器创建参数

  • certificate: string, HTTPS服务器的证书
  • key: string, HTTPS服务器的证书key
  • formatters: object, 自定义的response的content-type
  • log: object, 服务器日志,可以配合bunyan一起使用
  • name: string, 服务器的response header
  • spdy: Object, 允许集成node-spdy服务器
  • version: string, 路由版本
  • responseTimeHeader: string, X-Response-Time
  • responseTimeFormatter: function, 格式化header的值

2). server.use(): 注册handler

注册服务器控制组件,按照代码顺序执行,需要放在路由代码之前。

3). server.get(): 路由控制

REST响应G请求:

  • server.get(): 响应GET请求
  • server.post(): 响应POST请求
  • server.put(): 响应PUT请求
  • server.head(): 响应HEAD请求
  • server.del(): 响应DELETE请求

路由控制代码


 function send(req, res, next) {
   res.send('hello ' + req.params.name);
   return next();
 }

 server.post('/hello', function create(req, res, next) {
   res.send(201, Math.random().toString(36).substr(3, 8));
   return next();
 });
 server.put('/hello', send);
 server.get(/^\/([a-zA-Z0-9_\.~-]+)\/(.*)/, send);
 server.head('/hello/:name', send);
 server.del('hello/:name', function rm(req, res, next) {
   res.send(204);
   return next();
 });

4). server.formatters: 设置Response的content-type

content-type类型

  • application/json
  • text/plain
  • application/octet-stream

增加自定义的content-type:application/foo


var server = restify.createServer({
  formatters: {
    'application/foo': function formatFoo(req, res, body) {
      if (body instanceof Error)
        return body.stack;

      if (Buffer.isBuffer(body))
        return body.toString('base64');

      return util.inspect(body);
    }
  }
});

显式的设置Response的content-type


res.setHeader('content-type', 'application/foo');
res.send({hello: 'world'});

5). 异常处理

异常处理代码


var server = restify.createServer();
server.get('/hello/:name', function(req, res, next) {
  return next(new restify.InvalidArgumentError("I just don't like you"));
});

运行程序


~ curl -i localhost:3900/err/conan

HTTP/1.1 409 Conflict
Content-Type: application/json
Content-Length: 60
Date: Mon, 13 Jan 2014 11:06:26 GMT
Connection: keep-alive

RestError的内置的异常类型:

  • BadDigestError: 400
  • BadMethodError: 405
  • InternalError: 500
  • InvalidArgumentError: 409
  • InvalidContentError: 400
  • InvalidCredentialsError: 401
  • InvalidHeaderError: 400
  • InvalidVersionError: 400
  • MissingParameterError: 409
  • NotAuthorizedError: 403
  • RequestExpiredError: 400
  • RequestThrottledError: 429
  • ResourceNotFoundError: 404
  • WrongAcceptError: 406

自定义异常:MyError, errorCode:418


var restify = require('restify');
var util = require('util');

function MyError(message) {
  restify.RestError.call(this, {
    restCode: 'MyError'
    statusCode: 418,
    message: message,
    constructorOpt: MyError
  });
  this.name = 'MyError';
};
util.inherits(MyError, restify.RestError);

6). Socket.IO集成

服务器端集成代码


var server = restify.createServer();
var io = socketio.listen(server);

server.get('/', function indexHTML(req, res, next) {
    fs.readFile(__dirname + '/index.html', function (err, data) {
        if (err) {
            next(err);
            return;
        }

        res.setHeader('Content-Type', 'text/html');
        res.writeHead(200);
        res.end(data);
        next();
});


io.sockets.on('connection', function (socket) {
    socket.emit('news', { hello: 'world' });
    socket.on('my other event', function (data) {
            console.log(data);
    });
});

server.listen(8080, function () {
    console.log('socket.io server listening at %s', server.url);
});

7). Server API

Events事件监听:

  • NotFound: 404 handler
  • MethodNotAllowed: 405 handler
  • VersionNotAllowed: 400 handler
  • UnsupportedMediaType: 415 handler
  • after: 在所有handler之后执行
  • uncaughtException: 未处理的异常

注: uncaughtException的处理,可以参考文章, Nodejs异步异常处理domain

Properties配置属性:

  • name: string, 服务器名字
  • version: string, 路由默认版本
  • log: Object, 日志对象
  • acceptable: Array(String), content-types列表
  • url: string, 服务器信息

Methods函数:

  • address(): 绑定地址
  • listen(port, [host], [callback]): 启动服务器
  • close(): 停止服务器
  • pre(): 在路由之前触发的组件
  • use(): 注册组件

8). 组件管理

restify已支持的组件

  • Accept header parsing: 解析aceept header,返回客户端
  • Authorization header parsing: HTTP Basic Auth认证
  • Date header parsing: 数据头解析
  • JSONP support: JSONP请求
  • Gzip Response: 设置accept-encoding:gzip
  • Query string parsing: 解析URL参数
  • Body parsing (JSON/URL-encoded/multipart form): 解析内容
  • Static file serving: 静态文件处理
  • Throttling: 优化服务器性能配置
  • Conditional request handling: 设置请求条件
  • Audit logger: 日志记录

注册组件server.use()


var server = restify.createServer();
server.use(restify.acceptParser(server.acceptable));
server.use(restify.authorizationParser());
server.use(restify.dateParser());
server.use(restify.queryParser());
server.use(restify.jsonp());
server.use(restify.gzipResponse());
server.use(restify.bodyParser());
server.get(/\/docs\/public\/?.*/, restify.serveStatic({
  directory: './public'
}));
server.use(restify.throttle({
  burst: 100,
  rate: 50,
  ip: true,
  overrides: {
    '192.168.1.1': {
      rate: 0,        // unlimited
      burst: 0
    }
  }
}));
server.use(function setETag(req, res, next) {
  res.header('ETag', 'myETag');
  res.header('Last-Modified', new Date());
});
server.use(restify.conditionalRequest());
server.on('after', restify.auditLogger({
  log: bunyan.createLogger({
    name: 'audit',
    stream: process.stdout
  })
}));

9). Request API

对node内核API:http.ServerRequest的封装

10). Response API

对node内核API:http.ServerResponse的封装

5. restify客户端API

  • JsonClient: 收application/json, 发application/json
  • StringClient: 收text/plain, 发url-encoded request
  • HttpClient: 封装http/https

新建服务器端测试程序


var restify = require('restify');
var server = restify.createServer();
server.use(restify.acceptParser(server.acceptable));
server.use(restify.authorizationParser());
server.use(restify.dateParser());
server.use(restify.queryParser());
server.use(restify.jsonp());
server.use(restify.gzipResponse());
server.use(restify.bodyParser());

# 用于处理JsonClient请求
server.get('/json/v1',function(req,res,next){
  var a = {name:'conan',blog:'blog.fens.me'}
  res.send(a);
});

# 用于处理StringClient请求
server.get('/json/v2',function(req,res,next){
  var a = {name:'conan',blog:'blog.fens.me'}
  res.send(JSON.stringify(a));
});


server.listen(3900, function() {
  console.log('%s listening at %s', server.name, server.url);
});

1). JsonClient

curl请求


~ curl -i http://localhost:3900/json/v1

HTTP/1.1 200 OK
Content-Type: application/json
Content-Length: 38
Date: Mon, 13 Jan 2014 12:09:36 GMT
Connection: keep-alive

{"name":"conan","blog":"blog.fens.me"}

创建文件:jsonclient.js


~ vi jsonclient.js

var restify = require('restify');

var client = restify.createJsonClient({
  url: 'http://localhost:3900'
});

client.get('/json/v1', function(err, req, res, obj) {
  if(err) console.log(err)
  console.log(JSON.stringify(obj, null, 2));
});

运行程序:


~ node jsonclient.js
{
  "name": "conan",
  "blog": "blog.fens.me"
}

2). StringClient

curl请求


~ curl -i http://localhost:3900/json/v2 -H 'accept:text/plain'

HTTP/1.1 200 OK
Content-Type: text/plain
Content-Length: 38
Date: Mon, 13 Jan 2014 12:10:07 GMT
Connection: keep-alive

{"name":"conan","blog":"blog.fens.me"}

创建文件:stringclient.js


~ vi stringclient.js

var restify = require('restify');

var client = restify.createStringClient({
  url: 'http://localhost:3900'
});

client.get('/json/v2', function(err, req, res, obj) {
  if(err) console.log(err)
  console.log(JSON.stringify(obj, null, 2));
});

运行程序:


~ node stringclient.js
"{\"name\":\"conan\",\"blog\":\"blog.fens.me\"}"

3). HttpClient

创建文件: httpclient.js


~ vi httpclient.js

var restify = require('restify');

var client = restify.createClient({
  url: 'http://localhost:3900'
});

client.get('/json/v1', function(err, req) {

  req.on('result', function(err, res) {
    res.body = '';
    res.setEncoding('utf8');
    res.on('data', function(chunk) {
      res.body += chunk;
    });

    res.on('end', function() {
      console.log(res.body);
    });
});
});

运行程序


~ node httpclient.js
{"name":"conan","blog":"blog.fens.me"}

我们已经全面了解的了restify包,接下就可以快速构建我们自己的REST服务了。Node实现如此之简单,让原来Java程序员如何生存呢?!

转载请注明出处:
http://blog.fens.me/nodejs-restify/

打赏作者

This entry was posted in Javascript语言实践

  • Dc Zhu

    好像用express也可以啊。不过好像很少见用express的朋友用put 和 delete

    • 是的,和express类似,这个框架向restful更友好。

  • 楼主,我最近也在用restify,但是遇到bodyParser无法正常解析问题

    • 是不是由于包升级造成的不兼容? bodyParser应该是connect包的中间件吧,参考这里看一下:http://blog.fens.me/nodejs-connect/

  • whilefor

    这个程序好像是官网API GUIDE一个example

  • cc

    博主,如果进行用户验证,不适用basic auth,采取什么方式比较合适?

    • basic auth都是早期站点使用的,有安全问题。一般都是自已实现登陆验证。

      • cc

        如果在站点前期,api的用户验证,在手机客户端的情况下,如果使用token作为每次查询的凭证是否合适,不走https。

        • 你可用OAuth协议,现在大部分的开放API都是这么做的。
          登陆验证最好用https,如果不用token可能会泄露,另外GFW会查询所有明文,可能会有风险。

          • cc

            明了了,谢博主!

  • Pingback: Nodejs学习路线图 | 粉丝日志()

  • mark

  • > ~ sudo npm install restify

    Why `sudo`?

  • 学习啦,谢谢分享

  • henry

    很详细,感谢分享!!

  • muzi_xiangxiang

    怎么判断用户发出的是get还是del呢?

    • 通过header可以知道。

      • muzi_xiangxiang

        谢谢

  • muzi_xiangxiang

    怎么配置2个路由
    app.use(‘/’, routes);
    app.use(‘/layer’,layers);
    访问localhost调用routes的方法,访问localhost/layer调用layers的方法

    • 应该是这样的。

      • muzi_xiangxiang

        我试了下 这样在express模版可以但是在restify提示不是一个function

        • 看一下restify的例子是怎么实现的吧,本文写的时间比较早了,有可能API变了。

    • superchen

      请问配置多个Router的问题,你解决了吗?