根据前文手写一个简单的UDP通信实例
IP 字符串 → 网络序数字
#include <arpa/inet.h>
int inet_pton(int af, const char *src, void *dst);
将人类可读的点分十进制 IP 字符串 (如 "192.168.1.100")转换为网络字节序(大端)的二进制整数 ,用于 Socket 编程中配置 sockaddr_in 等地址结构体。
1. 核心优点
- 跨平台:POSIX 标准,支持 Linux/BSD/macOS,替代老旧的
inet_addr; - 无坑:能正确处理
255.255.255.255(inet_addr会误判为失败); - 支持 IPv6与IPV4:仅需将地址族改为
AF_INET6,适配性强; - 线程安全:无静态缓冲区,结果写入用户指定的内存。
| 参数 | 含义 | 注意事项 |
|---|---|---|
af |
地址族:IPv4 填 AF_INET,IPv6 填 AF_INET6 |
必须与 src 的 IP 类型匹配 |
src |
输入:待转换的 IP 字符串(如 "192.168.1.100") |
格式错误会返回 0 |
dst |
输出:存储网络序数字的缓冲区 | IPv4 传 struct in_addr*,IPv6 传 struct in6_addr* |
2. 使用例子(IPv4 核心场景)
#include <stdio.h>
#include <arpa/inet.h>
#include <errno.h>
int main() {
const char *ip_str = "192.168.1.100"; // 待转换的IP字符串
struct in_addr ip_net; // 存储网络序结果的结构体
// 调用inet_pton转换
int ret = inet_pton(AF_INET, ip_str, &ip_net);
// 解析返回值
if (ret == 1) {
// 成功:打印网络序数字(十六进制更直观)
printf("IP字符串 %s → 网络序数字:0x%x\n", ip_str, ip_net.s_addr);
// 输出:IP字符串 192.168.1.100 → 网络序数字:0xc0a80164
} else if (ret == 0) {
printf("错误:IP字符串格式无效(%s)\n", ip_str);
} else { // ret == -1
perror("inet_pton 失败"); // 如af参数错误、内存错误等
return -1;
}
return 0;
}
3. 返回值说明
| 返回值 | 含义 | 处理方式 |
|---|---|---|
1 |
转换成功 | 直接使用 dst 中的网络序数字 |
0 |
IP 字符串格式无效(如 "192.168.1.300") |
检查 IP 字符串拼写 |
-1 |
函数调用失败(如 af 传错、dst 内存非法) |
用 perror() 查看具体错误(如 EAFNOSUPPORT 表示地址族不支持) |
网络序数字 → IP 字符串
#include <arpa/inet.h>
const char *inet_ntop(int af, const void *src, char *dst, socklen_t size);
将网络字节序(大端)的二进制整数 (如 0xc0a80164)转换为人类可读的点分十进制 IP 字符串,用于打印、日志输出或展示 IP 地址。
1. 核心优点
- 线程安全:需用户手动传入缓冲区,无静态缓冲区覆盖问题(对比老旧的
inet_ntoa); - 支持 IPv6:兼容 IPv4/IPv6,无需改逻辑;
- 防越界:通过缓冲区大小参数控制,避免内存溢出;
- 跨平台:POSIX 标准,替代线程不安全的
inet_ntoa。
| 参数 | 含义 | 注意事项 |
|---|---|---|
af |
地址族:IPv4 填 AF_INET,IPv6 填 AF_INET6 |
必须与 src 的 IP 类型匹配 |
src |
输入:网络序数字的缓冲区(IPv4 传 struct in_addr*) |
必须是网络序(大端),无需转主机序 |
dst |
输出:存储 IP 字符串的缓冲区 | 需提前分配内存 |
size |
缓冲区大小:IPv4 用 INET_ADDRSTRLEN(固定 16 字节),IPv6 用 INET6_ADDRSTRLEN(46 字节) |
过小会返回 NULL,触发 ENOSPC 错误 |
2. 使用例子
#include <stdio.h>
#include <arpa/inet.h>
#include <errno.h>
int main() {
// 待转换的网络序数字(对应 192.168.1.100)
struct in_addr ip_net;
ip_net.s_addr = 0xc0a80164;
// 分配缓冲区(IPv4 固定16字节,用宏更安全)
char ip_str[INET_ADDRSTRLEN];
// 调用inet_ntop转换
const char *ret = inet_ntop(AF_INET, &ip_net, ip_str, INET_ADDRSTRLEN);
// 解析返回值
if (ret != NULL) {
printf("网络序数字 0x%x → IP字符串:%s\n", ip_net.s_addr, ip_str);
// 输出:网络序数字 0xc0a80164 → IP字符串:192.168.1.100
} else {
perror("inet_ntop 失败"); // 如缓冲区过小、af参数错误
return -1;
}
return 0;
}
3. 返回值说明
| 返回值 | 含义 | 处理方式 |
|---|---|---|
| 非 NULL | 转换成功:返回 dst 缓冲区的地址(可直接打印) |
直接使用 dst 中的 IP 字符串 |
NULL |
转换失败 | 用 perror() 查看错误:① ENOSPC:缓冲区 size 过小;② EAFNOSUPPORT:af 参数错误;③ EINVAL:src 不是有效的网络序 IP |
UDP发数据sento
#include <sys/socket.h>
ssize_t sendto(
int sockfd, // ① UDP 的 socket 文件描述符(用 socket() 创建的)
const void *buf, // ② 要发送的数据缓冲区(比如存字符串的数组)
size_t len, // ③ 要发送的数据长度(字节数,比如 strlen("hello"))
int flags, // ④ 一般传 0(阻塞发送);非阻塞传 MSG_DONTWAIT
const struct sockaddr *dest_addr, // ⑤ 目标地址:对方的 IP+端口(结构体)
socklen_t addrlen // ⑥ 目标地址结构体的长度(sizeof(struct sockaddr_in))
);
参数讲解
|-----------|----------------------------------------------------------------|
| sockfd | 你创建的 UDP "通信端口"(比如 int fd = socket(AF_INET, SOCK_DGRAM, 0);) |
| buf | 要发的数据存在这里(比如 char msg[] = "你好 UDP";) |
| len | 数据有多长(比如 sizeof(msg) 或 strlen(msg)) |
| flags | 新手直接填 0 就行(阻塞模式,等数据发出去) |
| dest_addr | 告诉系统 "要发给谁":填对方的 IP 和端口(用 struct sockaddr_in 封装) |
| addrlen | 固定填 sizeof(struct sockaddr_in)(告诉系统地址结构体的大小) |
返回值讲解
|-----|-----------------------------------------------------------------------|
| >0 | 成功!返回实际发送的字节数(UDP 要么发完一个包,要么失败,不会只发一半) |
| -1 | 失败!常见原因:① errno=EAGAIN(非阻塞时发送缓冲区满);② errno=EBADF(sockfd 已关闭);③ 目标地址无效 |
| 0 | 成功发送0个字节(如下图) |

