上篇文章中我们实现了一个简单的网络通信EchoServer,客户端给服务端发送一条消息,服务端接收后再转发给客户端,最后客户端接收后回显在控制台上。
那么这篇文章呢,我们就把客户端发来的信息当作英文单词,服务端翻译成中文再转发回去,以此来实现一个英译汉的网络字典。

1. 网络通信部分

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

代码如下:

#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 框架

#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; // 路径 + 文件名
      };

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

apple: 苹果
banana: 香蕉
cat: 猫
dog: 狗
book: 书
pen: 笔
happy: 快乐的
sad: 悲伤的
hello:
: 你好
run: 跑
jump: 跳
teacher: 老师
student: 学生
car: 汽车
bus: 公交车
love: 爱
hate: 恨
hello: 你好
goodbye: 再见
summer: 夏天
winter: 冬天

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

2.2 加载字典

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

const std::string sep = ": ";

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

// 加载预先准备好的字典
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",存在就返回中文

// 翻译
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

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

代码如下:

#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;
    }

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

#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地址和端口打印出来,那肯定还会需要字节序列转换,我们干脆将字节序列转换封装一个类

#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;
            };

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

// 翻译
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;
    }

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

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

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

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

再来测试一下

在这里插入图片描述


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

posted on 2025-10-12 12:57  lxjshuju  阅读(13)  评论(0)    收藏  举报