UDP也叫用户数据报协议,位于传输层,是无连接、不可靠、面向数据的协议。
UDP通信前不需要建立连接,发送端直接打包数据发送,接收端被动接受,不保证数据一定送达、有序、不重复。
特点
- 无连接:不需要像TCP一样三次握手连接,四次挥手断开,发送前不需要和对方协商,拿到对方IP+端口就直接发,霸道总裁强制爱,拿到你的地址和电话就给你送东西过去了
- 面向数据报:每次发送的是独立完整数据,不会拆分/合并;一次sendto对应一个完整的UDP包;接收方recvfrom时必须读完一整个包,读不完剩下的数据就直接丢了,不会缓存
- 不可靠传输:不确认、不重传、不排序、无流量/拥塞控制;可能会丢包、乱序、重复接收
- 首部开销小:UDP首部固定8字节,结构简单,转发效率高
- 支持一对多通信:原生支持广播、组播,TCP做不到
- 全双工:这个和TCP一样,双方都可以收发数据
UDP报文头部
共四个字段,每个字段2字节:
- 源端口:发送方端口
- 目的端口:接收方端口
- 数据报长度:整个UDP包(首部+数据)总长度
- 校验和:简单校验是否损坏,出错直接丢弃
UDP通信流程
服务端(接收方)
- 创建UDP socket套接字------socket(AF_INET,SOCK_DGRAM,0)
- 绑定IP+端口------bind()
- 循环调用recvfrom阻塞等待接收数据------recvfrom()
- 处理数据,可选sendto回显数据
客户端(发送方)
- 创建UDP socket套接字------socket(AF_INET,SOCK_DGRAM,0)
- 直接调用sendto,指定服务端IP+端口号发送数据
- 可选用recvfrom接收回复
流程图如下:

涉及函数讲解
1.recvfrom函数
函数原型:
ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags,struct sockaddr *src_addr, socklen_t *addrlen);
参数:
- sockfd:表示客户端的socket套接字
- buf:要接受的数据首地址
- len:可接受的数据最大长度
- flags:控制选项(如MSG_DONTWAIT非阻塞,MSG_PEEK窥视数据,0阻塞)
- src_addr:源地址,获取发送发信息
- addrlen:地址长度
返回值:
- 成功:返回接收的字节数
- 失败:-1(重置错误码)
2.sendto函数
函数原型:
ssize_t sendto(int sockfd, const void *buf, size_t len, int flags,const struct sockaddr *dest_addr, socklen_t addrlen);
参数:
- sockfd:表示客户端的socket套接字
- buf:要发送的数据首地址
- len:数据大小
- flags:控制选项(如MSG_DONTWAIT非阻塞,MSG_PEEK窥视数据,0阻塞)
- src_addr:目的地址,数据要发送到哪一个ip地址的主机
- addrlen:地址长度
返回值:
- 成功:返回发送的字节数
- 失败:-1(重置错误码)
UDP适用场景
追求低延迟、高效率,能容忍少量丢包的场景:
- 音视频通话、直播、短视频
- 游戏联机实时对战
- DNS域名解析
- 广播、设备发现、物联网上报
UDP通信方式
一、单播
一对一通信,数据从单一源地址发送到单一目标地址。
流程与各层职责:
- 应用层:调用sendto()发送数据到指定目标IP和端口;调用recvfrom()接收来自特定源的数据。
- 传输层:封装UDP头部:源端口、目标端口、长度、校验和;不建立连接,直接发送数据报。
- 网络层:封装IP头部:源IP、目标IP(单播地址,如192.168.1.100);根据目标IP查找路由表,选择下一跳。
- 数据链路层:根据目标IP的MAC地址(通过ARP解析)封装以太网帧;通过物理网络设备(如网卡)发送到目标主机。
如下图所示:

