37、UDP网络编程入门

UDP网络编程入门

一、网络编程基础理论

1.1 网络模型:OSI七层与TCP/IP四层

网络通信的体系结构有两大核心模型,其中OSI模型是通用参考模型,TCP/IP模型是互联网实际应用模型,二者的对应关系如下:

OSI七层模型 核心功能 TCP/IP四层模型 核心协议/技术
应用层 为用户提供网络服务(邮件、文件传输等) 应用层 HTTP、FTP、TFTP、DNS、SNMP
表示层 统一数据表示形式,完成数据加密/解密 应用层 (TCP/IP模型中合并至应用层)
会话层 管理进程会话,协调通信过程 应用层 (TCP/IP模型中合并至应用层)
传输层 管理端到端数据传输,提供可靠/不可靠服务 传输层 TCP(可靠传输)、UDP(实时传输)
网络层 路由选择、网际互连,定位目标主机 网络层 IP、ICMP(ping命令)、RIP、OSPF、IGMP
数据链路层 物理地址寻址、数据帧封装、差错控制 接口层 ARP(IP转MAC)、RARP
物理层 将数据转为电信号,通过网络介质传输 接口层 网卡、双绞线、光纤、无线信道

1.2 核心概念解析

(1)IP地址与端口
  • IP地址 :由网络位+主机位组成,用于识别网络中的主机,主流为IPv4(32位,如192.168.14.128),未来将逐步过渡到IPv6。
  • 端口号 :用于识别主机上的应用程序,范围为165535,其中11023为系统保留端口,1024~65535为用户自定义端口。
  • IP+端口:唯一标识网络中的某个应用程序,是网络通信的基础地址标识。
(2)Socket(套接字)

Socket是应用程序与内核网络协议栈的接口,本质是一个文件描述符,通过它可以实现网络数据的收发操作,是网络编程的核心入口。

(3)字节序
  • 主机字节序:主流CPU(Intel、AMD、ARM)采用小端存储(低字节存低地址)。
  • 网络字节序:网络设备统一采用大端存储(高字节存低地址)。
  • 转换函数:htons()(主机短整型转网络字节序)、inet_addr()(点分十进制IP转网络字节序IP)。
(4)网络配置相关命令
命令 功能
ifconfig 查看/临时配置网络信息(如ifconfig ens33 192.168.0.13/24 up临时设置IP)
sudo vim /etc/network/interfaces 编辑Ubuntu永久网络配置文件
sudo /etc/init.d/networking restart 重启网络服务,加载永久配置
ping www.baidu.com 测试网络连通性
netstat -anp 查看本机所有网络通信连接与进程关联
sudo reboot -f 强制重启系统,生效网络配置

1.3 DHCP与DNS

  • DHCP(动态主机配置协议):自动为主机分配IP地址、子网掩码、网关等网络参数,无需手动配置。
  • DNS(域名系统) :将人类易记的域名(如www.baidu.com)翻译成机器可识别的IP地址,实现域名到IP的映射。

二、UDP协议详解

2.1 UDP核心特性

UDP(用户数据报协议)是一种无连接的传输层协议,核心特性如下:

  1. 无连接:通信双方无需建立连接,直接发送数据,节省连接建立开销。
  2. 低延迟:无需确认、重传等机制,数据传输速度快,适合实时场景。
  3. 网络资源使用率低:协议头部简单(仅8字节),开销小。
  4. 不可靠传输:不保证数据的有序到达和完整性,可能出现丢包、乱序。

2.2 UDP数据报特点

  1. 数据与数据之间有明确边界,接收方可以区分不同的数据包。
  2. 发送和接收的次数必须对应,发送10次数据,接收方也需调用10次接收函数才能完整获取。
  3. 无写阻塞:发送速度过快时,内核缓冲区满会导致数据丢失,需自行控制发送速率。
  4. 有读阻塞:接收方若无数据可读,会阻塞等待直到有数据到达。

