初级node http server development

也许,你已经高频屡次听到了node。毕竟它真的很火。但是你还在犹豫,毕竟,学习一门语言以及库,是一个开坑和被坑的过程。也担忧学习后不知道能够作点什么。javascript

我也和你同样。通过半年的学习,阅读了很多代码,我试图以此文,引导你作一个http server。php

东西成了,学习也就成了。html

安装

安装node,在windows/mac 上很是简单,和其余应用软件也没有什么区别:下载安装包,而后执行,遵从它的指示,一步步的走。完成后,在command line输入命令:java

$node -v
  v0.12.4

看到版本号?成功。版本号的话,偶数(偶数是稳定版,奇数是开发版)就好,大点就好。node

Linux 上复杂点。不过这和咱们的内容关系不大。能够看官方的安装指南。本身消化下。git

Hello World

Hello world 太多,但是初学者都喜欢。因此,我老着脸,就再来一个。程序员

建立一个helloworld.js文件(哦,我爱sublime text)。代码:github

console.log("Hello World");

保存文件,到command line执行:golang

node helloworld.js

正常的话,就会在终端输出Hello World 。ajax

选择一个叫作“简洁”的角度闻过去,有点c的味道,比c的味道更浓。你看,不须要#include,不须要main{}。
也不须要设置环境变量。关于最后一条,java,golang两位同窗,我没有针对你。

我喜欢这种一点点多余的泡泡肉也没有的感受。

不想重复的node app.js ?

输入node app.js ,ctrl+c ,而后一百遍的重复,以便重写测试代码。这样的输入一天下来也真是厌倦。若是你和我同样,那么 nodemon 能够帮忙你。

它会监视当前目录,若是发现代码有修改,就会自动重启代码。

npm i nodemon 
nodemon app.js

npm i表示从npm仓库安装nodemon。npm是node社区一位领袖建立,依我看是目前最好的模块系统。模块数量也是主流脚本中数量最高的。虽然这不表明质量,可是说明门槛低,方便,你们所以愿意提交模块。npm内置,简单,极其方便,算得上node的一大特点。

而后修改你的app.js ,会发现nodemon自动运行app.js 。

个人双显示器正好派上用场。一块运行nodemon,另一块做为编辑器的工做台,编写个人app.js,而后save。这个小小的机器人不厌其烦的检测file save->重启app.js->显示错误(甚至app.js也crash。固然nodemon不会所以也crash)->待你修正保存。直接正确为止。

虽然功能简单,可是恰如其分,一个好工具。

当我准备好代码app.js

console.log("hi")

而后nodemon app.js ,能够看到输出:

6 Jul 08:45:12 - [nodemon] v1.3.7
6 Jul 08:45:12 - [nodemon] to restart at any time, enter `rs`
6 Jul 08:45:12 - [nodemon] watching: *.*
6 Jul 08:45:12 - [nodemon] starting `node app.js`
hi
6 Jul 08:45:12 - [nodemon] clean exit - waiting for changes before restart

打印了hi。这时我想要改下代码,输出点具体的:

console.log("hi,node")

在保存,就能够看到:

6 Jul 08:47:17 - [nodemon] restarting due to changes...
6 Jul 08:47:17 - [nodemon] starting `node app.js`
hi,node
6 Jul 08:47:17 - [nodemon] clean exit - waiting for changes before restart

你看,我不须要在本身执行node app.js ,它会执行后等待变化,而后启动。
即便我改变代码为:

process.exit(0)

也不会整体退出:

6 Jul 08:51:22 - [nodemon] restarting due to changes...

6 Jul 08:51:22 - [nodemon] starting `node app.js`
6 Jul 08:51:22 - [nodemon] clean exit - waiting for changes before restart

虽然感受稍微慢了点,总比我编码快,够用了。

异步来了

要是想要启动后延时1秒在say hi,怎么办?

function hi(){console.log("hi")}
setTimeout(hi,1000)

setTimeout是一个全局函数,文档这样说明它的规格:

setTimeout(callback, delay[, arg][, ...])#

