UDP應用1:翻譯軟件
本篇介紹
本篇基于UDP編程接口基本使用中封裝的服務器和客戶端進行改寫,基本功能如下:
- 從配置文件
dict.txt
讀取到所有的單詞和意思 - 客戶端向服務端發送英文
- 服務端向客戶端發送英文對應的中文意思
配置文件內容
下面的內容是本次實現使用的配置文件
apple: 蘋果
banana: 香蕉
cat: 貓
dog: 狗
book: 書
pen: 筆
happy: 快樂的
sad: 悲傷的
run: 跑
jump: 跳
teacher: 老師
student: 學生
car: 汽車
bus: 公交車
love: 愛
hate: 恨
hello: 你好
goodbye: 再見
summer: 夏天
winter: 冬天
設計翻譯軟件
根據上面對功能描述,下面對功能實現進行具體分析
設計字典類
既然是字典類,那么對應的就需要一個便于查詢的結構,本次以哈希表為例
因為服務端主要是接收客戶端的數據,所以服務端本身不直接處理翻譯功能,而是交給一個函數進行處理,但是如果這個函數直接裸露在外部就會導致字典本身和翻譯函數分開,所以本次考慮將翻譯函數作為字典類的成員函數
需要注意,本次默認使用的配置文件中英文單詞和中文意思使用的是一個冒號和空格進行分割,即:
,可以考慮讓用戶自己提供配置文件和分割符,所以類基本結構如下:
// 默認配置文件位置和配置文件
const std::string default_path = "./";
const std::string default_file = "dict.txt";// 默認分隔符
const std::string default_sep = ": ";class Dictionary
{
public:Dictionary(const std::string &path = default_path, const std::string &file = default_file, const std::string &sep = default_sep): _path(path), _file(file), _sep(sep){// 1. 讀取配置文件// 2. 分割字符串并將key和value添加到哈希表}// 翻譯std::string translate(const std::string &word){}~Dictionary(){}private:std::unordered_map<std::string, std::string> _dict; // 字典哈希表std::string _path; // 配置文件路徑std::string _file; // 配置文件std::string _sep; // 分隔符
};
讀取配置文件并分割
首先是讀取配置文件,本次考慮將從配置文件中讀取到的字符串通過分割成key
和value
依次存入到一個哈希表中。并且因為是字典類,所以考慮在創建字典類對象時就完成前面的讀取和存儲操作
根據上面的思路需要考慮兩個步驟:
- 獲取并讀取配置文件
- 分割字符串獲取到
key
和value
存儲到哈希表
讀取配置文件的操作就是打開指定的文件并讀取其中的內容,在本次配置文件的內容中,一行為一組數據,所以在讀取文件內容時需要按照行讀取
讀到一行數據后,需要將該行數據按照指定的分隔符進行切割存入key
和value
中,這里有兩種思路:
- 自行實現分割邏輯
- 使用庫函數
本次考慮使用第一種思路,那么對于分割邏輯來說,基本思路如下:
- 找到
:
第一次出現的位置,以該位置為終點,以字符串第一個字符為起點,切出第一個子串作為key
- 找到分隔符的下一個字符,以該位置為起點,以字符串結尾為終點,切出第二個子串作為
value
將獲取到的key
和value
存入到哈希表中
重復上面的步驟直到讀取完畢文件,將文件關閉,代碼如下:
// 1. 讀取配置文件
std::string dictPath = default_path + default_file;
// 1.1 打開文件
std::fstream out(dictPath);// 2. 分割字符串并將key和value添加到哈希表
std::string message;
while (std::getline(out, message))
{// 分割字符串auto word_end = message.find(_sep, 0);std::string key = message.substr(0, word_end);std::string value = message.substr(word_end + sep.size(), message.size());// 存儲到哈希表中_dict.insert({key, value});
}out.close();
翻譯函數
所謂翻譯就是在哈希表中根據指定字符串查找對應的value
并返回,基本代碼如下:
// 翻譯
std::string translate(const std::string &word)
{auto pos = _dict.find(word);if (pos == _dict.end())return "無指定單詞對應的中文意思";return pos->second;
}
修改服務端類
本次服務端做的任務就是接收客戶端發送的數據,再調用字典類的翻譯函數將翻譯結果返回給客戶端,所以實際上需要修改的就是兩點:
- 服務端需要拿到翻譯函數
- 執行翻譯功能并返回結果給客戶端
首先修改第一點,因為服務端本身沒有翻譯函數,所以需要外界傳遞,此時可以考慮在構造服務端時要求外部傳遞翻譯函數,對應地服務端就需要一個成員用于接收這個函數:
using task_t = std::function<std::string(const std::string &)>;class UdpServer
{
public:UdpServer(task_t t, uint16_t port = default_port): // ...,_translate(t){// ...}// ...
private:// ...task_t _translate; // 翻譯函數
};
需要注意,在構造函數中,沒有默認參數的形式參數一定要寫在有默認參數的形式參數之前,具體原因見C++中的缺省參數
接著修改第二點,執行翻譯函數并將結果返回給客戶端,這一步主要涉及到服務端的任務部分。在上一節中,主要任務是將客戶端發送的信息再發給客戶端,這次就是調用翻譯函數再將結果返回給客戶端,所以代碼如下:
// 啟動服務器
void start()
{if (!_isRunning){_isRunning = true;while (true){// 1. 接收客戶端信息// ...if (ret > 0){// ...// 翻譯std::string ret = _translate(buffer);ssize_t n = sendto(_socketfd, ret.c_str(), ret.size(), 0, &temp, temp.getLength());// ...}}}
}
修改服務端主函數
主要修改的地方就是創建服務端對象部分,因為需要傳遞一個翻譯任務函數,所以需要先創建一個字典類對象,再調用字典類對象中的翻譯方法,需要注意的是,不能直接將字典類的翻譯函數作為參數傳遞給服務端對象的構造函數,因為該函數的參數列表除了需要顯示傳遞的字符串對象外,還存在一個字典類對象,這并不符合服務端類構造函數要求的函數類型,這里提供兩個方法:
- 使用綁定,將字典類對象作為固定參數綁定給翻譯函數
- 使用lambda表達式,在表達式中調用翻譯函數
即:
=== “綁定”
// 創建字典類對象
std::shared_ptr<Dictionary> dict;
// 創建UdpServerModule對象
std::shared_ptr<UdpServer> udp_server = = std::make_shared<UdpServer>(std::bind(&Dictionary::translate, dict.get(), std::placeholders::_1));
=== “lambda表達式”
// 創建字典類對象
std::shared_ptr<Dictionary> dict;
// 創建UdpServerModule對象
std::shared_ptr<UdpServer> udp_server = = std::make_shared<UdpServer>([&dict](const std::string word){ dict->translate(word);
});
測試
以綁定為例,服務端整體代碼如下:
#include "udp_server.hpp"
#include "dictionary.hpp"
#include <memory>using namespace UdpServerModule;
using namespace DictionaryModule;int main(int argc, char *argv[])
{// 創建字典類對象std::shared_ptr<Dictionary> dict;// 創建UdpServerModule對象std::shared_ptr<UdpServer> udp_server;if (argc == 1){// 綁定udp_server = std::make_shared<UdpServer>(std::bind(&Dictionary::translate, dict.get(), std::placeholders::_1));}else if (argc == 2){// std::string ip = argv[1]; 去除uint16_t port = std::stoi(argv[1]);udp_server = std::make_shared<UdpServer>(std::bind(&Dictionary::translate, dict.get(), std::placeholders::_1), port);}else{LOG(LogLevel::ERROR) << "錯誤使用,正確使用:" << argv[0] << " 端口(或者不寫)";exit(4);}udp_server->start();return 0;
}
客戶端代碼保持和上一節一樣,此處不再展示,運行結果如下: