网络编程(一)UDP

网络编程概述


1.1 网络解决的问题

  • 互联网的本质问题:实现设备与设备之间,在不同区域、不同网络环境下的数据交换操作。

  • 设备类型:不限,包含手机、电脑、平板、智能穿戴设备等。

  • 要求:设备必须支持数据传输的网络协议栈

  • 协议概念

    • 协议 = 数据传递的规则和格式标准

    • 例如:

      • Socket 协议:提供进程间通信接口

      • IP 协议:提供主机寻址和路由功能

      • TCP/UDP:提供数据传输服务

      • 链路层/硬件协议:保证物理传输

总结:网络就是通过标准化的协议,使不同设备、不同系统之间的数据能顺利交换。


1.2 网络起源

  • 历史背景

    • 1957 年,苏联发射 Sputnik 卫星,引起美国对国防安全的关注。

    • 1958 年 1 月 7 日,美国国会拨款成立 ARPA(高级研究计划署)。

  • 计算机网络的设计原则

    1. 不用于电话通信,而是用于数据传输

    2. 可靠地传输数据,即使部分节点损坏也能继续工作

    3. 能够连接不同类型的计算机(不同品牌、操作系统)

    4. 网络中每个节点同等重要(去中心化设计)

    5. 必须有冗余路由,保证在节点或链路故障时数据仍能到达

理解:这些原则奠定了现代互联网的分布式、冗余和可靠性的基础。


1.3 网络结构

  • 网络分层

    • 终端设备:电脑、手机、传感器等

    • 交换设备:路由器、交换机

    • 通信链路:光纤、以太网、电缆、无线链路

  • 分层逻辑

    • 每一层完成自己的任务,向上提供服务,向下使用服务

    • 例如:应用层负责具体业务,传输层负责端到端通信,网络层负责路由,链路层负责物理传输

图示说明

  • 三层网络层级(可画图表示核心/汇聚/接入层)

  • 网络组成图例(终端 ↔ 交换 ↔ 链路)


1.4 网络传递的主要协议(重点)

层级功能说明示例协议
应用层应用程序之间的数据通信FTP、HTTP、Telnet
传输层端到端进程数据传输,提供逻辑通信TCP、UDP
网络层数据包封装、寻址和路由,保证数据尽可能到达目的地IP、ICMP
链路层数据帧发送、接收和错误检测Ethernet、Wi-Fi

七层模型

层级功能说明典型协议 / 技术
应用层提供用户应用服务,如文件传输、邮件、网页访问HTTP、FTP、SMTP、Telnet
表示层数据格式转换、加密解密、压缩JPEG、MPEG、SSL/TLS
会话层会话管理、建立/保持/关闭会话RPC、NetBIOS、PPTP
传输层端到端进程通信、可靠传输(TCP)或快速传输(UDP)TCP、UDP
网络层路由选择、逻辑寻址IP、ICMP、ARP
数据链路层数据帧封装、差错检测、MAC 地址识别Ethernet、PPP、Wi-Fi
物理层信号传输、电压、电缆类型光纤、同轴电缆、网线、无线信号

理解

    • 物理层:数据在媒介上真实传输

    • 数据链路层:保证帧正确发送、接收

    • 网络层:负责寻址、路由

    • 传输层:提供进程间可靠/快速通信

    • 会话层/表示层/应用层:管理会话、数据格式、提供具体应用服务


1.5 IP 地址

  • IPv4 地址

    • 32 位二进制

    • 编程中存储方式:4 字节整数

    • 常用表示:点分十进制 "192.168.16.125"

  • IP 地址作用

    • 唯一标识网络中的设备

    • 传输数据时必须提供目标 IP

  • 存储示例(编程中):

二进制对应十进制
1100 0000192
1010 1000168
0001 000016
0111 1101125

1.6 端口号

  • 概念

    • 每台主机的每个进程都有唯一端口号

    • 数据到达主机后,根据端口号交给对应进程

  • 范围:0 ~ 65535

    • 建议使用 1000 以上端口

    • 避开常用端口:22、80、3306、8080

  • 总结:数据传输必须同时提供目标 IP + 端口号


1.7 补充说明

  • MAC 地址:网卡物理地址,无法修改,可用于过滤白名单/黑名单

  • 子网掩码 NetMask:判断两个 IP 是否在同一网段

  • 回环地址/本地地址

    • IPv4:127.0.0.1

    • IPv6:::1

网络字节序 / 大端字节序


