2月阅读周·深入浅出的Node.js | 网络编程,正式拉开Node在网络服务器方向的帷幕

举报
叶一一 发表于 2024/02/26 10:07:00 2024/02/26
【摘要】 背景去年下半年,我在微信书架里加入了许多技术书籍,各种类别的都有,断断续续的读了一部分。没有计划的阅读,收效甚微。新年伊始,我准备尝试一下其他方式,比如阅读周。每月抽出1~2个非连续周,完整阅读一本书籍。这个“玩法”虽然常见且板正,但是有效。已读完书籍:《架构简洁之道》。当前阅读周书籍:《深入浅出的Node.js》。网络编程构建TCP服务TCPTCP全名为传输控制协议,在OSI模型(由七层组...

背景

去年下半年,我在微信书架里加入了许多技术书籍,各种类别的都有,断断续续的读了一部分。

没有计划的阅读,收效甚微。

新年伊始,我准备尝试一下其他方式,比如阅读周。每月抽出1~2个非连续周,完整阅读一本书籍。

这个“玩法”虽然常见且板正,但是有效。

已读完书籍《架构简洁之道》。

当前阅读周书籍《深入浅出的Node.js》

网络编程

构建TCP服务

TCP

TCP全名为传输控制协议,在OSI模型(由七层组成,分别为物理层、数据链结层、网络层、传输层、会话层、表示层、应用层)中属于传输层协议。

7-1.png

创建TCP服务器端

下面的代码会创建一个TCP服务器端来接受网络请求:

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(8027, function () {
  console.log('server bound');
});

然后,我们通过net模块自行构造客户端进行会话,测试上面构建的TCP服务的代码:

var net = require('net');
var client = net.connect({ port: 8027 }, function () {
  //'connect' listener
  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');
});

最终得到的会话信息如下:

> node test2.js
> client connected
> 欢迎光临《深入浅出Node.js》示例:
> 
> 你好
> client disconnected

TCP服务的事件

TCP服务的事件分为服务器事件和连接事件。

1、服务器事件

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

  • listening:在调用server.listen()绑定端口或者Domain Socket后触发,简洁写法为server.listen(port, listeningListener),通过listen()方法的第二个参数传入。
  • connection:每个客户端套接字连接到服务器端时触发。
  • close:当服务器关闭时触发,在调用server.close()后,服务器将停止接受新的套接字连接,但保持当前存在的连接,等待所有连接都断开后,会触发该事件。
  • error:当服务器发生异常时,将会触发该事件。


2、连接事件

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

Stream对象可以用于服务器端和客户端之间的通信,它具有如下自定义事件:

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

构建UDP服务

UDP又称用户数据包协议,与TCP一样同属于网络传输层。UDP与TCP最大的不同是UDP不是面向连接的。在UDP中,一个套接字可以与多个UDP服务通信。

UDP目前应用很广泛,DNS服务即是基于它实现的。

创建UDP套接字

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

创建UDP套接字十分简单,方式如下:

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

创建UDP服务器端

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

如下代码可以创建一个完整的UDP服务器端:

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客户端

然后,我们可以创建一个客户端与服务器端进行对话:

var dgram = require('dgram');

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

执行前面的代码文件,得到如下输出:

> node test.js
> server listening 0.0.0.0:41234
> server got: 深入浅出Node.js from 127.0.0.1:51910

UDP套接字事件

UDP套接字是一个EventEmitter的实例,它具备如下自定义事件:

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

构建HTTP服务

Node提供了基本的http和https模块用于HTTP和HTTPS的封装,在Node中构建HTTP服务极其容易,Node官网上就提供了经典例子:

var http = require('http');
http
  .createServer(function (req, res) {
    res.writeHead(200, { 'Content-Type': 'text/plain' });
    res.end('Hello World\n');
  })
  .listen(1234, '127.0.0.1');
console.log('Server running at http://127.0.0.1:1234/');

HTTP

HTTP的全称是超文本传输协议,HTTP构建在TCP之上,属于应用层协议。在HTTP的两端是服务器和浏览器,即著名的B/S模式。

HTTP报文

无论是HTTP请求报文还是HTTP响应报文,报文内容都包含两个部分:报文头和报文体。

http模块

Node的http模块包含对HTTP处理的封装。HTTP服务以request为单位进行服务。http模块即是将connection到request的过程进行了封装。

7-2.png

http模块将连接所用套接字的读写抽象为ServerRequest和ServerResponse对象,它们分别对应请求和响应操作。

1、HTTP请求

对于TCP连接的读操作,http模块将其封装为ServerRequest对象。请求报文如下:

> GET / HTTP/1.1
> User-Agent: curl/7.24.0 (x86_64-apple-darwin12.0) libcurl/7.24.0 OpenSSL/0.9.8r zlib/1.2.5
> Host: 127.0.0.1:1337
> Accept: */*
  • req.method属性:值为GET,是为请求方法,常见的请求方法有GET、POST、DELETE、PUT、CONNECT等几种。
  • req.url属性:值为/。
  • req.httpVersion属性:值为1.1。

2、HTTP响应

HTTP响应对象封装了对底层连接的写操作,可以将其看成一个可写的流对象。它影响响应报文头部信息的API为res.setHeader()和res. writeHead()。

3、HTTP服务的事件

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

  • connection事件:在开始HTTP请求和响应前,客户端与服务器端需要建立底层的TCP连接,这个连接可能因为开启了keep-alive,可以在多次请求响应之间使用;当这个连接建立时,服务器触发一次connection事件。
  • request事件:建立TCP连接后,http模块底层将在数据流中抽象出HTTP请求和HTTP响应,当请求数据发送到服务器端,在解析出HTTP请求头后,将会触发该事件;在res.end()后,TCP连接可能将用于下一次请求响应。
  • close事件:调用server.close()方法停止接受新的连接,当已有的连接都断开时,触发该事件;可以给server.close()传递一个回调函数来快速注册该事件。
  • checkContinue事件:某些客户端在发送较大的数据时,并不会将数据直接发送,而是先发送一个头部带Expect: 100-continue的请求到服务器,服务器将会触发checkContinue事件。
  • connect事件:当客户端发起CONNECT请求时触发,而发起CONNECT请求通常在HTTP代理时出现;如果不监听该事件,发起该请求的连接将会关闭。
  • upgrade事件:当客户端要求升级连接的协议时,需要和服务器端协商,客户端会在请求头中带上Upgrade字段,服务器端会在接收到这样的请求时触发该事件。如果不监听该事件,发起该请求的连接将会关闭。
  • clientError事件:连接的客户端触发error事件时,这个错误会传递到服务器端,此时触发该事件。

HTTP客户端

http模块提供了一个底层API:http.request(options, connect),用于构造HTTP客户端。

var http = require('http');
var options = {
  hostname: '127.0.0.1',
  port: 1234,
  path: '/',
  method: 'GET',
};

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();

执行上面的代码文件,得到如下输出:

> node test2.js
> STATUS: 200
> HEADERS: {"content-type":"text/plain","date":"Thu, 22 Feb 2024 06:28:27 GMT","connection":"close","transfer-encoding":"chunked"}
> Hello World

构建WebSocket服务

WebSocket客户端基于事件的编程模型与Node中自定义事件相差无几。

WebSocket实现了客户端与服务器端之间的长连接,而Node事件驱动的方式十分擅长与大量的客户端保持高并发连接。

WebSocket与传统HTTP有如下好处:

  1. 客户端与服务器端只建立一个TCP连接,可以使用更少的连接。
  2. WebSocket服务器端可以推送数据到客户端,这远比HTTP请求响应模式更灵活、更高效。
  3. 有更轻量级的协议头,减少数据传送量。

WebSocket服务端

如下代码可以创建一个的WebSocket服务器端:

// 引入WebSocket模块
const WebSocket = require('ws');

// 创建WebSocket服务器监听端口3000
const wss = new WebSocket.Server({ port: 3000 });

wss.on('connection', function connection(ws) {
  // 当客户端连接时,发送欢迎消息
  ws.send('欢迎连接WebSocket服务器!');

  // 接收客户端消息
  ws.on('message', function incoming(message) {
    console.log('收到消息: %s', message);
  });

  // 当连接关闭时
  ws.on('close', function close() {
    console.log('连接已关闭。');
  });

  // 处理错误
  ws.on('error', function error(e) {
    console.log('发生错误: %s', e);
  });
});

console.log('WebSocket服务器运行在 ws://localhost:3000');

WebSocket客户端

接着我们再新增一个WebSocket在客户端:

const WebSocket = require('ws');

const ws = new WebSocket('ws://localhost:3000');

ws.on('open', function open() {
  console.log('connected');
  ws.send('hello');
});

ws.on('close', function close() {
  console.log('disconnected');
});

ws.on('message', function incoming(data) {
  console.log('received: %s', data);
});

执行上面的代码文件,得到如下输出:

> node test2.js
> connected
> received: 欢迎连接WebSocket服务器!

网络服务与安全

Node在网络安全上提供了3个模块,分别为crypto、tls、https。

  • crypto:主要用于加密解密,包括SHA1、MD5等加密算法。
  • TLS/SSL:是一个公钥/私钥的结构,它是一个非对称的结构,每个服务器端和客户端都有自己的公私钥。公钥用来加密要传输的数据,私钥用来解密接收到的数据。公钥和私钥是配对的,通过公钥加密的数据,只有通过私钥才能解密。
  • HTTPS服务:就是工作在TLS/SSL上的HTTP。HTTPS服务需要用到私钥和签名证书

总结

我们来总结一下本篇的主要内容:

  • Node提供了相对底层的网络调用,以及基于事件的编程接口,使得开发者在这些模块上十分轻松地构建网络应用。
  • UDP目前应用很广泛,DNS服务即是基于它实现的。
  • WebSocket实现了客户端与服务器端之间的长连接,而Node事件驱动的方式十分擅长与大量的客户端保持高并发连接。
  • Node基于事件驱动的方式使得它应对WebSocket这类长连接的应用场景可以轻松地处理大量并发请求。

作者介绍
非职业「传道授业解惑」的开发者叶一一。
《趣学前端》、《CSS畅想》等系列作者。华夏美食、国漫、古风重度爱好者,刑侦、无限流小说初级玩家。
如果看完文章有所收获,欢迎点赞👍 | 收藏️ | 留言📝

【版权声明】本文为华为云社区用户原创内容,转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息, 否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。