Node.js与网络

June 10, 2019 by Sylvenas

套接字和流

套接字指的是一个通讯终端,而网络套接字指的是在不同的计算机上运行的两个应用程序之间进行通讯所使用的终端。在套接字之间流通的数据就是我们所熟知的流。流中的数据可以以二进制的形式在Buffer中传输,也可以作为Unicode字符串来传递。两种数据类型都被称为封包(数据被切割为长度相似的块)来传输。有一种特殊的数据封包,即尾包(FIN)。被作为数据传输结束的信号发送给套接字。

举个很常见的案例来模拟一下上面的流程,我们经常会看到好莱坞战争片,地面军队被围困之后,使用对讲机呼叫支援!对讲机就是通讯终端,也是就是我们所说的套接字

在电影场景中士兵好不容易从各种悬崖边/大楼边缘拿到对讲机之后,开始对着对讲机按键一通按之后(同时便随着滋滋滋的声音),终于和远方的指挥所联系上了,而这个过程转换到计算机世界就是两个终端建立连接的过程

地面士兵开始给指挥所讲诉自己的地貌特征和地理坐标之后,最后说一句“END!”,表示自己说完了,不再继续说话,转而等待回复,这个也就是发送数据的尾包

FIN

然后指挥收到消息之后,一般会回复一句“Copy That!”。下一个场景一般是各种悍马开始打火,猛禽开始起飞...

Copy That

上面的这个流程在电影院我们经历了好多次,这个场景转换到计算机世界实际上就是半双工通信的流程。为什么叫半双工呢,因为在同一时间只能有一方在讲话,另一方在收听,然后收听方接收消息之后,回复消息方。

既然有半双工通讯,那么也一定存在全双工通讯,典型的全双工通讯的案例就是情侣打电话吵架,两个人分别在沟通的时候,经常会发生一起说话的场景,就是Boy向Girl发送数据的同时,Girl也在向Boy发送数据。

单工就更简单了,A可以向B发送数据,B不能向A发送数据,单方向的!

http

http server

很多Node.js的核心API都旨在创建某些服务来监听特定类型的通信。其中典型的有http模块用来创建Web服务器,进而监听http请求的,下面是一个典型的监听HTTP POST请求,接收POST请求中Body中信息,并添加一个serverTag字段之后回传给客户端的案例:

const http = require('http');
const querystring = require('querystring');

const server = http.createServer().listen(3232);

server.on('request', (request, response) => {
  if (request.method === 'POST') {
    let body = '';

    request.on('data', data => {
      body += data;
    });

    request.on('end', () => {
      let post = querystring.parse(body);
      post = {
        ...post,
        serverTag: 'server append the data',
      };
      console.log(post);

      response.writeHead(200, { 'Content-Type': 'text/plain' });
      response.end(JSON.stringify(post));
    });
  }
});

console.log('server listening on port 3232');

下面是使用PostMan做测试的截图: Node.js POST

http client

Node.js可不仅仅能提供创建服务端的能力,同样提供了创建客户端发送数据的能力,下面我们自己编写一个http client来代替Postman来发送数据:

const http = require('http');
const querystring = require('querystring');

const postData = querystring.stringify({
  name: 'James',
  age: 34,
});

const options = {
  hostname: 'localhost',
  port: 3232,
  method: 'POST',
  header: {
    'Content-Type': 'application/x-www-form-urlencoded',
    'Content-Length': postData.length,
  },
};

const req = http.request(options, (res) => {
  console.log('status', res.statusCode);
  console.log('headers', JSON.stringify(res.headers));

  res.setEncoding('utf8');

  let body = '';
  res.on('data', data => {
    body += data;
  });

  res.on('end', () => {
    console.log('No more data in response');
    console.log(body);
  });
});

req.write(postData);
req.end();

http client

TCP

TCP为大多数互联网应用提供了通讯平台,比如Web服务器和邮箱。它提供了一种客户端和服务端套接字之间传输数据的可靠通道。TCP为应用层(i.e.,http)的存在提供了基础的架构。

