上篇文章中我们实现了一个简单的网络通信EchoServer,客户端给服务端发送一条消息,服务端接收后再转发给客户端,最后客户端接收后回显在控制台上。
那么这篇文章呢,我们就把客户端发来的信息当作英文单词,服务端翻译成中文再转发回去,以此来实现一个英译汉的网络字典。
文章目录
- [1. 网络通信部分](#1. 网络通信部分)
- [2. 字典类](#2. 字典类)
-
- [2.1 框架](#2.1 框架)
- [2.2 加载字典](#2.2 加载字典)
- [2.3 翻译](#2.3 翻译)
- [3. Udpserver.cc](#3. Udpserver.cc)
- [4. 封装InetAddr类](#4. 封装InetAddr类)
1. 网络通信部分
首先我们网络通信不需要改变,只需要稍微修改添加一些新的变量,服务端在接收客户端发来的数据,然后回调去处理翻译这个动作,所以我们可以使用包装器function来包装一个函数指针,用于回调处理翻译
代码如下:
cpp
#pragma once
#include <iostream>
#include <string>
#include <functional>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include "Log.hpp"
using namespace LogModule;
using func_t = std::function<std::string(const std::string&)>;
class UdpServer
{
public:
UdpServer(uint16_t port, func_t func)
:_socketfd(-1), _port(port), _isrunning(false), _func(func)
{}
void Init()
{
_socketfd = socket(AF_INET, SOCK_DGRAM, 0);
if(_socketfd < 0)
{
LOG(LogLevel::FATAL) << "socket error";
exit(1);
}
LOG(LogLevel::INFO) << "socket success, socketfd: " << _socketfd;
// 填充sockaddr_in结构体
struct sockaddr_in local;
bzero(&local, sizeof(local));
local.sin_family = AF_INET;
local.sin_port = htons(_port);
//local.sin_addr.s_addr = inet_addr(_ip.c_str()); // TODO
local.sin_addr.s_addr = INADDR_ANY;
// 绑定IPv4地址结构
int n = bind(_socketfd, (struct sockaddr*)&local, sizeof(local));
if(n < 0)
{
LOG(LogLevel::FATAL) << "bind error";
exit(2);
}
LOG(LogLevel::INFO) << "bind success, sockfd : " << _socketfd;
}
void Start()
{
_isrunning = true;
while(_isrunning)
{
char buffer[1024];
struct sockaddr_in peer;
socklen_t len = sizeof(peer);
ssize_t n = recvfrom(_socketfd, buffer, sizeof(buffer)-1, 0, (struct sockaddr*)&peer, &len);
if(n > 0)
{
// 服务端需要知道客户端的ip和端口号
uint16_t peer_port = ntohs(peer.sin_port); // 从网络中拿到的数据
std::string peer_ip = inet_ntoa(peer.sin_addr); // 网络字节序转点分十进制
buffer[n] = 0;
// 将收到的数据,当作英语单词进行回调处理、
std::string result = _func(buffer);
// LOG(LogLevel::DEBUG) << "[" << peer_ip << ":" << peer_port<< "]# " << buffer; // 客户端发送的消息内容
// 转发回去
//std::string result = "Server echo# ";
//result += buffer;
ssize_t m = sendto(_socketfd, result.c_str(), result.size(), 0, (struct sockaddr*)&peer, len);
if(n < 0)
{
LOG(LogLevel::FATAL) << "sendto error";
exit(3);
}
}
}
}
~UdpServer() {}
private:
int _socketfd;
// std::string _ip; // 用的是字符串风格,点分十进制
uint16_t _port; // 端口号
bool _isrunning;
func_t _func;
};
2. 字典类
2.1 框架
cpp
#pragma once
#include <iostream>
#include <unordered_map>
#include "Log.hpp"
using namespace LogModule;
const std::string defaultdict = "./dictionary.txt";
class Dict
{
public:
Dict(const std::string& path = defaultdict)
:_dict_path(path)
{}
// 加载预先准备好的字典
bool LoadDict()
{
}
// 翻译
std::string Translate(std::string& word)
{
}
~Dict()
{}
private:
std::unordered_map<std::string, std::string> _dict;
std::string _dict_path; // 路径 + 文件名
};
这里我们可以使用键值对的方式来查询英文单词对应的中文,并且我们可以预先将准备好的字典存入文件中,然后在翻译前将文件中的字典全部加载到哈希表中
bash
apple: 苹果
banana: 香蕉
cat: 猫
dog: 狗
book: 书
pen: 笔
happy: 快乐的
sad: 悲伤的
hello:
: 你好
run: 跑
jump: 跳
teacher: 老师
student: 学生
car: 汽车
bus: 公交车
love: 爱
hate: 恨
hello: 你好
goodbye: 再见
summer: 夏天
winter: 冬天
这里我们准备了一些单词和对应的中文,同时也增加了一些错误的格式,我们再加载的时候要注意处理
2.2 加载字典
由于我们预加载时,需要将每一行的英文单词和中文插入到哈希表中,所以我们需要将字符串分割,那我们为了方便,如下处理:
cpp
const std::string sep = ": ";
直接将在文件中准备好的字典加载到哈希表中,对于错误格式的单词,我们打印一条日志后不做处理,继续加载后续单词
cpp
// 加载预先准备好的字典
bool LoadDict()
{
std::fstream in(_dict_path);
if(!in.is_open())
{
LOG(LogLevel::ERROR) << "打开字典" << _dict_path << "失败";
return false;
}
std::string line;
while(std::getline(in, line))
{
// english: chinese
auto pos = line.find(sep);
if(pos == std::string::npos)
{
LOG(LogLevel::WARNING) << "解析: " << line << "失败";
continue; // 解析失败就跳过这个继续加载后续单词
}
std::string english = line.substr(0, pos);
std::string chinese = line.substr(pos + sep.size());
if(english.empty() || chinese.empty())
{
LOG(LogLevel::WARNING) << "没有有效内容: " << line;
continue;
}
_dict.insert(std::make_pair(english, chinese));
LOG(LogLevel::DEBUG) << "加载: " << line;
}
in.close();
}
2.3 翻译
翻译还是比较简单的,在哈希表中查找,如果英文单词不存在就返回字符串"None",存在就返回中文
cpp
// 翻译
std::string Translate(const std::string& word)
{
auto iter = _dict.find(word);
if(iter == _dict.end())
{
LOG(LogLevel::DEBUG) << "进入到了翻译模块: " << word << "->None";
return "None";
}
LOG(LogLevel::DEBUG) << "进入到了翻译模块: " << word << "->" << iter->second;
return iter->second;
}
3. Udpserver.cc
服务端主程序已经有网络通信的功能了,我们现在只需要实例化字典对象,先加载字典到哈希表中,再在网络通信时进行翻译
代码如下:
cpp
#include <memory>
#include "UdpServer.hpp"
#include "Dict.hpp"
// ./udpserver port
int main(int argc, char* argv[])
{
if(argc != 2)
{
std::cerr << "Usage: " << argv[0] << " port" << std::endl;
return 1;
}
uint16_t port = std::stoi(argv[1]);
Enable_Console_Log_Strategy();
// 字典对象提供翻译功能
Dict dict;
dict.LoadDict();
// 网络服务器对象提供网络通信功能
std::unique_ptr<UdpServer> usvr = std::make_unique<UdpServer>(port, [&dict](const std::string& word)->std::string{
return dict.Translate(word);
});
usvr->Init();
usvr->Start();
return 0;
}
客户端不需要动,代码如下:
cpp
#include <iostream>
#include <string>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include "Log.hpp"
using namespace LogModule;
// ./udpclient server_ip server_port
int main(int argc, char* argv[])
{
// 客户端需要绑定服务器的ip和port
if(argc != 3)
{
std::cerr << "Usage: " << argv[0] << " server_ip server_port" << std::endl;
return 1;
}
Enable_Console_Log_Strategy();
std::string server_ip = argv[1];
uint16_t server_port = std::stoi(argv[2]);
int sockfd = socket(AF_INET, SOCK_DGRAM, 0);
if(sockfd < 0)
{
LOG(LogLevel::FATAL) << "socket error";
return 2;
}
// 不需要手动绑定ip和端口,操作系统会分配一个临时端口与ip进行绑定
// 填写服务器信息
struct sockaddr_in server;
memset(&server, 0, sizeof(server)); // 这里使用memset
server.sin_family = AF_INET;
server.sin_port = htons(server_port); // 转成网络字节序
server.sin_addr.s_addr = inet_addr(server_ip.c_str()); // 字符串->网络字节序
while(true)
{
// 从键盘获取要发送的数据
std::string input;
std::cout << "Client Enter# ";
std::getline(std::cin, input);
// 发送数据给服务器
ssize_t n = sendto(sockfd, input.c_str(), input.size(), 0, (struct sockaddr*)&server, sizeof(server));
if(n < 0)
{
LOG(LogLevel::FATAL) << "sendto error";
return 3;
}
// 接收服务器转发回来的数据并回显在控制台上
char buffer[1024];
struct sockaddr_in peer;
socklen_t len = sizeof(peer);
ssize_t m = recvfrom(sockfd, buffer, sizeof(buffer)-1, 0, (struct sockaddr*)&peer, &len);
if(m > 0)
{
buffer[m] = 0;
std::cout << buffer << std::endl;
}
}
return 0;
}
运行测试一下:

4. 封装InetAddr类
如果有多个客户端访问服务端,进行单词翻译,但是我们并不能看到是哪个客户端发送的数据,所以我们这里可以将客户端信息也打印出来,方便我们知道是哪个客户端在发送数据
所以我们这里可以将客户端的ip地址和端口打印出来,那肯定还会需要字节序列转换,我们干脆将字节序列转换封装一个类
cpp
#pragma once
#include <iostream>
#include <string>
#include <sys/socket.h>
#include <sys/types.h>
#include <arpa/inet.h>
#include <netinet/in.h>
// 网络地址和主机地址之间进行转换的类
class InetAddr
{
public:
InetAddr(struct sockaddr_in &addr)
: _addr(addr)
{
_port = ntohs(_addr.sin_port); // 从网络中拿到的数据
_ip = inet_ntoa(_addr.sin_addr); // 网络字节序转点分十进制
}
uint16_t Port()
{
return _port;
}
std::string Ip()
{
return _ip;
}
~InetAddr() {}
private:
struct sockaddr_in _addr;
std::string _ip;
uint16_t _port;
};
注意我们是想要在进入翻译模块时,将客户端信息给打印出来查看
cpp
// 翻译
std::string Translate(const std::string& word, InetAddr& client)
{
auto iter = _dict.find(word);
if(iter == _dict.end())
{
LOG(LogLevel::DEBUG) << "进入到了翻译模块: " << "[" << client.Ip() << ":" << client.Port() << "]" << word << "->None";
return "None";
}
LOG(LogLevel::DEBUG) << "进入到了翻译模块: " << "[" << client.Ip() << ":" << client.Port() << "]" << word << "->" << iter->second;
return iter->second;
}
那么在主程序调用翻译时就需要增加参数
cpp
// 网络服务器对象提供网络通信功能
std::unique_ptr<UdpServer> usvr = std::make_unique<UdpServer>(port, [&dict](const std::string& word, InetAddr& client)->std::string{
return dict.Translate(word, client);
});
那么包装器也需要增加一个参数
cpp
using func_t = std::function<std::string(const std::string&, InetAddr&)>;
再来测试一下

这篇文章中我们将处理的结果数据转发给一个客户端,那我们可不可以把数据转发给多个客户端呢,让大家都能看到,那不就相当于一个聊天室,大家都可以看到转发的数据。所以我们下篇文章就来实现一个简易版的聊天室