【Linux编程】C++ UDP的UdpClient 类详解与网络通信实现(三))

UdpClient 类详解与网络通信实现

在现代网络编程中,UDP(用户数据报协议)因其简单和高效而被广泛应用于各种场景,如视频流、在线游戏和实时通信等。UdpClient 类正是基于这种协议实现的网络通信类,它封装了UDP通信的核心功能,包括数据的发送和接收、套接字的管理以及事件驱动的网络事件处理。本文将详细解析UdpClient类的实现,探讨其主要函数的作用和代码逻辑。

1. UdpClient 类构造与析构

UdpClient类的构造函数UdpClient::UdpClient()初始化了套接字文件描述符socket_fd和epoll文件描述符epoll_fd,并将它们设置为0。同时,它还清空了本地地址local_addr、远程地址remote_addr、组播地址multicast_addr和广播地址broadcast_addr。这些地址结构体是sockaddr_in类型,用于存储网络地址信息。

UdpClient::UdpClient() : socket_fd(0), epoll_fd(0)
{
    memset(&local_addr, 0, sizeof(local_addr));
    memset(&remote_addr, 0, sizeof(remote_addr));
    memset(&multicast_addr, 0, sizeof(multicast_addr));
    memset(&broadcast_addr, 0, sizeof(broadcast_addr));
}

析构函数UdpClient::~UdpClient()目前为空,这意味着如果需要进行资源释放或清理工作,应在其他成员函数中实现。

2. 绑定与组播设置

UdpClient::Bind(const std::string _host, const int _port)函数用于将UDP客户端绑定到指定的主机和端口。它通过IPEndPoint类(未在代码中给出,假设其存在)创建一个端点对象,并从中获取sockaddr_in结构体,然后将其赋值给local_addr

void UdpClient::Bind(const std::string _host, const int _port)
{
    IPEndPoint endpoint = IPEndPoint(_host, _port);
    local_addr = endpoint.get_socketaddr_in();
}

UdpClient::JoinMulticastGroup(const IPEndPoint &_end_point)函数允许客户端加入一个组播组,通过设置multicast_addr来实现。

void UdpClient::JoinMulticastGroup(const IPEndPoint &_end_point)
{
    multicast_addr = _end_point.get_socketaddr_in();
}

3. 启动与网络事件管理

UdpClient::Start()函数是客户端启动的核心,它创建套接字、设置套接字选项、绑定到本地地址,并加入组播组(如果设置了组播地址)。此外,它还设置了非阻塞模式,并创建了epoll实例来管理事件。

void UdpClient::Start()
{
    create_socket();

    if (local_addr.sin_port != 0)
    {
        int yes = 1;
        if (setsockopt(socket_fd, SOL_SOCKET, SO_REUSEADDR, &yes, sizeof(yes)) < 0)
        {
            std::cerr << "Setsockopt(SO_REUSEADDR) failed: " << strerror(errno) << std::endl;
            return;
        }

        int ret = bind(socket_fd, (sockaddr *)&local_addr, sizeof(local_addr));
        if (ret < 0)
            std::cout << "Bind error:" << strerror(errno) << std::endl;
        std::cout << "绑定、监听: " << IPEndPoint(local_addr).ToString() << std::endl;
    }
    if (multicast_addr.sin_port != 0)
    {
        add_multicast_group(IPEndPoint(multicast_addr));
    }

    set_epoll_mode(socket_fd, O_NONBLOCK);
    create_epoll();
    add_epoll_event(socket_fd, epoll_fd, EPOLLIN | EPOLLET);

    start_receive();
}

4. 数据发送与接收

UdpClient::Write(char *_data, int _offset, int _count, const IPEndPoint &_end_point)函数用于向指定的远程端点发送数据。它使用sendto系统调用来发送数据,并打印发送结果。

int UdpClient::Write(char *_data, int _offset, int _count, const IPEndPoint &_end_point)
{
    auto sendaddr = _end_point.get_socketaddr_in();
    auto ret = sendto(socket_fd, _data + _offset, _count, 0, (const sockaddr *)&sendaddr, sizeof(sendaddr));
    std::cout << "数据发送: " << ret << " : " << strerror(errno) << std::endl;
    return ret;
}

