Linux网络编程:UDP 的DictServer

前言:

大家好啊,我们在上篇文章中,已经简单的实现了一个echo server,也就是,当我们的客户端向用户端发送消息时,客户端会收到消息并以此反应发回一个信息。

而本文将会为大家实现一个简单的字典查询的功能。

也就是说,我们让客户端发送要查询的英文,随后,我们服务端接收到了这个消息之后,会进行一个查询,具体怎么做我们后面会说。

为了简便,我们这里直接将上篇文章所用到的代码全部复制粘贴一份到一个新的项目文件夹中。

点此跳转

一、字典的实现

1、数据

字典字典,那我们是不是需要一个txt文件里面包含了我们用来测试字典的数据?

我们创建一个data.txt文件,从中添加一系列格式为英文:中文的测试用例:

随便写几个测试用例就行了。

2、字典类与头文件

我们先写出一个字典类,把这个类定义到我们的字典头文件中去。

随后用两个全局变量来记录data.txt文件的路径与文件名。因为我们要使用字典,必然会读取文件里面的数据,这里就涉及到了文件操作。

一个字典的功能必须要有初始化与翻译,所以我们可以先把这两个接口给定义一下:

cpp 复制代码
#pragma once
#ifndef __DICTIONARY_HPP__
#define __DICTIONARY_HPP__
#include <unordered_map>
#include <string>

const std::string dataname = "data.txt"; // 定义好我们存储字典数据的文件名
const std::string path = "./";           // 定义好我们存储字典数据的路径

class dictionary
{
public:
    dictionary()
    {
    }
    ~dictionary() {}

    bool LoadDictionary() // 我们需要一个函数来加载字典数据,初始化我们的字典对象,填充字典数据到哈希表里
    {
    }
    std::string Translate(const std::string &word) // 我们之后用这个函数来实现一个翻译的功能
    {
    }

private:
    std::unordered_map<std::string, std::string> dict;
};

#endif

我们还可以给我们的类增加一下路径和名字成员变量,方便记录管理与使用。

cpp 复制代码
    std::string _filename;
    std::string _path;// 我们可以把文件名和路径都存储在字典对象里

在构造一个字典对象时,就在构造函数中通过确实的上面的全局变量值,对我们成员变量进行初始化:

cpp 复制代码
    dictionary(const std::string filename=dataname,std::string path=defaultpath)
    :_filename(filename),
    _path(path)
    {
    }

接下来我们来实现一下加载字典的操作。读取一个txt的数据,必然要涉及到文件操作。我们这里使用C++的文件操作流。

所以可以在LoadDictionary中先新建一个输入流,但是这里打开文件涉及到这个文件的路径,所以我们就需要先合成一下这个文件的路径信息,随后进行ifstream的创建。如果打开失败,就打印错误提示信息:

cpp 复制代码
        std::string filepath=_path+_filename; // 拼接文件路径和文件名
        std::ifstream fd(filepath.c_str()); // 打开文件,默认以读取方式打开

        if(!fd.is_open())
        {
            LOG(LogLevel::ERROR)<<"Failed to open dictionary file: " << filepath;
            return false; // 如果文件打开失败,返回false
        }

如果打开成功,我们就需要读取数据了。怎么读取呢?

我们通过getline读取到一行string,格式为:apple: 苹果。

我们这里使用哈希表存储,这个格式怎么可以存储呢?

所以我们需要对其进行切割?

如何切割?

我们定义一个切割接口,并传入以下函数参数:string类型的line值,string*的word与value值用来接收被分割后的字符串,这两个都是输出型参数。最后一个sep,表示用什么来切割。

cpp 复制代码
    bool Split(std::string &line,std::string *word,std::string *value,const std::string sep)
    {
        int pos= line.find(sep);// 找到:的位置
        if(pos==std::string::npos)//没找到
        {
            return false;
        }

        *word=line.substr(0,pos);//pos找到之后所指向的位置就是:的位置,substr是一个左闭右开区间
        *value = line.substr(pos+sep.size());// 从pos+sep.size()开始截取,直到字符串结束
        if(value->empty()||word->empty())
        {
            return false; // 如果截取的值为空,返回false
        }
        return true;
    }

完成切割后,就把该对pair加入到我们的字典中去:

