I code it

Code and Life

第15章 服务器端的JavaScript(上)

15.1 node.js

15.1.1 node.js简介

Node.js是一个基于Google V8 JavaScript引擎的框架,它的设计目标为提供一个构建快速,可伸缩网络应用程序的平台。Node.js是一个基于事件,非阻塞I/O的框架,可以很好的处理高并发的断业务。这个在每个平台,每个语言中均有类似的对应,如C语言中的libevent/libev,python语言中的twisted等。

基于事件+非阻塞是保证Node保持高速的关键所在,与客户端的JavaScript类似,通过注册事件处理器来确保当关心的事件发生时进行回调,这样主线程就进入了MainLoop。这样可以仅通过一个线程就处理大量的并发。但是这种模式的限制在于,每个事件的处理器不能太耗时,否则其他的事件要么排队等待,要么会丢失。

基于事件+非阻塞IO的流程大致如下图,当有数据可读的时候,select将返回可读/写的描述符,然后才开始真正的read/write操作:

clip_image002

图 多路复用模式示意图

这种模型在监听多个客户端描述符时,会非常的高效,且占用的系统资源非常少。node.js支持扩展。由于实现了CommonJS的一些规范,node.js支持模块的require和exports,从而使得代码更加模块化,更易于管理。

15.1.2 node.js 使用示例

我们可以通过一些简单的实例来查看Node如何提高开发服务器的效率,事实上,使用Node来编写一个简单的echo服务器,仅需要6行代码:
 1: var net = require("net");
 2:
 3: var server = net.createServer(function(socket){
 4:     socket.write("Echo server\n");
 5:     socket.pipe(socket);
 6: });
 7:
 8: server.listen(8580, "10.111.43.117");
 

我们在8580端口启动一个socket服务器,它将客户端的请求原封不动的返回:

 1: [juntao@rd117 ~]$ telnet 10.111.43.117 8580
 2: Trying 10.111.43.117...
 3: Connected to rd117 (10.111.43.117).
 4: Escape character is '^]'.
 5: Echo server
 6: hello, echo
 7: hello, echo
 8: Are you really node?
 9: Are you really node?
 10: bye
 11: bye
 12: ^]
 13: telnet> quit
 14: Connection closed.
 

加粗的为服务器端返回的数据。require是node中的包管理机制,net是一个内置的模块,用以提供原生的socket访问。net的createServer接受一个参数,其类型为一个函数,当有连接到达时,node会调用这个函数。

下面是一个简单的http服务器,当接收到客户端请求之后,返回200及一个字符串”hello,world”。

 1: var http = require("http");
 2:
 3: http.createServer(function(request, response){
 4:     response.writeHead(200, {"Content-Type":"text/plain"});
 5:     response.end("hello, world\n");
 6: }).listen(9527, "10.111.43.117");
 7:
 8: console.log("http server running at http://10.111.43.117:9527");
 

运行效果如下:

clip_image004

console是一个全局的对象,console.log用以在控制台上打印日志信息。通过log,我们可以在后台监控客户端传递的数据。

最后来看一个传输文件的Node脚本:

 1: var http = require("http"),
 2: url = require("url"),
 3: path = require("path"),
 4: fs = require("fs");
 5:
 6: http.createServer(function(request, response) {
 7: var uri = url.parse(request.url).pathname;
 8: var filename = path.join(process.cwd(), uri);
 9:
 10: path.exists(filename, function(exists) {
 11:     if(!exists) {
 12:         response.writeHead(404, {"Content-Type": "text/plain"});
 13:         response.write("404 Not Found\n");
 14:         return;
 15:     }
 16:
 17:     fs.readFile(filename, "binary", function(err, file) {
 18:         if(err) {
 19:             response.writeHead(500, {"Content-Type": "text/plain"});
 20:             response.write(err + "\n");
 21:             return;
 22:         }
 23:
 24:         response.writeHead(200);
 25:         response.write(file, "binary");
 26:         response.end();
 27:     });
 28: });
 29: }).listen(8080);
 30:
 31: console.log("File Server running at http://localhost:8080/");
 

