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

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

上篇文章我们实现了英译汉的网络字典,客服端向服务端发送英文,服务端接收数据后回调处理,将翻译后的中文再转发给客户端,这其实和EchoSever一样都是一对一的网络通信。我们也可以实现多个客户端之间进行网络通信,通过服务端将一个客户端发送的消息转发给所有客户端,这样大家都能够看到你发的消息,以此来达到一个简易聊天室的效果。所以我们服务端在处理数据时就不再是简单的进行翻译了,而是要实现一个可以路由转发的功能。

文章目录

  • 1. 封装路由转发功能的类
  • 2. UdpServer.hpp——服务端通信
  • 3. UdpServer.cc——服务端主程序
  • 4. UdpServer.cc——客户端主程序
  • 5. 引入线程池
  • 6. 补充拓展
    • 6.1 关于inet_ntoa
    • 6.2 inet_ntop - 二进制到字符串转换
    • 6.3 inet_pton - 字符串到二进制转换

1. 封装路由转发功能的类

现在我们就不再是将数据进行翻译了,而是进行路由转发

框架如下:

#pragma once#include <iostream>
#include <string>
#include <vector>
#include "Log.hpp"
#include "InetAddr.hpp"using namespace LogModule;class Route
{
public:Route(){}// 路由转发信息void MessageRoute(int sockfd, const std::string& message, InetAddr& peer){}~Route() {}
private:std::vector<InetAddr> _online_user; // 管理在线用户
};

我们要转发给所有在线用户,要怎么发呢?先描述再组织,通过数组来管理我们的在线用户(当然也可以使用其他数据结构,这里采用数组),而在线用户我们可以使用ip和端口号来标识,同时这里我们默认为第一次发送消息就等同于用户登录。

注意:参数中要有文件描述符,因为我们转发给每个在线用户时,是使用sendto系统调用,该函数参数中是需要文件描述符的

那么我们在转发前需要判断用户存不存在,不存在就要把用户添加到数组中

	bool IsExist(InetAddr& peer){for(auto& user : _online_user){if(user == peer){return true;}}return false;}

我们直接将 InetAddr 类的对象比较是不可以的,因为我们没有在 InetAddr 类中重载比较运算符,所以我们可以先重载一个比较运算符

	bool operator==(const InetAddr& addr){return _ip == addr._ip && _port == addr._port;}

添加新用户

	void AddUser(InetAddr& peer){LOG(LogLevel::INFO) << "新增一个在线用户: " << peer.StringAddr();_online_user.push_back(peer);}

同样 ,这里为方便展示每个用户的信息,我们也还可以在 InetAddr 类中实现一个函数,直接返回用户信息的字符串

	std::string StringAddr(){return _ip + ":" + std::to_string(_port);}

如果用户想要退出呢?我们可以让用户通过输入 “QUIT” 来确认用户是否需要退出

	// 路由转发信息void MessageRoute(int sockfd, const std::string& message, InetAddr& peer){if(!IsExist(peer)){AddUser(peer);}std::string send_message = peer.StringAddr() + "# " + message;// 转发for(auto& user : _online_user){sendto(sockfd, send_message.c_str(), send_message.size(), 0, (const struct sockaddr*)&user.NetAddr(), sizeof(user.NetAddr()));}if(message == "QUIT"){LOG(LogLevel::INFO) << "在线用户: " << peer.StringAddr() << " 退出聊天";DeleteUser(peer);}}

注意:我们转发消息的时候,发给每个在线用户是需要每个用户的网络地址的,所以我们可以在实现一个获取用户网络地址的函数(InetAddr中实现)

	struct sockaddr_in& NetAddr(){return _addr;}

转发消息也需要转发给自己,因为我自己也需要看到我发出的消息

最后我们这里还需要实现一个删除用户函数

	void DeleteUser(InetAddr& peer){for(auto iter = _online_user.begin(); iter != _online_user.end(); iter++){if(*iter == peer){LOG(LogLevel::INFO) << "删除一个在线用户: " << peer.StringAddr() << " 成功";_online_user.erase(iter);break;}}}

2. UdpServer.hpp——服务端通信