UdpClient::data_receive()函数负责接收数据。它使用recvfrom系统调用来读取数据,并触发DataReceived事件。

int UdpClient::data_receive()
{
    int recv_length = -1;
    socklen_t len = sizeof(remote_addr);
    while (true)
    {
        int length = 0;
        if (ioctl(socket_fd, FIONREAD, &length) == -1)
        {
            std::cout << "读取客户端(" << IPEndPoint(remote_addr).ToString() << ")缓冲区数据长度失败:" << strerror(errno) << std::endl;
            break;
        }
        if (length == 0)
            break;

        recv_length = recvfrom(socket_fd, recv_data, recv_data_length, 0, (sockaddr *)&remote_addr, &len);
        if (recv_length > 0)
        {
            DataReceiveEventArgs e(recv_data, recv_length, this);
            DataReceived.Invoke(this, &e);
        }
        else
        {
            if (recv_length == 0 || errno == EAGAIN || errno == EWOULDBLOCK)
                break;
            else
            {
                // 客户端异常断开连接处理
                break;
            }
        }
    }
    return recv_length;
}

5. 资源管理与清理

UdpClient::Close()函数负责清理资源。它关闭epoll文件描述符、套接字文件描述符,并释放分配的内存资源。

void UdpClient::Close()
{
    if (!running)
        return;
    int ret = -2;
    running = false;
    if (epoll_fd != 0)
    {
        client_event.data.fd = socket_fd;
        ret = epoll_ctl(epoll_fd, EPOLL_CTL_DEL, socket_fd, &client_event);
        std::cout << "epoll_fd已关闭:" << ret << " : " << strerror(errno) << std::endl;
    }
    ret = close(socket_fd);
    std::cout << "client_fd已关闭:" << ret << " : " << strerror(errno) << std::endl;
    if (epoll_fd != 0)
    {
        ret = close(epoll_fd);
        std::cout << "epoll_fd已关闭:" << ret << " : " << std::endl;
    }

    if (recv_data != nullptr)
        delete[] recv_data;
    recv_data = nullptr;
    while (!isDispose)
    {
        std::this_thread::sleep_for(std::chrono::milliseconds(10));
    }
}

6. 非阻塞模式与Epoll机制

UdpClient::set_epoll_mode(int sock_fd, int mode)函数将套接字设置为非阻塞模式,这对于异步事件驱动的网络编程至关重要。

int UdpClient::set_epoll_mode(int sock_fd, int mode)
{
    int flags = fcntl(sock_fd, F_GETFL, 0);
    if (flags == -1)
    {
        std::cout << "epoll_mode failed:" << sock_fd << std::endl;
        return -1;
    }
    std::string mode_str = (mode == O_NONBLOCK) ? "非阻塞模式" : "阻塞模式";
    int ret = fcntl(sock_fd, F_SETFL, flags | mode);
    std::cout << "设置epoll模式为: " << mode_str << " : " << ret << " : " << strerror(errno) << std::endl;
    return ret;
}

UdpClient::create_epoll()函数创建了一个epoll实例,用于管理多个套接字的事件。

int UdpClient::create_epoll()
{
    epoll_fd = epoll_create1(0);
    if (epoll_fd < 0)
    {
        std::cout << "epoll create failed:" << strerror(errno) << std::endl;
        close(socket_fd);
    }
    std::cout << "创建epoll描述符: " << epoll_fd << std::endl;
    return epoll_fd;
}

UdpClient::add_epoll_event(int _sock_fd, int _epoll_fd, uint32_t _events)函数将套接字添加到epoll实例中,并指定感兴趣的事件类型。

int UdpClient::add_epoll_event(int _sock_fd, int _epoll_fd, uint32_t _events)
{
    epoll_event _epoll_event;
    _epoll_event.events = _events;
    _epoll_event.data.fd = _sock_fd;
    int ret = epoll_ctl(_epoll_fd, EPOLL_CTL_ADD, _sock_fd, &_epoll_event);
    if (ret == -1)
    {
        std::cout << "epoll ctl add failed:" << strerror(errno) << std::endl;
        close(_sock_fd);
        close(_epoll_fd);
        return -1;
    }
    return ret;
}

