开篇:从 “回显” 到 “字典”,核心变在哪?

上一篇我们实现了 Echo 服务器 —— 网络层和业务层是 “绑死” 的:网络层收到数据后,直接把原数据发回去。但实际开发中,业务逻辑会复杂得多(比如查字典、查天气),如果每次改业务都要动网络代码,效率太低。

这篇的核心目标:用 “解耦” 的思想,把 UDP 服务器改造成字典服务—— 客户端输入英文单词,服务器返回中文翻译。你会学到:如何封装业务逻辑(字典加载与查询)、如何用 C++ 函数对象(std::function)分离网络层和业务层,以及如何封装 Socket 操作让代码更复用。

一、先搞懂:字典服务器的核心流程

字典服务器的逻辑比 Echo 稍复杂,但很清晰:

  1. 服务器启动时,加载dict.txt(存 “apple: 苹果” 这类键值对)到内存(用unordered_map存储,查询更快);

  2. 客户端发送英文单词(如 “apple”);

  3. 服务器接收单词后,查内存中的字典,得到中文翻译(如 “苹果”);

  4. 服务器把翻译结果发回客户端。

整个流程中,网络层只负责 “收发数据”,业务层只负责 “查字典”,两者互不干扰 —— 这就是解耦的精髓。

二、核心代码拆解:从字典类到解耦的服务器

我们分三部分讲:字典业务类(Dict)、解耦的 UDP 服务器(UdpServer)、封装版 Socket(可选,提升代码复用性)。

1. 第一步:封装字典业务 ——Dict

首先实现字典的 “加载” 和 “查询” 功能,这个类完全不涉及网络操作,纯业务逻辑。

(1)dict.txt文件格式

先准备一个简单的字典文件,每行是 “英文:中文”(注意冒号后有空格):

apple: 苹果banana: 香蕉cat: 猫dog: 狗book: 书happy: 快乐的hello: 你好goodbye: 再见
(2)Dict类代码实现
#pragma once
#include <iostream>
#include <string>
#include <fstream>  // 用于读取文件
#include <unordered_map>  // 用于存储字典(哈希表,查询O(1))// 分隔符:dict.txt里是“英文: 中文”,所以分隔符是“: ”
const std::string sep = ": ";class Dict {
public:// 构造函数:传入字典文件路径,初始化时加载字典Dict(const std::string &confpath) : _confpath(confpath) {LoadDict();  // 加载字典到内存}// 核心方法:查询单词,返回翻译(未查到返回“Unknown”)std::string Translate(const std::string &key) {auto iter = _dict.find(key);  // 哈希表查询if (iter == _dict.end()) {return "Unknown";  // 未找到}return iter->second;  // 返回中文翻译}private:// 私有方法:加载字典文件到_unordered_mapvoid LoadDict() {std::ifstream in(_confpath);  // 打开文件if (!in.is_open()) {  // 检查文件是否打开成功std::cerr << "open dict file error: " << _confpath << std::endl;return;}std::string line;// 逐行读取文件while (std::getline(in, line)) {if (line.empty()) continue;  // 跳过空行// 找到分隔符“: ”的位置auto pos = line.find(sep);if (pos == std::string::npos) {  // 没有找到分隔符,跳过这行continue;}// 截取英文(key)和中文(value)std::string key = line.substr(0, pos);  // 从0到pos的子串(英文)std::string value = line.substr(pos + sep.size());  // 分隔符后的子串(中文)_dict.insert(std::make_pair(key, value));  // 插入哈希表}in.close();  // 关闭文件std::cout << "load dict success! total words: " << _dict.size() << std::endl;}private:std::string _confpath;  // 字典文件路径std::unordered_map<std::string, std::string> _dict;  // 存储字典的哈希表
};

通俗解释

  • LoadDict():把dict.txt的内容读到_dict里,就像把 “单词 - 翻译” 存到一本 “快速查询手册” 里,以后查单词不用再读文件,直接查手册(内存),速度快。

  • Translate():给一个英文单词(key),查手册,有就返回翻译,没有就返回 “Unknown”。

  • 为什么用unordered_map?因为它是哈希表,查询速度是 O (1)(瞬间查到),如果用vector,查询要遍历所有元素,单词多了会很慢。

2. 第二步:解耦 UDP 服务器 —— 用std::function分离网络与业务

