当前位置: 首页 > news >正文

深入了解linux网络—— 基于UDP实现翻译和聊天功能

前言

通过学习UDP相关接口,了解了如何使用UDP来进行网络通信;

本篇文章就基于UDP网络通信,增加一些简单的业务(翻译、聊天室)来深刻自己对UDP网络通信的理解。

翻译

首先要实现一个翻译的业务:clinet端给server发送信息,我们将该信息当做一个单词,进行翻译再返回给client端。

要实现翻译,首先就要有一个翻译的字典(english与中文的映射)。

这里就基于文件来实现该字典,在server端运行时,手动调用Load加载字典:

//dict.hpp
#pragma once
#include <iostream>
#include <string>
#include <unordered_map>
#include <fstream>
static std::string default_dictpath = "./dict.txt";
class Dict
{
public:Dict() {}~Dict() {}private:std::unordered_map<std::string, std::string> _dict; // 字典
};

这里要实现Dict这样一个模块,来完善翻译所需要的字典。

1. 加载字典

加载字典,首先得有字典:

dict.txt

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

这里统一使用English : 中文的形式,方便解析。

要加载字典(从文件中读取,并建立映射关系)

  • 这里使用fstream流,打开当前目录下的dict.txt文件;
  • 打开文件之后,就按行读取文件中的内容,并对其进行解析,建立英语单词和中文意思的映射。
  • 在解析时,可能该行内容是无法解析的,这里就简单判断然后输出一条日志;然后继续解析下行内容。