示例代码如下:
服务端:
/*
此示例代码用于构建UDP单播服务器
*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#define PORT 8888
#define BUFSIZE 1024
int main(int argc,const char *argv[])
{
//创建UDP套接字
int sockfd = socket(AF_INET,SOCK_DGRAM,0);
if(sockfd == -1) {
perror("Scoket创建失败");
return -1;
}
//配置服务器地址结构体
struct sockaddr_in serverInfo;
memset(&serverInfo,0,sizeof(serverInfo));
serverInfo.sin_family = AF_INET;
serverInfo.sin_port = htons(PORT);
serverInfo.sin_addr.s_addr = INADDR_ANY;//表示监听所有网卡
//绑定套接字到地址和端口
int ret_bind = bind(sockfd,(struct sockaddr*)&serverInfo,sizeof(serverInfo));
if(ret_bind == -1) {
perror("绑定失败");
close(sockfd);
return -1;
}
printf("UDP服务器已启动,监听端口:%d\n",PORT);
//循环接收并回显数据
while(1) {
char buf[BUFSIZE] = {0};
//初始化客户端地址
struct sockaddr_in clientInfo;
socklen_t clientInfo_len = sizeof(clientInfo);
//接收数据
ssize_t recv_len = recvfrom(sockfd,buf,sizeof(buf),0,(struct sockaddr*)&clientInfo,&clientInfo_len);
if(recv_len == -1) {
perror("接收数据失败");
continue;//继续等待下一个数据包
}
//打印客户端信息
printf("收到来自[%s:%d]的信息:%s\n",inet_ntoa(clientInfo.sin_addr),ntohs(clientInfo.sin_port),buf);
//回显数据
ssize_t send_len = sendto(sockfd,buf,recv_len,0,(struct sockaddr*)&clientInfo,clientInfo_len);
if(send_len == -1) {
perror("回显数据失败");
}
}
close(sockfd);
return 0;
}
客户端:
/*
此示例代码用于构建UDP单播客户端
*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#define PORT 8888
#define SERVER_IP "192.168.179.100"
#define BUFSIZE 1024
int main(int argc,const char *argv[])
{
//创建套接字
int sockfd = socket(AF_INET,SOCK_DGRAM,0);
if(sockfd == -1) {
perror("创建套接字失败");
return -1;
}
//配置服务器信息
struct sockaddr_in serverInfo;
memset(&serverInfo,0,sizeof(serverInfo));
serverInfo.sin_family = AF_INET;
serverInfo.sin_port = htons(PORT);
serverInfo.sin_addr.s_addr = inet_addr(SERVER_IP);
//发送数据:sendto
/*
函数原型:ssize_t sendto(
int sockfd, 客户端的socket套接字
const void *buf, 要发送的数据首地址
size_t len, 数据大小
int flags,控制选项:
MSG_DONTWAIT非阻塞,
MSG_PEEK窥视数据,
0阻塞
const struct sockaddr *dest_addr, 目的地址
socklen_t addrlen地址长度
);
返回值:
成功返回发送字节数,
失败返回-1
*/
char message[] = "Hello,UDP Server!";
ssize_t ret_sendto = sendto(sockfd,message,strlen(message),0,(struct sockaddr*)&serverInfo,sizeof(serverInfo));
if(ret_sendto == -1) {
perror("发送失败");
close(sockfd);
return -1;
}
printf("已经发送消息:%s\n",message);
//接收回显消息:recvfrom
/*
函数原型:ssize_t recvfrom(
int sockfd, 客户端的socket套接字
void *buf, 要发送的数据首地址
size_t len, 数据大小
int flags,控制选项:
MSG_DONTWAIT非阻塞,
MSG_PEEK窥视数据,
0阻塞
struct sockaddr *src_addr, 源地址
socklen_t *addrlen 地址长度
);
返回值:
成功返回接收字节数,
失败返回-1
*/
char buf[BUFSIZE] = {0};
struct sockaddr_in fromInfo;
socklen_t fromInfo_len = sizeof(fromInfo);
ssize_t ret_recvfrom = recvfrom(sockfd,buf,sizeof(buf)-1,0,(struct sockaddr*)&fromInfo,&fromInfo_len);
if(ret_recvfrom == -1) {
perror("接收失败");
} else {
buf[ret_recvfrom] = '\0';
printf("服务器回显:%s\n",buf);
}
close(sockfd);
return 0;
}
二、组播
一对多通信,数据包发送到一个组播里,组内所有成员均可接收。
流程与各层职责:
- 应用层:发送端:调用sendto发送数据到组播地址;接收端:调用setsockopt加入组播组。
- 传输层:封装UDP头部:目标端口为组播端口;组播成员无需提前建立连接。
- 网络层:封装IP头部:目标IP为组播地址(D类地址);接收端通过IGMP报文通知路由器加入/离开组播组;路由器维护组播成员列表,仅向存在成员的子网转发数据。
- 数据链路层:组播MAC地址映射:将IP组播地址转换成以太网组播MAC;交换机/路由器:根据组播MAC地址复制数据包到多个端口。
如下图所示:

涉及函数讲解:
setsockopt函数
作用:设置 Socket 的选项参数,控制 Socket 的收发、地址、超时、广播、组播、缓存、复用等底层行为。
函数原型:
int setsockopt(
SOCKET sockfd, // 要设置的 socket
int level, // 选项级别(IP层/TCP层/Socket层)
int optname, // 具体选项名
const void *optval, // 选项的值
socklen_t optlen // 值的长度
);
参数:
sockfd:你创建的 UDP/TCP 套接字level:设置哪一层(Socket 层 / IP 层 / TCP 层)optname:你要开哪个功能optval:开 / 关 或 具体数值optlen:数据长度
组播示例代码:
发送方:
/*此示例代码用于构建UDP组播发送方*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#define PORT 12345
#define MULTICAST_IP "239.255.0.1"
int main(int argc,const char *argv[])
{
//创建套接字:socket
int sockfd = socket(AF_INET,SOCK_DGRAM,0);
if(sockfd == -1) {
perror("创建套接字失败");
return -1;
}
//填充组播信息
struct sockaddr_in multicast_addr;
memset(&multicast_addr,0,sizeof(multicast_addr));
multicast_addr.sin_family = AF_INET;
multicast_addr.sin_port = htons(PORT);
multicast_addr.sin_addr.s_addr = inet_addr(MULTICAST_IP);
//设置组播包的TTL(TTL->Time to live):setsockopt
int ttl = 1;//1表示数据包只在局域网内传播
int ret_opt = setsockopt(sockfd,IPPROTO_IP,IP_MULTICAST_TTL,&ttl,sizeof(ttl));
if(ret_opt == -1) {
perror("加入组播失败");
close(sockfd);
return -1;
}
//发送数据到组播组:snedto
char message[] = "Hello Multicast Group!";
ssize_t ret_send = sendto(sockfd,message,strlen(message),0,(struct sockaddr*)&multicast_addr,sizeof(multicast_addr));
if(ret_send == -1) {
perror("数据发送失败");
} else {
printf("数据[%s]发送成功:%zd bytes\n",message,ret_send);
}
return 0;
}
接收方:
/*此示例代码用于构建UDP组播接收方*/
#define _DEFAULT_SOURCE
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#define PORT 12345
#define MULTICAST_IP "239.255.0.1"
#define BUFSIZE 1024
int main(int argc,const char *argv[])
{
//创建套接字:socket
int sockfd = socket(AF_INET,SOCK_DGRAM,0);
if(sockfd == -1) {
perror("创建套接字失败");
return -1;
}
//填充组播信息
struct sockaddr_in local_addr;
memset(&local_addr,0,sizeof(local_addr));
local_addr.sin_family = AF_INET;
local_addr.sin_port = htons(PORT);
local_addr.sin_addr.s_addr = htonl(INADDR_ANY);
//绑定套接字与组播信息:bind
int ret_bind = bind(sockfd,(struct sockaddr*)&local_addr,sizeof(local_addr));
if(ret_bind == -1) {
perror("绑定失败");
close(sockfd);
return -1;
}
//设置加入多播组:setsockopt
struct ip_mreq mrep;
//组播地址
mrep.imr_multiaddr.s_addr = inet_addr(MULTICAST_IP);
//指定本地网卡
mrep.imr_interface.s_addr = htonl(INADDR_ANY);
int ret_opt = setsockopt(sockfd,IPPROTO_IP,IP_ADD_MEMBERSHIP,&mrep,sizeof(mrep));
if(ret_opt == -1) {
perror("加入组播失败");
close(sockfd);
return -1;
}
printf("已经加入组播组:%s,监听端口:%d\n",MULTICAST_IP,PORT);
//循环接收数据:recvfrom
while (1) {
char buf[BUFSIZE] = {0};
struct sockaddr_in send_addr;
socklen_t send_len = sizeof(send_addr);
ssize_t ret_recv = recvfrom(sockfd,buf,sizeof(buf),0,(struct sockaddr*)&send_addr,&send_len);
if(ret_recv == -1) {
perror("接收数据失败");
continue;
}
buf[ret_recv] = '\0';//添加字符串结束符
printf("接收到来自[%s:%d]的组播消息:%s\n",inet_ntoa(send_addr.sin_addr),ntohs(send_addr.sin_port),buf);
}
return 0;
}
三、广播
一对所有通信,数据包发送到同一网络内所有主机。
流程与各层职责:
- 应用层:发送端:调用setsockopt()启用广播选项(SO_BROADCAST);调用sendto()发送数据到广播地址(如255.255.255.255)。
- 传输层:封装UDP头部:目标端口为广播端口;接收端无需加入组,但需监听指定端口。
- 网络层:封装IP头部:目标IP为广播地址(受限广播255.255.255.255或定向广播192.168.1.255);受限广播仅在本局域网内传播,路由器默认不转发;定向广播可跨子网(需路由器支持,通常被禁止)。
- 数据链路层:广播MAC地址:FF:FF:FF:FF:FF:FF;交换机将广播包泛洪到所有端口(除源端口)。
如下图所示:

广播示例代码如下:
发送方:
/*此示例代码用于构建UDP广播发送方*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#define PORT 9999
#define BROADCAST_IP "255.255.255.255"
int main(int argc,const char *argv[])
{
//创建UDP套接字
int sockfd = socket(AF_INET,SOCK_DGRAM,0);
if(sockfd == -1) {
perror("创建套接字失败");
return -1;
}
//启动广播权限
int opt = 1;
int ret_opt = setsockopt(sockfd,SOL_SOCKET,SO_BROADCAST,&opt,sizeof(opt));
if(ret_opt == -1) {
perror("启动广播失败");
close(sockfd);
return -1;
}
//配置广播目标地址
struct sockaddr_in broadcast_addr;
memset(&broadcast_addr,0,sizeof(broadcast_addr));
broadcast_addr.sin_family = AF_INET;
broadcast_addr.sin_port = htons(PORT);
broadcast_addr.sin_addr.s_addr = inet_addr(BROADCAST_IP);
//发送广播数据
char message[] = "Hello Broadcast!";
ssize_t ret_send = sendto(sockfd,message,strlen(message),0,(struct sockaddr*)&broadcast_addr,sizeof(broadcast_addr));
if(ret_opt == -1) {
perror("发送广播数据失败");
} else {
printf("广播发送成功:%s:%zd bytes\n",message,ret_send);
}
close(sockfd);
return 0;
}
接收方:
/*此示例代码用于构建UDP广播接收方*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#define PORT 9999
#define BUFSIZE 1024
int main(int argc,const char *argv[])
{
//创建套接字
int sockfd = socket(AF_INET,SOCK_DGRAM,0);
if(sockfd == -1) {
perror("创建套接字失败");
return -1;
}
//绑定到本地接口
struct sockaddr_in local_addr;
memset(&local_addr,0,sizeof(local_addr));
local_addr.sin_family = AF_INET;
local_addr.sin_port = htons(PORT);
local_addr.sin_addr.s_addr = INADDR_ANY;
int ret_bind = bind(sockfd,(struct sockaddr*)&local_addr,sizeof(local_addr));
if(ret_bind == -1) {
perror("绑定失败");
close(sockfd);
return -1;
}
printf("监听广播端口:%d\n",PORT);
//循环接收数据
while (1) {
char buf[BUFSIZE] = {0};
struct sockaddr_in sender_addr;
socklen_t sender_addr_len = sizeof(sender_addr);
ssize_t ret_recv = recvfrom(sockfd,buf,sizeof(buf),0,(struct sockaddr*)&sender_addr,&sender_addr_len);
if(ret_recv == -1) {
perror("接收数据失败");
continue;
}
buf[ret_recv] = '\0';
printf("收到广播信息%s(来自[%s:%d])\n",buf,inet_ntoa(sender_addr.sin_addr),ntohs(sender_addr.sin_port));
}
close(sockfd);
return 0;
}
UDP与TCP核心对比
| 对比项 | TCP | UDP |
|---|---|---|
| 连接特性 | 面向连接(三次握手建连,四次挥手断连) | 无连接(直接发包) |
| 可靠性 | 可靠传输:不丢包、不乱序、无重复 | 不可靠:可能丢包、乱序、重复 |
| 传输方式 | 面向字节流(数据流式,无边界) | 面向数据报(包有边界,独立传输) |
| 首部长度 | 20~60 字节(可变) | 固定 8 字节 |
| 控制机制 | 确认应答、超时重传、排序、流量控制、拥塞控制 | 仅校验和,无额外控制 |
| 广播 / 组播 | 不支持 | 原生支持 |
| 延迟 | 较高(建连、确认、重传开销) | 极低(无额外开销) |
| 应用场景 | 文件传输、网页、邮件、登录(要求 100% 可靠) | 音视频、游戏、DNS、广播(要求低延迟) |
| 边界问题 | 粘包问题(字节流无边界) | 无粘包,有包截断问题 |
常见面试问题
1.UDP不可靠为什么还大量使用?
因为在实时场景下,延迟的重要性大于可靠性,比如语音丢一两帧人耳感觉不出来的,但是重传会造成卡顿,像实时通话一卡一卡的肯定体验感不好,同时UDP开销小、转发快。
2.能不能在UDP之上实现可靠传输?
可以。很多自定义协议(如QUIC、游戏私有协议)基于UDP自己实现:重传、确认、排序、拥塞控制,兼顾UDP低延迟+TCP可靠性,像很多实时性要求高又要求可靠性的游戏比如王者荣耀这种MOBA类游戏都会有自己的协议去保证可靠性。
3.TCP有粘包,为什么UDP没有?
因为TCP是字节流传输,数据无边界,内核合并数据或者拆分数据,很容易导致粘包;UDP是数据报内核严格保留包边界,所以无粘包,只有包截断。