UDP接受数据recvfrom
#include <sys/socket.h>
// 返回值:成功=实际接收的字节数;失败=-1(无0返回!)
ssize_t recvfrom(
int sockfd, // ① UDP 的 socket 文件描述符
void *buf, // ② 接收数据的缓冲区(存收到的内容)
size_t len, // ③ 缓冲区最大长度(避免数据装不下)
int flags, // ④ 一般传 0(阻塞接收);非阻塞传 MSG_DONTWAIT
struct sockaddr *src_addr, // ⑤ 输出参数:存储"发送方"的 IP+端口
socklen_t *addrlen // ⑥ 输入输出参数:先填 sizeof(struct sockaddr_in),返回实际长度
);
参数讲解
|----------|---------------------------------------------|
| sockfd | 和 sendto 用同一个 UDP socketfd |
| buf | 收到的数据存在这里(比如 char buf[1024] = {0};) |
| len | 缓冲区能装多少字节(比如 sizeof(buf)-1,留 1 字节存字符串结束符) |
| flags | 新手填 0 就行(阻塞模式,等有数据才返回) |
| src_addr | 系统会把 "谁发的消息" 填到这里(比如知道对方的 IP 和端口) |
addrlen 是 值-结果 参数:
-
调用前 你必须先给
src_addr分配空间(struct sockaddr_in peer;)socklen_t addrlen = sizeof(peer); // 告诉内核:我准备的结构体有多大 -
调用时 内核把实际填写的字节数写回
addrlen(比如sizeof(struct sockaddr_in))。ssize_t n = recvfrom(sockfd, buf, len, flags, (struct sockaddr *)&peer, &addrlen); -
调用后
-
peer里拿到发送方 IP+端口 -
addrlen变成已用长度,后续可原样传给sendto回包,或仅做日志。
-
"先给大小,后得大小"------这就是值-结果参数的用法。
| 返回值 | TCP 调用改函数 | UDP 调用该函数 |
|---|---|---|
| >0 | 收到 N 字节数据 | 收到 N 字节数据 |
| 0 | 对端优雅关闭(FIN) | 收到空数据报 |
| -1 | 出错,查 errno | 出错,查 errno |
注意!!!写端关闭,不会导致UDP的读端返回0,如果是阻塞模式他会卡住。

