【Linux网络编程】第四弹---构建UDP服务器与字典翻译系统:源码结构与关键组件解析_linux udp server服务器代码
✨个人主页: 熬夜学编程的小林
💗系列专栏: 【C语言详解】 【数据结构详解】【C++详解】【Linux系统编程】【Linux网络编程】
目录
1、UdpServer.hpp
1.1、函数对象声明
1.2、Server类基本结构
1.3、构造函数
1.4、Start()
2、Dict.hpp
2.1、基本结构
2.3、构造函数
2.4、翻译函数
2.5、dict.txt
3、UdpServerMain.cc
4、完整源码
4.1、Dict.hpp
4.2、dict.txt
4.3、InetAddr.hpp
4.4、LockGuard.hpp
4.5、Log.hpp
4.6、Makefile
4.7、nocopy.hpp
4.8、UdpClientMain.cc
4.9、UdpServerMain.cc
上一弹我们能够完成客户端与服务端的正常通信,但是我们在实际生活中不仅仅是要进行通信,还需要完成某种功能,此弹实现一个英文翻译成中文版服务端!!
注意:此弹内容的实现是在上一弹的原始代码基础上修改的!!
1、UdpServer.hpp
Server类的基本实现没变,但是要增加实现中英文翻译成中文功能的执行函数(即函数类型成员变量),并适当调整构造函数和启动函数!!!
1.1、函数对象声明
此处需要实现一个英文翻译成中文的执行函数,因此函数参数是一个字符串,返回值也是一个字符串!!!
// 声明函数对象using func_t = std::function;
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 _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 8888int 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 usvr = std::make_unique(translate, port); // C++14标准 usvr->InitServer(); usvr->Start(); return 0;}
运行结果
4、完整源码
4.1、Dict.hpp
#pragma once#include #include #include #include #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 _dict; // 字典结构 std::string _dict_path; // 文件路径};
4.2、dict.txt
apple: 苹果banana: 香蕉cat: 猫dog: 狗book: 书pen: 笔happy: 快乐的sad: 悲伤的run: 跑jump: 跳teacher: 老师student: 学生car: 汽车bus: 公交车love: 爱hate: 恨hello: 你好goodbye: 再见summer: 夏天winter: 冬天
4.3、InetAddr.hpp
#pragma once#include #include #include #include #include #include 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
#pragma once #include 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
#pragma once#include #include #include #include #include #include #include #include #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
.PHONY:allall:udpserver udpclientudpserver:UdpServerMain.ccg++ -o $@ $^ -std=c++14udpclient:UdpClientMain.ccg++ -o $@ $^ -std=c++14.PHONY:clean clean:rm -rf udpserver udpclient
4.7、nocopy.hpp
#pragma onceclass nocopy{public: nocopy(){} ~nocopy(){} nocopy(const nocopy&) = delete; const nocopy& operator=(const nocopy&) = delete;};
4.8、UdpClientMain.cc
#include \"UdpServer.hpp\"#include #include #include #include #include #include // 客户端在未来一定要知道服务器的IP地址和端口号// .udp_client server-ip server-port// .udp_client 127.0.0.1 8888int 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 < 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
#include \"UdpServer.hpp\"#include \"Dict.hpp\"#include // .udp_client local-port// .udp_client 8888int 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 usvr = std::make_unique(translate, port); // C++14标准 usvr->InitServer(); usvr->Start(); return 0;}