【Linux网络编程】第四弹---构建UDP服务器与字典翻译系统:源码结构与关键组件解析

服务器 0

个人主页: 熬夜学编程的小林

💗系列专栏: 【C语言详解】 【数据结构详解】【C++详解】【Linux系统编程】【Linux网络编程】

目录

1、UdpServer.hpp

1.1、函数对象声明

1.2、Server类基本结构

1.3、构造函数

1.4、Start()

2、Dict.hpp

2.1、基本结构

2.2、加载字典文件

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<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 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<UdpServer> usvr = std::make_unique<UdpServer>(translate, port); // C++14标准    usvr->InitServer();    usvr->Start();    return 0;}

运行结果 

4、完整源码

4.1、Dict.hpp

#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

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 <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

#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

#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

.PHONY:allall:udpserver udpclientudpserver:UdpServerMain.cc	g++ -o $@ $^ -std=c++14udpclient:UdpClientMain.cc	g++ -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 <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 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 << "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

#include "UdpServer.hpp"#include "Dict.hpp"#include <memory>// .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<UdpServer> usvr = std::make_unique<UdpServer>(translate, port); // C++14标准    usvr->InitServer();    usvr->Start();    return 0;}

也许您对下面的内容还感兴趣: