【Linux网络】Socket编程:UDP网络编程实现DictServer

上篇文章中我们实现了一个简单的网络通信EchoServer,客户端给服务端发送一条消息,服务端接收后再转发给客户端,最后客户端接收后回显在控制台上。

那么这篇文章呢,我们就把客户端发来的信息当作英文单词,服务端翻译成中文再转发回去,以此来实现一个英译汉的网络字典。

文章目录

  • [1. 网络通信部分](#1. 网络通信部分)
  • [2. 字典类](#2. 字典类)
    • [2.1 框架](#2.1 框架)
    • [2.2 加载字典](#2.2 加载字典)
    • [2.3 翻译](#2.3 翻译)
  • [3. Udpserver.cc](#3. Udpserver.cc)
  • [4. 封装InetAddr类](#4. 封装InetAddr类)

1. 网络通信部分

首先我们网络通信不需要改变,只需要稍微修改添加一些新的变量,服务端在接收客户端发来的数据,然后回调去处理翻译这个动作,所以我们可以使用包装器function来包装一个函数指针,用于回调处理翻译

代码如下:

cpp 复制代码
#pragma once

#include <iostream>
#include <string>
#include <functional>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include "Log.hpp"


using namespace LogModule;

using func_t = std::function<std::string(const std::string&)>;

class UdpServer
{
public:
    UdpServer(uint16_t port, func_t func)
        :_socketfd(-1), _port(port), _isrunning(false), _func(func)
    {}

    void Init()
    {
        _socketfd = socket(AF_INET, SOCK_DGRAM, 0);
        if(_socketfd < 0)
        {
            LOG(LogLevel::FATAL) << "socket error";
            exit(1);
        }
        LOG(LogLevel::INFO) << "socket success, socketfd: " << _socketfd;

        // 填充sockaddr_in结构体
        struct sockaddr_in local;
        bzero(&local, sizeof(local));
        local.sin_family = AF_INET;
        local.sin_port = htons(_port);
        //local.sin_addr.s_addr = inet_addr(_ip.c_str()); // TODO
        local.sin_addr.s_addr = INADDR_ANY;

        // 绑定IPv4地址结构
        int n = bind(_socketfd, (struct sockaddr*)&local, sizeof(local));
        if(n < 0)
        {
            LOG(LogLevel::FATAL) << "bind error";
            exit(2);
        }
        LOG(LogLevel::INFO) << "bind success, sockfd : " << _socketfd;
    }

    void Start()
    {
        _isrunning = true;
        while(_isrunning)
        {
            char buffer[1024];
            struct sockaddr_in peer;
            socklen_t len = sizeof(peer);
            ssize_t n = recvfrom(_socketfd, buffer, sizeof(buffer)-1, 0, (struct sockaddr*)&peer, &len);
            if(n > 0)
            {
                // 服务端需要知道客户端的ip和端口号
                uint16_t peer_port = ntohs(peer.sin_port); // 从网络中拿到的数据
                std::string peer_ip = inet_ntoa(peer.sin_addr); // 网络字节序转点分十进制

                buffer[n] = 0;

                // 将收到的数据,当作英语单词进行回调处理、
                std::string result = _func(buffer);

                // LOG(LogLevel::DEBUG) << "[" << peer_ip << ":" << peer_port<< "]# " << buffer; // 客户端发送的消息内容

                // 转发回去
                //std::string result = "Server echo# ";
                //result += buffer;
                ssize_t m = sendto(_socketfd, result.c_str(), result.size(), 0, (struct sockaddr*)&peer, len);
                if(n < 0)
                {
                    LOG(LogLevel::FATAL) << "sendto error";
                    exit(3);
                }
            }
        }
    }

    ~UdpServer() {}
private:
    int _socketfd;
    // std::string _ip; // 用的是字符串风格,点分十进制
    uint16_t _port; // 端口号
    bool _isrunning;

    func_t _func;
};

2. 字典类

2.1 框架

cpp 复制代码
#pragma once

#include <iostream>
#include <unordered_map>
#include "Log.hpp"


using namespace LogModule;

const std::string defaultdict = "./dictionary.txt";


class Dict
{
public:
    Dict(const std::string& path = defaultdict)
        :_dict_path(path)
    {}

    // 加载预先准备好的字典
    bool LoadDict()
    {

    }

    // 翻译
    std::string Translate(std::string& word)
    {

    }

    ~Dict()
    {}
private:
    std::unordered_map<std::string, std::string> _dict;
    std::string _dict_path; // 路径 + 文件名
};

这里我们可以使用键值对的方式来查询英文单词对应的中文,并且我们可以预先将准备好的字典存入文件中,然后在翻译前将文件中的字典全部加载到哈希表中

bash 复制代码
apple: 苹果
banana: 香蕉
cat: 猫
dog: 狗
book: 书
pen: 笔
happy: 快乐的
sad: 悲伤的
hello: 
: 你好



run: 跑
jump: 跳
teacher: 老师
student: 学生
car: 汽车
bus: 公交车
love: 爱
hate: 恨
hello: 你好
goodbye: 再见
summer: 夏天
winter: 冬天

这里我们准备了一些单词和对应的中文,同时也增加了一些错误的格式,我们再加载的时候要注意处理

2.2 加载字典

由于我们预加载时,需要将每一行的英文单词和中文插入到哈希表中,所以我们需要将字符串分割,那我们为了方便,如下处理:

cpp 复制代码
const std::string sep = ": ";

直接将在文件中准备好的字典加载到哈希表中,对于错误格式的单词,我们打印一条日志后不做处理,继续加载后续单词

cpp 复制代码
	// 加载预先准备好的字典
    bool LoadDict()
    {
        std::fstream in(_dict_path);
        if(!in.is_open())
        {
            LOG(LogLevel::ERROR) << "打开字典" << _dict_path << "失败";
            return false;
        }
        std::string line;
        while(std::getline(in, line))
        {
            // english: chinese
            auto pos = line.find(sep);
            if(pos == std::string::npos)
            {
                LOG(LogLevel::WARNING) << "解析: " << line << "失败";
                continue; // 解析失败就跳过这个继续加载后续单词
            }
            std::string english = line.substr(0, pos);
            std::string chinese = line.substr(pos + sep.size());
            if(english.empty() || chinese.empty())
            {
                LOG(LogLevel::WARNING) << "没有有效内容: " << line;
                continue; 
            }

            _dict.insert(std::make_pair(english, chinese));
            LOG(LogLevel::DEBUG) << "加载: " << line;
        }

        in.close();
    }

2.3 翻译

翻译还是比较简单的,在哈希表中查找,如果英文单词不存在就返回字符串"None",存在就返回中文

cpp 复制代码
	// 翻译
    std::string Translate(const std::string& word)
    {
        auto iter = _dict.find(word);
        if(iter == _dict.end())
        {
            LOG(LogLevel::DEBUG) << "进入到了翻译模块: " << word << "->None";
            return "None";
        }
        LOG(LogLevel::DEBUG) << "进入到了翻译模块: " << word << "->" << iter->second;
        return iter->second;  
    }

3. Udpserver.cc

服务端主程序已经有网络通信的功能了,我们现在只需要实例化字典对象,先加载字典到哈希表中,再在网络通信时进行翻译

代码如下:

cpp 复制代码
#include <memory>
#include "UdpServer.hpp"
#include "Dict.hpp"

// ./udpserver port
int main(int argc, char* argv[])
{
    if(argc != 2)
    {
        std::cerr << "Usage: " << argv[0] << " port" << std::endl;
        return 1;
    }

    uint16_t port = std::stoi(argv[1]);

    Enable_Console_Log_Strategy();

    // 字典对象提供翻译功能
    Dict dict;
    dict.LoadDict();

    // 网络服务器对象提供网络通信功能
    std::unique_ptr<UdpServer> usvr = std::make_unique<UdpServer>(port, [&dict](const std::string& word)->std::string{
        return dict.Translate(word);
    });
    usvr->Init();
    usvr->Start();
    return 0;
}

客户端不需要动,代码如下:

cpp 复制代码
#include <iostream>
#include <string>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include "Log.hpp"

using namespace LogModule;

// ./udpclient server_ip server_port
int main(int argc, char* argv[])
{
    // 客户端需要绑定服务器的ip和port
    if(argc != 3)
    {
        std::cerr << "Usage: " << argv[0] << " server_ip server_port" << std::endl;
        return 1;
    }

    Enable_Console_Log_Strategy();

    std::string server_ip = argv[1];
    uint16_t server_port = std::stoi(argv[2]);

    int sockfd = socket(AF_INET, SOCK_DGRAM, 0);
    if(sockfd < 0)
    {
        LOG(LogLevel::FATAL) << "socket error";
        return 2;
    }

    // 不需要手动绑定ip和端口,操作系统会分配一个临时端口与ip进行绑定

    // 填写服务器信息
    struct sockaddr_in server;
    memset(&server, 0, sizeof(server)); // 这里使用memset
    server.sin_family = AF_INET;
    server.sin_port = htons(server_port); // 转成网络字节序
    server.sin_addr.s_addr = inet_addr(server_ip.c_str()); // 字符串->网络字节序
    while(true)
    {
        // 从键盘获取要发送的数据
        std::string input;
        std::cout << "Client Enter# ";
        std::getline(std::cin, input);

        // 发送数据给服务器
        ssize_t n = sendto(sockfd, input.c_str(), input.size(), 0, (struct sockaddr*)&server, sizeof(server));
        if(n < 0)
        {
            LOG(LogLevel::FATAL) << "sendto error";
            return 3;
        }

        // 接收服务器转发回来的数据并回显在控制台上
        char buffer[1024];
        struct sockaddr_in peer;
        socklen_t len = sizeof(peer);
        ssize_t m = recvfrom(sockfd, buffer, sizeof(buffer)-1, 0, (struct sockaddr*)&peer, &len);
        if(m > 0)
        {
            buffer[m] = 0;
            std::cout << buffer << std::endl;
        }
    }

    return 0;
}

运行测试一下:


4. 封装InetAddr类

如果有多个客户端访问服务端,进行单词翻译,但是我们并不能看到是哪个客户端发送的数据,所以我们这里可以将客户端信息也打印出来,方便我们知道是哪个客户端在发送数据

所以我们这里可以将客户端的ip地址和端口打印出来,那肯定还会需要字节序列转换,我们干脆将字节序列转换封装一个类

cpp 复制代码
#pragma once
#include <iostream>
#include <string>
#include <sys/socket.h>
#include <sys/types.h>
#include <arpa/inet.h>
#include <netinet/in.h>
// 网络地址和主机地址之间进行转换的类

class InetAddr
{
public:
    InetAddr(struct sockaddr_in &addr)
        : _addr(addr)
    {
        _port = ntohs(_addr.sin_port);   // 从网络中拿到的数据
        _ip = inet_ntoa(_addr.sin_addr); // 网络字节序转点分十进制
    }

    uint16_t Port()
    {
        return _port;
    }

    std::string Ip()
    {
        return _ip;
    }

    ~InetAddr() {}

private:
    struct sockaddr_in _addr;
    std::string _ip;
    uint16_t _port;
};

注意我们是想要在进入翻译模块时,将客户端信息给打印出来查看

cpp 复制代码
	// 翻译
    std::string Translate(const std::string& word, InetAddr& client)
    {
        auto iter = _dict.find(word);
        if(iter == _dict.end())
        {
            LOG(LogLevel::DEBUG) << "进入到了翻译模块: " << "[" << client.Ip() << ":" << client.Port() << "]" << word << "->None";
            return "None";
        }
        LOG(LogLevel::DEBUG) << "进入到了翻译模块: " << "[" << client.Ip() << ":" << client.Port() << "]" << word << "->" << iter->second;
        return iter->second;  
    }

那么在主程序调用翻译时就需要增加参数

cpp 复制代码
	// 网络服务器对象提供网络通信功能
    std::unique_ptr<UdpServer> usvr = std::make_unique<UdpServer>(port, [&dict](const std::string& word, InetAddr& client)->std::string{
        return dict.Translate(word, client);
    });

那么包装器也需要增加一个参数

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

再来测试一下


这篇文章中我们将处理的结果数据转发给一个客户端,那我们可不可以把数据转发给多个客户端呢,让大家都能看到,那不就相当于一个聊天室,大家都可以看到转发的数据。所以我们下篇文章就来实现一个简易版的聊天室

相关推荐
EndingCoder3 小时前
中间件详解与自定义
服务器·javascript·中间件·node.js
AAAAA92403 小时前
5G RedCap模组应用领域分析
网络·物联网·5g·信息与通信
SundayBear3 小时前
基于MCU的文件系统
linux·服务器·单片机
爱研究的小梁3 小时前
乾元通渠道商中标民勤县应急指挥能力提升项目
网络
想睡hhh4 小时前
网络实践——基于epoll_ET工作、Reactor设计模式的HTTP服务
网络·http·设计模式·reactor·epoll
爱隐身的官人5 小时前
Linux配置Java/JDK(解决Kali启动ysoserial.jar JRMPListener报错)暨 Kali安装JAVA8和切换JDK版本的详细过程
java·linux·kali
Algebraaaaa6 小时前
Linux 基本命令超详细解释第三期 grep | wc | 管道符‘|’ | echo | tail | 重定向符
linux
先知后行。7 小时前
Qt 网络编程
开发语言·网络·qt
小生不才yz8 小时前
(三)命令管理-命令历史-history命令的使用
linux