2.3 UDP通信角色与流程

(1)通信角色
  • 服务端:提供服务的一端,通常只有1个,负责绑定固定IP和端口,等待并处理客户端请求。
  • 客户端:使用服务的一端,可有多台,无需绑定固定端口,直接向服务端发送请求。
(2)函数调用流程
角色 核心流程 关键函数
服务端 创建套接字→绑定IP和端口→循环接收客户端数据→回复数据→关闭套接字 socket()bind()recvfrom()sendto()close()
客户端 创建套接字→向服务端发送数据→接收服务端回复→关闭套接字 socket()sendto()recvfrom()close()

三、UDP实战:时间戳回显服务(完整代码)

本节实现一个简单的UDP服务端,接收客户端发送的消息后,拼接当前时间戳再回复给客户端;客户端循环发送消息并接收回复,展示UDP通信的完整流程。

3.1 服务端代码(udp_server.c)

c 复制代码
#include <arpa/inet.h>
#include <netinet/in.h>
#include <netinet/ip.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <time.h>
#include <unistd.h>

// 定义套接字地址结构体指针别名,简化代码
typedef struct sockaddr *(SA);

int main(int argc, char **argv)
{
    // 1. 创建UDP套接字
    // AF_INET:IPv4地址族,SOCK_DGRAM:UDP数据报套接字,0:自动适配协议
    int sockfd = socket(AF_INET, SOCK_DGRAM, 0);
    if (-1 == sockfd)
    {
        perror("socket create failed");
        return 1;
    }
    printf("UDP套接字创建成功,套接字描述符:%d\n", sockfd);

    // 2. 初始化服务端地址结构体
    struct sockaddr_in ser_addr, cli_addr;
    bzero(&ser_addr, sizeof(ser_addr));  // 清空结构体内存
    bzero(&cli_addr, sizeof(cli_addr));
    ser_addr.sin_family = AF_INET;  // 指定IPv4地址族
    ser_addr.sin_port = htons(50000);  // 端口号转网络字节序(50000为自定义端口)
    // 绑定本机IP(192.168.14.128),也可使用INADDR_ANY绑定所有网卡IP
    ser_addr.sin_addr.s_addr = inet_addr("192.168.14.128");

    // 3. 绑定套接字与服务端IP+端口
    int ret = bind(sockfd, (SA)&ser_addr, sizeof(ser_addr));
    if (-1 == ret)
    {
        perror("bind failed");
        close(sockfd);
        return 1;
    }
    printf("套接字绑定成功,IP:192.168.14.128,端口:50000\n");

    // 4. 循环接收客户端数据并回复
    socklen_t cli_addr_len = sizeof(cli_addr);  // 客户端地址长度
    while (1)
    {
        char buf[512] = {0};  // 接收缓冲区

        // 接收客户端数据(阻塞等待)
        // (SA)&cli_addr:用于存储客户端地址信息,&cli_addr_len:客户端地址长度
        ssize_t recv_len = recvfrom(sockfd, buf, sizeof(buf), 0, (SA)&cli_addr, &cli_addr_len);
        if (recv_len < 0)
        {
            perror("recvfrom failed");
            continue;
        }
        // 打印客户端信息与接收的数据
        printf("收到客户端[%s:%d]消息:%s\n", 
               inet_ntoa(cli_addr.sin_addr), ntohs(cli_addr.sin_port), buf);

        // 获取当前系统时间,拼接至接收的消息后
        time_t current_time;
        time(&current_time);
        struct tm *time_info = localtime(&current_time);
        sprintf(buf, "%s %02d:%02d:%02d", buf, 
                time_info->tm_hour, time_info->tm_min, time_info->tm_sec);

        // 回复客户端数据
        ssize_t send_len = sendto(sockfd, buf, strlen(buf) + 1, 0, (SA)&cli_addr, cli_addr_len);
        if (send_len < 0)
        {
            perror("sendto failed");
            continue;
        }
        printf("已回复客户端[%s:%d]消息:%s\n", 
               inet_ntoa(cli_addr.sin_addr), ntohs(cli_addr.sin_port), buf);
    }

    // 5. 关闭套接字(实际因while(1)循环,此处不会执行,可通过Ctrl+C终止)
    close(sockfd);
    return 0;
}
服务端代码关键解析
  • socket(AF_INET, SOCK_DGRAM, 0):创建IPv4 UDP套接字,返回文件描述符。
  • bind():将套接字与固定IP和端口绑定,服务端必须绑定,否则客户端无法定位服务端。
  • recvfrom():接收客户端数据,同时获取客户端地址信息,便于回复。
  • sendto():向指定客户端地址回复数据,需传入客户端地址结构体。
  • htons()/inet_addr():完成主机字节序到网络字节序的转换,保证跨设备通信兼容性。

