UDP通信

UDP也叫用户数据报协议,位于传输层,是无连接、不可靠、面向数据的协议。

UDP通信前不需要建立连接,发送端直接打包数据发送,接收端被动接受,不保证数据一定送达、有序、不重复。

特点

  1. 无连接:不需要像TCP一样三次握手连接,四次挥手断开,发送前不需要和对方协商,拿到对方IP+端口就直接发,霸道总裁强制爱,拿到你的地址和电话就给你送东西过去了
  2. 面向数据报:每次发送的是独立完整数据,不会拆分/合并;一次sendto对应一个完整的UDP包;接收方recvfrom时必须读完一整个包,读不完剩下的数据就直接丢了,不会缓存
  3. 不可靠传输:不确认、不重传、不排序、无流量/拥塞控制;可能会丢包、乱序、重复接收
  4. 首部开销小:UDP首部固定8字节,结构简单,转发效率高
  5. 支持一对多通信:原生支持广播、组播,TCP做不到
  6. 全双工:这个和TCP一样,双方都可以收发数据

UDP报文头部

共四个字段,每个字段2字节:

  1. 源端口:发送方端口
  2. 目的端口:接收方端口
  3. 数据报长度:整个UDP包(首部+数据)总长度
  4. 校验和:简单校验是否损坏,出错直接丢弃

UDP通信流程

服务端(接收方)

  1. 创建UDP socket套接字------socket(AF_INET,SOCK_DGRAM,0)
  2. 绑定IP+端口------bind()
  3. 循环调用recvfrom阻塞等待接收数据------recvfrom()
  4. 处理数据,可选sendto回显数据

客户端(发送方)

  1. 创建UDP socket套接字------socket(AF_INET,SOCK_DGRAM,0)
  2. 直接调用sendto,指定服务端IP+端口号发送数据
  3. 可选用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通信方式

一、单播

一对一通信,数据从单一源地址发送到单一目标地址。

流程与各层职责:

  1. 应用层:调用sendto()发送数据到指定目标IP和端口;调用recvfrom()接收来自特定源的数据。
  2. 传输层:封装UDP头部:源端口、目标端口、长度、校验和;不建立连接,直接发送数据报。
  3. 网络层:封装IP头部:源IP、目标IP(单播地址,如192.168.1.100);根据目标IP查找路由表,选择下一跳。
  4. 数据链路层:根据目标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;
}

二、组播

一对多通信,数据包发送到一个组播里,组内所有成员均可接收。

流程与各层职责:

  1. 应用层:发送端:调用sendto发送数据到组播地址;接收端:调用setsockopt加入组播组。
  2. 传输层:封装UDP头部:目标端口为组播端口;组播成员无需提前建立连接。
  3. 网络层:封装IP头部:目标IP为组播地址(D类地址);接收端通过IGMP报文通知路由器加入/离开组播组;路由器维护组播成员列表,仅向存在成员的子网转发数据。
  4. 数据链路层:组播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;
}

三、广播

一对所有通信,数据包发送到同一网络内所有主机。

流程与各层职责:

  1. 应用层:发送端:调用setsockopt()启用广播选项(SO_BROADCAST);调用sendto()发送数据到广播地址(如255.255.255.255)。
  2. 传输层:封装UDP头部:目标端口为广播端口;接收端无需加入组,但需监听指定端口。
  3. 网络层:封装IP头部:目标IP为广播地址(受限广播255.255.255.255或定向广播192.168.1.255);受限广播仅在本局域网内传播,路由器默认不转发;定向广播可跨子网(需路由器支持,通常被禁止)。
  4. 数据链路层:广播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是数据报内核严格保留包边界,所以无粘包,只有包截断。

相关推荐
.千余13 小时前
【测试】测试用例设计攻略(6大设计方法)
服务器·网络·笔记·学习·测试用例
Yang961113 小时前
鼎讯信通 OM‑T 台式频谱分析仪:铁路通信干扰排查与信号测试利器
网络·信息与通信
枕星而眠14 小时前
Linux网络协议三部曲:从UDP/TCP到HTTP,一篇打通任督二核
linux·网络协议·udp
Multipath71214 小时前
与辉同行山东行看大型户外活动的通信保障
网络·5g·安全·无人机·实时音视频
剑神一笑14 小时前
Linux curl 命令深度解析:从 HTTP 请求到网络调试实战
linux·网络·http
SLD_Allen14 小时前
在LLM HTTP底层交互中大模型的Agent Skill功能
网络协议·http·交互·agent skill
IT策士14 小时前
Docker Compose 文件详解:服务、网络与卷
网络·docker·容器
rcms1527026921814 小时前
Silicon Graphics ADS512101 嵌入式开发主板
网络
IT策士14 小时前
Docker 网络入门:桥接、自定义与主机网络
网络·docker·容器