第一个参数,名字为callback,做为js的文档约定,说明此参数能够是一个函数。咱们能够把函数做为变量传递给SetTimeout。这里传递的不是hi的结果,而是hi 自己!setTimeout会在它的实现内调用它。

还能够简洁。hi这个名字的存在不太必要,咱们能够在应用hi的地方,直接定义这个函数:

setTimeout(function(){console.log("hi")},1000)

这个函数定义存在,功能可用,可是无名。它就是“匿名函数"。

一个函数能够做为变量传递给另外一个函数。咱们能够先定义一个函数,而后传递,也能够在传递参数的地方直接定义函数。

简洁还在。可是有了callback,感受稍微不太同样了,特别是和php等相比。

当setTimeout执行时,1s后会打印,那么<1s的时间,在干啥?等待。内部实现来讲,node会把这个hi做为callback排到队列内。当道setTimeout的时间一到就会触发callback的执行。

setTimeout(function(){console.log("hi")},1000)
console.log("ready")

输出:

ready
  hi

这个期间,node能够继续处理其余的工做,setTimeout 不会被阻塞,而是能够继续执行后面的代码。2行代码,其实执行线索上看有两条。

node大量使用异步代码,以此为卖点。怎么强调这个特性也不为过。对于强调并发的服务器编码,能够无需诉诸于多线程就能多线索的处理并发客户端需求。后面会看到在http sever内对此特性的使用和分析。

由于来了事件就调用callback,因此异步编程和事件驱动就经常一块儿出现了。尽管他们并不相同,在node 内经常是一回事,咱们也不去细分了。

来个http server

以往个人主语言是c#,那会儿,做为程序员,只能是IIS的用户。用户这个词,深深的伤害了我。如今node能够帮我报一箭之仇。

看看咱们能够作点什么:

  • 用户能够经过浏览器使用咱们的应用
  • 用户请求http://domain/时,能够看到一个Apache Style的 It works
  • 用户访问http://domain/start ,能够看到一个upload Form,利用它来上传图片
  • 用户访问http://domain/show , 能够显示此上传图片

自顶向下,分而治之

咱们来分解一下这个应用,为了实现上文的用例,咱们须要实现哪些部分呢?

  • 提供html页面,-> 须要HTTP Server
  • 路由。不一样的URL,会有不一样的处理模块(function)。匹配二者的模块,就叫作路由。
  • 能处理POST数据,可以处理上传图片

路由这样的工做,以往是有Web Server会处理。但是咱们如今要本身作。

Http Server

如今创建一个目录,比如是frodo. touch 一个 server.js的文件出来,输入:

var http = require("http");
http.createServer(function(request, response) {      
  response.setHeader('content-type', 'text/plain')
  response.end("42");
}).listen(8888);

// visit http://localhost:8888

呃。完了?嗯。用node跑跑。

nodemon server.js

开一个浏览器(我爱chrome)访问http://localhost:8888/,看到 42 就成了。

从代码到人话

不少时候咱们须要基于他人的工做。作http就应该引用http模块。它是node的内置模块。

咱们能够先看以上代码的主线索,启动服务器,并侦听8888端口:

var http = require("http");

var server = http.createServer();
server.listen(8888);

createServer。建立一个http server,侦听 8888端口。若是有请求到,就调用匿名函数:

function(request, response) {
    response.setHeader('content-type', 'text/plain')
    response.end("42");
  }

在此函数内,调用response.end,把内容(42)发送给Browser。

setHeader指明返回给浏览器的内容的格式。这里指明内容为平文本(text/plain)。还有比较多的经常使用格式,包括text/html,image/jpeg ,text/script 。望文生义便可。我不写这一行的话,现代的浏览器经常能够自动识别内容的格式。因此我经常也偷个懒。

这样固然并不严谨。为了快速的观其大略,有些细节能够暂时忽略。

玩玩http

启动服务后

nodemon server.js

能够在chrome内访问 localhost:8888,多开几个标签,都来打开 http://localhost:8888/,能够看到这个server总能够沉着的、稳定而单调的返回42 。多用户访问哦。

更多时候,我会用curl,一个命令行的browser模拟器。

