理解網絡編程-2

該文章閱讀須要5分鐘,更多文章請點擊本人博客halu886node

前言

Node是一個面向網絡而生的平臺,具備事件驅動,無阻塞,單線程等特性。具備良好的可伸縮性,適合在分佈式式網絡中扮演各式各樣的角色。同時提供的網絡基礎API很是貼合網絡,很是適合基於基礎API構建靈活的網絡服務。算法

利用Node能夠很容易的搭建Web服務。像其餘語言都須要Web服務器做爲容器。如ASP,ASP.NET須要IIS做爲服務器。PHP須要搭建Apache或Nginx環境等。JSP須要Tomcat等等。可是對於Node來講,只須要僅僅幾行代碼就能夠構建服務器,不須要額外的容器。shell

Node提供了net,dgram,http,https這4個模塊,分別用來處理TCP,UDP,HTTP,HTTPS,適用於服務端和客戶端。瀏覽器

構建TCP服務

TCP在網絡中十分常見,目前大部分應用都是構建在TCP上的。緩存

TCP

TCP全稱是傳輸控制協議,在OSI模型(分爲七層,物理層,數據鏈路層,網絡層,傳輸層,會話層,表現層,應用層)位於傳輸層協議,典型的HTTP,SMPT,IMAP等就是構建在TCP上。bash

1

TCP是典型的面向鏈接的協議,特徵是在傳輸以前須要3次握手造成會話。服務器

2

只有會話造成,客戶端和服務端才能開始傳輸數據。建立過程當中,服務端和客戶端分別提供一個的套接字共同造成一個連接。服務端和客戶端經過套接字造成二者連接的操做。網絡

建立TCP服務器端

如今咱們來建立一個TCP服務端接受網絡請求。併發

var net = require('net');

var server = net.createServer(function(socket){
     socket.on('data',function(data){
          socket.write("你好");
     })

     socket.on('end',function(){
          console.log('鏈接斷開');
     })
     socket.write("hello world!\n");
});

server.listen(8124function(){
     console.log('server bound');
})
複製代碼

咱們經過net.createServer()建立一個TCP服務器,server.listen表示connection的偵聽器,也能夠用以下方式:app

var server = net.createServer();

server.on('connection',function(socket){
     // 新的鏈接
})

server.listen(8124);
複製代碼

咱們能夠用telnet做爲客戶端和剛剛建立的簡易服務器進行會話交流。

$ telnet 127.0.0.1 8124
Trying 127.0.0.1
Connected to localhost.
Escape character is '^]'.
hello world
hi
複製代碼

除了端口外,咱們還能夠對Domain Socket進行監聽。

server.listen('/tmp/echo.sock');
複製代碼

經過nc工具進行上述的構建的TCP服務的會話測試。

$ nc -U /tmp/echo.sock
hello world
hi
複製代碼

也能經過net模塊自行構建客戶端進行會話。

var net = require('net')

var client = net.connect({port:8124},function(){
     console.log('client connected');
     client.write('world!\r\n');
});

client.on('data',function(data){
     console.log(data.toString());
     client.end();
})

client.on('end',function(data){
     console.log('client disconnected');
})
複製代碼

將以上客戶端存爲client.js,執行以下

$ node client.js
client connected
hello world!

你好
client disconnected
複製代碼

其結果與使用nc和Telnet的結果並沒有區別。若是是Domain Socket,填寫path便可。

var client = net.nonnect({path:'/tmp/echo.sock'});
複製代碼

TCP服務的事件

以上例子中,事件分爲服務器事件和鏈接事件

服務器事件

對於經過net.createServer()建立的服務器而言,它是一個EventEmitter實例,它的自定義事件以下幾種。

  • listening:再調用server.listen()綁定端口或者Domain Socket後觸發。
  • connect:每一個客戶端鏈接到服務端時觸發,簡潔用法能夠用net.createServer(),最後一個參數傳遞。
  • close:當服務端關閉時觸發,當調用sever.close()後,中止接受新的客戶端套接字鏈接,同時等待鏈接斷開後觸發該事件。
  • error:當服務器發生異常,則會觸發這個事件。例如偵聽一個正在使用中的端口,則會觸發這個事件。若是沒有偵聽error事件,服務器則會拋出異常。

鏈接事件

服務器能夠和多個客戶端保持鏈接,對於每一個鏈接來講都存在一個可讀可寫Stream流用於服務端到客戶端的通訊。能夠偵聽data事件進行讀取另外一端的數據,經過write()像另外一端發送數據。

  • data:當一端經過write發送數據,另外一端則會觸發data事件。接受到數據則是write傳入的數據。
  • end:當任何一段發送Fin數據包,則會觸發這個事件。
  • connect:用於客戶端鏈接服務端,當套接字與服務端鏈接成功時觸發。
  • drain:當任意一端調用write()時,當前這端觸發該事件。
  • error:異常發生時,觸發該事件。
  • close:當套接字徹底關閉時,觸發該事件。
  • timeout:當鏈接閒置了一段時間後,觸發該事件。

