Socket 编程 UDP

目录

[UDP 网络编程](#UDP 网络编程)

[V1 版本 - echo server](#V1 版本 - echo server)

防拷贝头文件nocopy.hpp

网络封装类InetAddr.hpp

日志类log.hpp

UDP服务器类UdpServer.hpp

基础构成

类结构

核心方法

[Udp服务器源文件 UdpServerMain.cc](#Udp服务器源文件 UdpServerMain.cc)

Udp客户端源文件UdpClientMain.cc

运行结果


UDP 网络编程

V1 版本 - echo server

功能:简单的回显服务器和客户端代码

备注: 代码中会用到地址转换函数 .

防拷贝头文件nocopy.hpp

cpp 复制代码
#pragma once

// 防拷贝
class nocopy
{
public:
    nocopy() {}
    ~nocopy() {}
    nocopy(const nocopy &) = delete;
    const nocopy &operator=(const nocopy &) = delete;
};

我们设计这个类的目的是让我们的实现功能类通过继承它来做到单例模式,这样可以方便我们的代码编写。

网络封装类InetAddr.hpp

cpp 复制代码
#pragma once

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

// 封装网络地址类
class InetAddr
{
private:
    void ToHost(const struct sockaddr_in &addr)
    {
        _port = ntohs(addr.sin_port);// 将网络字节序的端口号转换为主机字节序
        _ip = inet_ntoa(addr.sin_addr);// 将网络字节序的32位IP地址转换为点分十进制字符串
    }

public:
    InetAddr(const struct sockaddr_in &addr)
        : _addr(addr)
    {
        ToHost(addr); // 将addr进行转换
    }
    std::string Ip()
    {
        return _ip;
    }
    uint16_t Port()
    {
        return _port;
    }
    ~InetAddr()
    {
    }

private:
    std::string _ip;
    uint16_t _port;
    struct sockaddr_in _addr;
};

这个类的作用是帮助我们管理网络信息的基本操作,帮助我们更加快速的获取其ip和端口的信息。

日志类log.hpp

这个类跟我们的多线程代码的代码是一模一样的,我们是直接拿过来用的,所以大家可以直接去看我多线程的那篇文章。

UDP服务器类UdpServer.hpp

cpp 复制代码
#pragma once

#include <iostream>
#include <unistd.h>
#include <string>
#include <cstring>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>

#include "nocopy.hpp"
#include "Log.hpp"
#include "InetAddr.hpp"

using namespace log_ns; // 打开日志的命名空间

static const int gsocketfd = -1;         // default socket套接字文件描述符
static const uint16_t glocalport = 8888; // default 端口号

enum
{
    SOCKET_ERROR = 1,
    BIND_ERROR
};

// UdpServer user("192.1.1.1",8888);
// 一般服务器主要是用来进行网络数据读取和写入的。IO的。
// 服务器 IO逻辑 和 业务逻辑 解耦
class UdpServer : public nocopy
{
public:
    // UdpServer(const std::string &localip, uint16_t localport = glocalport)
    UdpServer(uint16_t localport = glocalport)
        : _socketfd(gsocketfd), _localport(localport), _isrunning(false)
    {
    }

    void InitServer()
    {
        // 1.创建socket文件
        _socketfd = ::socket(AF_INET, SOCK_DGRAM, 0); // AF_INET网络通信方式创建,SOCK_DGRAM udp协议
        if (_socketfd < 0)
        {
            LOG(FATAL, "socket error\n");
            exit(SOCKET_ERROR);
        }
        LOG(DEBUG, "socket create success,_sockfd: %d\n", _socketfd);

        // 2.bind
        struct sockaddr_in local;
        memset(&local, 0, sizeof(local));
        local.sin_family = AF_INET;
        local.sin_port = htons(_localport);
        // local.sin_addr.s_addr = inet_addr(_localip.c_str());//需要4字节、需要网络序列的ip
        local.sin_addr.s_addr = INADDR_ANY; // 服务器端,进行任意ip地址绑定0

        int n = ::bind(_socketfd, (struct sockaddr *)&local, sizeof(local));
        if (n < 0)
        {
            LOG(FATAL, "bind error\n");
            exit(BIND_ERROR);
        }
        LOG(DEBUG, "socket bind success\n");
    }
    void Start()
    {
        _isrunning = true;
        char inbuffer[1024];
        while (_isrunning)
        {
            struct sockaddr_in peer;
            socklen_t len = sizeof(peer);
            ssize_t n = recvfrom(_socketfd, inbuffer, sizeof(inbuffer) - 1, 0, (struct sockaddr *)&peer, &len);
            if (n > 0)
            {
                InetAddr addr(peer);
                inbuffer[n] = 0;
                std::cout << "[" << addr.Ip() << ":" << addr.Port() << "]#" << inbuffer << std::endl;

                std::string echo_string = "[udp_server echo] #";
                echo_string += inbuffer;
                sendto(_socketfd, echo_string.c_str(), echo_string.size(), 0, (struct sockaddr *)&peer, len);
            }
            else
            {
                std::cout << "recvfrom , error" << std::endl;
            }
        }
    }

    ~UdpServer()
    {
        if (_socketfd > gsocketfd)
            ::close(_socketfd);
    }

private:
    int _socketfd;       // 套接字fd
    uint16_t _localport; // 端口号
    // std::string _localip; // ip
    bool _isrunning; // 服务端业务是否执行
};

这段代码定义了一个 UDP 服务器类UdpServer,主要功能是创建 UDP 套接字、绑定端口并处理客户端发送的数据。以下是详细说明:

基础构成
  • 包含了网络编程所需的系统头文件(如 socket、netinet 相关)和自定义组件(nocopy、Log、InetAddr)
  • 定义了基础常量:默认无效套接字描述符(gsocketfd)、默认端口(glocalport=8888)和错误码(SOCKET_ERROR、BIND_ERROR)

类结构

  • 继承自 nocopy 类,作用是禁止对象的拷贝和赋值操作
  • 私有成员包括:套接字描述符(_socketfd)、本地端口(_localport)、运行状态标志(_isrunning)

核心方法

  • 构造函数

    • 接受端口号参数(默认 8888)
    • 初始化成员变量:套接字为无效值,运行状态为 false
  • 初始化方法(InitServer)

    • 创建 UDP 套接字:使用 IPv4 协议(AF_INET)和数据报服务(SOCK_DGRAM)

    • 绑定操作:将套接字与本地端口绑定,使用 INADDR_ANY 表示绑定到所有可用网络接口

    • 函数调用解析

      • ::bind:使用作用域解析符::,表示调用的是全局的系统bind函数,而非类的成员函数
      • 第一个参数_socketfd:是之前通过socket函数创建的套接字文件描述符,标识要绑定的套接字
      • 第二个参数(struct sockaddr *)&local:将本地地址结构体指针转换为通用的sockaddr类型指针(因为系统调用要求这种类型)
      • 第三个参数sizeof(local):表示地址结构体的大小
    • 绑定的意义

      • 对于服务器程序,绑定操作是必需的,它指定了服务器监听的端口号
      • 绑定后,操作系统就知道将发送到该端口的网络数据交给这个套接字处理
      • 代码中使用INADDR_ANY作为 IP 地址,表示绑定到本机所有可用的网络接口
    • 返回值处理

      • 函数返回值n表示绑定操作的结果
      • 如果n < 0,说明绑定失败(可能是端口被占用、权限不足等原因)
      • 代码中对绑定失败的情况进行了处理:记录错误日志并退出程序
    • 任何步骤失败都会记录日志并退出程序

  • 启动方法(Start)

    • 启动服务器

      • _isrunning设为true,标记服务器进入运行状态
      • 创建一个 1024 字节的缓冲区inbuffer,用于存放接收的数据
    • 主循环处理

      • 通过while (_isrunning)进入无限循环,持续处理客户端请求
      • 定义struct sockaddr_in peer存储客户端的地址信息
      • socklen_t len = sizeof(peer)用于指定地址结构体的长度
    • 接收客户端数据

      • 调用recvfrom()函数接收数据:
        • 第一个参数_socketfd是服务器的套接字描述符
        • 第二个参数inbuffer是接收数据的缓冲区
        • 第三个参数sizeof(inbuffer) - 1指定最大接收字节数(预留一个字节给结束符)
        • 第四个参数0是默认标志位
        • 第五、六个参数用于获取发送方(客户端)的地址和地址长度
      • 返回值n表示实际接收的字节数
    • 处理接收到的数据

      • n > 0(成功接收数据)时:
        • InetAddr addr(peer)封装客户端地址,方便获取 IP 和端口
        • 在接收数据末尾添加\0,将其转为 C 风格字符串
        • 打印客户端信息和接收的内容,格式为[IP:端口]#数据
        • 构造回声字符串(在原数据前加[udp_server echo] #前缀)
        • 通过sendto()将处理后的字符串发回给客户端,使用获取到的客户端地址
    • 错误处理

      • recvfrom()返回值不大于 0 时,输出接收错误信息
  • 析构函数

    • 关闭套接字,释放系统资源

Udp服务器源文件 UdpServerMain.cc

cpp 复制代码
#include "UdpServer.hpp"

#include <memory>
// ./udpserver 8888
int main(int argc, char *argv[])
{
    // std::string ip = "127.0.0.1"; // 本主机 localhost
    if (argc != 2)
    {
        std::cerr << "Usage: " << argv[0] << "local-port" << std::endl;
        exit(0);
    }
    uint16_t port = std::stoi(argv[1]);

    EnableScreen();
    // std::unique_ptr<UdpServer> usvr = std::make_unique<UdpServer>(ip);
    std::unique_ptr<UdpServer> usvr = std::make_unique<UdpServer>(port);
    usvr->InitServer();
    usvr->Start();
    return 0;
}

这段代码是UDP服务器的主程序,功能是启动并运行服务器,流程如下:

  1. 引入必要头文件:包含 UDP 服务器类定义和智能指针库

  2. 命令行参数处理:

    • 要求必须传入一个参数(本地端口号)
    • 若参数数量不对,输出用法提示(如 "./udpserver 8888")并退出
    • 将参数转换为 16 位无符号整数类型的端口号
  3. 日志设置:

    • 调用 EnableScreen () 启用屏幕日志输出
  4. 服务器创建与运行:

    • 使用智能指针(unique_ptr)创建 UdpServer 对象,传入端口参数
    • 调用 InitServer () 初始化服务器(创建套接字、绑定端口等)
    • 调用 Start () 启动服务器主循环,开始处理客户端数据

程序特点:

  • 用智能指针管理服务器对象,自动释放资源,避免内存泄漏
  • 通过命令行参数指定端口,增强灵活性
  • 包含参数校验,提供清晰的使用指引

运行时需在命令行指定端口,如输入 "./udpserver 8888",即可在 8888 端口启动 UDP 服务器。

Udp客户端源文件UdpClientMain.cc

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

// 客户端未来一定要知道服务器的IP地址和端口号
//  ./udpclient 127.0.0.1 8888
int main(int argc, char *argv[])
{
    if (argc != 3)
    {
        std::cerr << "Usage: " << argv[0] << "server-ip server-port" << std::endl;
        exit(0);
    }

    std::string serverip = argv[1];
    uint16_t serverport = std::stoi(argv[2]);

    int sockfd = ::socket(AF_INET, SOCK_DGRAM, 0);
    if (sockfd < 0)
    {
        std::cerr << "create socket error" << std::endl;
        exit(1);
    }

    // client 的端口号,一般不让用户自己设定,而是让client OS随机选择?怎么选择,什么时候选择呢?
    // client 需要bind它自己的IP和端口,但是client不需要"显示"bind它自己的IP和端口
    // client 在首次向服务器发送数据的时候,OS会自动给client bind它自己的IP和端口

    struct sockaddr_in server;
    memset(&server, 0, sizeof(server));
    server.sin_family = AF_INET;
    server.sin_port = htons(serverport);
    server.sin_addr.s_addr = inet_addr(serverip.c_str());
    while (1)
    {
        std::string line;
        std::cout << "Please Enter# ";
        std::getline(std::cin, line);

        int n = sendto(sockfd, line.c_str(), line.size(), 0, (struct sockaddr *)&server, sizeof(server));
        if (n > 0)
        {
            struct sockaddr_in temp;
            socklen_t len = sizeof(temp);
            char buffer[1024];
            int m = recvfrom(sockfd, buffer, sizeof(buffer) - 1, 0, (struct sockaddr *)&temp, &len);
            if (m > 0)
            {
                buffer[m] = 0;
                std::cout << buffer << std::endl;
            }
            else
            {
                std::cout << "recvfrom error" << std::endl;
                break;
            }
        }
        else
        {
            std::cout << "sendto error" << std::endl;
            break;
        }
    }

    ::close(sockfd);
    return 0;
}

这段 UDP 客户端程序的核心功能是与指定的 UDP 服务器进行数据交互,以下是详细说明:

  1. 程序启动要求

    运行时需提供两个命令行参数:服务器 IP 地址和端口号,格式为./udpclient server-ip server-port。若参数不正确,会提示用法并退出。

  2. 套接字创建

    通过socket(AF_INET, SOCK_DGRAM, 0)创建 UDP 套接字:

    • AF_INET指定使用 IPv4 协议
    • SOCK_DGRAM表明这是一个 UDP 类型的套接字(无连接、面向数据报)
    • 创建失败时会输出错误信息并退出
  3. 服务器地址配置

    sockaddr_in结构体存储服务器网络信息:

    • 设置协议族为 IPv4
    • 端口号通过htons()转换为网络字节序
    • IP 地址通过inet_addr()将字符串形式转换为网络字节序的整数
  4. 客户端端口特性

    客户端无需显式绑定自身 IP 和端口,首次发送数据时,操作系统会自动分配本机 IP 和一个随机端口完成绑定。

  5. 数据交互流程

    程序进入无限循环:

    • 提示用户输入文本,通过getline获取输入内容
    • 使用sendto将数据发送到指定服务器
    • 发送成功后,通过recvfrom等待接收服务器的响应
    • 将接收到的响应数据以字符串形式输出
    • 若发送或接收失败,会提示错误并退出循环
  6. 资源释放

    循环结束后,通过close关闭套接字,释放资源。

该程序实现了基本的 UDP 客户端功能,适合作为简单网络通信的示例,展示了 UDP 协议无连接特性下的数据发送和接收过程。

运行结果

相关推荐
伍柏_14 小时前
2025.8.31基于UDP的网络聊天室项目
网络·网络协议·udp
m0_619731194 小时前
linux和RTOS架构区别
linux·运维·服务器
啊QQQQQ5 小时前
《websocketpp使用指北》
网络·网络协议
ayaya_mana5 小时前
oha:一款轻量级HTTP负载测试工具
网络协议·测试工具·http
情深不寿3175 小时前
序列化和反序列化
linux·网络·c++·tcp/ip
tuokuac5 小时前
如何确定虚拟机的IP
网络·网络协议·tcp/ip
青草地溪水旁6 小时前
linux修改权限命令chmod
linux·chmod
羑悻的小杀马特7 小时前
从Cgroups精准调控到LXC容器全流程操作:用pidstat/stress测试Cgroups限流,手把手玩转Ubuntu LXC容器全流程
linux·服务器·数据库·docker·lxc·cgroups
poemyang7 小时前
站在巨人的肩膀上:gRPC通过HTTP/2构建云原生时代的通信标准
网络协议·云原生·rpc·grpc·http2.0