curl http://localhost:8888/
42

实际上,开发node应用,第一次我经常会用chrome访问测试,后来的反复越多,我越会倾向于使用curl。若是我作这样app,我只有关心返回的是否是我指望的42,而没必要关心chrome的进度条,菜单,状态栏。。。多好。42 !最低眼球识别成本。

所以我不爱ide,而爱 sublime text 也基于一样的理由。

提供html

易如反掌:

var http = require("http");
  http.createServer(function(request, response) {
    response.end("<b>it works</b><a href='/start'>start</a>");
  }).listen(80);

  $curl localhost
  <b>it works</b><a href='/start'>start</a>

说明:
为了再省点事儿,我侦听改成 80 ,这样browser输入url的时候,不须要输入port。

请求路由

http server过来的都是URL,而咱们的代码是一个个的函数。URL 映射到函数的方法,就是路由。

所以,咱们须要查看HTTP请求,从中提取出请求URL:

var http = require("http");
http.createServer(function(request, response) {
  var pathname = url.parse(request.url).pathname;
  console.log(pathname);
  response.end("<b>it works</b><a href='/start'>start</a>");
}).listen(80);

点击start url,会看到/start 打印出来。

http 模块来的url,形如 http://domain.com:80/start?foo=bar&baz=bzz。能够经过url模块,解析它的pathname。这里的pathname = "/start"

var url = require("url");
var assert = require("assert")
var u = "http://domain.com:80/start?foo=bar&baz=bzz"
assert.equal("/start",url.parse(u).pathname)

路由

有了路由,来自/start和/upload的请求会导流到不一样函数。因此,咱们应该有一个结构,map二者的关系

var m = [
  {path:"/",func:function (){return "/"}},
  {path:"/start",func:function (){return "/start"}},
  {path:"/upload",func:function (){return "/upload"}}
]

首先,加入路由函数:

var http = require("http");
http.createServer(function(request, response) {
  var pathname = require("url").parse(request.url).pathname;
  var r = route(pathname)
  if (r)
     response.end(r());
  else
     response.end("<b>it works</b>");
}).listen(80);
function route(pathname){
  for(var i=0;i<m.length;i++){
    if (m[i].path == pathname)
      return m[i].func
  }
  return null
}

咱们故伎重演,用curl解放眼球:

$ curl localhost/upload
upload
$ curl localhost/start
start
$ curl localhost/
/

等效变幻

数学上,有时候仅仅是改变下公式内元素的位置,就可让解析或者证实变得更加容易。代码也是。咱们把上面的m 映射改为:

var m ={}
m["/"] = function (){return "/"}
m["/start"] = function (){return "/start"}
m["/upload"] = function (){return "/upload"}

表达的内容是等效的 。可是对于解析函数route会更加简单。

function route(pathname){
  return m[pathname]
}

目前咱们什么都混在一块儿。也会继续混到一块儿:代码还很少,这样有利于把握总体。

服务器特定问题:阻塞

客户端总要考虑客户的使用友好,不要卡死,界面漂亮;而服务器须要处理的就是减小阻塞。

何为阻塞?

让代码慢下来,就能够看到阻塞。咱们来让start()睡一会,模拟下。

function sleep(milliSeconds) {
  var startTime = new Date().getTime();
  while (new Date().getTime() < startTime + milliSeconds);
}
function start() {
  sleep(5000);
  return "/start";
}

故伎重演。不过稍做变化。由于curl能够帮助统计运行时间,因此咱们来利用下:

curl  -w %{time_total}\\n localhost:8888/upload
/upload 0.002

很快出结果,0.002,就是2毫秒。

$ curl  -w %{time_total}\\n localhost:8888/start
start 5.001

5毫秒。多一点。正如所愿。

一个一个的,很好。若是并发呢。

打开两个命令行窗口。

一个输入curl -w %{time_total}\n localhost:8888/upload,可是不执行
一个输入curl -w %{time_total}\n localhost:8888/start,可是不执行

而后,一二三,执行第二个,而后执行第一个。快点。

$ curl  -w \\n%{time_total}\\n localhost:8888/start
/start
5.013


