UDP协议
UDP简介
核心定义
UDP(User Datagram Protocol)即用户数据报协议,是面向无连接的传输层协议,因无需连接建立、确认机制,被称为“不可靠协议”(无法确保接收方是否收到数据)。
核心特征
- 无连接:通信前无需建立连接,资源占用少
- 无确认:接收数据后不向发送方返回回执
- 无序性:不保证数据传输顺序,丢失不重发
- 帧同步:通信双方每次收发数据量相等(数据报形式)
- 高效率:无需缓冲区、分包序列号,传输速度快
适用场景
- 小尺寸数据传输(如DNS查询、NTP时间同步)
- 无线网络等难应答场景
- 广播、组播模式(如局域网设备发现)
- 实时数据传输(即时通讯文本/音视频、流媒体、VoIP、IPTV)
类比:UDP类似“寄信”,寄件人无法确认收件人是否签收,但流程简单、效率高。
UDP通信流程
整体流程框架
| 角色 | 核心步骤 |
|---|---|
| 发送端(客户端) | 1. 创建UDP套接字 → 2. 设置接收端地址 → 3. 发送数据 → 4. 关闭套接字 |
| 接收端(服务器) | 1. 创建UDP套接字 → 2. 绑定本地地址 → 3. 接收数据 → 4. 关闭套接字 |
发送端详细步骤
- 创建UDP通信对象:通过
socket()函数创建套接字,指定协议族和类型 - 设置目标地址:定义
struct sockaddr_in结构体,填写接收端IP和端口(需字节序转换) - 发送数据报:通过
sendto()函数发送数据,指定目标地址和地址长度 - 关闭资源:通过
close()关闭套接字
接收端详细步骤
- 创建UDP通信对象:与发送端一致,使用
socket()函数 - 绑定本地地址:通过
bind()函数将套接字与本地IP、端口绑定(对外公布联系方式) - 接收数据报:通过
recvfrom()函数阻塞等待数据,可获取发送方地址(可选) - 关闭资源:通过
close()关闭套接字
注意:接收端必须绑定地址,否则无法被发送端定位;发送端可无需绑定(系统自动分配临时端口)。
关键API接口
套接字创建(socket)
#include <sys/socket.h>
int socket(int domain, int type, int protocol);
- 参数说明:
domain:通信协议族(AF_INET=IPv4,AF_UNIX=本地通信)type:套接字类型(SOCK_DGRAM=UDP,SOCK_STREAM=TCP)protocol:协议控制(默认0,自动匹配前两个参数)
- 返回值:成功返回文件描述符,失败返回-1
数据发送(sendto)
#include <sys/socket.h>
ssize_t sendto(int sockfd, const void *buf, size_t len, int flags,
const struct sockaddr *dest_addr, socklen_t addrlen);
- 参数说明:
sockfd:UDP套接字文件描述符buf:待发送数据的缓冲区地址len:待发送数据的实际长度flags:发送标记(默认0,无特殊行为)dest_addr:目标地址结构体(接收端IP+端口)addrlen:目标地址结构体长度(sizeof(dest_addr))
- 返回值:成功返回发送字节数,失败返回-1
数据接收(recvfrom)
#include <sys/socket.h>
ssize_t recvfrom(int sockfd, void *restrict buffer, size_t length,
int flags, struct sockaddr *restrict address,
socklen_t *restrict address_len);
- 参数说明:
sockfd:UDP套接字文件描述符buffer:存储接收数据的缓冲区length:缓冲区最大容量flags:接收标记(默认0)address:发送方地址结构体(可选,NULL表示不获取)address_len:发送方地址长度(可选,NULL表示不获取)
- 返回值:成功返回接收字节数,失败返回-1
- 特性:无数据时会阻塞等待
地址绑定(bind)
#include <sys/socket.h>
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
- 参数说明:
sockfd:需要绑定的套接字addr:本地地址结构体(IP+端口)addrlen:地址结构体长度
- 返回值:成功返回0,失败返回-1
地址结构体(sockaddr_in)
#include <netinet/in.h>
struct sockaddr_in {
sa_family_t sin_family; // 协议族(AF_INET)
in_port_t sin_port; // 端口号(网络字节序)
struct in_addr sin_addr; // IP地址结构体
};
struct in_addr {
uint32_t s_addr; // IP地址(网络字节序)
};
字节序与IP转换函数
#include <arpa/inet.h>
// 字符串IP → 网络字节序IP
in_addr_t inet_addr(const char *cp); // 如"192.168.9.1" → 网络字节序
int inet_aton(const char *cp, struct in_addr *inp);
// 网络字节序IP → 字符串IP
char *inet_ntoa(struct in_addr in); // 如网络字节序 → "192.168.9.1"
// 主机字节序 ↔ 网络字节序(端口/IP转换)
uint16_t htons(uint16_t hostshort); // 主机→网络(端口,16位)
uint32_t htonl(uint32_t hostlong); // 主机→网络(IP,32位)
uint16_t ntohs(uint16_t netshort); // 网络→主机(端口)
uint32_t ntohl(uint32_t netlong); // 网络→主机(IP)
基础示例代码
UDP发送端(客户端)
#include <stdio.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <string.h>
#include <unistd.h>
int main() {
// 1. 创建UDP对象
int udp_socket = socket(AF_INET, SOCK_DGRAM, 0);
if (udp_socket < 0) {
perror("udp socket error");
return -1;
}
perror("udp socket");
// 2. 设置接收端地址
struct sockaddr_in dest_addr;
dest_addr.sin_family = AF_INET;
dest_addr.sin_port = htons(8888); // 目标端口(网络字节序)
dest_addr.sin_addr.s_addr = inet_addr("192.168.9.1"); // 目标IP
// 3. 循环发送数据
char send_buf[128] = {0};
while (1) {
bzero(send_buf, sizeof(send_buf));
fgets(send_buf, sizeof(send_buf), stdin); // 从键盘读取数据
int ret = sendto(udp_socket, send_buf, strlen(send_buf), 0,
(struct sockaddr *)&dest_addr, sizeof(dest_addr));
if (ret > 0) {
printf("成功发送%d字节数据\n", ret);
}
}
// 4. 关闭套接字(实际不会执行,需手动终止)
close(udp_socket);
return 0;
}
UDP接收端(服务器)
#include <stdio.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <string.h>
#include <unistd.h>
int main() {
// 1. 创建UDP对象
int udp_socket = socket(AF_INET, SOCK_DGRAM, 0);
if (udp_socket < 0) {
perror("udp socket error");
return -1;
}
perror("udp socket");
// 2. 设置本地地址并绑定
struct sockaddr_in my_addr;
my_addr.sin_family = AF_INET;
my_addr.sin_port = htons(8888); // 本地端口
my_addr.sin_addr.s_addr = inet_addr("192.168.9.1"); // 本地IP(0.0.0.0 =INADDR_ANY)换作0可以接收所有人的信息
if (bind(udp_socket, (struct sockaddr *)&my_addr, sizeof(my_addr)) < 0) {
perror("bind error");
return -1;
}
perror("bind");
// 3. 循环接收数据
char recv_buf[1024] = {0};
struct sockaddr_in send_addr; // 存储发送方地址
socklen_t addr_len = sizeof(send_addr);
while (1) {
bzero(recv_buf, sizeof(recv_buf));
int ret = recvfrom(udp_socket, recv_buf, sizeof(recv_buf), 0,
(struct sockaddr *)&send_addr, &addr_len);
if (ret > 0) {
// 转换发送方IP和端口为本地字节序
char *send_ip = inet_ntoa(send_addr.sin_addr);
int send_port = ntohs(send_addr.sin_port);
printf("[%s:%d] 接收%d字节:%s\n", send_ip, send_port, ret, recv_buf);
}
}
// 4. 关闭套接字
close(udp_socket);
return 0;
}
UDP双向通信实现
// 接收端(同时支持发送响应)
#include <stdio.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <string.h>
#include <unistd.h>
int main() {
int udp_socket = socket(AF_INET, SOCK_DGRAM, 0);
if (udp_socket < 0) { perror("socket error"); return -1; }
struct sockaddr_in my_addr = {AF_INET, htons(8888), inet_addr("192.168.9.1")};
if (bind(udp_socket, (struct sockaddr *)&my_addr, sizeof(my_addr)) < 0) {
perror("bind error"); return -1;
}
char recv_buf[1024] = {0}, send_buf[1024] = {0};
struct sockaddr_in client_addr;
socklen_t addr_len = sizeof(client_addr);
while (1) {
// 接收客户端数据
bzero(recv_buf, sizeof(recv_buf));
int recv_len = recvfrom(udp_socket, recv_buf, sizeof(recv_buf), 0,
(struct sockaddr *)&client_addr, &addr_len);
if (recv_len <= 0) continue;
printf("客户端[%s:%d]:%s", inet_ntoa(client_addr.sin_addr),
ntohs(client_addr.sin_port), recv_buf);
// 发送响应数据
bzero(send_buf, sizeof(send_buf));
printf("请输入响应:");
fgets(send_buf, sizeof(send_buf), stdin);
sendto(udp_socket, send_buf, strlen(send_buf), 0,
(struct sockaddr *)&client_addr, addr_len);
}
close(udp_socket);
return 0;
}
UDP高级功能
广播功能
广播定义
向当前网络中所有主机发送数据,只需往网络的广播地址发送UDP数据包(如192.168.9.255)。
广播实现步骤
- 创建UDP套接字(同基础流程)
- 开启广播功能(通过
setsockopt()设置SO_BROADCAST属性) - 设置广播地址(目标IP为网络广播地址,端口统一)
- 发送广播数据(使用
sendto())
广播示例代码
#include <stdio.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <string.h>
#include <unistd.h>
int main() {
// 1. 创建UDP对象
int udp_socket = socket(AF_INET, SOCK_DGRAM, 0);
if (udp_socket < 0) { perror("socket error"); return -1; }
// 2. 开启广播功能
int broadcast_flag = 1;
if (setsockopt(udp_socket, SOL_SOCKET, SO_BROADCAST,
&broadcast_flag, sizeof(broadcast_flag)) < 0) {
perror("开启广播失败");
close(udp_socket);
return -1;
}
printf("开启广播成功\n");
// 3. 设置广播地址
struct sockaddr_in broadcast_addr;
broadcast_addr.sin_family = AF_INET;
broadcast_addr.sin_port = htons(8888);
broadcast_addr.sin_addr.s_addr = inet_addr("192.168.9.255"); // 广播地址
// 4. 发送广播数据
char *msg = "Hello, 所有主机!";
int ret = sendto(udp_socket, msg, strlen(msg), 0,
(struct sockaddr *)&broadcast_addr, sizeof(broadcast_addr));
if (ret > 0) {
printf("广播发送成功,字节数:%d\n", ret);
} else {
perror("广播发送失败");
}
close(udp_socket);
return 0;
}
网络属性设置(setsockopt/getsockopt)
常用属性列表
| 属性层次 | 属性名称 | 数据格式 | 说明 |
|---|---|---|---|
| SOL_SOCKET | SO_DEBUG | bool | 开启网络调试信息 |
| SOL_SOCKET | SO_REUSEADDR | bool | 关闭后立即重用本地地址和端口 |
| SOL_SOCKET | SO_BROADCAST | bool | 开启广播功能 |
| SOL_SOCKET | SO_RCVBUF | int | 接收缓冲区大小 |
| SOL_SOCKET | SO_SNDBUF | int | 发送缓冲区大小 |
| SOL_SOCKET | SO_RCVTIMEO | struct timeval | 接收数据超时时间 |
| SOL_SOCKET | SO_SNDTIMEO | struct timeval | 发送数据超时时间 |
| IPPROTO_IP | IP_TTL | int | 数据包生存时间(跳数限制) |
缓冲区大小设置
#include <sys/socket.h>
#include <stdio.h>
int main() {
int sockfd = socket(AF_INET, SOCK_DGRAM, 0);
if (sockfd < 0) { perror("socket error"); return -1; }
// 获取默认缓冲区大小
int sndbuf, rcvbuf;
socklen_t len = sizeof(sndbuf);
getsockopt(sockfd, SOL_SOCKET, SO_SNDBUF, &sndbuf, &len);
getsockopt(sockfd, SOL_SOCKET, SO_RCVBUF, &rcvbuf, &len);
printf("默认发送缓冲区:%d,接收缓冲区:%d\n", sndbuf, rcvbuf);
// 设置缓冲区大小
sndbuf = 2048;
rcvbuf = 4096;
setsockopt(sockfd, SOL_SOCKET, SO_SNDBUF, &sndbuf, sizeof(sndbuf));
setsockopt(sockfd, SOL_SOCKET, SO_RCVBUF, &rcvbuf, sizeof(rcvbuf));
// 验证设置结果
getsockopt(sockfd, SOL_SOCKET, SO_SNDBUF, &sndbuf, &len);
getsockopt(sockfd, SOL_SOCKET, SO_RCVBUF, &rcvbuf, &len);
printf("设置后发送缓冲区:%d,接收缓冲区:%d\n", sndbuf, rcvbuf);
close(sockfd);
return 0;
}
接收超时设置
#include <sys/socket.h>
#include <netinet/in.h>
#include <stdio.h>
#include <unistd.h>
#include <sys/time.h>
int main() {
int udp_socket = socket(AF_INET, SOCK_DGRAM, 0);
if (udp_socket < 0) { perror("socket error"); return -1; }
// 设置接收超时为5秒
struct timeval tv;
tv.tv_sec = 5; // 秒
tv.tv_usec = 0; // 微秒
if (setsockopt(udp_socket, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof(tv)) < 0) {
perror("设置超时失败");
close(udp_socket);
return -1;
}
// 绑定地址(省略,同接收端基础代码)
// ...
// 接收数据(5秒内无数据则返回错误)
char buf[1024] = {0};
struct sockaddr_in send_addr;
socklen_t addr_len = sizeof(send_addr);
int ret = recvfrom(udp_socket, buf, sizeof(buf), 0,
(struct sockaddr *)&send_addr, &addr_len);
if (ret < 0) {
perror("接收超时或失败");
close(udp_socket);
return -1;
}
printf("接收数据:%s\n", buf);
close(udp_socket);
return 0;
}
地址端口复用(解决Address already in use)
#include <sys/socket.h>
#include <stdio.h>
int main() {
int sockfd = socket(AF_INET, SOCK_DGRAM, 0);
if (sockfd < 0) { perror("socket error"); return -1; }
// 开启地址端口复用
int reuse_flag = 1;
if (setsockopt(sockfd, SOL_SOCKET, SO_REUSEADDR,
&reuse_flag, sizeof(reuse_flag)) < 0) {
perror("开启复用失败");
close(sockfd);
return -1;
}
// 绑定地址(即使端口被TIME_WAIT占用也能绑定成功)
// ...
close(sockfd);
return 0;
}
数据校验
网络检索接口
常用检索API列表
| 序号 | API名称 | 功能描述 |
|---|---|---|
| 1 | gethostname() | 获取本地主机名 |
| 2 | getsockname() | 获取本端套接字绑定的地址信息 |
| 3 | getpeername() | 获取对端套接字的地址信息(仅TCP连接) |
| 4 | gethostbyname() | 根据域名获取主机IP地址(如www.baidu.com) |
| 5 | gethostbyaddr() | 根据IP地址获取主机信息 |
| 6 | getprotobyname() | 根据协议名获取协议号(如"udp"→17) |
| 7 | getservbyname() | 根据服务名获取端口号(如"http"→80) |
示例:根据域名获取IP(gethostbyname)
#include <stdio.h>
#include <netdb.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
int main() {
// 获取百度域名的IP地址
struct hostent *host_info = gethostbyname("www.baidu.com");
if (host_info == NULL) { perror("获取主机信息失败"); return -1; }
// 遍历所有IP地址(一个域名可能对应多个IP)
printf("www.baidu.com 的IP地址:\n");
for (int i = 0; host_info->h_addr_list[i] != NULL; i++) {
struct in_addr *ip_addr = (struct in_addr *)host_info->h_addr_list[i];
printf("%s\n", inet_ntoa(*ip_addr));
}
return 0;
}
示例:获取本端套接字地址(getsockname)
#include <stdio.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
int main() {
int udp_socket = socket(AF_INET, SOCK_DGRAM, 0);
if (udp_socket < 0) { perror("socket error"); return -1; }
// 绑定本地地址
struct sockaddr_in my_addr = {AF_INET, htons(8888), inet_addr("192.168.9.1")};
bind(udp_socket, (struct sockaddr *)&my_addr, sizeof(my_addr));
// 获取绑定的地址信息
struct sockaddr_in bound_addr;
socklen_t addr_len = sizeof(bound_addr);
getsockname(udp_socket, (struct sockaddr *)&bound_addr, &addr_len);
printf("本端绑定IP:%s\n", inet_ntoa(bound_addr.sin_addr));
printf("本端绑定端口:%d\n", ntohs(bound_addr.sin_port));
close(udp_socket);
return 0;
}
UDP与TCP对比
| 特性 | UDP | TCP |
|---|---|---|
| 连接方式 | 无连接(无需建立/释放连接) | 面向连接(三次握手建立,四次挥手释放) |
| 可靠性 | 不可靠(无确认、无重传) | 可靠(确认、重传、有序、流量控制) |
| 数据传输方式 | 数据报(帧同步,大小固定) | 字节流(无边界,需手动分包) |
| 效率 | 高(资源占用少,无额外开销) | 低(连接管理、确认机制开销大) |
| 适用场景 | 实时数据、广播、小数据传输 | 文件传输、HTTP通信、可靠数据传输 |
| 头部开销 | 小(8字节) | 大(20字节基础头部) |
| 阻塞特性 | 仅recvfrom()阻塞(无数据时) | 连接建立、recv()、send()均可能阻塞 |
远程通信与SSH配置(阿里云服务器)
服务器准备步骤
- 注册阿里云账号,购买/免费试用ECS云服务器
- 选择操作系统(推荐Ubuntu 22.04、CentOS 7.9)
- 记录服务器公网IP(如8.138.175.40)和登录账号密码
SSH远程连接(VS Code)
- 安装VS Code插件“Remote - SSH”
- 点击插件图标 → “Connect to Host” → 输入
ssh 用户名@公网IP(如ssh ubuntu@8.138.175.40) - 输入服务器密码,连接成功后即可远程编辑代码、编译运行
安全组配置(开放端口)
- 登录阿里云ECS控制台 → 网络与安全 → 安全组
- 手动添加规则:
- 协议类型:自定义UDP/TCP
- 端口范围:1/65535(或指定端口如8888)
- 授权对象:0.0.0.0/0(允许所有IP访问,生产环境需限制)
- 保存规则,确保UDP/TCP数据能通过服务器端口传输
远程UDP通信测试
- 服务器端:运行UDP接收端代码(绑定公网IP或0.0.0.0,端口8888)
- 本地端:运行UDP发送端代码(目标IP为服务器公网IP,端口8888)
- 验证:本地发送数据,服务器能接收则说明配置成功
UDP常见问题排查
绑定失败(Address already in use)
- 原因:端口被占用(如程序异常退出后端口处于TIME_WAIT状态)
- 解决:开启SO_REUSEADDR属性(地址端口复用),或更换端口
数据发送成功但接收不到
- 原因1:目标IP/端口错误(确认接收端绑定地址正确)
- 原因2:防火墙/安全组未开放端口(检查服务器安全组、本地防火墙)
- 原因3:广播数据未开启SO_BROADCAST属性(广播场景)
- 解决:逐一排查IP、端口、防火墙配置,确认广播功能开启
数据丢失
- 原因1:缓冲区溢出(接收端缓冲区太小,未及时读取数据)
- 原因2:网络拥堵(UDP无流量控制,数据包被丢弃)
- 原因3:数据报超过MTU大小(默认1500字节,超过会分片丢失)
- 解决:增大接收缓冲区、控制发送速率、拆分大数据报
接收端阻塞无响应
- 原因1:未开启超时设置(recvfrom()一直阻塞)
- 原因2:发送端未发送数据或发送失败
- 解决:设置SO_RCVTIMEO超时属性,检查发送端代码和网络连通性

浙公网安备 33010602011771号