《深入浅出node.js》读书笔记6-1网络编程

Node是一个面向网络而生的平台,它具有事件驱动、无阻塞、单线程等特性,具备良好的可伸缩性,使它十分轻量,适合在分布式网络中扮演各种各样的角色。
Node只需几行代码即可构建服务器,无需额外的容器。
Node提供net、dgram、http、https4个模块,分别用于TCP、UDP、HTTP、HTTPS。适用于服务器和客户端。

构建TCP服务

TCP服务在网络应用中十分常见,目前大多数的应用都是基于TCP搭建而成。

TCP

TCP全名为传输控制协议,在OSI模型中属于传输层协议。TCP协议是面向连接的协议,其显著的特征是在传输之前需要3次握手形成会话。只有形成会话之后,服务器端和客户端之间才能互相发送数据。

创建TCP服务器端

创建一个TCP服务端来接受网络请求

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

var server = net.createServer(function(socket){
// 新的连接
socket.on('data', function(data){
socket.write('你好')
})

socket.on('end', function() {
console.log('连接断开')
})
socket.write("欢迎光临《深入浅出node.js》示例:\n");
})

server.listen(8124, function(){
console.log('server bound');
})

通过net模块自行构建客户端进行会话,测试上面构建的TCP服务的代码,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
var net = require('net');
var client = net.connect({port: 8124}, function(){
console.log('client connected');
client.write('world!\r\n');
})

client.on('data', function(data){
console.log(data.toString());
client.end();
})

client.on('end', function(){
console.log('client disconnected');
})

TCP服务的事件

1. 服务器事件

对于通过net.createServer()创建的服务器而言,它是一个EventEmitter实例,它的自定义事件有如下几种:

  • listening: 在调用server.listen()绑定端口或者Domain Socket后触发。
  • connection: 每个客户端套接字连接到服务器时触发,简洁写法为通过net.createServer()的最后一个参数传递。
  • close: 当服务器关闭时触发,在调用server.close()后,服务器将停止接受新的套接字连接,但保持当前存在但连接,等待所有连接都断开后,会触发该事件。
  • error: 当服务器发送异常时,将会触发该事件。比如侦听一个使用中的端口,将会触发一个异常,如果不侦听error事件,服务器将会抛出异常。

2. 连接事件

服务器可以同时与多个客户端保持连接,对于每个连接而言是典型的可读可写Stream对象。Stream对象可以用于服务队与客户端之间的通信,既可以通过data事件从一端读取另一端发来的数据,也可以通过write()方法从一端向另一端发送数据。它具有以下自定义事件:

  • data: 当一端用write()发送数据时,另一端会触发data事件,事件传递的数据即是write()发送的数据。
  • end: 当连接中的任意一端发送了FIN数据时,将会触发该事件。
  • connect: 该事件用于客户端,当套接字与服务器连接成功时会被触发。
  • drain: 当任意一端调用write()发送数据时,当前这端会触发该事件。
  • error: 当异常发生时,触发该事件。
  • close: 当套接字完全关闭时,触发该事件。
  • timeout: 当一定时间后连接不再活跃时,该事件会被触发,通知用户当前该连接已经闲置了。

另外,TCP套接字是可读可写的Stream对象,可以利用pipe()方法巧妙地实现管道操作,代码如下:

1
2
3
4
5
6
var net = require('net');

var server = net.createServer(function(socket){
socket.write('Echo server\r\n')
socket.pipe()
})

在node中, 默认开启Nagle算法,可以调用socket.setNoDelay(true)去掉Nagle算法,使得write()可以立即发生数据到网络中。

构建UDP服务

UDP又称用户数据包协议,也属于网路传输层。UDP与TCP最大的不同是UDP不是面向连接的。TCP一旦建立,所有的会话都基于连接完成,客户端如果要与另一个TCP服务通信,需要另创建一个套接字来完成连接。但在UDP中,一个套接字可以与多个UDP服务通信。它虽然提供面向事务的简单不可靠信息传输服务,在网络差的情况下存在丢包严重的问题,但是由于它无须连接,资源消耗低,处理快速且灵活,所以常常用在偶尔丢一两个数据包也不会产生重大影响的场景,比如音频、视频、DNS等。DNS服务即是基于它实现的。

创建UDP套接字

UDP套接字一旦创建,既可以作为客户端发生数据,也可以作为服务器接收数据。下面代码创建UDP套接字:

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

创建UDP服务器端

若想让UDP套接字接收网络消息,只要调用dgram.bind(post, [address])方法对网卡和端口进行绑定即可。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
var dgram = require('dgram');

var server = dgram.createSocket('udp4');

