@bit::Shadow
✧(≖ ◡ ≖✿
目录
[一、总览:四个函数在 UDP 通信中的分工](#一、总览:四个函数在 UDP 通信中的分工)
[1. socket() ------ 创建通信端点](#1. socket() —— 创建通信端点)
[2. bind() ------ 绑定地址和端口](#2. bind() —— 绑定地址和端口)
[3. recvfrom() ------ 接收数据并获取发送方](#3. recvfrom() —— 接收数据并获取发送方)
[4. sendto() ------ 发送数据到指定地址](#4. sendto() —— 发送数据到指定地址)
gitee服务端、客户端(待链接)🔗
四大接口简解(基于Udp的实现)
套接字socket()创建的含义
向内核申请一个"网络通信端点",返回一个文件描述符(fd),后续所有网络操作都通过它来标识这个通信通道。
进行bind()的含义
把套接字与一个本地 IP 地址和端口号关联起来,告诉内核"这个套接字负责接收发往这个 IP:端口 的数据"。
recvfrom的含义
从套接字的接收缓冲区中取出一个 UDP 数据报,同时把发送方的 IP 地址和端口号返回给调用者。
sendto()的含义
把数据封装成 UDP 数据报,发送到指定的目标 IP 地址和端口。
四大接口详解
这个问题问得非常系统,相当于把 UDP 通信的完整生命周期 串起来了。我结合你之前的代码和实际运行过程,把 socket()、bind()、recvfrom()、sendto() 这四个核心系统调用,用**"寄快递"** 的比喻 + 技术原理解析,一次性讲透它们的含义和关系。
一、总览:四个函数在 UDP 通信中的分工
| 函数 | 一句话含义 | 快递类比 | 调用方 |
|---|---|---|---|
socket() |
创建通信端点(获得一个"信筒") | 向邮局申请一个专属信箱 | 服务端和客户端都需要 |
bind() |
绑定地址和端口(给信筒贴上门牌号) | 在信箱上写上地址和收件人 | 服务端必须,客户端可选 |
recvfrom() |
接收数据 + 获取发送方地址(收信并看寄件人) | 从信箱取信,同时记住寄件人地址 | 服务端/客户端都可能用 |
sendto() |
发送数据到指定地址(寄信) | 把信寄给指定地址的收件人 | 服务端/客户端都可能用 |
二、逐个深入解析
1. socket() ------ 创建通信端点
含义:
向内核申请一个"网络通信端点",返回一个文件描述符(fd),后续所有网络操作都通过它来标识这个通信通道。
技术本质:
-
内核创建
struct socket和struct sock对象。 -
分配发送/接收缓冲区。
-
关联协议处理函数(如 UDP 的
udp_sendmsg/udp_recvmsg)。 -
返回一个非负整数 fd。
在你代码中的位置:
cpp
// UdpServer 的 Init() 方法内部(推测)
_sockfd = socket(AF_INET, SOCK_DGRAM, 0);
// 此时 _sockfd 是一个有效的套接字,但还没有绑定地址
快递类比:
你去邮局租了一个信箱 (套接字),得到一个信箱编号 (fd)。现在你有了收发信的能力,但还没有告诉邮局你的地址(
bind),所以别人还无法给你寄信。
关键点:
-
socket()本身不涉及任何网络通信,只是创建了一个内核对象。 -
UDP 和 TCP 的
socket()调用方式不同(SOCK_DGRAMvsSOCK_STREAM)。 -
socket()成功后,fd 是"未绑定、未连接"的,需要后续操作赋予它具体功能。
2. bind() ------ 绑定地址和端口
含义:
把套接字与一个本地 IP 地址和端口号关联起来,告诉内核"这个套接字负责接收发往这个 IP:端口 的数据"。
技术本质:
-
将
struct sockaddr_in(IPv4 地址 + 端口)与套接字绑定。 -
内核在 UDP 端口表中记录这个映射关系。
-
当数据包到达该 IP:端口 时,内核能快速找到对应的套接字,将数据放入其接收缓冲区。
在你代码中的位置:
cpp
// UdpServer 的 Init() 方法内部(推测)
struct sockaddr_in addr;
addr.sin_family = AF_INET;
addr.sin_addr.s_addr = INADDR_ANY; // 0.0.0.0,监听所有网卡
addr.sin_port = htons(port);
if (bind(_sockfd, (struct sockaddr*)&addr, sizeof(addr)) < 0) {
// 错误处理
}
快递类比:
你在信箱上贴了一个地址标签(比如"北京市朝阳区XX路18号 8888房间")。这样邮递员(内核)才知道,发往这个地址的信应该投递到你的信箱里。
关键点:
-
服务端必须
bind(),否则客户端不知道往哪个地址发数据。 -
客户端可以不用
bind(),内核会自动分配一个临时端口(49152-65535)。 -
INADDR_ANY(0.0.0.0)表示监听本机所有网卡的该端口。 -
端口号需要用
htons()转换字节序(大端 → 网络字节序)。
cpp
ssize_t sendto(int sockfd, const void *buf, size_t len, int flags,
const struct sockaddr *dest_addr, socklen_t addrlen);
ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags,
struct sockaddr *src_addr, socklen_t *addrlen);
3. recvfrom() ------ 接收数据并获取发送方
含义:
从套接字的接收缓冲区中取出一个 UDP 数据报,同时把发送方的 IP 地址和端口号返回给调用者。
技术本质:
-
阻塞(或非阻塞)等待数据到达。
-
从内核的接收缓冲区拷贝数据到用户空间的
buf。 -
同时将发送方的地址结构(struct sockaddr_in)填充到
src_addr参数中。 -
UDP 是数据报模式 :每次
recvfrom返回一个完整数据报。
在你代码中的位置:
cpp
// 你之前问过的 recvfrom
struct sockaddr_in client_addr;
socklen_t addr_len = sizeof(client_addr);
ssize_t n = recvfrom(_sockfd, buffer, sizeof(buffer) - 1, 0,
(struct sockaddr*)&client_addr, &addr_len);
if (n > 0) {
// 此时 client_addr 中保存了发送方的 IP 和端口
// 可以用 sendto 回复这个地址
}
快递类比:
你从信箱里取出一封信(数据),信上盖着寄件人的地址戳 (
src_addr)。你不仅读到了信的内容,还知道了是谁寄来的------这样你就可以回信了。
关键点:
-
这是 UDP 服务端获取客户端地址的唯一途径(因为 UDP 无连接)。
-
addr_len是输入输出参数:调用前传入缓冲区大小,调用后内核回填实际地址长度。 -
如果
buf小于数据报大小,剩余数据会被丢弃(UDP 的特有坑)。
4. sendto() ------ 发送数据到指定地址
含义:
把数据封装成 UDP 数据报,发送到指定的目标 IP 地址和端口。
技术本质:
-
将用户空间的
buf拷贝到内核。 -
封装 UDP 头(源端口、目标端口、长度、校验和)。
-
交给 IP 层(加上 IP 头),然后通过路由表决定从哪个网卡发出。
-
UDP 是不可靠 的:
sendto()返回成功只表示数据已交给 IP 层,不保证对端已收到。
在你代码中的位置:
cpp
// 你之前用过的 sendto
ssize_t ret2 = sendto(_sockfd, buf, sizeof(buf) - 1, 0,
(const sockaddr *)&remote, sizeof(remote));
// ^^^^^^^^^^
// 注意:是 remote 的大小,不是 socklen_t 类型的大小
快递类比:
你写好了信,贴上邮票,然后根据寄件人地址(
dest_addr)把信投递出去 。你投进邮筒(sendto)后,只表示信已经进了邮局系统,但不保证对方一定收到(UDP 的不可靠性)。
关键点:
-
目标地址必须正确 (IP + 端口),否则会返回
ENETUNREACH或EHOSTUNREACH。 -
数据长度不能超过 65507 字节(UDP 最大载荷),否则返回
EMSGSIZE。 -
dest_addr可以是:-
客户端发数据给服务器时:服务器地址
-
服务器回复客户端时:从
recvfrom获取的client_addr
-
三、一个完整的交互流程(串起来)
假设有一个 UDP 服务器(IP 192.168.1.100,端口 8888)和一个客户端(IP 192.168.1.10,临时端口 52345):
cpp
【服务端】 【客户端】
1. socket() → 创建套接字 fd 1. socket() → 创建套接字 fd
(获得一个信箱) (也获得一个信箱)
2. bind() → 绑定 INADDR_ANY:8888 (客户端不需要 bind)
(贴上地址标签:"8888 房间") (内核自动分配临时端口 52345)
3. recvfrom() → 阻塞等待数据 3. sendto() → 发送数据到 192.168.1.100:8888
(坐在信箱前等信) (把信寄给服务端地址)
4. recvfrom() 返回,收到数据, (客户端继续)
同时获取到客户端地址(192.168.1.10:52345)
(取信并看到寄件人地址)
5. sendto() → 回复数据到客户端地址 5. recvfrom() → 接收服务端回复
(回信给寄件人) (从自己的信箱取回信)
☆☆客户端、服务端通信的特异性的确定
./udpClient IP port 运行可执行程序
需不需要显示bind?看似需要实则相反!
☆☆☆要回答此问题必须明确前提:bind实现通信的原理 {IP,port,目标IP,目标port} 只要两组这四元组不完全相同,UDP通信就完全没有问题。
IP:服务端默认接受所有IP请求。为区分不同的客户端所以每个"客户"必须保证不同IP(若有两个网卡,即一台机器有了两个IP,可被客户端区分,允许分别请求)。
port:服务端需要指定端口号。客户端链接指定的服务端端口号,客户端的port由系统自动生成。(recvfrom 、 sendto时自动调用connect()分配)
四、核心要点总结(面试/考试常考)
| 函数 | 服务端是否必须 | 客户端是否必须 | 核心作用 |
|---|---|---|---|
socket() |
✅ 是 | ✅ 是 | 创建套接字端点 |
bind() |
✅ 必须(固定端口) | ❌ 可选(不调用则内核分配临时端口) | 绑定本地地址和端口 |
recvfrom() |
✅ 必须(接收请求) | ❌ 可选(看是否需要接收回复) | 接收数据报 + 获取发送方地址 |
sendto() |
✅ 必须(回复客户端) | ✅ 必须(发送请求) | 向指定地址发送数据报 |
感谢支持,长期连载
欢迎关注
