UDP原理和极简socket编程demo

一、通信基石:端口和五元组

什么是五元组

在TCP/IP协议中,标识一个唯一的通信连接需要五个元素:

  • 源IP
  • 源端口号
  • 目的IP
  • 目的端口号
  • 协议号(如TCP或UDP)

在Linux中,可以使用netstat -n命令查看这些信息

端口号划分

  • 知名端口号0~1023
  • 私有端口号:1024~65535

一个进程可以绑定多个端口号吗?

可以,一个进程可以创建多个Socket文件描述符,每个Socket都可以绑定一个独立的端口

一个端口号可以被多个进程绑定吗?

默认不行,有特例

  • 若进程A绑定了8080,进程B再绑定8080,OS会抛出"地址被使用"错误
  • 特例:如果使用了setsockopt设置了SO_REUSEADDRSO_REUSEPORT

二、 UDP报文结构

UDP协议头格式

UDP头部固定8个字节,协议就是结构体:

在内核视角下:

cpp 复制代码
struct udphdr {
    uint16_t source;  // 16位源端口号
    uint16_t dest;    // 16位目的端口号
    uint16_t len;     // UDP长度(报头+数据)
    uint16_t check;   // 校验和
};
字段 长度 作用
源端口号 16位 标识发送进程
目的端口号 16位 标识接收进程 (用于分用)
UDP长度 16位 表示整个数据报(首部+数据)的最大长度
UDP校验和 16位 如果校验和出错,数据会被直接丢弃

怎么解包?

OS读取报文的前8个字节,剩下的内容就是有效载荷

怎么分用?

基于16位目的端口号,内核根据这个端口号,把数据推送绑定了该端口的应用层进程的接收缓冲区当中

UDP的痛点:16位长度的限制

UDP长度只有16位,则为216−1=655352^{16} - 1= 65535216−1=65535,这意味着一个UDP包(包含头部)最大只能是64KB

如果UDP要上传大于64KB的数据怎么办?

必须在应用层手动分包,程序员自己实现把大文件切成小块,发送过去,再接收拼装

UDP特点

  • 面向数据报

    • 定义:应用层交给UDP多长的报文,UDP原样转发,不拆分,也不合并
  • 缓冲区机制:

    • 发送缓冲区: UDP没有真正意义上的缓冲区,调用sendto会直接交给内核,内核拷贝给网卡驱动

    • 接收缓冲区: UDP有接收缓冲区

      • 接收缓冲区不能保证收到数据包的顺序和发送顺序一致
      • 丢包:如果接收缓冲区满了,新到达的数据就会直接被丢弃
    • 怎么做到数据转发?

      应用层调用sendto发数据时:

      1. 数据从应用层拷贝到传输层
      2. 内核在数据前添加UDP报头
      3. 继续交给下层处理

总结:

  • 面向数据报:上层给我什么样,我就加个报头,不做任何处理向下层交付
  • 无连接:知道对方的IP和端口号就直接传输,无需连接
  • 不可靠:没有确认机制,没有重传机制,如果因为网络故障该段无法发到对方, UDP协议也不会给应用层返回任何错误信息

三、 UDP Socket编程demo

服务端

cpp 复制代码
#include <iostream>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <cstring>
#include <unistd.h>

#define PORT 8080
#define BUFFER_SIZE 1024

