一、项目背景
在上一篇博客中,我们掌握了 UDP Socket 编程的核心基础和接口,本次将基于这些知识实现第一个实战项目 ------简易英译汉翻译服务器
该项目采用C/S 架构(客户端 / 服务端),基于 UDP 协议实现,核心功能是:客户端输入英文单词,发送到服务端,服务端查询字典并返回对应的中文释义,若单词不存在则返回 "Unknown / 未查到"
二、项目整体设计
1. 架构设计
- 服务端:负责加载字典文件到内存、监听固定端口、接收客户端的单词请求、查询字典并返回释义
- 客户端:负责获取用户输入的英文单词、发送到服务端、接收服务端返回的中文释义并打印
- 通信协议:UDP(无连接,无需维护客户端状态,服务端可同时响应多个客户端请求)
2. 核心功能模块
服务端模块
- Socket 通信模块:基于 UDP Socket 实现,包含创建、绑定、接收、发送接口
- 字典加载模块:从本地字典文件(txt)读取英文单词 - 中文释义映射,存储到哈希表(unordered_map),提高查询效率
- 业务处理模块 :通过回调函数实现,接收客户端的单词请求,查询哈希表并生成响应结果
- 主逻辑模块:初始化 Socket、加载字典、进入循环接收请求,调用业务处理模块并返回响应
客户端模块
- Socket 通信模块:基于 UDP Socket 实现,创建 Socket、发送单词请求、接收服务端响应
- 用户交互模块:获取用户从标准输入的英文单词,打印服务端返回的中文释义
- 主逻辑模块:初始化 Socket、循环处理用户输入、与服务端通信
3. 字典文件设计
字典文件采用txt 格式 ,每行一个键值对,格式为英文单词: 中文释义(冒号后加空格,作为分隔符),示例(dict.txt):
apple: 苹果
banana: 香蕉
cat: 猫
dog: 狗
book: 书
happy: 快乐的
sad: 悲伤的
hello: 你好
world: 世界
Unknown: 未查到
三、核心技术点解析
1. 字典加载与查询 ------ 哈希表(unordered_map)
为了提高单词查询效率,服务端将字典文件的内容加载到C++STL 的 unordered_map 中,key为英文单词(string),value为中文释义(string),查询时间复杂度为 O (1)。
核心步骤:
- 打开字典文件(ifstream)
- 逐行读取文件内容,通过分隔符
:分割为单词和释义 - 将键值对插入到 unordered_map 中
- 提供查询接口,根据单词返回释义,不存在则返回 "未查到"
2. 代码封装思想 ------ 类的封装
将重复的功能封装为类,提高代码的复用性和可维护性,本次项目主要封装两个类:
- Dict 类 :封装字典的加载和查询功能,对外提供
Translate()接口 - UdpServer 类 :封装 UDP Socket 服务端的核心接口(创建、绑定、启动),通过回调函数解耦 "网络通信" 和 "业务处理"
3. 回调函数解耦 ------function<void (const string&, string*)>
服务端的核心是 "网络通信" 和 "业务处理",为了让 UdpServer 类更通用(可适配不同业务),采用C++11 的 function 回调函数将业务处理逻辑传递给 UdpServer 类,实现网络层与业务层的解耦
即 UdpServer 类只负责网络通信(接收请求、发送响应),不关心具体的业务处理,业务处理由外部的回调函数实现(本次为字典查询)
四、完整代码实现(C++)
1. 字典文件(dict.txt)
创建上述格式的 dict.txt,与服务端可执行文件放在同一目录下
2. 服务端代码(dict_server.cpp)
整合 Dict 类和 UdpServer 类,实现核心功能:
#include <iostream>
#include <string>
#include <cstring>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <fstream>
#include <unordered_map>
#include <functional>
#include <cstdlib>
#include <cerrno>
using namespace std;
// 全局常量
const uint16_t DEFAULT_PORT = 8888;
const int DEFAULT_BUFF_SIZE = 1024;
const string DICT_PATH = "./dict.txt";
const string SEP = ": ";
const string UNKNOWN = "未查到";
// 字典类:封装加载和查询功能
class Dict {
private:
unordered_map<string, string> _dict; // 存储单词-释义映射
// 加载字典文件到哈希表
void LoadDict() {
ifstream in(DICT_PATH);
if (!in.is_open()) {
cerr << "打开字典文件失败:" << strerror(errno) << endl;
return;
}
string line;
while (getline(in, line)) {
if (line.empty()) continue;
size_t pos = line.find(SEP);
if (pos == string::npos) continue; // 格式错误,跳过
string key = line.substr(0, pos);
string value = line.substr(pos + SEP.size());
_dict.insert(make_pair(key, value));
}
in.close();
cout << "字典加载完成,共加载" << _dict.size() << "个单词" << endl;
}
public:
// 构造函数:初始化时加载字典
Dict() {
LoadDict();
}
// 查询接口:根据单词返回释义
string Translate(const string& key) {
auto iter = _dict.find(key);
if (iter == _dict.end()) {
return UNKNOWN;
}
return iter->second;
}
};
// UDP服务端类:封装网络通信功能,解耦业务处理
class UdpServer {
private:
int _sockfd; // Socket描述符
uint16_t _port; // 监听端口
function<void(const string&, string*)> _func; // 业务处理回调函数
// 初始化Socket:创建+绑定
bool Init() {
// 1. 创建UDP Socket
_sockfd = socket(AF_INET, SOCK_DGRAM, 0);
if (_sockfd < 0) {
cerr << "创建Socket失败:" << strerror(errno) << endl;
return false;
}
// 2. 绑定IP和端口
struct sockaddr_in local;
memset(&local, 0, sizeof(local));
local.sin_family = AF_INET;
local.sin_port = htons(_port);
local.sin_addr.s_addr = INADDR_ANY; // 绑定本机所有IP
if (bind(_sockfd, (struct sockaddr*)&local, sizeof(local)) < 0) {
cerr << "绑定端口失败:" << strerror(errno) << endl;
close(_sockfd);
return false;
}
cout << "UDP服务端初始化成功,监听端口:" << _port << endl;
return true;
}
public:
// 构造函数:传入端口和业务处理回调函数
UdpServer(uint16_t port = DEFAULT_PORT, function<void(const string&, string*)> func = nullptr)
: _port(port), _func(func), _sockfd(-1) {}
// 析构函数:关闭Socket
~UdpServer() {
if (_sockfd >= 0) {
close(_sockfd);
}
}
// 启动服务端:循环接收请求,调用回调函数处理,返回响应
void Start() {
if (!Init() || _func == nullptr) {
cerr << "服务端启动失败" << endl;
return;
}
char buffer[DEFAULT_BUFF_SIZE] = {0};
while (true) { // 无限循环,持续提供服务
struct sockaddr_in peer;
socklen_t peer_len = sizeof(peer);
// 接收客户端请求
ssize_t n = recvfrom(_sockfd, buffer, sizeof(buffer)-1, 0, (struct sockaddr*)&peer, &peer_len);
if (n > 0) {
buffer[n] = 0;
string req = buffer;
string resp;
// 调用回调函数处理业务(查询字典)
_func(req, &resp);
// 向客户端返回响应
sendto(_sockfd, resp.c_str(), resp.size(), 0, (struct sockaddr*)&peer, peer_len);
// 打印日志
string peer_ip = inet_ntoa(peer.sin_addr);
uint16_t peer_port = ntohs(peer.sin_port);
cout << "[" << peer_ip << ":" << peer_port << "] 查词:" << req << " → " << resp << endl;
}
}
}
};
// 业务处理函数:调用Dict的Translate接口
void HandleRequest(const string& req, string* resp) {
static Dict dict; // 静态对象,只加载一次字典
*resp = dict.Translate(req);
}
// 主函数
int main(int argc, char* argv[]) {
// 可选:通过命令行指定端口,如./dict_server 9999
uint16_t port = DEFAULT_PORT;
if (argc == 2) {
port = atoi(argv[1]);
}
// 创建UDP服务端,传入端口和业务处理回调函数
UdpServer server(port, HandleRequest);
// 启动服务端
server.Start();
return 0;
}
3. 客户端代码(dict_client.cpp)
实现用户交互和与服务端的通信:
#include <iostream>
#include <string>
#include <cstring>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <cerrno>
#include <cstdlib>
using namespace std;
// 全局常量
const int DEFAULT_BUFF_SIZE = 1024;
// 用法提示
void Usage(const string& proc) {
cout << "Usage: " << proc << " server_ip server_port" << endl;
cout << "Example: " << proc << " 127.0.0.1 8888" << endl;
}
// 客户端主逻辑
int main(int argc, char* argv[]) {
// 校验命令行参数:./dict_client server_ip server_port
if (argc != 3) {
Usage(argv[0]);
return 1;
}
string server_ip = argv[1];
uint16_t server_port = atoi(argv[2]);
// 1. 创建UDP Socket
int sockfd = socket(AF_INET, SOCK_DGRAM, 0);
if (sockfd < 0) {
cerr << "创建Socket失败:" << strerror(errno) << endl;
return 2;
}
// 2. 填充服务端地址结构
struct sockaddr_in server;
memset(&server, 0, sizeof(server));
server.sin_family = AF_INET;
server.sin_port = htons(server_port);
server.sin_addr.s_addr = inet_addr(server_ip.c_str());
if (server.sin_addr.s_addr == INADDR_NONE) {
cerr << "IP地址格式错误" << endl;
close(sockfd);
return 3;
}
cout << "客户端启动成功,连接服务端:" << server_ip << ":" << server_port << endl;
cout << "请输入英文单词(输入q退出):" << endl;
string word;
char buffer[DEFAULT_BUFF_SIZE] = {0};
while (true) {
cout << ">> ";
getline(cin, word);
// 输入q退出
if (word == "q" || word == "Q") {
cout << "客户端退出" << endl;
break;
}
if (word.empty()) {
continue;
}
// 3. 发送单词请求到服务端
ssize_t n = sendto(sockfd, word.c_str(), word.size(), 0, (struct sockaddr*)&server, sizeof(server));
if (n < 0) {
cerr << "发送失败:" << strerror(errno) << endl;
continue;
}
// 4. 接收服务端的中文释义
struct sockaddr_in temp;
socklen_t temp_len = sizeof(temp);
ssize_t m = recvfrom(sockfd, buffer, sizeof(buffer)-1, 0, (struct sockaddr*)&temp, &temp_len);
if (m > 0) {
buffer[m] = 0;
cout << word << " → " << buffer << endl;
} else {
cerr << "接收失败:" << strerror(errno) << endl;
}
// 清空缓冲区
memset(buffer, 0, sizeof(buffer));
}
// 5. 关闭Socket
close(sockfd);
return 0;
}
五、编译与运行
1. 编译代码
# 编译服务端
g++ -o dict_server dict_server.cpp -std=c++11
# 编译客户端
g++ -o dict_client dict_client.cpp -std=c++11
2. 启动服务端
# 方式1:使用默认端口8888
./dict_server
# 方式2:指定端口,如9999
./dict_server 9999
服务端启动成功会输出:字典加载完成,共加载N个单词 + UDP服务端初始化成功,监听端口:8888。
3. 启动客户端
本地测试(服务端和客户端在同一台机器):
./dict_client 127.0.0.1 8888
远程测试(服务端在阿里云服务器,客户端在本地):
./dict_client 阿里云服务器公网IP 8888
注意 :阿里云服务器需要在防火墙 中放行对应的端口(如 8888),协议为UDP。
4. 运行效果
客户端:
客户端启动成功,连接服务端:127.0.0.1:8888
请输入英文单词(输入q退出):
>> apple
apple → 苹果
>> happy
happy → 快乐的
>> test
test → 未查到
>> q
客户端退出
服务端:
字典加载完成,共加载10个单词
UDP服务端初始化成功,监听端口:8888
[127.0.0.1:40652] 查词:apple → 苹果
[127.0.0.1:40652] 查词:happy → 快乐的
[127.0.0.1:40652] 查词:test → 未查到
六、项目扩展与优化
- 支持多字典:修改 Dict 类,支持加载多个字典文件,或加载中英 / 英中双向字典
- 支持模糊查询:当单词不存在时,返回相似的英文单词,提高用户体验
- 增加日志功能:将服务端的日志写入文件,而非仅打印到终端,方便后续排查问题
- 异常处理优化:增加对字典文件空、单词过长、网络中断等异常情况的处理
- 支持 TCP 协议:将 UDP 替换为 TCP,实现可靠的查词服务,适用于对数据可靠性要求高的场景
- 客户端优化:增加命令行参数解析、输入提示、历史记录等功能
七、总结
本次项目基于 UDP Socket 编程实现了简易英译汉翻译服务器,核心掌握了:
- UDP Socket C/S 架构的实际开发流程
- 类的封装思想,将字典和网络通信封装为独立的类,提高代码复用性
- 回调函数的使用,实现网络层与业务层的解耦,让 UdpServer 类更通用
- 哈希表的实际应用,利用 unordered_map 实现高效的键值对查询
- Linux 下文件操作、命令行参数解析、网络字节序转换的综合使用
下一篇博客将实现第二个 UDP Socket 实战项目 ------简单 UDP 双向通信程序,在本次项目的基础上,实现客户端与服务端的双向实时通信,进一步掌握 UDP 的全双工特性
老样子,我把完整代码放在最后面,要是大家觉得麻烦,也可以直接访问我的gitee(在主页)哦~~
