UDP组播收发程序:Windows下网络调试的实用工具

发布时间:2026/10/17 2:54:45

UDP组播收发程序:Windows下网络调试的实用工具 简介一份面向Windows网络编程开发者的UDP组播多播工程示例基于Winsock应用程序接口实现数据报的发送与接收适合正在学习网际协议多播原理、练习套接字编程的入门或进阶开发者参考。资源包共36个文件整体大小仅315KB包含Visual Studio工程文件、C源代码、头文件、目标文件以及调试信息等结构紧凑可直接用Visual Studio打开编译快速搭建组播收发测试环境。压缩包内发送与接收两个子工程划分清晰分别对应发送端和接收端完整演示了加入多播组、设置网络接口、绑定端口、发送与接收数据、关闭套接字等关键步骤并结合实际代码展示了WIN32环境下的组播配置细节。已有1680人学习或下载对于希望理清UDP组播在Windows平台下落地实现、掌握多播套接字选项设置的应用开发者是一份简洁实用的参考工程。1. UDP组播收发程序排查网络问题时最趁手的调试工具在 Windows 下用 Visual Studio 调试局域网服务时经常遇到「网络包发出去了另一台机器就是收不到」的玄学问题。单播可以排查 IP、端口、防火墙广播又会造成全网冲击于是 UDP 组播成了最常用的中间态同一份数据只发给加入同一组播组的成员非常适合做服务发现、状态同步和设备控制。这个项目就是一个可在 VS 里直接编译运行的 UDP 组播发送与接收程序发送端支持自定义目标组播地址、端口、发送间隔和报文内容接收端可以实时显示组播数据并统计收包数。不管你是在调网络协议还是想给局域网内的多台设备同步数据都可以拿它当基础模板改一改。2. 组播原理与工程选型为什么 Windows 下要单独写一套收发程序2.1 组播地址与端口怎么选组播地址属于 D 类地址范围是 224.0.0.0 到 239.255.255.255。其中 224.0.0.0 到 224.0.0.255 是保留段用于本地链路协议不能随意使用比如 224.0.0.1 代表所有组播主机224.0.0.2 代表所有组播路由器。真正可分配给应用的是 224.0.1.0 到 238.255.255.255以及 239.0.0.0 到 239.255.255.255其中 239 开头的管理限定地址最适合局域网调试因为路由器默认不会向外转发。端口选择上UDP 组播和普通 UDP 没有区别仍然是 0 到 65535。但在 Windows 下接收端绑定的端口必须和发送端目标端口一致否则内核不会把报文投递到你的套接字。程序中我建议用 45678 这类高位端口避开系统服务常用的 1024 以下端口也避开可能被其他调试工具占用的 5000、8000 等热门端口。2.2 技术栈WinSock2 与 Visual Studio 工程配置这个项目基于 WinSock2 编写对应winsock2.h头文件和ws2_32.lib链接库。工程上我建立了两个互相独立的控制台应用一个做发送端一个做接收端。这样在 VS 里可以先启动接收端再启动发送端调试时不用来回切窗口也能分别设置不同项目的启动参数。创建工程时注意VS 自带的空项目模板默认不会链接ws2_32.lib需要在「项目属性 - 链接器 - 输入 - 附加依赖项」里手动加上更省事的做法是代码里写#pragma comment(lib, ws2_32.lib)让编译器直接把依赖链入。字符集选择上如果你的报文内容要支持中文建议把「项目属性 - 常规 - 字符集」设为「使用多字节字符集」或者代码里统一用 UTF-8 并在发送前转码。我习惯用多字节字符集因为组播调试场景里大部分报文是 ASCII 可见字符最省事。工程结构上发送端和接收端共用一个Common.h头文件里面定义了组播地址、端口、缓冲区大小等常量#pragma once #define WIN32_LEAN_AND_MEAN #include winsock2.h #include ws2tcpip.h #include stdio.h #include string.h #pragma comment(lib, ws2_32.lib) #define MULTICAST_ADDR 239.255.0.100 // 组播地址 #define MULTICAST_PORT 45678 // 组播端口 #define BUFFER_SIZE 1024 // 接收缓冲这里把 Winsock 相关的头文件固定成winsock2.h避免和windows.h里的旧版 socket 定义冲突。WIN32_LEAN_AND_MEAN是为了裁剪 Windows 头文件的体积防止某些宏定义干扰 Winsock 的 API。3. 发送端核心实现构造组播报文与自动重发机制3.1 发送端初始化创建套接字、设置 TTL、指定出口网卡发送端的第一步是调用WSAStartup初始化 Winsock 环境这个函数需要传入请求的版本号MAKEWORD(2, 2)并返回WSADATA结构。初始化成功后用socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP)创建一个 UDP 套接字SOCK_DGRAM表示数据报套接字。接着最关键的一步是设置 TTL也就是组播报文的存活时间。TTL 值决定报文能在网络中穿越多少跳默认值在不同系统上不一样Windows 下如果不设置默认可能是 1这就导致报文只在本机或直连网段内有效。如果要多级路由转发需要增大 TTL。另一个容易踩坑的是多网卡机器。一个 Windows 主机上可能有有线网卡、无线网卡和虚拟网卡组播报文默认从系统选中的网卡发出可能不是你期望的那块。正确做法是枚举本机网卡把想要使用的网卡 IP 通过IP_MULTICAST_IF选项绑定到套接字上。SOCKET sendSock; struct sockaddr_in localAddr; sendSock socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP); if (sendSock INVALID_SOCKET) { printf(socket failed: %d\n, WSAGetLastError()); return 1; } // 设置组播 TTL允许跨网段 unsigned char ttl 16; setsockopt(sendSock, IPPROTO_IP, IP_MULTICAST_TTL, (char*)ttl, sizeof(ttl)); // 指定出口网卡 IP这里用 192.168.1.100 示例 struct in_addr localItf; localItf.s_addr inet_addr(192.168.1.100); setsockopt(sendSock, IPPROTO_IP, IP_MULTICAST_IF, (char*)localItf, sizeof(localItf)); // 初始化目标组播地址结构 struct sockaddr_in remoteAddr; memset(remoteAddr, 0, sizeof(remoteAddr)); remoteAddr.sin_family AF_INET; remoteAddr.sin_port htons(MULTICAST_PORT); remoteAddr.sin_addr.s_addr inet_addr(MULTICAST_ADDR);这段代码里IP_MULTICAST_TTL的选项级别是IPPROTO_IP和常用的SOL_SOCKET不一样写错的后果是setsockopt直接返回失败而调用WSAGetLastError时错误码往往会指向无效参数。IP_MULTICAST_IF设置的 exit 接口是网卡的 IP 地址不是网卡名也不是 MAC 地址这一点和 Linux 下的setsockopt参数值类型基本一致但传给 Windows 的setsockopt时结构体字段需要手动赋 IP 值。3.2 循环发送与状态反馈发送端进入while循环后可以按固定间隔发送自定义报文。这里我加了一个序号字段方便接收端判断报文是否乱序或丢失。发送函数sendto的参数里目标地址就是刚才构造的remoteAddr返回值是实际发送的字节数如果返回SOCKET_ERROR需要用WSAGetLastError拿到错误码定位问题。char buffer[BUFFER_SIZE]; int seq 0; while (1) { // 构造一个带序号和时间戳的报文 snprintf(buffer, sizeof(buffer), HELLO_MCAST seq%d time%lu, seq, GetTickCount64()); int ret sendto(sendSock, buffer, (int)strlen(buffer), 0, (struct sockaddr*)remoteAddr, sizeof(remoteAddr)); if (ret SOCKET_ERROR) { printf(sendto failed: %d\n, WSAGetLastError()); break; } printf([send] %s (%d bytes)\n, buffer, ret); Sleep(1000); // 每次发送后休眠 1 秒 }sleep这里用的是 Windows 的Sleep单位是毫秒把 1000 改成 500 就是每 0.5 秒发一帧。如果你在调试设备发现协议的交互节奏往往需要把间隔调到 10 毫秒甚至 1 毫秒这时候要注意Sleep(1)实际精度不到 1 毫秒用timeBeginPeriod(1)可以提高系统定时器精度或者改用select超时来实现更准确的等待。GetTickCount64()返回的是开机以来的毫秒数用来标记发送时间很方便但它是相对时间不是 UTC 时间戳接收端如果要做跨设备时延计算需要同时传本机启动时间和当前时间。4. 接收端核心实现加入组播组、绑定端口与数据解析4.1 接收端初始化SO_REUSEADDR 与绑定本地端口接收端和发送端最大的区别是必须绑定端口并且要主动加入组播组。绑定端口时很多新手直接写bind(sock, (struct sockaddr*)localAddr, sizeof(localAddr))并把localAddr.sin_addr.s_addr设成INADDR_ANY这样绑的是本地所有地址的45678端口本身没问题。但如果同一台机器上有多个组播程序都在监听同一个端口默认情况下的绑定会冲突要么后启动的失败要么两个进程互踢。解决办法是在绑定之前设置SO_REUSEADDR。这个选项的语义在 Windows 上比 Linux 复杂一点它会允许多个套接字同时绑定同一端口前提是组播地址也相同。如果只是单进程单套接字设不设这个选项都能跑但我在调试时经常同一机器开多个接收端对比报文所以统一设置。SOCKET recvSock; struct sockaddr_in localAddr; recvSock socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP); // 允许端口复用方便多实例调试 int reuse 1; setsockopt(recvSock, SOL_SOCKET, SO_REUSEADDR, (char*)reuse, sizeof(reuse)); memset(localAddr, 0, sizeof(localAddr)); localAddr.sin_family AF_INET; localAddr.sin_port htons(MULTICAST_PORT); localAddr.sin_addr.s_addr htonl(INADDR_ANY); int ret bind(recvSock, (struct sockaddr*)localAddr, sizeof(localAddr)); if (ret SOCKET_ERROR) { printf(bind failed: %d\n, WSAGetLastError()); }INADDR_ANY是 0.0.0.0 的宏用htonl(INADDR_ANY)和直接INADDR_ANY效果一样因为 0 的字节序反转还是 0。绑定地址如果写成本机具体 IP则只会收到目的地址是该 IP 或组播地址的数据但绑定具体 IP 有一个副作用Windows 有时会把组播包统一从默认网卡旁的逻辑接口收进来导致你收不到其他网卡上的组播流。4.2 加入组播组与 recvfrom 循环在bind之后需要调用setsockopt并设置IP_ADD_MEMBERSHIP来加入组播组。这个选项的值是一个struct ip_mreq结构里面包含imr_multiaddr和imr_interface两个字段。imr_multiaddr是要加入的组播地址imr_interface是本机网卡 IP。如果imr_interface设为INADDR_ANY系统会自动选择一个网卡加入组播组通常能收到默认网卡的组播包但不保证是全部网卡。struct ip_mreq mreq; memset(mreq, 0, sizeof(mreq)); mreq.imr_multiaddr.s_addr inet_addr(MULTICAST_ADDR); mreq.imr_interface.s_addr htonl(INADDR_ANY); // 也可以用具体网卡 IP ret setsockopt(recvSock, IPPROTO_IP, IP_ADD_MEMBERSHIP, (char*)mreq, sizeof(mreq)); if (ret SOCKET_ERROR) { printf(IP_ADD_MEMBERSHIP failed: %d\n, WSAGetLastError()); return 1; }组播加入成功后接收端就进入recvfrom循环了。这里我把recvfrom设计成带超时的模式使用setsockopt配合SO_RCVTIMEO设置接收超时这样可以让主循环每 500 毫秒醒一次即使没有数据也能刷新统计界面不会阻塞在recvfrom里永远出不来。DWORD timeout 500; setsockopt(recvSock, SOL_SOCKET, SO_RCVTIMEO, (char*)timeout, sizeof(timeout)); char buffer[BUFFER_SIZE]; struct sockaddr_in fromAddr; int fromLen sizeof(fromAddr); int timeoutCount 0; while (1) { int len recvfrom(recvSock, buffer, BUFFER_SIZE - 1, 0, (struct sockaddr*)fromAddr, fromLen); if (len SOCKET_ERROR) { int err WSAGetLastError(); if (err WSAETIMEDOUT) { timeoutCount; printf([wait] receive timeout, count%d\n, timeoutCount); continue; } printf(recvfrom error: %d\n, err); break; } buffer[len] \0; printf([recv] %s, from %s:%d\n, buffer, inet_ntoa(fromAddr.sin_addr), ntohs(fromAddr.sin_port)); }WSAETIMEDOUT错误码只有在设置了SO_RCVTIMEO之后才会出现它是 10060。如果不设置超时recvfrom会一直阻塞关程序时只能强制终止进程。inet_ntoa返回的字符串是静态缓存下一次再调用会被覆盖如果要把来源 IP 保存下来必须用strcpy复制到自己分配的缓冲区中。5. 常见问题避坑组播收不到包时的排查路线5.1 防火墙拦截导致发送端 100% 丢包现象发送端一直打印sendto 成功接收端却一行数据也没输出Netstat或抓包工具看不到任何组播包。原因Windows 防火墙默认拦截传入的 UDP 数据报尤其是来自子网外或标记为不安全的组播流量。即使你在同一个局域网内某些杀毒软件的网络防护也会静默丢弃组播包发送端没有返回值因为这个丢包发生在协议栈之外。解决打开「控制面板 - Windows Defender 防火墙 - 高级设置 - 入站规则」新建一条允许 UDP 端口45678的规则作用域限定在本地子网。如果只做开发调试也可以临时关闭防火墙但记得测完立刻打开。还有一个更隐蔽的点发送端自身的出站规则也可能被拦截出站 UDP 到组播地址的报文有时候会被云安全客户端收走需要检查安全软件的日志。5.2 多网卡环境下数据从错误网卡发出现象主机上同时有以太网和 Wi-Fi发送端设置的是 Wi-Fi 网段的组播地址但接收端在以太网线缆上的另一台机器永远收不到。原因sendto的时候系统根据目标地址和路由表选择出口网卡如果没设置IP_MULTICAST_IF组播报文可能走了默认路由对应的网卡而不是加入了同一组播组的那个网卡。解决发送端强制设置出口 IP接收端加入组播组时也绑定同网段 IP。更通用的做法是用GetAdaptersAddresses枚举所有网卡过滤出Up状态的接口让用户通过命令行参数选择。顺序上一定先设置IP_MULTICAST_IF再调用sendto因为IP_MULTICAST_IF改变的是套接字的出口属性每次发送都会使用这个属性不是一次性参数。5.3 同机收发失效回环模式没开现象接收端和发送端在同一台 Windows 机器上接收端收不到发送端发出去的组播包但在另一台机器上能收到。原因组播默认会回环到本机吗Windows 的组播回环默认是开启的但有些网络驱动或虚拟网卡环境会关闭。更常见的坑是接收端绑定端口时没设置SO_REUSEADDR发送端也没有主动设置回环导致本机发出的组播报文无法进入本机接收队列。解决在发送端主动设置IP_MULTICAST_LOOP为允许回环int loop 1; setsockopt(sendSock, IPPROTO_IP, IP_MULTICAST_LOOP, (char*)loop, sizeof(loop));如果设了IP_MULTICAST_IF指定了某个特定网卡而接收端绑定在INADDR_ANY上回环机制有时还是能收到但最保险的做法是收发两端都指定同网卡 IP。同机调试时组播地址偶尔会和本机虚拟网卡冲突建议先只用一张物理网卡。5.4 绑定地址选错导致接收失败现象接收端绑定到192.168.1.10这个具体 IP然后在另一块网卡192.168.2.33上发送组播接收端收不到换成INADDR_ANY就收到了。原因Windows 的组播实现中绑定到单播 IP 会让套接字只监听该 IP 对应的数据路径。当组播报文从另一块网卡进入时内核组播成员关系是在另一个网络接口上维护的所以recvfrom不会收到数据。解决普通接收场景直接绑定INADDR_ANY这样协议栈会在所有已加入组播组的接口上分发数据。只针对特定接口接收时才绑定具体 IP同时IP_ADD_MEMBERSHIP里也要写对应的imr_interface。这里有个记忆技巧bind管的是本地收包「谁可以找过来」IP_ADD_MEMBERSHIP管的是「你要参与的组播会话在哪条链路上」。6. 二次开发技巧把收发程序改造成可配置的组播调试助手6.1 用配置文件管理地址、端口、网卡与报文内容把硬编码地址抽到配置文件里是二次开发最开始要做的事。我一般会做一个简单的config.ini格式用键值对避免引入第三方解析库[multicast] address239.255.0.100 port45678 ttl16 local_ip192.168.1.100 interval_ms1000 message_templateHELLO_MCAST seq%d time%lu程序启动时逐行读入把local_ip设置生效。这里有个坑Windows 下GetPrivateProfileString可以快速读取 ini但它要求文件编码固定在 ANSI如果你在 VS 工程里用 UTF-8 保存配置文件会读到乱码。我更喜欢自己写一个简单的解析函数按行读取、按切分兼容 UTF-8 不带 BOM。改造后发送端变成一个可配置的探测工具组播地址改成224.0.0.251就等于顺便探测 mDNS 流量改成239.255.255.250就能和 SSDP 服务通信。这个自由度比每次都改代码重新编译强太多。6.2 增加报文收发日志与统计调试组播程序时最需要的是统计指标发送总包数、每秒钟包速率、重传次数、接收端收到报文的时间间隔抖动。我在接收端加了三个全局计数器volatile long long totalPackets 0; volatile long long lastSecondPackets 0; volatile long long lastSecondTimestamp 0;每次recvfrom成功后在主循环里更新totalPackets然后另起一个线程每秒计算一次吞吐量并打印while (1) { Sleep(1000); long long now GetTickCount64(); double rate (now - lastSecondTimestamp 0) ? lastSecondPackets * 1000.0 / (now - lastSecondTimestamp) : 0.0; printf(throughput: %.2f pkt/s, total%lld\n, rate, totalPackets); lastSecondPackets 0; lastSecondTimestamp now; }同时把报文内容和来源 IP 写入日志文件按天滚动。这样你在现场调设备时只需要把运行日志拿来就能确认是组播包根本没有到达本机还是到达了但应用层处理太慢。6.3 扩展思路多线程接收与定时器发送如果要把这个程序当长期运行的组播网关就不能让发送端和接收端各跑各的控制台了。我建议合并成一个进程内部开两个线程接收线程负责recvfrom并将数据推入环形队列主线程负责处理业务逻辑并把响应组播回去。这样就需要考虑套接字的线程安全性。Winsock 的套接字句柄在同一个进程内跨线程使用操作系统的同步由内核保证但你自己得保证不要同时对一个句柄调用closesocket。我在做类似改动时的习惯是接收线程独占recvfrom发送线程独占sendto共享数据用锁或原子变量来保护。如果要做定时发送可以用timeSetEvent或者CreateTimerQueueTimer回调里调sendto不能阻塞太久因为回调线程是系统线程阻塞会影响整个进程的定时精度。实测下来 1 毫秒到 10 毫秒级别的组播定时发送这个方案都稳定。最后分享一下我的教训当年调一台嵌入式设备的组播发现协议怎么改 IP 都收不到折腾一整天才发现是设备网络上有两台机器绑定了相同的组播地址导致某台机器频繁发起 IGMP 离开请求把我的接收端踢出了组播组。从那以后我每次调试组播都会先抓包看 IGMP 报文再确认发送端和接收端都加入了同一个组播组避免在地址和端口上反复碰运气。这组收发程序就是我日常排查问题的基础工具希望你也能通过它跑通自己的组播链路。本文还有配套的精品资源点击获取
延伸阅读

