移动云计算中开发和测试用户注册服务器

MongoHQ服务是类似于Amazon S3的云服务,只不过它专注于在云中托管MongoDB实例。可以访问http://mongohq.com来注册服务。小于16MB的数据库是免费的,可以用它来运行本章的示例。在线用户界面易于使用,让你可以快速地浏览数据。

设置好MongoHQ账户之后,应该使用在线的用户界面创建名为Lifestream的数据库。你会得到数据库服务器的名称和端口号,对于每个MongoHQ数据库,这些信息是不同的。还必须输入访问数据库的用户名和密码。在线界面将提供数据库登录的详细信息。

在下面的示例中,会把完整的Lifestream应用程序所需的所有文件放在一起。首先,需要得到配置信息,测试到MongoHQ服务的连接,并验证最基本的功能可以实际工作。在第9章,构建了图片上传功能。现在,可以将它放到一边,集中精力完成用户注册功能。介绍将用户添加到系统的基本概念是引出其余功能必不可少的。在本示例中专注于核心功能,下面将完成一个非常简单的用户注册功能。这里不使用密码,而是为每个物理设备分配一个唯一的令牌。最终,将使用OAuth令牌进行用户注册,但现在要实现一个唯一的设备令牌。下面是具体操作步骤。

(1) 进入lifestream/server文件夹(继续使用和第8章、第9章一样的文件夹结构),运行下面的npm模块安装命令。

npm install connect
npm install mongodb
npm install knox  
npm install uuid  
npm install oauth 
npm install url 
npm install request
npm install cookies

前面已经安装了一些模块,npm将报告已安装的版本信息或将模块升级到最新的版本。

(2) 新建一个名为config.js的文件来存储服务器的配置信息,将下面的代码插入到文件中,用自己的配置信息替换突出显示的内容。 

exports.mongohq   = { 
  username:''YOUR_DB_USERNAME',     
  password: 'YOUR_DB_PASSWORD',
  name:     'YOUR_DB_NAME',
  host:     'YOUR_DB_HOST',
  port:     YOUR_DB_PORT
}
 
exports.amazon  = { 
  s3bucket: 'YOUR_S3_BUCKET_NAME',
  keyid:    'YOUR_AWS_KEY_ID',     
 secret:   'YOUR_AWS_SECRET'
}
 
exports.twitter = { 
  keyid:  'YOUR_TWITTER_KEY_ID', 
  secret: 'YOUR_TWITTER_SECRET'
}
 
exports.facebook = { 
  keyid:  'YOUR_FACEBOOK_KEY_ID',
  secret: 'YOUR_FACEBOOK_SECRET'
}
 
exports.server = 'YOUR_IP_ADDRESS'
exports.max_stream_size = 100

代码片段位于lifestream/server/config.js

(3) 使用下面的更新版本替换lifestream/server文件夹下common.js文件的内容。

var util                        = exports.util                  = require('util')
var connect         =exports.connect       = require('connect')
var knox                        = exports.knox                  = require('knox')
var uuid                        =exports.uuid                  =require('node-uuid')
var oauth                   =exports.oauth             =require('oauth')
var url                         =exports.url                       =require('url')
var request         =exports.request       = require('request')
var Cookies         =exports.Cookies       = require('Cookies')
 
var config = exports.config =require('./config.js')
 
 
// JSON functions
 
exports.readjson = function(req,win,fail) {
  var bodyarr= [];
 req.on('data',function(chunk){
   bodyarr.push(chunk);
  })
  req.on('end',function(){
    varbodystr = bodyarr.join('');
   util.debug('READJSON:'+req.url+':'+bodystr);
    try {
      varbody = JSON.parse(bodystr);
      win&& win(body);
    }
    catch(e){
      fail&& fail(e)
    }
  })
}
 
exports.sendjson = function(res,obj){
 res.writeHead(200,{
   'Content-Type': 'text/json',
   'Cache-Control': 'private, max-age=0'
  });
  var objstr= JSON.stringify(obj);
 util.debug('SENDJSON:'+objstr);
  res.end(objstr );
}
 
// mongo functions
 
