Linux笔记---UDP套接字实战:简易聊天室

1. 项目需求分析

我们要设计的是一个简单的匿名聊天室,用户的客户端要求用户输入自己的昵称之后即可在一个公共的群聊当中聊天。

为了简单起见,我们设计用户在终端当中与客户端交互,而在一个文件当中显式群聊信息:

当用户输入的聊天内容为[logout]时,客户端将退出,同时服务端删除其在线信息:

程序大致的结构如下:

2. 项目代码

2.1 Server.cpp

#include "Server.hpp"
#include "Router.hpp"
#include "ThreadPool.hpp"
#include <functional>using task_t = std::function<void()>;
using namespace ThreadPoolModule;int main(int argc, char* args[])
{if(argc != 2){std::cerr << "Usage: Server + port" << std::endl;exit(errno); }Router router;in_port_t port = std::stoi(args[1]);UDPServer server(port, [&router](const std::string& message, const InetAddr& client, int fd){task_t func = std::bind(&Router::RouteMessage, &router, message, client, fd);ThreadPool<task_t>::GetInstance()->PushTask(func);});server.Start();return 0;
}

在上面的代码中,我们创建了一个Router对象,这个类用于处理服务端接收到的来自客户端的消息。如何让服务端与Router对象交互呢?

这里我们使用了一个捕获了Router对象的lambda表达式作为服务端接收到消息的回调函数。

在函数内部,使用bind函数将参数绑定到Router对象的核心处理函数上,在将得到的结果放入到线程池的任务队列当中。

所以,对于服务端来说,接下来我们呢只需要完善UDPServer类和Router类即可。在下面这篇文章当中,我们已经将较为通用的UDPServer类给完成了,只需要做一点小调整即可。

Linux笔记---UDP套接字编程-CSDN博客

2.2 Server.hpp

