Node.js服務器開發(2)
npm模塊管理 第三方模塊
1.node.js生態裏的第三方模塊可以通過npm工具來安裝使用.
2.npm安裝node.js模塊:
npm install 本地安裝, 運行npm目錄/node_modules
也就是你項目目錄下的node_modules
npm install -g全局安裝 安裝到系統的node_modules
全局安裝就是要你 install 後面加一個-g 表示全局
3.nodejs第三方模塊安裝分為兩種
(1)本地安裝:安裝好後第三方模塊代碼全部打包到本地項目中來
這時候,把這個項目發給別人,別人就不需要再次安裝這個模塊了.
缺點是:你本地每一個新項目 都要重新安裝到項目裏才行.
(2)全局安裝:安裝完了後,以後你所有的項目都可以使用,方便開發.
他的缺點是,因為他是全局的是放在你系統下面,這時候別人使用
你的代碼的時候,還需要手動再去安裝這個模塊.
在全局安裝一個websocket模塊
安裝好會輸出版本 和 安裝路勁 默認路勁就是這個
我們就可以找到這個模塊了,這個目錄就是NODE_PATH指定的路勁
裝好了之後,就是全局的了,所有的項目都能使用這個模塊
本地安裝ws模塊,本地安裝是在運行npm目錄/node_modules
比如說我的項目在這個目錄下 D:/nodejs/myserver
所以本地安裝的話,就會把模塊放到這個項目目錄下的node_modules
在這個文件夾下面安裝shitf+鼠標右鍵 在此處打開命令窗口
這樣我們就能直接在這運行npm了
安裝
成功
但是他會提示你一個小錯誤,就是沒有package_json 說明包
他會生成一個類似說明文件的東西.
只需要你 在命令行輸入 nmp init 只需要使用一次即可
這時候輸入項目名稱
版本
描述
入口函數 默認index.js
測試命令 跳過
跳過
關鍵字
作者
然後這個文件就創建成功了,
當你再次使用npm安裝模塊的時候,就不會有錯誤了
4.在項目中導入和使用模塊require
(1)require項目文件.js代碼.json文本,.node二進制文件必須使用
絕對路勁(/)或者相對路勁(./,../);只有文件才能使用路勁
但是如果你是引入模塊,一定不能使用路勁,模塊不加路勁.
比如要使用websocket模塊. 這樣才能正確導入.
var ws = require("ws"); //正確的導入模塊
(2)沒有寫後綴名的require項目文件,依次加載:.js,.json,.node
或者你可以直接使用後綴名 require("abc.js")
(3)如果沒有以絕對路勁開頭或相對路勁開頭的 就是加載模塊.
a):系統模塊去查找能否找到NODE_PATH,如果有這個模塊就用
這個目錄下的模塊.
b):如果系統沒有,再到當前這個項目目錄下的./node_modules目錄下
查找,
c):如果這裏沒有,他返回上一級文件夾查找node_modules,
因為你的node_modules放在第一級,而你的代碼文件放在第4
級的文件下,所以就需要這中方法去查找.
二、websocket模塊的使用
Websocket
1.websocket是一種通訊協議,底層是tcp socket,基於TCP
它加入了字節的協議用來傳輸數據
2.是h5為了上層方便使用socket而產生的
3.發送數據帶著長度信息,避免粘包問題,其底層已經處理了這個過程
4.客戶端/服務器向事件驅動一樣的編寫代碼,不用來考慮
底層復雜的事件模型。
握手協議過程
服務器首先解析客戶端發送的報文
紅色的那個就是客戶端發過來的隨機Key,就是要得到這個key
解析好之後,就要給客戶端回報文
把那個key解析出來之後,要加上一個固定migic字符串
然後通過SHA-A加密,在通過base-64加密發送給客戶端,
這時候就完成了握手協議.
接收/發送數據協議.
1.比如你客戶端要發送一個HELLO,他發送的時候把這個字符串
每個字符轉成ASCLL碼, 並且他不是直接發送出去.
(1)固定字節(1000 0001或1000 0010)0x81 0x82
(2)包長度字節,總共8個位,第一位固定是1,剩下7位得到整數(0-127)
125以內直接表示長度,如果126那就後面兩個字節表示長度,
127後面8個字節表示數據長度.
(3)mask掩碼 是包長後面4個字節
(4)數據 在mask掩碼之後的就是需要的數據了.
得到這個數據的方法,就是拿掩碼第一個字節和第一個數據做
位異或(xor)運算,第二個掩碼字節和第二個數據運算,以此類推
第五個數據字節,又和第一個掩碼字節做運算.
WS模塊
這個模塊已經封裝好了底層的這些處理流程,很方便的就可以使用了
服務端的編寫:
首先開啟websocket服務器
connection事件:有客戶連接建立
error事件:監聽錯誤
headers事件:握手協議的時候,回給客戶端的字符串
客戶端成功完成握手後的事件: 用客戶端通訊的sock綁定
message事件:接收到數據
close事件:關閉事件
error事件:錯誤事件
發送數據:send
//加載模塊 var ws = require("ws"); //開啟基於websocket的服務器 //監聽客戶端的連接 var server = new ws.Server({ host: "127.0.0.1", port: 8005, }); //給客戶端添加監聽事件的函數 function ws_add_listener(c_sock){ //close事件 c_sock.on("close",function(){ console.log("client close"); }); //error c_sock.on("error",function(errs){ console.log("client error",errs); }); //message事件 c_sock.on("message",function(data){ //data是解包好的原始數據 //就是websocket協議解碼開來的原始數據 console.log(data); c_sock.send("你好,成功連接啦"); }); } //connection事件:有客戶端接入 function on_server_client_comming(client_sock){ console.log("wsClient連接"); //給這個客戶端socket綁定事件,就能收到信息啦 ws_add_listener(client_sock); //回給客戶端一個信息 } //綁定事件 server.on("connection",on_server_client_comming); //error事件:監聽錯誤 function on_server_listen_error(err){ console.log("錯誤",err); } server.on("error",on_server_listen_error); //headers事件:拿到握手連接回給客戶端的字符 function on_server_headers(data){ //console.log(data); } server.on("headers",on_server_headers);
客戶端編寫:
open事件:連接握手成功
error事件:當連接發生錯誤的時候調用
message事件:當有數據進來的時候調用
close事件:當有數據進來的時候調用
message事件:data已經是根據websocket協議解碼出來的原始數據,
websocket底層有數據包的封包協議,所以,決定不會出現粘包的情況
每解一個數據包,就會觸發一個message事件
//加載模塊 var ws = require("ws"); //創建連接 會發生握手過程 //首先創建一個客戶端socket,然後讓 //這個客戶端去連接服務器的socket //url地址 以ws:// 開頭 var sock = new ws("ws://127.0.0.1:8005"); //open事件:連接握手成功 function on_client_connimg_success(){ console.log("connect success!!!"); //連接成功就向服務器發送數據 sock.send("HelloWebSocket"); //發送幾個就服務器就收到幾個不出現粘包現象 //因為你每次發送的時候,底層會進行封包 sock.send("HHno你好是是是sadsa"); } sock.on("open",on_client_connimg_success); //error事件 function on_client_error(err){ console.log("error",err); } sock.on("error",on_client_error); //close關閉事件 function on_client_close(){ console.log("close"); } sock.on("close",on_client_close); //message收到數據的事件 function on_client_recv_message(data){ console.log(data); } sock.on("message",on_client_recv_message);
在瀏覽器腳本裏面使用websocket
三、TCP通訊拆包和封包 以及粘包處理
TCP粘包問題
1在通訊過程中,我們可能有發送很多數據包,數據包A
數據包B,C此時我們期望依次收到數據包A,B,C,但是TCP
底層為了傳送性能,可能會把ABC所有的數據一起傳過來,
這時候可能收到的就是A+B+C,這時候上層的程序根本就無法
區分A,B,C這個叫做--粘包。
2.產生粘包的原因有兩種:1發送方造成 2接收放造成
(1)發送方引起粘包是由TCP協議本身造成的,TCP為了提高傳輸效率,
發送方往往要收集足夠的數據才發送一包數據,若連續幾次發送的數據
都很少,通常TCP會根據優化算法把這些數據合成一包後一次發送出去,
這樣接收方就收到了粘包的數據
(2)接收方引起粘包是由於接收方用戶進程不及時接收數據.從而導致粘包現象,
這是因為接收方先把收到的數據放在系統接收緩沖區,用戶進程從緩沖區讀取
數據,若下一包數據到達時,前一包數據尚未被用戶進程取走,則下一包數據
放到系統接收緩沖區時就接到前一包數據之後,而用戶進程再次讀取緩沖區數據.
這樣就造成一次讀取多個包的數據.
3.比如說我要發送3個命令:hello, new ,alloc,使用tcp發送後
收到的數據可能就是hellonewalloc粘在一起了,
包體協議
1.處理粘包的辦法:在處理的時候,對所有要發送的數據進行封包,
建立一個封包規則,解包也使用這個規則,比如說:前面兩個字節數據長度,
後面是數據, 這時候即使他們粘包了,客戶端收到這個數據,
通過拆包,就可以解開所有的包. 客戶端收到包只需要得到前面的長度,
就可以知道第一個包有多長, 這樣多余的包就是粘住的包了.
2.打入長度信息或者加特定的結尾符都是解決粘包的辦法.
size+body, 一般用於二進制數據
body+\r\n結尾符 一般用於Json
3.處理過程中會出現集中情況:
(1)收到的剛好是1一個包或n個完整包
(2)收到1.5個包,也就是還有1半還沒收到,那半個包就要保存起來
(3)收到不足一個包,這個包要直接保存起來,等下一次接收.
封包 獲取長度 模擬粘包 分包
var netpkg = { //根據封包協議讀取包體長度 offset是從哪開始讀 read_pkg_size: function(pkgdata,offset){ if(offset > pkgdata.length - 2){ //剩下的長度從開始讀位置,都不足兩個字節 //沒有辦法獲取長度信息 return -1; } //這裏就是讀取兩個字節的長度 //使用小尾來讀取出來 無符號兩個字節 var len = pkgdata.readUInt16LE(offset); return len; }, //把要發送的數據封包,兩個字節長度+數據 package_string: function(data){ var buf = Buffer.allocUnsafe(2+data.length); //前面小尾法寫入兩個字節長度 buf.writeInt16LE(2 + data.length, 0); //填充這個buff value string|Buffer|Integer //offset填充buf的位置,默認0 //end結束填充buf的位置,模式buf.length //encoding如果value是字符串,則是字符編碼utf8 buf.fill(data,2); //返回封好的包 console.log(buf); return buf; }, //模擬粘包 test_pkg_two_action:function(action1,action2){ //如果有兩個命令 var buf = Buffer.allocUnsafe(2 + 2+action1.length+action2.length); buf.writeInt16LE(2+action1.length,0); buf.fill(action1,2); //把第二個包粘在一起 var offset = 2 + action1.length; buf.writeInt16LE(2 + action2.length,offset); buf.fill(action2,offset+2); console.log("模擬粘包",buf); return buf; }, //模擬一個數據包 分兩次發送 test_pkg_slice: function(pkg1,pkg2){ var buf1 = Buffer.allocUnsafe(2 + pkg1.length); //寫入長度信息 因為他們是同一個包 buf1.writeInt16LE(2+pkg1.length + pkg2.length,0); buf1.fill(pkg1,2); console.log("buf1 = ",buf1); //剩下的包 var buf2 = Buffer.allocUnsafe(pkg2.length); buf2.fill(pkg2,0); //將這兩個包作為數組返回 return [buf1,buf2]; } }; module.exports = netpkg;
服務器接收部分
//引入net模塊 var net = require("net"); var netpkg = require("./netpak"); //全局變量記錄多余的包 var last_pkg = null; var server = net.createServer((client_sock)=>{ console.log("client comming"); }); console.log("開始等待客戶端連接"); server.listen({ host: "127.0.0.1", //host: 'localhost', port: 6800, exclusive: true, }); server.on("listening",function(){ console.log("start listening ..."); }); server.on("connection",function(client_sock){ console.log("新的鏈接建立了"); console.log(client_sock.remoteAddress, client_sock.remotePort); //綁定 客戶端socket 關閉 事件 client_sock.on("close",function(){ console.log("客戶端關閉連接"); }); client_sock.on("data",function(data){ if(last_pkg != null){ //到這裏表示有沒處理完的包 //把這個包和收到的包合並 var buf = Buffer.concat([last_pkg,data], last_pkg.length + data.length); last_pkg = buf; }else{ //如果沒有要處理的 直接獲取data last_pkg = data; } //開始讀長度的位置 var offset = 0; //讀取長度信息 var pkg_len = netpkg.read_pkg_size(last_pkg,offset); if(pkg_len < 0 ){ //沒有讀到長度信息 return; } console.log("數據內容:",last_pkg); //可能有多個包,offset是包開始的索引 //如果他加上包讀到的長度 小與等於 //說明last_pkg這裏面有一個數據包的 //因為你長度是5 數據包長度是10 //這個5就是我們讀到一個包的長度,說明他 //就是有一個完整的包,在這就可以讀取了 while(offset + pkg_len <= last_pkg.length){ //根據長度信息來讀取數據 //假設傳過來的是文本數據 //這個包就是offset 到 pkg_len的內容 //申請一段內存 減2 因為他包含了長度信息 var c_buf = Buffer.allocUnsafe(pkg_len - 2); console.log("包長",pkg_len); console.log("收到數據長度",last_pkg.length); //使用copy函數來拷貝到這個c_buf last_pkg.copy(c_buf,0,offset+2,offset+pkg_len); console.log("recv cmd:",c_buf); console.log(c_buf.toString("utf8")); //起始位置跳過這個包 offset += pkg_len; if(offset >= last_pkg.length){ //正好這個包處理完成 console.log("正好處理完成"); break; } //到這裏說明還有未處理的包 再次讀取長度信息 pkg_len = netpkg.read_pkg_size(last_pkg,offset); console.log("粘包長度",pkg_len); console.log("ofsett:",offset); if(pkg_len < 0) {//沒有讀到長度信息 break; } } //如果只有半個包左右的數據 if(offset >= last_pkg.length){ //這裏表示所有的數據處理完成 console.log("完整包"); last_pkg = null; offset = 0; }else{ //如果沒有處理完 //將其實位置 到length的數據 拿到 //也就是offset反正就是一個包的起始位置 console.log(">>>>>>>>>不足一個包,等待下次接收"); console.log(last_pkg); var buf = Buffer.allocUnsafe(last_pkg.length- offset); //把剩余的數據從offset開始復制到buf,長度是所有 last_pkg.copy(buf,0,offset,last_pkg.length); last_pkg = buf; } }); //監聽錯誤事件 通訊可能會出錯 client_sock.on("error",function(e){ console.log("error",e); }); }); //綁定錯誤事件 server.on("error",function(){ console.log("listener err"); }); //綁定關閉事件 server.on("close",function(){ //服務器關閉 如果還有鏈接存在,直到所有連接關閉 //這個事件才會被觸發 console.log("server stop listener"); });
測試客戶端
//引入net模塊 var net = require("net"); var netpkg = require("./netpak"); var c_sock = net.connect({ port:6800, host:"127.0.0.1", },()=>{ console.log("connected to server!"); }); c_sock.on("connect",function(){ console.log("connect success!"); //發送3個包 c_sock.write(netpkg.package_string("Hello")); c_sock.write(netpkg.package_string("starte")); c_sock.write(netpkg.package_string("end")); c_sock.write(netpkg.package_string("starte")); c_sock.write(netpkg.package_string("end")); //發送一個粘包數據 模擬粘包 c_sock.write(netpkg.test_pkg_two_action("AAAA","BBBB")); var buf_set = netpkg.test_pkg_slice("ABC","DEF"); console.log("模擬分包",buf_set); //發送一半 c_sock.write(buf_set[0]); //間隔一點時間 5秒後再次發送 剩余的包 setTimeout(function(){ c_sock.write(buf_set[1]); },5000); }); //綁定錯誤事件 c_sock.on("error",function(err){ console.log("錯誤"+err); }); //綁定關閉事件 c_sock.on("close",function(){ console.log("關閉socket"); }); c_sock.setEncoding("utf-8"); //接受數據的事件 c_sock.on("data",function(data){ console.log("收到數據",data); });
Node.js服務器開發(2)