一、UDP基础
UDP提供的是一种无连接的、不可靠的数据传输方式。
UDP的不可靠特性不代表它不可靠,其可靠性保证和流控制可以由UDP用户(即应用程序)决定。
UDP传输给IP的数据单元称作UDP数据报(Datagram)。
UDP使用端口号为不同的应用保留其各自的数据传输通道。UDP使用Socket,只不过是无连接的数据报Socket。
dgram核心模块用于实现UDP通信。导入该模块:
const dgram = require('dgram');
二、dgram模块提供的API
1、dgram.Socket类
dgram.Socket类提供实现UDP应用的基本框架。dgram.Socket对象是一个封装了数据报功能的事件触发器。dgram.Socket实例由dgram.createSocket()方法创建。
dgram.Socket类实现的事件
close事件:使用close()方法关闭一个Socket之后触发该事件。
error:发生任何错误都会触发该事件。
listening:当一个Socket开始监听数据报信息时触发该事件。
message:当有新的数据报被Socket接收时触发该事件。
dgram.Socket类提供的方法
socket.bind()方法用于设置Socket在指定的端口和地址上监听数据报信息。
socket.send()方法用于在Socket上发送一个数据报。
2、dgram.createSocket()方法
该方法用于创建dgram.Socket对象。一旦创建了Socket,调用socket.bind()方法会指示Socket开始监听数据报消息。
基本用法
dgram.createSocket(options[, callback])
另一种用法:创建一个特定类型的dgram.Socket对象
dgram.createSocket(type[, callback])
三、应用
先安装 dgram 模块
服务端项代码如下:
/**
* UDP服务端
*/
//载入udp模块
const dgram = require("dgram");
//创建服务器
const server = dgram.createSocket("udp4");
server.on("message",(msg,rinfo)=>{
//将接收到的消息返回客户端
var strmsg = "你好,UDP客户端,消息已经收到!";
server.send(strmsg,rinfo.port,rinfo.address);
console.log("服务器接收到来自"+rinfo.address+":"+rinfo.port+" 的消息:"+msg.toString());
});
server.on("listening",()=>{
let adress = server.address();
console.log("服务器监听:",adress.adress+":"+adress.port);
});
server.on("error",(err)=>{
console.err("服务器异常错误:"+err.message);
});
server.bind(8234,"127.0.0.1");
客户端项代码如下:
/**
* UDP客户端
*/
const dgram = require("dgram");
const client = dgram.createSocket("udp4");
client.on("message",(msg,rinfo)=>{
console.log("接收来自:"+rinfo.address+":"+rinfo.port+"的消息:"+msg.toString());
});
client.on("error",(err)=>{
console.error("客户端错误:"+err.message);
});
client.on("close",()=>{
console.log("socket已关闭");
});
client.send("我是UDP客户端!",8234,"127.0.0.1",(err)=>{
if(err) client.close();
});
运行结果如下:
【服务端】
【客户端】
上例实现了一个简单的 UDP 服务,主要涉及了 dgram.createSocket、dgram.Socket 的使用,下面我们对其进行一一介绍。
dgram.createSocket
dgram.createSocket 主要用于创建 dgram.Socket 实例;有以下两种签名:
-
dgram.createSocket(type[, callback]):type:套接字类型,可用值为udp4、udp6;其中udp4指运行在IPv4下,udp6指运行在IPv6下;callback:该回调会追加到dgram.Socket的message事件监听队列中,当接收到另一端发送的数据时触发。
-
dgram.createSocket(options[, callback]):-
options:属性设置,相关属性如下:-
type:Socket 类型,可用值为udp4、udp6;其中udp4指运行在IPv4下,udp6指运行在IPv6下; -
reuseAddr:假如一个 socket 绑定了0.0.0.0:41234,另一个 socket 绑定了127.0.0.1:41234:- 如果
reuseAddr为false,将抛出EADDRINUSE异常,这是因为0.0.0.0代表任何一个 IP 地址,其他的 IP 地址(比如上文中的127.0.0.1)均会被系统认为已占用; - 如果
reuseAddr为true,此时0.0.0.0:41234和127.0.0.1:41234代表的是完全不同的地址端口对,所以这两个 socket 均能绑定成功。
该属性的默认值为
false。 - 如果
-
ipv6Only:是否禁用双协议栈;默认值为false; -
recvBufferSize:设置套接字SO_RCVBUF的值,即设置接收缓冲区大小; -
sendBufferSize:设置套接字SO_SNDBUF的值,即设置发送缓冲区大小; -
lookup:自定义 DNS 查询逻辑,默认调用dns.lookup(); -
signal:使用指定的 AbortSignal 来关闭套接字。
-
-
callback:该回调会追加到dgram.Socket的message事件监听队列中,当接收到另一端发送的数据时触发。
-
dgram.Socket
dgram.Socket 主要作为服务端与客户端之间通信的桥梁。
常用方法
-
bind:绑定 IP 地址和端口号,这样通信另一端可通过指定的 IP 地址和端口号向该 socket 发送数据报信息,该方法有以下两种签名:-
socket.bind([port][, address][, callback]):port:要绑定的端口号,如果该参数的值为0或未指定,系统将随机分配段口号;address:要绑定的 IP 地址,默认值为0.0.0.0;callback:绑定完成后的回调函数。
-
socket.bind(options[, callback]):-
options:属性设置,相关属性如下:port:要绑定的端口号,如果该参数的值为0或未指定,系统将随机分配段口号;address:要绑定的 IP 地址,默认值为0.0.0.0;exclusive:在 cluster 中是否允许共享服务监听句柄;默认值为false;fd:已存在 socket 相关文件描述符,如指定则使用该 socket,否则将创建一个新的 socket。
-
callback:绑定完成后的回调函数。
-
-
-
close:关闭套接字并停止监听来自通信另一端的数据报,该方法会触发close事件; -
connect:与通信另一端的地址与端口号建立关联,相关参数如下:port:通信另一端端口号;address:通信另一端地址,默认值udp4下为127.0.0.1,udp6下为::1;callback:连接成功后触发connect事件以及指定的callback,连接失败仅触发指定的callback。
前文我们说 UDP 是无连接的协议,因此这里的
connect并不是在通信双方之间建立真正的连接,而只是用来设置通信另一端的地址和端口号;连接建立后,socket.send()调用无需指定port和address参数,并且仅能收到连接指定的通信另一端的数据报。 -
disconnect:与通信另一端的地址与端口号取消关联; -
send:发送数据报给指定的通信另一端,相关参数如下:msg:要发送的数据报;offset:数据报第一个字节在缓冲区的偏移量;length:数据报的字节大小;port:通信另一端端口号,如果当前 socket 未连接,则需要指定该参数,否则将使用连接时指定的端口号,而无需指定该参数;address:通信另一端地址,如果当前 socket 未连接,则需要指定该参数,否则将使用连接时指定的地址,而无需指定该参数;callback:数据报发送成功后的回调函数。
-
setBroadcast:设置套接字选项SO_BROADCAST的值,用来控制是否允许发送广播数据,其参数flag为boolean类型; -
setMulticastInterface:设置多播接口,其参数multicastInterface为string类型;其值在IPv4和IPv6的要求如下:-
在
IPv4下,值为具体的 IP 地址,比如下面的例子:const socket = dgram.createSocket('udp4'); socket.bind(1234, () => { socket.setMulticastInterface('10.0.0.2'); }); 复制代码 -
在
IPv6下,值应该包含一个作用域,比如下面的例子:const socket = dgram.createSocket('udp6'); socket.bind(1234, () => { socket.setMulticastInterface('::%eth1'); }); 复制代码
-
-
addMembership:在指定的接口上将指定的地址加入到一个不限源的多播组中(内部使用了套接字选项IP_ADD_MEMBERSHIP),相关参数如下:multicastAddress:多播地址;multicastInterface:多播接口,如未指定,操作系统将自行选择一个接口。
-
dropMembership:在指定的接口上将指定的地址从不限源的多播组中移除(内部使用了套接字选项IP_DROP_MEMBERSHIP),相关参数如下:multicastAddress:多播地址;multicastInterface:多播接口,如未指定,将会从首个匹配的多播组中将指定的地址移除。
-
addSourceSpecificMembership:在指定的接口上加入一个特定于源的多播组(内部使用了套接字选项IP_ADD_SOURCE_MEMBERSHIP),相关参数如下:sourceAddress:源地址;groupAddress:多播组地址;multicastInterface:多播接口,如未指定,操作系统将自行选择一个接口。
-
dropSourceSpecificMembership:在指定的接口上移除特定于源的多播组(内部使用了套接字选项IP_DROP_SOURCE_MEMBERSHIP),相关参数如下:sourceAddress:源地址;groupAddress:多播组地址;multicastInterface:多播接口,如未指定,将会解除首个匹配的特定于源的多播组之间的成员关系。
-
setMulticastLoopback:设置套接字选项IP_MULTICAST_LOOP的值,用来控制数据是否可以回送到本地的回环接口(默认情况下,当本机发送多播数据到某个网络接口时,在 IP 层,数据会回送到本地的回环接口),其参数为flag为boolean类型。
相关事件
-
listening:当 socket 已准备好,可以接收数据时触发;该事件可通过socket.bind()显示触发,也可通过socket.send()隐式触发;需要注意的是,套接字相关的系统资源在该事件触发之前将不可用; -
connect:通过调用socket.connect()与远程端成功建立起连接后触发; -
message:当 socket 接收到新的数据报时触发;回调函数的参数如下:-
msg:数据报信息,类型为Buffer; -
rinfo:数据报发送端信息,相关属性如下:address:数据报发送端的 IP 地址;family:数据报发送端 IP 地址协议版本,值为IPv4或IPv6;port:数据报发送端的端口号;size:数据报大小。
-
-
error:发生异常时触发; -
close:通过调用socket.close()成功关闭 socket 后触发;一旦触发了该事件,将不会再触发message事件。
四、通过UDP实现文件上传
1、文件上传服务器
const fs = require('fs');
var port = 41234;
var server = dgram.createSocket('udp4');
var writeStream = fs.createWriteStream('upfile');//创建一个可写流
server.on('error', (err) => {
console.log(`服务器异常:\n${err.stack}`);
server.close();
});
server.on('message', function(msg, rinfo) {//message事件触发
process.stdout.write(msg.toString());//将接收到的数据输出到终端
writeStream.write(msg.toString()); //将接收到的数据写入可写流
});
server.on('listening', function() {
console.log('文件传输服务器已准备好:', server.address());
});
server.bind(port);
2、文件上传客户端
const fs = require('fs');
var remoteIP = '127.0.0.1';
var port = 41234;
var defaultSize = 16;//流读取的默认块大小
var readStream = fs.createReadStream(__filename);//从当前文件创建一个可读流
var client = dgram.createSocket('udp4');
readStream.on('readable', function() {//可读流准备好之后就开始发送数据
sendData();
});
function sendData() {
var message = readStream.read(defaultSize);
if (!message) {
return client.unref(); //完成文件传输之后,关闭Socket
}
client.send(message,0,message.length, port, remoteIP, function (err, bytes) {
sendData();
});
}
总结
本文我们首先对 UDP 协议进行介绍;希望能够通过这种从原理到实践的方式让大家真正掌握 Node.js 网络编程。