server.on('message', function(msg, rinfo){
console.log('server got: ' + msg + ' from' + rinfo.address + ':' + rinfo.port);
})

server.on('listening', function(){
var address = server.address();
console.log('server listening' + address.address + ':' + address.port);
})

server.bind(41234)

该套接字将接收所用网卡上41234端口上的消息。在绑定完成后,将触发listening事件。

创建UDP客户端

我们创建一个客户端与服务端进行对话:

1
2
3
4
5
6
7
var dgram = require('dgram');

var message = new Buffer('深入浅出Node.js');
var client = dgram.createSocket('udp4');
client.send(message, 0, message.length, 41234, 'localhost', function(err, bytes){
client.close();
})

当套接字对象用在客户端时,可以调用send()方法,文档

UDP套接字事件

UDP套接字只是一个Event Emitter实例,而非Stream的实例。它具备如下自定义事件:

  • message: 当UDP套接字侦听网卡端口后,接收到消息时触发该事件,触发携带当数据为消息Buffer对象和一个远程地址信息。
  • listening: 当UDP套接字开始侦听时触发事件。
  • close: 调用close()方法时触发该事件,并不再触发message事件。如需再次触发message事件,重新绑定即可。
  • error: 当异常发生时触发该事件,如果不侦听,异常将直接抛出,使进程退出。

构建HTTP服务

Node提供基本的http和https模块用于HTTP和HTTPS的封装。

1
2
3
4
5
6
var http = require('http');
http.createServer(function(req, res){
res.writeHead(200, {'Content-type': 'text/plain'});
res.end('Hello World\n');
}).listen(1337, '127.0.0.1');
console.log('Server running at http://127.0.0.1:1337/');

HTTP模块

Node中,HTTP服务与TCP服务有区别在于:
开启keep live后,一个TCP会话可以用于多次请求和响应。
TCP服务以connection为单位进行服务,HTTP服务以request为单位进行服务。http模块即是将connection到request的过程进行封装。

1. HTTP请求

对于TCP连接的读操作,http模块将其封装为ServerRequest对象。
请求行会被http_parse解析为req.method,req.url,req.httpVersion属性.
请求报文头部被解析为req.headers
请求报名实体则抽象为一个只读流对象,如果业务逻辑需要读取报文体中的数据,则需要在这个数据流结束后才能进行操作。如下

1
2
3
4
5
6
7
8
9
10
function(req, res) {
var buffers = [];
req.on('data', function(trunk){
buffers.push(thrunk);
}).on('end', function(){
var buffer = Buffer.concat(buffers);
//TODO
res.end('Hello world');
});
}

2. HTTP响应

HTTP响应封装流对底层连接对写操作,可以将其看成一个可写对流对象。
res.setHeader(),res.writeHead()用于设置响应报文头部。
res.write(),res.end()用于设置响应报文实体。

3. HTTP服务事件

HTTP服务器也是一个EventEmitter实例,也抽象一些事件,以供应用层使用。

HTTP客户端

http.request(options, connect),用于构造HTTP客户端。

1
2
3
4
5
6
7
8
9
10
11
12
13
var options = { 
hostname: '127.0.0.1',
port: 1334,
path: '/',
};
var req = http.request(options, function(res) {
console.log('STATUS: ' + res.statusCode);
console.log('HEADERS: ' + JSON.stringify(res.headers)); res.setEncoding('utf8');
res.on('data', function (chunk) {
console.log(chunk);
});
});
req.end();

options参数决定了这个HTTP请求头中的内容,文档.

1. HTTP代理

和服务器端的实现一样,http提供的ClientRequest对象也是基于TCP层实现的。在keeplive的情况下,一个底层会话连接可以多次用于请求。为了重用TCP连接,http模块包含一个默认的客户端代理对象http.globalAgent。它对每个服务端(host + port)创建的连接进行了管理,默认情况下,通过ClientRequest对象对同一个服务器端发起对HTTP请求最多可以创建5个连接。在options中可以设置自定义的agent对象,解除默认连接限制。

2. HTTP客户端事件

与服务队对应,HTTP客户端也有相应的事件:

构建WebSocket服务

Node与WebSocket协议是绝配:

  • WebSocket客户端基于事件的编程模型与Node的自定义事件相差无几;
  • WebSocket实现了客户端与服务端之间的长连接,而Node事件驱动的方式十分擅长与大量的客户端保持高并发连接。

WebSocket与传统HTTP有如下好处:

  • 客户端与服务端只建立一个TCP连接,更少的连接请求;
  • WebSocket服务器可以推送数据到客户端;
  • 更轻量级的协议头,减少数据传送量。

WebSocket在客户端的应用示例:

1
2
3
4
5
6
7
8
9
10
11
12
var socket = new WebSocket('ws://127.0.0.1:12010/updates');
socket.onopen = function(){
setInterval(function(){
if(socket.bufferedAmount === 0) {
socket.send(getUpdateData());
}
}, 50);
};

socket.onmessage = function(event) {
//TODO
};

WebSocket协议主要分为:握手和数据传输。
WebSocket是在TCP上定义独立的协议,但是WebSocket的握手部分是有HTTP完成的。

WebSocket握手

客户端建立连接时,通过HTTP发起请求报文,如下:

1
2
3
4
5
GET /chat HTTP/1.1
Host: server.example.com
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ== Sec-WebSocket-Protocol: chat, superchat Sec-WebSocket-Version: 13

与普通的HTTP请求协议略有区别的部分在于这些协议头:

1
2
3
4
5
6
7
8
9
10
// 上面两个协议表示请求服务器升级协议为WebSocket
Upgrade: websocket,
Connection: Upgrade,

// 用于安全校验
Sec-Websocket-Key: dGhlIHNhbXBsZSBub25jZQ==

// 指定子协议和版本号
Sec-WebSocket-Protocol: chat, superchat
Sec-WebSocket-Version: 13

服务端在处理完请求后,响应报文如下:

1
2
3
4
HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade 6 Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=
Sec-WebSocket-Protocol: chat

WebSocket 数据传输

握手完成后,当前连接不再进行HTTP交互,而是开始WebSocket的数据帧协议。

握手完成后,客户端的onopen()将会被触发执行。
服务端则没有onopen()。为了完成TCP套接字事件到WebSocket事件的封装,需要在接收数据时进行处理,WebSocket的数据帧即是在底层data事件上封装完成的。同样的数据发送时,也需要做封装操作。
为了安全考虑,客户端需要发送的数据帧进行掩码处理,服务端一旦收到无掩码帧,连接将关闭。而服务端发送到客户端的数据帧无须做掩码处理,客户端收到带掩码的数据帧,连接也将关闭。
下图为WebSocket数据帧的定义,每8位为一列,也即1个字节。其中每一位都有它的意义。

每一位的定义见书P168。

网络服务与安全

IEIF将SSL(Secure Sockets Layer,安全套接层)标准化为TLS(Transport Layer Security,安全传输层协议)。
Node在网络上提供3个模块:

  • crypto: 用于加密解密,
  • tls: 与net模块类似,但是区别在于它建立在TLS/SSL加密的TCP连接上,
  • https: 与http模块完全一致,区别也仅在于它建立于安全的连接之上

    TLS/SSL

    1. 密钥

    TLS/SSL是一个公钥/私钥的结构,它是一个非对称的结构。每个客户端与服务器端都有自己的公私钥。公钥用来加密要传输的数据,私钥用来解密接收到的数据。公钥和私钥是配对的,通过公钥加密的数据,只有通过私钥才能解密,所以在建立安全传输之前,客户端和服务器端之间需要交换公钥。客户端发送数据时要通过服务器端的公钥进行加密,服务器端发送数据时则需要客户端的公钥进行加密。如图

Node的底层采用的是openssl实现TLS/SSL的。
在服务器和客户端生成私钥:

1
2
3
4
// 生成1024位长的RSA私钥文件
$ openssl genrsa -out server.key 1024
$ openssl genrsa -out client.key 1024
`

在服务器和客户端生成公钥:

1
2
3
// 在私钥基础上生成公钥
$ openssl rsa -in server.key -pubout -out server.pem
$ openssl rsa -in client.key -pubout -out client.pem

为了解决窃听情况(如中间人攻击),TSL/SSL引入数字证书来进行认证。数字证书中包含另服务器的名称和主机名、服务器的公钥、签名颁发机构的名称、来自签名颁发机构的签名。在建立连接前,会通过证书中的签名确认收到的公钥来自目标服务器。

2. 数字证书

CA(Certificate Authority, 数字证书认证中心),用来为站点颁发证书,且这个证书中具有CA通过自己的公钥和私钥实现的签名。
为了得到签名证书,服务器端需要通过自己的私钥生成CSR文件(Certificate Signing Request, 证书签名请求)。CA机构将通过这个文件颁发属于该服务器的签名证书。
通过CA机构颁发证书通常是一个繁琐的过程,需要付出一定的精力和费用。对于中小型企业而言,多半是采用自签名证书来构建安全网络。所谓自签名证书,就是自己扮演CA机构,给自己的服务器端颁发签名证书。以下为生成私钥、生成CSR文件、通过私钥自签名生成证书的过程:

1
2
3
$ openssl genrsa -out ca.key 1024
$ openssl req -new -key ca.key -out ca.csr
$ openssl x509 -req -in ca.csr -signkey ca.key -out ca.crt

其流程如图:

上述步骤完成了扮演CA角色需要的文件。
接下来回到服务器端,服务器端需要向CA机构申请签名证书。在申请签名证书之前依然是要创建自己的CSR文件。(这个过程中的Common Name要匹配服务器域名)。

1
$ openssl req -new -key server.key -out server.csr

得到CSR文件后,向我们自己的CA机构申请签名把,签名过程需要CA的证书和私钥参与,最终颁发一个带有CA签名的证书,如下:

1
$ openssl x509 -req -CA ca.crt -CAkey ca.key -CAcreateserial -in server.csr -out server.crt

客户端在发起安全连接前会去获取服务器端的证书,并对CA的证书验证服务器端的证书、服务器名称、IP地址进行验证。

CA机构将证书颁发给服务器端后,证书在请求端过程中会被发送给客户端,客户端需要通过CA端证书验证真伪。如果是知名的CA机构,它们的证书一般预装在浏览器中。如果是自己扮演CA机构,颁发自有签名证书则不能享受这个福利,客户端需要获取到CA的证书才能进行验证。
在CA那里的证书不需要上级证书参与签名,这个证书通常成为根证书。

TLS服务

1. 创建服务器端

将构建服务所需要的证书都备齐之后,我们通过Node的tls模块创建一个安全的TCP服务,代码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
var tls = require('tls'); var fs = require('fs');
var options = {
key: fs.readFileSync('./keys/server.key'),
cert: fs.readFileSync('./keys/server.crt'), requestCert: true,
ca: [ fs.readFileSync('./keys/ca.crt') ]
};
var server = tls.createServer(options, function (stream) {
console.log('server connected', stream.authorized ? 'authorized' : 'unauthorized');
stream.write("welcome!\n");
stream.setEncoding('utf8');
stream.pipe(stream);
});
server.listen(8000, function() {
console.log('server bound');
});

启动上述服务后,通过下面的命令可以测试证书是否正常:

1
$ openssl s_client -connect 127.0.0.1:8000

2. TLS客户端

我们用Node来模拟客户端。
首先,客户端生成属于自己的私钥、签名

1
2
3
4
5
6
// 创建私􏲋
$ openssl genrsa -out client.key 1024
// 生成CSR
$ openssl req -new -key client.key -out client.csr
// 生成签名证书
$ openssl x509 -req -CA ca.crt -CAkey ca.key -CAcreateserial -in client.csr -out client.crt

然后创建客户端

1
2
3
4
5
6
7
8
9
10
11
12
var fs = require('fs'); 
var tls = require('tls');
var options = {
key: fs.readFileSync('./keys/client.key'), cert: fs.readFileSync('./keys/client.crt'), ca: [ fs.readFileSync('./keys/ca.crt') ]
};
var stream = tls.connect(8000, options, function () {
console.log('client connected', stream.authorized ? 'authorized' : 'unauthorized'); process.stdin.pipe(stream);
});
stream.setEncoding('utf8'); stream.on('data', function(data) {
console.log(data); });
stream.on('end', function() { server.close();
});

HTTPS服务

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

1. 装备证书

HTTPS服务需要用到私钥和签名证书。

2. 创建HTTPS服务

创建HTTPS服务只比HTTP服务多一个选项配置,其余地方几乎相同:

1
2
3
4
5
6
7
var https = require('https'); var fs = require('fs');
var options = {
key: fs.readFileSync('./keys/server.key'), cert: fs.readFileSync('./keys/server.crt')
};
https.createServer(options, function (req, res) { res.writeHead(200);
res.end("hello world\n");
}).listen(8000);

3. HTTPS客户端

我们用Node来实现HTTPS的客户端

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
var https = require('https');
var fs = require('fs'); 2
var options = {
hostname: 'localhost',
port: 8000,
path: '/', 3 method: 'GET',
key: fs.readFileSync('./keys/client.key'),
cert: fs.readFileSync('./keys/client.crt'),
ca: [fs.readFileSync('./keys/ca.crt')]
};
options.agent = new https.Agent(options);
var req = https.request(options, function(res) { res.setEncoding('utf-8');
res.on('data', function(d) {
console.log(d);
});
});
req.end();
req.on('error', function(e) {
console.log(e);
});

小结

Node基于事件驱动和非阻塞设计,在分布式环境中尤其能发挥出它的特长,基于事件驱动可以实现与大量的客户端进行连接,非阻塞设计则让它可以更好的提升网络的响应吞吐。Node提供了相对底层的网路调用,以及基于事件的编程接口,使得开发者能轻松的构建网络应用。

liborn wechat
欢迎您扫一扫上面的微信二维码,订阅我的公众号!