另外,TCP套接字是一個可讀可寫的Stream流,能夠經過pipe()實現管道操做。

var net = require('net');
var server = net.createServe(function(socket){
     socket.write('Echo server\r\n');
     socket.pipe(socket);
})

server.listen(1337,'127.0.0.1');
複製代碼

不過,TCP在傳輸小數據包時存在一個優化策略。Nagle算法,若是不存在這個算法的話,網絡傳輸中全是相同的小數據包,十分浪費網絡資源。這個算法是將數據緩存在一個緩衝區到必定數據量或必定時間量時再一塊兒發出去,因此小數據包會被Nagle算法合併,可以起到節省寬帶的做用。不過這樣的反作用則是有可能數據包會被延遲發送。

在Node中,TCP默認採用了啓用Nagle算法,能夠經過Socket.setNotDaley(true)去掉Nagle算法。使得write()後當即將數據發出到網絡中。

不過,經過write()寫入數據後會觸發data事件,關閉Nagle算法後,也不意味這每次執行write方法都會觸發data事件。另外一端將多個小數據包合併,而後只觸發一次data事件。

構建UDP服務

UDP稱爲用戶數據包協議,和TCP同屬於網絡傳輸層。和TCP最大的不一樣是UDP不是面向鏈接。TCP鏈接一旦創建,會話都是基於鏈接完成,而且每一個不一樣的鏈接都須要不一樣的套接字。但在UDP中,一個套接字能夠和多個UPD服務端會話。雖然提供的是面向事務的簡單不可靠傳輸服務,在網絡狀況差的狀況下丟包嚴重,可是無需鏈接,資源消耗低,處理快速且靈活。因此經常應用於丟一兩個包也不影響的場景,例如音頻,視頻。UDP目前引用很是普遍,CDN服務就是基於UDP實現的。

建立UPD套接字

建立UPD套接字十分簡單,UDP套接字一旦創建,既能夠做爲客戶端發送數據,也能夠做爲服務端接收數據。

var dgram = require('dgram');
var socket = dgram.createSocket('udp4');
複製代碼

建立UDP服務端

若想讓UPD套接字接受網絡消息,只須要用dgram.bind(port,address)對網卡和端口進行綁定便可。

var dgram = require("dgram");

var server = dgram.createSocket("udp4");

server.on("message",function(msg,rinfo){
     console.log(msg +" from " + rinfo.address + ":" +rinfo.port);
})

server.on("listening",function(){
     var address = server.address();
     console.log(address.address+ ":" + address.port);
})

server.bind(41234);
複製代碼

建立UDP客戶端

建立一個UDP客戶端與客戶端通訊。

var dgram = require('dgram');

var message = new Buffer('hello world');

var client = dgram.createSocket('udp4');

client.send(message,0,message.length,41234,"localhost",function(err,bytes){
     client.close();
})
複製代碼

保存client.js並執行。

$ node server.js
server listening 0.0.0.0:41234
server got:hello world from 127.0.0.1:58682
複製代碼

當套接字用在客戶端時,使用send()方法發送消息到網絡中。send()方法參數以下:

socket.send(buf,offset,length,port,address,[callback])
複製代碼

這些參數分別爲要發送的Buffer,偏移位,長度,端口,地址以及回調。它與TCP相比,參數相對複雜一點,可是能夠隨意發送到網絡中服務端,可是若是TCP須要從新發送數據另外一個服務端的話,則須要經過套接字構造一個新的鏈接了。

UDP套接字事件

UDP套接字使用起來相對容易一點,只是一個EventEmitter的實例,而非Steam的實例。

  • message:當UDP套接字偵聽網口端口後,接收到消息後觸發該事件,攜帶的數據爲Buffer數據和和一個遠程地址信息。
  • listening:當UDP套接字開始偵聽時觸發該事件。
  • close:調用close時觸發該事件,不在觸發message事件,從新偵聽地址和端口則可從新觸發。
  • error:當異常發生時觸發該事件,若是不偵聽該事件,則異常拋出,使進程退出。

以上知識點均來自<<深刻淺出Node.js>>,更多細節建議閱讀書籍:-)

該文章閱讀須要5分鐘,更多文章請點擊本人博客halu886

構建HTTP服務

TCP和UDP都屬於傳輸層的協議,若是須要構想高性能的網絡傳輸,就應該從傳輸層入手。 若是對於經典的業務場景,應用層協議遠遠夠了,例如http和smtp等。

用Node構建一個http服務經過幾行代碼就夠了。