7. 组播与广播设置

UdpClient::add_multicast_group(const IPEndPoint &_ip_mreq)函数允许客户端加入一个组播组。

void UdpClient::add_multicast_group(const IPEndPoint &_ip_mreq)
{
    ip_mreq _mreq;
    _mreq.imr_multiaddr.s_addr = _ip_mreq.get_socketaddr_in().sin_addr.s_addr;
    _mreq.imr_interface.s_addr = INADDR_ANY;
    if (setsockopt(socket_fd, IPPROTO_IP, IP_ADD_MEMBERSHIP, &_mreq, sizeof(_mreq)) < 0)
    {
        std::cout << "加入多播组 出错:" << strerror(errno) << std::endl;
        close(socket_fd);
    }
}

UdpClient::add_broadcast(const IPEndPoint &_ip_broad)函数设置广播选项,允许客户端发送广播消息。

int UdpClient::add_broadcast(const IPEndPoint &_ip_broad)
{
    int broadcastEnable = 1;
    int ret = setsockopt(socket_fd, SOL_SOCKET, SO_BROADCAST, &broadcastEnable, sizeof(broadcastEnable));
    if (ret < 0)
    {
        std::cerr << "设置广播模式出错:" << strerror(errno) << std::endl;
        return -1;
    }
    return ret;
}

8. 数据接收线程

UdpClient::start_receive()函数启动一个新线程来处理数据接收,这是事件驱动模型的一部分。

void UdpClient::start_receive()
{
    running = true;
    recv_data = new char[recv_data_length];
    std::thread th = std::thread(&UdpClient::data_received_thread, this);
    th.detach();
}

UdpClient::data_received_thread()函数是数据接收线程的主体,它在一个循环中等待epoll事件的发生,并处理数据接收。

void UdpClient::data_received_thread()
{
    struct epoll_event events[10];
    while (running)
    {
        int ret = epoll_wait(epoll_fd, events, 10, 1000);
        if (ret < 0)
        {
            if (errno == EINTR || errno == EWOULDBLOCK)
                continue;
            else
            {
                std::cout << "epoll wait failed:" << strerror(errno) << std::endl;
                break;
            }
        }
        for (int i = 0; i < ret; i++)
        {
            if (events[i].data.fd == socket_fd)
            {
                if (events[i].events & EPOLLIN)
                    data_receive();
                if (events[i].events & EPOLLOUT)
                {
                    // 处理EPOLLOUT事件
                }
            }
        }
    }
    Close();
    isDispose = true;
}

完整的代码:
UdpClient.h 头文件

#pragma once

#include <iostream>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <string>
#include <sys/epoll.h>
#include <cstring>
#include <fcntl.h>
#include <unistd.h>
#include <thread>
#include "../mscorlib/EventHandler.h"
#include "DataReceiveEventArgs.h"
#include "IPEndPoint.h"

class UdpClient
{
public:
    UdpClient();
    ~UdpClient();

public:
    EventHandler<DataReceiveEventArgs> DataReceived;

public:
    void Bind(const std::string _host, const int _port);
    void JoinMulticastGroup(const IPEndPoint &_end_point);
    void Start();
    int Write(char *_data, int _offset, int _count, const IPEndPoint &_end_point);
    void Close();
    bool IsDispose();
    IPEndPoint RemoteEndPoint();
    IPEndPoint RemoteEndPoint() const;

private:
    int create_socket();
    int set_epoll_mode(int sock_fd, int mode); // epoll模式--创建socket时,为非阻塞模式
    int create_epoll();
    int add_epoll_event(int _sock_fd, int _epoll_fd, uint32_t _events);

    void start_receive();
    void add_multicast_group(const IPEndPoint &_ip_mreq);
    int add_broadcast(const IPEndPoint &_ip_broad);
    void data_received_thread();
    int data_receive();
    int set_data_cache_size(const int &_size, const int &_cache /*_cache = SO_SNDBUF | SO_RCVBUF*/);

