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;
}
相关推荐
Dawn-bit31 分钟前
Linux网络进阶:TCP三次握手和四次挥手与TCP连接状态
linux·服务器·网络·tcp/ip·云计算·智能路由器
TheBestRucy1 小时前
Python 九阳神功:从零筑基到线程飞升
服务器·开发语言·网络·人工智能·python
xier_ran2 小时前
【infra之路】GPU 存储层次总结:L1 / L2 / HBM
java·网络·数据库
小网洞2 小时前
免费WPA无线握手包在线分析跑包网站
网络·网络安全·密码学·wps
@insist1234 小时前
系统集成项目管理工程师-信息和文档管理
网络·数据库·软考·系统集成项目管理工程师·软考中项·软件水平考试
极客互动API4 小时前
企业微信 iPad 协议服务搭建与 AI 回调实战
网络·汇编·人工智能·微信·企业微信·ipad
QH139292318804 小时前
Keysight N5183B N5183B 微波模拟信号发生器
网络·单片机·嵌入式硬件·集成测试·信息与通信
LabVIEW开发4 小时前
基于原始套接字实现LabVIEW网络Ping检测的编程方法
网络·labview·labview知识·labview功能·labview程序
东莞和裕包装4 小时前
如何管控广州定制纸箱生产中的啤切精度与印刷色差质量问题
大数据·运维·网络·人工智能
Zguigo6 小时前
lesson19-23计算机网络数据链路层完全指南:从帧封装到滑动窗口协议
网络·计算机网络