var http = require('http');
http.createServer(function(req,res){
    res.writeHead(200,{'Content-Type':'text/plain'});
    res.end('Hello World\n');
}).listen(1337,'127.0.0.1');
console.log('Server running at http://127.0.0.1:1337/');
複製代碼

雖然只能返回hello world,可是維持的併發量和QPS都不容小覷的。

Http

初識HTTP

HTTP稱爲超文本傳輸協議,寫做HyperText Transfer Protocol。構建在TCP協議上。屬於應用層協議。 用於服務端和客戶端通訊,著名的B/S模式,

HTTP的發展是W3C和IETF的合做結果,他們最終發佈了一系列RFC標準,目前最著名的HTTP標準就是RFC2612。

Http報文

咱們經過curl調用上述代碼啓動的本地http服務器的接口,查看http報文。

$ curl -v http://127.0.0.1:1337
* About to connect() to 127.0.0.1 port 1337 (#0)
* Trying 127.0.0.1...
* connected
* Connected to 127.0.0.1(127.0.0.1) port 1337 (#0)
> Get / HTTP/1.1
> Uesr-Agent: curl/7.24.0(x86_64-apple-darwin12.0) libcurl/7.24.0 OpenSSL/0.9.0r zlib/1.2.5
> Host:127.0.0.1:1337
> Accept: */*
>
< HTTP/1.1 200 OK
< Content-Type: text/plain
< Date: Sat,06 Apr 2013 08:01:44 GMT
< Connection:keep-alive
< Transfer-Encoding: chunked
<
Hello World
* Connection #0 to host 127.0.0.1 left intact
* Cloing connection #0
複製代碼

上述的報文分爲幾個階段,最經典的就是三次TCP三次握手。

* About to connect() to 127.0.0.1 port 1337(#0)
*   Trying 127.0.0.1..
* connected
* Connected to 127.0.0.1 (127.0.0.1) port 1337 (#0)
複製代碼

第二部分是向服務端發送請求報頭

> GET / HTTP/1.1
> User-Agent: curl/7.24.0 (x86_64-apple-darwin12.0) libcurl/7.24.0 openSSL/0.9.8r zlib/1.2.5
> Host: 127.0.0.1:1337
> Accept: */*
>
複製代碼

第三部分是服務器端向客戶端發送返回內容,包括響應頭和響應體。

< HTTP/1.1 200 OK
< Content-Type: text/plain
< Date: Sat, 06 Apr 2013 08:01:44 GMT
< Connection: keep-alive
< Transfer-Encoding: chunked
<
Hello World
複製代碼

最後是結束會話的階段

* Connection #0 to host 127.0.0.1 left intact
* Closing connetion #0
複製代碼

從上述報文能夠看出,http是基於請求響應式,一問一答實現服務的。雖然基於TCP會話,可是不存在會話的特色。

從協議的角度上來講,瀏覽器就是一個HTTP代理,將用戶的行爲轉化爲請求報文發送給服務端,服務端處理請求,返回響應報文給http代理,代理解析響應報文,將內容顯示在界面上。以查看圖片爲例子,瀏覽器發送請求報文,服務器解析圖片路徑,將磁盤中圖片文件以報文的形式發送給客戶端。瀏覽器接收到報文後,調用渲染引擎將內容顯示給用戶。http服務只有兩件事,處理http請求,發送http響應。

不管是http請求體仍是響應體,都只包含兩件部分,報文頭和報文體。

上述部分<和>分別爲請求頭和響應頭,因爲是get請求,因此沒有報文體,hello world是響應的報文體。

http模塊

Node的Http模塊是繼承自TCP模塊,基於事件驅動,不會爲每個請求就生成額外線程或者進程,內存佔用比很是底,因此能實現高併發。Http模塊與TCP模塊的區別在於,啓動了keepalive後,tcp可以用於屢次請求和響應。HTTP服務是以Request爲單位進行服務,Tcp服務是以Connection爲單位進行服務,http模塊即將connection到request進行封裝。

1

HTTP模塊將嵌套字的讀寫抽象爲ServerRequest對象和Respond對象,分別對應請求和響應操做。當請求產生時,http拿到鏈接中的數據,調用二進制模塊http_parse解析完報頭後,觸發Request事件,而後調用用戶的業務邏輯。

2

HTTP請求

對於TCP的鏈接的讀操做,http模塊將其封裝爲ServerRequest對象,以下是通過http_parse的請求報文

> GET / HTTP/1.1
> User-Agent: curl/7.24.0 (x86_64-apple-darwin12.0) libcurl/7.24.0 OpenSSL/0.9.8r zlib/1.2.5
> Host: 127.0.0.1:1337
> Accept: */*
>
複製代碼

報文頭 GET / HTTP/1.1 會被解析爲以下屬性

  • req.method熟悉爲請求方法,值爲GET,經常使用的有GET,PUT,POST,DELETE,CONNECT幾種。
  • req.url屬性,值爲/
  • req.httpVersion熟悉,值爲 1.1;

其餘報頭主要是Key:Value格式,解析完後給業務邏輯處理。

報文體被抽象爲一個流對象,只有在報文接收完畢後業務邏輯才能夠進行讀操做。

function(req,res){
    //console.log(req.headers);
    var buffers = [];
    req.on('data',function(trunk){
        buffers.push(trunk);
    }).on('end',function(){
        var buffer = Buffer.concat(buffers);
        res.end('Hello world')
    })
}
複製代碼

HTTP響應

HTTP響應是封裝流中的可寫操做,能夠當作一個可寫的流對象。

在編輯響應體的頭部主要分兩步setHeader()和writeHeader(),能夠經過setHeader()設置多個參數,可是隻有經過writeHeader後纔會寫入到鏈接。

而且http模塊會自動設置一些頭信息。

< Date: Sat, 06 Apr 2013 08:01:44 GMT
< Connection: keep-alive
< Transfer-Encoding:chunked
複製代碼

這裏的keep-alive表示的是當前的鏈接不關閉,能夠用於下一個請求。chunked表示的則是默認使用分塊傳輸協議(chunked transfer encoding),放棄採用緩存的方式傳輸返回體,並且分塊傳輸從而提高效率,最後一個數據塊用0表示傳輸完畢。

寫入響應體的內容則是經過write()和end(),二者的區別在於end()會調用write()將數據寫入鏈接中,而後告訴服務端響應結束。

一旦開始發送報文則writeHeader()和setHeader()不在生效,由於報頭在報文發送前發送,這是因爲http協議特性決定的。

HTTP事件

HTTP服務如TCP服務同樣抽象了一些事件,而且HTTP服務器也是一個EventEmiter實例。

  • connection:當開始發送請求和響應時,須要創建底層的TCP鏈接,有些時候也會開啓keep-live用以保持鏈接。當連接創建完成時觸發該事件。
  • request:當連接創建完成,請求的報頭髮送完畢解析完成時觸發該事件。
  • close:與TCP模塊服務相似,調用server.close再也不接收新的連接,當全部連接都斷開時觸發該事件,也能夠經過server.close()傳入回調快速觸發該事件。
  • checkContinue:當客戶端發送一個較大數據的請求時,會先發送一個報頭帶有expect:100-continue進行確認,當接收到請求報頭時觸發該事件。若是沒有監聽該事件,則默認返回狀態碼爲100 continue表示贊成接收,不然返回400 Bad Request拒絕接受。該事件與reqeust事件互斥,當監聽了該事件,request事件不會被觸發。當客戶端接收到100 continue從新發起請求,下一個請求才會觸發request事件。
  • connect:當客戶端發起CONNECT請求時(一般用於代理時發起),觸發該事件,當沒有監聽該事件時,自動關閉該鏈接。
  • upgrade:當請求報文中攜帶這個字段(用於與服務器端協商升級協議),若是沒有監聽該事件,自動斷開連接
  • clientError:當客戶端觸發error時,會將error傳遞給服務端,觸發該事件。

HTTP客戶端

HTTP模塊提供了一個底層API http.request(options,connect)用來實例化一個http客戶端。

options有以下選項

  • host:服務器端口和域名,默認爲127.0.0.1
  • hostname:服務器名稱
  • port:服務器端口,默認爲80
  • localAdress:創建鏈接的本地網卡
  • socketPath:domain套接字路徑
  • method:http請求方法,默認爲Get
  • path:http請求路徑,默認爲/
  • header:請求頭
  • auth:basic認證,用於headers中的Authorization部分

HTTP代理

HTTP模塊的ClientReqeust封裝的模塊相似於服務端。在keep-live的狀況下可複用鏈接,默認最大鏈接數是5。封裝了一個http.globalAgent進行管理鏈接池。

3

若是須要修改限制,則須要聲明一個agent配置maxSockets配置進行傳遞,false表示不進行配置

const agent = http.agent({
    maxSockets:20
});

http.request({
    agent
});
複製代碼

Agent對象中sockets和request表示當前的鏈接數和等待鏈接的請求,能夠經過觀察這兩個值進行判斷業務繁忙程度。

HTTP客戶端事件

  • response:在獲得服務端端響應時觸發該事件
  • socket:當底層的鏈接池分配鏈接給當前請求時出發該請求
  • connect:當發送connect請求時服務端返回200狀態碼時觸發該事件
  • upgrede:當客戶端發送upgrade請求時,服務端發送101 Switching Protocols時觸發該事件
  • continue:當客戶端發送expect:100-continue時,服務端返回100 continue觸發該事件

以上知識點均來自<<深刻淺出Node.js>>,更多細節建議閱讀書籍:-)

相關文章
相關標籤/搜索