UDP Socket 進階:從 Echo 到字典服務器,學會 “解耦” 網絡與業務

開篇:從 “回顯” 到 “字典”,核心變在哪?

上一篇我們實現了 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 語言開發 (…