这里我们服务端需要修改一下,我们现在接收客户端发来的消息后,并不是要将该消息处理好之后再次转发回原来的客户端,而是需要回调进行路由转发,将消息转发给所有在线用户,所以我们回调不需要得到返回结果,并且参数还要多增加一个文件描述符,因为路由转发需要用到文件描述符

那么包装器function需要修改

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

同时,我们也不需要再给原来的客户端再次转发了,因为我们路由转发时,已经给包括原来客户端在内的所有在线用户都转发过了

完整代码:

#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"
#include "InetAddr.hpp"using namespace LogModule;using func_t = std::function<void(int, const std::string&, InetAddr&)>;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 = 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和端口号InetAddr client(peer);buffer[n] = 0;// 回调进行路由转发、_func(_socketfd, buffer, client);}}}~UdpServer() {}
private:int _socketfd;uint16_t _port; // 端口号bool _isrunning;func_t _func;
};

3. UdpServer.cc——服务端主程序

这里我们要实例化出一个路由转发的类,由它提供路由转发的服务

#include <memory>
#include "UdpServer.hpp"
#include "Route.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();// 路由服务Route route;// 网络服务器对象提供网络通信功能std::unique_ptr<UdpServer> usvr = std::make_unique<UdpServer>(port, [&route](int sockfs, const std::string& message, InetAddr& client){route.MessageRoute(sockfs, message, client);});usvr->Init();usvr->Start();return 0;
}

4. UdpServer.cc——客户端主程序

我们先来看一下客户端主程序的代码

#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和portif(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)); // 这里使用memsetserver.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;
}

这里我们如果不发送消息就会阻塞在这,但是我们现实中,一般在群里聊天时,尽管我自己不发送消息,但是我也能看到别人发的消息啊,但是目前我们客户端的代码如果不发送消息就会一直阻塞,收不到任何其他客户端发送的消息,那要怎么做呢?

仔细想想,前面我们在学习多线程的时候,我们其实可以让一个线程发,一个线程收,并行处理,这样哪怕我今天不说话,被阻塞在这,但是不影响另一个线程收消息。

那么这里我们可以引入之前封装的线程

#include <iostream>
#include <string>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include "Log.hpp"
#include "Thread.hpp"using namespace LogModule;
using namespace ThreadModlue;int sockfd = 0;
std::string server_ip;
uint16_t server_port = 0;
pthread_t id;void Recv()
{while(true){// 接收服务器转发回来的数据并回显在控制台上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;}}
}void Send()
{// 填写服务器信息struct sockaddr_in server;memset(&server, 0, sizeof(server)); // 这里使用memsetserver.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";exit(3);}if(input == "QUIT"){pthread_cancel(id);break;}}
}// ./udpclient server_ip server_port
int main(int argc, char *argv[])
{// 客户端需要绑定服务器的ip和portif (argc != 3){std::cerr << "Usage: " << argv[0] << " server_ip server_port" << std::endl;return 1;}Enable_Console_Log_Strategy();server_ip = argv[1];server_port = std::stoi(argv[2]);sockfd = socket(AF_INET, SOCK_DGRAM, 0);if (sockfd < 0){LOG(LogLevel::FATAL) << "socket error";return 2;}// 不需要手动绑定ip和端口,操作系统会分配一个临时端口与ip进行绑定Thread recver(Recv);Thread sender(Send);recver.Start();sender.Start();id = recver.Id();recver.Join();sender.Join();return 0;
}

由于我们创建两个线程之后的执行函数,我们定义在了全局,所以我们需要用到的一些变量也需要改成全局才能在线程中使用

运行测试一下:

在这里插入图片描述
我们可以看到输出混在了一起,由于我们只是实现一个简单的聊天室,不去做前端的一些相关处理,但是这里我们可以使用重定向,可以将客户自己的输入,输出到标准输出中,所有客户发送的消息则输出到标准错误中,那么我们就可以再打开一个服务器,将标准错误重定向到该服务器的终端,这样就能解决输入输出都混在一起的情况。或者也可以使用命名管道打印在另一个服务器的终端,这里我们就不做演示了


5. 引入线程池

我们客户端可以使用多线程,服务端其实也可以,服务端只需要接收消息,然后将消息作为一个任务,后面会有多个客户端给服务端发送消息,那么就会有多个任务,把任务交给准备好的线程池去执行这个任务,也就是进行路由转发

那不就相当于我们服务端是一个生产者,线程池就是一个消费者,那这不就是我们前面说过的生产者消费者模型嘛
在这里插入图片描述

那么在服务端主程序中,我们就不应该直接让Route类型的对象去完成路由转发,而是交给线程池,让线程池中的线程去通过Route来完成路由转发这个任务。

#include <memory>
#include "UdpServer.hpp"
#include "Route.hpp"
#include "ThreadPool.hpp"using namespace ThreadPoolModule;using task_t = std::function<void()>;// ./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();// 路由服务Route route;// 线程池auto tp = ThreadPool<task_t>::GetInstance();// 网络服务器对象提供网络通信功能std::unique_ptr<UdpServer> usvr = std::make_unique<UdpServer>(port, [&route, &tp](int sockfd, const std::string& message, InetAddr& client){task_t t = std::bind(&Route::MessageRoute, &route, sockfd, message, client);tp->Enqueue(t);});usvr->Init();usvr->Start();return 0;
}