3.2 客户端代码(udp_client.c)

c 复制代码
#include <arpa/inet.h>
#include <netinet/in.h>
#include <netinet/ip.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <time.h>
#include <unistd.h>

typedef struct sockaddr *(SA);

int main(int argc, char **argv)
{
    // 1. 创建UDP套接字
    int sockfd = socket(AF_INET, SOCK_DGRAM, 0);
    if (-1 == sockfd)
    {
        perror("socket create failed");
        return 1;
    }
    printf("UDP客户端套接字创建成功\n");

    // 2. 初始化服务端地址结构体(指定服务端IP和端口)
    struct sockaddr_in ser_addr;
    bzero(&ser_addr, sizeof(ser_addr));
    ser_addr.sin_family = AF_INET;
    ser_addr.sin_port = htons(50000);  // 与服务端端口保持一致
    ser_addr.sin_addr.s_addr = inet_addr("192.168.14.128");  // 服务端IP

    // 3. 循环发送消息给服务端,接收回复
    int send_count = 10;  // 循环发送10次消息
    while (send_count--)
    {
        char buf[512] = "hello";  // 要发送的消息
        // 向服务端发送数据
        ssize_t send_len = sendto(sockfd, buf, strlen(buf), 0, (SA)&ser_addr, sizeof(ser_addr));
        if (send_len < 0)
        {
            perror("sendto failed");
            close(sockfd);
            return 1;
        }
        printf("已向服务端发送消息:%s(剩余发送次数:%d)\n", buf, send_count);

        // 接收服务端回复
        bzero(buf, sizeof(buf));  // 清空接收缓冲区
        ssize_t recv_len = recvfrom(sockfd, buf, sizeof(buf), 0, NULL, NULL);
        if (recv_len < 0)
        {
            perror("recvfrom failed");
            close(sockfd);
            return 1;
        }
        printf("收到服务端回复:%s\n", buf);

        sleep(1);  // 每隔1秒发送一次
    }

    // 4. 关闭套接字
    close(sockfd);
    printf("客户端正常退出\n");
    return 0;
}
客户端代码关键解析
  • 客户端无需调用bind():内核会自动为客户端分配随机端口和本机IP,不影响与服务端通信。
  • sendto():直接向服务端地址发送数据,需指定服务端的IP和端口。
  • recvfrom(NULL, NULL):客户端无需关心服务端地址(已明确),故传入NULL忽略服务端地址信息。
  • sleep(1):控制发送速率,避免因发送过快导致丢包。

四、编译与运行步骤

4.1 环境准备

  1. 安装编译环境 :Ubuntu系统中安装gcc编译器

    bash 复制代码
    sudo apt update && sudo apt install gcc -y
  2. 配置网络 :确保服务端与客户端在同一局域网,或通过桥接模式实现通信

    • 临时设置服务端IP:ifconfig ens33 192.168.14.128/24 up
    • 测试网络连通性:ping 192.168.14.128(客户端ping服务端,确保网络通畅)

4.2 编译代码

