{T}

UDP 通信

UDP(User Datagram Protocol)是一种无连接的、不可靠的传输层协议,提供面向事务的简单不可靠信息传送服务。

基本概念

UDP 特点

  • 无连接:发送数据前不需要建立连接
  • 不可靠:不保证数据送达、不保证顺序
  • 高效:无连接开销,传输速度快
  • 面向数据报:保留消息边界

UDP vs TCP

特性UDPTCP
连接无连接面向连接
可靠性不可靠可靠
顺序不保证保证顺序
速度较慢
边界保留消息边界流式(无边界)
开销

适用场景

  • 实时音视频传输
  • 在线游戏
  • DNS 查询
  • 广播/多播
  • 实时数据推送

数据报生命周期

图表渲染中…

创建 UDP 服务器

基本示例

javascript
const dgram = require('dgram');

// 创建 Socket
const server = dgram.createSocket('udp4');

// 监听消息
server.on('message', (msg, rinfo) => {
  console.log('收到消息:', msg.toString());
  console.log('来自:', rinfo.address, rinfo.port);

  // 发送响应
  server.send('服务器收到: ' + msg, rinfo.port, rinfo.address);
});

// 监听错误
server.on('error', (err) => {
  console.error('服务器错误:', err);
  server.close();
});

// 开始监听
server.bind(3000, () => {
  console.log('UDP 服务器运行在端口 3000');
});

获取地址信息

javascript
const server = dgram.createSocket('udp4');

server.bind(3000, () => {
  const address = server.address();
  console.log('地址:', address.address);
  console.log('端口:', address.port);
  console.log('协议:', address.family);
});

创建 UDP 客户端

基本示例

javascript
const dgram = require('dgram');

const client = dgram.createSocket('udp4');

// 发送消息
const message = Buffer.from('Hello UDP Server');

client.send(message, 3000, 'localhost', (err) => {
  if (err) {
    console.error('发送失败:', err);
    client.close();
    return;
  }
  console.log('消息已发送');
});

// 接收响应
client.on('message', (msg, rinfo) => {
  console.log('收到响应:', msg.toString());
  console.log('来自:', rinfo.address, rinfo.port);

  client.close();
});

// 错误处理
client.on('error', (err) => {
  console.error('客户端错误:', err);
  client.close();
});

发送多种数据

javascript
const dgram = require('dgram');
const client = dgram.createSocket('udp4');

// 发送字符串
client.send('Hello', 3000, 'localhost');

// 发送 Buffer
client.send(Buffer.from('Hello'), 3000, 'localhost');

// 发送数组(多个 Buffer)
client.send([Buffer.from('Hello'), Buffer.from('World')], 3000, 'localhost');

// 指定源端口
client.send('Hello', 3000, 'localhost', () => {
  client.close();
});

广播通信

服务端(接收广播)

javascript
const dgram = require('dgram');

const server = dgram.createSocket('udp4');

server.on('message', (msg, rinfo) => {
  console.log('收到广播消息:', msg.toString());
});

server.bind(3000, () => {
  console.log('服务器等待广播消息...');
});

客户端(发送广播)

javascript
const dgram = require('dgram');

const client = dgram.createSocket('udp4');

// 开启广播
client.bind(() => {
  client.setBroadcast(true);

  const message = Buffer.from('广播消息');

  // 发送广播(255.255.255.255)
  client.send(message, 3000, '255.255.255.255', (err) => {
    if (err) console.error(err);
    console.log('广播已发送');
    client.close();
  });
});

指定广播地址

javascript
const dgram = require('dgram');

const client = dgram.createSocket('udp4');

client.bind(() => {
  client.setBroadcast(true);

  // 发送到特定子网广播地址
  // 如 192.168.1.255
  client.send('Hello', 3000, '192.168.1.255', () => {
    client.close();
  });
});

多播通信

服务端(加入多播组)

javascript
const dgram = require('dgram');

const server = dgram.createSocket('udp4');

server.on('message', (msg, rinfo) => {
  console.log('收到多播消息:', msg.toString());
});

server.bind(3000, () => {
  // 加入多播组
  server.addMembership('230.185.192.108');

  console.log('已加入多播组 230.185.192.108');
});

客户端(发送多播)

javascript
const dgram = require('dgram');

const client = dgram.createSocket('udp4');

// 设置多播 TTL(生存时间)
client.setMulticastTTL(128);