$ curl  -w \\n%{time_total}\\n localhost:8888/upload
/upload
4.353

upload没有任何修改,原本执行很快,如今却慢到须要几乎5ms呢?

由于upload被start()阻塞了。start()的慢速,阻塞了其余的工做。

Node是单线程的。它经过事件轮询(event loop)来实现并行操做。若是轮询过来执行的代码时间长,就会没法处理后来的请求。所以,咱们须要尽量快的完成操做,以便返回控制权给node,让它能够抽身处理队列内等待的任务。

POST 文本块到服务器

简单的用例:

  1. 显示一个文本区(textarea)供用户输入内容,而后经过POST请求到服务器。
  2. 服务器经过处理程序将输入的内容展现到浏览器中。

/start请求处理程序用于生成带文本区的表单,所以,咱们将 app.js修改成以下形式:

var http = require("http");
var url = require("url");

var m ={}
m["/form"] = form
m["/upload"] = upload
m[404] = h404

function onRequest(request, response) {
  var postData = "";
  var pathname = url.parse(request.url).pathname;
  console.log("Request for " + pathname + " received.");
  var f = m[pathname]
  if(f)
    f(request, response)
  else  
    h404()
}
http.createServer(onRequest).listen(80);
function h404(request, response){
      response.writeHead(404, {"Content-Type": "text/plain"});
      response.write("404 Not found");
      response.end();
}
function upload(request, response){
    request.setEncoding("utf8");
    var postData
    var count = 0 
    request.addListener("data", function(postDataChunk) {
      console.log("postDataChunk.length:",postDataChunk.length);
      postData += postDataChunk;
      count++      
    });
    request.addListener("end", function() {
      console.log(count);
    });
}
function form(request, response){
  var body = 
    '<form action="/upload" method="post">'+
    '<textarea name="text" rows="20" cols="60"></textarea>'+
    '<input type="submit" value="Submit text" />'

    response.writeHead(200, {"Content-Type": "text/html"});
    response.write(body);
    response.end();
}

接受upload text

POST数据可能很大,为了使整个过程不会阻塞,Node会将POST数据拆分红小块。这也要求咱们经过侦听触发事件,把它们从新拼接起来。咱们须要:

  1. 侦听data事件。表示新的小数据块到达了
  2. 侦听end事件。全部的数据都已经接收完毕

以下所示:

request.addListener("data", function(postDataChunk) {
    console.log("postDataChunk.length:",postDataChunk.length);       
    postData += postDataChunk;
    count++      
  });
  request.addListener("end", function() {
    console.log(count);          
    response.writeHead(200, {"Content-Type": "text/plain"});
    response.write("Received: " + postData);
    response.end();
  });

实验体会:尝试着去输入大段内容,就会发现data事件会触发屡次。就是说,打印出来的count可能不是1,而每一个postDataChunk.length也不尽相同。

浏览器内容回显

咱们在/upload页面,展现用户输入的内容。

request.addListener("end", function() {
    console.log(count);          
    response.writeHead(200, {"Content-Type": "text/plain"});
    response.write("Received: " + postData);
    response.end();
  });

文件上传

最后,实现用例:

  1. 容许用户上传图片
  2. 并将该图片在浏览器中显示出来。

咱们要用到的外部模块:node-formidable,用来处理文件上传。

完成模块安装:

npm install formidable

用require语句引入:

var formidable = require("formidable");

该模块能够解析来自HTTP POST的表单:

var formidable = require('formidable'),
    http = require('http'),
    util = require('util');

http.createServer(function(req, res) {
  if (req.url == '/upload' && req.method.toLowerCase() == 'post') {
    var form = new formidable.IncomingForm();
    form.parse(req, function(err, fields, files) {      
      res.end('received upload:\n',files.upload.path);
    });    
  }

  // show a file upload form
  res.writeHead(200, {'content-type': 'text/html'});
  res.end(
    '<form action="/upload" enctype="multipart/form-data" '+
    'method="post">'+
    '<input type="text" name="title"><br>'+
    '<input type="file" name="upload" multiple="multiple"><br>'+
    '<input type="submit" value="Upload">'+
    '</form>'
  );
}).listen(8888);

