UDP Socket的实现

本篇主线

环节 要解决的问题 结论
地址准备 IP 怎么写进 sockaddr_in 字符串 → 4 字节 → 网络序,inet_addr 一步完成
创建 网络文件怎么打开 socket() 返回一个文件描述符
★ 关键认知 填完结构体就算设置好了吗 没有 。填的只是自己栈上的变量,必须 bind 才交给内核
服务端骨架 为什么程序不能退出 服务器必须 7×24 死循环
收 数据怎么上来、怎么知道是谁发的 recvfrom,地址是出参
发 怎么回给对方 sendto,地址是入参
客户端 要不要 bind 要,但不显式绑 ,由 OS 首次 sendto 时随机分配
★ 地址坑 服务端该绑哪个 IP 不绑特定 IP ,用 INADDR_ANY(0.0.0.0)

一、起点:报文里必须带上自己的地址

前一篇的结论是:IP + Port 才能定位全网唯一的一个进程。落到代码上,这条结论立刻变成一个硬要求:

方向 报文里必须携带
客户端 → 服务端 自己的 IP、自己的端口
服务端 → 客户端 自己的 IP、自己的端口

原因很直接:对方将来要拿着这两个值把消息发回来。如果不带,对方就不知道该往哪儿回。

于是填充 sockaddr_in 变成两件事:

字段 含义 要点
sin_family 网络通信还是本地通信 地址类型(协议家族)
sin_port 端口号 2 字节 16 位 → htons() 主机序转网络序
sin_addr IP号 4 字节 IP → 也要转网络序

💡 注意端口号是 16 位、IP 地址是 32 位,所以用的转换函数不同:htons 对 16 位,htonl 对 32 位。而 IP 地址的转换在库函数里被合并成一个接口,见下文。


二、把点分十进制 IP 变成 4 字节

✔ 结论:这套转换自己写完全可行,但没必要 ------ 系统已经提供了现成接口。

系统接口:inet_addr

c 复制代码
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>

in_addr_t inet_addr(const char *cp);
项 内容
输入 点分十进制字符串
输出 in_addr_t(本质就是 uint32_t),已经是网络序
效果 上文的两步(字符串 → 4 字节、4 字节 → 网络序)一步完成
失败 返回 INADDR_NONE

💡 也就是说,前面费力气讲的拆分与大小端,目的不是「以后自己写」,而是理解这个接口内部在干什么 。理解了,后面看到 inet_addr 就不会觉得它是魔法。

常见的三个 IP 转换接口分工:

接口 作用 备注
inet_addr() 字符串 → 网络序 4 字节 最常用;无法区分 255.255.255.255 与出错
inet_aton() 字符串 → struct in_addr 有独立的返回值,更稳妥
inet_ntoa() 4 字节 → 字符串 打印地址时用

三、★ 填结构体 ≠ 设置给内核

这是本讲最容易出错、也最值得单独记住的一个认知。

写完下面这段代码后,一个自然的问题是:到这里,网络文件是不是已经拥有 IP 和端口了?

c 复制代码
struct sockaddr_in local;
local.sin_family = AF_INET;
local.sin_port   = htons(port);
local.sin_addr.s_addr = inet_addr(ip);

✔ 答案:没有。

原因很干脆:

  • struct sockaddr_in 只是系统提供的一种数据类型;
  • struct sockaddr_in local; 是在自己的用户栈(自己的虚拟地址空间)上开辟的一个变量;
  • 往 local 里填字段,改的只是自己栈上这个变量的内容;
  • 整个过程没有一次和内核发生交互。

✔ 一句话:填结构体只是「准备好一张地址表」,真正把这张表交给内核的动作是 bind。

这个区分非常重要,因为它解释了为什么必须有 bind 这个系统调用 ------ 否则内核根本不知道「哪个网络文件对应哪个地址」。


四、socket():创建套接字

第一阶段叫「创建套接字」,用一句话说就是:打开一个网络文件。

c 复制代码
int sockfd = socket(AF_INET, SOCK_DGRAM, 0);
参数 取值 含义
地址家族 AF_INET 网络通信(IP 协议家族)
套接字类型 SOCK_DGRAM 数据报套接字 → 对应 UDP
协议 0 默认,由前两个参数决定

💡 如果第二个参数写 SOCK_STREAM,对应的就是 TCP 的字节流套接字。

