在构建网络通信服务方面,相比于其他老牌后端语言,Node.js 同样能够胜任(也许更胜一筹),并且有自己独特的处理方式。node是一个面向网络而生的平台,它的事件驱动、非阻塞、单线程使node应用程序具有低内存、高并发、伸缩性强的优良特性,适合在分布式网络大展身手。Node底层实现了传输层TCP/UDP、应用层HTTP/HTTPS的功能并封装成贴合网络的API,并且可以自己创建服务器而不依赖三方服务,使用起来非常方便、简单、灵活。对于网络编程,node提供了net、dgram、http、https 4个模块,分别用于处理TCP、UDP、HTTP、HTTPS。本文将介绍这些模块并利用这些模块提供的API构建简单的网络服务。

正文

其实无论什么语言、什么平台,实现网络编程都需要遵循网络标准规范,只不过具体实现或者提供的API不同而已。因此,在探讨node网络编程之前,我们需要了解用于网络通信的网络协议(推荐阅读《图解HTTP》)。理解了网络通信的规范和机制,再熟悉一下API就可以了。

图片来源https://yjhjstz.gitbooks.io/deep-into-node/content/chapter9/chapter9-1.html

TCP/UDP

TCP(Transmission Control Protocol)传输控制协议是面向连接的协议,也就是必须建立连接才能发送数据。TCP在传输之前需要与服务器端进行3次握手形成会话(SYN是同步信号,ACK是确认信号):

TCP传送数据比较可靠,如果丢失数据会重传,并且会对传送数据进行排序。适用于重要、有序数据的传送。

UDP(User Datagram Protocol)用户数据报协议是无连接协议,不面向连接,面向事务,创建过程相对简单,占用内存底,处理快速且灵活,发送数据不需要与另一端建立连接,且不分客户端、服务器端,在一端即可以发送数据也可以接收数据。传送的数据是无序的,网络中断会导致丢包。UDP的简单不可靠特性适用于丢失一部分数据不会造成太大影响的场景,如音视频数据传送等。

socket

网络上的两个程序通过一个双向的通信连接实现数据的交换,这个连接的一端称为一个socket(套接字),因此建立网络通信连接至少要一对端口号(socket)。socket本质是对TCP/IP协议栈的封装,它提供了一个针对TCP或者UDP编程的接口,并不是另一种协议。通过socket,你可以使用TCP/IP协议。

Socket的英文原义是“孔”或“插座”。作为BSD UNIX的进程通信机制,取后一种意思。通常也称作”套接字“,用于描述IP地址和端口,是一个通信链的句柄,可以用来实现不同虚拟机或不同计算机之间的通信。在Internet上的主机一般运行了多个服务软件,同时提供几种服务。每种服务都打开一个Socket,并绑定到一个端口上,不同的端口对应于不同的服务。Socket正如其英文原意那样,像一个多孔插座。一台主机犹如布满各种插座的房间,每个插座有一个编号,有的插座提供220伏交流电, 有的提供110伏交流电,有的则提供有线电视节目。 客户软件将插头插到不同编号的插座,就可以得到不同的服务。

​ ——百度百科

创建TCP、UDP客户端和服务端

在node中,net模块提供创建基于TCP协议的网络通信的API,net.Socket类提供了 TCP 或 UNIX Socket 的抽象,net.createServer用于创建服务端,net.Socketnet.connect用于创建客户端。

dgram模块用于创建基于UDP协议的网络服务,创建不分客户端不分客户端、服务器端,在一端使用dgram.createSocket即可发送数据也可以接收数据。

http/https

http是应用层协议,建立在TCP/IP之上,https则建立在TLS、SSL加密层协议之上,现代web基本都是http/https应用。TCP在建立连接要发送报文,http也是,http报文分为请求报文和响应报文,报文格式如下:

1
2
3
4
5
6
HTTP/1.0 200 OK    //起始行

Content-type:text/plain //头部
Content-length:19 //头部

Hi I'm a message! //主体

其中最重要的莫过于头部报文了,它定义了请求或响应的行为方式,是客户端与服务器端交流的重要信息。http报文头部的属性多达几十个,而且越来越多,保证客户端与服务器端充分交流。

现代浏览器,集成了HTTP代理功能,用户点击链接等行为会由浏览器生成HTTP请求报文发送给服务器端,收到响应后会解析报文,渲染报文中的主体内容。

node中的http

node中http模块提供创建基于http协议的网络通信应用的接口,继承于net模块,采用事件驱动机制,能与多个客户端保持连接,并不为每个连接开启新的进程或线程,低内存、高并发,性能优良。

“http模块将连接所用套接字(socket)的读写抽象为ServerRequest和ServerResponse对象,它们分别对应请求和响应操作。在请求产生的过程中,http模块拿到连接中传来的数据,调用二进制模块http_parser进行解析,在解析完请求报文的报头后,触发request事件,调用用户的业务逻辑。”

​ ——朴灵《深入浅出Node.js》

从上图可以看到,node中http模块所做的事情就是继承net模块使用TCP协议、封装http请求、产生http事件、响应事件绑定的处理程序。

