【Linux】UDP套接字---执行指令

一、UDP 协议基础

UDP:用户数据报协议,无连接、不可靠、保留报文边界

  1. 无连接:通信双方不需要握手,发数据直接扔出去;类似发短信,不需要拨号建立会话。
  2. 不可靠 :内核不做确认、重传,报文可能丢包、乱序、重复;可靠性需要业务应用层自己实现
  3. 保留数据报边界 :一次 recvfrom 读到的一定是对方一次 sendto 发送完整报文,不会拆包粘包。
  4. UDP 头部固定只有 8 字节,开销很小,适合直播、游戏、DNS 等低延迟场景。

网络通信本质进程间通信;IP 定位主机,端口号定位主机上面具体进程。

报文五元组源 IP、源端口、目的 IP、目的端口、协议(UDP/TCP),每个 UDP 报文一定携带这 5 个信息。

二、UDP 服务端完整工作流程

1. 核心系统调用

  1. socket() 创建套接字

    cpp 复制代码
    // AF_INET:IPv4 协议族;SOCK_DGRAM:UDP 数据报;protocol 填 0 自动选择 UDP
    int sockfd = socket(AF_INET, SOCK_DGRAM, 0);
    • 返回值 :成功返回文件描述符,失败返回 -1
    • 本质 :在内核开辟套接字资源,相当于打开网卡的"文件",后续所有收发都依靠这个 fd。此时套接字还没有 IP、端口信息
  2. bind() 绑定:给套接字设置固定 IP + 端口

    服务器必须显式 bind,客户端不建议手动 bind。服务器端口必须固定公开,客户端端口由操作系统自动分配临时端口。

    cpp 复制代码
    struct sockaddr_in local;
    bzero(&local, sizeof(local));
    local.sin_family = AF_INET;
    local.sin_port = htons(port); // 主机字节序 → 网络字节序
    local.sin_addr.s_addr = INADDR_ANY; // 0.0.0.0,任意地址绑定
    int ret = bind(sockfd, (struct sockaddr *)&local, sizeof(local));
    • bind 的本质 :把用户栈上填充好的 sockaddr_in 地址信息拷贝到内核套接字对象中。
    • struct sockaddr通用套接字结构体 ,是接口的参数;实际 IPv4 填充 sockaddr_in,传参强制类型转换。

    INADDR_ANY(0.0.0.0) 任意地址绑定

    云服务器不要绑定公网 IP ,会报错错误码 99 Cannot assign requested address;云服务器公网 IP 是虚拟化的,不是本机网卡真实 IP。

    0.0.0.0 含义:本机所有网卡收到的、目标端口匹配的 UDP 报文,全部交付这个进程;机器有多块网卡多个 IP,全部都可以接收报文。

    虚拟机环境可以绑定具体 IP;127.0.0.1 本地回环,只能本机内部通信,数据包不会走物理网卡,用于本地测试。

    端口范围

    • 0‑1023:系统特权端口,普通用户不能绑定,需要 root;被知名协议占用(80、443、22、3306)
    • 1024‑65535:用户可用端口,写服务器优先选这个区间。
    • 一个端口同一时刻只能被一个进程绑定;一个进程可以 bind 多个不同端口。
  3. recvfrom() 接收 UDP 报文

    UDP 没有 read,因为 read 面向字节流(TCP),UDP 必须使用 recvfrom不但拿到数据,还拿到对端客户端地址信息(IP + 端口),后续回复客户端就靠这个地址。

    cpp 复制代码
    char buf[1024];
    struct sockaddr_in peer;
    socklen_t peer_len = sizeof(peer);
    ssize_t n = recvfrom(sockfd, buf, sizeof(buf)-1, 0, (struct sockaddr *)&peer, &peer_len);

    参数说明

    • 最后两个参数是输出参数:内核把发送方的 IP、端口填进 peer 结构体。
    • 返回 n > 0:读到 n 字节;n = -1 出错。

    读到数据之后手动 buf[n] = '\0',当做字符串处理,UDP 本身不携带字符串结束符。

  4. sendto() 发送 UDP 报文

    发送的时候必须指定目标对端地址;回显服务器就把 recvfrom 拿到的 peer 直接传给 sendto。

    cpp 复制代码
    ssize_t m = sendto(sockfd, data, len, 0, (struct sockaddr *)&peer, peer_len);

    UDP 服务器整体循环逻辑

    plaintext 复制代码
    socket → bind → while(true) { recvfrom → 业务处理 → sendto }

    UDP 不需要 listen、accept!!这是 TCP 专属接口。

三、UDP 客户端流程

