NodeJS实战之创建服务器

初步构建

桃客侠:NodeJS实战之静态文件服务器9 赞同 · 0 评论文章

首先,回顾上次实战,分析一下最简单的服务器应该实现:1. 接收请求2. 做出响应

第一步,先实现一个简单的http服务器。在文件夹目录下创建server.js文件。代码:

var http = require(http); //引入模块 http.createServer(function(request, response) {//创建http服务 response.writeHead(200, {Content-Type : "text/plain"});//发送http状态 response.write(Hello World);//发送文本 response.end();//完成响应 }).listen(8888); //监听8888端口

通过这几行代码就创建了最简单的NodeJS服务器。

之后,封装代码,使得代码结构化,便于我们进一步编写程序。将server.js也封装成一个模块,比如像http调用createServer的共用方法一样,我们可以将server.js代码放在一个共用方法之中,比如我们在server.js创建一个start方法.

var http = require("http"); function start(){ function onRequest(request, response) { response.writeHand(200, {Content-Type : text/plain}); response.write("Hello World"); response.end(); } http.createServer(onRequest).listen(8888); console.log("Server has started.");//输出信息,显示代码执行情况。 } exports.start = start;

通过exports导出start给其它页面引用。

创建一个index.js文件,处理请求和作出回应.

var server = require("./server"); server.start();

在命令行执行node index.js,就可以在:8888这个网页上看到Hello World;

模块化

这里使用到了模块化。模块化代码使得代码易于管理使用,提高开发效率。使用模块的方法:

require:用于在当前模块中加载和使用别的模块,传入一个模块名,返回一个模块导出对象。模块名可使用相对路径(以./开头),或者是绝对路径(以/或C:之类的盘符开头),可以省略.js后缀.另外,还可以加载和使用一个JSON文件exports:对象是当前模块的导出对象,用于导出模块公有方法和属性。别的模块通过require函数使用当前模块时得到的就是当前模块的exports对象。

导出公有方法

exports.hello = function() { console.log(Hello World!); }module:通过module对象可以访问到当前模块的一些相关信息,但最多的用途是替换当前模块的导出对象。个模块中的JS代码仅在模块第一次被使用时执行一次,并在执行过程中初始化模块的导出对象。之后,缓存起来的导出对象被重复利用。

实现路由

进一步改善服务器,实现服务器根据不同的URL来返回不同的页面,即引入路由功能。

思路:1. 得到用户输入的URL2. 解析URL,对不同的URL分配不同的事件处理方法。3. 对于无法处理的URL,抛出错误

这里使用NodeJS的url模块来解析网址

NodeJS之url模块

引入模块:const url = require(url);url解析网址的方法有:

url.parse(urlString, boolean, boolean)

parse方法可以将一个url字符串解析并返回一个url对象。第二个参数(可省)传入一个布尔值,默认为false,返回的query属性为字符串类型,为true时,返回的url对象中,query属性为一个对象。(不常用)

parse将完整的URL地址,分为很多部分,例如:

:8888/p/a/t?query=string#hash 网址解析后为: host: www.abc.com:8888, port: 8888, pathname: /p/a/t, path: /p/a/t?query=string, query: query=stringurl.format(urlObj)

format这个方法是将传入的url对象编程一个url字符串并返回,参数:urlObj指一个url对象

url.format({ protocol:"http:", host:"127.0.0.1", port:"80" }); /* 返回值: :80 */url.resolve(from,to)

resolve这个方法返回一个格式为"from/to"的字符串,在宝宝看来是对传入的两个参数用"/"符号进行拼接,并返回.

url.resolve("","abc"); /* 返回值: /abc */

了解完url,模块之后,编写router.js代码:

function route(pathname) { console.log("About to route a request for " + pathname);//打印出获取的pathname } exports.route = route;

修改server.js

var http = require("http"); var url = require("url"); function start(route){//添加参数 function onRequest(request, response) { var pathname = url.parse(request.url).pathname;//解析网址 route(pathname); } http.createServer(onRequest).listen(8888); console.log("Server has started.");//输出信息,显示代码执行情况。 } exports.start = start;

修改index.js

var server = require("./server"); var router = require("./router"); server.start(router.route);

事件处理

下一步实现对不同url请求做出不同的处理。编写事件处理模块requestHandlers.js

function start() {//pathname为`/start`的处理事件 console.log("Request handler start was called."); return "Hello Start"; } function upload() {//pathname为`/upload`的处理事件 console.log("Request handler upload was called."); return "Hello Upload"; } exports.start = start; exports.upload = upload;

修改index.js文件

var requestHandlers = require("./requestHandlers"); //获取事件处理模块 var handle = {};//创建处理事件的数组 //对不同网址,采取不同的处理。 handle["/"] = requestHandlers.start; //默认调用start函数进行处理 handle["/start"] = requestHandlers.start; handle["/upload"] = requestHandlers.upload; //pathname为`/upload`的采用upload函数处理 server.start(router.route, handle);

将handle作为我们地关联数组对象,声明三个来触发相对应地事件处理程序,这样的好处是不用因为新的url或请求程序而重复编写

修改server.js,使得server的start中使用route函数来处理handle。

#server.js文件 function start(route, handle) {//传入新参数handle function onRequest(request, response) { response.writeHead(200, {"Content-Type": "text/plain"}); var content = route(handle, pathname);//使用新参数handle response.write(content); } }

router.js执行事件处理,并判断传递的url是否需要处理,如果不需要,则报出错误。

#router.js文件 function route(handle, pathname) {//添加新参数handle console.log("About to route a request for " + pathname); if (typeof handle[pathname] === function) {//判断事件处理方法是否存在. return handle[pathname]();//调用处理函数 } else { console.log("No request handler found for " + pathname);//提示错误 return "404 Not found"; } } exports.route = route;

测试网址及结果为:

:8888/start //显示Hello Start :8888/upload //显示Hello Upload :8888/foo//显示404 Not found

项目结构为:

- /NodeJS-Server/ # 工程目录 server.js # 创建服务器的模块 router.js # 路由模块 requestHandlers.js# 事件处理模块 index.js# 初始化模块 + tmp/# 存放上传文件的临时目录

阻塞

现在,NodeJS服务器已经初步构建。但仍存在隐患,由于NodeJS是单线程,可以在不新增额外线程的情况下对任务进行并行处理,如果编写的代码里含有阻塞操作,就可能阻塞所有其他代码的处理工作,造成延迟,影响体验。

例如:修改requestHandler.js的start方法

function start() { console.log("Request handler start was called."); function sleep(milliSeconds) { //通过循环实现延迟执行代码,达到休眠的效果 var startTime = new Date().getTime();//获取当前时间 //添加循环,更新时间,如果小于当前时间+millisSeconds所表示的时间,则循环 while (new Date().getTime() < startTime + milliSeconds); } sleep(10000); //休眠10秒 return "Hello Start"; }

这时,在访问/start的时候,在新的网页访问/upload,会发现/upload也延迟了近10秒。这就是由于Node单线程的原因造成的。

解决

因为nodejs通过事件轮询(event loop)来实现并行操作,我们应该要充分利用这一点: 尽可能的避免阻塞操作,取而代之,多使用非阻塞操作。

这里采用child_process模块来处理阻塞。

NodeJS的child_process模块

child_process模块给予node任意创建子进程的能力,node官方文档对于child_proces模块给出了四种方法,都是在操作系统上创建子进程。

child_process.exec(command[, options][, callback]) 启动

子进程来执行shell命令,可以通过回调参数callback来获取脚本shell执行结果

child_process.execfile(file[, args][, options][, callback])

与exec类型不同的是,它执行的不是shell命令而是一个可执行文件

child_process.spawn(command[, args][, options])仅仅执行一个shell命令,不需要获取执行结果child_process.fork(modulePath[, args][, options])

可以用node执行的.js文件,也不需要获取执行结果。fork出来的子进程一定是node进程

exec()与execfile()在创建的时候可以指定timeout属性设置超时时间,一旦超时会被杀死如果使用execfile()执行可执行文件,那么头部一定是#!/usr/bin/env node

理解了child_process模块后,开始修改服务器代码。修改server.js中的onRequest方法:

function onRequest(request, response) { var pathname = url.parse(request.url).pathname; console.log("Request for " + pathname + " received."); route(handle, pathname, response);//添加参数response }

将response对象(从服务器的回调函数onRequest()获取)通过请求路由传递给请求处理程序。随后,处理程序就可以采用该对象上的函数来对请求作出响应。也就是将所有有关response函数的调用都移除,转交给route函数完成。这么做的目的是:保证并行处理后,能及时response回结果,避免因为并行而导致代码执行顺序发生变化。

修改router.js文件

function route(handle, pathname, response) { console.log("About to route a request for " + pathname); if (typeof handle[pathname] === function) { handle[pathname](response); //传递参数response给handle处理 } else {//处理response console.log("No request handler found for " + pathname); response.writeHead(404, {"Content-Type": "text/plain"}); response.write("404 Not found"); response.end(); } } exports.route = route;

修改requestHandler.js文件的start函数和upload函数

var exec = require(child_process).exec;//引入模块 function start(response) { console.log("Request handler start was called."); exec("ls -lah", { timeout: 10000, maxBuffer: 20000*1024 }, function (error, stdout, stderr) { response.writeHead(200, {"Content-Type": "text/plain"}); response.write(stdout); response.end(); });//添加参数验证是否会阻塞 } function upload(response) { console.log("Request handler upload was called."); response.writeHead(200, {"Content-Type": "text/plain"}); response.write("Hello Upload"); response.end(); }

处理表单

进一步给服务器添加处理表单的功能,包括:提交表单数据,并在浏览器中返回。

步骤:1. 通过表单的textarea,以POST方式提交数据给服务器处理。2. 将表单展示交给requestHandlers.js的start处理。3. 通过response.write()生成表单

先修改requestHandler.js的start方法(去掉操作阻塞代码):

function start(response) { console.log("Request handler start was called."); var body = <html> + <head>+ <meta http-equiv="Content-Type" content="text/html; + charset=UTF-8" />+ </head>+ <body>+ <form action="/upload" method="post">+ <textarea name="text" rows="20" cols="60"></textarea>+ <div></div>+ <input type="submit" value="Submit text" />+ </form>+ </body>+ </html>;//表单代码 response.writeHead(200, {"Content-Type": "text/html"}); response.write(body); //写入表单 response.end(); }

当提交表单时,触发/upload请求处理表单发送的POST请求。 为了使整个过程非阻塞,nodejs会将POST数据拆分成很多小的数据块,然后通过触发特定的事件,将这些小数据块传递给回调函数。

服务器的request会接受到POST的信息并进行处理,处理方式通过注册监听器(listener)实现修改server.js的onRequest函数:

function onRequest(request, response) { var pathname = url.parse(request.url).pathname; var postData = "";//创建字符串,用于接受post的信息 request.setEncoding("utf8"); //设置编码,防止乱码 //data事件用于收集每次接收到的新数据块 request.addListener("data", function(postDataChunk) { postData += postDataChunk; console.log("Received POST data chunk "+ postDataChunk + "."); }); //end事件负责将请求路由的调用移到end事件处理程序中,以确保它只会当所有数据接收完毕后才触发,并且只触发一次。 request.addListener("end", function() { route(handle, pathname, response, postData); }); }

修改相应的路由router.js代码:

function route(handle, pathname, response, postData) {//新加postData参数 console.log("About to route a request for " + pathname); if (typeof handle[pathname] === function) { handle[pathname](response, postData); //传递参数response和postData给handle处理 } else {//处理response console.log("No request handler found for " + pathname); response.writeHead(404, {"Content-Type": "text/plain"}); response.write("404 Not found"); response.end(); } }

最后,修改requestHandler.js里的upload函数

function upload(response, postData) {//新加postData参数 response.write("Youve sent:" + postData); response.end(); }

postData其实包含了很多其他的信息(如html文本),但是我们只是想对form表单的文本(text)进行处理,所以这里又可以使用一个NodeJS的模块来解析了:querystring。

NodeJS之querystring模块

querystring作用是查询字符串,一般是对http请求所带的数据进行解析。使用前,先使用var querystring = require("querystring");

常用方法:

querystring.parse(str,separator,eq,options)将一个字符串反序列化为一个对象.querystring.parse("name=whitemu&sex=man&sex=women"); //return: //{ name: whitemu, sex: [ man, women ] }querystring.stringify将一个对象序列化成一个字符串,与querystring.parse相对。querystring.escape(str):escape可使传入的字符串进行编码querystring.escape("name=慕白"); //return: //name%3D%E6%85%95%E7%99%BDquerystring.unescape(str):将含有%的字符串进行解码querystring.unescape(name%3D%E6%85%95%E7%99%BD); //return: //name=慕白

最后,在修改requestHandlers为

var querystring = require("querystring"); function upload(response, postData) {//新加postData参数 response.write("Youve sent:" + querystring.parse(postData).text); response.end(); }

上传结果:

图片上传

接在实现网站的图片上传功能。上传图片涉及到文件操作,需要导入

vaf fs = require(fs);

通过/show路由来展示图片,通过硬编码的方式将上传的图片展示在浏览器中。

首先,在requestHandlers.js中编写show方法:

function show(response) { console.log("Request handler show was called."); fs.readFile("./tmp/test.png", "binary", function(error, file) {//读取图片 if (error) {//回调函数出错时网页显示错误。 response.writeHead(500, {"Content-Type": "text/plain"}); response.write(error + "\n"); response.end(); } else {//读取成功时写入到网站上 response.writeHead(200, {"Content-Type": "image/png"}); 指定类型为image/png response.write(file, "binary");//写入方式是binary response.end(); } }); } exports.show = show;

在index.js中注册路由:

handle["/show"] = requestHandlers.show;

这样就实现了实现图片展示。

接下来要快速实现图片上传功能。

首先,修改requestHandlers.js里的start函数里的表单

function start(response) { console.log("Request handler start was called."); var body = <html>+ <head>+ <meta http-equiv="Content-Type" + content="text/html; charset=UTF-8" />+ </head>+ <body>+ <form action="/upload" enctype="multipart/form-data" +//定义支持上传的enctype method="post">+ <input type="file" name="upload">+//指定type属性为file,上传文件 <input type="submit" value="Upload file" />+ </form>+ </body>+ </html>; response.writeHead(200, {"Content-Type": "text/html"}); response.write(body); response.end(); }

之后,修改server.js文件

function onRequest(request, response) { var pathname = url.parse(request.url).pathname; console.log("Request for " + pathname + " received."); route(handle, pathname, response, request);//注意此处传递request参数 }

之后,修改router.js文件

function route(handle, pathname, response, request) { if (typeof handle[pathname] === function) { handle[pathname](response, request);//修改参数为request } else { // 此处省略没有修改的代码 } }

使用node-formidable模块会将上传的文件保存到本地/tmp目录中

var formidable = require("./formidable");//引入模块

修改requestHandler.js里的upload函数

var form = new formidable.IncomingForm(); function upload(response, request) {//新加postData参数 // 实例化一个formidable.IncomingForm; console.log("about to parse"); form.uploadDir = "tmp"; // 指定上传目录 form.parse(request, function(error, fields, files) { // parse负责解析文件 console.log("parsing done"); fs.renameSync(files.upload.path, "./tmp/test.png"); // fs模块的renameSync进行重命名 response.writeHead(200, {"Content-Type": "text/html"}); response.write("received image:<br/>"); response.write("<img src=/show />"); // 使用img 标签来显示图片 ,因为show方法会返回一张图片 response.end(); }); }

实现功能!

参考资料

Node入门

node child_process模块学习笔记

nodejs之querystring模块

轻松创建 Node.js 服务器