更多相关文章

2026/10/17 2:49:45

初识C语言(mac版)

个人学习的心得记录和知识梳理1. C语言是什么?C语言是一门计算机语言,是人和计算机交流的语言。 人和人交流使用的是自然语言,如:汉语、英语、日语。 那人和计算机是怎么交流的呢?使用计算机语言; 人是通过计算机语言写的程序,给计算…

2026/10/17 2:49:45

面试官问:AI十分钟能写完的代码,你为什么要学五年?

摘要:2026年9月,一位5年经验的Java工程师在大厂终面被问:“你简历上这些技术栈,AI十分钟就能写出生产级代码。你花五年学的东西,价值到底在哪?”他答不上来。评论区几百条回复几乎全是同一个意思&#xff1…

2026/10/17 4:14:50

AI驱动的企业数字孪生:从仿真复现到决策优化

1. 为什么企业数字孪生必须叠加AI:传统仿真模型的三个死穴前两年我在一家制造企业做产线仿真项目,当时用传统数字孪生工具给车间搭了一个高精度三维模型,设备运动逻辑、节拍、缓存区全都对上了,模型跑起来跟现场监控画面几乎一帧不…

2026/10/17 4:14:50

从思维链到LoRA:DeepSeek应用手册核心机制全解读

简介:一份面向DeepSeek用户的实操型应用手册,聚焦多模型协同、私有数据集接入与参数理解等核心主题,适合深度学习初学者、自然语言处理开发者和需要借助大模型提升效率的内容创作者。手册围绕R1与V3的分工配合、文件上传类型、联网搜索时机、…