http代理

node中http模块提供了一个类http.Agent,它称为http代理,它的作用就是为了重用TCP连接,减少资源浪费。那么http代理是如何重用TCP连接呢?http代理维护一个连接池,从客户端发起的http请求都经由代理管理:

它为一个给定的主机与端口维护着一个等待请求的队列,且为每个请求重复使用一个单一的 socket(TCP) 连接直到队列为空,此时 socket(TCP 连接) 会被销毁或被放入一个连接池中,在连接池中等待被有着相同主机与端口的请求再次使用。 是否被销毁或被放入连接池取决于 keepAlive选项

​ ——Node.js 8.9.0中文文档

连接池如何管理连接还得取决于服务器:

即便连接池中的连接的 TCP Keep-Alive 是开启的,服务器仍然可能关闭闲置的连接,在这种情况下,这些连接会被移出连接池,且当一个新的 HTTP 请求被创建时再为指定的主机与端口创建一个新的连接。 服务器也可能拒绝允许同一连接上有多个请求,在这种情况下,连接会为每个请求重新创建,且不能被放入连接池。Agent 仍然会创建请求到服务器,但每个请求会出现在一个新的连接。

但一个连接被客户端或服务器关闭时,它会被移出连接池。 连接池中任何未被使用的 socket 会被释放,从而使 Node.js 进程在没有请求时不用保持运行。

​ ——Node.js 8.9.0中文文档

http模块除了提供代理类,还提供了:

  • http.ClientRequest类—— 表示一个正在处理的请求,这个请求还能设置请求头
  • http.Server类——继承net.Server,并添加了一些事件
  • http.ServerResponse类——代表响应
  • http.createServer方法——创建服务器,返回http.Server实例
  • http.request方法——显式发出请求
  • 各种请求、响应对应的事件

创建http服务器

1
2
3
4
5
6
7
// node创建服务器非常简单,不需要任何三方代理
var http = require('http');
http.createServer(function (req, res) {
res.writeHead(200, {'Content-Type': 'text/plain'})
res.end('Hello World\n');
}).listen(8880, '127.0.0.1');
console.log('Server running at http://127.0.0.1:8880/')

创建http请求

使用http.request即可发送请求。

WebSocket

一问一答是HTTP协议的特点,然而服务器主动向客户端推送数据的场景也是常见的、被需要的。在WebSocket出现之前,实现客户端和服务器端双工通信一般只能通过多开几个HTTP连接、以轮询方式来实现。由于HTTP一问一答的特点不适合这种场景,就算HTTP1.1新增的Keep-Alive也不能很好的解决这种问题,于是WebSocket协议就出现了。

WebSocket协议可以让客户端与服务器端实现双向通信,服务端可以主动发送数据到客户端。建立WebSocket协议连接时,客户端会发送一条HTTP请求,请求服务器端切换协议为WebSocket,服务器端如果支持WebSocket协议,就会返回一条HTTP响应表示正在切换WebSocket协议并切换。之后就可以互相发送数据了。

使用WebSocket协议构建应用有以下优点:

  • 客户端可以与服务器端实现双向通信,服务端可以主动发送数据到客户端
  • 通过第一个request建立了TCP连接之后,之后交换的数据都不需要发送 HTTP header就能交换数据

目前大多数浏览器已经实现WebSocket,可以直接使用:

1
2
3
4
5
6
7
var socket = new WebSocket('ws://localhost:3000/') // 路径中的协议改为ws(WebSocket)
socket.onopen = function () {
// 连接打开要做的事
};
socket.onmessage = function (event) {
// 接收到服务端的信息(event.data)
};

调用WebSocket后浏览器会发送一个HTTP请求,请求报文如下:

1
2
3
4
5
6
7
GET /chat HTTP/1.1
Host: server.example.com
Upgrade: websocket // 请求协议升级为websocket
Connection: Upgrade
Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ== //校验值
Sec-WebSocket-Protocol: chat, superchat
Sec-WebSocket-Version: 13

在Node原生模块中没有支持WebSocket协议连接功能的模块,但Node有很多三方模块来帮助做这件事,常用的ws模块方法如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// 导入WebSocket模块:
const WebSocket = require('ws');

// 引用Server类:
const WebSocketServer = WebSocket.Server;

// 实例化:
const wss = new WebSocketServer({ // 在本地3000端口打开一个WebSocket Server
port: 3000
});

wss.on('connection', function (ws) {
console.log(`[SERVER] connection()`);
ws.on('message', function (message) {
console.log(`[SERVER] Received: ${message}`);
ws.send(`ECHO: ${message}`, (err) => {
if (err) {
console.log(`[SERVER] error: ${err}`);
}
});
})
});

服务器返回的响应报文如下:

1
2
3
4
5
HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo= // 返回经过计算得出的校验值
Sec-WebSocket-Protocol: chat

使用框架进行网络编程

Node网络模块中提供的API较为底层,有时在构建网络应用程序并不需要关心底层实现,这时就可以借助三方框架封装好的API来帮助我们,常用的框架包括express、koa、connect等。