    // sockaddr_in get_local_addr();
    // sockaddr_in get_remote_addr();

    // std::string sockaddr_to_string(sockaddr_in _sock_addr);
    // std::string sockaddr_to_string(sockaddr_in _sock_addr) const;

private:
    int socket_fd;
    int epoll_fd;
    bool running = false;

    int recv_data_size = 0;
    int send_data_size = 0;
    char *recv_data = nullptr;
    int recv_data_length = 1024 * 1024;
    // 将客户端套接字添加到 epoll 中,监控 EPOLLIN 事件(表示有数据可读) EPOLLOUT(可写)  EPOLLET(边沿触发)
    struct epoll_event client_event;
    sockaddr_in local_addr;
    sockaddr_in remote_addr;
    sockaddr_in multicast_addr;
    sockaddr_in broadcast_addr;
    bool isDispose = false;
};

UdpClient.cpp

#include "UdpClient.h"

UdpClient::UdpClient() : socket_fd(0), epoll_fd(0)
{
    memset(&local_addr, 0, sizeof(local_addr));
    memset(&remote_addr, 0, sizeof(remote_addr));
    memset(&multicast_addr, 0, sizeof(multicast_addr));
    memset(&broadcast_addr, 0, sizeof(broadcast_addr));
}

UdpClient::~UdpClient()
{
}

void UdpClient::Bind(const std::string _host, const int _port)
{
    // 设置本地端口
    IPEndPoint endpoint = IPEndPoint(_host, _port);
    local_addr = endpoint.get_socketaddr_in();
    // local_addr.sin_addr.s_addr = INADDR_ANY; // 本地 IP 地址为任意地址
}

void UdpClient::JoinMulticastGroup(const IPEndPoint &_end_point)
{
    multicast_addr = _end_point.get_socketaddr_in();
}

void UdpClient::Start()
{
    create_socket();

    if (local_addr.sin_port != 0)
    {
        // 端口可重复使用
        int yes = 1;
        if (setsockopt(socket_fd, SOL_SOCKET, SO_REUSEADDR, &yes, sizeof(yes)) < 0)
        {
            std::cerr << "Setsockopt(SO_REUSEADDR) failed: " << strerror(errno) << std::endl;
            return;
        }

        int ret = bind(socket_fd, (sockaddr *)&local_addr, sizeof(local_addr));
        if (ret < 0)
            std::cout << "Bind error:" << strerror(errno) << std::endl;
        std::cout << "绑定、监听: " << IPEndPoint(local_addr).ToString() << std::endl;
    }
    // 加入组播
    if (multicast_addr.sin_port != 0)
    {
        add_multicast_group(IPEndPoint(multicast_addr));
    }

    set_epoll_mode(socket_fd, O_NONBLOCK);
    create_epoll();
    add_epoll_event(socket_fd, epoll_fd, EPOLLIN | EPOLLET);

    start_receive();
}

int UdpClient::Write(char *_data, int _offset, int _count, const IPEndPoint &_end_point)
{
    // 向客户端发送回应
    auto sendaddr = _end_point.get_socketaddr_in();
    auto ret = sendto(socket_fd, _data + _offset, _count, 0, (const sockaddr *)&sendaddr, sizeof(sendaddr));
    std::cout << "数据发送: " << ret << " : " << strerror(errno) << std::endl;
    return ret;
}

void UdpClient::Close()
{
    if (!running)
        return;
    int ret = -2;
    running = false;
    // 1. 删除client_fd的epoll事件
    if (epoll_fd != 0)
    {
        client_event.data.fd = socket_fd;

        ret = epoll_ctl(epoll_fd, EPOLL_CTL_DEL, socket_fd, &client_event);
        std::cout << "epoll_fd已关闭:" << ret << " : " << strerror(errno) << std::endl;
    }
    // 2. 关闭监听套接字
    ret = close(socket_fd);
    std::cout << "client_fd已关闭:" << ret << " : " << strerror(errno) << std::endl;
    // 3. 关闭 epoll 文件描述符
    if (epoll_fd != 0)
    {
        ret = close(epoll_fd);
        std::cout << "epoll_fd已关闭:" << ret << " : " << std::endl;
    }

    if (recv_data != nullptr)
        delete[] recv_data;
    recv_data = nullptr;
    while (!isDispose)
    {
        std::this_thread::sleep_for(std::chrono::milliseconds(10));
    }
}

