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;
}
相关推荐
YuMiao2 天前
gstatic连接问题导致Google Gemini / Studio页面乱码或图标缺失问题
服务器·网络协议
Jony_5 天前
高可用移动网络连接
网络协议
chilix5 天前
Linux 跨网段路由转发配置
网络协议
DianSan_ERP7 天前
电商API接口全链路监控:构建坚不可摧的线上运维防线
大数据·运维·网络·人工智能·git·servlet
呉師傅7 天前
火狐浏览器报错配置文件缺失如何解决#操作技巧#
运维·网络·windows·电脑
gihigo19987 天前
基于TCP协议实现视频采集与通信
网络协议·tcp/ip·音视频
2501_946205527 天前
晶圆机器人双臂怎么选型?适配2-12寸晶圆的末端效应器有哪些?
服务器·网络·机器人
linux kernel7 天前
第七部分:高级IO
服务器·网络
数字护盾(和中)7 天前
BAS+ATT&CK:企业主动防御的黄金组合
服务器·网络·数据库
~远在太平洋~7 天前
Debian系统如何删除多余的kernel
linux·网络·debian