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 >> 更实用 |

相关推荐
weixin_6042366722 分钟前
华三 路由器 极简核心配置
运维·服务器·网络·h3c·h3c路由器
鹤落晴春1 小时前
【Linux复习】管理SELinux安全性
linux·运维·服务器
yz_aiks1 小时前
Linux Jar包配置Systemd自启动实战:从排查到配置全流程
linux·python·jar·自启动·systemd
AI智图坊1 小时前
多件装组合SKU图的批量生产效率分析:从PS手工到AI自动化的工作流改造
大数据·运维·人工智能·gpt·ai作画·自动化·aigc
bjzhang753 小时前
CentOS下安装MySQL详解
linux·mysql·centos
Jason_chen4 小时前
Linux 6.2 音频机制深度解析:AI驱动的低延迟音频与零信任音频安全架构
linux
下午写HelloWorld4 小时前
Linux系统及Ubuntu常用指令
linux·ubuntu·操作系统
云计算磊哥@6 小时前
运维开发宝典026-MySQL02数据库表操作
运维·数据库·运维开发
weixin_523185326 小时前
Collections.unmodifiableMap详解:真的不可修改吗?
java·linux·前端
天天进步20156 小时前
Tunnelto 源码解析 #9:控制服务器设计:Warp、WebSocket、Ping/Pong 与连接保活
运维·服务器·websocket