1. 程式人生 > >Node.js服務器開發(2)

Node.js服務器開發(2)

不能 兩種 nag 事件驅動 可能 ace 協議 blob 本地

一、npm模塊安裝與管理


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)