Node.js也提供了创建TCP服务器和客户端的API,也就是net模块。创建TCP服务器的代码和创建http服务器的代码非常类似,创建TCP服务器的时候,我们不会向创建服务的函数传递包含request和response对象的requestListener函数,TCP回调函数只有一个参数,就是用来接收和传递数据的套接字对象,看代码:

const net = require('net');

const port = 3232;

const server = net.createServer(conn => {
  console.log('connected');

  conn.on('data', data => {
    console.log(data + ' from' + conn.remoteAddress + ' ' + conn.remotePort);
    conn.write(data);
  });

  conn.on('close', () => {
    console.log('client closed connection');
  });
});

server.listen(port);

conn对象可以监听接收数据和断开连接,我们使用OSX的netcat工具来测试一下连接,读取当前文件夹下的app.json文件的内容并发送给TCP服务端,TCP服务端接收到数据之后,打印出来,并把数据回传给客户端

nc localhost 3232 < app.json

tcp server

前面多了::fff的原因是IP V4地址被解析为了IP V6

tcp client

TCP客户端

Node.js同样提供了创建tcp客户端的API,我们简单了解一下:

const net = require('net');

const client = new net.Socket();
client.setEncoding('utf8');
// 连接到服务端,并发送数据
client.connect('3232', 'localhost', () => {
  console.log('connected to server');
  client.write('Hi, I am client');
});
// 客户端接收数据
client.on('data', data => {
  console.log(data);
});
// 服务端关闭
client.on('close', () => {
  console.log('connection is closed');
});

客户端终端打印的日志为:

node client

connected to server
Hi, I am client

客户端和服务端的连接会一直保持,直到某一端的进程被关闭;关闭一端,另一端都会收到一个close的事件。 服务端可以同时处理多个客户端的连接,因为所有相关的函数都是异步的。

TCP服务不仅可以通过监听一个端口号来创建服务,还可以直接绑定一个UNIX套接字的方式来创建服务,这个用的比较少,可以看看文档

TCP连接建立的三次握手和断开的四次挥手的详细细节可以参考另一篇文章

UDP

TCP需要在两个终端之间建立专门的连接。UDP则是一个无连接的协议,也就意味着两个终端之间并不保证有连接存在。出于这个原因,UDP比TCP的可靠性和安全性都要差一点。然后UDP又比TCP速度快,所以更适用于对实时性要求比较高的场景(直播),以及那些使用TCP连接可能会对信号质量造成负面影响的场景。

毫无疑问,Node.js同样支持UDP套接字服务端和客户端的创建,UDP相关的模块是dgram,

const dgram = require('dgram');

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

server.on('message', (msg, rinfo) => {
  console.log(msg + ' from ' + rinfo.address + ':' + rinfo.port); // => Hello World from 127.0.0.1:50392
});

server.bind(3232);

要创建UDP套接字要需要给createSocket方法传入套接字类型:udp4 or udp6。还可以传入一个回调函数用来监听事件。和TCP发送消息不同,使用UDP发送消息必须要使用Buffer,而不能使用字符串,但是我们在使用的时候可以直接传入string,send方法会把数据转换为Buffer的

const dgram = require('dgram');

const client = dgram.createSocket('udp4');

const data = 'Hello World';