在表单中添加一个文件上传元素。只须要在HTML表单中,添加一个multipart/form-data的编码类型。

formidable 会把此上传文件放到一个当前用户的临时目录内。并在files.upload.path 通知调用者具体位置:

received upload:C:\Users\rita\AppData\Local\Temp\upload_b3fa645d2425bc9f768494573a09b8ce

展示图片到浏览器

咱们来添加/show 请求处理程序,它硬编码显示刚刚传递的png到浏览器中。

var http = require("http");
var url = require("url");

var m ={}
m["/show"] = show 
m["/favicon"] = favicon
function onRequest(request, response) {
  var pathname = url.parse(request.url).pathname;
  console.log("Request for " + pathname + " received.");
  var f = m[pathname]
  if(f)
    f(request, response)
  else  
    h404(request, response)
}
http.createServer(onRequest).listen(80);


function show(request,response) {  
  var fs = require("fs")
  // 替换为你的文件
  var last_uploadfile ="C:/Users/rita/AppData/Local/Temp/upload_b3fa645d2425bc9f768494573a09b8ce"
  fs.readFile(last_uploadfile, "binary", function(error, file) {
    if(error) {
      h404(request,response)
    } else {
      response.writeHead(200, {"Content-Type": "image/png"});
      response.write(file, "binary");
      response.end();
    }
  });
}
function h404(request, response){
  if (response){
        response.writeHead(404, {"Content-Type": "text/plain"});
        response.write("404 Not found");
        response.end();}
}
function favicon(request, response){}

重启服务器以后,经过访问http://localhost/show,就能够看到保存在刚刚上传的图片了

wrapper up

恭喜,咱们的半成品完成了。关于语言自己,须要理解的就是模块和Callback。做为服务器端脚本,概念就稍微多点点:阻塞与非阻塞,事件驱动,以及HTTP协议,文件Post上传,MIME类型。

一回生二回熟。至此,Node对咱们而言,有些亲切了。

和路由相关的代码展现了做为服务器框架的一个重要构成的概念。对此有兴趣的话,能够继续研究express框架。

另外,代码也都堆积到一个文件,根本没有考虑重构,也没有考虑到模块划分。对于较大的程序来讲,这固然会构成一个问题。我在(极简node模块开发)[note.md]探究此技术。

学无止境。学习node经常会有哦也的赞叹,这样的乐趣相伴左右。

格外说明

本文是nodebeginner对应的中文版的阅读笔记。可是在实验代码的过程当中,也顺手加入了些本身的一些文字与代码的风味:

-简洁:行文简化,代码也作了重构。而且表意也直接(总以为别人啰嗦)。还忽略和模块等和主题不太相关的内容。
-也有些个人想法。好比curl替代browser作响应验证

通过这个工做,我更好的学习了原文,体会到node的精要之处。因此感谢nodebeginer做者的创造和译者的工做。

说说我和js的交往吧。

过去N年,我一直是一家企业的技术团队管理者,同时也是MS技术的开发者。我采用c#作b/s 企业应用。其中涉及到的javascript不多,有的话,基本也就是数据核对。或者玩点动画之类的动态内容。一直认为js很简单,故而也谈不上作稍微深刻的研究。

而后ajax技术告诉我,这个看起来很小的玩意其实能够很强大。

接着,出现了Node,服务端的JavaScript,以及火热的NPM模块仓库。一块儿来的,还有不太熟悉的面孔,像是事件驱动的,非阻塞等等。

这几年社区明显的火起来。在github上算得上第一语言,即便MS也在为她作工具(Node tool,Visual studio code ),甚至创造了一门(再一个)能够编译到js的语言:TypeScript。

我(一路大跌眼镜)[http://1000copy.farbox.com/post/crossing-eye-s-hell],一次次的修正本身的认识,因而我真心的想要花点气力研究,以便充分的今后语言中获益。

不管如何,js是b/s编程的一个必选项。反正都要选,若是还能够同时完成后端的代码,只是想一想也会感到很棒。

相关文章
相关标签/搜索