2.1 字节序概念

  • 字节序(Endianness):多字节数据在内存中存储顺序的约定

  • 常见类型

    1. 小端序(Little Endian)

      • 数据低位存放在低地址,高位存放在高地址

      • CPU 类型:x86 系列常用小端序

      • 内存示例(32-bit 整数 0x12345678):

        
        

        地址↑ : 低 → 高 内容 : 78 56 34 12

    2. 大端序(Big Endian)

      • 数据高位存放在低地址,低位存放在高地址

      • 网络传输标准采用大端序

      • 内存示例(32-bit 整数 0x12345678):

        
        

        地址↑ : 低 → 高 内容 : 12 34 56 78

总结:不同 CPU 架构可能使用不同字节序,而网络协议要求统一的大端序,保证多设备间通信一致。


2.2 网络传输与本地存储

  • 问题

    • 本地存储可能是小端

    • 网络协议要求大端

    • 如果不做转换,多字节数据(IP、端口、整数)在不同主机间可能解析错误

  • 解决方案

    • 提供 字节序转换函数,在本地和网络间转换数据

    • 函数分为两类:

      1. 处理 4 字节数据(32-bit 整数)

        • 本地 → 网络:htonl

        • 网络 → 本地:ntohl

      2. 处理 2 字节数据(16-bit 整数)

        • 本地 → 网络:htons

        • 网络 → 本地:ntohs


2.3 头文件

#include <arpa/inet.h>
  • 提供字节序转换函数:

    • htonl(uint32_t hostlong):本地 32-bit → 网络大端

    • htons(uint16_t hostshort):本地 16-bit → 网络大端

    • ntohl(uint32_t netlong):网络 32-bit → 本地

    • ntohs(uint16_t netshort):网络 16-bit → 本地


2.4 函数详细说明

函数功能说明参数说明返回值
uint32_t htonl(uint32_t hostlong)将本地 32-bit 无符号整数转为网络大端字节序hostlong:本地整数转换为大端字节序整数
uint16_t htons(uint16_t hostshort)将本地 16-bit 无符号整数转为网络大端字节序hostshort:本地短整数转换为大端字节序短整数
uint32_t ntohl(uint32_t netlong)将网络大端 32-bit 数据转为本地字节序netlong:网络数据本地字节序整数
uint16_t ntohs(uint16_t netshort)将网络大端 16-bit 数据转为本地字节序netshort:网络数据本地字节序短整数

2.5 应用场景

  1. IP 地址转换

    • IPv4 地址存储为 4 字节整数,需要在本地和网络间转换

  2. 端口号转换

    • 16-bit 端口号发送前必须转为网络字节序

  3. 跨平台通信

    • 保证不同 CPU 架构(小端/大端)设备间数据一致


2.6 代码案例

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdint.h>
#include <arpa/inet.h>

#define IP "192.168.16.96"
#define IP_B 11000000101010000001000001100000 
#define IP_D 3232239712
#define PORT 8889

int main(int argc, char const *argv[])
{
    uint32_t net = htonl((uint32_t)IP_D);
    uint16_t netp = htons((uint16_t)PORT);
    uint32_t IpD = ntohl(net);
    uint16_t port = ntohs(netp);
    printf("%d %d\n",net,netp);
    printf("%u %d\n",IpD,port);
    return 0;
}

你可以自己填入 IP、端口和打印代码进行测试。

IP 地址转换操作函数


3.1 IP 地址存储与表示

  • IPv4 地址

    • 4 字节无符号整数(编程中使用,网络传输)

    • 点分十进制字符串(常见表示方式,便于阅读)

      192.168.16.125

  • 网络传输要求

    • 数据必须以 4 字节整数大端字节序传输

  • 本地表示要求

    • 为了可读性和操作方便,常使用字符串表示

总结:网络传输用整数(大端),本地显示用字符串(点分十进制)


3.2 字符串 IP → 网络 IP(本地 → 网络)

函数:inet_pton

#include <arpa/inet.h>

int inet_pton(int af, const char *src, void *dst);
参数说明
int af协议族,IPv4 使用 AF_INET,IPv6 使用 AF_INET6
const char *src点分十进制字符串 IPv4 或 IPv6 地址
void *dst存储网络字节序 IP 地址的大端字节序变量地址
  • 返回值

    • 1 :转换成功

    • 0 :字符串不合法

    • -1:协议族非法,设置 errno

应用场景:UDP/TCP 发送端和接收端设置目标 IP 时,必须将字符串 IP 转为网络字节序整数。


3.3 网络 IP → 字符串 IP(网络 → 本地)

函数:inet_ntop

#include <arpa/inet.h>

const char *inet_ntop(int af, const void *src, char *dst, socklen_t size);
参数说明
int af协议族,IPv4 使用 AF_INET,IPv6 使用 AF_INET6
const void *src网络字节序大端 IP 地址
char *dst存储转换后的字符串缓冲区
socklen_t size缓冲区字节长度,一般 IPv4 用 16,IPv6 用 46
  • 返回值

    • 成功:返回 dst 地址

    • 失败:返回 NULL,同时设置 errno