从系统层面理解这个 sockfd,其实一点都不陌生:

步骤 发生的事
① 进程的文件描述符表 中打开一个 struct file 对象
② 把该对象的地址填入文件描述符表
③ 返回一个合法的数组下标 ,这就是 sockfd

✔ 所以套接字在系统层面和普通文件是同一个模型 ------ 只不过它背后接的不是磁盘,而是协议栈。

返回值判断:sockfd < 0 表示失败,打印 errno 并退出(退出码习惯上给 2,便于区分是创建失败还是绑定失败)。


五、bind():把地址绑定到套接字

5.1 三个参数

c 复制代码
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
参数 传什么 说明
sockfd 上一步创建的 fd 要绑定的那个网络文件
addr (struct sockaddr *)&local 类型不匹配,必须强转
addrlen sizeof(local) 该结构体的长度

为什么要强转? 这是前一篇 sockaddr 继承体系的直接应用:

  • 接口只认「基类指针」struct sockaddr *;
  • 实际填的是「子类」struct sockaddr_in;
  • 子类的首字段保留了基类的 16 位地址类型,所以强转之后,函数内部按 sa_family 分发,能正确还原成 sockaddr_in 来解释。

5.2 返回值与错误处理

返回值 含义
0 成功
-1 失败,errno 被设置

绑定失败要不要继续?不能继续。

绑不上就意味着这个服务器既没有 IP 也没有端口 ------ 在全网范围内它根本无法被定位,起来也没有意义。

所以标准处理是:打印 errno → exit(2)。

5.3 服务端初始化只有两步

走到这里,UDP 服务端的「初始化」就全部完成了:

步骤 动作 结果
① socket() 有了一个网络文件(fd)
② bind() 这个文件与本地 IP + 端口建立关联

✔ 就这么两步。 不需要连接、不需要握手、不需要监听队列 ------ 因为 UDP 不面向连接。


六、★ 服务器必须是一个死循环

6.1 一个被忽略的差异

平时写的 C/C++ 程序,跑起来一瞬间就结束、结果打印出来,进程退出。但真正在用的软件启动之后默认不退出,除非用户主动关闭:

软件 行为
直播客户端 不关就一直开着
画图板 / VSCode / 终端 / 音乐软件 不关就一直开着
手机上的抖音、快手、拼多多 同上
某些后台软件 开机自启,关机才退

6.2 服务器是 7×24 的

对服务器而言这不是习惯,而是要求:

  • 半夜能刷抖音、逛淘宝,是因为对端服务一直在跑;
  • 云服务器买到手几乎不会关机 ------ 服务器永远不退,除非报废;
  • 一旦部署了服务,从启动到下线,整个过程都不能退出。

所以服务端主循环写成:

cpp 复制代码
bool is_running = true;

while (is_running) {
    // 收消息
    // 处理
    // 发消息
}

💡 死循环怎么杀掉?靠信号让它响应(信号部分后续再展开)。所以「死循环」不是缺陷,而是服务端程序的正常形态。


七、recvfrom():收数据,顺手记下对方是谁

c 复制代码
ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags,
                 struct sockaddr *src_addr, socklen_t *addrlen);

7.1 六个参数逐一看

# 参数 类型 谁填 含义
1 sockfd int 传 自己创建的套接字
2 buf void * 传 用户自定义的接收缓冲区
3 len size_t 传 缓冲区有多大
4 flags int 传 通常 0 → 阻塞读
5 src_addr struct sockaddr * 内核回填 对端(谁发来的)地址
6 addrlen socklen_t * 出入参 结构体大小 / 实际读到的信息大小

第 2、3 个参数的作用,用以前写文件读取的经验就能类推:要读数据,总得自己准备一块缓冲区。区别只在于 ------ 这次数据是从内核协议栈里读上来的,而不是从磁盘。

7.2 阻塞式 IO

flags = 0 表示阻塞式 IO。

概念 含义
阻塞等待 条件不满足就一直等(进程等待时学过)
进程状态 有阻塞态、阻塞挂起(进程状态时学过)
本次的具体表现 对方不发数据,进程就停在这个函数里等

💡 这个行为和 scanf 完全一样:从键盘读不到数据时,scanf 就卡在那里。网络收不到数据时,recvfrom 也卡在那里。

一旦数据到达,函数立刻返回,并把数据从内核缓冲区拷贝到用户缓冲区。

