Node.js网络编程

在Web领域,大多数编程语言需要专门的Web服务器容器,如ASP需要IIS服务器,Java需要Tomcat服务器,PHP需要Apache服务器。Node则不需要额外的容器。

Node提供了多个模块:

  • net:TCP

  • dgram:UDP

  • http:HTTP

  • https:HTTPS

构建TCP服务

TCP:传输控制协议。传输层

大多数网络应用是基于TCP搭建的。

创建会话,服务端和客户端分别提供一个套接字,实现连接。

创建TCP服务器端:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
var net = require('net');
var server = net.createServer(function(socket){
socket.on('data', function(data){
// 接收到数据
socket.write('接收到数据:' + data);
});
socket.on('end', function(){
// 连接断开
socket.write('连接断开');
});
socket.write('Hello!!!');
});
// 监听接口
server.listen(8124, fcuntion(socket){
// 新的连接
});

TCP服务事件:

  • 服务器事件

  • 连接事件

服务器可以同时与多个客户端保持连接,每个连接都是可读可写的Stream对象。

TCP对小数据包有优化策略:Nagle算法。合并成一个大数据包再发送,可以导致延迟,Node默认是开启的,可以关闭。

1
socket.setNoDelay(true);

构建UDP服务

UDP:用户数据包服务。不是面向连接的。

在UDP中,一个socket可以与多个UDP服务通信。

适用于允许丢包的场景,如音视频。

创建UDP套接字,既可以作为客户端发送数据,也可以作为服务器接收数据。

1
2
var dgram = require('dgram');
var socket = dgram.createSocket('udp4');

接收数据

1
2
3
4
5
6
7
8
9
10
11
// 监听接收的数据
socket.on('message', function(msg, rinfo){
console.log('接收到:', msg);
console.log('来自:' + rinfo.address)
});
// 监听开始事件
socket.on('listen', function(){

});
// 设置接收端口
sokcet.bind(41234);

发送数据

1
2
3
4
5
var msg = Buffer.form('Hello');
// 给指定主机发送数据
socket.send(msg, 0, msg.length, 41234, 'localhost', function(err, bytes){
// 发送完成回调
});

构建HTTP服务

TCP和UDP都是传输层协议,用于构建高效的网络应用。如果支持普通的应用,使用应用层协议就绰绰有余,如 HTTP。

HTTP构建在TCP之上

HTTP报文:报文头+报文体

TCP服务以connection为单位,HTTP服务以request为单位,一个TCP会话可以用于多次HTTP请求和响应。

http模块是将connection到request的过程进行了封装。

HTTP服务端

ServerRequest

ServerResponse

1
2
3
4
5
6
7
8
9
10
11
12
13
14
var http = require('http');
var server = http.createServer(function(req, res){
// req是请求报文
// req.method
// req.url
// 写入响应头
res.writeHead(200, {
'Content-Type': 'text/plain'
});
res.write();
res.end();
});
//
server.listen(1337, '127.0.0.1');

请求报文的主体是一个只读流对象,响应报文的主体是一个可写流对象。

HTTP服务的事件

  • connection:客户端与服务端建立连接时触发

  • request:在解析出请求头时触发

  • close:服务器关闭时(所有连接都断开)触发

  • connect:当客户端发起CONNECT请求时触发

  • checkContinue:当客户端发送头部带Expect: 100-continue的请求时触发

  • upgrade:当客户端要求升级协议时触发,与WebSocket有关

  • clientError

HTTP客户端

ClientRequest

ClientResponse

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
var http = require('http');
// 请求头内容
var options = {
hostname: '127.0.0.1',
port: 1334,
method: 'GET',
path:'/'
}
// 创建请求ClientRequest
var req = http.request(options, function(res){
// 响应内容
res.setEncoding('utf8');
res.on('data', function(chunk){

})
});
req.end();

http模块包含一个默认的客户端代码对象http.globalAgent,对连接进行管理,底层是线程池,对同一个服务端的请求最多创建5个连接(同浏览器限制相同)

HTTP客户端事件

  • response:得到服务端响应

  • socket:当底层连接分配给当前请求时

  • connect:当服务端响应了CONNECT请求时

  • upgrade:当服务端响应了101 Switching Protocols状态时

  • continue:当服务端响应了100 Continue状态时

构建WebSocket服务

Node没有内置WebSocket库,社区的ws模块封装了WebSocket底层实现。

WebSocket相对HTTP的好处:

  • 客户端与服务端只建立一个TCP连接

  • 服务端可以推送数据到客户端,双向通信

  • 协议头更轻量

WebSocket客户端

1
2
3
4
5
6
7
8
9
10
var socket = new WebSocket('ws://127.0.0.1:12100/updates');
// 建立连接
socket.onopen(function(){
// 向服务端发送数据
socket.send();
});
// 接收服务端事件
socket.onmessage(function(event){

});

WebSocket协议分为:握手和数据传输

通过HTTP发送升级协议请求,特有协议头:

1
2
3
4
5
Upgrade: websocket,
Connection: Upgrade
Sec-WebSocket-Key:
Sec-WebSocket-Protocol: char, superchat
Sec-WebSocket-Version: 13

