初級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編程的一個必選項。反正都要選,若是還能夠同時完成後端的代碼,只是想一想也會感到很棒。

相關文章
相關標籤/搜索