当前位置: 首页 > 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相等)

//InetAddr
bool operator==(InetAddr &addr) { return _ip == addr._ip && _port == addr._port; }
std::string ToString() { return _ip + ":" + std::to_string(_port); }
//Rounte
class 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.hskmm.com/?act=detail&tid=24311

相关文章:

  • Rewind: Codeforces Round 1055 (Div.1+Div.2)
  • 10.4模拟赛总结
  • 01.linux基础
  • 英语完形填空
  • 2025整体橱柜厂家TOP企业品牌推荐排行榜,云南昆明整体橱柜全瓷砖,开放式厨房,经济型,一站式无烟柴火灶,嵌入式,智能,多功能,全屋无烟柴火灶整体橱柜公司推荐
  • Centos7安装mysql8
  • vite-vue3脚手架(参考帝莎编程-后台管理系统开发)
  • 上传文件的后端程序handleFileUpload()、getOriginalFilename()、UUID
  • 从模拟入侵到渗透测试:我摸清了黑客的套路,也懂了企业的软肋 - 详解
  • 同样的Python代码,在Windows上运行没有错误,在Linux Centos上运行出行错误。
  • FreeBSD 14发布后的技术问题解析
  • handleFileUpload()
  • 实用指南:Typescript高级类型详解
  • 集合幂级数,FMT 与 FWT 学习笔记
  • 2025多校CSP模拟赛1
  • 上传文件前端需要注意的三个点:
  • AT_arc189_b [ARC189B] Minimize Sum
  • Jenkins安装与配备
  • 2025-10-04 60S读世界
  • 适合新手的PPT模板网站,简单操作但效果好!
  • 2025多校冲刺CSP模拟赛2 总结
  • pip list 可以查到某个包,但是,import某个包,出现 ModuleNotFoundError: No module named
  • 详细介绍:conda使用指南
  • 探索 Docker/K8s 部署 MySQL 的创新实践与优化技巧 - 详解
  • 基于Registry搭建docker加速镜像服务
  • mssql 无锁读取
  • 2025年四川大学计算机学院专硕考研经验分享
  • 基础数学拾遗
  • 2025多校冲刺CSP模拟赛2(普通的颓唐)
  • 模板大全