var mongodb = require('mongodb')
 
var mongo = {
  mongo:mongodb,
  db: null,
}
 
mongo.init = function( opts, win, fail ){
 util.log('mongo: '+opts.host+':'+opts.port+'/'+opts.name)
 
  mongo.db = 
    newmongodb.Db(
     opts.name, 
      newmongodb.Server(opts.host, opts.port, {}),
     {native_parser:true,auto_reconnect:true});
 
 mongo.db.open(function(){
    if(opts.username ) {
     mongo.db.authenticate(
       opts.username,
       opts.password,
       function(err){
          if(err) {
           fail && fail(err)
          }
         else {
           win && win(mongo.db)
          }
        })
    }
    else {
      win&& win(mongo.db)
    }
  },fail)
}
 
mongo.res = function( win, fail ){
  returnfunction(err,res) {
    if( err ){
     util.log('mongo:err:'+JSON.stringify(err));
      fail&& 'function' == typeof(fail) && fail(err);
    }
    else {
      win&& 'function' == typeof(win) && win(res);
    }
  }
}
 
mongo.open = function(win,fail){
 mongo.db.open(mongo.res(function(){
   util.log('mongo:ok');
    win&& win();
  },fail))
}
 
mongo.coll = function(name,win,fail){
 mongo.db.collection(name,mongo.res(win,fail));
}
 
exports.mongo = mongo

代码片段位于lifestream/server/common.js

这个common.js的更新版本支持MongoDB验证,这需要使用MongoHQ的云数据库服务。

(4) 在文件夹lifestream/server下新建一个名为server.mongo.js的文件,将下面的代码插入到该文件中。

var common  = require('./common.js')
var config  = common.config
var mongo   = common.mongo

var util					= common.util
var connect		= common.connect
var knox					= common.knox
var uuid					= common.uuid
var oauth				= common.oauth
var url						= common.url
var request		= common.request
var Cookies		= common.Cookies


// API functions

function search(req,res){
  var merr = mongoerr400(res)

  mongo.coll(
    'user',
    function(coll){
      coll.find(
        {username:{$regex:new RegExp('^'+req.params.query)}},
        {fields:['username']},
        merr(function(cursor){
          var list = []
          cursor.each(merr(function(user){
            if( user ) {
              list.push(user.username)
            }
            else {
              common.sendjson(res,{ok:true,list:list})
            }
          }))
        })
      )
    }
  )
}

function loaduser(req,res) {
  var merr = mongoerr400(res)

  finduser(true,['username','name','following','followers','stream'],
           req,res,function(user)
  {
    var userout = 
      { username:  user.username,
        name:      user.name,
        followers: user.followers,
        following: user.following,
        stream:     user.stream
      }
    common.sendjson(res,userout)
  })
}


function register(req,res) {
  var merr = mongoerr400(res)

  mongo.coll(
    'user',
    function(coll){

      coll.findOne(
        {username:req.json.username},

        merr(function(user){
          if( user ) {
            err400(res)()
          }
          else {
            var token = common.uuid()
            coll.insert(
              { username:		req.json.username,
                token:						token,
                followers:		[],
                following:		[],
                stream:					[]
              },
              merr(function(){
                common.sendjson(res,{ok:true,token:token})
              })
            )
          }
        })
      )
    }
  )
}


// utility functions

function finduser(mustfind,fields,req,res,found){
  var merr = mongoerr400(res)
  mongo.coll(
    'user',
function(coll){
      var options = {}

      if( fields ) {
        options.fields = fields
      }

      coll.findOne(
        {username:req.params.username},
        options,
        merr(function(user){
          if( mustfind && !user ) {
            err400(res)
          }
          else {
            found(user,coll)
          }
        })
      )
    }
  )
}

function mongoerr400(res){
  return function(win){
    return mongo.res(
      win,
      function(dataerr) {
        err400(res)(dataerr)
      }
    )
  }
}

function err400(res,why) {
  return function(details) {
    util.debug('ERROR 400 '+why+' '+details)
    res.writeHead(400,''+why)
    res.end(''+details)
  }
}


