1.中译英字典 -- DictServer
我们这里先中途插入一个趣味的翻译显示实验,在 EchoServer 的基础上来实现,大部分代码基本都没变,修改了一少部分代码,大家可以仔细看看
先给定一些等会我们要翻译的单词数据 dict.txt
apple: 苹果
banana: 香蕉
cat: 猫
dog: 狗
book: 书
pen: 笔
happy: 快乐的
sad: 悲伤的
run: 跑
jump: 跳
teacher: 老师
student: 学生
car: 汽车
bus: 公交车
love: 爱
hate: 恨
hello: 你好
goodbye: 再见
summer: 夏天
winter: 冬天
IsLand1314: 本人
Common.hpp 修改如下:
Dictionary.hpp
#pragma once#include <iostream>
#include <string>
#include <unordered_map>
#include <fstream>
#include "Common.hpp"const std::string gpath = "./";
const std::string gdictname = "dict.txt";
const std::string gsep = ": "; using namespace LogModule;class Dictionary
{
private:bool LoadDictionary() // 加载字典{std::string file = _path + _filename;std::ifstream in(file.c_str());if(!in.is_open()){LOG(LogLevel::ERROR) << "open file" << file << "error";return false;}std::string line;while(std::getline(in, line)) // operator bool{// happy: 开心的std::string key, value;if(SplitString(line, &key, &value, gsep)){ // line -> key, value_dictionary.insert(std::make_pair(key, value));}}in.close();return true;}public:Dictionary(const std::string &path = gpath, const std::string &filename = gdictname): _path(path),_filename(filename){LoadDictionary();Print();}std::string Translate(const std::string &word){auto iter = _dictionary.find(word);if(iter == _dictionary.end()) return "None"; // 表示没找到return iter->second;}void Print(){for(auto &item : _dictionary){std::cout << item.first << ": " << item.second << std::endl;}}~Dictionary(){}private:std::unordered_map<std::string, std::string> _dictionary;std::string _path;std::string _filename;
};
UdpServer.hpp 修改如下:
#ifndef _UDP_SERVER_HPP__
#define _UDP_SERVER_HPP__#include <iostream>
#include <string>
#include <memory>
#include <cstring>
#include <cerrno>
#include <strings.h>
#include <functional>#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>#include "InetAddr.hpp"
#include "Log.hpp"
#include "Common.hpp"using namespace LogModule;const static int gsockfd = -1;
const static uint16_t gdefaultport = 8080; using func_t = std::function<std::string(const std::string&)>;class UdpServer
{
public:UdpServer(func_t func, uint16_t port = gdefaultport) // 如果不是全缺省,缺省参数一般都放在右边: _sockfd(gsockfd),_addr(port),_isrunning(false),_func(func){}// 都是套路void InitServer(){// 1. 创建套接字 socket_sockfd = ::socket(AF_INET, SOCK_DGRAM, 0); // IP Port 网络 本地if(_sockfd < 0){LOG(LogLevel::FATAL) << "socket: " << strerror(errno);Die(SOCKET_ERR);}LOG(LogLevel::INFO) << "socket success, sockfd is : " << _sockfd; // 测试int n = ::bind(_sockfd, _addr.NetAddr(), _addr.NetAddrLen());if(n < 0){LOG(LogLevel::FATAL) << "bind: " << strerror(errno);Die(BIND_ERR);}LOG(LogLevel::INFO) << "bind success"; // 测试}void Start(){ _isrunning = true; // 启动服务器while(true) // 服务器不能停{char inbuffer[1024]; // stringstruct sockaddr_in peer;socklen_t len = sizeof(peer); // 必须设定ssize_t n = ::recvfrom(_sockfd, inbuffer, sizeof(inbuffer) - 1, 0, CONV(&peer), &len) ; // -1 预留一个空位置给 \0,这里 0 表示采用阻塞的方式进行等待if(n > 0){// 把英文单词转化成为中文inbuffer[n] = 0;std::string result = _func(inbuffer); // 这个是回调,调完上层的接口之后还会回来::sendto(_sockfd, result.c_str(), result.size(), 0, CONV(&peer), sizeof(peer));}}_isrunning = false;}~UdpServer(){if(_sockfd > gsockfd)::close(_sockfd);}private:int _sockfd;InetAddr _addr;bool _isrunning; // 服务器运行状态// 业务(回调方法)func_t _func;
};#endif
UdpServerMain.cc 修改如下:
#include "UdpServer.hpp"
#include "Dictionary.hpp"// ./server_udp localip localport
int main(int argc, char *argv[])
{if(argc != 2){std::cerr << "Usage: " << argv[0] << " localPort" << std::endl;Die(USAGE_ERR);}uint16_t port = std::stoi(argv[1]);ENABLE_CONSOLE_LOG();std::shared_ptr<Dictionary> dict_sptr = std::make_shared<Dictionary>();std::unique_ptr<UdpServer> svr_uptr = std::make_unique<UdpServer>([&dict_sptr](const std::string &word){std::cout << "|" << word << "|" << std::endl;return dict_sptr->Translate(word);}, port);// func_t f = std::bind(&Dictionary::Translate, dict_sptr.get(), std::placeholders::_1); //std::placeholders::_1 是 C++11 引入的一个占位符,常用于绑定函数参数的操作,特别是在与 std::bind 配合使用// std::unique_ptr<UdpServer> svr_uptr = std::make_unique<UdpServer>(f, port);svr_uptr->InitServer();svr_uptr->Start();return 0;
}
2.网络聊天室 -- ChatServer
基本了解
话说我们之前 Echoserver 已经实现了给我发信息,信息也已经可以返回给我的功能,但是如果同时有多个人要发信息的话,这个时候发去的信息就需要记录下来发来的人信息,并且进行维护,然后再把维护的信息给多个人一起看,这就实现了 群聊 的功能
在之前的代码当中,Echo 服务器收到发的信息,然后再转发对应的信息,但是有个问题,这里不仅要一个人收消息,后面还要我们自己去转发给所有人,此时收消息转消息都是同一个人,UDP 数据一旦过大,服务器可能就没时间接收数据了,而且我们前面也说过 UDP 套接字本身是全双工的,全双工的意思就是 在收数据的同时,也可以发送数据,下面我们举个例子
如果我们今天收到一个消息,并且将其封装成一个转发的任务,然后由其他线程来做转发, 而本身服务器只负责进行网络读
注意:我们这个代码是基于 EchoServer 基础上进行修改完善的
User.hpp
#pragma once#include <iostream>
#include <string>
#include <list>
#include <memory>
#include <sys/types.h>
#include <sys/socket.h>
#include "InetAddr.hpp"
#include "Log.hpp"using namespace LogModule;class UserInterface
{
public:virtual ~UserInterface() = default;virtual void SendTo(int sockfd, const std::string &message) = 0;virtual bool operator ==(const InetAddr &u) = 0;
};class User : public UserInterface
{
public:User(const InetAddr &id): _id(id){}void SendTo(int sockfd, const std::string &message) override{LOG(LogLevel::DEBUG) << "send message to" << _id.Addr() << ", info: " << message;int n = ::sendto(sockfd, message.c_str(), message.size(), 0, _id.NetAddr(), _id.NetAddrLen());(void)n;}bool operator ==(const InetAddr &u) override{return _id == u;}~User(){}
private:InetAddr _id;
};// 对用户消息进行路由
// UserManage 把所有用户先管理起来// 把一个新用户添加到在线用户列表,一旦要发信息,我们的 UserManage 不做发送,他要调的就是 User 提供的公共方法
// 这种设计模式就称为 观察者模式 -> observerclass UserManage
{
public:UserManage(){}void AddUser(InetAddr &id){for(auto &user: _online_user){if(*user == id){LOG(LogLevel::INFO) << id.Addr() << " 这个用户已经存在";return ;}}LOG(LogLevel::INFO) << "新增该用户: " << id.Addr();_online_user.push_back(std::make_shared<User>(id)); // 构建 User 对象}void DelUser(const InetAddr &id){}void Router(int sockfd, const std::string &message) // 消息转发{for(auto &user : _online_user){user->SendTo(sockfd, message);}}~UserManage(){}private:std::list<std::shared_ptr<UserInterface>> _online_user; // 在线用户
};
上面这段代码实现了一个简化的用户管理和消息转发系统。它使用了 C++ 的面向对象编程、智能指针以及观察者设计模式。以下是对代码的逐步分析:
1. UserInterface 类
class UserInterface
{
public:virtual ~UserInterface() = default;virtual void SendTo(int sockfd, const std::string &message) = 0;virtual bool operator ==(const InetAddr &u) = 0;
};
UserInterface
是一个抽象基类,定义了用户类应实现的接口。
SendTo
方法用于向指定的套接字发送消息(纯虚函数,子类需要实现)。- 重载
operator ==
用于比较用户的网络地址(也是纯虚函数,子类需要实现)。 - 该类的析构函数是虚拟的,以确保在删除派生类对象时能正确调用析构函数。
2. User 类
class UserInterface
{
public:virtual ~UserInterface() = default;virtual void SendTo(int sockfd, const std::string &message) = 0;virtual bool operator ==(const InetAddr &u) = 0;
};
User
类继承自UserInterface
,实现了SendTo
和operator ==
方法。- 构造函数通过网络地址
InetAddr
来初始化_id
。 SendTo
方法通过sendto
函数将消息发送到指定的用户。日志记录了发送的信息和目标地址。operator ==
用于比较两个User
是否相同,依据是它们的InetAddr
。- 析构函数为空,析构时会自动释放
User
对象。
UserManage 类
class User : public UserInterface
{
public:User(const InetAddr &id): _id(id){}void SendTo(int sockfd, const std::string &message) override{LOG(LogLevel::DEBUG) << "send message to" << _id.Addr() << ", info: " << message;int n = ::sendto(sockfd, message.c_str(), message.size(), 0, _id.NetAddr(), _id.NetAddrLen());(void)n;}bool operator ==(const InetAddr &u) override{return _id == u;}~User(){}private:InetAddr _id; // 用户的网络地址
};
UserManage
类负责管理在线用户,主要功能包括:- 添加用户:
AddUser
方法会检查用户是否已经存在,若不存在则将用户加入到_online_user
列表中。用户通过InetAddr
标识。 - 删除用户:
DelUser
方法目前是空的,可能在后续实现中用于移除用户。 - 消息路由:
Router
方法会遍历所有在线用户,并调用每个用户的SendTo
方法,将消息发送给所有用户。可以理解为消息的广播。
- 添加用户:
_online_user
是一个shared_ptr
类型的列表,管理所有在线用户,避免手动内存管理的麻烦。
4. 观察者模式
代码采用了观察者模式(Observer Pattern),其中:
UserManage
是观察者(Observer),负责管理所有的用户,并能对用户的状态进行操作。User
是被观察者(Subject),通过SendTo
方法接收并处理来自UserManage
的消息。- 当有新消息需要发送时,
UserManage
会通知所有用户调用SendTo
方法,这样的设计能有效地将消息发送逻辑和用户管理逻辑解耦。
观察者模式概念
观察者模式(发布订阅模式)是一种行为型设计模式,用于定义对象之间的一种一对多的依赖关系,使得一个对象状态发生变化时,所有依赖它的对象都会收到通知并自动更新。
其目的:将观察者和被观察者代码解耦,使得一个对象或者说事件的变更,让不同观察者可以有不同的处理,非常灵活,扩展性很强,是事件驱动编程的基础。
UdpServer.hpp 修改如下:
#ifndef _UDP_SERVER_HPP__
#define _UDP_SERVER_HPP__#include <iostream>
#include <string>
#include <memory>
#include <cstring>
#include <cerrno>
#include <strings.h>
#include <functional>#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>#include "InetAddr.hpp"
#include "Log.hpp"
#include "Common.hpp"using namespace LogModule;const static int gsockfd = -1;
const static uint16_t gdefaultport = 8080; using adduser_t = std::function<void (InetAddr &id)>;class UdpServer
{
public:UdpServer(adduser_t adduser, uint16_t port = gdefaultport): _sockfd(gsockfd),_addr(port),_isrunning(false),_adduser(adduser){}// 都是套路void InitServer(){// 1. 创建套接字 socket_sockfd = ::socket(AF_INET, SOCK_DGRAM, 0); // IP Port 网络 本地if(_sockfd < 0){LOG(LogLevel::FATAL) << "socket: " << strerror(errno);Die(SOCKET_ERR);}LOG(LogLevel::INFO) << "socket success, sockfd is : " << _sockfd; // 测试int n = ::bind(_sockfd, _addr.NetAddr(), _addr.NetAddrLen());if(n < 0){LOG(LogLevel::FATAL) << "bind: " << strerror(errno);Die(BIND_ERR);}LOG(LogLevel::INFO) << "bind success"; // 测试}void Start(){ _isrunning = true; // 启动服务器while(true) // 服务器不能停{char inbuffer[1024]; // stringstruct sockaddr_in peer;socklen_t len = sizeof(peer); // 必须设定ssize_t n = ::recvfrom(_sockfd, inbuffer, sizeof(inbuffer) - 1, 0, CONV(&peer), &len) ; // -1 预留一个空位置给 \0,这里 0 表示采用阻塞的方式进行等待if(n > 0){ // 1. 消息内容 && 谁发的InetAddr cli(peer);inbuffer[n] = 0;// 2. 新增用户_adduser(cli);std::string clientinfo = cli.GetIp() + ":" + std::to_string(cli.GetPort()) + " # " + inbuffer;// LOG(LogLevel::DEBUG) << "client say@" << inbuffer; LOG(LogLevel::DEBUG) << clientinfo;std::string echo_string = "echo# ";echo_string += inbuffer;::sendto(_sockfd, echo_string.c_str(), echo_string.size(), 0, CONV(&peer), sizeof(peer));}}_isrunning = false;}~UdpServer(){if(_sockfd > gsockfd)::close(_sockfd);}private:int _sockfd;InetAddr _addr;bool _isrunning; // 服务器运行状态// 新增用户adduser_t _adduser;
};#endif
UdpServerMain.cc 修改如下:
#include "UdpServer.hpp"
#include "User.hpp"// ./server_udp localip localport
int main(int argc, char *argv[])
{if(argc != 2){std::cerr << "Usage: " << argv[0] << " localPort" << std::endl;Die(USAGE_ERR);}uint16_t port = std::stoi(argv[1]);ENABLE_CONSOLE_LOG();// 用户管理模块std::shared_ptr<UserManage> um = std::make_shared<UserManage>();// 网络模块std::unique_ptr<UdpServer> svr_uptr = std::make_unique<UdpServer>([&um](InetAddr &id){um->AddUser(id);}, port);svr_uptr->InitServer();svr_uptr->Start();return 0;
}