其中使用到了很多Node的模块:url,path,http,fs。如果文件不存在,则返回404,如果读文件出错,则返回500,否则将文件内容写入response,并终止连接。这个http服务器的根为process.cwd(),即进程启动的目录,这样我们可以在另一个终端中,通过curl来请求上例中的echo.js:

 1: [juntao@rd117 ~]$ curl http://10.111.43.117:8080/echo.js 
 2: var net = require("net");
 3:
 4: var server = net.createServer(function(socket){
 5:         socket.write("Echo server\n");
 6:         socket.pipe(socket);
 7: });
 8:
 9: server.listen(8580, "10.111.43.117");
 10:
 

事实上,使用Node作为一个间接层,可以极大的提高服务器的并发能力,将耗时的操作(数据库,I/O等)作为独立的进程。然后使用Node的高并发能力将数据存入一个队列,并通知该独立进程进行耗时的操作。这种模式事实上是将单线程-事件模型-非阻塞与多线程结合起来,实践证明,这种模式是处理高并发,频繁业务很好的方式。

下图是一个典型的场景示意图:

clip_image006

再来看一个node.js中模块的使用,比如我们建立了一个模块,名叫producter.js,其中定义了一个变量和一个函数,然后想要在另一个模块customer.js中访问这个变量和函数。

producter.js中,定义了pname和pfunc,然后通过node.js提供的exports来将其公开:

 1: var pname = "javascript-core";
 2:
 3: function pfunc(){
 4:     return "this is javascript-core";
 5: }
 6:
 7: exports.pname = pname;
 8: exports.pfunc = pfunc;
 

然后在customer.js中,使用require来将这个模块导入:

 1: var producter = require("./producter.js");
 2:
 3: console.log(producter.pname);
 4: var s = producter.pfunc();
 5: console.log(s);
 

运行结果如下:

 1: javascript-core
 2: this is javascript-core
15.1.3 node.js实例

在这个小节中,我们将实现一个使用node.js开发的HTTP服务器,这个服务器提供REST方式的API来对客户端的数据进行处理,简单起见,这个服务器仅处理POST的数据,同时,这个服务器仅处理三种字符串处理操作:

  • 回显(echo),将客户端发送的字符串原封不动的返回
  • 转大写(upper),将客户端发送的字符串转换为大写并返回
  • 转小写(lower),将客户端发送的字符串转换为小写并返回
访问方式为:
 1: curl -X POST http://10.111.43.117:8080/upper -d "hello, world"
 

该服务器将返回”HELLO, WORLD”。

我们建立两个模块,httpserver.js负责接受请求,并根据请求类型分发;而actions.js则负责实际的请求处理(字符串转换)。

httpserver.js的原型如下,加载node.js内置的http模块,然后调用createServer,并在8080端口监听:

 1: var http = require("http");
 2:
 3: http.createServer(function(request, response){
 4:     response.writeHead(200, {"Content-Type":"text/plain"});
 5:     response.write("httpserver");
 6:     response.end();
 7: }).listen(8080);
我们可以将createServer中的匿名函数抽取出来,使得代码更为简洁:
 1: var http = require("http");
 2:
 3: function handler(request, response){
 4:     response.writeHead(200, {"Content-Type":"text/plain"});
 5:     response.write("httpserver");
 6:     response.end();
 7: }
 8:
 9: http.createServer(handler).listen(8080);
然后我们就可以放心的改造handler函数了,首先我们需要接受客户端发送的请求,这个在node.js中很容易实现,为request添加两个监听器:对于数据到达的事件,node会触发”data”事件,当数据传输完成之后,会触发”end”事件。
 1: function handler(request, response){
 2:     var data = "";
 3:
 4:     request.addListener("data", function(chunk){
 5:         data += chunk;
 6:     });
 7:
 8:     request.addListener("end", function(){
 9:         //invoke the real action handler
 10:     });
 11: }