7.3 ★ 为什么必须要 src_addr

先用一个现实场景把问题问清楚:服务端收到一条「你好」,需不需要知道是谁发的?

答案是需要,而且必须知道。理由有两层:

层次 理由
业务层 一个服务器同时会服务很多客户端,必须区分来源
技术层 要回消息,就必须知道往哪个地址回

过年收礼的类比 :家里会来很多客人,各自带来礼物。收下礼物只是第一件事;还必须记住每一份礼物是谁带来的 ------ 因为将来要去还礼。如果不知道谁送的,回礼时就出问题了。

于是 recvfrom 的第 5、6 个参数承担这件事:

复制代码
src_addr  →  内核把报文的源 IP + 源端口填进来
addrlen   →  内核把「实际写了多少字节」填回来

✔ 这就是 UDP 的必然设计:因为不面向连接,服务端事先不可能知道谁会发来数据,只能「收完之后从报文里把对方地址读出来」。

7.4 addrlen 为什么是「出入参」

阶段 它的值 作用
调用前 sizeof(peer) 告诉内核:这块缓冲区有多大(防止写越界)
调用后 实际写入的字节数 告诉调用者:真正填了多少信息

在纯网络场景下这两个值往往相等,但接口必须留出这个位置 ------ 因为同一套接口还要支持本地套接字(sockaddr_un),那时结构体长度可能不同。

✔ 使用上的硬规矩:每次调用前都要重新赋值为 sizeof(peer),不能只赋一次。

7.5 读完之后的处理

c 复制代码
ssize_t s = recvfrom(sockfd, buf, sizeof(buf) - 1, 0,
                     (struct sockaddr *)&peer, &len);

if (s > 0) {
    buf[s] = 0;          // 当字符串看,手动补结尾,否则打印乱码
    // 业务处理 + 日志
}
返回值 含义
> 0 实际收到的字节数
-1 出错,errno 被设置

💡 缓冲区传 sizeof(buf) - 1 是为了给结尾的 '\0' 留一个位置。


八、sendto():发数据,必须指明发给谁

c 复制代码
ssize_t sendto(int sockfd, const void *buf, size_t len, int flags,
               const struct sockaddr *dest_addr, socklen_t addrlen);

8.1 与 recvfrom 的参数对照

位置 recvfrom sendto 差别
1 sockfd sockfd 同一个
2 void *buf const void *buf 多了 const
3 len len 同上
4 flags flags 同上
5 src_addr 出参 dest_addr 入参 方向相反
6 addrlen 出入参 addrlen 入参 发送时不需要回填

✔ 唯一的本质差别:收的时候地址是「事后拿到」,发的时候地址是「事先必须知道」。

返回值同样是「实际发送的字节数」,失败返回 -1 并设置 errno。

8.2 ★ 全双工

recvfrom 和 sendto 用的是同一个 sockfd。这一点和普通文件不同:

对象 读 / 写能力
普通文件 以读方式打开只能读、以写方式打开只能写;读写方式打开还要处理读写位置
UDP 套接字 同一个 fd 既能读又能写

✔ 结论:UDP 通信是全双工的。

回顾「半双工 / 全双工」这一对概念(讲管道时出现过):

模式 定义 现实类比
半双工 任一时刻只允许一方发、另一方收 日常对话:一方说,另一方听
全双工 双方可以同时收发 吵架:两个人同时输出,谁也没听对方

💡 补充:管道其实是特殊的半双工。


九、Echo Server 的完整实现

9.1 什么是 Echo

客户端发什么,服务器就回什么。 一句来、一句回,用来验证网络链路是否通。

角色 动作
客户端 输入一段文字 → 发给服务器 → 等服务器回显
服务端 收到文字 → 原样(可加前缀)→ 发回给来源

9.2 为什么第一份网络代码要写 Echo

理由 说明
UDP 最简单 不建连接、不管可靠性、不处理字节流边界,注意力可以集中在接口本身
语义最好验证 收到什么就该是什么,一眼看出对错
最小可运行骨架 换成别的业务,只需要改「处理」那一步

9.3 服务端流程

阶段 动作
初始化 socket() → 填写地址结构 → bind()
主循环 recvfrom() → 判断 → 拼回显字符串 → sendto() → 回到 recvfrom()
cpp 复制代码
// 初始化
int sockfd = socket(AF_INET, SOCK_DGRAM, 0);
if (sockfd < 0) { /* 打印 errno,exit(2) */ }

struct sockaddr_in local;
memset(&local, 0, sizeof(local));
local.sin_family = AF_INET;
local.sin_port   = htons(port);
local.sin_addr.s_addr = INADDR_ANY;          // 关键,见第十一节

if (bind(sockfd, (struct sockaddr *)&local, sizeof(local)) < 0) {
    /* 打印 errno,exit(2) */
}

// 主循环
bool is_running = true;
while (is_running) {
    struct sockaddr_in peer;
    socklen_t len = sizeof(peer);
    char buf[1024];

    ssize_t s = recvfrom(sockfd, buf, sizeof(buf) - 1, 0,
                         (struct sockaddr *)&peer, &len);
    if (s > 0) {
        buf[s] = 0;
        // 日志:打印收到的内容
        // 日志:打印对端地址(需用 inet_ntoa 转成字符串)

        std::string echo = "echo server: ";
        echo += buf;
        sendto(sockfd, echo.c_str(), echo.size(), 0,
               (struct sockaddr *)&peer, len);
    }
}

peer 这个变量虽然本讲没有做后续处理,但参数不能省 ------ 一个客户端未来完全可能同时访问多台服务器,所以「这次是谁回的」必须由接口回填。

9.4 命令行参数

服务端启动形式:

bash 复制代码
./udp_server <server_port>
检查 处理
argc != 2 打印 usage,return 1
端口号 argv[1] 是字符串,需 atoi 转成整数

💡 参数数量必须数准:程序名占 argv[0],所以「一个端口号」对应的 argc 是 2。


十、客户端

10.1 ★ 客户端怎么知道服务器的 IP 和端口

这是很多人第一次写网络程序时的疑问:服务器在别人机房,地址从哪儿来?

答案只有一句话:

✔ 客户端和服务器,是一家公司写的。

场景 地址从哪来
抖音客户端 内置了字节跳动的服务器 IP 和端口
淘宝客户端 内置了淘宝的服务器地址
Linux 的 apt / yum 软件源地址写在配置里,本质上也是内置
自己写的客户端 启动时由命令行参数传入

✔ 所以客户端知道服务器地址一点都不奇怪 ------ 它是随软件一起发布的内置信息。

这也是课堂上能把编译好的客户端分发给所有人、直接连演示用服务器的原因:客户端本身就带着目标地址。

10.2 ★ 客户端要不要 bind

这个问题要分两半回答。

问题 答案
客户端需不需要有 IP 和端口? 需要 ------ 否则无法在全网被定位,对方没法回消息
客户端需不需要显式 bind? 不需要 ,而且强烈不建议

那它的地址从哪来?

✔ 首次调用 sendto 时,由操作系统自动完成绑定 :IP 用本机 IP,端口从空闲端口里随机取一个。

为什么要随机?

因为「一个端口只能被一个进程占用」。一台手机上装了几十个甚至上百个 App,如果每个都显式绑一个固定端口:

情形 结果
京东 App 绑了 6666 占用成功
淘宝 App 也想绑 6666 绑不上,起不来
两家公司不可能事先商量「一方绑 6666、另一方绑 7777」 ------

✔ 结论:客户端用随机端口,是为了避免本机多个客户端之间的端口冲突。

由此可得两条推论:

  • 客户端的端口号是几并不重要,唯一就行;
  • 同一台机器上所有 App 的 IP 是一样的,区别只在端口。

对照服务端:

角色 为什么要这样
服务端 会被大量客户端访问,IP 和端口必须众所周知且不能改,一改客户端就找不到
类比 110 / 119 / 120 这类公共服务号码绝不能变;个人手机号则随便换

10.3 客户端流程

步骤 动作 说明
① socket() 创建套接字
② 填写服务端 的 sockaddr_in sin_port = htons(server_port)、sin_addr = inet_addr(server_ip)
③ sendto() 必须带上服务端地址
④ recvfrom() 等回显;地址参数同样不能省
⑤ 关闭 释放 fd
cpp 复制代码
// 1. 创建
int sockfd = socket(AF_INET, SOCK_DGRAM, 0);

// 2. 填写【服务端】地址
struct sockaddr_in server;
memset(&server, 0, sizeof(server));
server.sin_family = AF_INET;
server.sin_port   = htons(server_port);
server.sin_addr.s_addr = inet_addr(server_ip);