cpp 复制代码
     bool LoadDictionary() // 我们需要一个函数来加载字典数据,初始化我们的字典对象,填充字典数据到哈希表里
    {
        std::string filepath=_path+_filename; // 拼接文件路径和文件名
        std::ifstream fd(filepath.c_str()); // 打开文件,默认以读取方式打开

        if(!fd.is_open())
        {
            LOG(LogLevel::ERROR)<<"Failed to open dictionary file: " << filepath;
            return false; // 如果文件打开失败,返回false
        }
        std::string line;
        while(getline(fd,line))
        {
            std::string word,value;
            if(Split(line,&word,&value,": ")) // 假设我们用:来分隔单词和释义
            {
                dict[word]=value; // 将单词和释义存入哈希表
            }
        }
     }

那么接下来就是完成我们的翻译工作了。

首先这个翻译功能我们想要实现的效果就是传递一个word值,调用这个接口进行翻译。我们在翻译里面查找哈希表,如果找到该word就返回这个word所对应的value:

cpp 复制代码
     std::string Translate(const std::string &word) // 我们之后用这个函数来实现一个翻译的功能
    {
        auto it=dict.find(word);
        if(it != dict.end()) // 如果找到了这个单词
        {
            return it->second; // 返回对应的释义
        }
        else
        {
            LOG(LogLevel::WARN)<<"Word not found: " << word;
            return "Not found"; // 如果没找到,返回一个默认值
        }
    }

至此,我们的一个字典类就大功告成了:

cpp 复制代码
#pragma once
#ifndef __DICTIONARY_HPP__
#define __DICTIONARY_HPP__
#include <unordered_map>
#include <string>
#include <iostream>
#include<stdio.h>
#include"log.hpp"

using namespace LogModule;

const std::string dataname = "data.txt"; // 定义好我们存储字典数据的文件名
const std::string defaultpath = "./";           // 定义好我们存储字典数据的路径

class dictionary
{
public:
    dictionary(const std::string filename=dataname,std::string path=defaultpath)
    :_filename(filename),
    _path(path)
    {
    }
    ~dictionary() {}
    bool Split(std::string &line,std::string *word,std::string *value,const std::string sep)
    {
        int pos= line.find(sep);// 找到:的位置
        if(pos==std::string::npos)//没找到
        {
            return false;
        }

        *word=line.substr(0,pos);//pos找到之后所指向的位置就是:的位置,substr是一个左闭右开区间
        *value = line.substr(pos+sep.size());// 从pos+sep.size()开始截取,直到字符串结束
        if(value->empty()||word->empty())
        {
            return false; // 如果截取的值为空,返回false
        }
        return true;
    }
    bool LoadDictionary() // 我们需要一个函数来加载字典数据,初始化我们的字典对象,填充字典数据到哈希表里
    {
        std::string filepath=_path+_filename; // 拼接文件路径和文件名
        std::ifstream fd(filepath.c_str()); // 打开文件,默认以读取方式打开

        if(!fd.is_open())
        {
            LOG(LogLevel::ERROR)<<"Failed to open dictionary file: " << filepath;
            return false; // 如果文件打开失败,返回false
        }
        std::string line;
        while(getline(fd,line))
        {
            std::string word,value;
            if(Split(line,&word,&value,": ")) // 假设我们用:来分隔单词和释义
            {
                dict[word]=value; // 将单词和释义存入哈希表
            }
        }
    }
    std::string Translate(const std::string &word) // 我们之后用这个函数来实现一个翻译的功能
    {
        auto it=dict.find(word);
        if(it != dict.end()) // 如果找到了这个单词
        {
            return it->second; // 返回对应的释义
        }
        else
        {
            LOG(LogLevel::WARN)<<"Word not found: " << word;
            return "Not found"; // 如果没找到,返回一个默认值
        }
    }

private:
    std::unordered_map<std::string, std::string> dict;
    std::string _filename;
    std::string _path;// 我们可以把文件名和路径都存储在字典对象里
};

#endif

二、使用字典类

那我们应该如何使用我们写好的字典类呢?

这里就要修改一下我们的服务端了,我们需要修改一下我们服务端启动的参数。

比如,我们可以先使用智能指针创建一个字典对象,随后调用这个对象中的加载数据的函数。

