☰
仓颉编程语言 std.net 库 UDP 套接字编程实战:UdpSocket 收发报文完整指南
2026/10/14 21:08:25 网站建设 项目流程
  • 文档
  • 知识库
  • 教程
  • OpenHarmony

【免费下载链接】CangjieCorpus

本仓库为仓颉编程语言语料库,聚焦 仓颉编程语言 核心知识体系,整合官方开发指南、API 文档、典型代码示例及语法规范等多维度资源,经结构化处理与专业标注,构建为支持 RAG(检索增强生成)技术 的高质量知识基座,适配智能编码助手、学习辅助工具等应用开发需求。 欢迎开发者基于本语料库构建仓颉语言智能应用,推动鸿蒙开发生态智能化升级!

项目地址:https://gitcode.com/Cangjie/CangjieCorpus
点击查看免费下载

导读

本文以仓颉标准库std.net网络编程包中的 UDP 使用示例(udp.md)为主体,讲解如何使用UdpSocket完成无连接的数据报收发:从套接字创建、绑定、报文收发、地址解析到超时控制与线程协同,并深入剖析 UdpSocket 的底层 API 设计与其所属的 DatagramSocket 接口体系。读者读完本文后,可以独立编写一个可运行的单机 UDP 服务端/客户端程序,并理解 UDP 报文收发模型与 TCP 流式传输的本质差异。

UDP 协议与 std.net 中的 Socket 体系

UDP(User Datagram Protocol,用户数据报协议)是一种无连接的传输协议。它不提供可靠性和流量控制,但具有较低的延迟和较小的网络开销,主要用于视频直播、在线游戏等实时性要求高的应用场景(详见 net_package_overview.md)。

std.net包同时支持 UDP/TCP/UDS 三种 Socket 类型,其类继承关系如下:

Hierarchy Resource ├StreamingSocket │ ├TcpSocket │ └UnixSocket │ ├DatagramSocket │ ├UdpSocket │ └UnixDatagramSocket │ └ServerSocket ├TcpServerSocket └UnixServerSocket

从继承关系可以清晰地看出,UDP 对应的UdpSocket归属于DatagramSocket(数据报套接字)分支,与TcpSocket所属的StreamingSocket(流式套接字)分支并列。这种架构从类型层面就区分了"面向报文"与"面向流"两类网络模型。

DatagramSocket 接口:报文收发的基本契约

UdpSocket的父接口DatagramSocket定义了数据报套接字的核心能力(见 net_package_interfaces.md):

public interface DatagramSocket <: Resource & ToString { prop localAddress: SocketAddress prop remoteAddress: ?SocketAddress mut prop receiveTimeout: ?Duration mut prop sendTimeout: ?Duration func receiveFrom(buffer: Array<Byte>): (SocketAddress, Int64) func sendTo(address: SocketAddress, payload: Array<Byte>): Unit }

从接口定义可以提炼出数据报传输的几条关键语义:

  • 数据包边界保持:数据包大小在发送端决定,一端发送了 10 字节和 15 字节的报文,对端将收到相同大小的对应报文,不会像 TCP 那样发生字节流重组;
  • 传输不可靠:数据包传输不保证顺序、不保证送达,也允许空数据包存在;
  • 单包大小上限:UDP 套接字一次可以处理最大 64KB 的数据包;
  • 收发需要地址:receiveFrom返回(SocketAddress, Int64)二元组,同时给出"报文来自哪个地址"与"收取了多少字节";sendTo则需要显式传入对端地址。

完整示例:UDP 服务端与客户端收发 3 字节报文

示例源码位于 udp.md,它是一个"服务端先就绪、客户端后发送"的单机 UDP 通信程序:

import std.net.* import std.sync.* let SERVER_PORT: UInt16 = 33333 let barrier = Barrier(2) func runUdpServer() { try (serverSocket = UdpSocket(bindAt: SERVER_PORT)) { serverSocket.bind() barrier.wait() let buf = Array<Byte>(3, repeat: 0) let (clientAddr, count) = serverSocket.receiveFrom(buf) let sender = (clientAddr as IPSocketAddress)?.address.toString() ?? "" // Server receive 3 bytes: [1, 2, 3] from 127.0.0.1 println("Server receive ${count} bytes: ${buf} from ${sender}") } } main(): Int64 { let fut = spawn { runUdpServer() } barrier.wait() try (udpSocket = UdpSocket(bindAt: 0)) { // random port udpSocket.sendTimeout = Duration.second * 2 udpSocket.bind() udpSocket.sendTo( IPSocketAddress("127.0.0.1", SERVER_PORT), [1, 2, 3] ) } fut.get() return 0 }

运行结果:

Server receive 3 bytes: [1, 2, 3] from 127.0.0.1

代码结构逐段解析

1. 端口与同步原语

let SERVER_PORT: UInt16 = 33333 let barrier = Barrier(2)
  • SERVER_PORT是服务端监听端口,类型为UInt16(端口范围 0~65535);
  • Barrier(2)来自std.sync包,用于协调主线程与服务端线程"同时就绪"。Barrier提供协调多个线程一起执行到某一个程序点的功能:率先到达的线程进入阻塞,当所有线程都到达后才一起继续执行(见 sync_package_classes.md)。这里替代了sleep轮询等待,是更精确的同步手段。

2. 服务端:创建、绑定、阻塞接收

try (serverSocket = UdpSocket(bindAt: SERVER_PORT)) { serverSocket.bind() ... let (clientAddr, count) = serverSocket.receiveFrom(buf) }
  • UdpSocket(bindAt: SERVER_PORT)构造一个未绑定的套接字实例,bindAt!是标签参数,可传SocketAddress或UInt16(见 net_package_classes.md);
  • 创建实例后必须调用bind()完成绑定,才能在不与远端建连的前提下接收报文;
  • receiveFrom是阻塞式调用,将报文内容写入buf(3 字节缓冲区),返回发送端地址与报文大小。buf应选择合适的大小,否则报文会被截断,且返回值会大于缓冲区大小;
  • try (…)是仓颉的资源管理语法,块结束时自动调用close()回收套接字。UdpSocket需要被显式close()。

3. 发送端地址解析

let sender = (clientAddr as IPSocketAddress)?.address.toString() ?? ""

receiveFrom返回的clientAddr类型是SocketAddress,需要通过as IPSocketAddress向下转型为 IP 套接字地址,再取出其中的IPAddress并转成字符串;若转型失败则为None,用?? ""兜底。

4. 客户端:随机端口 + 超时 + 指定地址发送

try (udpSocket = UdpSocket(bindAt: 0)) { // random port udpSocket.sendTimeout = Duration.second * 2 udpSocket.bind() udpSocket.sendTo( IPSocketAddress("127.0.0.1", SERVER_PORT), [1, 2, 3] ) }
  • bindAt: 0表示请求操作系统分配随机空闲端口,适用于"只管发、不关心从哪个端口发"的客户端场景;
  • sendTimeout = Duration.second * 2设置send/sendTo操作的超时时间。超时时间设置过小会被钳制为最小时钟周期值,过大则被钳制为最大超时时间(2⁶³-1 纳秒),默认值为None(永不超时);
  • sendTo(IPSocketAddress("127.0.0.1", SERVER_PORT), [1, 2, 3])向目标地址发送 3 字节载荷[1, 2, 3],无需事先建立连接;
  • [1, 2, 3]会被自动推断为Array<Byte>。

5. 线程协同与收尾

let fut = spawn { runUdpServer() } barrier.wait() ... fut.get()
  • spawn创建仓颉线程执行服务端逻辑(见 create_thread.md)。仓颉采用抢占式线程模型与M:N线程调度,spawn接受无形参 lambda 作为新线程的入口;
  • 主线程与服务端线程各自调用一次barrier.wait(),凑齐 2 个等待者后同时放行,保证服务端bind()完成、客户端再开始发送;
  • fut.get()等待服务端线程结束,避免主线程提前退出导致服务端未打印结果;
  • main(): Int64返回 0 表示程序正常结束。

UdpSocket 核心 API 纵深解析

UdpSocket类的完整定义(见 net_package_classes.md):

public class UdpSocket <: DatagramSocket { public init(bindAt!: SocketAddress) public init(bindAt!: UInt16) }

构造与生命周期

API说明
init(bindAt!: SocketAddress)创建一个未绑定的UdpSocket实例,绑定地址与端口由SocketAddress指定
init(bindAt!: UInt16)创建一个未绑定的UdpSocket实例,仅指定绑定端口(本机地址自动选择)
bind(): Unit绑定本地端口。绑定失败后需要close套接字,不支持多次重试
close(): Unit关闭套接字,关闭后除close/isClosed外的所有操作均不允许再调用;接口允许多次调用
isClosed(): Bool判断套接字是否已通过close显式关闭

报文收发

API说明
receiveFrom(buffer: Array<Byte>): (SocketAddress, Int64)接收报文,返回发送端地址与实际收取的报文大小(可能为 0,或大于 buffer 大小)。当本机缓存过小无法读取报文时抛SocketException,读取超时抛SocketTimeoutException
sendTo(recipient: SocketAddress, payload: Array<Byte>): Unit发送报文到指定远端地址,对端缓存不足时可能被阻塞,报文可能被丢弃。payload 超出系统限制、系统发送失败(如connect被调用后收到异常 ICMP 报文)时抛SocketException
receive(buffer: Array<Byte>): Int64从connect连接到的地址收取报文(需先建连)
send(payload: Array<Byte>): Unit发送报文到connect连接到的地址(需先建连)

值得注意的是,UDP 套接字虽然本质是"无连接"的,但UdpSocket也提供了connect(remote: SocketAddress)/disconnect()接口实现逻辑上的"定向通信":

  • connect必须在bind之后调用,连接后仅接受该远端地址的报文,且此时可使用send/receive免地址收发;
  • 连接后端口会开始接收 ICMP 报文,若收到异常报文,可能导致send/sendTo执行失败;
  • disconnect可取消定向连接,可在connect前调用、可多次调用。

属性配置

属性类型对应系统选项说明
localAddressSocketAddress—读取套接字将要或已经绑定的本地地址
remoteAddress?SocketAddress—读取已连接的远端地址,未连接时返回None
receiveTimeout?Duration—设置/读取receive/receiveFrom超时,默认None
sendTimeout?Duration—设置/读取send/sendTo超时,默认None
receiveBufferSizeInt64SO_RCVBUF设置/读取接收缓冲区,size ≤ 0 抛IllegalArgumentException
sendBufferSizeInt64SO_SNDBUF设置/读取发送缓冲区,size ≤ 0 抛IllegalArgumentException
reuseAddressBoolSO_REUSEADDR设置/读取地址复用,默认与生效行为取决于系统
reusePortBoolSO_REUSEPORTWindows 无此属性,访问会抛SocketException

此外,UdpSocket还提供了底层getSocketOption/setSocketOption系列方法(含Bool与IntNative便捷重载),可通过level(如SOL_SOCKET)与option(如SO_KEEPALIVE)直接操作原生套接字选项。这一底层通道也是 socket_option.md 中"增加自定义属性"示例的实现基础——示例通过extend TcpSocket配合getSocketOptionIntNative/setSocketOptionIntNative自定义了customNoDelay属性,同样的手法可推广到UdpSocket。

地址类型:IPSocketAddress 与 IPAddress

示例中的IPSocketAddress("127.0.0.1", SERVER_PORT)与clientAddr as IPSocketAddress涉及std.net的两类核心地址对象(见 net_package_classes.md):

  • IPSocketAddress:实现 IP 协议 Socket 地址(IP 地址 + 端口号),提供三个构造函数:init(address: Array<Byte>, port: UInt16)(大端序字节数组)、init(address: String, port: UInt16)、init(address: IPAddress, port: UInt16);属性address(IPAddress)、port(UInt16)、family(AddressFamily)、size(原始字节长度);静态方法parse("192.168.1.2:8080")、tryParse;
  • IPAddress:IP 协议地址的抽象基类(IPv4Address/IPv6Address),提供parse/tryParse/resolve(域名解析)、isIPv4/isIPv6/isLoopback/isPrivate/isMulticast/isLinkLocal/isGlobalUnicast/isUnspecified等地址分类判断,以及getAddressBytes、getPrefix(prefixLen)等字节与子网操作。

异常体系

UDP 收发可能触及的两类异常(见 net_package_exceptions.md):

  • SocketException:继承自IOException,覆盖绑定失败、缓存不足无法读取报文、系统发送失败等套接字相关错误;
  • SocketTimeoutException:继承自Exception,在receiveFrom/sendTo超过设定的receiveTimeout/sendTimeout时抛出,便于实现带超时的收发逻辑。

UDP 编程模型:与 TCP 的对照

std.net官方 Socket 指南(net_socket.md)对两种场景给出了明确的编程模型对比:

UDP(不可靠传输)编程模型:

  1. 创建套接字,并指定本端绑定地址;
  2. 执行绑定;
  3. 指定远端地址进行报文发送;
  4. 不连接远端地址场景下,可以收取来自不同远端地址的报文,并返回远端地址信息。

TCP(可靠传输)编程模型(对照参考):

  1. 创建服务端套接字,指定本端绑定地址;
  2. 执行绑定;
  3. 执行accept阻塞等待客户端连接;
  4. 创建客户端套接字,指定远端连接地址;
  5. 执行连接;
  6. 连接成功后服务端在accept返回新套接字,双方通过该套接字读写。

两者的本质区别在于:TCP 是"面向连接 + 字节流",必须先建连再收发,数据无边界;UDP 是"无连接 + 数据报",绑定即可收发,每个报文自带边界、携带对端地址。这也是UdpSocket归属于DatagramSocket、而TcpSocket归属于StreamingSocket的原因。

仓颉并发文档(concurrency_overview.md)也提示了一个 UDP 场景相关的注意事项:仓颉采用M:N线程模型,当仓颉线程调用socket等可能阻塞的 foreign 函数时,会直接阻塞承载它的 native 线程。因此在 UDP 服务端中把阻塞式receiveFrom放入spawn的子线程执行(如示例所示),可以让主线程继续执行其他逻辑,避免整程序吞吐量下降。

实战要点与扩展方向

  • 报文大小边界:UDP 单报文最大 64KB,发送前应确保 payload 不超过该上限,接收方缓冲区应预置足够大小,否则报文将被截断;
  • 同步替代轮询:示例用Barrier(2)保证"服务端已绑定、客户端再发送",相比 sleep 固定时长更稳定;仓颉还提供SyncCounter、Channel等同步原语(参见 TCP 示例 中SyncCounter(1)+waitUntilZero()的用法);
  • 收包来源解析:receiveFrom返回的SocketAddress需向下转型为IPSocketAddress才能读取发送方 IP 与端口,这在高并发多客户端 UDP 服务中尤其关键——不同客户端的数据可能来自不同地址;
  • 定向连接优化:若通信对象固定,可先connect再使用send/receive免去每次携带目标地址,但注意connect会过滤掉其他来源的报文;
  • 同族参考:std.net还提供UnixDatagramSocket(基于数据包的本机进程间通信,不支持 Windows),其收发模型与UdpSocket一致,可参考 unix_datagram.md 对照学习;
  • 运行验证:示例代码可直接编译运行(cjc编译后在本机执行),输出Server receive 3 bytes: [1, 2, 3] from 127.0.0.1即表示收发链路正常。

参考文档索引

  • UDP 使用示例(本文主体)
  • std.net 包总览
  • UdpSocket 类完整 API
  • DatagramSocket 接口定义
  • IPSocketAddress / IPAddress 地址体系
  • Socket 编程指南(UDP/TCP 模型对照)
  • Barrier 同步原语
  • 仓颉线程创建
  • 套接字属性配置与自定义选项
  • 文档
  • 知识库
  • 教程
  • OpenHarmony

【免费下载链接】CangjieCorpus

本仓库为仓颉编程语言语料库,聚焦 仓颉编程语言 核心知识体系,整合官方开发指南、API 文档、典型代码示例及语法规范等多维度资源,经结构化处理与专业标注,构建为支持 RAG(检索增强生成)技术 的高质量知识基座,适配智能编码助手、学习辅助工具等应用开发需求。 欢迎开发者基于本语料库构建仓颉语言智能应用,推动鸿蒙开发生态智能化升级!

项目地址:https://gitcode.com/Cangjie/CangjieCorpus
点击查看免费下载

相关推荐

上一篇:如何一次性通过 WCAG 2.2 无障碍审计?web-quality-skills 的 40+ 条 a11y 规则速查清单
下一篇:Apache Mesos 代码格式化指南:ClangFormat 配置、编辑器集成与风格对齐实践

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询