function collect() {
  return function(req,res,next) {
    if( 'POST' == req.method ) {
      common.readjson(
        req,
        function(input) {
          req.json = input
          next()
        },
        err400(res,'read-json')
      )
    }
    else {
      next()
    }
  }
}

function auth() {
  return function(req,res,next) {
    var merr = mongoerr400(res)

    mongo.coll(
      'user',
      function(coll){
      
        coll.findOne(
          {token:req.headers['x-lifestream-token']},
          {fields:['username']},
          merr(function(user){          
            if( user ) {
              next()
            }
            else {
              res.writeHead(401)
              res.end(JSON.stringify({ok:false,err:'unauthorized'}))
            }
          })
        )
      }
    )
  }
}

var db					= null
var server	= null

mongo.init(
  {
    name:							config.mongohq.name,
    host:							config.mongohq.host,
    port:							config.mongohq.port,
    username:			config.mongohq.username,
    password:			config.mongohq.password,
  }, 
  function(res){
    db = res
    var prefix = '/lifestream/api/user/'
    server = connect.createServer(
      connect.logger(),
      collect(),

      connect.router(function(app){
        app.post( prefix+'register', register)
        ,app.get(  prefix+'search/:query', search)
      }),
      auth(),
      connect.router(function(app){
        app.get(  prefix+':username', loaduser)
      })
    )
    server.listen(3009)
  },
  function(err){
    util.debug(err)
  }
)

代码片段位于lifestream/server/server.mongo.js

 

(5) 在文件夹lifestream/server下新建一个名为accept.mongo.js的文件,将下面的代码插入到该文件中。 

var common  = require('./common.js')
var config  = common.config

var util					= common.util  
var request		= common.request  

var assert			= require('assert')
var eyes					= require('eyes')

var urlprefix		= 'http://'+config.server+':3009/lifestream/api'
var headers				= {}

function handle(cb) {
  return function (error, response, body) {
    if( error ) {
      util.debug(error)
    }
    else {
      var code = response.statusCode
      var json = JSON.parse(body)
      util.debug('  '+code+': '+JSON.stringify(json))
      assert.equal(null,error)
      assert.equal(200,code)

      cb(json)
    }
  }
}
function get(username,uri,cb){
  util.debug('GET '+uri)
  request.get(
    {
      uri:uri,
      headers:headers[username] || {}
    }, 
    handle(cb)
  )
}

function post(username, uri,json,cb){
  util.debug('POST '+uri+': '+JSON.stringify(json))
  request.post(
    {
      uri:uri,
      json:json,
      headers:headers[username] || {}
    }, 
    handle(cb)
  )
}


module.exports = {

  api:function() {
    var foo = (''+Math.random()).substring(10)
    var bar = (''+Math.random()).substring(10)


    // create and load

    ;post(
      null,
      urlprefix+'/user/register',
      {username:foo},
      function(json){
        assert.ok(json.ok)
        headers[foo] = {
          'x-lifestream-token':json.token
        }

    ;get(
      foo, 
      urlprefix+'/user/'+foo,
      function(json){
        assert.equal(foo,json.username)
        assert.equal(0,json.followers.length)
        assert.equal(0,json.following.length)


    ;post(
      null,
      urlprefix+'/user/register',
      {username:bar},
      function(json){
        assert.ok(json.ok)
        headers[bar] = {
          ‚x-lifestream-token':json.token
        }

    ;get(
      bar, 
      urlprefix+'/user/'+bar,
      function(json){
        assert.equal(bar,json.username)
        assert.equal(0,json.followers.length)
        assert.equal(0,json.following.length)


    // search
    ;get(
      null,
      urlprefix+'/user/search/'+foo.substring(0,4),
      function(json){
        assert.ok(json.ok)
        assert.equal(1,json.list.length)
        assert.equal(json.list[0],foo)


    ;})  // search
    ;})  // get 
    ;})  // post
    ;})  // get
    ;})  // post
  }
}

代码片段位于lifestream/server/accept.mongo.js

 

这是一个验收测试,用于测试运行中的服务器。每个测试案例都在前一个测试案例的回调函数中运行,要确保测试按顺序运行。