bool UdpClient::IsDispose()
{
    return this->isDispose;
}

IPEndPoint UdpClient::RemoteEndPoint()
{
    return IPEndPoint(remote_addr);
}

IPEndPoint UdpClient::RemoteEndPoint() const
{
    return IPEndPoint(remote_addr);
}

int UdpClient::create_socket()
{
    socket_fd = socket(AF_INET, SOCK_DGRAM, 0);
    if (socket_fd == -1)
    {
        std::cout << "Socket creation failed:" << strerror(errno) << std::endl;
        return -1;
    }
    return socket_fd;
}

int UdpClient::set_epoll_mode(int sock_fd, int mode)
{
    /*
    O_NONBLOCK(非阻塞模式):如果设置了这个标志,表示该套接字(或文件)是非阻塞的,执行读写操作时不会阻塞调用进程或线程。
    套接字在没有数据可读或可写时不会让程序等待,而是立即返回。
    O_RDWR、O_WRONLY、O_RDONLY(访问模式):表示套接字的打开方式。
    O_APPEND(追加模式):指示文件或套接字在写操作时会追加数据。
    */
    int flags = fcntl(sock_fd, F_GETFL, 0); // 获取当前套接字的文件状态标志
    if (flags == -1)
    {
        std::cout << "epoll_mode failed:" << sock_fd << std::endl;
        return -1;
    }

    std::string mode_str = (mode == O_NONBLOCK) ? "非阻塞模式" : "阻塞模式";
    // 设置套接字为非阻塞模式
    int ret = fcntl(sock_fd, F_SETFL, flags | mode);
    std::cout << "设置epoll模式为: " << mode_str << " : " << ret << " : " << strerror(errno) << std::endl;
    return ret;
}

int UdpClient::create_epoll()
{
    // // 获取本地IP和端口
    // sockaddr_in local_addr;
    // socklen_t addr_len = sizeof(local_addr);
    // getsockname(client_fd, (sockaddr *)&local_addr, &addr_len);
    // std::cout << "本地IP和端口:" << "(" << ret << ")" << inet_ntoa(local_addr.sin_addr) << ":" << htons(local_addr.sin_port) << std::endl;
    // 创建epoll
    epoll_fd = epoll_create1(0);
    if (epoll_fd < 0)
    {
        std::cout << "epoll create failed:" << strerror(errno) << std::endl;
        close(socket_fd);
    }
    std::cout << "创建epoll描述符: " << epoll_fd << std::endl;
    return epoll_fd;
}

int UdpClient::add_epoll_event(int _sock_fd, int _epoll_fd, uint32_t _events)
{
    epoll_event _epoll_event;
    _epoll_event.events = _events;
    _epoll_event.data.fd = _sock_fd;
    int ret = epoll_ctl(_epoll_fd, EPOLL_CTL_ADD, _sock_fd, &_epoll_event);
    if (ret == -1)
    {
        std::cout << "epoll ctl add failed:" << strerror(errno) << std::endl;
        close(_sock_fd);
        close(_epoll_fd);
        return -1;
    }
    return ret;
}

void UdpClient::start_receive()
{
    running = true;
    recv_data = new char[recv_data_length];
    std::thread th = std::thread(&UdpClient::data_received_thread, this);
    th.detach();
}

void UdpClient::add_multicast_group(const IPEndPoint &_ip_mreq)
{
    // 加入组播组
    ip_mreq _mreq;
    //_mreq.imr_multiaddr.s_addr = inet_addr(_ip_mreq.Host().c_str()); // 或
    _mreq.imr_multiaddr.s_addr = _ip_mreq.get_socketaddr_in().sin_addr.s_addr;
    _mreq.imr_interface.s_addr = INADDR_ANY;
    if (setsockopt(socket_fd, IPPROTO_IP, IP_ADD_MEMBERSHIP, &_mreq, sizeof(_mreq)) < 0)
    {
        std::cout << "加入多播组 出错:" << strerror(errno) << std::endl;
        close(socket_fd);
    }
}