上一篇的UdpServer是 “网络层 + 业务层” 绑定的(直接回显),这篇我们改造它:让UdpServer只负责 “收发数据”,业务逻辑(查字典)通过 “函数对象” 传进来 —— 以后想改业务(比如改成天气查询),只需要传一个新的函数,不用动UdpServer的代码。

(1)改造后的UdpServer类核心代码
#pragma once
// 省略头文件(和上一篇类似,增加#include <functional>)
#include "nocopy.hpp"
#include "Log.hpp"
#include "Comm.hpp"
#include "InetAddr.hpp"const static uint16_t defaultport = 8888;
const static int defaultfd = -1;
const static int defaultsize = 1024;// 关键:定义函数对象类型func_t
// 输入:客户端的请求(req,如“apple”)
// 输出:服务器的响应(resp,如“苹果”)
using func_t = std::function<void(const std::string &req, std::string *resp)>;class UdpServer : public nocopy {
public:// 构造函数:传入业务逻辑函数(func)和端口UdpServer(func_t func, uint16_t port = defaultport) : _func(func), _port(port), _sockfd(defaultfd) {}// Init()方法:和上一篇完全一样(创建socket、绑定)void Init() {// 代码和上一篇相同,省略...}// Start()方法:改造业务逻辑调用void Start() {char buffer[defaultsize];for (;;) {  // 死循环运行struct sockaddr_in peer;socklen_t len = sizeof(peer);// 1. 接收客户端请求(和上一篇一样)ssize_t n = recvfrom(_sockfd, buffer, sizeof(buffer)-1, 0, (struct sockaddr *)&peer, &len);if (n > 0) {buffer[n] = 0;InetAddr addr(peer);std::cout << "[" << addr.PrintDebug() << "]# " << buffer << std::endl;// 2. 调用业务逻辑函数(查字典),而不是直接回显std::string resp;  // 存储响应结果_func(buffer, &resp);  // 传入请求,获取响应(解耦的核心!)// 3. 发送响应给客户端(和上一篇一样)sendto(_sockfd, resp.c_str(), resp.size(), 0, (struct sockaddr *)&peer, len);}}}~UdpServer() {if (_sockfd != defaultfd) {close(_sockfd);  // 析构时关闭socket}}private:int _sockfd;uint16_t _port;func_t _func;  // 存储业务逻辑函数(查字典、回显等)
};

解耦的核心:func_t_func

  • func_t是一个函数对象类型,它规定了 “业务函数” 的格式:必须接收const std::string &req(请求)和std::string *resp(响应的指针,用于输出结果)。

  • _funcUdpServer的成员变量,存储传入的业务函数。在Start()中,服务器收到请求后,不自己处理,而是调用_func(req, &resp),让业务函数生成响应 —— 这样网络层和业务层就完全分开了。

3. 第三步:主函数 —— 组装服务器和业务逻辑

有了Dict类和改造后的UdpServer,主函数的工作就是 “组装”:创建字典对象、定义业务函数、创建服务器并启动。

#include "UdpServer.hpp"
#include "Comm.hpp"
#include "Dict.hpp"
#include <memory>  // 用于智能指针(可选,避免内存泄漏)// 全局字典对象:启动时加载dict.txt
Dict gdict("./dict.txt");// 业务逻辑函数:符合func_t的格式
void Execute(const std::string &req, std::string *resp) {// 调用Dict的Translate方法,把结果存入resp*resp = gdict.Translate(req);
}// 主函数:解析参数,启动服务器
int main(int argc, char *argv[]) {// 检查参数:需要传入端口号(如./udp_server 8888)if (argc != 2) {std::cout << "Usage: " << argv[0] << " local_port" << std::endl;return Usage_Err;}uint16_t port = std::stoi(argv[1]);  // 解析端口号// 创建服务器:传入业务函数Execute和端口// 用智能指针(std::unique_ptr)管理服务器对象,自动释放内存std::unique_ptr<UdpServer> usvr = std::make_unique<UdpServer>(Execute, port);// 初始化并启动服务器usvr->Init();usvr->Start();return 0;
}

关键细节

  • gdict是全局的字典对象:因为字典只需要加载一次(启动时),全局对象会在main前初始化,避免每次查询都重新加载文件。

  • Execute函数:就是把DictTranslate方法包装成func_t格式 —— 输入req(英文单词),输出resp(中文翻译)。

  • 智能指针std::unique_ptr:避免手动delete服务器对象,防止内存泄漏,是 C++ 中推荐的做法。