bash 复制代码
# 编译服务端
gcc udp_server.c -o udp_server
# 编译客户端
gcc udp_client.c -o udp_client

4.3 运行程序

步骤1:启动服务端(先运行)
bash 复制代码
./udp_server

服务端输出:

复制代码
UDP套接字创建成功,套接字描述符:3
套接字绑定成功,IP:192.168.14.128,端口:50000
步骤2:启动客户端(新开终端,后运行)
bash 复制代码
./udp_client

客户端输出(示例):

复制代码
UDP客户端套接字创建成功
已向服务端发送消息:hello(剩余发送次数:9)
收到服务端回复:hello 15:30:25
已向服务端发送消息:hello(剩余发送次数:8)
收到服务端回复:hello 15:30:26
...
客户端正常退出
步骤3:查看服务端输出(示例)
复制代码
收到客户端[192.168.14.129:41235]消息:hello
已回复客户端[192.168.14.129:41235]消息:hello 15:30:25
收到客户端[192.168.14.129:41235]消息:hello
已回复客户端[192.168.14.129:41235]消息:hello 15:30:26
...

五、常见问题排查

  1. bind()失败:Address already in use

    • 原因:端口50000已被其他进程占用。
    • 解决:使用netstat -anp | grep 50000查找占用进程,杀死进程或更换端口。
  2. 无法接收/发送数据

    • 检查服务端与客户端的IP是否正确,确保在同一网段。
    • 检查防火墙是否拦截UDP端口,可关闭防火墙(sudo ufw disable)。
    • 确认网络模式为桥接模式,避免NAT模式导致的网段隔离。
  3. 数据丢失

    • 原因:UDP无流量控制,发送速率过快导致内核缓冲区溢出。
    • 解决:在客户端增加sleep()控制发送速率,或在应用层实现流量控制。

六、总结与扩展

6.1 本文总结

  1. 网络模型:OSI七层是参考模型,TCP/IP四层是实际应用模型,二者对应关系需牢记。
  2. UDP特性:无连接、低延迟、不可靠,适合实时场景,数据报有明确边界,收发次数需对应。
  3. UDP编程流程:服务端(创建套接字→绑定→收发数据),客户端(创建套接字→收发数据)。
  4. 核心函数:socket()bind()recvfrom()sendto()是UDP编程的基础。

6.2 扩展方向

  1. 多客户端通信 :服务端通过recvfrom()获取不同客户端的地址,实现一对多通信。
  2. 数据校验:UDP无可靠性保证,可在应用层添加校验和、重传机制,提升数据传输可靠性。
  3. TCP与UDP对比学习:TCP是面向连接的可靠传输,适合文件传输等场景,可进一步学习TCP编程。
  4. 网络编程进阶:学习套接字选项设置、非阻塞IO、多路复用(select/poll/epoll)等高级技术。
相关推荐
勇往直前plus2 小时前
Jackson 反序列化首字母大写字段映射失败的底层原因与解决方案
java·开发语言·前端
转转技术团队2 小时前
基于微前端 qiankun 多实例保活的工程实践
前端·javascript·前端工程化
数字护盾(和中)2 小时前
BAS模拟入侵攻击系统:前置防控核心,守护企业网络安全
网络·安全·web安全
坐吃山猪2 小时前
Python命令行工具Click
linux·开发语言·python
宠..2 小时前
为单选按钮绑定事件
运维·服务器·开发语言·数据库·c++·qt·microsoft
QC七哥2 小时前
基于vnstat监控服务器的网卡流量
运维·服务器·监控·vnstat
Lueeee.2 小时前
FFMPEG核心结构体
linux·ffmpeg
德迅云安全—珍珍2 小时前
游戏掉线使用游戏盾SDK能优化网络吗
网络·游戏
山土成旧客2 小时前
【Python学习打卡-Day28】类的蓝图:从模板到对象的构建艺术
linux·python·学习