// 发送多播消息
const message = Buffer.from('多播消息');

client.send(message, 3000, '230.185.192.108', (err) => {
  if (err) console.error(err);
  console.log('多播已发送');
  client.close();
});

多播选项

javascript
const server = dgram.createSocket('udp4');

server.bind(3000, () => {
  // 加入多播组
  server.addMembership('230.185.192.108', '192.168.1.100');

  // 设置多播接口
  server.setMulticastInterface('192.168.1.100');

  // 设置多播回环(是否接收自己发送的消息)
  server.setMulticastLoopback(true);
});

// 离开多播组
// server.dropMembership('230.185.192.108');

实时聊天示例

服务端

javascript
const dgram = require('dgram');

const server = dgram.createSocket('udp4');
const clients = new Map();

server.on('message', (msg, rinfo) => {
  try {
    const data = JSON.parse(msg.toString());
    const clientId = `${rinfo.address}:${rinfo.port}`;

    switch (data.type) {
      case 'join':
        // 用户加入
        clients.set(clientId, { address: rinfo.address, port: rinfo.port, name: data.name });
        broadcast(`用户 ${data.name} 加入了聊天室`);
        console.log(`${data.name} 已加入,当前用户数: ${clients.size}`);
        break;

      case 'chat':
        // 聊天消息
        const client = clients.get(clientId);
        if (client) {
          broadcast(`${client.name}: ${data.content}`, clientId);
        }
        break;

      case 'leave':
        // 用户离开
        const leavingClient = clients.get(clientId);
        if (leavingClient) {
          clients.delete(clientId);
          broadcast(`用户 ${leavingClient.name} 离开了聊天室`);
        }
        break;
    }
  } catch (e) {
    console.error('消息解析失败:', e);
  }
});

function broadcast(message, excludeId = null) {
  const data = JSON.stringify({ type: 'broadcast', content: message });

  clients.forEach((client, id) => {
    if (id !== excludeId) {
      server.send(data, client.port, client.address);
    }
  });
}

server.bind(3000, () => {
  console.log('UDP 聊天服务器运行在端口 3000');
});

客户端

javascript
const dgram = require('dgram');
const readline = require('readline');

const client = dgram.createSocket('udp4');
const rl = readline.createInterface({
  input: process.stdin,
  output: process.stdout
});

// 接收消息
client.on('message', (msg) => {
  console.log(msg.toString());
});

// 加入聊天室
const name = process.argv[2] || 'Anonymous';
const joinMsg = JSON.stringify({ type: 'join', name });

client.send(joinMsg, 3000, 'localhost');

// 发送消息
rl.on('line', (input) => {
  if (input === '/quit') {
    const leaveMsg = JSON.stringify({ type: 'leave' });
    client.send(leaveMsg, 3000, 'localhost', () => {
      client.close();
      rl.close();
    });
  } else {
    const chatMsg = JSON.stringify({ type: 'chat', content: input });
    client.send(chatMsg, 3000, 'localhost');
  }
});

client.on('close', () => {
  console.log('已断开连接');
});

DNS 查询示例

javascript
const dgram = require('dgram');

// DNS 服务器地址(Google DNS)
const DNS_SERVER = '8.8.8.8';
const DNS_PORT = 53;

// 构建 DNS 查询包
function buildDNSQuery(domain) {
  // DNS 报文头部
  const header = Buffer.alloc(12);
  header.writeUInt16BE(0x0001, 0);  // ID
  header.writeUInt16BE(0x0100, 2);  // 标志:标准查询
  header.writeUInt16BE(0x0001, 4);  // 问题数
  header.writeUInt16BE(0x0000, 6);  // 资源记录数
  header.writeUInt16BE(0x0000, 8);  // 授权记录数
  header.writeUInt16BE(0x0000, 10); // 附加记录数

  // 构建域名部分
  const parts = domain.split('.');
  const question = [];
  parts.forEach(part => {
    question.push(part.length);
    question.push(...Buffer.from(part));
  });
  question.push(0); // 结束标记

  const questionBuffer = Buffer.from(question);
  const typeClass = Buffer.from([0x00, 0x01, 0x00, 0x01]); // A 记录,IN 类

  return Buffer.concat([header, questionBuffer, typeClass]);
}

