2月阅读周·深入浅出的Node.js | 网络编程,正式拉开Node在网络服务器方向的帷幕
背景
去年下半年,我在微信书架里加入了许多技术书籍,各种类别的都有,断断续续的读了一部分。
没有计划的阅读,收效甚微。
新年伊始,我准备尝试一下其他方式,比如阅读周。每月抽出1~2个非连续周,完整阅读一本书籍。
这个“玩法”虽然常见且板正,但是有效。
已读完书籍:《架构简洁之道》。
当前阅读周书籍:《深入浅出的Node.js》。
网络编程
构建TCP服务
TCP
TCP全名为传输控制协议,在OSI模型(由七层组成,分别为物理层、数据链结层、网络层、传输层、会话层、表示层、应用层)中属于传输层协议。
创建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的过程进行了封装。
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有如下好处:
- 客户端与服务器端只建立一个TCP连接,可以使用更少的连接。
- WebSocket服务器端可以推送数据到客户端,这远比HTTP请求响应模式更灵活、更高效。
- 有更轻量级的协议头,减少数据传送量。
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畅想》等系列作者。华夏美食、国漫、古风重度爱好者,刑侦、无限流小说初级玩家。
如果看完文章有所收获,欢迎点赞👍 | 收藏⭐️ | 留言📝。
- 点赞
- 收藏
- 关注作者
评论(0)