Node.js服务器开发(2)-阿里云开发者社区

开发者社区> 技术小胖子> 正文

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模块

blob.png

安装好会输出版本 和 安装路劲 默认路劲就是这个

blob.png

我们就可以找到这个模块了,这个目录就是NODE_PATH指定的路劲

装好了之后,就是全局的了,所有的项目都能使用这个模块

blob.png




本地安装ws模块,本地安装是在运行npm目录/node_modules

比如说我的项目在这个目录下 D:/nodejs/myserver

所以本地安装的话,就会把模块放到这个项目目录下的node_modules

在这个文件夹下面安装shitf+鼠标右键 在此处打开命令窗口

这样我们就能直接在这运行npm了  

blob.png


安装

blob.png


成功

blob.png blob.png


但是他会提示你一个小错误,就是没有package_json 说明包

他会生成一个类似说明文件的东西.

只需要你 在命令行输入 nmp init 只需要使用一次即可

这时候输入项目名称

blob.png

版本

blob.png


描述

blob.png

入口函数 默认index.js

blob.png


测试命令 跳过

blob.png

跳过

blob.png

关键字

blob.png

作者

blob.png


然后这个文件就创建成功了, 

blob.png

当你再次使用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

blob.png



解析好之后,就要给客户端回报文

把那个key解析出来之后,要加上一个固定migic字符串

然后通过SHA-A加密,在通过base-64加密发送给客户端,

这时候就完成了握手协议.

blob.png



接收/发送数据协议.


1.比如你客户端要发送一个HELLO,他发送的时候把这个字符串

每个字符转成ASCLL码, 并且他不是直接发送出去.


blob.png

(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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
//加载模块
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事件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
//加载模块
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);









blob.png


在浏览器脚本里面使用websocket

blob.png










三、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)收到不足一个包,这个包要直接保存起来,等下一次接收.


封包 获取长度 模拟粘包 分包

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
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;
1
<br data-filtered="filtered">


服务器接收部分

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
//引入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");
 
});


测试客户端

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
//引入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);
});

blob.png





 本文转自超级极客51CTO博客,原文链接:http://blog.51cto.com/12158490/2068658,如需转载请自行联系原作者


版权声明:本文内容由阿里云实名注册用户自发贡献,版权归原作者所有,阿里云开发者社区不拥有其著作权,亦不承担相应法律责任。具体规则请查看《阿里云开发者社区用户服务协议》和《阿里云开发者社区知识产权保护指引》。如果您发现本社区中有涉嫌抄袭的内容,填写侵权投诉表单进行举报,一经查实,本社区将立刻删除涉嫌侵权内容。

相关文章
阿里云服务器如何登录?阿里云服务器的三种登录方法
购买阿里云ECS云服务器后如何登录?场景不同,大概有三种登录方式:
2578 0
阿里云服务器ECS远程登录用户名密码查询方法
阿里云服务器ECS远程连接登录输入用户名和密码,阿里云没有默认密码,如果购买时没设置需要先重置实例密码,Windows用户名是administrator,Linux账号是root,阿小云来详细说下阿里云服务器远程登录连接用户名和密码查询方法
9950 0
阿里云服务器端口号设置
阿里云服务器初级使用者可能面临的问题之一. 使用tomcat或者其他服务器软件设置端口号后,比如 一些不是默认的, mysql的 3306, mssql的1433,有时候打不开网页, 原因是没有在ecs安全组去设置这个端口号. 解决: 点击ecs下网络和安全下的安全组 在弹出的安全组中,如果没有就新建安全组,然后点击配置规则 最后如上图点击添加...或快速创建.   have fun!  将编程看作是一门艺术,而不单单是个技术。
9324 0
使用SSH远程登录阿里云ECS服务器
远程连接服务器以及配置环境
2143 0
使用OpenApi弹性释放和设置云服务器ECS释放
云服务器ECS的一个重要特性就是按需创建资源。您可以在业务高峰期按需弹性的自定义规则进行资源创建,在完成业务计算的时候释放资源。本篇将提供几个Tips帮助您更加容易和自动化的完成云服务器的释放和弹性设置。
11443 0
windows server 2008阿里云ECS服务器安全设置
最近我们Sinesafe安全公司在为客户使用阿里云ecs服务器做安全的过程中,发现服务器基础安全性都没有做。为了为站长们提供更加有效的安全基础解决方案,我们Sinesafe将对阿里云服务器win2008 系统进行基础安全部署实战过程! 比较重要的几部分 1.
8148 0
阿里云服务器如何登录?阿里云服务器的三种登录方法
购买阿里云ECS云服务器后如何登录?场景不同,云吞铺子总结大概有三种登录方式: 登录到ECS云服务器控制台 在ECS云服务器控制台用户可以更改密码、更换系统盘、创建快照、配置安全组等操作如何登录ECS云服务器控制台? 1、先登录到阿里云ECS服务器控制台 2、点击顶部的“控制台” 3、通过左侧栏,切换到“云服务器ECS”即可,如下图所示 通过ECS控制台的远程连接来登录到云服务器 阿里云ECS云服务器自带远程连接功能,使用该功能可以登录到云服务器,简单且方便,如下图:点击“远程连接”,第一次连接会自动生成6位数字密码,输入密码即可登录到云服务器上。
20998 0
21119
文章
0
问答
文章排行榜
最热
最新
相关电子书
更多
《2021云上架构与运维峰会演讲合集》
立即下载
《零基础CSS入门教程》
立即下载
《零基础HTML入门教程》
立即下载