当data事件触发后,陆续的将接收到的数据拼接在data变量中,当end事件触发时,我们就可以将数据传递给真实的处理函数来完成了。

客户端请求的URL格式为:http://host:ip/path,我们可以通过node.js的url模块来解析这个path,这样我们就得到的操作类型。

 1: var url = require("url");
 2:
 3: function handler(request, response){
 4:     var data = "";
 5:     var path = url.parse(request.url).pathname;
 6:
 7:     request.addListener("data", function(chunk){
 8:         data += chunk;
 9:     });
 10:
 11:     request.addListener("end", function(){
 12:         //RESUful
 13:         if(path == ""){
 14:             //do something
 15:         }else if(path == ""){
 16:             //do something else
 17:         }else{
 18:             //error
 19:         }
 20:     });
 21: }
 

然后,在end事件触发时,我们可以根据请求名称来进行分发。但是这样的写法并不好,如果请求类型变得很大的时候,代码中会有很多的if-else-if判断,不但代码会很丑陋,而且效率会降低。于是我们将定义一个请求名称和请求处理的映射表,当客户端的请求和映射表中的键匹配时,就调用这个键对应的值(这个值是一个函数):

 1: var error = function(path, response){}
 2: var echo = function(data, response){}
 3: var upper = function(data, response){}
 4: var lower = function(data, response){}
 5:
 6: var map = {
 7:     "/echo"  : echo,
 8:     "/upper" : upper,
 9:     "/lower" : lower,
 10:     "error" : error
 11: };
我们将这个映射表放在actions.js模块中,并将map暴露出来,然后在httpserver.js中require它即可。这样在end事件的处理函数中,就变得非常简单了:
 1: request.addListener("end", function(){
 2:     if(path in map){
 3:         map[path](data, response);
 4:     }else{
 5:         map["error"](path, response);
 6:     }
 7: });
这时,我们的handler就有点名不副实了,它不负责处理具体的请求,仅仅是做分发,因此更名为dispatch,这样我们的httpserver.js就已经开发完成了:
 1: var http = require("http");
 2: var url = require("url");
 3: var actions = require("./actions");
 4:
 5: map = actions.map;
 6:
 7: function dispatch(request, response){
 8:     var data = "";
 9:     var path = url.parse(request.url).pathname;
 10:
 11:     request.addListener("data", function(chunk){
 12:         data += chunk;
 13:     });
 14:
 15:     request.addListener("end", function(){
 16:         //RESUful
 17:         if(path in map){
 18:             map[path](data, response);
 19:         }else{
 20:             map["error"](path, response);
 21:         }
 22:     });
 23: }
 24:
 25: http.createServer(dispatch).listen(8080);
 

而actions.js中,我们的业务极为简单,只是对字符串进行大小写转换:

 1: var error = function(path, response){
 2:     response.writeHead(500, {"Content-Type" : "text/plain"});
 3:     response.write("no such action handler for "+path);
 4:     response.end();
 5: }
 6:
 7: var echo = function(data, response){
 8:     response.writeHead(200, {"Content-Type" : "text/plain"});
 9:     response.write(data);
 10:     response.end();
 11: }
 12:
 13: var upper = function(data, response){
 14:     response.writeHead(200, {"Content-Type" : "text/plain"});
 15:     response.write(data.toUpper());
 16:     response.end();
 17: }
 18:
 19: var lower = function(data, response){
 20:     response.writeHead(200, {"Content-Type" : "text/plain"});
 21:     response.write(data.toLower());
 22:     response.end();
 23: }
 24:
 25: var map = {
 26:     "/echo"  : echo,
 27:     "/upper" : upper,
 28:     "/lower" : lower,
 29:     "error" : error
 30: };
 31:
 32: exports.map = map;
运行服务器:
 1: node httpserver.js
 

 

然后使用crul进行测试:

 1: $ curl -X POST http://10.111.43.117:8080/upper -d "hello, world"
 2: HELLO, WORLD
我们可以很容易的对这个小型的服务器进行扩展,以支持更多的action。

Comments