cpp 复制代码
socket(AF_INET, SOCK_DGRAM, 0); // 不需要 bind!!操作系统第一次 sendto 的时候,自动分配临时源端口
while(true) {
    用户输入数据;
    sendto(发给服务器地址);
    recvfrom(接收服务器应答);
}
close(sockfd);

为什么客户端不手动 bind?

客户端如果写死 bind 固定端口:同一台机器开多个客户端就会端口冲突,程序启动失败。

操作系统在客户端第一次调用 sendto 的时候,自动分配 1024‑65535 空闲临时端口,完成隐式 bind。

客户端依然需要源 IP + 源端口,只是交给 OS 自动管理,不需要程序员写代码。

客户端必须知道服务器 IP 和端口,作为 sendto 的目标地址;服务器从 recvfrom 获取客户端地址。

四、字节序与 IP 地址转换函数

网络统一使用大端字节序;主机可能是小端(x86),必须做转换。

  1. 端口(16bit)

    • htons():host‑to‑network‑short,主机转网络
    • ntohs():network‑to‑host‑short,网络转主机
  2. IP 地址转换

    函数 作用 注意事项
    inet_addr() 点分字符串 → 网络序 32 位整数 过时,错误返回和 255.255.255.255 冲突
    inet_ntoa() 网络序 in_addr → 点分字符串 非线程安全!内部静态缓冲区,多线程会覆盖结果,APUE 明确警告不推荐
    inet_pton() 字符串 IP → 网络二进制 线程安全,调用者提供缓冲区,支持 IPv4/IPv6,生产环境首选
    inet_ntop() 网络二进制 → 字符串 IP 线程安全,调用方提供输出缓冲区,替代 inet_ntoa

    说明:inet_ntoa 内部是静态全局缓冲区,多线程并发调用会数据覆盖;开发优先用 inet_pton / inet_ntop

五、代码

Udpclient.cc

复制代码
#include"Common.hpp"

const int size = 1024;

void Usage()
{
    std::cout << "UdpClientSrv serverip serverport\n";
}
int main(int argc, char* argv[])
{
    if(argc != 3)
    {
        Usage();
        exit(0);
    }

    //创建套接字
    int sockfd = socket(AF_INET, SOCK_DGRAM, 0);
    if(sockfd < 0)
    {
        std::cerr << "create sockfd error\n";
        exit(1);
    }

    std::cout << "sockfd : " << sockfd << std::endl;
    //客户端的套接字绑定端口号在第一次向服务器发起请求时,由操作系统自动绑定
    
    //客户端发送数据&&接收数据(可能多个服务器回复同一个客户端)
    std::string msg;
    struct sockaddr_in server;
    bzero(&server, sizeof(server));
    server.sin_addr.s_addr = inet_addr(argv[1]);
    server.sin_family = AF_INET;
    server.sin_port = htons(std::stoi(argv[2]));
    socklen_t len = sizeof(server);
    while(true)
    {
        std::cout << "Please Enter@: ";
        //发送数据
        std::cin >> msg;
        std::cout << msg <<std::endl;
        int m = sendto(sockfd, msg.c_str(), msg.size(), 0, (const sockaddr*)&server, len);
        std::cout << "send " << m << "data\n";
        //接收数据

        char buffer[size];
        struct sockaddr_in tmp;
        bzero(&tmp, sizeof(tmp));
        socklen_t len_;
        int n = recvfrom(sockfd, buffer, size-1, 0, (sockaddr*)&tmp, &len_);
        if(n > 0) buffer[n] = 0;

        std::cout << buffer << std::endl;
    }
}

Udpserver.hpp

复制代码
#include "Common.hpp"

const uint16_t defaultport = 8080;
const int size = 1024;

using func_t = std::function<std::string(const std::string&)>;

