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
英文地址,請注明出處:http://en.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;適配多場景需求。智慧交通…