首页 诗词 字典 板报 句子 名言 友答 励志 学校 网站地图
当前位置: 首页 > 教程频道 > 服务器 > 云计算 >

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

2013-01-02 
移动云计算中开发和测试用户注册服务器MongoHQ服务是类于Amazon S3的云服务,只不过它专注于在云中托管Mong

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

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

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

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

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

npm install connectnpm install mongodbnpm install knox  npm install uuid  npm install oauth npm install url npm install requestnpm 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.configvar mongo   = common.mongovar util= common.utilvar connect= common.connectvar knox= common.knoxvar uuid= common.uuidvar oauth= common.oauthvar url= common.urlvar request= common.requestvar Cookies= common.Cookies// API functionsfunction 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 functionsfunction 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= nullvar server= nullmongo.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.configvar 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.js21 Mar 13:39:47 - mongo: flame.mongohq.com:27044/lifestream

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

expresso accept.mongo.jsDEBUG: 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/707915425DEBUG:   200: {"username":"707915425",      "followers":[],"following":[],"stream":[]}...

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

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

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

 

示例说明

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

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

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

本章的common.js文件包括了前几章的所有实用功能。这些实用功能让你很容易在HTTP API中处理JSON的请求和响应,以及使用MongoDB的API。还有一个额外的功能。为了使用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实用函数。这个函数不返回纯粹的数据库结果,因为这样做可能会公开内部系统的细节,如MongoDB的id字段。相反,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。实际上它是一个永久的登录令牌。在生产环境中不应使用这种设计,但在这里可以用它模拟用户管理的逻辑,从而演示注册和认证,以及后来与Facebook和Twitter的集成。在开发过程中,需要删除全部现有的登录。可以通过删除并重新安装应用程序来实现这一点。如果在浏览器中测试示例,则只需要从本地存储系统中删除user项。

用户名的值来自于标准Node请求对象的json属性,这看起来相当奇怪。json属性是collect实用函数注入请求对象中的自定义属性,它包含了请求提交的任何JSON内容的解析值。collect函数截获HTTP的POST请求,通过使用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函数类似,都是通过用户名查找用户。它也需要一个自定义的HTTP头X-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操作正常与否。可以通过构建验收测试实现这一点。使用Node的expresso模块,构建单元和验收测试。虽然应该创建单元测试和验收测试,但该示例的重点是验收测试。单元测试和验收测试之间的区别是什么呢?验收测试依赖于外部资源,而单元测试则不是。为了测试服务器可以正常使用MongoHQ(外部资源)工作,需要验收测试。

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

在此使用了common.js文件以避免重复代码。handle、get和post函数是实用函数,用来跟踪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),

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

 

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

热点排行