重点:Sec-WebSocket-Key的值用于校验。

(1)客户端随机生成一个Base64编码的字符串key。

(2)服务端拿到该字符串后,与一串特定字符串拼接,计算SHA1散列值,再进行Base64编码,放在响应的Sec-WebSocket-Accept中。

1
Base64(sha1(key+'xxx'))

(3)客户端通过响应拿到返回值,进行校验

1
Base64(sha1(key+'xxx')) == accept

WeBSocket数据帧协议

客户端需要对发送的数据帧进行掩码处理,服务端发送给客户端的则不能做掩码处理。

每8位(1个字节)为一列。

每一位的意义:

  • opcode:4位操作码,用来描述当前帧

  • masked:1位,是否进行掩码处理,客户端为1需要掩码,服务端为0不需要掩码

  • payload length:7或7+16或7+64位,标识数据长度

  • masking key:32位,掩码

  • payload data:位数为8的倍数,数据

发送数据时,需要构造一个或多个数据帧协议报文。

客户端发送报文时,需要使用掩码对二进制进行加密,服务端收到后,需要进行解密。

服务端发送报文时,无需掩码。

网络服务与安全

SSL(Secure Sockets Layer,安全套接协议)很早就提出了,是一种安全协议,在传输层对网络连接进行加密和解密,应用层是透明的,后来标准化为 TLS(Transport Layer Security)安全传输层协议。

Node提供了3个安全方面的模块:

  • crypto:各种加密算法

  • tls:功能与net模块类似,但建立在 TLS/SSL 加密的 TCP 连接上

  • https:功能和http模块类似,只是建立在安全连接上

TLS/SSL

基于非对称加密

客户端和服务端都有自己的公钥/私钥。公钥加密要发送的数据,私钥解密接收到的数据。

在建立安全传输之前,客户端和服务端需要互换公钥。给谁发送消息就用谁的公钥进行加密,对方接收到使用自己的私钥就可以解密。

Node的底层采用openssl来实现TLS/SSL。

生成私钥

1
openssl genrsa

生成公钥

1
openssl rsa

中间人攻击:在客户端和服务器交换公钥时,攻击者插入其中,同时扮演客户端和服务端,获得双方的公钥,这样就可以解密拿到双方的真实数据。

为了解决中间人攻击,TLS/SSL引入了“数字证书”。

数字证书

主要包含:

  • 服务器基本信息

  • 服务器公钥

  • 认证机构信息

  • 认证机构的签名

引入了一个第三方——CA(Certificate Authority,证书认证机构),CA的作用是为站点颁发证书。

通过正规的证书机构颁发证书需要付出一定的精力和费用,大部分小公司采用“自签名证书”,即自己扮演CA,给自己的服务器颁发证书。

服务器端:

  • 通过私钥生成CSR(Certificate Signing Request,证书签名请求)

  • 把CSR交给CA

CA端颁发证书:

  • 生成自己的私钥key

  • 生成自己的csr

  • 生成自己的证书crt

  • 用自己的私钥key和证书crt给服务器的csr生成服务器的crt

  • 把服务器crt交给服务器

客户端连接前会获取服务器证书,并通过CA证书验证服务端证书的真伪。

知名CA机构的证书一般预装在浏览器中,自签名证书客户端需要获取。

TLS服务

创建服务器端

1
2
3
4
5
6
7
8
9
10
11
12
13
var tls = require('tls')
var fs = require('fs')
var options = {
// 服务器私钥
key: fs.readFileSync('/keys/server.key'),
// 服务器证书
cert: fs.readFileSync('/keys/server.cert'),
requestCert: true,
// CA证书
ca: [fs.readFileSync('/keys/ca.cert')]
}
var server = tls.createServer(options, function(stream))
server.listen(8000, function)

创建客户端

1
2
3
4
5
6
7
8
9
10
11
12
13
14
var tls = require('tls')
var fs = require('fs')
var options = {
// 客户端私钥
key: fs.readFileSync('/keys/server.key'),
// 客户端证书
cert: fs.readFileSync('/keys/server.cert'),
// CA证书
ca: [fs.readFileSync('/keys/ca.cert')]
}
var stream = tls.connect(8000, options, function)
stream.setEncoding('uft8')
stream.on('data', function);
stream.on('end', function);

HTTPS服务

HTTPS服务是工作在TLS/SSL上的HTTP。

服务端需要准备私钥和签名证书。

HTTPS服务端

1
2
3
4
5
6
7
8
9
10
var https = require('https')
var fs = require('fs')
var options = {
key: fs.readFileSync('./keys/server.key'),
cert: fs.readFileSync('./keys/server.cert')
}
https.createServer(options, function(req, res){
res.writeHead()
res.end()
}).listen(8000);

HTTPS客户端

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
var https = require('https')
var fs = require('fs')


var options = {
hostname: 'localhost',
port: 8000,
path: '/',
method: 'GET',
key: fs.readFileSync('./keys/client.key'),
cert: fs.readFileSync('./keys/client.cert'),
ca: [fs.readFileSync('./keys/ca.cert')]
}
options.agent = new https.Agent(options)
var req = https.request(options, function(res){})
req.end()