4. 可选:封装 Socket 操作 ——udp_socket.hpp

文档里还提供了一个 “封装版” 的UdpSocket类,把socketbindrecvfromsendto这些系统调用封装成类方法,让代码更简洁、复用性更高。

核心封装代码示例:

#pragma once
#include <stdio.h>
#include <string.h>
#include <string>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>class UdpSocket {
public:UdpSocket() : fd_(-1) {}// 创建socketbool Socket() {fd_ = socket(AF_INET, SOCK_DGRAM, 0);if (fd_ < 0) {perror("socket");  // 打印错误信息return false;}return true;}// 绑定IP和端口bool Bind(const std::string& ip, uint16_t port) {sockaddr_in addr;addr.sin_family = AF_INET;addr.sin_addr.s_addr = inet_addr(ip.c_str());addr.sin_port = htons(port);int ret = bind(fd_, (struct sockaddr*)&addr, sizeof(addr));if (ret < 0) {perror("bind");return false;}return true;}// 接收数据:输出buf(消息)、ip(发送方IP)、port(发送方端口)bool RecvFrom(std::string* buf, std::string* ip = NULL, uint16_t* port = NULL) {char tmp[1024*10] = {0};sockaddr_in peer;socklen_t len = sizeof(peer);ssize_t read_size = recvfrom(fd_, tmp, sizeof(tmp)-1, 0, (struct sockaddr*)&peer, &len);if (read_size < 0) {perror("recvfrom");return false;}buf->assign(tmp, read_size);  // 把接收的字节存入bufif (ip != NULL) {*ip = inet_ntoa(peer.sin_addr);  // 转换IP为字符串}if (port != NULL) {*port = ntohs(peer.sin_port);  // 转换端口为主机字节序}return true;}// 发送数据:输入buf(消息)、ip(接收方IP)、port(接收方端口)bool SendTo(const std::string& buf, const std::string& ip, uint16_t port) {sockaddr_in addr;addr.sin_family = AF_INET;addr.sin_addr.s_addr = inet_addr(ip.c_str());addr.sin_port = htons(port);ssize_t write_size = sendto(fd_, buf.data(), buf.size(), 0, (struct sockaddr*)&addr, sizeof(addr));if (write_size < 0) {perror("sendto");return false;}return true;}// 关闭socketbool Close() {if (fd_ != -1) {close(fd_);fd_ = -1;}return true;}private:int fd_;  // socket文件句柄
};

封装的好处

  • 不用重复写struct sockaddr_in、字节序转换这些繁琐的代码;

  • 错误处理更统一(用perror打印错误,返回bool表示成功 / 失败);

  • 后续写其他 UDP 程序(如聊天室),可以直接用这个类,不用重新写 Socket 操作。

三、动手运行:测试字典服务

和上一篇的 Echo 服务器运行步骤类似,客户端可以复用上一篇的(因为客户端只负责收发字符串,不关心服务器的业务逻辑)。

1. 准备文件

  • dict.txt:按前面的格式准备好单词和翻译;

  • 编译服务器:g++ ``main.cc`` UdpServer.cpp Dict.cpp -o udp_server -std=c++11(如果拆分了.cpp 文件);

  • 客户端用上一篇的udp_client

2. 运行测试

  • 启动服务器:./udp_server 8888,会看到load dict success! total words: 10(根据dict.txt的单词数而定);

  • 启动客户端:./udp_client ``127.0.0.1`` 8888

  • 输入 “apple”,客户端会显示server echo# 苹果;输入 “test”,会显示server echo# Unknown

四、总结与思考

这篇我们实现了一个 “可扩展” 的字典服务器,核心收获是:

  1. 业务逻辑封装:用Dict类把 “加载字典” 和 “查询翻译” 封装起来,纯业务不沾网络;

  2. 网络与业务解耦:用std::functionUdpServer只负责收发数据,业务逻辑通过函数对象传入,灵活可换;

  3. Socket 封装:用UdpSocket类简化 Socket 操作,提升代码复用性。

思考问题