// 3. 发
std::string input;
std::getline(std::cin, input);
sendto(sockfd, input.c_str(), input.size(), 0,
       (struct sockaddr *)&server, sizeof(server));

// 4. 收
char buf[1024];
//客户端对接受的信息暂时不作处理,这里只要把后续的参数填充就行了
struct sockaddr_in peer;
socklen_t len = sizeof(peer);
ssize_t s = recvfrom(sockfd, buf, sizeof(buf) - 1, 0,
                     (struct sockaddr *)&peer, &len);
if (s > 0) {
    buf[s] = 0;
    std::cout << buf << std::endl;
}

💡 注意客户端也可以不 bind :第一次 sendto 时内核自动补上本地地址。但 recvfrom 的后两个参数仍然要填,因为将来可能同时访问多台服务器。

10.4 服务端与客户端的动作顺序

角色 顺序
客户端 先 发 ,后 收
服务端 先 收 ,后 发

✔ 这个顺序差异源自角色分工:请求永远由客户端发起,服务端被动处理。


十一、★ bind 的四种情况

代码写完之后,实际运行时出现了四种现象。这一节是整堂课最有价值的部分,因为它把「地址」这件事彻底讲透了。

11.1 现象汇总

# server 绑的地址 client 用哪个地址访问 结果
① 公网 IP(如 8.137.19.140) ------ 绑定直接失败
② 127.0.0.1 127.0.0.1 ✔ 通
③ 内网 IP(如 172.18.45.153) 同一个内网 IP ✔ 通
④ 内网 IP 127.0.0.1 ✖ 访问不了(反过来也不行)

11.2 公网 IP 为什么绑不上

关键观察:在机器上执行 ifconfig / ip addr,根本看不到那个公网 IP。

原因在于云厂商的地址分配方式:

步骤 发生的事
① 公网 IP 由腾讯云 / 阿里云持有,再分配给用户
② 但这个地址并没有真正配置到本机网卡上
③ 云厂商内部做了一次公网 IP → 内网 IP 的映射
④ 外部报文先到云厂商,再由它转发到对应的内网 IP

✔ bind 的本质是「占用本机某块网卡上的某个地址」。机器上根本没有这个地址,自然绑不上。

💡 附带的好处是安全:机器不直接暴露在公网上,云厂商在中间挡了一层。后面讲代理服务器时,这件事会更清楚。

⚠ 但这带来一个现实问题:绑不上公网 IP,别人怎么跨网络访问? 课堂上留作后续话题。

11.3 绑了哪个 IP,就只能用哪个 IP 访问

用 netstat 可以直观看到原因。

bash 复制代码
netstat -naup

参数含义(顺序无关):

参数 含义
a 显示所有
u 只看 UDP
n 全部数字化 (不加 n 会把地址反解成主机名、端口反解成服务名)
p 显示进程相关信息

绑定内网 IP 时,输出是:

复制代码
Local Address          PID/Program name
172.18.45.153:8080     xxxx/udp_server

而客户端访问的是 127.0.0.1:8080。内核在本机上找「127.0.0.1:8080 这个地址」------不存在,所以访问不了。

✔ 结论:一旦显式 bind 了某个 IP,客户端就必须使用那个 IP 访问,哪怕是同一台机器上的另一个可用 IP 也不行。

11.4 127.0.0.1:本地环回

127.0.0.1 是 loopback(本地环回) 地址。

特性 说明
数据去向 不会被推送到网络
实际路径 在操作系统内部、经过协议栈转一圈,直接交给对端进程
前提 客户端和服务端必须跑在同一台机器上
典型用途 网络代码的本地测试

为什么测试要用它?

因为排障要分层。如果把报文直接扔进网络,一旦通信失败,可能是代码问题,也可能是网络问题。而走本地环回:

✔ 本地通了 → 说明代码是对的 ;这时跨机不通,问题一定在网络。

ifconfig 里能看到它:

复制代码
lo: flags=73<UP,LOOPBACK,RUNNING>  mtu 65536
    inet 127.0.0.1  netmask 255.0.0.0
    ...

11.5 ★ 推荐做法:INADDR_ANY

既然「公网 IP 绑不上」,「绑了哪个就只能用哪个」,那么服务端到底该绑什么?

✔ 结论:服务端不建议手动绑定任何特定 IP,而是绑定 INADDR_ANY。