static std::string default_dictpath = "./dict.txt";
static std::string sep = " : ";
class Dict
{
public:Dict() {}~Dict() {}void Load(){// 打开文件std::fstream in(default_dictpath);if (!in.is_open()){LOG(Level::FATAL) << "file open error";exit(1);}// 读取std::string line;while (std::getline(in, line)){// 处理一行信息,建立映射关系auto pos = line.find(sep);if (pos == std::string::npos){LOG(Level::WARNING) << "load error : " << line;continue;}std::string english = line.substr(0, pos);std::string chinese = line.substr(pos + 1);if (english.empty() || chinese.empty()){LOG(Level::WARNING) << " unknow : " << line;continue;}_dict.insert(std::make_pair(english, chinese));LOG(Level::DEBUG) << "load : " << english << " -> " << chinese;}}private:std::unordered_map<std::string, std::string> _dict; // 字典
};

这样,server端创建Dict对象,调用Load()方法加载字典;然后再创建UdpServer对象,启动服务器。

2. 翻译功能

上述实现了Dict字典记载Loadserver端现在可以创建Dict对象;

但是英文和中文的映射_dictDict类内,我们在外部是无法直接访问_dict的,所以Dict就要提供一个方法,该方法的功能就是将给定的英文单词,翻译成中文,然后返回

这个翻译功能就扣实现起来还是非常简单的,只需要通过传递进来的参数word找到对应的中文,然后返回即可。(在未来,在该方法内如果想要知道谁要进行翻译,也可以通过参数获取client端的IP地址和端口号

    std::string Translate(std::string word){if (_dict.count(word) == 0){return "Unknow";}return _dict[word];}

到这里,实现了Dict加载功能,也实现了翻译功能;

但是,接受信息是在UdpServer内部的,对于接受到的信息,如何调用Dict类内部的Translate方法呢?

在之前所实现的Udp通信,server接受到信息之后,只是输出到显示器,然后再信息发送给client端,并没有做数据处理。

这里我们要进行数据处理(将收到的信息当做单词,翻译之后返回)。

这里就可以在Udpserver中新增一个函数对象(回调函数),处理信息只需要调用该函数,将信息传递进去,然后获取返回值即可。

对于这个函数的类型,可以根据实际情况而定

这里就简单一点:using func_t = std::function<std::string(std::string)>;

在后续中,可能想要知道client端的IP地址和端口号,就需要修改该函数类型,将client的IP地址和端口号传递给回调函数。

//udpserver.hpp
using func_t = std::function<std::string(std::string)>;
class UdpServer
{
public:UdpServer(uint16_t port, func_t func) : _sockfd(-1), _port(port), _func(func){}~UdpServer() {}void Init(){// 1. 创建socket_sockfd = socket(AF_INET, SOCK_DGRAM, 0); // AF_INET 网络通信  SOCK_DGRAM  面向数据报if (_sockfd < 0){LOG(Level::DEBUG) << "socket error";exit(1);}LOG(Level::DEBUG) << "socket success, sockfd : " << _sockfd;// 2.1 构建sockaddr_in对象struct sockaddr_in sockin;bzero(&sockin, sizeof(sockin));sockin.sin_family = AF_INET;sockin.sin_addr.s_addr = INADDR_ANY;sockin.sin_port = htons(_port);// 2.2 绑定IP、端口号int n = bind(_sockfd, (struct sockaddr *)&sockin, sizeof(sockin));if (n < 0){LOG(Level::DEBUG) << "bind error";exit(2);}LOG(Level::DEBUG) << "socket success";}void Start(){while (true){char buff[256];struct sockaddr_in peer;socklen_t len;// 接受信息int n = recvfrom(_sockfd, buff, sizeof(buff), 0, (struct sockaddr *)&peer, &len);if (n < 0){LOG(Level::WARNING) << "recvfrom error";continue;}buff[n] = '\0';// 调用回调函数,将读取到的信息传递进去std::string chinese = _func(buff);// 将翻译结果发送给client端int m = sendto(_sockfd, chinese.c_str(), chinese.size(), 0, (struct sockaddr *)&peer, len);if (m < 0){LOG(Level::WARNING) << "sendto error";continue;}}}
private:int _sockfd;uint16_t _port;func_t _func;
};

这里,在使用UdpServer时就要由上层传递信息处理的方法。

也就是说,由上层接收到的信息如何处理;UdpServer只需要通过回调函数调用即可。

//udpserver.cc
int main(int argc, char *argv[])
{if (argc != 2){std::cout << argv[0] << " port" << std::endl;return -1;}uint16_t port = std::stoi(argv[1]);// 1. 加载翻译字典Dict d;d.Load();UdpServer usvr(port, [&d](std::string word) -> std::string{ return d.Translate(word); });usvr.Init();usvr.Start();return 0;
}

到这里基于Udp实现翻译功能就基本完成了,这里通过实现翻译模块,通过回调函数让server在接收到信息之后将信息传给上层,由上层决定如何去处理数据,最后获取返回信息,将返回信息发送给client端。

在这里插入图片描述

扩展:封装IP和Port

在上述的操作中,都是手动创建struct sockaddr_in结构体对象;我们知道struct sockaddr_in中存在三个字段(sin_familysin_addrsin_port)。

这里就对sin_addrsin_port进行封装,在之后使用时,就可以自动化构建;(后续传参需要IPport也可以直接传递封装好的对象)。

封装实现InetAddr

class InetAddr
{
public:InetAddr(){}
private:struct sockaddr_in _addr;std::string _ip;uint16_t _port;
};

而我们在调用bindsendtorecvfrom这些接口都需要传递struct sockaddr*的参数,这里就可以实现类内成员方法来获取struct sockaddr*

以及在后续可能需要IP地址和端口号port,这里都可以实现类内方法来获取:

    struct sockaddr *GetInetAddr() { return (struct sockaddr *)&_addr; }std::string GetIP() { return _ip; }uint16_t GetPort() { return _port; }

此外,我们可以通过IP地址和端口号port来构建InetAddr,有时我们可以绑定IP为INADDR_ANY,就不需要IP地址,直接通过端口号就可以构建struct sockaddr结构体对象。

而我们也可能需要通过struct sockaddr_in结构体对象来获取IP和端口号,这里就通过重载构造函数来实现:

class InetAddr
{
public://通过IP地址和端口号构建InetAddr(std::string ip, uint16_t port): _ip(ip), _port(port){_addr.sin_family = AF_INET;inet_aton(_ip.c_str(), &_addr.sin_addr);_addr.sin_port = htons(_port);}//通过struct sockaddr_in结构体对象构建InetAddr(struct sockaddr_in addr) : _addr(addr){_ip = inet_ntoa(_addr.sin_addr);_port = ntohs(addr.sin_port);}//通过端口号构建InetAddr(uint16_t port) : _ip("0"), _port(port){_addr.sin_family = AF_INET;_addr.sin_addr.s_addr = INADDR_ANY;_addr.sin_port = htons(_port);}
private:struct sockaddr_in _addr;std::string _ip;uint16_t _port;
};

也是我们也需要传递struct sockaddr_in的长度,例如sendto

这里也通过类内函数实现,获取该长度:

    socklen_t GetLen() { return sizeof(_addr); }

到这里就对IP地址和端口号进行了封装,就可以使用InetAddr来构建struct sockaddr对象;也可以获取IP地址和端口号。

有了对IP地址和端口号的封装,在初始化UdpServer时,就无需再自己构建struct sockaddr_in结构体对象,直接通过端口号构建InetAddr对象,通过调用成员函数获取地址和长度即可。

    void Init(){// 1. 创建socket_sockfd = socket(AF_INET, SOCK_DGRAM, 0); // AF_INET 网络通信  SOCK_DGRAM  面向数据报if (_sockfd < 0){LOG(Level::DEBUG) << "socket error";exit(1);}LOG(Level::DEBUG) << "socket success, sockfd : " << _sockfd;InetAddr addr(_port);// 2.2 绑定IP、端口号int n = bind(_sockfd, addr.GetInetAddr(), addr.GetLen());if (n < 0){LOG(Level::DEBUG) << "bind error";exit(2);}LOG(Level::DEBUG) << "socket success";}

以及client端通过命令行参数获取的IP地址和端口号也可以构建InetAddr对象;

聊天室

上述使用UDP通信,简单实现了一个翻译功能;

其中还存在很多问题:

client端是一个进程(线程)既要发送信息,也要接受信息;

server也是一个进程(线程)接受信息、处理信息、发送信息。

在这里插入图片描述

这里简单实现一个聊天室功能,支持群聊;并且将其设计为多线程版本:

client端:

一个线程发送信息、另外一个线程接受信息(一个w线程和一个r线程);(可以通过重定向将键盘输入和接受信息输出分离

server端:

  • 主线程从网络中接受信息之后,将该信息封装成一个任务,将该任务放入线程池任务队列中;

  • 线程池中有任务,唤醒一部分线程去执行任务。

  • 这里要实现聊天室的功能,任务很显然就是将信息分发给所有在线用户

    所以,这里就要再实现一个模块:来完成消息路由

所以,这里要实现的聊天功能抽象来说就是:

在这里插入图片描述

1. 信息路由

要实现聊天室,很显然就要先实现信息路由;

server端将信息封装成一个任务,要让线程去执行(将信息发送给所有在线用户),那是不是就要将所有在线用户组织管理起来;所以,在Rounte中就要存在一个在线用户信息(IP和端口号)的数组(也可以使用set等等)

    class Rounte{Rounte() {}~Rounte() {}private:std::vector<InetAddr> _online_users;};

server要向线程池中放任务,那这个任务(信息路由)就应该在Rounte类内实现;

参数:

  • 要发送信息,首先就要知道sockfd,而创建套接字是servermain线程执行的,要让线程池中的线程去发送信息,那就要将sockfd传递给线程(通过任务传参);
  • 此外,要发送信息,肯定也要将发送的信息传递进来吧。
  • 最后,是不是也要知道这一条信息是谁发的啊(IP地址+端口号);所以,这里就使用封装的InetAddr来传递client端的IP地址和端口号。

那该函数,该如何实现呢?

  1. 首先要维护所有在线用户,在发送信息之前,就要先判断当前用户是否在_online_users中(如果不在就新增);
  2. 然后就是,将信息发送给所有的在线用户(所有的在线用户都在_online_users中,遍历依次发送即可);
  3. 最后,**用户如何退出呢?**这里就简单一些,如果用户发送的信息是QUIT,就表示用户要退出;

用户退出,这里也显示输出一下哪个用户退出,在InetAddr中实现一个方法将IP地址和端口号转化为字符串。

要判断当前用户是否在_online_users中,那我们封装的InetAddr就要支持==判断相等。(IP地址和端口号都相等才认为InetAddr相等)

//InetAddrbool operator==(InetAddr &addr) { return _ip == addr._ip && _port == addr._port; }std::string ToString() { return _ip + ":" + std::to_string(_port); }
//Rounteclass Rounte{bool IsExist(InetAddr &addr){for (auto &user : _online_users){if (user == addr)return true;}return false;}public:Rounte() {}~Rounte() {}void SendTask(int sockfd, const std::string &massage, InetAddr &peer){if (IsExist(peer) == false){_online_users.push_back(peer);LOG(Level::INFO) << "新增了一个在线用户";}// 发送信息std::string str = peer.ToString() + '#' + massage;for (auto &user : _online_users){sendto(sockfd, str.c_str(), str.size(), 0, user.GetInetAddr(), user.GetLen());}if (massage == "QUIT"){LOG(Level::INFO) << peer.ToString() << "用户退出";auto pos = _online_users.begin();while (pos != _online_users.end()){if (*pos == peer)break;}_online_users.erase(pos);}}private:std::vector<InetAddr> _online_users;};

有了Rounte,接下来将server更改为多线程版本,这里直接复用之前实现好的线程池代码;

2. 线程池版server

首先就是接收到信息时,处理信息的函数;

上述Rounte实现的SenTask函数是void(Rounte*, int,const std::string&, InetAddr&)类型,而之前线程池中实现的任务类型是void(void)类型,如何将其连通起来呢?

我们可以在上层使用lambda表达式,将参数传递进来;而在lambda表达式内部,使用C++11中的bind,绑定参数列表;让后再将任务入队列。

using task_t = std::function<void()>;
int main(int argc, char *argv[])
{if (argc != 2){std::cout << argv[0] << " port" << std::endl;return -1;}uint16_t port = std::stoi(argv[1]);// 消息路由Rounte r;// 线程池std::unique_ptr<Threadpool<task_t>> thp = std::make_unique<Threadpool<task_t>>();thp->Start();// 网络通信UdpServer usvr(port, [&r, &thp](int sockfd, const std::string &massage, InetAddr &addr){auto b = std::bind(&Rounte::SendTask,&r,sockfd,massage, addr);thp->Enqueue(b); });usvr.Init();usvr.Start();return 0;
}

这样在server接收到信息之后,只需要调用回调函数将任务入队列,唤醒线程池中线程去执行即可。

//udpserver.hpp
using Task_t = std::function<void(int, const std::string &, InetAddr &)>;
class UdpServer
{
public:UdpServer(uint16_t port, Task_t func) : _sockfd(-1), _port(port), _task(func){}~UdpServer() {}void Init(){// 1. 创建socket_sockfd = socket(AF_INET, SOCK_DGRAM, 0); // AF_INET 网络通信  SOCK_DGRAM  面向数据报if (_sockfd < 0){LOG(Level::DEBUG) << "socket error";exit(1);}LOG(Level::DEBUG) << "socket success, sockfd : " << _sockfd;InetAddr addr(_port);// 2.2 绑定IP、端口号int n = bind(_sockfd, addr.GetInetAddr(), addr.GetLen());if (n < 0){LOG(Level::DEBUG) << "bind error";exit(2);}LOG(Level::DEBUG) << "socket success";}void Start(){while (true){char buff[256];struct sockaddr_in peer;socklen_t len;// 接受信息int n = recvfrom(_sockfd, buff, sizeof(buff), 0, (struct sockaddr *)&peer, &len);if (n < 0){LOG(Level::WARNING) << "recvfrom error";continue;}buff[n] = '\0';InetAddr client(peer);_task(_sockfd, buff, client);//回调函数}}
private:int _sockfd;uint16_t _port;Task_t _task;
};

3. 多线程版client

在上述代码中,server端引入线程池,使用线程池任务向所有在线用户发送信息;

现在对于client,我们也要修改为多线程版本,一个线程写,应该线程读

这个相对比较简单了,这里将所用到的sockfdserver端IP地址和端口号定义成全局方便使用

int sockfd;
InetAddr server;
void *Send(void *argv)
{while (true){std::string massage;std::getline(std::cin, massage);// 发送信息sendto(sockfd, massage.c_str(), massage.size(), 0, server.GetInetAddr(), server.GetLen());}
}
void *recv(void *argv)
{while (true){// 接受信息struct sockaddr_in peer;bzero(&peer, sizeof(peer));socklen_t len = sizeof(len);char buff[256];int n = recvfrom(sockfd, buff, sizeof(buff), 0, (struct sockaddr *)&peer, &len);if (n < 0){std::cerr << "recvfrom error";continue;}buff[n] = '\0';std::cerr << buff << std::endl;}
}
int main(int agrc, char *argv[])
{if (agrc != 3){std::cout << argv[0] << " serverip  serverport" << std::endl;return -1;}server.Set(argv[1], std::stoi(argv[2]));// 创建套接字sockfd = socket(AF_INET, SOCK_DGRAM, 0);if (sockfd < 0){std::cerr << "socket error" << std::endl;return -1;}std::cout << "socket success" << std::endl;pthread_t s, r;pthread_create(&s, nullptr, Send, nullptr);pthread_create(&r, nullptr, recv, nullptr);pthread_join(s, nullptr);pthread_join(r, nullptr);return 0;
}

当然,这里也可以将sockfdInetAddr封装成一个结构体,通过参数传递给新线程。

到这里本篇文章内容就结束了,感谢各位大佬的支持
我的博客即将同步至腾讯云开发者社区,邀请大家一同入驻:https://cloud.tencent.com/developer/support-plan?invite_code=2oul0hvapjsws

http://www.dtcms.com/a/435106.html

相关文章:

  • 基于高斯函数傅里叶变换的函数傅里叶变换求解(含多项式与三角函数项)
  • 2025,跨领域发展的低门槛技能切入路径
  • 如何通过UKey实现文件加密?——基于硬件密钥的端到端数据保护实战指南
  • 公司建的站加油违法吗网站设计知名企业
  • 张家界网站制作与代运营常州微信网站建设
  • 电影网站建设哪家便宜深圳市做网站公司
  • 实战破解前端渲染:当 Requests 无法获取数据时(Selenium/Playwright 入门)
  • 如何建立小企业网站论坛源码哪个好
  • 网站建设摊销时间是多久seo咨询服务
  • 精细化工企业安全运营:危化品投料记录与反应釜压力实时监控方案
  • 网站的ftp信息推广公司哪里找
  • 【精品资料鉴赏】384页WORD版小学智慧校园项目建设初步设计方案
  • 手机移动网站建设怎么把网站放到服务器
  • 《牛刀小试!C++ string类核心接口实战编程题集》
  • 做视频网站资源采集软件app定制开发
  • 【原创】SpringBoot3+Vue3商品信息管理系统
  • 3 阐述网站建设的步骤过程哪种网站开发简单
  • Spring Boot 热部署配置与自定义排除项
  • B007基于博途西门子1200PLC四节传送带控制系统仿真
  • C++11新特性解析与应用(1)
  • 【LangChain】P7 对话记忆完全指南:从原理到实战(下)
  • 上海建设房屋网站下载好了网站模板怎么开始做网站
  • 远程智能康养实训室:训练学生驾驭物联网,服务未来居家康养新时代
  • ⚡ WSL2 搭建 s5p6818 Linux 嵌入式开发平台(part 1):环境准备与架构设计
  • 学科建设网站wordpress 主体安装
  • 如何免费建立自己的网站中国建设摩托车
  • 主机服务器网站 怎么做孝义网站建设
  • 快速搭建企业网站阿里虚拟机建设网站
  • 山西建设机械网站首页备案添加网站
  • 店面建设网站的必要性58同城装修设计师