news 2026/10/27 10:26:09

Linux网络篇——UDP(一)之C++实现UDP英汉词典翻译服务器

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Linux网络篇——UDP(一)之C++实现UDP英汉词典翻译服务器

牢蒯的源码在下面的仓库里,各位大佬可以自行取用,古法编程,难免有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 项目文件清单

  1. Mutex.hpp:封装pthread互斥锁,实现RAII自动守卫锁
  2. Log.hpp:日志组件,支持控制台输出、文件落盘两种输出策略
  3. InetAddr.hpp:对sockaddr_in网络地址结构体封装,转换为可读IP与端口
  4. UdpServer.hpp:UDP服务端封装,使用std::function回调解耦网络IO和业务逻辑
  5. Dick.hpp:词典业务类,读取解析词典文件,提供单词翻译接口
  6. Dictionary.txt:词典数据源,格式固定为英文: 中文
  7. main.cc:程序入口,组装各个模块,启动UDP服务器
  8. Makefile:编译脚本

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接收客户端报文。

✨设计亮点:

1.使用std::function回调,彻底解耦网络IO与上层业务代码,网络模块不需要关心具体是词典翻译还是其他业务。

2.❗INADDR_ANY监听本机所有网卡

local.sin_addr.s_addr = INADDR_ANY;

因为一台机器(运行server的机器)上有多块网卡,因而就有多个ip地址,所以不能单单绑定一个ip地址;如果只绑定一个,当客户端使用另一块网卡的ip访问服务器时,就会访问不了。

INADDR_ANY将server绑定的ip变成0.0.0.0,就意味着client可以用server网卡的任意一个ip都能访问server。

#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

4 项目执行流程

  1. 程序启动,加载Dictionary.txt词典文件,将键值对存入unordered_map哈希表。
  2. UdpServer完成socket创建、bind绑定8080端口,进入循环等待接收UDP报文。
  3. 客户端发送英文单词,recvfrom拿到报文与客户端网络地址。
  4. 调用lambda回调,进入Dick的Translate方法做哈希查询。
  5. 查询结果返回网络层,sendto把结果发送回客户端。
  6. 全程各个关键节点打印日志,日志可以输出控制台或者写入本地log目录。

5 原始代码问题修复点

  1. GuardMutex参数值拷贝锁对象,修改为引用传递,保证锁逻辑生效。
  2. DickLoad函数硬编码默认词典路径,改为读取成员变量_dick_path。
  3. 替换线程不安全的inet_ntoa为inet_ntop。
  4. 补充全局Logger对象定义,解决多头文件extern声明链接报错。

6 可拓展优化方向

  1. 增加信号捕获,捕获SIGINT信号实现Ctrl+C优雅退出服务器。
  2. 当前为单线程UDP服务器,可以引入线程池提升并发处理能力。
  3. 实现词典热加载,不需要重启程序重新加载词典文本。
  4. 日志模块优化,消除每次写日志反复打开关闭文件的IO开销,实现日志按大小分割。
  5. 增加报文格式校验,过滤非法脏数据包。
版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/27 10:23:56

AI智能体席卷制造业,3000万人才缺口背后的程序员生存指南

2025年被称为AI智能体元年&#xff0c;制造业正经历从“人驱动”向“数智原生”的变革。企业招聘标准从经验筛选转向AI协同&#xff0c;急需既懂AI又懂制造的复合型人才。目前制造业人才缺口近3000万&#xff0c;蓝领招工难&#xff0c;高技能人才稀缺。本文揭示人才真相&#…

作者头像 李华
网站建设 2026/10/27 10:18:32

45.7MB 整合包,OpenClaw 双平台部署实操全记录

&#x1f4cc; 说明 本文以 OpenClaw 为例展开讲解&#xff0c;全程采用图形化、可视化的交互方式。整合包已内置全部运行依赖&#xff0c;即使是普通用户&#xff0c;也能轻松复现完整的部署流程。 ✨核心亮点&#xff1a; 全程可视化图形交互界面&#xff0c;自动补齐全部运行…

作者头像 李华