Node js 开发入门 —UDP 编程,小白也能轻松学会

576 阅读8分钟

一、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.createSocketdgram.Socket 的使用,下面我们对其进行一一介绍。

dgram.createSocket

dgram.createSocket 主要用于创建 dgram.Socket 实例;有以下两种签名:

  • dgram.createSocket(type[, callback])

    • type:套接字类型,可用值为 udp4udp6;其中 udp4 指运行在 IPv4 下,udp6 指运行在 IPv6 下;
    • callback:该回调会追加到 dgram.Socket 的 message 事件监听队列中,当接收到另一端发送的数据时触发。
  • dgram.createSocket(options[, callback])

    • options:属性设置,相关属性如下:

      • type:Socket 类型,可用值为 udp4udp6;其中 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.1udp6 下为 ::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 网络编程。