错误码
| 错误码 | 触发场景 | 处理策略 |
|---|---|---|
| EAGAIN / EWOULDBLOCK | 非阻塞 socket 缓冲区空(读)或满(写) | 立即返回 ,应用稍后再次 recv/send(轮询/epoll 等待就绪) |
| EINTR | 信号中断了系统调用,没数据也没错 | 重新调用 同一函数(通常包在 do{...}while(...) 里) |
| 其他 | 发生错误 | 应当打印日志然后直接退出 |
代码示例
server.cc
//socket编程
#include<unistd.h>
#include<sys/socket.h>
#include<arpa/inet.h>
#include<iostream>
using namespace std;
//服务端
int main(){
//ip 127.0.0.1 端口8080 开启地址重用
int fd=socket(AF_INET,SOCK_DGRAM,0);//ip4 ,udp
if(fd<0){
cout<<"socket faild"<<endl;
}
struct sockaddr_in addr;
addr.sin_family=AF_INET;
addr.sin_port=htons(8080);
inet_pton(AF_INET,"127.0.0.1",&addr.sin_addr);
int ret=bind(fd,(sockaddr*)&addr,sizeof addr);
if(ret<0)
cout<<"bind faild"<<endl;
int i=0;
while(true){
i++;
sleep(1);
char buff[1024];
sprintf(buff,"%s :%d","hello client",i);
struct sockaddr_in addr1;
addr1.sin_family=AF_INET;
addr1.sin_port=htons(8081);
inet_pton(AF_INET,"127.0.0.1",&addr1.sin_addr);
socklen_t len=sizeof addr1;
int n= sendto(fd,buff,sizeof buff,0,(sockaddr*)&addr1,len);
cout<<"send over:n"<<n<<endl;
size_t n=recvfrom(fd,buff,sizeof buff,0,(sockaddr*)&addr1,&len);
cout<<"client say:"<<buff<<endl;
}
}
client.cc
//socket编程
#include<sys/socket.h>
#include<arpa/inet.h>
#include<iostream>
#include <unistd.h>
using namespace std;
//服务端
int main(){
//ip 127.0.0.1 端口8080 开启地址重用
int fd=socket(AF_INET,SOCK_DGRAM,0);//ip4 ,udp
if(fd<0){
cout<<"socket faild"<<endl;
}
struct sockaddr_in addr;
addr.sin_family=AF_INET;
addr.sin_port=htons(8081);
inet_pton(AF_INET,"127.0.0.1",&addr.sin_addr);
int ret=bind(fd,(sockaddr*)&addr,sizeof addr);
if(ret<0)
cout<<"bind faild"<<endl;
int i=0;
while(true){
i++;
sleep(1);
char buff[1024];
struct sockaddr_in addr1;
socklen_t len=sizeof addr1;
size_t n=recvfrom(fd,buff,sizeof buff,0,(sockaddr*)&addr1,&len);
cout<<"server say:"<<buff<<endl;
sprintf(buff,"%s :%d","hello server",i);
cout<<"n:"<<n<<endl;
sendto(fd,buff,sizeof buff,0,(sockaddr*)&addr1,len);
}
}
此份代码必须先启动客户端在启动服务端否则会卡死,为什么?