(6) 安装expresso测试框架,需要用它来运行accept.mongo.js脚本。

npm install expresso

(7) 打开一个新的终端窗口,并启动服务器。

node server.mongo.js
21 Mar 13:39:47 - mongo: flame.mongohq.com:27044/lifestream

(8) 打开另一个新的终端窗口,运行验收测试。

expresso accept.mongo.js
DEBUG: POST http://192.168.100.112:3009/
  lifestream/api/user/register: {"username":"707915425"}
DEBUG:   200: {"ok":true,
     "token":"0C257205-AB94-4768-9FCC-A1B1321AD2A5"}
DEBUG: GET http://192.168.100.112:3009/
      lifestream/api/user/707915425
DEBUG:   200: {"username":"707915425",
      "followers":[],"following":[],"stream":[]}
...

服务器和验收测试都会生成调试输出,按顺序显示HTTP请求和响应。

(9) 转到MongoHQ网站,检查user集合的内容。在user集合中,应该看到两个文档

警告:验收测试需要一个真实的网络连接,因为服务器必需和远程的MongoHQ服务通信,以存储和检索数据。这就是它被称为验收测试而非单元测试的原因。根据定义,验收测试要有外部依赖。

 

示例说明

本章中的应用程序是一个完整的应用程序,包含许多不同的功能,它依赖很多npm模块。前面几章已经用过大多数的模块。以前没有用过的模块包括urlrequest cookies模块,这些模块都是处理HTTP请求的辅助模块。

本章还介绍了使用config.js文件存储服务器配置的概念。这只是前面章节中所使用的keys.js文件的扩展。创建用于生产的应用程序时,从实现中分离出配置,并且不在代码中嵌入配置的设置是一个好主意。

在本章的前面注册了MongoHQ,在前面的章节中也应该有AmazonTwitterFacebook的键,可以使用这些键来填写设置。

