目录
一、UdpServer.hpp
#pragma once
#include <iostream>
#include <string>
#include <functional>
#include <strings.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include "Log.hpp"
using namespace LogModule;
// 定义处理函数的类型别名:接收string参数,返回string结果
using func_t = std::function<std::string(const std::string&)>;
// 默认文件描述符值,表示未初始化
const int defaultfd = -1;
// UDP服务器类,用于网络通信
class UdpServer
{
public:
// 构造函数
// @param port: 服务器监听的端口号
// @param func: 数据处理回调函数
UdpServer(uint16_t port, func_t func)
: _sockfd(defaultfd),
_port(port),
_isrunning(false),
_func(func)
{
}
// 初始化服务器,创建socket并绑定端口
void Init()
{
// 1. 创建UDP套接字
// AF_INET: IPv4地址族
// SOCK_DGRAM: 数据报套接字(UDP)
// 0: 让系统自动选择协议(对于UDP是IPPROTO_UDP)
_sockfd = socket(AF_INET, SOCK_DGRAM, 0);
if (_sockfd < 0)
{
LOG(LogLevel::FATAL) << "socket error!";
exit(1);
}
LOG(LogLevel::INFO) << "socket success, sockfd : " << _sockfd;
// 2. 绑定socket到指定端口
struct sockaddr_in local;
// 清零结构体,避免未初始化数据
bzero(&local, sizeof(local));
// 设置地址族为IPv4
local.sin_family = AF_INET;
// 将端口号从主机字节序转换为网络字节序(大端)
local.sin_port = htons(_port);
// INADDR_ANY表示绑定到所有本地接口
// 服务器通常这样设置以接收所有网络接口的数据
local.sin_addr.s_addr = INADDR_ANY;
// 绑定socket到指定的地址和端口
// 对于服务器必须显式绑定,因为需要固定监听端口
int n = bind(_sockfd, (struct sockaddr *)&local, sizeof(local));
if (n < 0)
{
LOG(LogLevel::FATAL) << "bind error";
exit(2);
}
LOG(LogLevel::INFO) << "bind success, sockfd : " << _sockfd;
}
// 启动服务器,进入主事件循环
void Start()
{
_isrunning = true;
while (_isrunning)
{
char buffer[1024]; // 接收缓冲区
struct sockaddr_in peer; // 存储对端地址信息
socklen_t len = sizeof(peer); // 地址结构体长度
// 1. 接收客户端消息
// recvfrom是阻塞调用,直到收到数据或出错
ssize_t s = recvfrom(_sockfd, buffer, sizeof(buffer) - 1, 0,
(struct sockaddr *)&peer, &len);
if (s > 0)
{
// 处理接收到的数据
buffer[s] = '\0'; // 确保字符串正确终止
// 获取客户端信息
int peer_port = ntohs(peer.sin_port); // 网络字节序转主机字节序
std::string peer_ip = inet_ntoa(peer.sin_addr); // 网络字节序IP转点分十进制
// 调用回调函数处理数据
std::string result = _func(buffer);
// 2. 发送响应消息回客户端
sendto(_sockfd, result.c_str(), result.size(), 0,
(struct sockaddr*)&peer, len);
}
}
}
// 析构函数
~UdpServer()
{
// 注意:实际应该在这里关闭socket,但代码中遗漏了
// 应该在_sockfd != defaultfd时调用close(_sockfd)
}
private:
int _sockfd; // 服务器socket文件描述符
uint16_t _port; // 服务器监听端口
bool _isrunning; // 服务器运行状态标志
func_t _func; // 数据处理回调函数
};
1、代码详细讲解:UDP服务器实现
这段代码实现了一个基于UDP协议的服务器类UdpServer,下面我将从各个方面进行详细讲解。
1. 头文件和预处理指令
#pragma once
-
#pragma once是一个非标准但被广泛支持的预处理指令,用于防止头文件被多次包含。
2. 包含的头文件
#include <iostream>
#include <string>
#include <functional>
#include <strings.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include "Log.hpp"
-
标准库头文件:
<iostream>,<string>,<functional> -
系统头文件:
-
<strings.h>: 提供bzero等字符串操作函数 -
<sys/types.h>: 定义数据类型 -
<sys/socket.h>: 套接字API -
<netinet/in.h>: 互联网地址族定义 -
<arpa/inet.h>: IP地址转换函数
-
-
自定义头文件:
"Log.hpp"(日志模块)
3. 类型别名和常量定义
using func_t = std::function<std::string(const std::string&)>;
const int defaultfd = -1;
-
func_t: 定义了一个函数类型别名,表示接受一个const std::string&参数并返回std::string的函数 -
defaultfd: 定义了一个默认的文件描述符值(-1),用于初始化套接字
4. UdpServer类定义
4.1 构造函数
UdpServer(uint16_t port, func_t func)
: _sockfd(defaultfd),
_port(port),
_isrunning(false),
_func(func)
{
}
-
参数:
-
port: 服务器监听的端口号 -
func: 处理接收数据的回调函数
-
-
初始化列表:
-
_sockfd: 初始化为默认文件描述符(-1) -
_port: 设置为传入的端口号 -
_isrunning: 初始化为false -
_func: 设置为传入的回调函数
-
4.2 初始化方法 Init()
void Init()
{
// 1. 创建套接字
_sockfd = socket(AF_INET, SOCK_DGRAM, 0);
if (_sockfd < 0)
{
LOG(LogLevel::FATAL) << "socket error!";
exit(1);
}
LOG(LogLevel::INFO) << "socket success, sockfd : " << _sockfd;
// 2. 绑定socket信息
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;
int n = bind(_sockfd, (struct sockaddr *)&local, sizeof(local));
if (n < 0)
{
LOG(LogLevel::FATAL) << "bind error";
exit(2);
}
LOG(LogLevel::INFO) << "bind success, sockfd : " << _sockfd;
}
-
创建套接字:
-
使用
socket()函数创建UDP套接字(SOCK_DGRAM) -
失败时记录FATAL日志并退出程序
-
-
绑定地址信息:
-
创建
sockaddr_in结构体并清零 -
设置地址族为AF_INET(IPv4)
-
设置端口号(使用
htons()将主机字节序转换为网络字节序) -
设置IP地址为INADDR_ANY(表示绑定到所有可用接口)
-
调用
bind()绑定套接字到指定地址和端口 -
失败时记录FATAL日志并退出程序
-
4.3 启动方法 Start()
void Start()
{
_isrunning = true;
while (_isrunning)
{
char buffer[1024];
struct sockaddr_in peer;
socklen_t len = sizeof(peer);
// 1. 接收消息
ssize_t s = recvfrom(_sockfd, buffer, sizeof(buffer) - 1, 0,
(struct sockaddr *)&peer, &len);
if (s > 0)
{
// 处理对端信息
int peer_port = ntohs(peer.sin_port);
std::string peer_ip = inet_ntoa(peer.sin_addr);
buffer[s] = 0; // 确保字符串以null结尾
// 调用回调函数处理数据
std::string result = _func(buffer);
// 2. 发送响应
sendto(_sockfd, result.c_str(), result.size(), 0,
(struct sockaddr*)&peer, len);
}
}
}
-
主循环:
-
设置
_isrunning为true,进入循环 -
循环会一直运行直到
_isrunning变为false
-
-
接收数据:
-
准备缓冲区
buffer和存储对端地址的sockaddr_in结构体 -
使用
recvfrom()接收UDP数据报 -
如果成功接收(s > 0):
-
获取对端端口号(使用
ntohs()转换字节序) -
获取对端IP地址(使用
inet_ntoa()转换) -
确保缓冲区以null结尾
-
调用回调函数
_func处理接收到的数据
-
-
-
发送响应:
-
使用
sendto()将处理结果发送回对端 -
使用之前接收到的对端地址信息
-
4.4 析构函数
~UdpServer()
{
}
-
当前为空实现,理论上应该在这里关闭套接字和释放资源
4.5 成员变量
private:
int _sockfd; // 套接字文件描述符
uint16_t _port; // 服务器监听端口
bool _isrunning; // 服务器运行状态标志
func_t _func; // 数据处理回调函数
5. 代码特点分析
-
UDP协议:使用SOCK_DGRAM类型的套接字、无连接协议,每次收发都需要指定对端地址
-
回调机制:通过
func_t类型的回调函数处理接收到的数据、使服务器逻辑与通信逻辑分离,提高灵活性 -
日志记录:使用自定义的LogModule记录不同级别的日志、包括FATAL(致命错误)和INFO(信息性)日志
-
网络字节序转换:使用
htons()/ntohs()处理端口号、使用inet_ntoa()处理IP地址 -
多接口支持:使用INADDR_ANY绑定到所有可用网络接口
6. 潜在改进点
-
资源释放:析构函数中应关闭套接字、考虑添加Stop()方法设置
_isrunning为false -
错误处理:可以添加更多错误处理逻辑、考虑非致命错误的恢复机制
-
性能优化:缓冲区大小固定为1024,可能不适合大块数据、可考虑动态缓冲区或分包处理
-
线程安全:当前实现不是线程安全的、如果需要在多线程环境中使用,需要添加同步机制
-
IPv6支持:当前仅支持IPv4、可扩展支持IPv6
这个示例创建了一个监听8080端口的UDP服务器,对收到的每条消息都添加"Response to: "前缀后返回。
7. 总结
这段代码实现了一个基本的UDP服务器框架,具有以下特点:
-
使用面向对象方式封装UDP服务器逻辑
-
通过回调函数实现灵活的数据处理
-
包含基本的错误处理和日志记录
-
支持多网络接口
代码结构清晰,但还有一些可以改进的地方,如资源管理、错误处理和扩展性方面。
2、知识点整理
1. 网络编程基础
-
UDP协议特点:无连接、不可靠、面向数据报、传输效率高
-
套接字类型:
-
SOCK_DGRAM:数据报套接字(UDP) -
SOCK_STREAM:流套接字(TCP)
-
-
地址族:
-
AF_INET:IPv4地址族 -
AF_INET6:IPv6地址族
-
2. 套接字编程关键函数
socket():创建套接字
int socket(int domain, int type, int protocol);

