一、字典的介绍
**简单介绍:**实现⼀个简单的英译汉的⽹络字典,客户端只需要做的就是给服务端发英语单词即可,而服务端需要做的就是对客户端发的单词进行翻译,然后再发给客户端。这期间我们只需要明白一个思想,就是服务端只负责收和发就行了,对单词的翻译处理我们就交给上层就行了,可以通过function回调来实现类和类之间的交互,比如说我们还添加了两个功能类,一个是字典下载和翻译的类,一个是实现网络序列和本地序列转换的类,我们不要给服务端添加太多的工作。
二、字典各部分代码讲解
1、字典类
我们的字典类里面有两个成员函数,一个是下载功能,一个是翻译功能,成员变量有两个,一个是路径,一个是unordered_map类型的字典,存储中英文。
1.1 下载功能
我们应该将所有的中英文放到一个文本里,然后从文本里读取下载。

我把中英文都放到了文件dictionary.txt中。
首先我们介绍一个c++标准的文件输入流,ifstream,他是用来专门读取文件的,然后需要给它传路径,有了路径,它就会去尝试打开路径对应的文件,他有个成员函数是is_open(),判断文件有没有打开成功,成功了就返回true,失败了返回false。然后我们按行读取,我们对每一行的中英文进行分割,将英文和中文依次存储到unordered_map中,假如说这一行没有有效内容,直接跳过,进行下一行读取。
cpp
#pragma once
#include<iostream>
#include<fstream>
#include<string>
#include<unordered_map>
#include"Log.hpp"
#include"InetAddr.hpp"
const std::string defaultdict="./dictionary.txt";
const std::string sep=":";//设置分隔符
using namespace LogModule;
class Dict
{
public:
Dict(const std::string path=defaultdict):_dict_path(path)
{}
bool LoadDict()
{
std::ifstream in(_dict_path);
if(!in.is_open())
{
LOG(LogLevel::ERROR)<<"open file error";
return false;
}
std::string line;
while(std::getline(in,line))//从文件中按行读取
{
//直接开始分
auto pos=line.find(sep);//返回
if(pos==std::string::npos)
{
LOG(LogLevel::WARNING)<<"解析失败";
continue;//解析失败直接跳过
}
//解析成功
std::string english=line.substr(0,pos);
std::string chinese=line.substr(pos+sep.size());
if(chinese.empty()||english.empty())
{
LOG(LogLevel::WARNING)<<"加载失败";
continue;
}
//插入到_dict里面
_dict.insert(std::make_pair(english,chinese));
LOG(LogLevel::DEBUG) << "加载: " << line;
}
in.close();
return true;
}
std::string Translate(const std::string &word,InetAddr&client)
{
auto iter=_dict.find(word);
if(iter==_dict.end())
{
LOG(LogLevel::WARNING)<<"["<<client.Ip()<<":"<<client.Port()<<"]"<<"->"<<"None";
return "None";
}
LOG(LogLevel::DEBUG)<<"["<<client.Ip()<<":"<<client.Port()<<"]"<<"->"<<iter->second;
return iter->second;
}
//翻译功能
~Dict(){}
private:
std::string _dict_path;
std::unordered_map<std::string,std::string> _dict;
};
代码如上
1.2 翻译功能
翻译功能其实就很简单了,我们需要知道是谁给我们发的,然后返回value值即可
cpp
std::string Translate(const std::string &word,InetAddr&client)
{
auto iter=_dict.find(word);
if(iter==_dict.end())
{
LOG(LogLevel::WARNING)<<"["<<client.Ip()<<":"<<client.Port()<<"]"<<"->"<<"None";
return "None";
}
LOG(LogLevel::DEBUG)<<"["<<client.Ip()<<":"<<client.Port()<<"]"<<"->"<<iter->second;
return iter->second;
}
2、网络和本地序列转化的类
cpp
class InetAddr
{
public:
InetAddr(struct sockaddr_in&addr):_addr(addr)
{
_ip=inet_ntoa(addr.sin_addr);//将网络序列转化为点分十进制字符串序列
_port=ntohs(addr.sin_port);//将网络地址转化为本地地址
}
std::string Ip()
{
return _ip;
}
uint16_t Port()
{
return _port;
}
~InetAddr(){}
private:
struct sockaddr_in _addr;
std::string _ip;
uint16_t _port;
};
3、服务端代码
.hpp:
cpp
#pragma once
#include <iostream>
#include <string>
#include <functional>
#include <strings.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include "Log.hpp"
#include"InetAddr.hpp"
using namespace LogModule;
using func_t=std::function<std::string(const std::string&,InetAddr&)>;
const int defaultfd = -1;
// 你是为了进行网络通信的!
class UdpServer
{
public:
UdpServer(uint16_t port,func_t func)
: _sockfd(defaultfd),
// _ip(ip),
_port(port),
_isrunning(false),
_func(func)
{
}
void Init()
{
// 1. 创建套接字
_sockfd = socket(AF_INET, SOCK_DGRAM, 0);
if (_sockfd < 0)
{
LOG(LogLevel::FATAL) << "socket error!";
exit(1);
}
LOG(LogLevel::INFO) << "socket success, sockfd : " << _sockfd;
// 2. 绑定socket信息,ip和端口, ip(比较特殊,后续解释)
// 2.1 填充sockaddr_in结构体
struct sockaddr_in local;
bzero(&local, sizeof(local));//将其清0
local.sin_family = AF_INET;
// 我会不会把我的IP地址和端口号发送给对方?
// IP信息和端口信息,一定要发送到网络!
// 本地格式->网络序列
local.sin_port = htons(_port);
// IP也是如此,1. IP转成4字节 2. 4字节转成网络序列 -> in_addr_t inet_addr(const char *cp);
//local.sin_addr.s_addr = inet_addr(_ip.c_str()); // TODO
local.sin_addr.s_addr = INADDR_ANY;
// 那么为什么服务器端要显式的bind呢?IP和端口必须是众所周知且不能轻易改变的!
int n = bind(_sockfd, (struct sockaddr *)&local, sizeof(local));
if (n < 0)
{
LOG(LogLevel::FATAL) << "bind error";
exit(2);
}
LOG(LogLevel::INFO) << "bind success, sockfd : " << _sockfd;
}
void Start()
{
_isrunning = true;
while (_isrunning)
{
char buffer[1024];//缓冲区
struct sockaddr_in peer;
socklen_t len = sizeof(peer);
// 1. 收消息, client为什么要给服务器发送消息啊?不就是让服务端处理数据。
ssize_t s = recvfrom(_sockfd, buffer, sizeof(buffer) - 1, 0, (struct sockaddr *)&peer, &len);
if (s > 0)
{
InetAddr client(peer);
buffer[s] = 0;
//收到的内容,当做英文单词
std::string result = _func(buffer,client);
//LOG(LogLevel::DEBUG) << "[" << peer_ip << ":" << peer_port<< "]# " << buffer; // 1. 消息内容 2. 谁发的??
// 2. 发消息
//std::string echo_string = "server echo@ ";
//echo_string += buffer;
//sendto(_sockfd, echo_string.c_str(), echo_string.size(), 0, (struct sockaddr*)&peer, len);
sendto(_sockfd, result.c_str(), result.size(), 0, (struct sockaddr*)&peer, len);
}
}
}
~UdpServer()
{
}
private:
int _sockfd;
uint16_t _port;
// std::string _ip; // 用的是字符串风格,点分十进制, "192.168.1.1"
bool _isrunning;
func_t _func; // 服务器的回调函数,用来进行对数据进行处理
};
.cc:
cpp
#include <iostream>
#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;
}
// std::string ip = argv[1];
uint16_t port = std::stoi(argv[1]);
Enable_Console_Log_Strategy();
//我们以后可以采用类跟类之间的耦合。
//1.字典对象提供翻译功能
Dict dict;
dict.LoadDict();
//创建网络通信对象
std::unique_ptr<UdpServer> usvr=std::make_unique<UdpServer>(port,[&dict](const std::string& word,InetAddr& client)->std::string
{
return dict.Translate(word,client);
});
usvr->Init();
usvr->Start();
return 0;
}
分析回调思路:就记住function是用来存放满足签名要求的可调用实体的包装器:可以包装普通函数,lambda表达式,成员函数,函数对象,这次存的是lambda表达式,而服务端根本不需要包含Dict类,只需要拿到报文,然后调用回调函数func_t,拿到翻译结果,回复客户端。而lambda表达式就是被保存到了成员函数_func,当调用回调,其实就是通过了类型匹配匹配到了lambda表达式,执行了lambda表达式,返回的结果就是_func返回的结果,给到了result。
4、客户端代码
cpp
#include <iostream>
#include <string>
#include <cstring>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <sys/types.h>
#include <sys/socket.h>
// ./udpclient server_ip server_port
int main(int argc, char *argv[])
{
if (argc != 3)
{
std::cerr << "Usage: " << argv[0] << " server_ip server_port" << std::endl;
return 1;
}
std::string server_ip = argv[1];
uint16_t server_port = std::stoi(argv[2]);
// 1. 创建socket
int sockfd = socket(AF_INET, SOCK_DGRAM, 0);
if(sockfd < 0)
{
std::cerr << "socket error" << std::endl;
return 2;
}
// 2. 本地的ip和端口是什么?要不要和上面的"文件"关联呢?
// 问题:client要不要bind?需要bind.
// client要不要显式的bind?不要!!首次发送消息,OS会自动给client进行bind,OS知道IP,端口号采用随机端口号的方式
// 为什么?一个端口号,只能被一个进程bind,为了避免client端口冲突
// client端的端口号是几,不重要,只要是唯一的就行!
// 填写服务器信息
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());
while(true)
{
std::string input;
std::cout << "Please Enter# ";
std::getline(std::cin, input);
int n = sendto(sockfd, input.c_str(), input.size(), 0, (struct sockaddr*)&server, sizeof(server));
(void)n;
char buffer[1024];//定义一个缓冲区来收消息
struct sockaddr_in peer;//不做处理,直接留下来就可以
socklen_t len = sizeof(peer);
int 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;
}