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 消息的处理
要处理客户端的消息,我们就要明确来自客户端的以下三种消息:
- 登录消息:携带用户名。收到该消息需要将用户地址信息和昵称注册到在线用户表当中,同时也要检测地址信息与昵称是否重复。
_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博客