client.send(data, 0, data.length, 3232, (err, bytes) => {
  if (err) {
    console.log(err);
  } else {
    console.log('successful'); // => successful
});

TLS/SSL

Web应用程序的安全性不仅是用来确保用户不会访问应用程序的服务端。安全性说起来很复杂,甚至很吓人。幸运的是,对于Node应用程序来说,我们所需要做的几个安全性组建已经被创建好了,我们只需要在合适的时间和合适的地方引用即可。SSL和它的升级版(TSL)会确保客户端和服务端发生的通讯是安全的,防篡改的,TSL/SSL为https提供了底层加密。

TSL/SSL连接需要客户端和服务端之间进行一次握手。握手期间(通常是浏览器)会告诉服务端它所支持的安全性函数类型。然后服务端会挑选一个函数,并且发送一个SSL证书,该证书包含一个公钥。客户端确认收到证书后,会使用公钥生成一个随机数,然后发送给服务端端。最后服务端使用私钥对随机数揭秘,并使用解密的结果来对通信加密。

为了让这个过程运行起来,需要同时生成公钥,私钥,和证书。对于生产环境,证书将由受信任的机构(如域名注册商)签名;但在开发环境中,你可以使用自签名证书,这样做会让用户在浏览器中访问应用程序时出现一个明显的警告信息,但由于是开发站点,不会被外部访问,所以问题不大。

Lets Encrypt提供免费托管证书的服务,避免了商业授权的费用。

创建私钥

用于生成公钥,私钥,和证书的工具是SSL,OS X自带了该工具,下面先看生成私钥的终端命令:

openssl genrsa -des3 -out site.key 2048

这个命令会在当前文件夹下生成一个私钥文件(site.key),该私钥使用三重DES加密,以PEM格式保存,可以使用ASCII解密。在生成私钥的过程中你会被要求输入一个密码。这个密码先记住,后面在创建证书签名请求文件(CSR:certificate-signing-request)的时候会被用到。

创建证书签名请求文件

创建证书签名请求文件(这一步不是为了生成证书)的命令为:

openssl req -new -key site.key -out site.crs

生成证书签名请求文件的时候,会被要求输入密码,而该密码就是和生成私钥的是同一个密码,同时还会被要求回答一系列问题,包括国家缩写(比如ZH),州/省的名称,城市名称,公司名称,机构名称,电子邮箱地址。其中有一项是最重要的就是“通用名称”(common name),这个名称指的是网站的主机名,也就是访问网站所需要的域名,我们可以暂时填写一个测试的域名(比如:sylvenas.xyz)。

该命令会在当前文件夹下生成一个证书签名请求文件:site.csr

创建证书

openssl x509 -req -day 365 -in site.csr -signkey site.key -out final.crt

该命令会在当前文件夹下创建一个final.crt文件,也就是证书;现在我们拥有的创建https服务器的所有必备文件,下一步就是用Node.js创建https服务器了

创建https服务器

类似于http模块,Node.js通常提供了创建https服务器的API:

const fs = require('fs');
const https = require('https');

const preivateKey = fs.readFileSync('./site.key', 'utf8').toString();
const certificate = fs.readFileSync('./final.crt', 'utf8').toString();

const options = {
  key: preivateKey, //私钥
  cert: certificate, //证书
  passphrase: 'forthelichking', // 密码
};

https.createServer(options, (req, res) => {
  res.writeHead(200);
  res.end('Hello Secure World');
}).listen(443);

出了要配置options之外,其他的内容几乎和创建http服务器一样的。

这个时候我们在浏览器打开https://sylvenas.xyz/(记得把hosts中的sylvenas.xyz指向为localhost),赫然发现,我们的网站打不开了:

https 警告

这是为何呢?很简单因为我们的证书不是第三方受信任的机构提供的,而是我们自己提供的,这个时候,就需要我们把刚刚创建的证书导入钥匙串,然后添加信任。

添加证书并信任

然后刷新浏览器,就可以看到我们的网站可以打开了,但是Chrome依然给出了红色的安全警告:

安全警告

再次强调,使用可信的证书颁发结构颁发的可信证书,就可以消除警告

总结

到此为止,我们总结了使用Node.js创建HTTP,TCP,UDP,HTTPS服务器的整体梳理,希望阅读之后能有一个大概的了解。

很多人经常犯糊涂的一件事就是没有弄清楚TCP协议和UDP协议与TCP/IP协议的联系和区别,TCP/IP协议是一个协议簇,里面包含很多协议,包括TCP协议和UDP协议等等,之所以命名为TCP/IP协议,就是因为TCP、IP协议是两个很重要很常用的的协议,就用他两命名了。TCP/IP协议集包括应用层,传输层,网络层,网络访问层,这部分知识可以参考这篇文章