✨个人主页:熬夜学编程的小林
💗系列专栏: 【C语言详解】 【数据结构详解】【C++详解】【Linux系统编程】【Linux网络编程】
目录
上一弹我们能够完成客户端与服务端的正常通信,但是我们在实际生活中不仅仅是要进行通信,还需要完成某种功能,此弹实现一个英文翻译成中文版服务端!!
注意:此弹内容的实现是在上一弹的原始代码基础上修改的!!
1、UdpServer.hpp
Server类的基本实现没变,但是要增加实现中英文翻译成中文功能的执行函数(即函数类型成员变量) ,并适当调整构造函数和启动函数!!!
1.1、函数对象声明
此处需要实现一个英文翻译成中文的执行函数,因此函数参数是一个字符串,返回值也是一个字符串!!!
// 声明函数对象
using func_t = std::function<std::string(std::string)>;
1.2、Server类基本结构
基本结构与上一弹的Server类基本一致,只增加了函数对象类型!!!
class UdpServer : public nocopy
{
public:
UdpServer(func_t func,uint16_t localport = glocalport);
void InitServer();
void Start();
~UdpServer();
private:
int _sockfd; // 文件描述符
uint16_t _localport; // 端口号
bool _isrunning;
func_t _func; // 执行相应函数
};
1.3、构造函数
构造函数只需增加一个函数对象参数,初始化列表初始化变量即可!!!
UdpServer(func_t func,uint16_t localport = glocalport)
: _func(func), _sockfd(gsockfd), _localport(localport), _isrunning(false)
{
}
1.4、Start()
上一弹的Start()函数是先接收客户端的消息,然后将客户端的消息发送回去;此弹是接收客户端的英文单词,然后服务端翻译成中文,然后将中文发送回去!!!
void Start()
{
_isrunning = true;
char inbuffer[1024];
while (_isrunning)
{
// sleep(1);
struct sockaddr_in peer;
socklen_t len = sizeof(peer);
// 接收客户端的英文单词
ssize_t n = recvfrom(_sockfd, inbuffer, sizeof(inbuffer) - 1, 0, (struct sockaddr *)&peer, &len);
if (n > 0)
{
InetAddr addr(peer);
inbuffer[n] = 0;
// 一个一个的单词
std::cout << "[" << addr.Ip() << ":" << addr.Port() << "]# " << inbuffer << std::endl;
std::string result = _func(inbuffer); // 执行翻译功能
// 将翻译的中文结果发回客户端
sendto(_sockfd,result.c_str(),result.size(),0,(struct sockaddr *)&peer,len);
}
}
}
2、Dict.hpp
Dict类执行加载字典文件和执行翻译的功能!!
2.1、基本结构
Dict成员包含一个存储字典的KV结构和一个文件路径!
class Dict
{
private:
// 加载字典文件
void LoadDict(const std::string& path);
public:
// 构造
Dict(const std::string& dict_path);
// 英语翻译为中文
std::string Translate(std::string word);
~Dict()
{}
private:
std::unordered_map<std::string, std::string> _dict; // 字典结构
std::string _dict_path; // 文件路径
};
2.2、加载字典文件
注意:此处的字典文件是以冒号 + 空格来分割英文与中文的!
加载字典文件的本质是以KV的形式将英文单词和中文翻译插入到_dict哈希表中!
加载文件包含3个大的步骤:
-
1、读方式打开文件
-
2、按行读取内容[需要考虑中间有空格情况,一行中没找到分隔符情况]
-
3、关闭文件
const static std::string sep = ": "; // 分隔符 冒号+空格
// 加载字典文件
void LoadDict(const std::string &path)
{
// 1.读方式打开文件
std::ifstream in(path);
// 判断是否打开成功
if (!in.is_open())
{
LOG(FATAL, "open %s failed\n", path.c_str());
exit(1);
}std::string line; // 2.按行读取内容 while (std::getline(in, line)) { LOG(DEBUG, "load info : %s ,success\n", line.c_str()); if (line.empty()) continue; // 中间有空格情况 auto pos = line.find(sep); // 使用find找到分割符位置,返回迭代器位置 if (pos == std::string::npos) continue; // 一行中没找到分隔符 // apple: 苹果 std::string key = line.substr(0, pos); // [) 前闭后开 if (key.empty()) continue; std::string value = line.substr(pos + sep.size()); // 从pos + 分隔符长度开始到结尾 if (value.empty()) continue; _dict.insert(std::make_pair(key, value)); } LOG(INFO, "load %s done\n", path.c_str()); in.close(); // 3.关闭文件
}
2.3、构造函数
构造函数****初始化字典文件和加载字典文件(将字典文件以KV格式插入到_dict中)。
// 构造
Dict(const std::string &dict_path) : _dict_path(dict_path)
{
LoadDict(_dict_path);
}
2.4、翻译函数
翻译函数即在_dict中查找是否有该单词,有该单词则返回_dict的value值(没找到返回None)!
// 英语翻译为中文
std::string Translate(std::string word)
{
if (word.empty())
return "None";
auto iter = _dict.find(word);
if (iter == _dict.end())
return "None";
else
return iter->second;
}
2.5、dict.txt
dict.txt文件****存储对应的英文单词和翻译结果!
apple: 苹果
banana: 香蕉
cat: 猫
dog: 狗
book: 书
pen: 笔
happy: 快乐的
sad: 悲伤的
run: 跑
jump: 跳
teacher: 老师
student: 学生
car: 汽车
bus: 公交车
love: 爱
hate: 恨
hello: 你好
goodbye: 再见
summer: 夏天
winter: 冬天
3、UdpServerMain.cc
服务端主函数基本结构没变,但是构造指针对象时需要传递翻译函数对象 ,但是这个函数对象的参数是字符串类型,返回值是字符串类型,而Dict类中的翻译函数有this指针,因此我们需要使用bind()绑定函数!
// .udp_client local-port
// .udp_client 8888
int main(int argc, char *argv[])
{
if (argc != 2)
{
std::cerr << "Usage: " << argv[0] << " server-port" << std::endl;
exit(0);
}
uint16_t port = std::stoi(argv[1]);
EnableScreen();
Dict dict("./dict.txt"); // 构造字典类
func_t translate = std::bind(&Dict::Translate, &dict, std::placeholders::_1); // 绑定翻译函数
std::unique_ptr<UdpServer> usvr = std::make_unique<UdpServer>(translate, port); // C++14标准
usvr->InitServer();
usvr->Start();
return 0;
}
运行结果
4、完整源码
4.1、Dict.hpp
cpp
#pragma once
#include <iostream>
#include <fstream>
#include <string>
#include <unordered_map>
#include "Log.hpp"
using namespace log_ns;
const static std::string sep = ": "; // 分隔符 冒号+空格
class Dict
{
private:
// 加载字典文件
void LoadDict(const std::string &path)
{
// 1.读方式打开文件
std::ifstream in(path);
// 判断是否打开成功
if (!in.is_open())
{
LOG(FATAL, "open %s failed\n", path.c_str());
exit(1);
}
std::string line;
// 2.按行读取内容
while (std::getline(in, line))
{
LOG(DEBUG, "load info : %s ,success\n", line.c_str());
if (line.empty())
continue; // 中间有空格情况
auto pos = line.find(sep); // 使用find找到分割符位置,返回迭代器位置
if (pos == std::string::npos)
continue; // 一行中没找到分隔符
// apple: 苹果
std::string key = line.substr(0, pos); // [) 前闭后开
if (key.empty())
continue;
std::string value = line.substr(pos + sep.size()); // 从pos + 分隔符长度开始到结尾
if (value.empty())
continue;
_dict.insert(std::make_pair(key, value));
}
LOG(INFO, "load %s done\n", path.c_str());
in.close(); // 3.关闭文件
}
public:
// 构造
Dict(const std::string &dict_path) : _dict_path(dict_path)
{
LoadDict(_dict_path);
}
// 英语翻译为中文
std::string Translate(std::string word)
{
if (word.empty())
return "None";
auto iter = _dict.find(word);
if (iter == _dict.end())
return "None";
else
return iter->second;
}
~Dict()
{
}
private:
std::unordered_map<std::string, std::string> _dict; // 字典结构
std::string _dict_path; // 文件路径
};
4.2、dict.txt
cpp
apple: 苹果
banana: 香蕉
cat: 猫
dog: 狗
book: 书
pen: 笔
happy: 快乐的
sad: 悲伤的
run: 跑
jump: 跳
teacher: 老师
student: 学生
car: 汽车
bus: 公交车
love: 爱
hate: 恨
hello: 你好
goodbye: 再见
summer: 夏天
winter: 冬天
4.3、InetAddr.hpp
cpp
#pragma once
#include <iostream>
#include <string>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
class InetAddr
{
private:
// 网络地址转本地地址
void ToHost(const struct sockaddr_in& addr)
{
_port = ntohs(addr.sin_port); // 网络转主机
_ip = inet_ntoa(addr.sin_addr); // 结构化转字符串
}
public:
InetAddr(const struct sockaddr_in& addr):_addr(addr)
{
ToHost(addr);
}
std::string Ip()
{
return _ip;
}
uint16_t Port()
{
return _port;
}
~InetAddr()
{}
private:
std::string _ip;
uint16_t _port;
struct sockaddr_in _addr;
};
4.4、LockGuard.hpp
cpp
#pragma once
#include <pthread.h>
class LockGuard
{
public:
LockGuard(pthread_mutex_t* mutex):_mutex(mutex)
{
pthread_mutex_lock(_mutex);
}
~LockGuard()
{
pthread_mutex_unlock(_mutex);
}
private:
pthread_mutex_t* _mutex;
};
4.5、Log.hpp
cpp
#pragma once
#include <iostream>
#include <fstream>
#include <ctime>
#include <cstring>
#include <sys/types.h>
#include <unistd.h>
#include <stdarg.h>
#include <pthread.h>
#include "LockGuard.hpp"
namespace log_ns
{
// 日志等级
enum
{
DEBUG = 1,
INFO,
WARNING,
ERROR,
FATAL
};
std::string LevelToString(int level)
{
switch (level)
{
case DEBUG:
return "DEBUG";
case INFO:
return "INFO";
case WARNING:
return "WARNING";
case ERROR:
return "ERROR";
case FATAL:
return "FATAL";
default:
return "UNKNOW";
}
}
std::string GetCurrTime()
{
time_t now = time(nullptr); // 时间戳
struct tm *curr_time = localtime(&now);
char buffer[128];
snprintf(buffer, sizeof(buffer), "%d-%02d-%02d %02d:%02d:%02d",
curr_time->tm_year + 1900,
curr_time->tm_mon + 1,
curr_time->tm_mday,
curr_time->tm_hour,
curr_time->tm_min,
curr_time->tm_sec);
return buffer;
}
class logmessage
{
public:
std::string _level; // 日志等级
pid_t _id; // pid
std::string _filename; // 文件名
int _filenumber; // 文件行号
std::string _curr_time; // 当前时间
std::string _message_info; // 日志内容
};
#define SCREEN_TYPE 1
#define FILE_TYPE 2
const std::string glogfile = "./log.txt";
pthread_mutex_t glock = PTHREAD_MUTEX_INITIALIZER;
// log.logMessage(""/*文件名*/,12/*文件行号*/,INFO/*日志等级*/,"this is a %d message,%f,%s,hello world"/*日志内容*/,x,,);
class Log
{
public:
// 默认向显示器打印
Log(const std::string &logfile = glogfile) : _logfile(logfile), _type(SCREEN_TYPE)
{
}
// 打印方式
void Enable(int type)
{
_type = type;
}
// 向屏幕打印
void FlushToScreen(const logmessage &lg)
{
printf("[%s][%d][%s][%d][%s] %s",
lg._level.c_str(),
lg._id,
lg._filename.c_str(),
lg._filenumber,
lg._curr_time.c_str(),
lg._message_info.c_str());
}
// 向文件打印
void FlushToFile(const logmessage &lg)
{
std::ofstream out(_logfile, std::ios::app); // 追加打开文件
if (!out.is_open())
return; // 打开失败直接返回
char logtxt[2048];
snprintf(logtxt, sizeof(logtxt), "[%s][%d][%s][%d][%s] %s",
lg._level.c_str(),
lg._id,
lg._filename.c_str(),
lg._filenumber,
lg._curr_time.c_str(),
lg._message_info.c_str());
out.write(logtxt, strlen(logtxt)); // 写文件
out.close(); // 关闭文件
}
// 刷新日志
void FlushLog(const logmessage &lg)
{
// 加过滤逻辑 --- TODO
// ...
LockGuard lockguard(&glock); // RAII锁
switch (_type)
{
case SCREEN_TYPE:
FlushToScreen(lg);
break;
case FILE_TYPE:
FlushToFile(lg);
break;
}
}
// ... 可变参数(C语言)
// 初始化日志信息
void logMessage(std::string filename, int filenumber, int level, const char *format, ...)
{
logmessage lg;
lg._level = LevelToString(level);
lg._id = getpid();
lg._filename = filename;
lg._filenumber = filenumber;
lg._curr_time = GetCurrTime();
va_list ap; // va_list-> char*指针
va_start(ap, format); // 初始化一个va_list类型的变量
char log_info[1024];
vsnprintf(log_info, sizeof(log_info), format, ap);
va_end(ap); // 释放由va_start宏初始化的va_list资源
lg._message_info = log_info;
// std::cout << lg._message_info << std::endl; // 测试
// 日志打印出来(显示器/文件)
FlushLog(lg);
}
~Log()
{
}
private:
int _type; // 打印方式
std::string _logfile; // 文件名
};
Log lg;
// 打印日志封装成宏,使用函数方式调用
#define LOG(Level, Format, ...) \
do \
{ \
lg.logMessage(__FILE__, __LINE__, Level, Format, ##__VA_ARGS__); \
} while (0)
// 设置打印方式,使用函数方式调用
#define EnableScreen() \
do \
{ \
lg.Enable(SCREEN_TYPE); \
} while (0)
// 设置打印方式,使用函数方式调用
#define EnableFile() \
do \
{ \
lg.Enable(FILE_TYPE); \
} while (0)
}
4.6、Makefile
cpp
.PHONY:all
all:udpserver udpclient
udpserver:UdpServerMain.cc
g++ -o $@ $^ -std=c++14
udpclient:UdpClientMain.cc
g++ -o $@ $^ -std=c++14
.PHONY:clean
clean:
rm -rf udpserver udpclient
4.7、nocopy.hpp
cpp
#pragma once
class nocopy
{
public:
nocopy(){}
~nocopy(){}
nocopy(const nocopy&) = delete;
const nocopy& operator=(const nocopy&) = delete;
};
4.8、UdpClientMain.cc
cpp
#include "UdpServer.hpp"
#include <string>
#include <cstring>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
// 客户端在未来一定要知道服务器的IP地址和端口号
// .udp_client server-ip server-port
// .udp_client 127.0.0.1 8888
int main(int argc, char *argv[])
{
if (argc != 3)
{
std::cerr << "Usage: " << argv[0] << " server-ip server-port" << std::endl;
exit(0);
}
std::string serverip = argv[1];
uint16_t serverport = std::stoi(argv[2]);
// 1.创建套接字
int sockfd = ::socket(AF_INET, SOCK_DGRAM, 0);
if (sockfd < 0)
{
std::cerr << "create socket eror\n"
<< std::endl;
exit(1);
}
// client的端口号,一般不让用户自己设定,而是让client 所在OS随机选择?怎么选择?什么时候?
// client 需要bind它自己的IP和端口,但是client 不需要 "显示" bind它自己的IP和端口
// client 在首次向服务器发送数据的时候,OS会自动给client bind它自己的IP和端口
struct sockaddr_in server;
memset(&server, 0, sizeof(server));
server.sin_family = AF_INET;
server.sin_port = htons(serverport); // 转换重要!!!
server.sin_addr.s_addr = inet_addr(serverip.c_str());
while (true)
{
std::string line;
std::cout << "Please Enter# ";
std::getline(std::cin, line); // 以行读取消息
// 发消息,你要知道发送给谁
int n = sendto(sockfd, line.c_str(), line.size(), 0, (struct sockaddr *)&server, sizeof(server));
if(n > 0)
{
// 收消息
struct sockaddr_in temp;
socklen_t len = sizeof(temp);
char buffer[1024];
int m = recvfrom(sockfd,buffer,sizeof(buffer),0,(struct sockaddr*)&temp,&len);
if(m > 0)
{
buffer[m] = 0;
std::cout << buffer << std::endl;
}
else
{
break;
}
}
else
{
break;
}
}
// 关闭套接字
::close(sockfd);
return 0;
}
4.9、UdpServerMain.cc
cpp
#include "UdpServer.hpp"
#include "Dict.hpp"
#include <memory>
// .udp_client local-port
// .udp_client 8888
int main(int argc, char *argv[])
{
if (argc != 2)
{
std::cerr << "Usage: " << argv[0] << " server-port" << std::endl;
exit(0);
}
uint16_t port = std::stoi(argv[1]);
EnableScreen();
Dict dict("./dict.txt"); // 构造字典类
func_t translate = std::bind(&Dict::Translate, &dict, std::placeholders::_1); // 绑定翻译函数
std::unique_ptr<UdpServer> usvr = std::make_unique<UdpServer>(translate, port); // C++14标准
usvr->InitServer();
usvr->Start();
return 0;
}