本文所做的研究基于Node.js v0.12.4 Linux版本。
说段题外话。算上大学跟老师一起做的项目,接触软件应该也有11年了。这期间接触过各类主流和非主流编程语言,自然深深体会到C的精悍、C++ STL的强大、Java的友好、bash的方便快捷、JavaScript的古灵精怪以及万变归宗的汇编和机器码。嗯,还有matlab…… 曾经也在网上因为编程语言的选择跟人争吵,然而人到中年,对编程语言和软件工程的理解确是上了一层楼。当年的真吵其实源自自己理解的肤浅和不自信,人总是会因为底气不足而与人真吵。
编程语言仅是实现软件工程的手段,根本没有优劣之分,只有适不适合。一个软件的成功远不是选对一个实现语言就能决定了的,即使选对了适合的语言对于软件的成功最多也是充分条件,而不是必要条件。一个软件或者一个软件方面的创新能不能快速普及仅仅取决于软件本身的扩散速度、范围和模式。也就是说扩散的意义要远大于创新本身,更远大于支撑这个创新的语言。
很多人莫名崇拜C,C++,鄙视JS。一句”toy language”尽显不屑,非要把JS囚禁在网页前端编程的牢笼里不可,直到Node的出现,JS才被无罪释放出来。其实为什么一定要斗得你死我活而不能彼此和谐共存呢?你看风清扬作为华山气宗一等一高手,一手独孤九剑不也舞得名扬武林么?
喜欢Node是因为它能把JS,C++这两种侧重点完全不同的语言优雅地结合在一起,并且结合操作系统的基础功能充分释放各自的最大优势。如果说JS以前是一门用于实现小功能的语言话,遇上了Node.js后, JS就像小排量发动机装上了涡轮增压器,瞬间变成小怪兽了。还有什么比给别人一个机会去”发现自己的潜能,做最好的自己”更好呢?
言归正传。这是Node.js挖掘的第4篇文章。前三篇,分别描述了Node.js架构、组成Node.js的几个重要英雄好汉以及以建立一个http server为例,讲述从我们自己的JavaScript代码,到Node内建的JS和C++模块,再到最后的libuv的长途奔袭过程。 图1中的步骤1,2,3,4,5展示了这样长途奔袭的路线。该路线开始于我们JS代码 server.listen(),终止于libuv库的uv_listen()。如果把libuv看做是与Node.js独立的项目的话,这个奔袭相当于跨越了3个国境。
图1:建立http server涉及的操作
如果说图1中的5个步骤是一个正向调用过程的话,那么当客户端向此http server发起一个TCP连接时,server端发生的一系列callback调用则是一个逆向调用过程。该逆向过程开始于libuv,终止于我们的JS代码里面设置的JS callback 函数。 本文主要讲述这个逆向过程中发生的事情以及为实现这个逆向调用所做的准备工作。
1. 正向调用发生的事情回顾
让我们先回顾一下正向调用过程中发生的事情,特别是Node.js提供的C++模块 tcp_wrap所做的事情。这样我们能更好地理解逆向过程。 图2所示是创建http server的正向调用过程。该过程大部分的时间花在net.js上,直到最下面红色方框内所标识的关键一步调用 createTCP()。
// constructor for lazy loading
function createTCP() {
var TCP = process.binding('tcp_wrap').TCP;
return new TCP();
}
代码很简单。绑定模块tcp_wrap,并调用TCP constructor。然而这真的简单吗? 表面平静的水面,下面很可能暗潮汹涌。让我们来看看水面下面发生了什么事情。
图2:create http server正向调用序列图
1.1 tcp_wrap模块的准备工作
我们来详细看下TCPWrap::Initialize()所做的工作。为了节省版面,我把代码做了排版,删除了一些与本文主题无关的代码。有强迫症的人可以自己去看源代码。
void TCPWrap::Initialize(Handle<Object> target, Handle<Value> unused, Handle<Context> context) {
Environment* env = Environment::GetCurrent(context);
Local<FunctionTemplate> t = FunctionTemplate::New(env->isolate(), New);
t->SetClassName(FIXED_ONE_BYTE_STRING(env->isolate(), "TCP"));
t->InstanceTemplate()->SetInternalFieldCount(1);
// Init properties
t->InstanceTemplate()->Set(String::NewFromUtf8(env->isolate(), "reading"),Boolean::New(env->isolate(), false));
t->InstanceTemplate()->Set(String::NewFromUtf8(env->isolate(), "owner"),Null(env->isolate()));
t->InstanceTemplate()->Set(String::NewFromUtf8(env->isolate(), "onread"),Null(env->isolate()));
t->InstanceTemplate()->Set(String::NewFromUtf8(env->isolate(),"onconnection"),Null(env->isolate()));
NODE_SET_PROTOTYPE_METHOD(t, "open", Open);
NODE_SET_PROTOTYPE_METHOD(t, "bind", Bind);
NODE_SET_PROTOTYPE_METHOD(t, "listen", Listen);
NODE_SET_PROTOTYPE_METHOD(t, "getsockname", GetSockName);
target->Set(FIXED_ONE_BYTE_STRING(env->isolate(), "TCP"), t->GetFunction());
}
NODE_MODULE_CONTEXT_AWARE_BUILTIN(tcp_wrap, node::TCPWrap::Initialize)
我想说,这真是一段非常优雅的代码。这个函数其实利用v8 engine的function template(函数模版)来模拟创建了一个JS类。类的constructor是 TCP(),类的prototype则由一系列NODE_SET_PROTOTYPE_METHOD()的调用设置,而类的属性设置则由t->InstanceTemplate()->Set()完成。 上面这段代码有几个重要地方值得关注:
- 函数模版t的类名设置成了”TCP”
- 函数模版t的类属性onconnection被设置成了null
- 函数模版t的类实例(对于JS而言,类其实就是函数)由类型为object的target参数带回,从JS角度看,即target的TCP属性被设置成了函数模版t实例化后的函数
- 这个模块对外导出时的名称是”tcp_wrap”, 模块注册函数为TCPWrap::Initialize()。
在net.js的函数createTCP()里调用process.binding(‘tcp_wrap’)时,其实会调用到图3所示的代码截图。在红色高亮方框里,可以看到参数target实际上是被设成了export对象。换句话说,我们的tcp_wrap模块的真正导出函数为TCP,而TCP被设置成了函数模版t的实例化函数。
图3:Binding代码截图
当然这段代码不可能真的生成JS代码,而是在编译时直接变成了机器码。 为了理解方便,下面显示的是模拟生成的JS代码。此处请关注一个重要的部分TCP.onconnection = null。后面会再次回过来分析这句话。
function TCP()
{
/*code*/
}
TCP.prototype.listen = function(){/*code*/};
TCP.prototype.bind = function(){/*code*/};
TCP.prototype.getsockname = function(){/*code*/};
TCP.onconnection = null;
TCP.reading = false;
当在net.js的函数createTCP()里调用net TCP()时,返回了上面函数模版t生的实例函数(或者上面模拟生成的JS代码)的对象。 这是非常重要的一步。因为这步返回的对象会 _listen2函数中被使用。 其实无论是JS语言,还是C++语言,最终都会编译成机器码,供CPU执行。用殊途同归来形容这两个不同的旅程再合适不过。
1.2 Server.prototype._listen2所做的准备工作
结合图2以及1.1节所做的分析,可以看到在函数_listen2中调用createServerHandle()返回了一个重要的对像_handle,如图4所示。
图4:net.js _listen2截图
图4中除了返回_handle这样一个重要的对象外,还有另外一个非常重要的操作: self._handle.onconnection = onconnection; 聪明的你一定想到了在1.1节我提到函数模版t的类属性onconnection被设置成了null。而这个地方将其设置成了真正有效的函数地址。
1.3 AsyncWrap所做的准备工作
图5:AsynWrap和Env代码截图
你一定会好奇我为什么要截一段代码放这里,却什么都不写。其实不是不写,而是未到写的时候,这段截图会在第2节中用到。
行文至此,让我们把上面3个准备工作做一个总结。
图6:准备工作总结
2. callback的逆向调用
终于到我们的正题了。没有办法,准备工作做得太多了。 我们的图2其实仅仅画到了Open()操作。其实如果你仔细看图4中的绿色高亮框里面的代码,会发现在正向调用过程中,还包括了listen()的调用,这个调用最终在TCPWrap::Listen()中实现。
需要强调的是,在正向调用过程中,从createServer()开始,到listen()结束,我们的目的还都是在创建一个基于TCP的http server。如果你熟悉socket编程,一定会知道,到此为止,创建的工作已经完成,剩下要做的是等客户端的连接过来。 本文所提到的所有的callback调用都是基于一个触发事件: 客户端TCP连接。
而这所有的callback执行的目的则是对应用程序开发者构造出一个类型为Socket的对象,并且基于此对象完成面向此连接的数据流读操作。这个数据流读操作由2.3节的callback connectionListener完成。
当数据流读操作完成,connectionListener()会创建出一个req对象,而你一定会知道这个req是什么。 图7描述了这样的过程。
图7:正向和逆向全局图
2.1 第一个callback调用
TCPWrap::Listen()通过libuv提供的函数uv_listen()实现了异步listen调用,并且指定了调用返回callback函数TCPWrap::OnConnection()。 结合图1所示的流程可以看奥OnConnection()由libuv的event loop调用。让我们看看这个callback调用发生了什么事情。
让我们跳过所有与主题无关的代码,直接看最后一行: tcp_wrap->MakeCallback(env->onconnection_string(), ARRAY_SIZE(argv), argv);
看到MakeCallback你一定会想到1.3节的截图。对,图5里面有MakeCallback。但是env->onconnection_string()又是什么呢?
在Node.js Env.h和Env-inl.h中搜索PER_ISOLATE_STRING_PROPERTIES(v)会出现若干引用以及宏定义。如果将其展开,就得到图5中所示的代码。 所以env->onconnection_string()会返回symbole “onconnection”。 而在图5的MakeCall中,通过object()->Get()来获取与symbol对应的函数。如果对C++熟悉的话,你一定会想起来这个地方的object是谁。那么这个地方与symbol对于的函数是谁呢? 答案是:我们在图4中设置的JS callback function _handle.onconnection = onconnection
2.2 第二个callback调用
后面的事情大家都猜到了。net.js中的onconnection()会被调用。我们来看下这个callback。图8是代码截图。
图8: net.js onconnection callback
很显然,这里做了2个重要的事情,一个是创建了Socket对象,另外一个是发出了一个connection事件。 那么这个connection事件是发给谁的呢? 让我们再回到图2。开发者调用createServer()时,其实是在执行 new Server()。而在类Server中对于connection时间,有一个侦听者,那就是connectionListener(),也就是下面要说的第3个callback。
2.3 第三个callback调用
我觉得后面的事情,可以不要再赘述了。
转载本文请注明作者和出处,请勿用于任何商业用途。如需帮助,请QQ联系作者:229848501
干活,给力。
马克
挖掘机系列好给力啊
@luoyjx 谢谢!还会继续写下去。看能不能挖到宝藏
更新了第2个和第3个callback调用。
给力! ps:风清扬是剑宗的
@wangyun122 风清扬的父亲在气宗一直很有身份,所以,从生下来那一天起,风清扬就注定要把练气放在武学里至高无上的地位,这一点,他没有什么自主选择的权利。你说的也对,但风清扬生于气宗,盛于剑宗。
其实,我没怎么弄懂,我还要满满琢磨
@hapiman 加油
从上一篇过来,这篇感觉没太懂。
好文呀。