int main(){
    // 1. 创建套接字 socket
    // AF_INET: IPv4
    // SOCK_DGRAM: UDP数据报类型
    // 0: 默认
    int sockfd = ::socket(AF_INET, SOCK_DGRAM, 0);
    if(sockfd < 0){
        std::cerr << "socket create err" << errno << std::endl;
        return -1;
    }

    // 2. 填充服务器地址结构体
    struct sockaddr_in server_addr;
    memset(&server_addr, 0, sizeof(server_addr));   // 清零,防止垃圾数据
    server_addr.sin_family = AF_INET;
    server_addr.sin_addr.s_addr = INADDR_ANY;   // 接收任意网卡的连接
    server_addr.sin_port = ::htons(PORT);   // 端口转网络字节序

    // 3. 绑定 bind
    // 必须绑定,否则客户端不知道发给谁
    if(::bind(sockfd, (struct sockaddr*)&server_addr, sizeof(server_addr)) < 0){
        std::cerr << "bind err" << errno << std::endl;
        return -1;
    }
    std::cout << "UDP Server is running on port " << PORT << "..." << std::endl;

    char buffer[BUFFER_SIZE];
    struct sockaddr_in client_addr;         // 必须保护客户端的信息
    socklen_t len = sizeof(client_addr);    // 必须初始化为结构体的大小
    while(1){
        // 4. 接收数据 recvfrom
        // 最后两个参数是输出型参数,内核会把"谁发的"填进去
        int n = ::recvfrom(sockfd, (char*)buffer, BUFFER_SIZE,
                           0, (struct sockaddr*)&client_addr, &len);
        buffer[n] = '\0';
        std::cout << "Client : " << buffer << std::endl;

        // 5. 发送回复 sendto
        const char* hello = "hello from server";
        ::sendto(sockfd, (const char*)hello, strlen(hello),
                 0, (const struct sockaddr*)&client_addr, len);
    }
    close(sockfd);

    return 0;
}

客户端

cpp 复制代码
#include <iostream>
#include <cstring>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>

#define PORT 8080
#define SERVER_IP "127.0.0.1" // 本地回环测试

int main() {
    // 1. 创建套接字
    int sockfd = socket(AF_INET, SOCK_DGRAM, 0);
    if (sockfd < 0) {
        perror("socket creation failed");
        return -1;
    }

    // 2. 填充目标(服务器)地址信息
    struct sockaddr_in servaddr;
    memset(&servaddr, 0, sizeof(servaddr));
    servaddr.sin_family = AF_INET;
    servaddr.sin_port = htons(PORT);
    // 将字符串IP转换为网络字节序
    servaddr.sin_addr.s_addr = inet_addr(SERVER_IP); 

    // 3. 直接发送 (Sendto)
    const char *hello = "Hello from Client";
    // 注意:UDP是无连接的,必须在发的时候指定发给谁 (&servaddr)
    sendto(sockfd, (const char *)hello, strlen(hello), 
           0, (const struct sockaddr *)&servaddr, sizeof(servaddr));
    
    std::cout << "Message sent." << std::endl;

    // 4. 接收回复 (Recvfrom)
    char buffer[1024];
    socklen_t len = sizeof(servaddr);
    
    // 这里也可以传 NULL,如果你不关心是谁回你的(但在UDP中最好校验源地址)
    int n = recvfrom(sockfd, (char *)buffer, 1024, 
                     0, (struct sockaddr *)&servaddr, &len);
    
    buffer[n] = '\0';
    std::cout << "Server : " << buffer << std::endl;

    close(sockfd);
    return 0;
}
相关推荐
安科士andxe2 小时前
深入解析|安科士1.25G CWDM SFP光模块核心技术,破解中长距离传输痛点
服务器·网络·5g
YJlio5 小时前
1.7 通过 Sysinternals Live 在线运行工具:不下载也能用的“云端工具箱”
c语言·网络·python·数码相机·ios·django·iphone
CTRA王大大5 小时前
【网络】FRP实战之frpc全套配置 - fnos飞牛os内网穿透(全网最通俗易懂)
网络
testpassportcn6 小时前
AWS DOP-C02 認證完整解析|AWS DevOps Engineer Professional 考試
网络·学习·改行学it
通信大师7 小时前
深度解析PCC策略计费控制:核心网产品与应用价值
运维·服务器·网络·5g
Tony Bai8 小时前
告别 Flaky Tests:Go 官方拟引入 testing/nettest,重塑内存网络测试标准
开发语言·网络·后端·golang·php
消失的旧时光-19438 小时前
从 0 开始理解 RPC —— 后端工程师扫盲版
网络·网络协议·rpc
叫我龙翔9 小时前
【计网】从零开始掌握序列化 --- JSON实现协议 + 设计 传输\会话\应用 三层结构
服务器·网络·c++·json
“αβ”9 小时前
网络层协议 -- ICMP协议
linux·服务器·网络·网络协议·icmp·traceroute·ping
袁小皮皮不皮11 小时前
数据通信18-网络管理与运维
运维·服务器·网络·网络协议·智能路由器