Linux UDP Socket 编程入门:Echo Server/Client实现

一、UDP 套接字编程的核心步骤

|-----|--------------------------------------------------------|
| 角色 | 步骤 |
| 服务端 | 1. 创建 socket → 2. bind 绑定地址和端口 → 3. 循环 recvfrom/sendto |
| 客户端 | 1. 创建 socket → 2. (可选 bind)→ 3. sendto/recvfrom |

为什么客户端通常不需要显式 bind?

  • UDP 客户端发送数据时,操作系统会自动分配一个临时端口。

  • 显式 bind 反而可能造成端口冲突。

  • 当然,如果你需要固定客户端的端口(例如某些 P2P 场景),也可以手动 bind。

二、核心 API 速查

socket() --- 创建套接字

复制代码
#include <sys/types.h>
#include <sys/socket.h>

int socket(int domain, int type, int protocol);

|------------|--------------------------------------------------------------------------|
| 参数 | 说明 |
| domain | 地址家族(Address Family),常用 AF_INET(IPv4)、AF_INET6(IPv6)、AF_UNIX(本地通信) |
| type | 套接字类型,SOCK_DGRAM(UDP,数据报)、SOCK_STREAM(TCP,字节流) |
| protocol | 协议类型,通常填 0 让系统自动选择 |

返回值:成功返回文件描述符(非负整数),失败返回 -1

💡 关键理解:socket() 仅仅是打开了内核中的一个"文件",但此时这个文件还没有和任何 IP、端口关联,所以需要进行下一步 ------ bind。

bind() --- 绑定地址到套接字

复制代码
#include <sys/types.h>
#include <sys/socket.h>

int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);

|-----------|------------------------------------|
| 参数 | 说明 |
| sockfd | socket() 返回的文件描述符 |
| addr | 指向地址结构体的指针(实际传入 sockaddr_in 并强转) |
| addrlen | 地址结构体的长度 |

返回值:成功返回 0,失败返回 -1

💡 为什么需要 bind? 创建套接字只是打开了文件,没有设置到内核的网络协议栈中。bind 的作用就是将 IP + 端口 与这个套接字文件关联,让内核知道:收到这个 IP 和端口的数据包,应该交给这个套接字处理。

地址转换与字节序处理

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

|--------------------------------------------------|-----------------------------------------------|
| 函数 | 作用 |
| inet_addr(const char *cp) | 将点分十进制 IP 字符串 → 网络字节序的 4 字节整数 |
| inet_aton(const char *cp, struct in_addr *inp) | 同上,但结果存入结构体 |
| inet_ntoa(struct in_addr in) | 将网络字节序的 4 字节整数 → 点分十进制字符串 |
| htons(uint16_t hostshort) | Host TO Network Short,主机字节序 → 网络字节序(16位,用于端口) |
| htonl(uint32_t hostlong) | Host TO Network Long,主机字节序 → 网络字节序(32位,用于IP) |

⚠️ 为什么需要字节序转换?

网络通信是双方进程之间的通信。网络协议规定使用大端字节序,而主机可能是大端或小端(x86 是小端)。所以:凡是发送到网络的数据,必须转成网络字节序。

sockaddr_in 结构体解析

复制代码
/* 描述 Internet 套接字地址的结构体 */
struct sockaddr_in {
    sa_family_t    sin_family;  /* 地址家族: AF_INET */
    in_port_t      sin_port;    /* 端口号(网络字节序) */
    struct in_addr sin_addr;    /* IP 地址(网络字节序) */
    unsigned char  sin_zero[8]; /* 填充字段,使大小与 struct sockaddr 相同 */
};

struct in_addr {
    uint32_t s_addr;  /* 32 位 IPv4 地址,网络字节序 */
};

底层宏展开:

复制代码
#define SOCKADDR_COMMON(sa_prefix) \
    sa_family_t sa_prefix##family
// 展开后: sa_family_t sin_family

数据收发函数

复制代码
// UDP 接收(可获取对端地址)
ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags,
                 struct sockaddr *src_addr, socklen_t *addrlen);

// UDP 发送(需要指定对端地址)
ssize_t sendto(int sockfd, const void *buf, size_t len, int flags,
               const struct sockaddr *dest_addr, socklen_t addrlen);

|--------------------------|------------------|
| 参数 | 说明 |
| src_addr / dest_addr | 对端地址信息(输入/输出型参数) |
| addrlen | 地址长度(既是输入又是输出) |
| flags | 默认为 0,阻塞式 I/O |

