个人主页:C++忠实粉丝
欢迎 点赞👍 收藏✨ 留言✉ 加关注💓本文由 C++忠实粉丝 原创
目录
功能介绍 :
1. nocopy.hpp
2. InetAddr.hpp
3. UdpServer.hpp
4. UdpServerMain.cc
5. UdpClientMain.cc
6. 效果展示
功能介绍 :
简单的回显服务器和客服端代码, 能接受多个客服端的代码.
1. nocopy.hpp
#pragma onceclass nocopy
{
public:nocopy(){}~nocopy(){}nocopy(const nocopy&) = delete;const nocopy& operator=(const nocopy&) = delete;
};
定义一个 nocopy 的类, 通过 C++11 delete 关键字的特性, 阻止该类的拷贝构造和拷贝赋值.
比如下面的例子都是不可以的 :
nocopy obj1;
nocopy obj2 = obj1; // 错误:拷贝构造函数被删除
nocopy obj1;
nocopy obj2;
obj2 = obj1; // 错误:拷贝赋值运算符被删除
为什么需要禁止对象的拷贝呢?
这样的设计常见于以下情况 :
1. 资源管理类 : 例如, 涉及底层资源 (如文件句柄, 网络连接等) 的类通常不希望被复制, 因为这些资源可能会导致资源管理上的混乱或错误~
2. 不想允许拷贝的类: 有些类可能设计上不希望被拷贝, 例如类的状态可能是唯一的, 拷贝它没有意义~~
我们这里显然是第一种~~
2. InetAddr.hpp
#pragma once#include <iostream>
#include <string>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>class InetAddr
{
private:void ToHost(const struct sockaddr_in &addr){_port = ntohs(addr.sin_port);_ip = inet_ntoa(addr.sin_addr);}public:InetAddr(const struct sockaddr_in &addr):_addr(addr){ToHost(addr);}std::string Ip(){return _ip;}uint16_t Port(){return _port;}~InetAddr(){}private:std::string _ip;uint16_t _port;struct sockaddr_in _addr;
};
InetAddr 类它封装了网络地址 (IP 端口), 以及提供了访问这些信息的方法, 该类通过了 struct sockaddr_in 来存储 IP 地址和端口, 并提供了转换和获取信息的功能
成员变量 :
_ip : 存储 IP 地址, 使用 std::string 类型, 因为 IP 地址通常表示一个点分十进制的字符串
_port : 存储端口号, 使用 uint16_t 类型, 端口号是一个16位的无符号整数
_addr : 存储原始的 struct sockaddr_in 结构体, 它包含了 IP 地址和端口信息
ToHost():
ToHost() : 将 struct sockaddr_in 中的网络字节数据转换为主机字节序, 并提取 IP 和端口.
ntohs(addr.sin_port) : 将网络字节序的端口号转换为主机字节序
inet_ntoa(addr.sin_addr) : 将网络字节序的 IP 地址转换为点分十进制的字符串的形式
构造函数 :
构造函数 : 接受一个 struct sockaddr_in 类型的参数, 表示网络地址
构造函数初始化 _addr 成员, 存储传入的地址
然后调用 ToHost() 方法来从该地址中提取 IP 和端口, 并进行转换
Ip() && Port()
Ip() : 返回存储的 IP 地址, 类型为 std::string
Port() : 返回存储的端口号, 类型为 uint16_t
析构函数 :
由于没有动态分配资源, 所以没有必要进行额外的清理工作~
3. UdpServer.hpp
#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 gsockfd = -1;
static const uint16_t glocalport = 8888;enum
{SOCKET_ERROR = 1,BIND_ERROR
};// UdpServer user("192.1.1.1", 8899);
// 一般服务器主要是用来进行网络数据读取和写入的。IO的
// 服务器IO逻辑 和 业务逻辑 解耦
class UdpServer : public nocopy
{
public:UdpServer(uint16_t localport = glocalport): _sockfd(gsockfd),_localport(localport),_isrunning(false){}void InitServer(){// 1. 创建socket文件_sockfd = ::socket(AF_INET, SOCK_DGRAM, 0);if (_sockfd < 0){LOG(FATAL, "socket error\n");exit(SOCKET_ERROR);}LOG(DEBUG, "socket create success, _sockfd: %d\n", _sockfd); // 3// 2. bindstruct 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()); // 1. 需要4字节IP 2. 需要网络序列的IP -- 暂时local.sin_addr.s_addr = INADDR_ANY; // 服务器端,进行任意IP地址绑定int n = ::bind(_sockfd, (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(_sockfd, 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(_sockfd, echo_string.c_str(), echo_string.size(), 0, (struct sockaddr *)&peer, len);}else{std::cout << "recvfrom , error" << std::endl;}}}~UdpServer(){if(_sockfd > gsockfd) ::close(_sockfd);}private:int _sockfd;uint16_t _localport;// std::string _localip; // TODO:后面专门要处理一下这个IPbool _isrunning;
};
这段代码定义了一个 UdpServer 类, 用于创建和管理一个 UDP 服务器, 它能够接受客户端的消息并发送相应~
1. 常量和枚举
gsockfd : 设置为 -1, 用作初始的 sockfd, 代表一个无效的套接字描述符
glocalport : 服务器的默认接口, 默认值为 8888
SOCKET_ERROR 和 BIND_ERROR : 自定义的错误代码, 分别代表套接字创建失败和绑定失败.
2. 成员变量
_sockfd : 存储套接字文件描述符
_localport : 服务器绑定的本地端口号
_isrunning : 一个布尔值, 指示服务器是否正在运行
3. 构造函数
UdpServer 类的构造函数的初始化了以下成员变量 :
_sockfd : 默认初始化为 gsockfd (-1), 表示无效的套接字
_localport : 使用传入的端口号 localport 或默认端口 glocalport (8888)
isrunning : 初始化为 false , 表示服务器的初始状态是未运行.
4. InitServer()
创建套接字 : 使用 socket() 函数创建一个 UDP 套接字, AF_INET 表示使用 IPv4 协议, SOCK_DGRAM 表示使用 UDP.
1. 如果创建失败, 记录日志并退出
2. 成功后, 输出日志, 显示套接字描述符 _sockfd.
绑定套接字 : 使用 bind() 函数将套接字与本地地址 (IP 和端口) 绑定
local.sin_family = AF_INET : 设置为 IPv4 地址族
local.sin_port = htons(_localport) : 将端口号转换为网络字节序
local.sin_addr.s_addr = INADDR_ANY : 服务器绑定到任意 IP 地址
band() 调用套接字与本地地址绑定, 如果绑定失败, 记录错误日志并退出
5. Start()
Start() 方法用于接收数据并发送响应
_isrunning = true : 启动服务器, 进入接收数据的循环
recvfrom() : 接收 UDP 数据包, 数据存放在 inbuffer 中, peer 存储发送方的地址信息
如果接收成功 (n > 0), 则:
1. 创建一个 InetAddr 对象, 解析发送对方的 IP 地址和端口
2. 输出接收到的消息及其来源 IP 和端口
3. 生成一个响应消息 ("[udp_server echo]#" + 接收的消息), 然后使用 sendto() 将其发送回客户端
如果 recvfrom() 失败, 打印错误消息.
6. 析构函数
在 UdpServer 对象销毁时, 关闭套接字 _sockfd, 如果 _sockfd 大于 gsockfd (即有效套接字), 则调用 close() 关闭套接字
4. UdpServerMain.cc
#include "UdpServer.hpp"#include <memory>// ./udp_server local-port
// ./udp_server 8888
int main(int argc, char *argv[])
{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>(port); //C++14的标准usvr->InitServer();usvr->Start();return 0;
}
std::stoi(argv[1]) : 将命令行参数中的端口号字符串 (argv[1]) 转换为 uint6_t 类型 (无符号16位整数), std::stoi 是 C++ 标准库函数, 用于将字符串转换为整数, 如果 argv[1] 不是一个有效的数组, std::stoi 将抛出异常
std::unique_ptr<UdpServer> usvr = std::make_unique<UdpServer>(port) :
这行代码使用了 C++14 引入的 std::make_unique, 它用来创建一个 std::unique_ptr, 智能指针类型, 用于管理动态分配的 UdpServer 对象.
std::make_unique<UdpServer>(port) : 动态创建一个 UdpServer 对象, 并将端口号 port 传递给它的构造函数.
std::unique_ptr<UdpServer> usvr : usvr 是一个智能指针, 指向创建的 Udpserver 对象, unique_ptr 确保在 usvr 离开作用域时自动销毁对象, 避免内存泄露.
UdpServer 类接收一个端口号作为构造参数, 表示该服务器将在指定的端口上运行
然后便直接 初始化服务器 和 启动服务
5. UdpClientMain.cc
#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地址和端口号
// ./udp_client server-ip server-port
// ./udp_client 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);// std::cout << "line message is@ " << line << std::endl;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;
}
1. 从命令行获取目标服务器的 IP 地址和端口号。
2. 创建 UDP 套接字并配置服务器的地址。
3. 进入循环,等待用户输入数据。
4. 将用户输入的数据通过 sendto 发送给服务器。
5. 接收服务器的响应数据并输出到控制台。
6. 出现错误时终止通信并关闭套接字。