Node.js服务器开发
Posted
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Node.js服务器开发相关的知识,希望对你有一定的参考价值。
一、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服务器开发的主要内容,如果未能解决你的问题,请参考以下文章