如果想让多个客户端同时用字典服务,当前的服务器能应付吗?因为Start()是单循环,一次只能处理一个客户端的请求 —— 如果客户端多了,会有延迟。下一篇我们讲如何用 “线程池” 实现并发处理,还会实现一个支持多客户端聊天的 UDP 聊天室。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。
如若转载,请注明出处:http://www.pswp.cn/diannao/99193.shtml
繁体地址,请注明出处:http://hk.pswp.cn/diannao/99193.shtml
英文地址,请注明出处:http://en.pswp.cn/diannao/99193.shtml

如若内容造成侵权/违法违规/事实不符,请联系英文站点网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

数据结构之复杂度

数据结构的理解 数据本身是杂乱无章的&#xff0c;需要结构进行增删查改等操作更好的管理数据&#xff1b; 比如&#xff1a;在程序中需要将大量的代码&#xff08;数据&#xff09;通过结构进行管理&#xff1b; 再比如&#xff1a;定义1000个整型变量的数组&#xff0c;我们…

运维安全06 - 服务安全

云计算服务安全 在当今数字化时代&#xff0c;各种服务&#xff08;如网络应用、云计算平台、数据库系统等&#xff09;已成为我们日常生活和工作中不可或缺的一部分。 然而&#xff0c;随着服务的广泛应用&#xff0c;其安全性问题也日益凸显。 一、服务安全 服务安全是一…

01数据结构-初探动态规划

01数据结构-初探动态规划前言1.基本思想2.重叠子问题3.斐波那契数列4.备忘录&#xff08;记忆化搜索表&#xff09;4.1备忘录&#xff08;记忆化搜索表&#xff09;代码实现5.DP table5.1DP table代码实现6.练习前言 在学习动态规划时切忌望文生义&#xff0c;因为其名字与其思…

[智能算法]可微的神经网络搜索算法-FBNet

一、概述 相较于基于强化学习的NAS&#xff0c;可微NAS能直接使用梯度下降更新模型结构超参数&#xff0c;其中较为有名的算法就是DARTS&#xff0c;其具体做法如下。 首先&#xff0c;用户需要定义一些候选模块&#xff0c;这些模块内部结构可以互不相同&#xff08;如设置不同…

Elasticsearch安装启动常见问题全解析

文章目录&#x1f4da; Elasticsearch 安装与启动问题总结一、核心问题概览二、详细问题分析与解决方案1. &#x1f510; **权限问题&#xff1a;AccessDeniedException**❌ 错误日志&#xff1a;&#x1f4cc; 原因&#xff1a;✅ 解决方案&#xff1a;2. ⚙️ **配置冲突&…

Uniapp中使用renderjs实现OpenLayers+天地图的展示与操作

Uniapp中自带的地图组件对支持的地图服务略有局限&#xff0c;同时&#xff0c;该组件在样式布局上层级过高且无法控制&#xff0c;无法满足部分高度自定义化的需求。故引入renderjs视图层工具搭配OpenLayers框架对地图功能进行实现&#xff0c;但由于renderjs的限制&#xff0…

从C++开始的编程生活(8)——内部类、匿名对象、对象拷贝时的编译器优化和内存管理

前言 本系列文章承接C语言的学习&#xff0c;需要有C语言的基础才能学会哦~ 第8篇主要讲的是有关于C的内部类、匿名对象、对象拷贝时的编译器优化和内存管理。 C才起步&#xff0c;都很简单&#xff01;&#xff01; 目录 前言 内部类 性质 匿名对象 性质 ※对象拷贝时的…

MT5追大速率回测BUG

将MT5策略测试器中的回测速率调到最大(最快速度),**确实非常容易导致出现不符合策略逻辑的秒级成交(闪电交易)**。这并非MT5的“bug”,而是由**回测引擎的工作方式**与**策略代码的编写方法**在高速运行下不匹配所导致的。 --- ### 为什么最大速率会导致问题? MT5回测…

[数据结构——lesson10.堆及堆的调整算法]

引言 上节我们学习完二叉树后[数据结构——lesson9.二叉树]&#xff0c;这节我们将学习数据结构——堆 学习目标 1.堆的概念及结构 堆是一种特殊的完全二叉树结构&#xff0c;在计算机科学和数据结构中广泛应用&#xff0c;特别是在堆排序算法和优先队列的实现中&#xff0c;…

九识智能与北控北斗合作研发的L4级燃气超微量高精准泄漏检测无人车闪耀服贸会,守护城市安全