这里我们使用了bind包装器,在【C++】专栏中我们有专门介绍过包装器

运行测试:
在这里插入图片描述
在这里插入图片描述

其实在引入线程池之后,我们这里就会出现一些新的问题,还记得在我们线程池中,执行任务不是互斥的,虽然取任务是在互斥量中进行的,但是执行任务在临界区外,所以我们管理在线用户的vector就是一个全局资源,没有互斥锁的保护,我们线程池在访问全局资源时就会产生并发问题

那我们就不仅可以使用一个数组管理在线用户,还可以使用一个数组(其他数据结构也可以)来管理删除的用户,将任务拆分出来,今天我们就是一股脑的就是一个任务,其实可以拆分成新增用户的任务,删除用户的任务,路由转发的任务等等,我们就可以在执行不同任务时加锁,但是这些需要配合协议来完成,但是我们今天还只是对协议有一个朴素的理解,所以我们今天做不了,但是我们可以做。

但是我们也可以直接简单粗暴的在使用消息路由转发函数时加锁

	// 路由转发信息void MessageRoute(int sockfd, const std::string& message, InetAddr& peer){LockGuard lockguard();if(!IsExist(peer)){AddUser(peer);}std::string send_message = peer.StringAddr() + "# " + message;// 转发for(auto& user : _online_user){sendto(sockfd, send_message.c_str(), send_message.size(), 0, (struct sockaddr*)&user.NetAddr(), sizeof(user.NetAddr()));}if(message == "QUIT"){LOG(LogLevel::INFO) << "在线用户: " << peer.StringAddr() << " 退出聊天";DeleteUser(peer);}}

6. 补充拓展

6.1 关于inet_ntoa

#include <arpa/inet.h>char *inet_ntoa(struct in_addr in);

inet_ntoa 这个函数返回了一个char*,很显然是这个函数自己在内部为我们申请了一块内存来保存ip的结果。那么是否需要调用者手动释放呢?

man手册上说,inet_ntoa 函数,是把这个返回结果放到了静态存储区。这个时候不需要我们手动进行释放.
在这里插入图片描述

那么问题来了,如果我们调用多次这个函数,会有什么样的效果呢?
在这里插入图片描述
运行结果如下:
在这里插入图片描述
因为 inet_ntoa 把结果放到自己内部的一个静态存储区,这样第二次调用时的结果会覆盖掉上一次的结果.

同时,inet_ntoa 是非线程安全的,在多线程环境中可能产生竞争条件。因此,在多线程环境下,推荐使用 inet_ntop。这个函数由调用者提供一个缓冲区保存结果,可以规避线程安全问题;


6.2 inet_ntop - 二进制到字符串转换

功能
将网络字节序的二进制 IP 地址转换为点分十进制字符串格式。

#include <arpa/inet.h>
const char *inet_ntop(int af, const void *src, char *dst, socklen_t size);

参数说明

  • af:地址族,如 AF_INET (IPv4) 或 AF_INET6 (IPv6)

  • src:指向网络地址结构的指针

  • dst:目标缓冲区,用于存储转换后的字符串

  • size:目标缓冲区的大小