💡 阻塞式 I/O:如果对方不发数据,recvfrom 会一直阻塞等待,等同于 scanf 的行为。

三、UDP Server 实现

复制代码
#pragma once

#include <iostream>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <string.h>
#include "Log.hpp"

using namespace LogModule;

const int dafultfd = -1;
class UdpServer
{
public:
    UdpServer(uint16_t port)
        : _sockfd(dafultfd),
          _port(port),
          _isrunning(false)

    {};

    // 初始化
    void Init()
    {
        // 1.创建套接字
        _sockfd = socket(AF_INET, SOCK_DGRAM, 0);
        if (_sockfd < 0)
        {
            LOG(LogLevel::FATAL) << "socket error!";
            exit(1); // 进程退出
        }
        LOG(LogLevel::INFO) << "socket success sockfd: " << _sockfd;

        // 2.绑定socket信息 , ip和端口,(ip比较特殊,后续解释)
        // 2.1 填充sockaddr_in结构体
        struct sockaddr_in local;
        bzero(&local, sizeof(local));
        local.sin_family = AF_INET;
        // 我会不会把我的IP地址和端口号发给对方?
        // IP信息和端口信息,一定要发送到网络
        // 本地格式->网络序列
        local.sin_port = htons(_port);
        // IP也是如此,1.IP转成4字节  2.4字节转成网络序列
        //->in_addr_t inet_addr(const char *cp);
        //local.sin_addr.s_addr = inet_addr(_ip.c_str());
        local.sin_addr.s_addr = INADDR_ANY  ;
        int n = bind(_sockfd, (struct sockaddr *)&local, sizeof(local));
        if (n < 0)
        {
            LOG(LogLevel::FATAL) << "bind error";
            exit(2);
        } 
        LOG(LogLevel::INFO) << "bind success, sockfd : " << _sockfd;
    }
    void Start()
    {
        // udp不面向连接,启动的时候,一直读写/收发
        _isrunning = true;
        while (_isrunning)
        {
            char buffer[1024];
            struct sockaddr_in peer;
            socklen_t len = sizeof(peer);
            // 1. 收消息
            ssize_t s = recvfrom(_sockfd, buffer, sizeof(buffer) - 1, 0, (struct sockaddr *)&peer, &len);
            if (s > 0)
            {
                buffer[s] = 0;  
                LOG(LogLevel::DEBUG) << "buffer: " << buffer; // 1.消息内容 2.谁发的???
                // 2. 发消息
                std::string echo_string  = "server say@ ";
                echo_string += buffer;
                sendto(_sockfd, echo_string.c_str(), echo_string.size(), 0, (struct sockaddr *)&peer, len);
            }
        }
    }

    ~UdpServer() {};

private:
    int _sockfd;
    uint16_t _port;
    //std::string _ip; // 用的是字符串风格,点分十进制
    bool _isrunning;
};

主函数入口

复制代码
#include <iostream>
#include "UdpServer.hpp"
#include <memory>


// ./udpserver port
int main(int argc,char* argv[])
{
    if(argc != 2)
    {
       std::cerr << "Usage: " << argv[0] << " port" << std::endl; 
    }

    uint16_t port = std::stoi(argv[1]);

    Enable_Console_Log_Stratege();

    std::unique_ptr<UdpServer> usvr = std::make_unique<UdpServer>(port);
    usvr->Init();
    usvr->Start();
    return 0;
}

四、UDP Client 完整实现

复制代码
#include <iostream>
#include "Log.hpp"
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <string>
#include <cstring>

using namespace LogModule;
// ./udpclient  server_ip server_port
int main(int argc, char *argv[])
{
    if (argc != 3)
    {
        LOG(LogLevel::DEBUG) << "Usage: " << argv[0] << " ip port";
        return 1;
    }
    std::string server_ip = argv[1];
    uint16_t server_port = std::stoi(argv[2]);

    // 1.创建套接字
    int sockfd = socket(AF_INET, SOCK_DGRAM, 0);
    if (sockfd < 0)
    {
        std::cerr << "socket error" << std::endl;
        return 2;
    }

    // 2.本地的ip和端口号是什么,需要和上面的'文件'关联吗?
    // 问题:客户端是否需要bind?client是或否需要显式的bind?为什么?
    // 首次发送消息,OS会自动给client进行bind,OS知道Ip,端口号采用随机端口号的方式~

    // 填写服务器信息
    struct sockaddr_in server;
    memset(&server, 0, sizeof(server));
    server.sin_family = AF_INET;
    server.sin_port = htons(server_port);
    server.sin_addr.s_addr = inet_addr(server_ip.c_str());

    while (true)
    {
        std::string input;
        std::cout << "Please Enter# ";
        std::getline(std::cin, input);

        int n = sendto(sockfd, input.c_str(), input.size(), 0, (struct sockaddr *)&server, sizeof(server));
        (void)n;

        char buffer[1024];
        struct sockaddr_in peer;
        socklen_t len = sizeof(peer);
        int m = recvfrom(sockfd, buffer, sizeof(buffer) - 1, 0, (struct sockaddr *)&peer, &len);
        if (m > 0)
        {
            buffer[m] = 0;
            std::cout << buffer << std::endl;
        }
    }

    return 0;
}

