牢蒯的源码在下面的仓库里,各位大佬可以自行取用,古法编程,难免有bug,大佬们可在评论区指出错误。
https://gitee.com/laotian666/linux_storage.git
0 前置知识:UDP网络编程核心接口与结构体
UDP属于无连接传输协议,通信双方不需要预先建立连接。本项目全部基于UDP套接字接口开发,在阅读项目代码前,先熟悉项目用到的系统结构体与系统调用。
0.1 sockaddr_in结构体
sockaddr_in是IPv4专用地址结构体,用于保存IP地址、端口、协议族信息。系统套接字接口的形参统一为通用结构体struct sockaddr,使用的时候必须做强制类型转换。
struct sockaddr_in
{
sa_family_t sin_family;
in_port_t sin_port;
struct in_addr sin_addr;
unsigned char sin_zero[8];
};
struct in_addr
{
uint32_t s_addr;
};
- sin_family需要设置为AF_INET,代表使用IPv4协议。
- sin_port存储端口号,端口必须为网络大端字节序,不能直接填写主机字节序数值。
- sin_addr用来存储4字节二进制IP地址。
- sin_zero是内存对齐占位字段,一般直接对整个结构体做清零处理。
特殊宏INADDR_ANY,可以让服务端绑定本机所有网卡,接收本机任意网卡上到来的客户端请求。将sockaddr_in传给bind、recvfrom、sendto接口时,必须强转为struct sockaddr*类型。
0.2 htons与ntohs字节序转换函数
网络传输统一使用大端字节序,家用计算机主机大多是小端模式,主机与网络之间需要进行字节序转换。
uint16_t htons(uint16_t hostshort);
uint16_t ntohs(uint16_t netshort);
- htons:完成主机短整型向网络大端转换,填充服务端端口时调用。
- ntohs:将网络大端端口转换为主机字节序,从recvfrom拿到客户端地址后调用,得到人类可读的端口数字。
端口是16位短整型,所以使用这一组接口,IP地址转换使用inet_ntop、inet_pton。
0.3 recvfrom系统调用
recvfrom是UDP接收数据的专用接口,接收报文的同时,内核会填充发送方的网络地址信息。
ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags,
struct sockaddr *src_addr, socklen_t *addrlen);
- sockfd是socket返回的套接字文件描述符。
- buf是应用层接收缓冲区,len代表缓冲区最大可接收字节。
- flags传0代表使用默认行为。
- src_addr是输出参数,内核会把发送方的IP和端口写入该结构体。
- addrlen属于输入输出参数,调用前传入结构体大小,调用完成内核会回写真实地址长度。
返回值大于0代表成功接收的字节数,返回-1代表调用出错。
⚠️重要:UDP报文不会自带字符串结束符\\0,拿到返回字节数之后,需要手动在缓冲区末尾补\\0,才可以当做C++字符串处理。
0.4 sendto系统调用
UDP发送数据的系统调用,因为UDP没有连接的概念,每次发送数据都必须显式指定目标客户端地址。
ssize_t sendto(int sockfd, const void *buf, size_t len, int flags,
const struct sockaddr *dest_addr, socklen_t addrlen);
- sockfd为UDP套接字。
- buf指向待发送数据缓冲区,len是发送字节长度。
- flags参数填0。
- dest_addr填写接收方的网络地址。
- addrlen传入地址结构体占用字节大小。
调用成功返回实际发送字节数,失败返回-1。每调用一次sendto就会生成一个完整UDP数据报。
0.5 socket创建套接字
int socket(int domain, int type, int protocol);
本项目调用形式为socket(AF_INET, SOCK_DGRAM, 0)。
- AF_INET:代表IPv4协议
- SOCK_DGRAM:代表数据报,即UDP协议
- 第三个参数填0自动匹配对应传输协议
调用成功返回套接字文件描述符,失败返回-1。
0.6 bind绑定端口
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
服务端程序需要调用bind,把socket和本机IP、端口绑定,操作系统才可以把对应端口收到的报文交给当前进程处理。
💡UDP客户端一般不需要手动bind,操作系统会自动分配临时端口。搭配INADDR_ANY可以监听本机全部网卡。
0.7 inet_ntop与inet_pton IP格式转换
const char *inet_ntop(int af, const void *src, char *dst, socklen_t size);
int inet_pton(int af, const char *src, void *dst);
- inet_ntop:把网络二进制IP转换成点分十进制字符串。
- inet_pton:将点分十进制字符串IP转换成网络二进制格式。
❗重点:舍弃老旧接口inet_ntoa。inet_ntoa内部使用静态缓冲区,多线程环境调用会出现数据覆盖,线程不安全,项目中统一使用inet_ntop。
0.8 UDP完整通信流程
服务端执行流程:调用socket创建UDP套接字 → 填充sockaddr_in结构体执行bind绑定端口 → 循环调用recvfrom等待接收客户端报文 → 执行业务逻辑处理数据 → 调用sendto将结果回传给客户端。
客户端执行流程:调用socket创建UDP套接字 → 填充服务端地址结构体 → 调用sendto发送请求报文 → 调用recvfrom接收服务端应答。
✨UDP无连接特性,每次收发数据都需要携带对方地址信息,因此UDP一般使用recvfrom、sendto,而不使用简单的read、write接口。
1 项目整体介绍
本项目使用C++实现一个UDP版本英汉词典翻译服务器。客户端通过UDP发送英文单词,服务端读取本地词典文件完成单词查询,将中文释义返回客户端;查询不到单词则返回None。
项目采用分层解耦设计,将互斥锁、日志系统、网络地址封装、UDP网络服务、词典业务相互解耦。 用到的技术点包含:RAII思想、策略模式、std::function回调函数、UDP套接字编程、C++17文件系统库。
1.1 项目文件清单
2 核心模块代码实现
2.1 Mutex.hpp互斥锁模块
原生pthread库锁容易出现忘记解锁造成死锁,这里做面向对象封装,并且实现RAII守卫锁,出作用域自动释放锁资源,避免忘记unlock。
#ifndef __MUTEX_HPP__
#define __MUTEX_HPP__
#include <pthread.h>
namespace MyMutex
{
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 GuardMutex
{
public:
//❗修复原bug:必须传引用,不能值拷贝Mutex对象
GuardMutex(Mutex& mutex)
:_mutex(mutex)
{
_mutex.Lock();
}
~GuardMutex()
{
_mutex.UnLock();
}
private:
Mutex& _mutex ;
};
}
#endif
❗原始代码bug:GuardMutex值传递Mutex对象,会拷贝锁对象,锁逻辑完全失效,本项目修复为引用传递。
2.2 Log.hpp日志模块
日志模块使用策略模式,抽象日志刷新基类,派生出控制台打印策略、文件写入策略。 每条日志自动携带:时间戳、日志等级、进程PID、源码文件名、代码行号。借助宏自动捕获__FILE__与__LINE__,使用流式<<写法输出日志。
#pragma once
#include<iostream>
#include<string>
#include"Mutex.hpp"
#include<fstream>
#include<cstdio>
#include<memory>
#include<unistd.h>
#include<ctime>
#include <filesystem>
#include<sstream>
namespace LogModule
{
const std::string gsep = "\\r\\n";
using namespace MyMutex;
//日志策略抽象基类
class LogStrategy
{
public:
~LogStrategy() = default;
virtual void SyncLog(const std::string& message) = 0;
};
//控制台输出策略
class ConsoleLogStrategy: public LogStrategy
{
public:
virtual void SyncLog(const std::string& message) override
{
GuardMutex lockGuard(_mutex);
std::cout << message << "\\r\\n";
}
~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)
{
GuardMutex lock_guard(_mutex);
if(std::filesystem::exists(_path))
{
return;
}
try
{
//C++17 创建目录
std::filesystem::create_directories(_path);
}
catch(const std::filesystem::filesystem_error &e)
{
std::cout << e.what() << '\\n';
}
}
virtual void SyncLog(const std::string& message) override
{
GuardMutex lockGuard(_mutex);
std::string filename = _path + (_path.back() == '/' ? "" : "/") + _file;
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;
};
//日志等级,从低到高 DEBUG < INFO < WARNNING < ERROR < FATAL
enum class LogLevel
{
DEBUG,
INFO,
WARNNING,
ERROR,
FATAL
};
inline std::string Level2Str(LogLevel level)
{
switch(level)
{
case LogLevel::DEBUG:
return "DEBUG";
case LogLevel::INFO:
return "INFO";
case LogLevel::WARNNING:
return "WARNNING";
case LogLevel::ERROR:
return "ERROR";
case LogLevel::FATAL:
return "FATAL";
default:
return "Unknown!";
}
}
inline std::string GetTimeStamp()
{
time_t curr = time(nullptr);
struct tm curr_tm;
localtime_r(&curr,&curr_tm);
char buffer[128];
snprintf(buffer,sizeof(buffer),"%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 buffer;
}
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_file,int line_number,Logger& logger)
:_level(level)
,_src_file(src_file)
,_line_number(line_number)
,_pid(getpid())
,_logger(logger)
,_time(GetTimeStamp())
{
std::stringstream ss;
ss << "[" << _time << "]"
<< "[" << Level2Str(_level) << "]"
<< "[" << _pid << "]"
<< "[" << _src_file << "]"
<< "[" << _line_number << "]"
<< "- ";
_log_info = ss.str();
}
template<typename T>
LogMessage& operator << (const T& info)
{
std::stringstream ss;
ss << info;
_log_info += ss.str();
return *this;
}
//❗**关键:临时对象析构,才执行日志落地输出**
~LogMessage()
{
if(_logger._fflush_strategy)
{
_logger._fflush_strategy->SyncLog(_log_info);
}
}
private:
std::string _time;
LogLevel _level;
pid_t _pid;
std::string _src_file;
int _line_number;
std::string _log_info;
Logger& _logger;
};
LogMessage operator()(LogLevel level,std::string src_file,int line_number)
{
return LogMessage(level,src_file,line_number,*this);
}
~Logger()
{}
private:
std::unique_ptr<LogStrategy> _fflush_strategy;
};
extern Logger log;
//宏自动填充__FILE__ __LINE__,简化调用
#define LOG(level) log(level,__FILE__,__LINE__)
#define Enable_File_Log_Strategy() log.EnableFileLogStrategy()
#define Enable_Console_Log_Strategy() log.EnableConsoleLogStrategy()
}
日志使用示例:
LOG(LogLevel::INFO) << "服务器启动成功";
Enable_File_Log_Strategy();
日志输出样例:
[2026-10-24 16:20:10][INFO][14523][UdpServer.hpp][40]- 创建套接字成功,套接字值为:4
💡核心原理:LogMessage是临时对象,执行完LOG(xxx) << "xxx";这一行,对象析构,才调用SyncLog完成日志输出。
2.3 InetAddr.hpp网络地址封装
将内核返回的sockaddr_in结构体进行封装,把网络字节序的端口、二进制IP转换成程序可读的字符串与uint16_t类型。
❗原始代码inet_ntoa使用静态缓冲区,多线程不安全,本项目替换为线程安全的inet_ntop。
#pragma once
#include<iostream>
#include<sys/socket.h>
#include<sys/types.h>
#include<netinet/in.h>
#include<arpa/inet.h>
#include"Log.hpp"
#include<string>
#include<strings.h>
#include<functional>
class InetAddr
{
public:
InetAddr(struct sockaddr_in& addr)
:_addr(addr)
{
//网络端口转主机字节序
_port = ntohs(_addr.sin_port);
char ipbuf[INET_ADDRSTRLEN];
//二进制ip转点分十进制字符串
inet_ntop(AF_INET,&_addr.sin_addr,ipbuf,INET_ADDRSTRLEN);
_ip = ipbuf;
}
std::string IP()
{
return _ip;
}
uint16_t Port()
{
return _port;
}
~InetAddr()
{}
private:
struct sockaddr_in _addr;
std::string _ip;
uint16_t _port;
};
2.4 UdpServer.hpp UDP服务端封装
UDP服务器类,负责socket创建、bind绑定端口,循环执行recvfrom接收客户端报文。
✨设计亮点:使用std::function回调,彻底解耦网络IO与上层业务代码,网络模块不需要关心具体是词典翻译还是其他业务。
#pragma once
#include<iostream>
#include<sys/socket.h>
#include<sys/types.h>
#include<netinet/in.h>
#include<arpa/inet.h>
#include"Log.hpp"
#include<string>
#include<strings.h>
#include<functional>
#include"InetAddr.hpp"
using namespace LogModule;
//回调函数签名:输入客户端字符串+客户端地址,返回处理完成要发送的字符串
using func = std::function<std::string(const std::string&,InetAddr&)>;
const int defaultfd = -1;
class UdpServer
{
public:
UdpServer(uint16_t port,func func)
:_sockfd(defaultfd)
,_port(port)
,_isrunning(false)
,_func(func)
{}
void Init()
{
_sockfd = socket(AF_INET,SOCK_DGRAM,0);
if(_sockfd < 0)
{
LOG(LogLevel::FATAL) << "创建套接字失败";
exit(1);
}
LOG(LogLevel::INFO) << "创建套接字成功,套接字值为:" << _sockfd;
struct sockaddr_in local;
bzero(&local,sizeof(local));
local.sin_family = AF_INET;
local.sin_port = htons(_port);
//❗INADDR_ANY监听本机所有网卡
local.sin_addr.s_addr = INADDR_ANY;
int n = bind(_sockfd,(struct sockaddr*)&local,sizeof(local));
if(n < 0)
{
LOG(LogLevel::FATAL) << "绑定失败";
exit(2);
}
LOG(LogLevel::INFO) << "绑定成功,套接字为:" << _sockfd;
}
void Start()
{
_isrunning = true;
while(_isrunning)
{
char buffer[1024];
struct sockaddr_in peer;
socklen_t len = sizeof(peer);
ssize_t s = recvfrom(_sockfd,buffer,sizeof(buffer) – 1,0,(struct sockaddr*)&peer,&len);
if(s > 0)
{
InetAddr client(peer);
//❗UDP不会自带\\0,手动补字符串结束符
buffer[s] = 0;
//调用外部传入的回调函数,执行业务逻辑
std::string res = _func(buffer,client);
sendto(_sockfd,res.c_str(),res.size(),0,(struct sockaddr*)&peer,len);
}
}
}
~UdpServer(){}
private:
int _sockfd;
uint16_t _port;
bool _isrunning;
func _func;
};
2.5 Dick.hpp词典业务模块
词典业务类,从本地文本文件读取词典内容,按英文: 中文分割,存入unordered_map哈希表做高速查询。对外提供Translate接口。
❗原始代码bug:DickLoad硬编码写死文件名,忽略构造函数传入的路径成员变量,本项目做修复。
#pragma once
#include<iostream>
#include<string>
#include<unordered_map>
#include"Log.hpp"
#include<fstream>
#include"InetAddr.hpp"
const std::string defaultdict = "./Dictionary.txt";
const std::string gap = ": ";
using namespace LogModule;
class Dick
{
public:
Dick(const std::string& path = ::defaultdict)
:_dick_path(path)
{}
bool DickLoad()
{
//使用成员变量,支持自定义词典路径
std::ifstream in(_dick_path);
if(!in.is_open())
{
LOG(LogLevel::FATAL) << "字典:" << _dick_path << "打开失败";
return false;
}
std::string line;
while(std::getline(in,line))
{
auto pos = line.find(gap);
if(pos == std::string::npos)
{
LOG(LogLevel::WARNNING) << "查找错误!";
continue;
}
std::string english = line.substr(0,pos);
std::string chinese = line.substr(pos + gap.size());
if(english.empty() || chinese.empty())
{
LOG(LogLevel::WARNNING) << "没有有效内容" << line;
continue;
}
_dick.insert(std::make_pair(english,chinese));
LOG(LogLevel::INFO) << "载入: " << line << "成功";
}
in.close();
return true;
}
//翻译接口:传入单词与客户端地址,返回释义,找不到返回"None"
std::string Translate(const std::string& word,InetAddr& client)
{
auto iter = _dick.find(word);
if(iter == _dick.end())
{
return "None";
}
LOG(LogLevel::DEBUG) << "已进入翻译模块," << "[" << client.IP() << ":" <<client.Port() << "]# " << word << iter->second;
return iter->second;
}
~Dick()
{}
private:
std::string _dick_path;
std::unordered_map<std::string,std::string> _dick;
};
2.6 Dictionary.txt词典数据
⚠️格式严格:分割符必须是冒号+空格 : ,要和代码中gap常量保持一致。
apple: 苹果
banana: 香蕉
cat: 猫
dog: 狗
book: 书
pen: 笔
happy: 快乐的
sad: 悲伤的
run: 跑
jump: 跳
teacher: 老师
student: 学生
car: 汽车
bus: 公交车
love: 爱
hate: 恨
hello: 你好
goodbye: 再见
summer: 夏天
winter: 冬天
2.7 main.cc主函数入口
组装全部模块,将词典翻译方法作为lambda回调传入UdpServer,启动服务器。补充全局log对象的定义,解决头文件extern声明链接报错。
#include"UdpServer.hpp"
#include"Dick.hpp"
//❗全局log对象定义,解决链接问题
namespace LogModule{
Logger log;
}
int main()
{
Enable_File_Log_Strategy();
Dick dict;
if(!dict.DickLoad())
{
LOG(LogLevel::FATAL) << "词典加载失败";
return 1;
}
uint16_t port = 8080;
//lambda表达式作为回调,网络层收到报文就会执行这个lambda
UdpServer server(port,[&dict](const std::string& word,InetAddr& client){
return dict.Translate(word,client);
});
server.Init();
server.Start();
return 0;
}
2.8 Makefile编译脚本
本项目依赖C++17标准,用到filesystem库,编译必须指定-std=c++17,链接线程库-lpthread。
CC=g++
CFLAGS=-std=c++17 -g -lpthread
src=main.cc
target=dick_server
$(target):$(src)
$(CC) $(src) -o $(target) $(CFLAGS)
.PHONY:clean
clean:
rm -f $(target)
rm -rf log
3 编译运行与测试
3.1 编译项目
把所有头文件、源文件放到同一目录,执行make命令完成编译。
make
3.2 启动UDP词典服务端
./dick_server
3.3 使用nc客户端测试
打开新终端,使用nc工具进行UDP交互测试。
nc -u 127.0.0.1 8080
输入英文单词,服务端返回中文释义,不存在单词返回None。
apple
苹果
dog
狗
test
None
网硕互联帮助中心




评论前必须登录!
注册