1. 基本框架:
前面我们已近完成了,基于UDP协议的网络通信,但是我们服务器接收到来自客户端的信息即字符串时只是进行了简单的发送会客户端和在日志中回显打印,并没有实际的业务服务。那么接下来,我们就设计一个字典翻译的模块,然后让网络通信模块和字典翻译模块进行解耦。也就是说,网络模块就负责进行通信即可,处理来自客户端的数据的任务交给字典翻译模块!这样两个模块之间就没有强耦合了!!
2. 走通基本逻辑:
很简单,我们在服务器内部定义一个包装器类型func_t,这个包装器可以接受所有可调用对象,我们在C++11当中就已经说过了。因为我们现在把客户端发送的字符传当做英文单词,我们希望给用户返回一个汉语。所以我们包装器的返回值和参数都为string。
这里简单设计一下字典模块细节先不填。
下面是我找的字典配置文件,放在最后了。
下面就是我们模块和模块之间进行解耦的原理了,本质就是用函数来进行回调,类用来封装模块,完成任务!!
下面来debug一下,看看逻辑能不能走通~~
OK啊,也是没有问题的 。那接下来就可以放心的完成字典翻译模块啦~~
2. 完善翻译逻辑【dict.hpp】
#pragma once
#include <iostream>
#include <string>
#include <unordered_map>
#include "log.hpp"
using namespace log_module;
const std::string default_dict = "./dict.txt";
const std::string sep = ": "; // 分隔符
class dict
{
public:
dict(const std::string &path = default_dict) : _path(path)
{
}
// 加载配置文件
bool down_load()
{
// 打开配置文件
std::ifstream in(_path);
if (!in.is_open())
{
LOG(log_level::Error) << "打开字典配置文件失败!" << " 路径" << _path;
return false;
}
// 读取配置文件
std::string line;
while (std::getline(in, line))
{
// apple: 苹果 [[9]]
auto pos = line.find(sep);
if (pos == std::string::npos) // 没有找到分隔符
{
LOG(log_level::Warning) << "加载" << line << "错误!" << " 跳过";
continue;
}
std::string English = line.substr(0, pos);
std::string Chinese = line.substr(pos + sep.size());
if (English.empty() || Chinese.empty())
{
LOG(log_level::Warning) << "加载数据" << line << "无效!" << " 跳过";
continue;
}
_map.insert(std::make_pair(English, Chinese));
LOG(log_level::Debug) << "加载" << line << "成功";
}
in.close();
return true;
}
// translate
std::string translate(const std::string &dict)
{
// LOG(log_level::Debug) << "走到了翻译逻辑";
auto tmp = _map.find(dict);
if (tmp == _map.end())
{
return "None";
}
return tmp->second;
}
~dict()
{
}
private:
std::string _path; // 配置文件的路径
std::unordered_map<std::string, std::string> _map; // 从配置文件加载的字典
};
测试成功!!
3. 封装InetAddr
现在,如果我还有一个需求,就是我们想在服务端翻译的时候获取是哪个IP地址,哪个端口号像我们发送的请求。其实,我们在服务端从网络上获取客户端信息时就拿到了该数据只是没有做处理和记录下来而已:
接下来,我们就来封装一个InetAddr类来对这些数据做处理,并且我们把处理之后的结果要传给translate函数,一旦翻译一条数据后我们就用日志回显出来。
InetAddr
class InetAddr
{
public:
InetAddr(const sockaddr_in &peer) : _peer(peer)
{
_port = ntohs(_port); // 端口号
_ip = inet_ntoa(_peer.sin_addr); // IP地址->点分十进制
}
uint16_t port()
{
return _port;
}
std::string IP()
{
return _ip;
}
~InetAddr()
{
}
private:
struct sockaddr_in _peer;
uint16_t _port;
std::string _ip;
};
具体的回调逻辑:
测试结果我们也看到了本地回环的IP和随机绑定的端口号。
至此,我们基于字典翻译服务的UDP网络通信就简单设计完成了。
4. 源码
server.cc
#include <iostream>
#include "server.hpp"
#include <memory>
#include "dict.hpp"
int main(int argc, char *argv[])
{
if (argc != 2)
{
std::cout << "Usage: ./server port" << std::endl;
return 1;
}
using_screen_strategy();
// std::string ip = argv[1];
uint16_t port = std::stoi(argv[1]);
dict d;
d.down_load();
std::unique_ptr<server> p1 = std::make_unique<server>(port,[&d](const std::string& english,InetAddr& client){
return d.translate(english,client);
});
p1->init();
p1->start();
return 0;
}
server.hpp
#pragma once
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
#include <string>
#include <string.h>
#include <netinet/in.h>
#include "log.hpp"
#include <arpa/inet.h>
#include <functional>
#include "inet_addr.hpp"
using namespace log_module;
const int default_sockfd = -1;
using func_t = std::function<std::string(const std::string &, InetAddr &)>;
class server
{
public:
server(uint16_t port, func_t func)
: _sockfd(default_sockfd),
_func(func),
// _ip(ip),
_port(port),
_isrunning(false)
{
}
~server()
{
}
// 服务端初始化和启动
void init()
{
// 创建套接字->打开网络文件
_sockfd = socket(AF_INET, SOCK_DGRAM, 0);
if (_sockfd < 0)
{
LOG(log_level::Fatal) << "socket 创建失败!";
exit(1);
}
LOG(log_level::Info) << "socket 创建成功!" << _sockfd;
// 填充sockaddr_in结构体
struct sockaddr_in local;
// 数据清空
bzero(&local, sizeof(local));
// 数据将来一定会发送到网络
// 端口号和IP地址 本地格式->网络序列
local.sin_family = AF_INET; // 协议家族
local.sin_port = htons(_port); // 端口号
// local.sin_addr.s_addr = inet_addr(_ip.c_str()); // IP地址
local.sin_addr.s_addr = INADDR_ANY; // IP地址任意绑定
int n = bind(_sockfd, (struct sockaddr *)&local, sizeof(local));
if (n < 0)
{
LOG(log_level::Fatal) << "bind 失败!";
exit(2);
}
LOG(log_level::Info) << "bind 成功!" << _sockfd;
}
void start()
{
_isrunning = true;
while (_isrunning)
{
char buffer[1024];
struct sockaddr_in local;
socklen_t len = sizeof(local);
// 收信息
ssize_t n = recvfrom(_sockfd, buffer, sizeof(buffer) - 1, 0, (struct sockaddr *)&local, &len);
if (n > 0) // 成功是收到信息
{
// 处理从网络获取的信息后续交给翻译
InetAddr client(local);
buffer[n] = 0;
// 讲数据回调给外层的函数进行处理,接收返回结果即可
std::string ret = _func(buffer, client);
// 将处理后的信息发给客户端
ssize_t m = sendto(_sockfd, ret.c_str(), ret.size(), 0, (struct sockaddr *)&local, sizeof(local));
}
}
}
private:
int _sockfd;
// std::string _ip; // IP地址用的字符串风格
uint16_t _port; // 端口号
bool _isrunning;
func_t _func;
};
client.cc
#include <iostream>
#include "server.hpp"
#include <memory>
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
#include <string>
#include <string.h>
#include <netinet/in.h>
#include "log.hpp"
#include <arpa/inet.h>
// 客户端知道服务器的IP地址和端口号
int main(int argc, char *argv[])
{
if (argc != 3)
{
std::cout << "Usage: ./server IP port" << std::endl;
return 1;
}
using_screen_strategy();
std::string ip = argv[1];
uint16_t port = std::stoi(argv[2]);
// 创建套接字
int sockfd = socket(AF_INET, SOCK_DGRAM, 0);
if (sockfd < 0)
{
LOG(log_level::Fatal) << "socket 创建失败!";
exit(1);
}
LOG(log_level::Info) << "socket 创建成功!" << sockfd;
// 客户端不需要显示的bind自己的IP地址和端口号
// 但是,在首次发送信息的时候,系统会自动绑定IP地址和端口号,端口号是系统随机分配的
// 为什么要这样做呢??首先,端口号原则上来说只能有一个进程占有,我们的一台主机上可以同时启动了多个进程
// 比如我先后打开了抖音和快手两个APP,如果这两个进程都显示的绑定系统的端口号,有没有可能这连个进程所绑定
// 端口号是一样的呢??所以,为了避免端口号冲突,系统会为我们分配随机未使用的端口号自动绑定。
// 填写服务器信息
sockaddr_in local;
memset(&local, 0, sizeof(local));
local.sin_family = AF_INET;
local.sin_port = htons(port);
local.sin_addr.s_addr = inet_addr(ip.c_str());
// 收发送消息
while (true)
{
std::string input;
std::cout << "请输入#";
std::getline(std::cin, input);
// 发送消息
int n = sendto(sockfd, input.c_str(), input.size(), 0, (struct sockaddr *)&local, sizeof(local));
// 接收消息【有可能连接多个服务器,所以要接收服务器端口号】
char buffer[1024];
sockaddr_in peer;
bzero(&peer, 0);
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;
std::cout << "------------------------------\n";
}
}
return 0;
}
dict.hpp
#pragma once
#include <iostream>
#include <string>
#include <unordered_map>
#include "log.hpp"
using namespace log_module;
const std::string default_dict = "./dict.txt";
const std::string sep = ": "; // 分隔符
class dict
{
public:
dict(const std::string &path = default_dict) : _path(path)
{
}
// 加载配置文件
bool down_load()
{
// 打开配置文件
std::ifstream in(_path);
if (!in.is_open())
{
LOG(log_level::Error) << "打开字典配置文件失败!" << " 路径" << _path;
return false;
}
// 读取配置文件
std::string line;
while (std::getline(in, line))
{
// apple: 苹果 [[9]]
auto pos = line.find(sep);
if (pos == std::string::npos) // 没有找到分隔符
{
LOG(log_level::Warning) << "加载" << line << "错误!" << " 跳过";
continue;
}
std::string English = line.substr(0, pos);
std::string Chinese = line.substr(pos + sep.size());
if (English.empty() || Chinese.empty())
{
LOG(log_level::Warning) << "加载数据" << line << "无效!" << " 跳过";
continue;
}
_map.insert(std::make_pair(English, Chinese));
LOG(log_level::Debug) << "加载" << line << "成功";
}
in.close();
return true;
}
// translate
std::string translate(const std::string &dict, InetAddr &peer)
{
// LOG(log_level::Debug) << "走到了翻译逻辑";
auto tmp = _map.find(dict);
if (tmp == _map.end())
{
return "None";
}
LOG(log_level::Info) << dict << "->" << tmp->second << "[" << peer.IP() << " : " << peer.port() << "]";
return tmp->second;
}
~dict()
{
}
private:
std::string _path; // 配置文件的路径
std::unordered_map<std::string, std::string> _map; // 从配置文件加载的字典
};
InetAddr.hpp
#pragma once
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
#include <string>
#include <string.h>
#include <netinet/in.h>
#include "log.hpp"
#include <arpa/inet.h>
#include <functional>
class InetAddr
{
public:
InetAddr(const sockaddr_in &peer) : _peer(peer)
{
_port = ntohs(_port); // 端口号
_ip = inet_ntoa(_peer.sin_addr); // IP地址->点分十进制
}
uint16_t port()
{
return _port;
}
std::string IP()
{
return _ip;
}
~InetAddr()
{
}
private:
struct sockaddr_in _peer;
uint16_t _port;
std::string _ip;
};
dict.txt
apple: 苹果 [[9]]
banana: 香蕉 [[9]]
divide: 分割 [[3]]
closed-door policy: 闭关自守 [[11]]
keep healthy: 保持健康 [[11]]
he: 他 [[9]]
countries: 国家 [[9]]
exam: 考试 [[8]]
lab: 实验室 [[8]]
UNESCO: 联合国教科文组织 [[8]]
smog: 烟雾 [[8]]
smoke: 烟 [[8]]
fog: 雾 [[8]]
carelessly: 粗心地 [[8]]
re-export: 再出口 [[8]]
undoubtedly: 无疑地 [[8]]
Vast lawns: 宽阔的草坪 [[7]]
gigantic trees: 参天大树 [[7]]
foliage: 树叶 [[7]]
extensive: 广阔的 [[7]]
sheets of vivid green: 鲜艳的绿绒似的毡毯 [[7]]
clumps of gigantic trees: 数株巨树 [[7]]
heaping up: 聚集 [[7]]
rich piles: 浓密的堆叠 [[7]]
here and there: 这里和那里 [[7]]
fancying himself: 自以为 [[5]]
so great: 非常了不起 [[5]]
walking: 走 [[5]]
fancy: 想象 [[5]]
great: 伟大的 [[5]]
self: 自己 [[5]]
East: 东 [[5]]
West: 西 [[5]]
mouse: 老鼠 [[9]]
photos: 照片 [[9]]
boy: 男孩 [[9]]
swim: 游泳 [[9]]
three: 三 [[9]]