2025年9月10日至14日&#xff0c;2025年中国国际服务贸易交易会将于北京首钢园举办。在这场国际盛会上&#xff0c;九识智能与北京北控北斗科技投资有限公司&#xff08;以下简称“北控北斗”&#xff09;合作研发的L4级燃气超微量高精准泄漏检测无人车及相关系统解决方案&…

【C语言入门】手把手教你实现顺序栈

栈是计算机科学中最基础且重要的数据结构之一&#xff0c;它遵循"后进先出"&#xff08;LIFO&#xff09;的原则。想象一下一叠盘子&#xff0c;你只能从最上面取放&#xff0c;这就是栈的直观体现。本文将用C语言带你一步步实现一个顺序栈&#xff0c;即使你是编程小…

北斗导航 | ARAIM(高级接收机自主完好性监测)算法在民航LPV-200进近中的具体实现流程

要详细说明ARAIM(高级接收机自主完好性监测)算法在民航LPV-200进近中的具体实现流程,需结合ARAIM的核心逻辑(多星座融合、多假设解分离、风险优化分配)与LPV-200的严格要求(垂直保护级VPL≤35米、垂直告警限VAL=35米、有效监测门限EMT≤15米等),以下是 step-by-step 的…

AIPex:AI + 自然语言驱动的浏览器自动化扩展

AIPex:AI + 自然语言驱动的浏览器自动化扩展 引言 一、快速上手 1.1 安装AIPex扩展 1.2 首次配置 1.3 界面介绍 第二章:30+工具详解 2.1 标签页管理工具集 🗂️ **get_all_tabs - 全局标签页概览** 🎯 **switch_to_tab - 智能标签页切换** 📋 **标签页批量操作** 📋 …

机器学习模型可信度与交叉验证:通俗讲解

先从一个故事说起&#xff1a;农场里的火鸡科学家&#xff0c;观察了一年发现“每天上午11点必有食物”&#xff0c;结果感恩节当天&#xff0c;它没等到食物&#xff0c;反而成了人类的食物。这个故事告诉我们&#xff1a;只靠过去的经验下结论&#xff0c;很可能出错——机器…

HTML5和CSS3新增的一些属性

1、HTML5新增特性这些新特性都有兼容性问题&#xff0c;基本是IE9以上版本浏览器才支持1&#xff09;新增语义化标签2&#xff09;新增多媒体标签音频&#xff1a;<audio>视频&#xff1a;<video>&#xff08;1&#xff09;视频<video>---尽量使用mp4格式<…

Redis的RedLock

RedLock算法深度解析RedLock是Redis作者针对分布式环境设计的多节点锁算法&#xff0c;核心目标是解决单点Redis在分布式锁场景中的可靠性缺陷。传统方案的局限性单节点Redis锁的问题单点故障&#xff1a;单个Redis实例宕机导致所有锁服务不可用可靠性不足&#xff1a;无法保证…

SpringMVC @RequestMapping的使用演示和细节 详解

目录 一、RequestMapping是什么&#xff1f; 二、RequestMapping 的使用演示 1.RequestMapping在方法上的使用&#xff1a; 2.RequestMapping同时在类和方法上使用&#xff1a; 3.RequestMapping指定请求参数&#xff1a; 4.RequestMapping使用Ant风格URL&#xff1a; 5.Requ…

flutter项目 -- 换logo、名称 、签名、打包

1、换logo, 透明底&#xff0c;下面5个尺寸&#xff0c;需要UI设计2、换名没配置型的改名方式如下 打开app/src/main/AndroidManifest.xml3、签名 运行 flutter doctor -vD:\project\Apk\keystore 自己建立的keystore文件夹&#xff0c; 注意命令后是 megoai-release-key(自…

【贪心算法】day9

&#x1f4dd;前言说明&#xff1a; 本专栏主要记录本人的贪心算法学习以及LeetCode刷题记录&#xff0c;按专题划分每题主要记录&#xff1a;&#xff08;1&#xff09;本人解法 本人屎山代码&#xff1b;&#xff08;2&#xff09;优质解法 优质代码&#xff1b;&#xff…

linux C 语言开发 (八) 进程基础

文章的目的为了记录使用C语言进行linux 开发学习的经历。开发流程和要点有些记忆模糊&#xff0c;赶紧记录&#xff0c;防止忘记。 相关链接&#xff1a; linux C 语言开发 (一) Window下用gcc编译和gdb调试 linux C 语言开发 (二) VsCode远程开发 linux linux C 语言开发 (…