Node.js服务器开发(2)

一、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);
});

原文地址:http://blog.51cto.com/12158490/2068658

时间: 2024-10-12 08:12:46

Node.js服务器开发(2)的相关文章

Node.js服务器原理详解

       本文和大家分享的是Node.js服务器原理相关内容,一起来看看吧,希望对大家有所帮助. web应用搭建需要什么条件? web应用,它需要一个客户端.还需要一个服务器.客户端这边,不需要我们去开发,我们直接通过浏览器就可以实现.而服务端这边需要我们自己开发.我们都知道,我们打开浏览器,比如说我们访问麦子学院.我们打开了这个网站过后,那么这个网站可以显示一个对应的网页.这个网页底层,它应该是一些html代码和css样式+我们的js动态效果来组成我们这个页面的.这些东西,我们要通过客户端

基于hapi的Node.js后端开发

基于hapi的Node.js后端开发 1.背景今年下半年公司线上程序频繁出现问题.不是内存跑满,就是CPU跑满就是程序自己挂掉了.严重影响了现有的小程序业务.目前线上主要架构是dubbo-x搭建的分布式.之前主要用来为app做数据服务.解决思路1:优化现有架构,找到问题(奈何本人虽然写了不少java代码.但是对java基本还是处于一窍不通的状态.)解决思路2:业务分离,为小程序从新搭建一套服务器系统.与原有的互不影响.这样既解决了小程序服务稳定性问题,也流出了时间来处理"思路1",不会

《Node.js入门》Windows 7下Node.js Web开发环境搭建笔记

近期想尝试一下在IBM Bluemix上使用Node.js创建Web应用程序.所以须要在本地搭建Node.js Web的开发測试环境. 这里讲的是Windows下的搭建方法,使用CentOS 的小伙伴请參考:<Node.js入门>CentOS 6.5下Node.js Web开发环境搭建笔记 Node.js是什么? 我们看看百科里怎么说的? JavaScript是一种执行在浏览器的脚本,它简单,轻巧.易于编辑,这样的脚本通经常使用于浏览器的前端编程.可是一位开发人员Ryan有一天发现这样的前端式

node.js项目开发问题集锦(不定期更新,随时把开发过程中遇到的问题加上)

1.用express开发站点时,怎么定义通用的头部和尾部 方案1:用类似asp时代的include添加,如ejs模板: <% include ../header.ejs %> <h1 class="page-header"> 这里是内容. 注:..表示header.ejs在上一级目录,ejs扩展名可以去掉,直接写:include ../header </h1> <% include ../footer %> 方案2:用类似于MVC的lay

Electron + React + Node.js + ES6 开发本地 App

Electron + React + Node.js + ES6 开发本地 App 1.概述 近来工作上需要做一款 PC 上的软件,这款软件大体来讲是类似 PPT 的一款课件制作软件.由于我最近几年专注于移动 App 的开发,对 PC 端开发的了解有些滞后.所以我首先需要看看,在 PC 上采用什么框架能够顺利完成我的工作. 我的目标是,在完成这款软件的同时能够顺便学习一下比较流行的技术.在经过前期技术调研后,我明确了实现这款软件所需要的技术条件: 不采用 C++ 方面的类库,比如 MFC.Qt.

node.js web开发:EXPRESS 4.x 以上使用session和cookie 的记录

关于session 和cookie 我搞了2-3天, 发现这个玩意也挺麻烦的. 很多教程都是把这种会话保存在nosql里面,比如mongo,或者redis等等.但是我还是想直接保存在计算机的内存中,比较符合传统的方式.网上也有很多这方面的文章,但基本上都是你抄我的,我抄你的,而且express有很多这种session/cookie的中间件,总是让人弄迷糊.很多文章都是点到为止,完全要靠自己慢慢去试,去看文档,去摸索. 我是基于connect这个中间件实现的.这个middleware 功能非常强大

从node.js模块化开发来了解javascript闭包

之前看过很多关于javascript闭包的解释,只有短短几个demo,一大段晦涩难懂的介绍,巴拉巴拉一大段闭包的概念,但是到最后也没看懂闭包到底是什么意思,起什么作用,直到昨晚凌晨,我在学习node.js模块开发的时候,才突然恍然大悟,所以赶紧记下来,免得以后忘记.代码来源:廖雪峰的网站node.js教程. 直接上代码: 我们首先编写了一个hello.js文件,这个hello.js文件就是一个模块,模块的名字就是文件名(去掉.js后缀),所以hello.js文件就是名为hello的模块. 我们把

SSH2+LigerUI+JBPM5+Node.js实战开发视频教程

基于SSH2+LigerUI+JBPM5+Node.js技术实现大型J2EE金融行业财务预算系统 (第三季适合二年以上的开发者)课程分类:Java框架适合人群:中级课时数量:51课时用到技术:SSH2.LigerUI.JBPM5.Node.js技术涉及项目:大型J2EE金融行业财务预算系统咨询qq:1840215592课程项目所采用的技术架构为:struts2+spring+hibernate+LigerUI+jbpm5+mysql+自定义表单+node.js+webservice+思维导图+j

纯正商业级应用-Node.js Koa2开发微信小程序服务端

第1章 前言.导学与node.js如何理解Node.js?前端到底要不要学习Node.js?本课程能让你学到什么? 第2章 Koa2的那点事儿与异步编程模型Koa非常的精简,基本上,没有经过二次开发的Koa根本“不能”用.本章我们讲解Koa的重要特性,理解什么是洋葱模型?以及在KOA中如何进行异步编程?很多同学都了解以上知识点,但听完本章,你会有一些不一样的理解,比如:为什么要有洋葱模型?没有会怎样?Koa中间件一定是异步的吗? ... 第3章 路由系统的改造Koa-router需要进行一些改造