五、关键问题解析

Q1: 客户端是否需要显式 bind?

答案:不需要。

|------------|--------------|--------------------------------------------------------|
| 角色 | 是否需要 bind | 原因 |
| Server | ✅ 必须显式 bind | 需要固定 IP + 端口,让客户端知道"去哪里找" |
| Client | ❌ 不需要显式 bind | 首次发送消息时, OS 自动进行隐式 bind。系统会自动分配临时端口,避免端口冲突 |

💡 核心原理:客户端是主动发起通信的一方。当第一次调用 sendto() 时,如果内核发现该套接字尚未绑定,会自动为其分配一个临时端口(Ephemeral Port,通常范围 1024-65535),并绑定客户端的 IP 地址。

Q2: 为什么 IP 地址要转换两次?

实际流程:

  1. inet_addr() 内部完成:字符串 → 4 字节整数 → 网络字节序

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

Q3: UDP 是全双工的吗?

是的! UDP 套接字既可以读也可以写,收发互不干扰。

复制代码
// 同一个 sockfd,既可以 recvfrom 也可以
sendto sendto(sockfd, ...); // 发
recvfrom(sockfd, ...); // 收

Q4: 服务器是"死循环"

普通 C/C++ 程序:启动 → 运行 → 结束 → 退出

服务器程序: 启动 → 7×24 小时一直运行(死循环收发数据)

Q5: 为什么客户端需要填写 server 地址?

**"你要发消息,**你需要知道消息发送给谁!!!"

客户端必须知道服务器的 IP 和端口 才能发送数据。通常有两种方式:

  1. 命令行参数传入(如本例:./udpclient 127.0.0.1 8080

  2. 配置文件 / 内置在 App 中**(客户端和服务器是同一家公司写的,App 以一定形式内置了服务器地址)**

六、数据流图示

|----------------------|-------------------------------------|
| 要点 | 说明 |
| memset/bzero 清零 | 避免结构体填充字段的干扰数据 |
| sizeof(buffer) - 1 | 为字符串结束符 \0 预留空间 |
| (void)n | 暂时忽略返回值,避免编译器警告,生产环境应检查错误 |
| peer 作为输出参数 | recvfrom 会填充实际发送方的地址信息 |
| std::getline | 读取整行输入(含空格),比 std::cin >> 更实用 |

相关推荐
中微子1 小时前
突然爆火的Warp 终端,开源1天破 4w Stars
linux·人工智能·开源
pengyi8710152 小时前
共享 IP 池多人使用 分层权限与配额管理方案
运维·服务器·网络
计算机安禾2 小时前
【Linux从入门到精通】第33篇:数据库MySQL/MariaDB安装与基础调优
linux·数据库·mysql
搞科研的小刘选手2 小时前
【高届数传感机电会议】第十二届传感器、机电一体化和自动化系统国际学术研讨会(ISSMAS 2026)
运维·人工智能·自动化·控制·传感器·传感·机电
楼兰公子2 小时前
读取rpi摄像头
linux·服务器·算法
李景琰2 小时前
Debian12安装配置Mqtt之EMQX
linux·运维·服务器
SimLine芯见2 小时前
专为空管环境打造的KVM切换器,满足主备自动化高速无缝切换需求
运维·自动化
测试员周周2 小时前
【AI测试系统】第1篇:LangGraph 实战:用 State Graph 搭建 AI测试流水线(4 步编排 + RAG 增强 + 完整代码)
linux·windows·python·功能测试·microsoft·单元测试·多轮对话
不做无法实现的梦~2 小时前
PX4 机载电脑 Linux 环境安装、串口、网络、ROS 完整配置
linux·运维·网络