bind():绑定地址和端口
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);

recvfrom():接收数据(UDP)
ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags,
struct sockaddr *src_addr, socklen_t *addrlen);

sendto():发送数据(UDP)
ssize_t sendto(int sockfd, const void *buf, size_t len, int flags,
const struct sockaddr *dest_addr, socklen_t addrlen);

3. 地址结构与转换
-
sockaddr_in结构体:
struct sockaddr_in { short sin_family; // 地址族(AF_INET) unsigned short sin_port; // 16位端口号(网络字节序) struct in_addr sin_addr; // 32位IPv4地址(网络字节序) char sin_zero[8]; // 未使用(填充为零) }; -
字节序转换:

-
htons():主机字节序转网络字节序(16位) -
ntohs():网络字节序转主机字节序(16位) -
htonl()/ntohl():32位版本
-
-
IP地址转换:

-
inet_addr():点分十进制字符串转网络字节序(已过时) -
inet_aton():字符串转网络字节序(更安全) -
inet_ntoa():网络字节序IP转点分十进制字符串 -
现代替代方案:
inet_pton()和inet_ntop()
-
4. 特殊地址与端口
-
INADDR_ANY:绑定到所有本地接口(0.0.0.0)、服务器通常使用此方式监听所有网络接口
-
端口号:
-
0:让系统自动分配临时端口
-
1-1023:特权端口(需要root权限)
-
1024-49151:用户注册端口
-
49152-65535:动态/私有端口
-
5. 错误处理与日志
-
代码中使用自定义日志模块(
Log.hpp)记录不同级别日志 -
系统调用失败时应检查返回值并适当处理
-
严重错误(
FATAL)导致程序退出
6. 面向对象设计
-
使用类封装UDP服务器功能
-
构造函数初始化关键参数
-
提供Init()和Start()方法分离初始化和运行逻辑
-
使用函数对象(std::function)实现灵活的数据处理回调
7. 资源管理
-
代码中遗漏了socket的关闭操作
-
最佳实践应在析构函数中关闭socket
-
可考虑使用RAII模式管理资源
8. 网络数据格式
-
UDP是面向数据报的协议
-
每次recvfrom()调用返回一个完整的数据报
-
需要处理缓冲区溢出问题(示例中固定1024字节缓冲区)
9. 多线程与扩展性
-
当前实现是单线程的,一次只能处理一个请求
-
可扩展为多线程/多进程模型处理并发请求
-
或使用I/O多路复用(select/poll/epoll)
10. 安全考虑
-
未验证客户端身份,容易受到伪造源IP攻击
-
未处理消息截断问题(缓冲区固定大小)
-
生产环境需要添加更多安全措施
这个UDP服务器实现展示了基本的网络编程模式,但生产环境需要更多错误处理、性能优化和安全考虑。
二、UdpServer.cc
#include <iostream>
#include <memory>
#include "UdpServer.hpp"
// 仅仅是用来进行测试的
std::string defaulthandler(const std::string &message)
{
std::string hello = "hello, ";
hello += message;
return hello;
}
// 翻译系统,字符串当成英文单词
// ./udpserver port
int main(int argc, char *argv[])
{
if(argc != 2)
{
std::cerr << "Usage: " << argv[0] << " port" << std::endl;
return 1;
}
// std::string ip = argv[1];
uint16_t port = std::stoi(argv[1]);
Enable_Console_Log_Strategy();
std::unique_ptr<UdpServer> usvr = std::make_unique<UdpServer>(port, defaulthandler);
usvr->Init();
usvr->Start();
return 0;
}
这段代码实现了一个简单的UDP服务器,使用C++编写。下面我将从整体结构、各个部分的功能以及关键点进行详细讲解。
1、头文件引入
#include <iostream>
#include <memory>
#include "UdpServer.hpp"
-
<iostream>: 提供标准输入输出功能 -
<memory>: 提供智能指针支持(这里使用了std::unique_ptr) -
"UdpServer.hpp": 自定义的头文件,应该包含了UDP服务器的类定义
2、默认消息处理函数
std::string defaulthandler(const std::string &message)
{
std::string hello = "hello, ";
hello += message;
return hello;
}
这是一个简单的消息处理函数,它:
-
接收一个字符串参数
message -
创建一个新的字符串"hello, "
-
将输入消息追加到这个字符串后面
-
返回组合后的字符串
这个函数将作为UDP服务器的默认消息处理器,当服务器收到消息时会调用它。
3、主函数
int main(int argc, char *argv[])
{
// 参数检查
if(argc != 2)
{
std::cerr << "Usage: " << argv[0] << " port" << std::endl;
return 1;
}
// 解析端口号
uint16_t port = std::stoi(argv[1]);
// 启用控制台日志策略
Enable_Console_Log_Strategy();
// 创建UDP服务器实例
std::unique_ptr<UdpServer> usvr = std::make_unique<UdpServer>(port, defaulthandler);
// 初始化并启动服务器
usvr->Init();
usvr->Start();
return 0;
}
3.1 参数检查
if(argc != 2)
{
std::cerr << "Usage: " << argv[0] << " port" << std::endl;
return 1;
}
-
检查命令行参数数量是否正确
-
如果不是2个参数(程序名+端口号),打印使用说明并返回错误码1
-
argv[0]是程序名,argv[1]应该是端口号
3.2 解析端口号
uint16_t port = std::stoi(argv[1]);
-
将命令行参数(字符串)转换为无符号16位整数(端口号的标准类型)
-
使用
std::stoi进行转换
3.3 日志配置
Enable_Console_Log_Strategy();
-
调用一个函数启用控制台日志策略
-
从函数名可知,这是配置日志输出到控制台的函数
-
具体实现在之前实现过的日志文件中(后面会加上展示)
3.4 创建UDP服务器
std::unique_ptr<UdpServer> usvr = std::make_unique<UdpServer>(port, defaulthandler);
-
使用
std::make_unique创建一个UdpServer的唯一指针 -
构造函数参数:
-
port: 服务器监听的端口号 -
defaulthandler: 前面定义的消息处理函数
-
-
使用智能指针可以自动管理内存,避免内存泄漏

我们可能会遇到这样的错误,这个错误是因为 std::make_unique 是 C++14 引入的特性。解决方案:使用 C++14 或更高标准编译,也就是说在编译命令中添加 -std=c++14 或更高标准:
g++ -std=c++14 your_file.cpp -o your_program
3.5 初始化和启动服务器
usvr->Init();
usvr->Start();
-
Init(): 初始化服务器(可能包括创建socket、绑定端口等操作) -
Start(): 启动服务器,开始监听和处理连接 -
这两个方法的具体实现在
UdpServer.hpp对应的源文件中
4、程序流程总结
-
检查命令行参数,确保提供了端口号
-
解析端口号
-
配置日志输出
-
创建UDP服务器实例,并指定消息处理函数
-
初始化服务器
-
启动服务器,进入事件循环
5、关键设计点
-
使用智能指针:通过
std::unique_ptr管理UdpServer对象,确保异常安全性和自动内存管理。 -
可扩展的消息处理:通过将处理函数作为参数传递给
UdpServer构造函数,实现了处理逻辑与网络层的解耦。可以轻松替换不同的处理函数。 -
简单的错误处理:目前只有基本的参数检查,实际的网络错误处理应该在
UdpServer类的实现中。 -
日志策略:使用
Enable_Console_Log_Strategy()函数,暗示了可能有多种日志输出方式,便于调试和生产环境切换。
6、可能的改进
-
添加更详细的错误处理,特别是网络相关的错误
-
允许通过配置文件或命令行参数指定不同的处理函数
-
添加信号处理,实现优雅关闭
-
增加日志级别控制
这段代码展示了一个基本的UDP服务器框架,核心功能依赖于UdpServer类的实现(未展示),但已经体现了良好的结构设计和资源管理实践。
三、UdpClient.cc
这段代码实现了一个基于UDP协议的简单客户端程序,能够与指定的UDP服务器进行双向通信。程序通过命令行参数接收服务器IP地址和端口号,然后进入交互模式,用户可以输入消息发送给服务器并接收服务器的响应。
#include <iostream>
#include <string>
#include <cstring>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <sys/types.h>
#include <sys/socket.h>
int main(int argc, char *argv[])
{
// 1. 参数检查
if (argc != 3) {
std::cerr << "Usage: " << argv[0] << " server_ip server_port" << std::endl;
return 1;
}
// 2. 解析服务器地址和端口
std::string server_ip = argv[1];
uint16_t server_port = std::stoi(argv[2]);
// 3. 创建UDP套接字
int sockfd = socket(AF_INET, SOCK_DGRAM, 0);
if (sockfd < 0) {
std::cerr << "socket error" << std::endl;
return 2;
}
// 4. 配置服务器地址结构
struct sockaddr_in server;
memset(&server, 0, sizeof(server));
server.sin_family = AF_INET;
server.sin_port = htons(server_port);
server.sin_addr.s_addr = inet_addr(server_ip.c_str());
// 5. 主交互循环
while (true) {
// 获取用户输入
std::string input;
std::cout << "Please Enter# ";
std::getline(std::cin, input);
// 发送数据到服务器
sendto(sockfd, input.c_str(), input.size(), 0,
(struct sockaddr*)&server, sizeof(server));
// 接收服务器响应
char buffer[1024];
struct sockaddr_in peer;
socklen_t len = sizeof(peer);
int m = recvfrom(sockfd, buffer, sizeof(buffer)-1, 0,
(struct sockaddr*)&peer, &len);
// 处理接收到的数据
if (m > 0) {
buffer[m] = '\0';
std::cout << "Server response: " << buffer << std::endl;
}
}
// 程序不会执行到这里,因为前面是无限循环
close(sockfd);
return 0;
}
1、UDP客户端代码详细讲解
这段代码实现了一个简单的UDP客户端,可以与UDP服务器进行通信。下面我将从整体结构、各个部分的功能以及关键点进行详细讲解。
1. 头文件引入
#include <iostream>
#include <string>
#include <cstring>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <sys/types.h>
#include <sys/socket.h>
-
<iostream>: 提供标准输入输出功能 -
<string>: 提供字符串处理功能 -
<cstring>: 提供C风格字符串处理函数(如memset) -
<netinet/in.h>: 定义IP地址和端口号等网络相关结构 -
<arpa/inet.h>: 提供IP地址转换函数(如inet_addr) -
<sys/types.h>和<sys/socket.h>: 提供套接字编程所需的各种类型和函数
2. 主函数
int main(int argc, char *argv[])
{
// 参数检查
if (argc != 3)
{
std::cerr << "Usage: " << argv[0] << " server_ip server_port" << std::endl;
return 1;
}
// 解析服务器IP和端口
std::string server_ip = argv[1];
uint16_t server_port = std::stoi(argv[2]);
2.1 参数检查
-
检查命令行参数数量是否正确
-
如果不是3个参数(程序名+服务器IP+服务器端口),打印使用说明并返回错误码1
-
argv[0]是程序名,argv[1]是服务器IP,argv[2]是服务器端口
2.2 解析参数
-
server_ip: 存储服务器IP地址 -
server_port: 将字符串端口号转换为无符号16位整数
3. 创建套接字
// 1. 创建socket
int sockfd = socket(AF_INET, SOCK_DGRAM, 0);
if(sockfd < 0)
{
std::cerr << "socket error" << std::endl;
return 2;
}
-
使用
socket()函数创建UDP套接字-
AF_INET: 使用IPv4协议族 -
SOCK_DGRAM: 使用数据报套接字(UDP) -
0: 使用默认协议(对于UDP是IPPROTO_UDP)
-
-
检查套接字是否创建成功,失败则打印错误并返回错误码2
4. 关于客户端绑定的说明
代码中的注释详细解释了UDP客户端是否需要绑定的问题:
本地IP和端口如何配置?需要与"文件"相关联吗? 关于客户端绑定问题:
-
必须绑定吗?是,需要绑定
-
需要显式绑定吗?不需要!首次发送消息时,操作系统会自动为客户端绑定。系统会自动获取IP,并使用随机端口号
-
原因:每个端口号只能被一个进程绑定,采用随机端口可以避免客户端端口冲突
-
注意:客户端端口号的具体数值不重要,只要保证唯一性即可
关键点:
-
UDP客户端需要绑定,但通常不需要显式绑定
-
第一次发送数据时,操作系统会自动为客户端分配一个临时端口
-
这样做可以避免端口冲突,因为客户端端口号不重要
5. 设置服务器地址
// 填写服务器信息
struct sockaddr_in server;
memset(&server, 0, sizeof(server));
server.sin_family = AF_INET;
server.sin_port = htons(server_port);
server.sin_addr.s_addr = inet_addr(server_ip.c_str());
-
创建
sockaddr_in结构体存储服务器地址信息 -
使用
memset初始化为0 -
设置:
-
sin_family: 地址族(AF_INET表示IPv4) -
sin_port: 端口号,使用htons()转换为网络字节序 -
sin_addr.s_addr: IP地址,使用inet_addr()将字符串IP转换为网络字节序
-
6. 主循环
while(true)
{
// 获取用户输入
std::string input;
std::cout << "Please Enter# ";
std::getline(std::cin, input);
// 发送消息到服务器
int n = sendto(sockfd, input.c_str(), input.size(), 0,
(struct sockaddr*)&server, sizeof(server));
(void)n; // 忽略返回值
// 接收服务器响应
char buffer[1024];
struct sockaddr_in peer;
socklen_t len = sizeof(peer);
int m = recvfrom(sockfd, buffer, sizeof(buffer)-1, 0,
(struct sockaddr*)&peer, &len);
if(m > 0)
{
buffer[m] = 0; // 添加字符串结束符
std::cout << buffer << std::endl;
}
}
6.1 获取用户输入
-
使用
std::getline从标准输入读取一行文本 -
提示用户输入"Please Enter# "
6.2 发送消息
-
使用
sendto()函数发送消息到服务器-
sockfd: 套接字描述符 -
input.c_str(): 要发送的数据 -
input.size(): 数据长度 -
0: 标志位,通常为0 -
(struct sockaddr*)&server: 服务器地址 -
sizeof(server): 地址长度
-
-
忽略返回值(用
(void)n避免编译器警告)
6.3 接收响应
-
使用
recvfrom()函数接收服务器响应-
sockfd: 套接字描述符 -
buffer: 存储接收数据的缓冲区 -
sizeof(buffer)-1: 缓冲区大小(保留1字节给字符串结束符) -
0: 标志位,通常为0 -
(struct sockaddr*)&peer: 存储对端地址信息 -
&len: 输入输出参数,指定缓冲区大小并返回实际地址长度
-
-
如果接收成功(
m > 0):-
在数据末尾添加字符串结束符
\0 -
打印接收到的消息
-
7. 程序特点
-
简单的交互式客户端:通过命令行与用户交互,每次输入都发送到服务器并等待响应
-
无显式绑定:依赖操作系统自动分配临时端口
-
基本的错误处理:只检查了套接字创建错误,其他错误被忽略
-
固定缓冲区大小:使用1024字节的固定缓冲区接收数据
8. 可能的改进
-
增强错误处理:检查
sendto和recvfrom的返回值,处理各种错误情况 -
动态缓冲区:根据接收数据大小动态分配缓冲区
-
超时机制:为接收操作设置超时,避免无限等待
-
多线程:分离发送和接收操作,提高响应性
-
协议设计:定义更复杂的消息协议,而不仅仅是字符串交换
9. UDP客户端工作流程总结
-
解析命令行参数获取服务器地址和端口
-
创建UDP套接字
-
设置服务器地址结构
-
进入主循环:
-
读取用户输入
-
发送消息到服务器
-
接收服务器响应并显示
-
-
(理论上)当用户退出时关闭套接字(虽然这里没有显式关闭)
这段代码展示了一个基本的UDP客户端实现,适合用于学习UDP套接字编程的基础概念。
2、关键知识点整理
1. UDP协议特点
-
无连接协议:不需要建立连接就可以直接发送数据
-
不可靠传输:不保证数据包顺序和可靠性
-
轻量级:相比TCP,头部开销小,传输效率高
-
适合场景:实时性要求高、允许少量丢包的应用(如视频流、在线游戏)
2. 套接字编程基础
套接字创建
int socket(int domain, int type, int protocol);

-
domain:地址族-
AF_INET:IPv4 -
AF_INET6:IPv6
-
-
type:套接字类型-
SOCK_STREAM:TCP -
SOCK_DGRAM:UDP
-
-
protocol:通常设为0,表示使用默认协议
地址结构
struct sockaddr_in {
short sin_family; // 地址族 (AF_INET)
unsigned short sin_port; // 16位端口号 (网络字节序)
struct in_addr sin_addr; // 32位IP地址
char sin_zero[8]; // 未使用(填充使结构体大小等于sockaddr)
};
3. 关键函数说明
sendto() - 发送数据
ssize_t sendto(int sockfd, const void *buf, size_t len, int flags,
const struct sockaddr *dest_addr, socklen_t addrlen);
-
sockfd:套接字描述符 -
buf:要发送的数据缓冲区 -
len:数据长度 -
flags:通常为0 -
dest_addr:目标地址 -
addrlen:地址结构长度
recvfrom() - 接收数据
ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags,
struct sockaddr *src_addr, socklen_t *addrlen);

-
sockfd:套接字描述符 -
buf:接收缓冲区 -
len:缓冲区大小 -
flags:通常为0 -
src_addr:返回发送方地址(可设为NULL如果不关心) -
addrlen:输入输出参数,指定地址结构大小并返回实际大小
4. 字节序转换
-
网络字节序:大端序(Big-Endian)
-
主机字节序:取决于CPU架构(x86为小端序)
-
关键函数:
-
htons():主机序转网络序(16位) -
htonl():主机序转网络序(32位) -
ntohs():网络序转主机序(16位) -
ntohl():网络序转主机序(32位)
-
5. IP地址转换
-
inet_addr():将点分十进制字符串IP转换为网络字节序的32位值 -
inet_ntoa():将网络字节序的32位IP转换为点分十进制字符串 -
更现代的替代函数:
-
inet_pton():支持IPv4和IPv6 -
inet_ntop():支持IPv4和IPv6
-
6. 客户端绑定问题
-
是否需要显式绑定:通常不需要
-
自动绑定机制:
-
第一次发送数据时,操作系统自动分配临时端口
-
避免端口冲突(因为客户端端口不重要)
-
使用随机高位端口(通常在32768-60999范围内)
-
7. 错误处理
-
当前代码只检查了套接字创建错误
-
实际开发中应检查:
-
sendto()返回值(实际发送的字节数) -
recvfrom()返回值(接收的字节数或错误) -
使用
perror()或strerror(errno)获取错误信息
-
代码执行流程
-
参数检查:验证命令行参数数量是否正确
-
地址解析:将字符串形式的IP和端口转换为二进制格式
-
套接字创建:建立UDP套接字
-
地址配置:设置服务器地址结构
-
交互循环:
-
读取用户输入
-
发送到服务器
-
接收服务器响应
-
显示响应内容
-
-
(理论上)资源释放:关闭套接字(但当前代码因无限循环不会执行到这里)
改进建议
-
增加错误处理:检查所有系统调用的返回值
-
添加超时机制:使用
select()或poll()设置接收超时 -
支持信号处理:如Ctrl+C中断时优雅退出
-
动态缓冲区:根据数据大小动态分配缓冲区
-
日志记录:记录通信过程便于调试
-
多线程处理:分离发送和接收操作
重点注意
云服务器不支持直接绑定公有IP,我们也不建议在编写服务器程序时绑定特定IP地址,推荐使用INADDR_ANY来绑定任意地址。
/* 接受所有来源消息的地址 */
#define INADDR_ANY ((in_addr_t) 0x00000000)
在网络编程中,当进程需要绑定网络端口进行通信时,可以使用INADDR_ANY作为IP地址参数。这种方式表示该端口可以接受来自任何IP地址的连接请求,无论是本地还是远程主机。例如,对于配备多网卡(每个网卡拥有不同IP地址)的服务器,使用INADDR_ANY可以免去识别数据具体来自哪个网卡/IP地址的麻烦。
总结
这段代码展示了UDP客户端的基本实现,涵盖了套接字创建、地址配置、数据发送和接收等核心操作。通过学习这段代码,可以掌握UDP网络编程的基本模式和关键API的使用方法。理解UDP的无连接特性、字节序转换和地址处理是掌握网络编程的基础。
四、相关必需头文件
Log.hpp
#ifndef __LOG_HPP__
#define __LOG_HPP__
#include <iostream>
#include <cstdio>
#include <string>
#include <filesystem> //C++17
#include <sstream>
#include <fstream>
#include <memory>
#include <ctime>
#include <unistd.h>
#include "Mutex.hpp"
namespace LogModule
{
using namespace MutexModule;
const std::string gsep = "\r\n";
// 策略模式,C++多态特性
// 2. 刷新策略 a: 显示器打印 b:向指定的文件写入
// 刷新策略基类
class LogStrategy
{
public:
~LogStrategy() = default;
virtual void SyncLog(const std::string &message) = 0;
};
// 显示器打印日志的策略 : 子类
class ConsoleLogStrategy : public LogStrategy
{
public:
ConsoleLogStrategy()
{
}
void SyncLog(const std::string &message) override
{
LockGuard lockguard(_mutex);
std::cout << message << gsep;
}
~ConsoleLogStrategy()
{
}
private:
Mutex _mutex;
};
// 文件打印日志的策略 : 子类
const std::string defaultpath = "./log";
const std::string defaultfile = "my.log";
class FileLogStrategy : public LogStrategy
{
public:
FileLogStrategy(const std::string &path = defaultpath, const std::string &file = defaultfile)
: _path(path),
_file(file)
{
LockGuard lockguard(_mutex);
if (std::filesystem::exists(_path))
{
return;
}
try
{
std::filesystem::create_directories(_path);
}
catch (const std::filesystem::filesystem_error &e)
{
std::cerr << e.what() << '\n';
}
}
void SyncLog(const std::string &message) override
{
LockGuard lockguard(_mutex);
std::string filename = _path + (_path.back() == '/' ? "" : "/") + _file; // "./log/" + "my.log"
std::ofstream out(filename, std::ios::app); // 追加写入的 方式打开
if (!out.is_open())
{
return;
}
out << message << gsep;
out.close();
}
~FileLogStrategy()
{
}
private:
std::string _path; // 日志文件所在路径
std::string _file; // 日志文件本身
Mutex _mutex;
};
// 形成一条完整的日志&&根据上面的策略,选择不同的刷新方式
// 1. 形成日志等级
enum class LogLevel
{
DEBUG,
INFO,
WARNING,
ERROR,
FATAL
};
std::string Level2Str(LogLevel level)
{
switch (level)
{
case LogLevel::DEBUG:
return "DEBUG";
case LogLevel::INFO:
return "INFO";
case LogLevel::WARNING:
return "WARNING";
case LogLevel::ERROR:
return "ERROR";
case LogLevel::FATAL:
return "FATAL";
default:
return "UNKNOWN";
}
}
std::string GetTimeStamp()
{
time_t curr = time(nullptr);
struct tm curr_tm;
localtime_r(&curr, &curr_tm);
char timebuffer[128];
snprintf(timebuffer, sizeof(timebuffer),"%4d-%02d-%02d %02d:%02d:%02d",
curr_tm.tm_year+1900,
curr_tm.tm_mon+1,
curr_tm.tm_mday,
curr_tm.tm_hour,
curr_tm.tm_min,
curr_tm.tm_sec
);
return timebuffer;
}
// 1. 形成日志 && 2. 根据不同的策略,完成刷新
class Logger
{
public:
Logger()
{
EnableConsoleLogStrategy();
}
void EnableFileLogStrategy()
{
_fflush_strategy = std::make_unique<FileLogStrategy>();
}
void EnableConsoleLogStrategy()
{
_fflush_strategy = std::make_unique<ConsoleLogStrategy>();
}
// 表示的是未来的一条日志
class LogMessage
{
public:
LogMessage(LogLevel &level, std::string &src_name, int line_number, Logger &logger)
: _curr_time(GetTimeStamp()),
_level(level),
_pid(getpid()),
_src_name(src_name),
_line_number(line_number),
_logger(logger)
{
// 日志的左边部分,合并起来
std::stringstream ss;
ss << "[" << _curr_time << "] "
<< "[" << Level2Str(_level) << "] "
<< "[" << _pid << "] "
<< "[" << _src_name << "] "
<< "[" << _line_number << "] "
<< "- ";
_loginfo = ss.str();
}
// LogMessage() << "hell world" << "XXXX" << 3.14 << 1234
template <typename T>
LogMessage &operator<<(const T &info)
{
// a = b = c =d;
// 日志的右半部分,可变的
std::stringstream ss;
ss << info;
_loginfo += ss.str();
return *this;
}
~LogMessage()
{
if (_logger._fflush_strategy)
{
_logger._fflush_strategy->SyncLog(_loginfo);
}
}
private:
std::string _curr_time;
LogLevel _level;
pid_t _pid;
std::string _src_name;
int _line_number;
std::string _loginfo; // 合并之后,一条完整的信息
Logger &_logger;
};
// 这里故意写成返回临时对象
LogMessage operator()(LogLevel level, std::string name, int line)
{
return LogMessage(level, name, line, *this);
}
~Logger()
{
}
private:
std::unique_ptr<LogStrategy> _fflush_strategy;
};
// 全局日志对象
Logger logger;
// 使用宏,简化用户操作,获取文件名和行号
#define LOG(level) logger(level, __FILE__, __LINE__)
#define Enable_Console_Log_Strategy() logger.EnableConsoleLogStrategy()
#define Enable_File_Log_Strategy() logger.EnableFileLogStrategy()
}
#endif
Mutex.hpp
#pragma once
#include <iostream>
#include <pthread.h>
namespace MutexModule
{
class Mutex
{
public:
Mutex()
{
pthread_mutex_init(&_mutex, nullptr);
}
void Lock()
{
int n = pthread_mutex_lock(&_mutex);
(void)n;
}
void Unlock()
{
int n = pthread_mutex_unlock(&_mutex);
(void)n;
}
~Mutex()
{
pthread_mutex_destroy(&_mutex);
}
pthread_mutex_t *Get()
{
return &_mutex;
}
private:
pthread_mutex_t _mutex;
};
class LockGuard
{
public:
LockGuard(Mutex &mutex):_mutex(mutex)
{
_mutex.Lock();
}
~LockGuard()
{
_mutex.Unlock();
}
private:
Mutex &_mutex;
};
}
五、运行输出示例
这个例子跟上一个简易回声客户端与服务端一样,只不过多加了一个之前已经实现过的日志功能而已,操作还是相同的:

也可以使用makefile文件来进行操作:
.PHONY:all
all:Udpclient Udpserver
Udpclient:UdpClient.cc
g++ -o $@ $^ -std=c++17 #-static
Udpserver:UdpServer.cc
g++ -o $@ $^ -std=c++17
.PHONY:clean
clean:
rm -f Udpclient Udpserver
首先我们运行和启动服务端:

然后我们再运行和启动客户端:

此时我们使用客户端给服务端发送一条信息,回车后然后可以看到服务端给客户端回显一条对应方法处理过的信息:

1098

被折叠的 条评论
为什么被折叠?



