Node是一个面向网络而生的平台,它具有事件驱动、无阻塞、单线程等特性,具备良好的可伸缩性,使它十分轻量,适合在分布式网络中扮演各种各样的角色。
Node只需几行代码即可构建服务器,无需额外的容器。
Node提供net、dgram、http、https4个模块,分别用于TCP、UDP、HTTP、HTTPS。适用于服务器和客户端。
构建TCP服务
TCP服务在网络应用中十分常见,目前大多数的应用都是基于TCP搭建而成。
TCP
TCP全名为传输控制协议,在OSI模型中属于传输层协议。TCP协议是面向连接的协议,其显著的特征是在传输之前需要3次握手形成会话。只有形成会话之后,服务器端和客户端之间才能互相发送数据。
创建TCP服务器端
创建一个TCP服务端来接受网络请求
1 | var net = require('net'); |
通过net模块自行构建客户端进行会话,测试上面构建的TCP服务的代码,如下:
1 | var net = require('net'); |
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 | var net = require('net'); |
在node中, 默认开启Nagle算法,可以调用socket.setNoDelay(true)
去掉Nagle算法,使得write()
可以立即发生数据到网络中。
构建UDP服务
UDP又称用户数据包协议,也属于网路传输层。UDP与TCP最大的不同是UDP不是面向连接的。TCP一旦建立,所有的会话都基于连接完成,客户端如果要与另一个TCP服务通信,需要另创建一个套接字来完成连接。但在UDP中,一个套接字可以与多个UDP服务通信。它虽然提供面向事务的简单不可靠信息传输服务,在网络差的情况下存在丢包严重的问题,但是由于它无须连接,资源消耗低,处理快速且灵活,所以常常用在偶尔丢一两个数据包也不会产生重大影响的场景,比如音频、视频、DNS等。DNS服务即是基于它实现的。
创建UDP套接字
UDP套接字一旦创建,既可以作为客户端发生数据,也可以作为服务器接收数据。下面代码创建UDP套接字:
1 | var dgram = require('dgram'); |
创建UDP服务器端
若想让UDP套接字接收网络消息,只要调用dgram.bind(post, [address])
方法对网卡和端口进行绑定即可。
1 | var dgram = require('dgram'); |
该套接字将接收所用网卡上41234端口上的消息。在绑定完成后,将触发listening事件。
创建UDP客户端
我们创建一个客户端与服务端进行对话:
1 | var dgram = require('dgram'); |
当套接字对象用在客户端时,可以调用send()
方法,文档
UDP套接字事件
UDP套接字只是一个Event Emitter实例,而非Stream的实例。它具备如下自定义事件:
- message: 当UDP套接字侦听网卡端口后,接收到消息时触发该事件,触发携带当数据为消息Buffer对象和一个远程地址信息。
- listening: 当UDP套接字开始侦听时触发事件。
- close: 调用
close()
方法时触发该事件,并不再触发message事件。如需再次触发message事件,重新绑定即可。 - error: 当异常发生时触发该事件,如果不侦听,异常将直接抛出,使进程退出。
构建HTTP服务
Node提供基本的http和https模块用于HTTP和HTTPS的封装。
1 | var http = require('http'); |
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 | function(req, res) { |
2. HTTP响应
HTTP响应封装流对底层连接对写操作,可以将其看成一个可写对流对象。res.setHeader()
,res.writeHead()
用于设置响应报文头部。res.write()
,res.end()
用于设置响应报文实体。
3. HTTP服务事件
HTTP服务器也是一个EventEmitter实例,也抽象一些事件,以供应用层使用。
HTTP客户端
http.request(options, connect)
,用于构造HTTP客户端。
1 | var options = { |
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 | var socket = new WebSocket('ws://127.0.0.1:12010/updates'); |
WebSocket协议主要分为:握手和数据传输。
WebSocket是在TCP上定义独立的协议,但是WebSocket的握手部分是有HTTP完成的。
WebSocket握手
客户端建立连接时,通过HTTP发起请求报文,如下:
1 | GET /chat HTTP/1.1 |
与普通的HTTP请求协议略有区别的部分在于这些协议头:
1 | // 上面两个协议表示请求服务器升级协议为WebSocket |
服务端在处理完请求后,响应报文如下:
1 | HTTP/1.1 101 Switching Protocols |
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 | // 生成1024位长的RSA私钥文件 |
在服务器和客户端生成公钥:
1 | // 在私钥基础上生成公钥 |
为了解决窃听情况(如中间人攻击),TSL/SSL引入数字证书来进行认证。数字证书中包含另服务器的名称和主机名、服务器的公钥、签名颁发机构的名称、来自签名颁发机构的签名。在建立连接前,会通过证书中的签名确认收到的公钥来自目标服务器。
2. 数字证书
CA(Certificate Authority, 数字证书认证中心),用来为站点颁发证书,且这个证书中具有CA通过自己的公钥和私钥实现的签名。
为了得到签名证书,服务器端需要通过自己的私钥生成CSR文件(Certificate Signing Request, 证书签名请求)。CA机构将通过这个文件颁发属于该服务器的签名证书。
通过CA机构颁发证书通常是一个繁琐的过程,需要付出一定的精力和费用。对于中小型企业而言,多半是采用自签名证书来构建安全网络。所谓自签名证书,就是自己扮演CA机构,给自己的服务器端颁发签名证书。以下为生成私钥、生成CSR文件、通过私钥自签名生成证书的过程:
1 | $ openssl genrsa -out ca.key 1024 |
其流程如图:
上述步骤完成了扮演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 | var tls = require('tls'); var fs = require('fs'); |
启动上述服务后,通过下面的命令可以测试证书是否正常:
1 | $ openssl s_client -connect 127.0.0.1:8000 |
2. TLS客户端
我们用Node来模拟客户端。
首先,客户端生成属于自己的私钥、签名
1 | // 创建私 |
然后创建客户端
1 | var fs = require('fs'); |
HTTPS服务
HTTPS服务就是工作在TLS/SSL上的HTTP。
1. 装备证书
HTTPS服务需要用到私钥和签名证书。
2. 创建HTTPS服务
创建HTTPS服务只比HTTP服务多一个选项配置,其余地方几乎相同:
1 | var https = require('https'); var fs = require('fs'); |
3. HTTPS客户端
我们用Node来实现HTTPS的客户端
1 | var https = require('https'); |
小结
Node基于事件驱动和非阻塞设计,在分布式环境中尤其能发挥出它的特长,基于事件驱动可以实现与大量的客户端进行连接,非阻塞设计则让它可以更好的提升网络的响应吞吐。Node提供了相对底层的网路调用,以及基于事件的编程接口,使得开发者能轻松的构建网络应用。