#pragma once
#include <iostream>
#include <functional>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <cstring>
#include "Log.hpp"
#include "InetAddr.hpp"
#define BUFFER_SIZE 1024
#define DEFAULT_PROT 8888
#define EXITSIGNAL "exit"using func_t = std::function<void(const std::string &, const InetAddr&, int)>;
void default_func(const std::string &message, const InetAddr& client, int)
{std::cout << "Client[" << client.Ip() << ":" << client.Port() << "] Massage# " << message << std::endl;
}using namespace LogModule;class UDPServer
{
public:UDPServer(in_port_t port = DEFAULT_PROT, func_t func = default_func): isrunning(false), _port(port), _func(func){// 面向数据报, UDP套接字_sockfd = socket(AF_INET, SOCK_DGRAM, 0);if (_sockfd < 0){LOG(LogLevel::FATAL) << "socket: 套接字创建失败! ";exit(errno);}// 初始化需要绑定的网络信息struct sockaddr_in addr;bzero(&addr, sizeof(addr));addr.sin_addr.s_addr = INADDR_ANY;addr.sin_family = AF_INET;addr.sin_port = htons(_port);int n = bind(_sockfd, (struct sockaddr *)&addr, sizeof(addr));if (n != 0){LOG(LogLevel::FATAL) << "bind: 网络信息绑定失败! ";exit(errno);}LOG(LogLevel::INFO) << "UDPServer: UDP套接字(sockfd=" << _sockfd << ")创建成功";}~UDPServer(){close(_sockfd);}UDPServer(const UDPServer&) = delete;UDPServer& operator=(const UDPServer&) = delete;void Start(){isrunning = true;char buffer[BUFFER_SIZE];while (isrunning){// 等待客户端发送信息struct sockaddr_in client_addr;socklen_t client_addr_len = sizeof(client_addr);ssize_t n = recvfrom(_sockfd, buffer, sizeof(buffer) - 1, 0, (struct sockaddr *)&client_addr, &client_addr_len);if (n > 0){buffer[n] = 0;InetAddr client(client_addr);_func(buffer, client, _sockfd);}}}private:int _sockfd;in_port_t _port;bool isrunning;func_t _func;
};

主要的变化就是,我们在这里包装了一个InetAddr类来简化代码,这个类负责解析并保存struct sockaddr_in当中包含的ip地址和端口号信息。

2.3 InetAddr.hpp

#pragma once
#include <sys/socket.h>
#include <sys/types.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include "Log.hpp"using namespace LogModule;class InetAddr
{
public:InetAddr(const struct sockaddr_in& addr): _addr(addr){_ip = inet_ntoa(_addr.sin_addr);_port = ntohs(_addr.sin_port);}bool operator==(const InetAddr& addr){return (_ip == addr._ip && _port == addr._port);}const std::string& Ip() const {return _ip;}const in_port_t& Port() const {return _port;}const struct sockaddr_in& NetAddr() const {return _addr;}std::string Info() const {return _ip + ":" + std::to_string(_port);}
private:std::string _ip;in_port_t _port;struct sockaddr_in _addr;
};

2.4 Router.hpp

2.4.1 消息的处理

要处理客户端的消息,我们就要明确来自客户端的以下三种消息:

  1. 登录消息:携带用户名。收到该消息需要将用户地址信息和昵称注册到在线用户表当中,同时也要检测地址信息与昵称是否重复。
    _methods[login] = [this](const std::string &context, const InetAddr &client, int sockfd)
    {std::string info;if (FindUser(client) != _online_users.end()){info = "用户[" + client.Info() + "]重复加入";LOG(LogLevel::WARNING) << info;SendToClient(context, client, sockfd);}else if (FindName(context) != _online_users.end()){info = "昵称[" + context + "]已存在";LOG(LogLevel::WARNING) << info;SendToClient(context, client, sockfd);}else{_mutex.lock();_online_users.emplace_back(client, context);_mutex.unlock();info = "用户[" + context + "]加入聊天";LOG(LogLevel::INFO) << info;// 登录成功,回复客户端SendToClient(login, client, sockfd);RouteToClients(info, sockfd);}
    };
  2. 普通消息:携带用户希望发送的内容。收到该消息需要将这条消息同步给所有的在线用户,同时也要检查用户是否已经登录。
    _methods[route] = [this](const std::string &context, const InetAddr &client, int sockfd)
    {auto user = FindUser(client);if (user == _online_users.end()){std::string info = "用户还未登录, 无法发送消息: " + context;LOG(LogLevel::ERROR) << info;SendToClient(info, client, sockfd);return;}std::string info = "[" + user->second + "]# " + context;RouteToClients(info, sockfd);
    };
  3. 登出消息:不携带任何内容。收到该消息需要从在线用户表当中将该用户删除。
    _methods[logout] = [this](const std::string &context, const InetAddr &client, int sockfd)
    {auto it = FindUser(client);if (it == _online_users.end()){std::string info = "用户[" + client.Info() + "]在未登录的情况下登出! ";LOG(LogLevel::WARNING) << info;SendToClient(info, client, sockfd);return;}_mutex.lock();_online_users.erase(it);_mutex.unlock();std::string info = "用户[" + it->second + "]离开聊天";LOG(LogLevel::INFO) << info;SendToClient(logout, client, sockfd);RouteToClients(info, sockfd);
    };

我们将消息的格式定义如下:

[消息类型][: ](冒号加空格作为分隔符)[消息内容]const std::string login = "login"; // 登录消息
const std::string logout = "logout"; // 登出消息
const std::string route = "route"; // 普通消息
const std::string esp = ": "; // 分隔符
2.4.2 成员变量
// 在线用户列表
std::vector<std::pair<InetAddr, std::string>> _online_users;
// 消息处理方法
std::unordered_map<std::string, func_t> _methods;
// 保护_online_users的互斥锁
Mutex _mutex;

我们在构造函数中,将上面的三种方法注册到_methods当中,方便在核心处理函数当中调用。

2.4.3 核心处理函数
void RouteMessage(const std::string message, const InetAddr &client, int sockfd)
{auto pos = message.find(esp);if (pos == std::string::npos){std::string info = "客户端信息格式错误[" + message + "]";LOG(LogLevel::ERROR) << info;SendToClient(info, client, sockfd);return;}std::string type = message.substr(0, pos);std::string context = message.substr(pos + esp.size());if (type.empty() || !_methods.count(type)){std::string info = "错误的消息类型[" + type + "]";LOG(LogLevel::ERROR) << info;SendToClient(info, client, sockfd);return;}_methods[type](context, client, sockfd);
}

只需要将消息的类型与内容分开,再用类型来调用_methods中的方法即可。

2.4.4 完整代码
#pragma once
#include <vector>
#include <unordered_map>
#include <functional>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include "InetAddr.hpp"
#include "Log.hpp"const std::string login = "login";
const std::string logout = "logout";
const std::string route = "route";
const std::string esp = ": ";
using func_t = std::function<void(const std::string &, const InetAddr &, int)>;
using iterator = std::vector<std::pair<InetAddr, std::string>>::iterator;
using namespace LogModule;
using namespace MutexModule;class Router
{
private:iterator FindUser(const InetAddr &client){iterator it;for (it = _online_users.begin(); it != _online_users.end(); it++){if (it->first == client)break;}return it;}iterator FindName(const std::string &name){iterator it;for (it = _online_users.begin(); it != _online_users.end(); it++){if (it->second == name)break;}return it;}void SendToClient(const std::string &context, const InetAddr &client, int sockfd){sendto(sockfd, context.c_str(), context.size(), 0, (struct sockaddr *)&client.NetAddr(), sizeof(client.NetAddr()));}void RouteToClients(const std::string &context, int sockfd){for (auto &user : _online_users){SendToClient(context, user.first, sockfd);}}public:Router(){_methods[login] = [this](const std::string &context, const InetAddr &client, int sockfd){std::string info;if (FindUser(client) != _online_users.end()){info = "用户[" + client.Info() + "]重复加入";LOG(LogLevel::WARNING) << info;SendToClient(context, client, sockfd);}else if (FindName(context) != _online_users.end()){info = "昵称[" + context + "]已存在";LOG(LogLevel::WARNING) << info;SendToClient(context, client, sockfd);}else{_mutex.lock();_online_users.emplace_back(client, context);_mutex.unlock();info = "用户[" + context + "]加入聊天";LOG(LogLevel::INFO) << info;// 登录成功,回复客户端SendToClient(login, client, sockfd);RouteToClients(info, sockfd);}};_methods[logout] = [this](const std::string &context, const InetAddr &client, int sockfd){auto it = FindUser(client);if (it == _online_users.end()){std::string info = "用户[" + client.Info() + "]在未登录的情况下登出! ";LOG(LogLevel::WARNING) << info;SendToClient(info, client, sockfd);return;}_mutex.lock();_online_users.erase(it);_mutex.unlock();std::string info = "用户[" + it->second + "]离开聊天";LOG(LogLevel::INFO) << info;SendToClient(logout, client, sockfd);RouteToClients(info, sockfd);};_methods[route] = [this](const std::string &context, const InetAddr &client, int sockfd) {auto user = FindUser(client);if (user == _online_users.end()){std::string info = "用户还未登录, 无法发送消息: " + context;LOG(LogLevel::ERROR) << info;SendToClient(info, client, sockfd);return;}std::string info = "[" + user->second + "]# " + context;RouteToClients(info, sockfd);};}void RouteMessage(const std::string message, const InetAddr &client, int sockfd){auto pos = message.find(esp);if (pos == std::string::npos){std::string info = "客户端信息格式错误[" + message + "]";LOG(LogLevel::ERROR) << info;SendToClient(info, client, sockfd);return;}std::string type = message.substr(0, pos);std::string context = message.substr(pos + esp.size());if (type.empty() || !_methods.count(type)){std::string info = "错误的消息类型[" + type + "]";LOG(LogLevel::ERROR) << info;SendToClient(info, client, sockfd);return;}_methods[type](context, client, sockfd);}private:std::vector<std::pair<InetAddr, std::string>> _online_users;std::unordered_map<std::string, func_t> _methods;Mutex _mutex;
};

2.5 Client.cpp

#include "Client.hpp"int main(int argc, char* args[])
{if(argc != 3){std::cerr << "Usage: Server + ip + port" << std::endl;exit(errno); }in_port_t port = std::stoi(args[2]);UDPClient client(args[1], port);client.Start();return 0;
}

2.6 Client.hpp

在客户端这边,我们需要有两个线程来分别处理发送与接收,而不是像之前那样发送一条再接收一条。这样才能保证其他用户的消息能及时显示在文件当中。

#pragma once
#include <iostream>
#include <fstream>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <cstring>
#include "Log.hpp"
#include "Thread.hpp"
#define BUFFER_SIZE 1024
#define DEFAULT_PROT 8888using namespace LogModule;
const std::string login = "login";
const std::string logout = "logout";
const std::string route = "route";
const std::string esp = ": ";
const std::string chat_file = "./ChatGroup.txt";class UDPClient
{
public:UDPClient(const std::string &ip, in_port_t port): _server_addr_len(sizeof(_server_addr)), _sender("sender", [this](){Send();}), _reciver("reciver", [this](){Recive();}){_server_addr.sin_addr.s_addr = inet_addr(ip.c_str());_server_addr.sin_family = AF_INET;_server_addr.sin_port = htons(port);_sockfd = socket(AF_INET, SOCK_DGRAM, 0);if (_sockfd < 0){LOG(LogLevel::FATAL) << "socket: 套接字创建失败! ";exit(errno);}LOG(LogLevel::INFO) << "UDPClient: UDP套接字(sockfd=" << _sockfd << ")创建成功";}~UDPClient(){_sender.Join();_reciver.Join();close(_sockfd);}UDPClient(const UDPClient&) = delete;UDPClient& operator=(const UDPClient&) = delete;void Start(){_sender.Start();}void Send(){std::string name, info;char buffer[BUFFER_SIZE] = {0};do{std::cout << "输入用户名以加入聊天: ";std::getline(std::cin, name);info = "login: " + name;sendto(_sockfd, info.c_str(), info.size(), 0, (struct sockaddr*)&_server_addr, _server_addr_len);ssize_t n = recvfrom(_sockfd, buffer, sizeof(buffer), 0, (struct sockaddr*)&_server_addr, &_server_addr_len);buffer[n] = 0;std::cout << buffer << std::endl;} while(buffer != login);_reciver.Start();do{std::cout << "发送到公屏: ";std::getline(std::cin, info);if(info == logout)info += esp;elseinfo = route + esp + info;sendto(_sockfd, info.c_str(), info.size(), 0, (struct sockaddr*)&_server_addr, _server_addr_len);}while(info != logout + esp);}void Recive(){std::fstream clean(chat_file, std::ios::out);clean.close();std::fstream ChatGroup("./ChatGroup.txt", std::ios::app);char buffer[BUFFER_SIZE];do{ssize_t n = recvfrom(_sockfd, buffer, sizeof(buffer), 0, (struct sockaddr*)&_server_addr, &_server_addr_len);buffer[n] = 0;ChatGroup << buffer << std::endl;fflush(stdout);}while(buffer != logout);ChatGroup.close();}private:int _sockfd;struct sockaddr_in _server_addr;socklen_t _server_addr_len;ThreadModule::Thread _sender;ThreadModule::Thread _reciver;
};

3. 其他代码

还有一些.hpp文件在往期的文章当中:

  • 线程池(ThreadPool.hpp):Linux笔记---单例模式与线程池_线程池 单例模式-CSDN博客
  • 日志(Log.hpp):Linux笔记---策略模式与日志-CSDN博客
  • 其他:Linux笔记---线程同步与互斥-CSDN博客

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。
如若转载,请注明出处:http://www.pswp.cn/diannao/97932.shtml
繁体地址,请注明出处:http://hk.pswp.cn/diannao/97932.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

RTP打包与解包全解析:从RFC规范到跨平台轻量级RTSP服务和低延迟RTSP播放器实现

引言 在实时音视频系统中&#xff0c;RTSP&#xff08;Real-Time Streaming Protocol&#xff09;负责会话与控制&#xff0c;而 RTP&#xff08;Real-time Transport Protocol&#xff09;负责媒体数据承载。开发者在实现跨平台、低延迟的 RTSP 播放器或轻量级 RTSP 服务时&a…

Ubuntu 用户和用户组

一、 Linux 用户linux 是一个多用户操作系统&#xff0c;不同的用户拥有不同的权限&#xff0c;可以查看和操作不同的文件。 Ubuntu 有三种用户1、初次创建的用户2、root 用户---上帝3、普通用户初次创建的用户权限比普通用户要多&#xff0c;但是没有 root 用户多。Linux 用户…

FastGPT社区版大语言模型知识库、Agent开源项目推荐

​ FastGPT 项目说明 项目概述 FastGPT 是一个基于大语言模型&#xff08;LLM&#xff09;的知识库问答系统&#xff0c;提供开箱即用的数据处理和模型调用能力&#xff0c;支持通过可视化工作流编排实现复杂问答场景。 技术架构 前端: Next.js TypeScript Chakra UI 后…

jsencrypt公钥分段加密,支持后端解密

前端使用jsencryp实现分段加密。 解决长文本RSA加密报错问题。 支持文本包含中文。 支持后端解密。前端加密代码&#xff1a; // import { JSEncrypt } from jsencrypt const JSEncrypt require(jsencrypt) /*** 使用 JSEncrypt 实现分段 RSA 加密&#xff08;正确处理中文字符…

生成一份关于电脑电池使用情况、健康状况和寿命估算的详细 HTML 报告

核心作用 powercfg /batteryreport 是一个在 Windows 命令提示符或 PowerShell 中运行的命令。它的核心作用是&#xff1a;生成一份关于电脑电池使用情况、健康状况和寿命估算的详细 HTML 报告。 这份报告非常有用&#xff0c;特别是对于笔记本电脑用户&#xff0c;它可以帮你&…

从 0 到 1 实现 PyTorch 食物图像分类:核心知识点与完整实

食物图像分类是计算机视觉的经典任务之一&#xff0c;其核心是让机器 “看懂” 图像中的食物类别。随着深度学习的发展&#xff0c;卷积神经网络&#xff08;CNN&#xff09;凭借强大的特征提取能力&#xff0c;成为图像分类的主流方案。本文将基于 PyTorch 框架&#xff0c;从…

Python 值传递 (Pass by Value) 和引用传递 (Pass by Reference)

Python 值传递 {Pass by Value} 和引用传递 {Pass by Reference}1. Mutable Objects and Immutable Objects in Python (Python 可变对象和不可变对象)2. Pass by Value and Pass by Reference2.1. What is Pass by Value in Python?2.2. What is Pass by Reference in Python…

aippt自动生成工具有哪些?一文看懂,总有一款适合你!

在当今快节奏的工作与学习环境中&#xff0c;传统耗时的PPT制作方式已难以满足高效表达的需求。随着人工智能技术的发展&#xff0c;AI自动生成PPT工具应运而生&#xff0c;成为提升演示文稿制作效率的利器。这类工具通过自然语言处理和深度学习技术&#xff0c;能够根据用户输…

Langflow 框架中 Prompt 技术底层实现分析

Langflow 框架中 Prompt 技术底层实现分析 1. Prompt 技术概述 Langflow 是一个基于 LangChain 的可视化 AI 工作流构建框架&#xff0c;其 Prompt 技术是整个系统的核心组件之一。Prompt 技术主要负责&#xff1a; 模板化处理&#xff1a;支持动态变量替换的提示词模板变量验证…

前端、node跨域问题

前端页面访问node后端接口跨域报错 Access to XMLHttpRequest at http://192.18.31.75/api/get?namess&age19 from origin http://127.0.0.1:5500 has been blocked by CORS policy: No Access-Control-Allow-Origin header is present on the requested resource. 这个报…

超越马力欧:如何为经典2D平台游戏注入全新灵魂

在游戏开发的世界里&#xff0c;2D平台游戏仿佛是一位熟悉的老朋友。从《超级马力欧兄弟》开启的黄金时代到现在&#xff0c;这个类型已经经历了数十年的演变与打磨。当每个基础设计似乎都已被探索殆尽时&#xff0c;我们如何才能打造出一款令人耳目一新的平台游戏&#xff1f;…

基于Springboot + vue3实现的时尚美妆电商网站

项目描述本系统包含管理员和用户两个角色。管理员角色&#xff1a;商品分类管理&#xff1a;新增、查看、修改、删除商品分类。商品信息管理&#xff1a;新增、查看、修改、删除、查看评论商品信息。用户管理&#xff1a;新增、查看、修改、删除用户。管理员管理&#xff1a;查…

网络协议之https?

写在前面 https协议还是挺复杂的&#xff0c;本人也是经过了很多次的学习&#xff0c;依然感觉一知半解&#xff0c;无法将所有的知识点串起来&#xff0c;本次学习呢&#xff0c;也是有很多的疑惑点&#xff0c;但是还是尽量的输出内容&#xff0c;来帮助自己和在看文章的你来…

word运行时错误‘53’,文件未找到:MathPage.WLL,更改加载项路径完美解决

最简单的方法解决&#xff01;&#xff01;&#xff01;安装Mathtype之后粘贴显示&#xff1a;运行时错误‘53’&#xff0c;文件未找到&#xff1a;MathPage.WLLwin11安装mathtype后会有这个错误&#xff0c;这是由于word中加载项加载mathtype路径出错导致的&#xff0c;这时候…

React实现列表拖拽排序

本文主要介绍一下React实现列表拖拽排序方法&#xff0c;具体样式如下图首先&#xff0c;简单展示一下组件的数据结构 const CodeSetting props > {const {$t, // 国际化翻译函数vm, // 视图模型数据vm: {CodeSet: { Enable [], …

将 MySQL 表数据导出为 CSV 文件

目录 一、实现思路 二、核心代码 1. 数据库连接部分 2. 数据导出核心逻辑 3. CSV文件写入 三、完整代码实现 五、输出结果 一、实现思路 建立数据库连接 查询目标表的数据总量和具体数据 获取表的列名作为CSV文件的表头 将查询结果转换为二维数组格式 使用Hutool工具…

一文读懂RAG:从生活场景到核心逻辑,AI“查资料答题”原来这么简单

一文读懂RAG&#xff1a;从生活场景到核心逻辑&#xff0c;AI“查资料答题”原来这么简单 要理解 RAG&#xff08;Retrieval-Augmented Generation&#xff0c;检索增强生成&#xff09;&#xff0c;不需要先背复杂公式&#xff0c;我们可以从一个生活场景切入——它本质是让AI…

git将当前分支推送到远端指定分支

在 Git 中&#xff0c;将当前本地分支推送到远程仓库的指定分支&#xff0c;可以使用 git push 命令&#xff0c;并指定本地分支和远程分支的映射关系。 基本语法 git push <远程名称> <本地分支名>:<远程分支名><远程名称>&#xff1a;通常是 origin&…

【Linux】线程封装

提示&#xff1a;文章写完后&#xff0c;目录可以自动生成&#xff0c;如何生成可参考右边的帮助文档 文章目录 一、为什么需要封装线程库&#xff1f; pthread的痛点&#xff1a; 封装带来的好处&#xff1a; 二、线程封装核心代码解析 1. 头文件定义&#xff08;Thread.hpp&a…

智慧交通管理信号灯通信4G工业路由器应用

在交通信号灯管理中传统的有线通讯&#xff08;光纤、网线&#xff09;存在部署成本高、偏远区域覆盖难、故障维修慢等问题&#xff0c;而4G工业路由器凭借无线化、高稳定、强适配的特性&#xff0c;成为信号灯与管控平台间的数据传输核心&#xff0c;适配多场景需求。智慧交通…