数据加载完毕之后,我们尝试回调函数的方式,将我们的Translate函数通过回调传递进去:

cpp 复制代码
    // 创建一个由智能指针所管理的字典对象
    std::shared_ptr<dictionary> dict_ptr = std::make_shared<dictionary>();
    dict_ptr->LoadDictionary(); // 加载字典数据

    // 我们先创建一个服务器对象,并用智能指针管理它
    std::unique_ptr<UdpServer> svr_ptr = std::make_unique<UdpServer>(std::string("127.0.0.1"), 8080, [dict_ptr](const std::string &word) -> std::string
                                                                     { return dict_ptr->Translate(word); }); // 回调函数的方式传递我们的翻译函数

三、UdpServer.hpp的回调实现

那么我们应该如何修改我们的头文件呢?

首先我们需要将一个lambda表达式传进去帮助这个服务端对象初始化,所以我们的服务端的初始化函数就必须新增对应的初始化。

由于传进去的是一个返回值是string,参数是const string&的一个函数类型。

我们可以在头文件中使用function帮助我们:

cpp 复制代码
using find_t =std::function<std::string(const std::string &)>;

至此,我们就能使用find_t类型代替我们传进来的类类型。

由于新增了初始化,所以我们的的服务端类需要新增一个类成员变量代表着传进来的翻译函数接口,并在构造函数中初始化:

我们这里也给其设置一个缺省函数,否则就要改变我们的参数顺序(非全缺省,缺省需要放在右边)

这样一来我们的_findword就代表着这个函数。

我们在start中会受到客户端的消息,我们拿到消息后进入翻译函数,并返回翻译信息:

cpp 复制代码
 void Start()
        {
            is_running = true;

            while (is_running)
            {

                char buffer[1024];
                struct sockaddr_in peer;      // 输出型参数
                socklen_t len = sizeof(peer); // 也是一个输出型参数
                ssize_t n = ::recvfrom(_sockfd, buffer, sizeof(buffer), 0, (struct sockaddr *)&peer, &len);
                if (n > 0)
                {

                    InetAddr temp(peer); // 通过上面的peer来进行初始化,这样以来我们就能获取到相关ip地址与端口并打印
                    buffer[n] = '\0';    // 确保字符串以null结尾
                    // LOG(LogLevel::INFO) << "client ip: " << temp.GetIp() << ", port: " << temp.GetPort()
                    //                     << "   client say: " << buffer;

                    std::string echo_str = "Translate: "; // 我们要给客户端回显一条消息
                    echo_str += _findword(std::string(buffer)); // 调用传入的函数来获取翻译结果

                    // 发送回显消息
                    ssize_t m = ::sendto(_sockfd, echo_str.c_str(), echo_str.size(), 0, (struct sockaddr *)&peer, len);
                    if (m < 0)
                    {
                        LOG(LogLevel::ERROR) << "sendto error: " << strerror(errno);
                    }
                }
            }
        }

这样一来,一个简单的字典程序就大功告成了。

我们可以运行一下代码:

总结:

今天的内容比较简单,这是我在为明天的聊天室做一个过渡作用。

希望大家那个熟练使用我们这里的回调方法,明天会经常用到。

相关推荐
努力一点9488 分钟前
ubuntu22.04系统入门 linux入门(二) 简单命令 多实践以及相关文件管理命令
linux·运维·服务器·人工智能·gpu算力
SKYDROID云卓小助手24 分钟前
无人设备遥控器之多设备协同技术篇
网络·人工智能·嵌入式硬件·算法·信号处理
Web极客码1 小时前
如何为你的WordPress网站选择合适的安全插件
网络·安全
UU_Yang1 小时前
Linux跑后台服务
linux·运维·服务器
寒士obj2 小时前
HTTPS的工作原理
网络协议·http·https
kfepiza2 小时前
vim的`:q!` 与 `ZQ` 笔记250729
linux·笔记·编辑器·vim
jack-hui62 小时前
docker配置gpu运行环境:linux离线安装nvidia-container,避免网络问题
linux·docker·容器
梅羽落3 小时前
PTE之路--01
运维·网络
吃不得辣条3 小时前
网络安全之防火墙
网络·web安全·apache