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 消息的處理
要處理客戶端的消息,我們就要明確來自客戶端的以下三種消息:
- 登錄消息:攜帶用戶名。收到該消息需要將用戶地址信息和昵稱注冊到在線用戶表當中,同時也要檢測地址信息與昵稱是否重復。
_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[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); };
- 登出消息:不攜帶任何內容。收到該消息需要從在線用戶表當中將該用戶刪除。
_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博客