应用场景:UDP/TCP 接收端打印发送端 IP 时,将网络大端 IP 转为可读字符串。


3.4 使用流程总结

  1. 发送端

    • IP 地址:字符串 → 网络字节序整数

    • 端口号:主机字节序 → 网络字节序(htons)

  2. 接收端

    • 网络字节序 IP → 字符串显示(inet_ntop)

    • 网络字节序端口 → 本地主机字节序(ntohs)


3.5 代码案例

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

#include <arpa/inet.h>

#define IP "192.168.16.96"
#define IP_B 11000000101010000001000001100000 
#define IP_D 3232239712
#define PORT 8889

int main()
{
    uint32_t net_ip;
    char ip_buf[16];

    if (inet_pton(AF_INET, IP, &net_ip) != 1) {
        perror("inet_pton failed");
        return 1;
    }

    if (inet_ntop(AF_INET, &net_ip, ip_buf, sizeof(ip_buf)) == NULL) {
        perror("inet_ntop failed");
        return 1;
    }

    printf("Original IP: %s\n", IP);
    printf("After conversion: %s\n", ip_buf);

    return 0;
}

UDP 网络传输


4.1 UDP 特性

  • 无连接:不需要建立连接就能发送数据

  • 不可靠:不保证数据到达顺序,也不确认接收

  • 数据包传输:以独立数据包(Datagram)方式传输

  • 速度快:因无连接、无需握手确认,传输效率高

  • 发送端/接收端概念

    • 没有严格客户端和服务器

    • 只有发送端和接收端

总结:UDP 更适合实时性要求高、允许少量丢包的场景,如视频流、语音通信、传感器数据。


4.2 UDP 通信流程

流程概览

  1. 创建套接字

    • 使用 socket 申请 UDP 套接字

  2. 发送端操作

    • 准备发送数据

    • 准备目标接收端 IP 与端口(字符串 IP → 网络字节序)

    • 使用 sendto 发送数据包

  3. 接收端操作

    • 准备接收 IP 和端口绑定结构体

    • 使用 bind 绑定本机端口(本机 IP + 端口)

    • 调用 recvfrom 接收数据,并获取发送端信息

  4. 关闭资源

    • 使用 close 关闭套接字


4.3 UDP 核心 API

1️⃣ socket - 套接字创建

#include <sys/types.h>
#include <sys/socket.h>

int socket(int domain, int type, int protocol);
参数说明
domain协议族,IPv4:AF_INET
type套接字类型:SOCK_DGRAM(UDP)
protocolIP 协议:IPPROTO_IP
  • 返回值

    • ≥0:套接字文件描述符

    • -1:创建失败,设置 errno


2️⃣ bind - 绑定本地端口

#include <sys/types.h>
#include <sys/socket.h>

int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
参数说明
sockfdsocket 文件描述符
addrstruct sockaddr 指针,包含 IP 协议、网络字节序 IP、端口
addrlen结构体大小
  • 返回值

    • 0:成功

    • -1:失败,设置 errno

注意:接收端必须绑定本机 IP + 端口,发送端可不绑定(系统自动分配端口)。


3️⃣ sendto - 发送 UDP 数据

#include <sys/types.h>
#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);
参数说明
sockfdUDP 套接字
buf发送缓冲区
len发送字节数
flags标志位,一般 0
dest_addr目标接收端 IP + 端口结构体
addrlen结构体字节数
  • 返回值

    • ≥0:发送成功,返回发送字节数

    • -1:失败,设置 errno


4️⃣ recvfrom - 接收 UDP 数据

#include <sys/types.h>
#include <sys/socket.h>

ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags,
                 struct sockaddr *src_addr, socklen_t *addrlen);
参数说明
sockfdUDP 套接字
buf接收缓冲区
len缓冲区大小
flags标志位,一般 0
src_addr可选,用于存储发送端 IP + 端口信息
addrlen指向 socklen_t,存储结构体大小
  • 返回值

    • 0:接收有效字节数

    • 0:EOF 或数据结束

    • -1:接收失败

