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
类能够高效地处理多个并发连接,适用于需要高性能网络通信的各种应用场景。