2026/10/17 4:14:50

FreeBSD 9.2 DPI系统PanabitFREE深度解析与实战部署

简介:PanabitFREE_SANGUOr10_20150513_FreeBSD9.2_dev.tar.gz 是一款面向网络管理员与开源系统开发者的深度流量审计与策略管控系统源码包,基于稳定高效的 FreeBSD 9.2 平台构建,专为中小企业及实验室环境提供免费、可定制的网络行为分析、带…

2026/10/17 4:14:50

AI指导AI跨领域创作全流程拆解:从一句诗到一条视频

要说这一年我在内容创作上最大的感受,就是AI工具链正在从一个“只会接活儿的打字员”,变成真正能和我一起开脑洞、做判断、改方案的创作搭子。尤其最近我把一个跨领域创作项目完整跑了一遍,整个流程从灵感到成片,几乎每一步都有AI…

2026/10/17 4:14:50

AI驱动数字孪生:从可视化大屏到决策优化的实战指南

1. 项目概述:数字孪生不是新概念,但AI驱动让价值彻底变了这两年“数字孪生”被说烂了,随便一个可视化大屏厂商都敢在自己的产品上贴这个标签。但如果你真在企业里做过数字化转型项目,就会明白:数字孪生的价值根本不在于…

2026/10/17 4:09:50

