mongo是mongodb的一个C++写的javascript交互式的可执行客户端。为了分析mongod对于服务请求的响应,这里分析下mongo。先来看看mongo的启动,mongo的启动代码在mongo/shell/dbshell.cpp中。其支持两个javascript 引擎,因为用visual stdio2012调试时默认编译的是spidermonkey,那么就分析spidermonkey那边吧。
当我们使用mongodb时最简单的一个保存命令是db.coll.save({a:1}),我们知道db是javascript对象,也是数据库名,coll是数据集collection,这里将{a:1}保存到coll的集合中,但是这里有一个疑问,javascript对象对于未定义的属性的访问不是通过db[coll]访问的吗,coll这里本应该是undefined,但是为什么结果成功的保存了呢。当我在阅读源码时就有这么个疑问,还好终于从源码中找到了答案,好了废话不多说了,直接分析。
po::options_description shell_options( "options" );//同样是一大堆启动命令,具体含义不说了,自己可以看相关的文档 po::options_description hidden_options( "Hidden options" ); po::options_description cmdline_options( "Command line options" ); po::positional_options_description positional_options; shell_options.add_options() ( "shell", "run the shell after executing files" ) ( "nodb", "don't connect to mongod on startup - no 'db address' arg expected" ) ( "norc", "will not run the \".mongorc.js\" file on start up" ) ( "quiet", "be less chatty" ) ( "port", po::value<string>( &port ), "port to connect to" ) ( "host", po::value<string>( &dbhost ), "server to connect to" ) ( "eval", po::value<string>( &script ), "evaluate javascript" ) ( "username,u", po::value<string>(&username), "username for authentication" ) ( "password,p", new mongo::PasswordValue( &password ), "password for authentication" ) ( "help,h", "show this usage information" ) ( "version", "show version information" ) ( "verbose", "increase verbosity" ) ( "ipv6", "enable IPv6 support (disabled by default)" )
if ( !nodb ) { // connect to db //if ( ! mongo::cmdLine.quiet ) cout << "url: " << url << endl; //如果启动时没有指定nodb,那么这里将生成一串javascript代码,用来连接mongod stringstream ss; if ( mongo::cmdLine.quiet ) ss << "__quiet = true;"; ss << "db = connect( \"" << fixHost( url , dbhost , port ) << "\")"; mongo::shell_utils::_dbConnect = ss.str(); if ( params.count( "password" ) && password.empty() ) password = mongo::askPassword(); if ( username.size() && password.size()) { stringstream ss; ss << "if ( ! db.auth( \"" << username << "\" , \"" << password << "\" ) ){ throw 'login failed'; }"; mongo::shell_utils::_dbAuth = ss.str(); } }
mongo::ScriptEngine::setConnectCallback( mongo::shell_utils::onConnect );//这个回调函数用来做连接成功后的记录工作 mongo::ScriptEngine::setup(); mongo::globalScriptEngine->setScopeInitCallback( mongo::shell_utils::initScope ); auto_ptr< mongo::Scope > scope( mongo::globalScriptEngine->newScope() ); shellMainScope = scope.get();
现在来看看mongo::globalScriptEngine->newScope(),在mongo::ScriptEngine::setup()函数中globalScriptEngine=new SMEngine()。
virtual Scope * newScope() { Scope *s = createScope(); if ( s && _scopeInitCallback )//_scopeInitCallback对应上面的mongo::shell_utils::initScope _scopeInitCallback( *s ); installGlobalUtils( *s ); return s; }createScope对应spideMonkey则为new SMScope()
SMScope() ://spideMonkey的初始化 _this( 0 ), _reportError( true ), _externalSetup( false ), _localConnect( false ) { smlock; _context = JS_NewContext( globalSMEngine->_runtime , 8192 ); _convertor = new Convertor( _context ); massert( 10431 , "JS_NewContext failed" , _context ); JS_SetOptions( _context , JSOPTION_VAROBJFIX); //JS_SetVersion( _context , JSVERSION_LATEST); TODO JS_SetErrorReporter( _context , errorReporter ); _global = JS_NewObject( _context , &global_class, NULL, NULL); massert( 10432 , "JS_NewObject failed for global" , _global ); JS_SetGlobalObject( _context , _global ); massert( 10433 , "js init failed" , JS_InitStandardClasses( _context , _global ) ); JS_SetOptions( _context , JS_GetOptions( _context ) | JSOPTION_VAROBJFIX ); JS_DefineFunctions( _context , _global , globalHelpers );//预先定义的几个函数 JS_DefineFunctions( _context , _convertor->getGlobalObject( "Object" ), objectHelpers ); //JS_SetGCCallback( _context , no_gc ); // this is useful for seeing if something is a gc problem _postCreateHacks(); }
JSFunctionSpec globalHelpers[] = {//这里定义的几个函数映射,javascript中执行相应的函数则会调用这里的本地函数。 { "print" , &native_print , 0 , 0 , 0 } , { "nativeHelper" , &native_helper , 1 , 0 , 0 } , { "load" , &native_load , 1 , 0 , 0 } , { "gc" , &native_gc , 1 , 0 , 0 } , { "UUID", &_UUID, 0, 0, 0 } , { "MD5", &_MD5, 0, 0, 0 } , { "HexData", &_HexData, 0, 0, 0 } , { 0 , 0 , 0 , 0 , 0 } };继续到newScope函数,接下来调用了_scopeInitCallback()函数,其实就是之前设置的initScope回调函数
void initScope( Scope &scope ) { scope.externalSetup(); mongo::shell_utils::installShellUtils( scope ); scope.execSetup(JSFiles::servers);//执行相应的javascript文件,相当于初始化javascript环境,以后命令行中执行相应代码时 scope.execSetup(JSFiles::shardingtest);//就能正确的找到环境了。 scope.execSetup(JSFiles::servers_misc); scope.execSetup(JSFiles::replsettest); scope.execSetup(JSFiles::replsetbridge); if ( !_dbConnect.empty() ) {//执行登录代码 uassert( 12513, "connect failed", scope.exec( _dbConnect , "(connect)" , false , true , false ) ); if ( !_dbAuth.empty() ) {//用户账号登陆认证 installGlobalUtils( scope ); uassert( 12514, "login failed", scope.exec( _dbAuth , "(auth)" , true , true , false ) ); } } }initScope->externalSetup->initMongoJS
void initMongoJS( SMScope * scope , JSContext * cx , JSObject * global , bool local ) { //JS Class的初始化过程,几乎所有的mongodb使用到的class都在这里创建,所有new XXX对象的构建都是下面这里的函数完成的,比如说db,collection,dbquery verify( JS_InitClass( cx , global , 0 , &mongo_class , local ? mongo_local_constructor : mongo_external_constructor , 0 , 0 , mongo_functions , 0 , 0 ) ); verify( JS_InitClass( cx , global , 0 , &object_id_class , object_id_constructor , 0 , 0 , 0 , 0 , 0 ) ); verify( JS_InitClass( cx , global , 0 , &db_class , db_constructor , 2 , 0 , 0 , 0 , 0 ) ); verify( JS_InitClass( cx , global , 0 , &db_collection_class , db_collection_constructor , 4 , 0 , 0 , 0 , 0 ) ); verify( JS_InitClass( cx , global , 0 , &internal_cursor_class , internal_cursor_constructor , 0 , 0 , internal_cursor_functions , 0 , 0 ) ); verify( JS_InitClass( cx , global , 0 , &dbquery_class , dbquery_constructor , 0 , 0 , 0 , 0 , 0 ) ); verify( JS_InitClass( cx , global , 0 , &dbpointer_class , dbpointer_constructor , 0 , 0 , dbpointer_functions , 0 , 0 ) ); verify( JS_InitClass( cx , global , 0 , &bindata_class , bindata_constructor , 0 , 0 , bindata_functions , 0 , 0 ) ); // verify( JS_InitClass( cx , global , 0 , &uuid_class , uuid_constructor , 0 , 0 , uuid_functions , 0 , 0 ) ); verify( JS_InitClass( cx , global , 0 , ×tamp_class , timestamp_constructor , 0 , 0 , 0 , 0 , 0 ) ); verify( JS_InitClass( cx , global , 0 , &numberlong_class , numberlong_constructor , 0 , 0 , numberlong_functions , 0 , 0 ) ); verify( JS_InitClass( cx , global , 0 , &numberint_class , numberint_constructor , 0 , 0 , numberint_functions , 0 , 0 ) ); verify( JS_InitClass( cx , global , 0 , &minkey_class , 0 , 0 , 0 , 0 , 0 , 0 ) ); verify( JS_InitClass( cx , global , 0 , &maxkey_class , 0 , 0 , 0 , 0 , 0 , 0 ) ); verify( JS_InitClass( cx , global , 0 , &map_class , map_constructor , 0 , 0 , map_functions , 0 , 0 ) ); verify( JS_InitClass( cx , global , 0 , &bson_ro_class , bson_cons , 0 , 0 , bson_functions , 0 , 0 ) ); verify( JS_InitClass( cx , global , 0 , &bson_class , bson_cons , 0 , 0 , bson_functions , 0 , 0 ) ); static const char *dbrefName = "DBRef"; dbref_class.name = dbrefName; verify( JS_InitClass( cx , global , 0 , &dbref_class , dbref_constructor , 2 , 0 , bson_functions , 0 , 0 ) ); scope->execCoreFiles(); }
这里local为false,使用的是mongo_external_constructor函数。先看看mongo_class:
JSClass mongo_class = {//为映射javascript对象而设置的函数,当javascript中new Mongo对象时会调用mongo_external_constructor来构造一个对象 "Mongo", // class name JSCLASS_HAS_PRIVATE | JSCLASS_NEW_RESOLVE, // flags JS_PropertyStub, // addProperty JS_PropertyStub, // delProperty JS_PropertyStub, // getProperty JS_PropertyStub, // setProperty JS_EnumerateStub, // enumerate JS_ResolveStub, // resolve JS_ConvertStub, // convert mongo_finalize, // finalize JSCLASS_NO_OPTIONAL_MEMBERS // optional members };再看看 mongo_functions:
JSFunctionSpec mongo_functions[] = {//当在javascript中调用相应的函数时,这里对应的native函数将会被调用到, { "auth" , mongo_auth , 0 , JSPROP_READONLY | JSPROP_PERMANENT, 0 } , { "logout", mongo_logout, 0, JSPROP_READONLY | JSPROP_PERMANENT, 0 }, { "find" , mongo_find , 0 , JSPROP_READONLY | JSPROP_PERMANENT, 0 } , { "update" , mongo_update , 0 , JSPROP_READONLY | JSPROP_PERMANENT, 0 } , { "insert" , mongo_insert , 0 , JSPROP_READONLY | JSPROP_PERMANENT, 0 } , { "remove" , mongo_remove , 0 , JSPROP_READONLY | JSPROP_PERMANENT, 0 } , { 0 } };我们继续看,现在是出来上面的问题了db.coll.save({a:1})为什么没有问题。
JSClass db_collection_class = { "DBCollection", // class name JSCLASS_HAS_PRIVATE | JSCLASS_NEW_RESOLVE, // flags JS_PropertyStub, // addProperty JS_PropertyStub, // delProperty JS_PropertyStub, // getProperty JS_PropertyStub, // setProperty JS_EnumerateStub, // enumerate (JSResolveOp)db_collection_resolve, // 注意这里,这是关键 JS_ConvertStub, // convert JS_FinalizeStub, // finalize JSCLASS_NO_OPTIONAL_MEMBERS // optional members };
JSBool db_collection_resolve( JSContext *cx, JSObject *obj, jsval id, uintN flags, JSObject **objp ) { try { if ( flags & JSRESOLVE_ASSIGNING ) return JS_TRUE; Convertor c( cx ); string collname = c.toString( id ); if ( isSpecialName( collname ) ) return JS_TRUE; if ( obj == c.getGlobalPrototype( "DBCollection" ) ) return JS_TRUE; JSObject * proto = JS_GetPrototype( cx , obj ); if ( c.hasProperty( obj , collname.c_str() ) || ( proto && c.hasProperty( proto , collname.c_str() ) ) ) return JS_TRUE; string name = c.toString( c.getProperty( obj , "_shortName" ) ); name += "."; name += collname; jsval db = c.getProperty( obj , "_db" ); if ( ! JSVAL_IS_OBJECT( db ) ) return JS_TRUE; JSObject * coll = doCreateCollection( cx , JSVAL_TO_OBJECT( db ) , name );//这个地方创建了个集合,当调用db.coll.save()时,javascript调用 if ( ! coll ) //resolve函数时在这里将coll集合创建了出来,所以coll不为undefined return JS_FALSE; //原来如此,spidermonkey的具体流程就不清楚了,有感兴趣的自己去看吧 c.setProperty( obj , collname.c_str() , OBJECT_TO_JSVAL( coll ) ); *objp = obj; } catch ( const AssertionException& e ) { if ( ! JS_IsExceptionPending( cx ) ) { JS_ReportError( cx, e.what() ); } return JS_FALSE; } catch ( const std::exception& e ) { log() << "unhandled exception: " << e.what() << ", throwing Fatal Assertion" << endl; fassertFailed( 16303 ); } return JS_TRUE; }继续回到上面 initMongoJS最后一行: scope->execCoreFiles();
void Scope::execCoreFiles() {//在javascript环境中执行一遍上面文件,然后javascript环境中就会留下那些文件的代码与数据,以后 // keeping same order as in SConstruct//在命令行中执行命令时就能正确执行相应代码了。 execSetup(JSFiles::utils); execSetup(JSFiles::utils_sh); execSetup(JSFiles::db); execSetup(JSFiles::mongo); execSetup(JSFiles::mr); execSetup(JSFiles::query); execSetup(JSFiles::collection); }回到initScope:
if ( !_dbConnect.empty() ) {//执行登录代码 uassert( 12513, "connect failed", scope.exec( _dbConnect , "(connect)" , false , true , false ) ); if ( !_dbAuth.empty() ) {//用户账号登陆认证 installGlobalUtils( scope ); uassert( 12514, "login failed", scope.exec( _dbAuth , "(auth)" , true , true , false ) ); } }
ss << "db = connect( \"" << fixHost( url , dbhost , port ) << "\")";这里就是产生的javascript连接代码:
string fixHost( string url , string host , string port ) {//url传进来初始化值为test,这里就可以看出为啥默认连接到了test数据库了。 //cout << "fixHost url: " << url << " host: " << host << " port: " << port << endl;//若什么都没设置也知道为什么连接到了 //127.0.0.1:27017 if ( host.size() == 0 && port.size() == 0 ) { if ( url.find( "/" ) == string::npos ) { // check for ips if ( url.find( "." ) != string::npos ) return url + "/test"; if ( url.rfind( ":" ) != string::npos && isdigit( url[url.rfind(":")+1] ) ) return url + "/test"; } return url; } if ( url.find( "/" ) != string::npos ) { cerr << "url can't have host or port if you specify them individually" << endl; ::_exit(-1); } if ( host.size() == 0 ) host = "127.0.0.1"; string newurl = host; if ( port.size() > 0 ) newurl += ":" + port; else if ( host.find(':') == string::npos ) { // need to add port with IPv6 addresses newurl += ":27017"; } newurl += "/" + url; return newurl; }下面来看看javascript代码connect
connect = function( url , user , pass ){ chatty( "connecting to: " + url ) if ( user && ! pass ) throw "you specified a user and not a password. either you need a password, or you're using the old connect api"; var idx = url.lastIndexOf( "/" ); var db; if ( idx < 0 ) db = new Mongo().getDB( url );//这里创建了Mongo对象,上面代码分析时已经提到New Mongo()会调用本地函数:mongo_external_constructor else db = new Mongo( url.substring( 0 , idx ) ).getDB( url.substring( idx + 1 ) ); if ( user && pass ){ if ( ! db.auth( user , pass ) ){ throw "couldn't login"; } } return db; }我们回到C++函数:mongo_external_constructor
JSBool mongo_external_constructor( JSContext *cx, JSObject *obj, uintN argc, jsval *argv, jsval *rval ) { try { smuassert( cx , "0 or 1 args to Mongo" , argc <= 1 ); string host( "127.0.0.1" ); Convertor c( cx ); if ( argc > 0 ) host = c.toString( argv[0] ); string errmsg;//注意这里的ConnectionString,其形式可为:server server:port foo/server:port,server:port server,server,server ConnectionString cs = ConnectionString::parse( host , errmsg );//可同时配置多个IP,还记得在QQ群里有个人问,使用replset模式时如 if ( ! cs.isValid() ) {//果主服务器挂了,那么php中怎么配置呢,我不知道php怎么配置, JS_ReportError( cx , errmsg.c_str() ); //但是看到这里大家应该明白了,mongo的连接地址配置其实是可以是 return JS_FALSE; //多地址的,其connection有几种模式具体参见:ConnectionString } shared_ptr< DBClientWithCommands > conn( cs.connect( errmsg ) );//这里通过了ConnectionString屏蔽了具体的连接过程 if ( ! conn ) { JS_ReportError( cx , errmsg.c_str() ); return JS_FALSE; } try { ScriptEngine::runConnectCallback( *conn );//连接成功调用上面说的函数mongo::shell_utils::onConnect记录连接 } catch ( const AssertionException& e ){ // Can happen if connection goes down while we're starting up here // Catch so that we don't get a hard-to-trace segfault from SM JS_ReportError( cx, ((string)( str::stream() << "Error during mongo startup." << causedBy( e ) )).c_str() ); return JS_FALSE; } catch ( const std::exception& e ) { log() << "unhandled exception: " << e.what() << ", throwing Fatal Assertion" << endl; fassertFailed( 16294 ); } verify( JS_SetPrivate( cx , obj , (void*)( new shared_ptr< DBClientWithCommands >( conn ) ) ) ); jsval host_val = c.toval( host.c_str() ); verify( JS_SetProperty( cx , obj , "host" , &host_val ) ); } catch ( const AssertionException& e ) { if ( ! JS_IsExceptionPending( cx ) ) { JS_ReportError( cx, e.what() ); } return JS_FALSE; } catch ( const std::exception& e ) { log() << "unhandled exception: " << e.what() << ", throwing Fatal Assertion" << endl; fassertFailed( 16295 ); } return JS_TRUE; }继续回到initScope,来看看认证过程。
if ( !_dbAuth.empty() ) {//用户账号登陆认证 installGlobalUtils( scope ); uassert( 12514, "login failed", scope.exec( _dbAuth , "(auth)" , true , true , false ) ); }
ss << "if ( ! db.auth( \"" << username << "\" , \"" << password << "\" ) ){ throw 'login failed'; }";通过上面的分析可知这里直接调用了函数mongo_auth。这个函数调用DBClientWithCommands::auth函数执行具体的认证动作。
bool DBClientWithCommands::auth(const string &dbname, const string &username, const string &password_text, string& errmsg, bool digestPassword, Auth::Level * level) { string password = password_text; if( digestPassword ) password = createPasswordDigest( username , password_text );//将用户名密码按照user:mongo:passwd做md5 if ( level != NULL ) *level = Auth::NONE; BSONObj info; string nonce; if( !runCommand(dbname, getnoncecmdobj, info) ) {//执行命令{getnonce:1}从服务端得到一个nonce的值 errmsg = "getnonce fails - connection problem?"; return false; } { BSONElement e = info.getField("nonce"); verify( e.type() == String ); nonce = e.valuestr(); } BSONObj authCmd; BSONObjBuilder b; { b << "authenticate" << 1 << "nonce" << nonce << "user" << username; md5digest d; { md5_state_t st; md5_init(&st); md5_append(&st, (const md5_byte_t *) nonce.c_str(), nonce.size() ); md5_append(&st, (const md5_byte_t *) username.data(), username.length()); md5_append(&st, (const md5_byte_t *) password.c_str(), password.size() ); md5_finish(&st, d); } b << "key" << digestToString( d ); authCmd = b.done(); } if( runCommand(dbname, authCmd, info) ) {//执行命令authenticate验证用户,数据发送到服务端后服务端验证用户通过后会将其加入到 if ( level != NULL ) {//一个map中,其保存了认证的数据库,用户名,以及权限,权限只有读或者写 if ( info.getField("readOnly").trueValue() ) *level = Auth::READ; else *level = Auth::WRITE; } return true; } errmsg = info.toString(); return false; }到这里initscope执行完毕,javascript初始化完毕。进入读取用户输入执行用户输入的javascript代码的流程中,部分代码如下:
bool wascmd = false; { string cmd = linePtr; if ( cmd.find( " " ) > 0 ) cmd = cmd.substr( 0 , cmd.find( " " ) ); if ( cmd.find( "\"" ) == string::npos ) { try {//这里判断用户的输入是否是命令比如说use database,it这种命令 scope->exec( (string)"__iscmd__ = shellHelper[\"" + cmd + "\"];" , "(shellhelp1)" , false , true , true ); if ( scope->getBoolean( "__iscmd__" ) ) {//这里确实是命令,那么就去执行这些命令 scope->exec( (string)"shellHelper( \"" + cmd + "\" , \"" + code.substr( cmd.size() ) + "\");" , "(shellhelp2)" , false , true , false ); wascmd = true; } } catch ( std::exception& e ) { cout << "error2:" << e.what() << endl; wascmd = true; } } } if ( ! wascmd ) { try {//不是确切的命令,按照一般javascript的代码规则执行 if ( scope->exec( code.c_str() , "(shell)" , false , true , false ) ) scope->exec( "shellPrintHelper( __lastres__ );" , "(shell2)" , true , true , false );//打印结果,比如说用户查询 } //结果就是这里打印出来的 catch ( std::exception& e ) { cout << "error:" << e.what() << endl; } } shellHistoryAdd( code.c_str() );//记录这个代码,下次可以使用,用户可以按上下键调出就像linux shell一样。 free( line ); } shellHistoryDone(); }