返回值

  • 成功:指向 dst 的指针

  • 失败:NULL,设置 errno

所以我们下面可以将 InetAddr 类中的网络字节序转换为点分十进制字符串格式的函数修改一下

	InetAddr(struct sockaddr_in &addr): _addr(addr){_port = ntohs(_addr.sin_port);   // 从网络中拿到的数据//_ip = inet_ntoa(_addr.sin_addr); // 网络字节序转点分十进制char ipbuffer[64];inet_ntop(AF_INET, &_addr.sin_addr, ipbuffer, sizeof(ipbuffer));_ip = ipbuffer;}

6.3 inet_pton - 字符串到二进制转换

介绍完 inet_ntop ,那就不得不再介绍一下 inet_pton

功能
将点分十进制字符串格式的 IP 地址转换为网络字节序的二进制格式。

#include <arpa/inet.h>// 字符串 -> 二进制 (Presentation to Network)
int inet_pton(int af, const char *src, void *dst);

参数

  • af:地址族 - AF_INET (IPv4) 或 AF_INET6 (IPv6)

  • src:源字符串(点分十进制 IP 地址)

  • dst:目标缓冲区,存储二进制结果

返回值

  • 1:成功

  • 0:输入不是有效的 IP 地址格式

  • -1:错误(地址族不支持等),设置 errno

我们这里还可以增加一个主机转网络构造函数

	InetAddr(const std::string &ip, uint16_t port):_ip(ip), _port(port){// 主机转网络memset(&_addr, 0, sizeof(_addr));_addr.sin_family = AF_INET;inet_pton(AF_INET, _ip.c_str(), &_addr.sin_addr);_addr.sin_port = htons(_port);}

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

相关文章:

  • Context Compliance Attack:大模型安全的新兴威胁与防御策略
  • 如何通过限制网络访问来降低服务器被攻击的风险?
  • 吉林省建设部网站yy直播是干什么的
  • 13.stack容器和queue容器
  • 详解STL中stack_queue为什么选择deque作为默认容器
  • ubuntu下AstrBot +NapCat QQ机器人
  • 新天力:食品容器领域的领军先锋正式开启资本市场新征程
  • iOS 不上架怎么安装?多种应用分发方式解析,ipa 文件安装、企业签名、Ad Hoc 与 TestFlight 实战经验
  • 郑州网站运营沥林行业网站建设
  • 算法面试(6)------mAP 是什么?如何计算?P-R 曲线怎么看?
  • 企业网站推广可以选择哪些方法?系统定制
  • 深度学习--行人重识别技术(超分辨率网络+ResNet101)附数据集
  • CS50ai: week2 Uncertainty我的笔记B版——当 AI 开始“承认不确定”
  • 泉州网站建设开发怎么制作h5棋牌软件
  • 深入Spring Boot生态中最核心部分 数据库交互spring-boot-starter-data-jpa和Hibernate (指南五)
  • 如何使用Python实现UDP广播
  • ThinkPHP 入门:快速构建 PHP Web 应用的强大框架
  • 系统架构 从_WHAT_走向_HOW_的锻造之路
  • UNIX下C语言编程与实践6-Make 工具与 Makefile 编写:从基础语法到复杂项目构建实战
  • 事业单位网站模板网站开发png图标素材
  • 电子商务网站建设外包服务p2p理财网站开发框架
  • Gateway 集成 JWT 身份认证:微服务统一认证的实战指南
  • C语言数据类型与变量详解
  • 【开题答辩全过程】以 php厦门旅游信息网站管理系统开题为例,包含答辩的问题和答案
  • 《重构工业运维链路:三大AI工具让设备故障“秒定位、少误判”》
  • 大模型的第一性原理考量:基于物理本质与数学基础的范式重构
  • Ubuntu 系统安装 Prometheus+Grafana(附shell脚本一键部署↓)
  • Airbnb内部核心键值存储系统 Mussel 已完成从 v1 到 v2 的重构升级
  • 漳州做网站配博大钱少awordpress国内网站吗
  • 在用户调研中应用卡尔曼滤波:用动态思维重构认知更新