一文浅析Node中的TCP和UDP

青灯夜游
Lepaskan: 2023-04-20 18:15:26
ke hadapan
1635 orang telah melayarinya

一文浅析Node中的TCP和UDP

Node是一个面向网络而生的平台,它具有事件驱动、无阻塞、单线程等特性,具备良好的可伸缩性,使得它十分轻量,适合在分布式网络中扮演各种各样的角色。

Node提供了netdgramhttphttp2https等模块,分别用于处理TCPUDPHTTPHTTPS,适用于服务端和客户端。

构建 TCP 服务

TCP服务在网络应用中十分常见,目前大多数的应用都是基于TCP搭建而成的,它的全名为传输控制协议,在OSI模型中属于传输层协议,许多应用层协议基于TCP构建,典型的HTTPSMTPIMAP等协议。在这里不讲TCP相关知识点了,如果感兴趣可以关注我计算机网络这个专栏进行学习。

创建 TCP 服务端

在基本了解TCP的工作原理之后,我们可以开始创建一个TCP服务器端来接受网络请求,net模块提供了一个异步网络API,用于创建基于streamTCPIPC服务器和客户端。【相关教程推荐:nodejs视频教程编程教学

请看下面这个例子,我们在server.js文件中编写以下代码,如下:

import net from "net"; const server = net.createServer((socket) => { socket.on("data", (data) => { console.log("监听到客户端的数据:", data.toString()); }); // 监听客户端断开连接事件 socket.on("end", () => { console.log("客户端断开连接"); }); // 发送数据给客户端 socket.end("over over over\n"); }); // 启动服务 server.listen(3000, () => { console.log("服务创建成功"); });
Salin selepas log masuk

我们通过net.createServer(listener)即可创建一个TCP服务器,该函数的参数是里链接事件connection的侦听器。

当我们在终端执行该文件时,服务创建成功输出在了终端。

nodemon .\server.js
Salin selepas log masuk

在前面我们通过net.createServer创建了一个服务端,那么接下来我们使用net.connect创建一个客户端进行会话,具体代码如下所示:

import net from "net"; const client = net.connect({ port: 3000 }, () => { client.write("今晚出去吃饭,收到请 over\n"); }); // 接收服务端的数据 client.on("data", (data) => { console.log("接收服务端的数据: ", data.toString()); // 断开连接 client.end(); }); // 断开连接 client.on("end", () => { console.log("断开连接"); });
Salin selepas log masuk

我们这个时候对两个文件执行,如下所示:

image.png

接下来我们还有这样的一个例子,具体代码如下图所示:

image.png

具体运行结果请看下面的动图:

动画.gif

在客户端我使用client.write()发送了多次数据,但是只有setTimeout之外的是正常的,setTimeout里面连续发送的似乎并不是每一次一返回,而是会随机合并返回了,在这里也就出现了粘包了。

TCP针对网络中的小数据包有一定的优化策略:Negle算法,如果每次只发送一个字节的内容而不优化,网络中将充满只有极少数有效数据的数据报,将十分浪费网络资源,该算法针对这种情况,要求缓冲区的数据达到一定数量或者一定时间后才将其发出,所以小数据包将会被该算法合并,以此来优化网络。这种优化虽然使网络有效地使用,但是数据有可能被延迟发送。

Node中,由于TCP默认启用了Negle算法,可以调用socket.setNoDelay(true)去掉Negle算法,使得write()可以立即发送数据到网络中:

image.png

关闭Nagle算法并不总是有效的,因为其是在服务端完成合并,TCP接收到数据会先存放于自己的缓冲区中,然后通知应用接收,应用层因为网络或其它的原因若不能及时从TCP缓冲区中取出数据,也会造成 TCP 缓冲区中存放多段数据块,就又会形成粘包。

TCP原理

Node中,调用createServer()等同于调用new Server(),具体结果如下图所示:

image.png

这主要的原因它在Node源码中有如下定义,所以调用createServer()函数实际上调用的是new Server(),具体代码如下图所示:

function createServer(options, connectionListener) { return new Server(options, connectionListener); }
Salin selepas log masuk

该构造函数的定义主要有如下所示:

function Server(options, connectionListener) { EventEmitter.call(this); // 注册连接到来时执行的回调 if (typeof options === "function") { connectionListener = options; options = {}; this.on("connection", connectionListener); } else if (options == null || typeof options === "object") { options = { ...options }; if (typeof connectionListener === "function") { this.on("connection", connectionListener); } } // 服务器建立的连接数 this._connections = 0; this[async_id_symbol] = -1; this._handle = null; this._usingWorkers = false; this._workers = []; this._unref = false; // 服务器下的所有连接是否允许半连接 this.allowHalfOpen = options.allowHalfOpen || false; // 有连接时是否注册读事件 this.pauseOnConnect = !!options.pauseOnConnect; this.noDelay = Boolean(options.noDelay); // 是否支持keepAlive this.keepAlive = Boolean(options.keepAlive); this.keepAliveInitialDelay = ~~(options.keepAliveInitialDelay / 1000); } ObjectSetPrototypeOf(Server.prototype, EventEmitter.prototype); ObjectSetPrototypeOf(Server, EventEmitter);
Salin selepas log masuk

listen

它返回的是一个普通的JavaScript对象,接着调用listen函数监听端口,listen方法支持多种使用方式主要有以下这几种方法:

  • 传入的是一个已经创建的TCP服务器,而不是需要创建的一个服务器;
  • 传进来是一个对象,并且带了fd字段;
  • 创建了一个TCP服务器,并启动该服务器,如果传入了host会对其进行域名解析;

该方法的的主要逻辑有如下代码所示:

Server.prototype.listen = function (...args) { /* 处理入参,根据文档我们知道listen可以接收好几个参数, 假设我们这里是只传了端口号9297 */ var normalized = normalizeArgs(args); // normalized = [{port: 9297}, null]; var options = normalized[0]; var cb = normalized[1]; // 第一次listen的时候会创建,如果非空说明已经listen过 if (this._handle) { throw new errors.Error("ERR_SERVER_ALREADY_LISTEN"); } // listen成功后执行的回调 var hasCallback = cb !== null; if (hasCallback) { // listen成功的回调 this.once("listening", cb); } options = options._handle || options.handle || options; // 第一种情况,传进来的是一个TCP服务器,而不是需要创建一个服务器 if (options instanceof TCP) { this._handle = options; this[async_id_symbol] = this._handle.getAsyncId(); listenIncluster(this, null, -1, -1, backlogFromArgs); return this; } // 第二种,传进来一个对象,并且带了fd if (typeof options.fd === "number" && options.fd >= 0) { listenIncluster(this, null, null, null, backlogFromArgs, options.fd); return this; } // 创建一个tcp服务器 var backlog; if (typeof options.port === "number" || typeof options.port === "string") { backlog = options.backlog || backlogFromArgs; // 第三种 启动一个TCP服务器,传了host则先进行DNS解析 if (options.host) { lookupAndListen( this, options.port | 0, options.host, backlog, options.exclusive ); } else { listenIncluster( this, null, options.port | 0, 4, backlog, undefined, options.exclusive ); } return this; } };
Salin selepas log masuk

listenInCluster

在每种方式的最后丢回调用listenIncluster方法,该方法主要做的事情是区分master进程 和worker进程,采用不同的处理策略:

  • mastr进程: 直接调用server._listen启动监听;
  • worker进程: 使用cluster._getServer处理传入的server对象,修改server._handle再调用了server._listen启动监听;

构建 UDP 服务

UDP又称用户数据包协议,与TCP一样同属于网络层传输层。UDPTCP最大的不同是UDP不是面向链接的。

创建UDP服务

创建UDP套接字十分简单,UDP套接字一旦创建,既可以作为客户端发送数据,也可以作为服务端接收数据,下面的代码创建了一个UDP套接字,具体代码如下所示:

import dgram from "node:dgram"; const server = dgram.createSocket("udp4"); server.on("error", (err) => { console.error(`server error:\n${err.stack}`); server.close(); }); server.on("message", (msg, rinfo) => { console.log(`server got: ${msg} from ${rinfo.address}:${rinfo.port}`); }); server.on("listening", () => { const address = server.address(); console.log(`server listening ${address.address}:${address.port}`); }); server.bind(3000);
Salin selepas log masuk

该套接字将接收所有网课上3000端口上的消息,在绑定完成后,将触发listening事件,会终端执行,会输出server listening 0.0.0.0:3000字段。

接下来我们创建一个客户端和服务端进行对话,具体代码如下所示:

import dgram from "node:dgram"; import { Buffer } from "node:buffer"; const message = Buffer.from("你个叼毛"); const client = dgram.createSocket("udp4"); client.send(message, 0, message.length, 3000, "localhost", () => { client.close(); });
Salin selepas log masuk

终端的最终输出结果如下图所示

image.png

UDP 广播

dgram模块中,可以使用socket端口对象的setBroadcast方法来进行数据的广播:

socket.setBroadcast(flag);
Salin selepas log masuk
  • flag: 当flagtrue时,UDP服务器或者客户端可以利用其所用的socket端口对象的send方法中的地址修改为广播地址。

服务端的代码定义在server.js文件,具体代码如下所示:

import dgram from "dgram"; const 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(3000);
Salin selepas log masuk

客户端的代码定义在server.js文件,具体代码如下定义:

import dgram from "dgram"; import { Buffer } from "buffer"; const socket = dgram.createSocket("udp4"); const params = process.argv.splice(2); socket.bind(function () { socket.setBroadcast(true); }); const message = Buffer.from(...params); socket.send(message, 0, message.length, 3000, "255.255.255.255", () => { socket.close(); });
Salin selepas log masuk

具体运行效果如下图所示:

动画.gif

更多node相关知识,请访问:nodejs 教程

Atas ialah kandungan terperinci 一文浅析Node中的TCP和UDP. Untuk maklumat lanjut, sila ikut artikel berkaitan lain di laman web China PHP!

Label berkaitan:
sumber:juejin.cn
Kenyataan Laman Web ini
Kandungan artikel ini disumbangkan secara sukarela oleh netizen, dan hak cipta adalah milik pengarang asal. Laman web ini tidak memikul tanggungjawab undang-undang yang sepadan. Jika anda menemui sebarang kandungan yang disyaki plagiarisme atau pelanggaran, sila hubungi admin@php.cn
Muat turun terkini
Lagi>
kesan web
Kod sumber laman web
Bahan laman web
Templat hujung hadapan
Tentang kita Penafian Sitemap
Laman web PHP Cina:Latihan PHP dalam talian kebajikan awam,Bantu pelajar PHP berkembang dengan cepat!