原生JS自定义右键菜单:从原理到实战的完整实现指南

1. 为什么2021年还在折腾自定义右键菜单浏览器默认的右键菜单从诞生那天起就没怎么变过——复制、粘贴、检查元素,翻来覆去就那几项。对于一个功能稍微复杂点的Web应用来说,这东西既占地方又帮不上忙。2021年那会儿,我手上正好有个在线协作类…

2026/10/16 5:33:43

Python调用Gemini Structured Outputs实现工单路由门禁

客服工单最怕的不是模型“答错一句话”,而是它给出一段看起来合理的说明,程序却从中猜错优先级。通俗做法是:要求模型只交 JSON(JavaScript Object Notation,轻量数据格式),再让代码验证它。Gem…

2026/10/16 4:03:48

Spring Boot超市进销存系统毕设实战:从需求拆解到答辩通关

最近带的一个学生项目组里,有A同学跑来问我:选什么毕设题目最稳妥,既能让评审老师觉得工作量够,又不会在答辩时被问到语无伦次。我第一反应就是推荐基于Spring Boot的超市仓库管理系统——也就是超市进销存系统。这个题目乍一看平…

2026/10/15 13:42:16

Flutter StatefulWidget 生命周期核心解析

很多刚开始接触 Flutter 的朋友,在看完一堆“Hello World”和基础组件之后,大概率都会撞上同一堵墙:StatefulWidget 里那堆 initState、build、dispose 方法,到底什么时候被调用?为什么顺序是那样?在里面到…

2026/10/17 0:04:32

PlanarSLAM:面向曼哈顿世界的点线面联合SLAM系统

简介:本资源是面向机器人视觉与SLAM研究者的C开源RGB-D SLAM系统实现,专为结构化室内场景(如办公室、走廊)设计,通过融合点、线、平面三类几何特征并引入“曼哈顿世界”假设,显著提升位姿跟踪鲁棒性与稠密地…

2026/10/17 0:04:32

TDH Workflow调度实战:从DAG依赖驱动到任务编排与故障排查

1. 从一个调度脚本说起:TDH Workflow到底在解决什么问题第一次接触TDH的Workflow,是在一个数据清洗项目里。当时团队每天要跑几十个ETL任务,任务之间有严格的先后依赖——A表清洗完才能跑B表的关联,B表跑完才能触发C表的聚合。最开…

还想了解更多?直接咨询顾问

免费诊断 + 免费方案 + 透明报价。

全国咨询热线400-8866-253
免费获取方案
☎咨询二维码 ☎ ↑