// 解析 DNS 响应
function parseDNSResponse(buffer) {
  // 跳过头部和问题部分
  let offset = 12;

  // 跳过域名
  while (buffer[offset] !== 0) {
    offset += buffer[offset] + 1;
  }
  offset += 5; // 0x00 + type(2) + class(2)

  // 读取回答
  const answers = [];
  const answerCount = buffer.readUInt16BE(6);

  for (let i = 0; i < answerCount; i++) {
    offset += 2; // 指针
    offset += 2; // type
    offset += 2; // class
    offset += 4; // TTL

    const dataLen = buffer.readUInt16BE(offset);
    offset += 2;

    if (dataLen === 4) { // IPv4
      const ip = `${buffer[offset]}.${buffer[offset + 1]}.${buffer[offset + 2]}.${buffer[offset + 3]}`;
      answers.push(ip);
    }
    offset += dataLen;
  }

  return answers;
}

// 发送 DNS 查询
function queryDNS(domain) {
  return new Promise((resolve, reject) => {
    const client = dgram.createSocket('udp4');
    const query = buildDNSQuery(domain);

    client.send(query, DNS_PORT, DNS_SERVER, (err) => {
      if (err) {
        client.close();
        reject(err);
      }
    });

    client.on('message', (msg) => {
      const answers = parseDNSResponse(msg);
      client.close();
      resolve(answers);
    });

    client.on('error', (err) => {
      client.close();
      reject(err);
    });
  });
}

// 使用
queryDNS('google.com').then(ips => {
  console.log('IP 地址:', ips);
}).catch(console.error);

Socket 选项

javascript
const server = dgram.createSocket('udp4');

server.bind(3000, () => {
  // 获取/设置接收缓冲区大小
  const recvBufferSize = server.getRecvBufferSize();
  server.setRecvBufferSize(1024 * 1024);

  // 获取/设置发送缓冲区大小
  const sendBufferSize = server.getSendBufferSize();
  server.setSendBufferSize(1024 * 1024);

  // 设置 TTL
  server.setTTL(64);

  // 开启广播
  server.setBroadcast(true);

  // 设置多播 TTL
  server.setMulticastTTL(128);

  // 设置多播回环
  server.setMulticastLoopback(true);

  console.log('Socket 选项已设置');
});

IPv6 支持

javascript
const dgram = require('dgram');

// 创建 IPv6 Socket
const server = dgram.createSocket('udp6');

server.on('message', (msg, rinfo) => {
  console.log('收到消息:', msg.toString());
  console.log('来自:', rinfo.address, rinfo.port);
  console.log('协议:', rinfo.family); // IPv6
});

server.bind(3000, '::', () => {
  console.log('UDP IPv6 服务器运行在端口 3000');
});

事件列表

事件说明
message接收到数据报
listeningSocket 开始监听
closeSocket 关闭
error发生错误

最佳实践

1. 消息确认机制

由于 UDP 不可靠,可以在应用层实现确认机制:

javascript
let sequence = 0;

function sendReliable(socket, msg, port, address) {
  return new Promise((resolve, reject) => {
    const seq = sequence++;
    const packet = JSON.stringify({ seq, data: msg });

    socket.send(packet, port, address);

    // 设置超时
    const timeout = setTimeout(() => {
      reject(new Error('超时'));
    }, 5000);

    // 等待确认
    const handler = (response, rinfo) => {
      const data = JSON.parse(response.toString());
      if (data.ack === seq) {
        clearTimeout(timeout);
        socket.off('message', handler);
        resolve();
      }
    };

    socket.on('message', handler);
  });
}

2. 数据包大小限制

UDP 数据包最大约 64KB,建议控制在 512 字节以内:

javascript
function safeSend(socket, msg, port, address) {
  const buffer = Buffer.from(msg);

  if (buffer.length > 512) {
    // 分片发送或报错
    console.warn('数据包过大,可能丢失');
  }

  socket.send(buffer, port, address);
}

3. 错误处理

javascript
const server = dgram.createSocket('udp4');

server.on('error', (err) => {
  console.error('服务器错误:', err);
  server.close();
});

server.bind(3000);

4. 资源清理

javascript
process.on('SIGINT', () => {
  server.close(() => {
    console.log('服务器已关闭');
    process.exit(0);
  });
});

5. 使用场景选择

  • 需要可靠传输 → 使用 TCP
  • 实时性要求高、可容忍少量丢包 → 使用 UDP
  • 需要广播/多播 → 使用 UDP
  • 文件传输 → 使用 TCP
  • 视频直播 → 使用 UDP