class UdpEchoSrv
{
private:
    uint16_t _port;
    int _sockfd;

public:
    UdpEchoSrv(uint16_t port = defaultport) : _port(port), _sockfd(-1) {}
    void Init()
    {
        // 创建套接字
        _sockfd = socket(AF_INET, SOCK_DGRAM, 0);
        if (_sockfd < 0)
            lg(Fatal, "create sockfd error");

        // 绑定端口号
        struct sockaddr_in server;
        int len = sizeof(server);

        bzero(&server, len);
        server.sin_family = AF_INET;
        server.sin_port = htons(_port);
        server.sin_addr.s_addr = INADDR_ANY;

        int res = bind(_sockfd, (const sockaddr *)&server, len);
        if (res < 0)
            {
                lg(Warning, "bind error");
                exit(2);
            }
        lg(Info, "bind success");
    }
    void Run(func_t func)
    {
        char buffer[size];
        
        while (true)
        {
            sockaddr_in client;
            socklen_t len = sizeof(client);
            // 接收数据
            // std::cout << "start recv data\n";
            ssize_t ret = recvfrom(_sockfd, buffer, sizeof(buffer) - 1, 0, (sockaddr *)&client, &len);
            if (ret < 0)
            {
                lg(Warning, "recvfrom error: %s", strerror(errno));
                break;
            }
            buffer[ret] = 0;
            // std::cout << buffer << std::endl;

            // 处理数据---echo,服务器收到数据打印出来,并且给客户端发送数据
            uint16_t clientport = ntohs(client.sin_port);
            std::string clientip = inet_ntoa(client.sin_addr);
            // lg(Info, "server get a request from %s:%d## %s", clientip.c_str(), clientport, buffer);
            printf("%s:%d  say : %s\n", clientip.c_str(), clientport, buffer);

            // std::string str = "server handled your send message: ";                                      ####什么都不处理就返回的版本

            std::string str = func(buffer);                                     //                          ##执行shell命令的版本
            str += buffer;
            std::cout << str << std::endl;

            // func(buffer);

            // ssize_t re = sendto(_sockfd, str.c_str(), str.size(), 0, (const sockaddr *)&client, len);
            // (void)re;
        }
    }
    ~UdpEchoSrv()
    {
        close(_sockfd);
    }
};

Udpserver.cc

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

void Useage()
{
    std::cout << "Udpserver port(1024+)\n";
}

std::string Handle(const std::string& str)
{

    ///                         执行shell命令
    char buffer[4096];
    FILE* fd = popen(str.c_str(), "r"); //以读的方式,读取结果,之后的逻辑再发送给客户端

    std::string res;
    while(true)
    {
        char* ch = fgets(buffer, sizeof(buffer), fd);
        if(ch == nullptr) break;     //当读到nullptr时,结束读取

        res += ch;
    }
    pclose(fd);

    return res;

    ///                    程序替换
    // signal(SIGCHLD, 0);
    // pid_t id = fork();
    // if(id == 0)
    // {
    //     execlp("sky", "sky", str.c_str(), NULL);
        
    // }
   
    // return "";
}

int main(int argc, char* argv[])
{
    if(argc != 2)
    {
        Useage();
        exit(0);
    }
    uint16_t port = std::stoi(argv[1]);
    std::unique_ptr<UdpEchoSrv> uptr(new UdpEchoSrv(port));
    uptr->Init();
    uptr->Run(Handle);

    return 0;
}

Log.hpp

复制代码
#include<string>
#include<unistd.h>
enum level
{
    Info,
    Warning,
    Error,
    Fatal
};

#define lg(level, format, ...)                                \
    do                                                        \
    {                                                         \
        std::string str;                                      \
        switch (level)                                        \
        {                                                     \
        case 0:                                               \
            str = "Info";                                     \
            break;                                            \
        case 1:                                               \
            str = "Warning";                                  \
            break;                                            \
        case 2:                                               \
            str = "Error";                                    \
            break;                                            \
        case 3:                                               \
            str = "Fatal";                                    \
            break;                                            \
        }                                                     \
        time_t t = time(nullptr);                             \
        struct tm *ltm = localtime(&t);                       \
        char tmp[32] = {0};                                   \
        strftime(tmp, 31, "%H:%M:%S", ltm);                   \
        fprintf(stdout, "%s->"                                \
                        "[%s %s:%d]" format "\n",             \
                str.c_str(), tmp, __FILE__, __LINE__, ##__VA_ARGS__); \
    }while (0);
相关推荐
ITyunwei09871 小时前
SSL 证书过期排查实战:openssl + Nginx 4步从发现到续签
运维·网络
Fluxproxy2 小时前
Python请求玄学根治:彻底解决脚本间歇性超时、断连、假死问题
网络·python·安全
北风toto2 小时前
TCP/IP网络基础知识
网络·网络协议·tcp/ip
zjxtxdy2 小时前
stm32usart通信接口
网络·笔记
赖赖-3 小时前
畅想视界巨匠P240G vs 华为擎云V540:谁更适合窗口场景?
网络·人工智能·电脑·边缘计算
那年窗外下的雪.3 小时前
VXLAN EVPN 分层排障:从 VTEP 可达、ARP/MAC 到 MAC Mobility
服务器·前端·网络·数据库·spine
AI风控技术指南3 小时前
大模型安全围栏厂商评估:以数美科技为例拆解能力、架构和 POC 指标
大数据·网络·人工智能
ZJU_统一阿萨姆3 小时前
【算子开发】全局内存访问与合并访存
java·服务器·网络·人工智能·语言模型
来让爷抱一个4 小时前
拯救我的“烂尾“项目:我用MonkeyCode把五个AI热点实践了个遍
网络·数据库·人工智能·prompt·ai编程