int UdpClient::add_broadcast(const IPEndPoint &_ip_broad)
{
    // 设置广播选项
    int broadcastEnable = 1;
    int ret = setsockopt(socket_fd, SOL_SOCKET, SO_BROADCAST, &broadcastEnable, sizeof(broadcastEnable));
    if (ret < 0)
    {
        std::cerr << "设置广播模式出错:" << strerror(errno) << std::endl;
        return -1;
    }
    return ret;
}

void UdpClient::data_received_thread()
{
    struct epoll_event events[10];
    while (running)
    {
        // 处理客户端数据
        int ret = epoll_wait(epoll_fd, events, 10, 1000);
        if (ret < 0)
        {
            if (errno == EINTR || errno == EWOULDBLOCK)
            {
                // 如果 epoll_wait 被信号中断,继续调用 epoll_wait
                continue;
            }
            else
            {
                std::cout << "epoll wait failed:" << strerror(errno) << std::endl;
                break;
            }
        }
        // 遍历所有发生的事件
        for (int i = 0; i < ret; i++)
        {
            if (events[i].data.fd == socket_fd)
            {
                if (events[i].events & EPOLLIN)
                {
                    data_receive();
                }
                if (events[i].events & EPOLLOUT)
                {
                    // connected = get_connect_state();
                    //  当send或write后,会触发此事件,可以做其他事情了
                    //  std::cout << (clock() / 1000 % 60) << "一触发一次写操作" << std::endl;
                }
            }
        }
    }
    Close();
    isDispose = true;
}

int UdpClient::data_receive()
{
    // 处理客户端数据
    int recv_length = -1;
    socklen_t len = sizeof(remote_addr);
    while (true)
    {
        int length = 0;
        if (ioctl(socket_fd, FIONREAD, &length) == -1)
        {
            std::cout << "读取客户端(" << IPEndPoint(remote_addr).ToString() << ")缓冲区数据长度失败:" << strerror(errno) << std::endl;
            break;
        }
        if (length == 0)
        {
            // std::cout << "读取客户端(" << IPEndPoint(remote_addr).ToString() << ")缓冲区数据长度没得了:" << strerror(errno) << std::endl;
            break;
        }

        recv_length = recvfrom(socket_fd, recv_data, recv_data_length, 0, (sockaddr *)&remote_addr, &len); // 从客户端读取数据
        if (recv_length > 0)
        {
            // 接收到客户端的数据,执行后续处理
            DataReceiveEventArgs e(recv_data, recv_length, this);
            // e.AutoRelease();
            DataReceived.Invoke(this, &e);
        }
        else
        {
            // 如果读取到 0 字节或者出错,表示客户端关闭连接或发生错误
            if (recv_length == 0)
            {
                std::cout << "客户端正常断开连接: " << recv_length << " : " << errno << " : " << strerror(errno) << std::endl; // 客户端正常断开连接

                //_client->Close();

                // std::cout << "关闭客户端:" << _client->GetLocalEndpointTostring() << std::endl;
            }
            else if (errno == EAGAIN || errno == EWOULDBLOCK)
            {
                // std::cout << "客户端无数据可接收了,请再试一次: " << recv_length << " : " << errno << " : " << strerror(errno) << std::endl; // 客户端正常断开连接
            }
            else
            {
                //_client->Close();
                // std::cout << "客户端异常断开连接: " << recv_length << " : " << errno << " : " << strerror(errno) << std::endl; // 客户端正常断开连接
            }
            break;
        }
    }

    return recv_length;
    // std::this_thread::sleep_for(std::chrono::milliseconds(20));
}

总结

UdpClient类提供了一个完整的UDP客户端实现,它封装了UDP通信的核心功能,包括数据的发送和接收、套接字的管理以及事件驱动的网络事件处理。通过非阻塞模式和epoll机制,UdpClient类能够高效地处理多个并发连接,适用于需要高性能网络通信的各种应用场景。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值