本章的common.js文件包括了前几章的所有实用功能。这些实用功能让你很容易在HTTP API中处理JSON的请求和响应,以及使用MongoDBAPI。还有一个额外的功能。为了使用MongoHQ服务,需要登录到数据库。可以使用下面的代码。

  mongo.db.open(function(){
    if( opts.username ) {
      mongo.db.authenticate(
        opts.username,
        opts.password,
        function(err){
          ...

为了更便于管理,在本章中添加新功能时,服务器端的代码会存储在单独的server.*.js文件中。该示例的文件名为server.mongo.js。通过本章的介绍可以比较这些文件,以帮助理解。服务器端的代码遵循前面章节中使用的结构。首先,有主要的API函数,然后是一些实用功能,之后是connect模块配置。

在本示例中,实现了搜索函数、用户注册及获取用户详细信息的函数。搜索函数使用MongoDB的正则表达式搜索功能,寻找一个与给定前缀相匹配的用户名。这仅仅是在应用程序中实现用户搜索功能的一个简单方法。代码使用mongoerr400实用函数处理MongoDB出现错误,如果出现问题,将HTTP 400状态码返回给所有客户端。在本节的后面会解释这是如何工作的。下面的代码解释了搜索函数的工作原理。

      coll.find(
        {username:{$regex:new RegExp('^'+req.params.query)}},
        {fields:['username']},
        merr(function(cursor){
          var list = []
          cursor.each(merr(function(user){
            if( user ) {
              list.push(user.username)
            }
            else {
              common.sendjson(res,{ok:true,list:list})
            }

这段代码中的第一行粗体行显示了在MongoDB中如何使用正则表达式查询。它遵循标准的MongoDB查询语法。查询的值是作为HTTP请求的参数提供的,由传递到函数的req对象暴露。

第二行粗体行显示了如何限制从MongoDB结果返回的字段。这样,可以避免返回每个用户的所有数据。如果只是想要匹配的用户名列表,返回数据的所有字段就是资源浪费。

查询的结果作为cursor对象返回。为了使用该对象,要为它的each函数提供一个回调。对于结果集中的每一项,都会调用回调函数。这和传统的SQL数据库游标工作的方式非常相似。当所有的项都被返回之后,将得到一个空(null)的对象,这是停止的信号。if语句检查用户参数是否为空,如果为空,返回JSON结果。否则,它持续追加用户名到JSON结果中。

loaduser函数将大部分的工作交给finduser实用函数。这个函数不返回纯粹的数据库结果,因为这样做可能会公开内部系统的细节,如MongoDBid字段。相反,loaduser函数只返回指定的数据集。以这种方式显式地过滤数据可能看起来有点偏执,但它是一个很好的安全经验法则

继续向下阅读脚本文件,在实用函数部分的finduser实用函数,完成实际到数据库中寻找用户的工作。关键的代码是调用集合对象的findOne函数,执行用户搜索。

      coll.findOne(
        {username:req.params.username},

用户名被指定为HTTP请求的参数。在API使用的URL结构中,对于针对用户的请求,用户名必须是URL路径的一部分。

注册函数与finduser函数非常相似,不同之处在于:如果用户不存在,它会执行一个操作。如果无法找到给定的用户名,说明用户不存在,可以注册。注册是由下面的insert操作执行的。

            var token = common.uuid()
            coll.insert(
              { username: req.json.username,
                token:     token,
                followers:[],
                following:[],
                stream:    []
              },

token是一个特殊的字段,用来验证用户的身份。uuid模块提供了一种方式,可以生成一个长的、随机的、唯一的字符串,特别适合作为令牌。因为这个示例的重点放在构建应用程序,而不是用户管理功能,所以没有实现密码系统。相反,代码采用了一条捷径。注册使用了先到先得的机制。令牌返回到客户端应用程序,客户端永久保存它。此令牌可以用来访问API。实际上它是一个永久的登录令牌。在生产环境中不应使用这种设计,但在这里可以用它模拟用户管理的逻辑,从而演示注册和认证,以及后来与FacebookTwitter的集成。在开发过程中,需要删除全部现有的登录。可以通过删除并重新安装应用程序来实现这一点。如果在浏览器中测试示例,则只需要从本地存储系统中删除user项。

用户名的值来自于标准Node请求对象的json属性,这看起来相当奇怪。json属性是collect实用函数注入请求对象中的自定义属性,它包含了请求提交的任何JSON内容的解析值。collect函数截获HTTPPOST请求,通过使用common.readjson函数取得其内容。

function collect() {
  return function(req,res,next) {
    if( 'POST' == req.method ) {
      common.readjson(
        req,
        function(input) {
          req.json = input
          next()
        },
        err400(res,'read-json')
      )
    }
    else {
      next()
    }
  }
}

collect函数特殊的另一个原因是,它实际上是connect模块中间件函数。中间件函数可以对HTTP请求做一些处理,然后将请求向前传递给其余的服务器。它处于请求的中间,因此而得名。connect模块是中间件函数的堆栈,每个函数都对请求做了一些工作。在前面的章节中,使用标准的router中间件定义自己的URL终点。在本示例中,建立了自己的中间件!

要定义connect中间件函数,需要编写一个函数,使用一些配置参数(collect还没有用到),并返回一个函数。函数接受三个参数:请求、响应和一个特殊的next函数。这是体现JavaScript强大功能的另一个示例:可以使用函数来动态构建另一个函数。

collect中间件函数的实际工作是在动态函数中完成的。检查POST请求,读取JSON信息,并设置req对象的自定义json属性。处理完后,调用特定的next函数。这样connect知道中间件已经完成处理工作,可以将请求传递到下一阶段进行处理。

如果出现错误该如何处理?因为正在建立一个可以独立于应用程序使用的API,所以需要确保很好地遵循HTTP协议。这意味着,如果是因为输入而产生错误,就需要返回一个400 BadRequest的状态代码。可以使用err400实用函数来执行这项任务,该函数创建了一个函数来完成实际工作。这样,就可以为代码不同的部分定义相应的错误消息。此外,mongoerr400函数针对MongoDB的错误创建了一个特殊的错误处理函数。正如在代码中看到的,可以使用这些函数,通过调用它们来为每个顶层API函数创建自定义的错误函数,如下所示。

  var merr = mongoerr400(res)

注意:在本示例中的错误处理代码总是返回400 Bad Request的状态代码。严格地说,如果是因为你而导致的错误(例如,如果数据库连接中断),应该返回一个500 Internal Server Error的状态代码。collect中间件函数不是该服务器代码中唯一的中间件函数。还有一个auth中间件函数用来处理用户身份验证。只有登录的用户才可以调用某些API。这可以防止其他用户访问他人的私人资料。auth中间件函数处于这些API调用之前,用于检查请求是否来自已经登录的用户。这就是为什么代码的connect部分被分成两个路由器部分:第一个是未经验证的动作,如登记和查询;第二个是已通过验证的动作,如获取用户的详细信息或关注其他用户。

auth函数与finduser函数类似,都是通过用户名查找用户。它也需要一个自定义的HTTPX-Lifestream-Token,其中包含的注册令牌必须与用户存储的令牌匹配。如果验证失败,返回HTTP401状态代码,表示这是未经授权的访问。否则,调用next函数,请求开始处理。下面的代码执行令牌搜索。

        coll.findOne(
          {token:req.headers['x-lifestream-token']},
          {fields:['username']},
          merr(function(user){       
            if( user ) {
              next()
            }
            else {
              res.writeHead(401)
              res.end(JSON.stringify({ok:false,err:'unauthorized'}))
            }
          })
        )

最后一部分代码,在创建到MongoDB数据库的连接后,设置了connect中间件堆栈。这些按顺序放在一起的函数实现了API结构的中间件。

不应该只是手动测试该服务器。也应该使用一套标准测试来验证API操作正常与否。可以通过构建验收测试实现这一点。使用Nodeexpresso模块,构建单元和验收测试。虽然应该创建单元测试和验收测试,但该示例的重点是验收测试。单元测试和验收测试之间的区别是什么呢?验收测试依赖于外部资源,而单元测试则不是。为了测试服务器可以正常使用MongoHQ(外部资源)工作,需要验收测试。

验收测试的代码位于accept.mongo.js脚本中。主服务器运行时,在一个单独的终端中运行该脚本。expresso模块将运行,测试任何被放置在特殊exports变量中的函数。在本示例中的代码只有一个主要测试:api函数。此函数包含了一组测试,按顺序运行并执行API的操作。

在此使用了common.js文件以避免重复代码。handlegetpost函数是实用函数,用来跟踪HTTP的请求,当它们到达时输出结果。因此,可以运行测试,看看直接会发生什么,这对于调试是非常有用的。

测试本身是API调用的序列。运行测试时,会注册两个用户,会请求它们的数据,并执行了一个搜索。

    ;post(
      null,
      urlprefix+'/user/register',
      {username:foo},
      function(json){
        ...
        headers[foo] = {
          'x-lifestream-token':json.token
        }

    ;get(
      foo, 
      urlprefix+'/user/'+foo,
      function(json){
        ...

    ;post(
      null,
      urlprefix+'/user/register',
      {username:bar},
      function(json){
        ...
        headers[bar] = {
          'x-lifestream-token':json.token
        }

    ;get(
      bar, 
      urlprefix+'/user/'+bar,
      function(json){
        ...

    // search
    ;get(
      null,
      urlprefix+'/user/search/'+foo.substring(0,4),

使用约定格式化代码,避免了很多恼人的缩进。因为每个测试必须在前一个测试的回调中执行,所以通常会在屏幕右侧结束代码缩进。为了避免这种情况,可以在行开始的地方使用分号(;)字符。这使你能重置缩进级别。要确保正确关闭了所有的括号,这样它们和注释在结尾以相反的顺序列出。还有其他的方法,通过使用各种库来解决这个格式的问题。它们在更复杂的情况是有用的,但在目前这种情况下,有一个简单的线性执行流程与约定,很容易就可以保持代码相对整洁。

 

《移动云计算应用开发入门经典》试读电子书免费提供,有需要的留下邮箱,一有空即发送给大家。 别忘啦顶哦!

  • 4
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值