注意

  • recvfrom 可以不传发送端信息(src_addr=NULL

  • UDP 无连接,所以每个数据包独立


4.4 UDP 发送端 / 接收端流程总结

步骤发送端接收端
1创建套接字创建套接字
2准备目标 IP + 端口准备本地 IP + 端口结构体
3数据打包绑定端口(bind)
4sendto 发送recvfrom 接收数据
5可选处理发送反馈可选处理接收端信息
6关闭套接字关闭套接字

核心思想:

  • UDP 不建立连接

  • 数据包独立传输

  • 发送端主动指定目标,接收端绑定端口监听


4.5 代码案例

1️⃣发送
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

#include <unistd.h>
#include <arpa/inet.h>
#include <sys/types.h> 
#include <sys/socket.h>

#define DEST_IP_ADDR "192.168.16.96"
#define DEFT_PORT    (8999)

int main(int argc, char const *argv[])
{
    // 1. 创建 UDP 套接字
    int sockFd = socket(AF_INET, SOCK_DGRAM, IPPROTO_IP);
    if (sockFd < 0)
    {
        perror("socket creation failed");
        exit(EXIT_FAILURE);
    }

    // 2. 准备发送数据
    char *str = "hello UDP";
    if (str == NULL)
    {
        fprintf(stderr, "Invalid data to send\n");
        close(sockFd);
        exit(EXIT_FAILURE);
    }

    // 3. 准备目标地址结构体
    struct sockaddr_in destAddr;
    memset(&destAddr, 0, sizeof(destAddr));

    destAddr.sin_family = AF_INET;
    destAddr.sin_port = htons(DEFT_PORT);

    if (inet_pton(AF_INET, DEST_IP_ADDR, &(destAddr.sin_addr.s_addr)) != 1)
    {
        fprintf(stderr, "Invalid IP address: %s\n", DEST_IP_ADDR);
        close(sockFd);
        exit(EXIT_FAILURE);
    }

    // 4. 发送数据
    ssize_t sentBytes = sendto(sockFd, str, strlen(str), 0,
                               (const struct sockaddr *)&destAddr,
                               (socklen_t)sizeof(destAddr));
    if (sentBytes < 0)
    {
        perror("sendto failed");
        close(sockFd);
        exit(EXIT_FAILURE);
    }
    else
    {
        printf("Sent %zd bytes to %s:%d\n", sentBytes, DEST_IP_ADDR, DEFT_PORT);
    }

    // 5. 关闭套接字
    if (close(sockFd) < 0)
    {
        perror("close socket failed");
        exit(EXIT_FAILURE);
    }

    return 0;
}
2️⃣接收
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

#include <unistd.h>
#include <arpa/inet.h>
#include <sys/types.h> 
#include <sys/socket.h>

#define HOST_IP_ADDR "192.168.16.96"
#define HOST_PORT    (8999)
#define BUFFER_SIZE  (512)

int main(int argc, char const *argv[])
{
    // 1. 创建 UDP 套接字
    int sockFd = socket(AF_INET, SOCK_DGRAM, IPPROTO_IP);
    if (sockFd < 0)
    {
        perror("socket creation failed");
        exit(EXIT_FAILURE);
    }

    // 2. 准备本机接收端地址
    struct sockaddr_in hostAddr;
    memset(&hostAddr, 0, sizeof(hostAddr));
    hostAddr.sin_family = AF_INET;
    hostAddr.sin_port = htons(HOST_PORT);

    if (inet_pton(AF_INET, HOST_IP_ADDR, &hostAddr.sin_addr.s_addr) != 1)
    {
        fprintf(stderr, "Invalid host IP address: %s\n", HOST_IP_ADDR);
        close(sockFd);
        exit(EXIT_FAILURE);
    }

    socklen_t addrLen = sizeof(hostAddr);

    // 3. 绑定端口
    if (bind(sockFd, (const struct sockaddr *)&hostAddr, addrLen) < 0)
    {
        perror("bind failed");
        close(sockFd);
        exit(EXIT_FAILURE);
    }

    // 4. 接收数据
    char buff[BUFFER_SIZE] = {0};
    struct sockaddr_in srcAddr;
    memset(&srcAddr, 0, sizeof(srcAddr));
    socklen_t srcLen = sizeof(srcAddr);

    ssize_t recvBytes = recvfrom(sockFd, buff, BUFFER_SIZE, 0,
                                 (struct sockaddr *)&srcAddr, &srcLen);
    if (recvBytes < 0)
    {
        perror("recvfrom failed");
        close(sockFd);
        exit(EXIT_FAILURE);
    }

    // 5. 打印接收信息
    char src_ip_addr[16] = {0};
    if (inet_ntop(AF_INET, &(srcAddr.sin_addr.s_addr), src_ip_addr, sizeof(src_ip_addr)) == NULL)
    {
        perror("inet_ntop failed");
        close(sockFd);
        exit(EXIT_FAILURE);
    }

    printf("Data from %s:%u, Data: %s\n",
           src_ip_addr,
           ntohs(srcAddr.sin_port),
           buff);
    fflush(stdout);

    // 6. 关闭套接字
    if (close(sockFd) < 0)
    {
        perror("close socket failed");
        exit(EXIT_FAILURE);
    }

    return 0;
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值