c 复制代码
local.sin_addr.s_addr = INADDR_ANY;     /* 值就是 0 */
/* 也可以直接写 0 ------ 0 的大小端都是 0,不需要转换 */

两个理由:

序号 理由
① 公网 IP 本来就不允许绑
② 绑了特定 IP 就锁死了入口

第二条理由更本质。考虑一台服务器配了多张网卡、多个 IP 的情况:

情形 结果
机器有 IP1 / IP2 / IP3,客户端分别用三个 IP 访问同一个 8080 端口 三个报文都应该到达同一个服务
若服务端绑了 IP1 只能收到发往 IP1 的报文,另外两个全丢

✔ 服务端要的是「凡是发到这个端口的所有报文都收下」,而不是「只收某一个 IP 的报文」。

所以 INADDR_ANY 的语义是:

对内核说:从今往后,凡是访问本机的报文,只要目的端口是 8080,全部收下。

实测效果:netstat 显示 0.0.0.0:8080;客户端用 127.0.0.1、内网 IP、公网 IP 三种地址都能访问成功。

观察项 绑定具体 IP 绑定 INADDR_ANY
netstat 显示 172.18.45.153:8080 0.0.0.0:8080
可访问的地址 只有绑定的那一个 本机所有地址
是否推荐 ✖ ✔

11.6 由此简化出的构造函数

既然 IP 不需要用户指定,服务端构造时只需要一个端口号:

bash 复制代码
./udp_server 8080

对应 argc != 2 判断;服务端内部把 sin_addr.s_addr 直接写成 INADDR_ANY。

💡 另外,端口号不能取 0--1023(知名端口),在云服务器上绑定会失败 ------ 应使用 1024 之后的端口,常用 8080 做测试。


十二、把整个程序跑起来

12.1 编译

bash 复制代码
make            # 一次生成 udp_client 与 udp_server 两个可执行文件

12.2 启动

bash 复制代码
# 终端 1:服务端
./udp_server 8080

# 终端 2:客户端(同机测试,走本地环回)
./udp_client 127.0.0.1 8080

12.3 观察

位置 现象
服务端 netstat -naup 中看到 0.0.0.0:8080,PID 与进程名可见
客户端 提示输入 → 输入「你好」→ 收到 echo server: 你好
服务端日志 打印收到内容,以及来源地址(inet_ntoa 转换后的字符串)

12.4 让客户端能在任何机器上跑

把客户端编译成静态链接:

bash 复制代码
g++ udp_client.cc -o udp_client -static -std=c++17
方式 依赖 适用
动态链接(默认) 依赖目标机器上的第三方库 / libstdc++ 本机、同环境
静态链接(-static) 不依赖任何第三方库 发给别人、跨环境运行

十三、速查表

接口 作用 关键点
socket(AF_INET, SOCK_DGRAM, 0) 创建 UDP 套接字 返回 fd,本质是打开网络文件
inet_addr(str) 字符串 → 网络序 4 字节 一步完成两步转换
htons(p) 16 位主机序 → 网络序 端口号(2字节)用
bind(fd, addr, len) 绑定本地地址 必须强转成 sockaddr *;绑不上就起不来
recvfrom(...) 收数据 + 取对端地址 地址是出参 ;addrlen 是出入参
sendto(...) 发数据 + 指定目的地址 地址是入参
inet_ntoa(addr) 4 字节 → 字符串 打印地址用
相关推荐
AIyy8661 小时前
个人在线知识库怎么搭?从三种知识形态说起
1024程序员节
会飞的大可1 小时前
AI 新闻周报 | 2026年10月19日 — 10月24日
1024程序员节
艸肅1 小时前
Docker 国内镜像源配置
linux·ubuntu·docker·1024程序员节
CRMEB系统商城1 小时前
服装连锁的导购分销与企微私域
java·大数据·开发语言·人工智能·小程序·php·1024程序员节
Lzg_na1 小时前
STP / RSTP 协议学习:从环路保护到快速收敛
网络
拓恒技术1 小时前
城市管理中的无人机AI识别算法:架构、场景与落地
人工智能·算法·视觉检测·无人机·1024程序员节
成信吴彦祖1 小时前
当前专业的无人机RID侦测主站供应商主要有哪些
python·1024程序员节
玥轩_5211 小时前
鸿蒙远程控制软件推荐:鸿蒙手机平板远程控制电脑哪个好用?
1024程序员节