- 文档
- 知识库
- 教程
- OpenHarmony
【免费下载链接】CangjieCorpus
本仓库为仓颉编程语言语料库,聚焦 仓颉编程语言 核心知识体系,整合官方开发指南、API 文档、典型代码示例及语法规范等多维度资源,经结构化处理与专业标注,构建为支持 RAG(检索增强生成)技术 的高质量知识基座,适配智能编码助手、学习辅助工具等应用开发需求。 欢迎开发者基于本语料库构建仓颉语言智能应用,推动鸿蒙开发生态智能化升级!
导读
本文以仓颉标准库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前调用、可多次调用。
属性配置
| 属性 | 类型 | 对应系统选项 | 说明 |
|---|---|---|---|
localAddress | SocketAddress | — | 读取套接字将要或已经绑定的本地地址 |
remoteAddress | ?SocketAddress | — | 读取已连接的远端地址,未连接时返回None |
receiveTimeout | ?Duration | — | 设置/读取receive/receiveFrom超时,默认None |
sendTimeout | ?Duration | — | 设置/读取send/sendTo超时,默认None |
receiveBufferSize | Int64 | SO_RCVBUF | 设置/读取接收缓冲区,size ≤ 0 抛IllegalArgumentException |
sendBufferSize | Int64 | SO_SNDBUF | 设置/读取发送缓冲区,size ≤ 0 抛IllegalArgumentException |
reuseAddress | Bool | SO_REUSEADDR | 设置/读取地址复用,默认与生效行为取决于系统 |
reusePort | Bool | SO_REUSEPORT | Windows 无此属性,访问会抛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(不可靠传输)编程模型:
- 创建套接字,并指定本端绑定地址;
- 执行绑定;
- 指定远端地址进行报文发送;
- 不连接远端地址场景下,可以收取来自不同远端地址的报文,并返回远端地址信息。
TCP(可靠传输)编程模型(对照参考):
- 创建服务端套接字,指定本端绑定地址;
- 执行绑定;
- 执行
accept阻塞等待客户端连接; - 创建客户端套接字,指定远端连接地址;
- 执行连接;
- 连接成功后服务端在
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(检索增强生成)技术 的高质量知识基座,适配智能编码助手、学习辅助工具等应用开发需求。 欢迎开发者基于本语料库构建仓颉语言智能应用,推动鸿蒙开发生态智能化升级!
相关推荐
仓颉语言 Socket 编程实战:基于 std.net 的 TCP 与 UDP 套接字开发指南
仓颉语言 Socket 编程实战:基于 std.net 的 TCP 与 UDP 套接字开发指南 本指南以仓颉标准库 std.net 中的套接字(Socket)编
文档编程语言仓颉 std.net 网络编程类库全解析:IP 地址、Socket 地址与 TCP/UDP/UDS 套接字实战指南
仓颉 std.net 网络编程类库全解析:IP 地址、Socket 地址与 TCP/UDP/UDS 套接字实战指南 导读 本文以仓颉编程语言标准库 std.ne
文档知识库教程OpenHarmony仓颉编程语言 std.net 套接字属性配置与自定义扩展实战:TcpSocket 选项调优指南
仓颉编程语言 std.net 套接字属性配置与自定义扩展实战:TcpSocket 选项调优指南 本文基于仓颉标准库 std.net 的官方示例文档,系统讲解如何
文档知识库教程OpenHarmony
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考