当前位置: 首页 > news >正文

Linux网络之----网络编程

一、简单的回显服务器和客户端

要完成这个内容,首先我们就要熟悉一下上节课的接口,在这里我们还是希望输出内容像这样一样,一段输出,另一端能接收到,而且是以日志的形式,所以logger.hpp还是要再拿过来,这里直接给出~

Logger.hpp

#pragma once#include <iostream>
#include <string>
#include <filesystem> // C++17 文件操作
#include <fstream>
#include <ctime>
#include <unistd.h>
#include <memory>
#include <sstream>
#include "Mutex.hpp"using namespace std;
// 规定出场景的日志等级
enum class LogLevel
{DEBUG,INFO,WARNING,ERROR,FATAL
};string Level2String(LogLevel level)
{switch (level){case LogLevel::DEBUG:return "DEBUG";case LogLevel::INFO:return "Info";case LogLevel::WARNING:return "Warning";case LogLevel::ERROR:return "Error";case LogLevel::FATAL:return "Fatal";default:return "Unknown";}
}string GetCurrentTime()
{// 获取时间戳time_t currtime = time(nullptr);// 把时间戳转换成为20XX-08-04 12:27:03struct tm currtm;localtime_r(&currtime, &currtm);// 转换成为字符串char timebuffer[64];snprintf(timebuffer, sizeof(timebuffer), "%4d-%02d-%02d %02d:%02d:%02d",currtm.tm_year + 1900,currtm.tm_mon + 1,currtm.tm_mday,currtm.tm_hour,currtm.tm_min,currtm.tm_sec);return timebuffer;
}
// 基类方法
class LogStrategy
{
public:virtual ~LogStrategy() = default;virtual void SyncLog(const string &logmessage) = 0;
};
// 显示器刷新
class ConsoleLogStrategy : public LogStrategy
{
public:~ConsoleLogStrategy(){}void SyncLog(const string &logmessage) override{LockGuard lockguard(&_lock);cout << logmessage << endl;}private:Mutex _lock;
};const std::string logdefaultdir = "log";
const static std::string logfilename = "test.log";// 文件刷新
class FileLogStrategy : public LogStrategy
{
public:FileLogStrategy(const string &dir = logdefaultdir,const string &filename = logfilename): _dir_path_name(dir), _filename(filename){LockGuard lockguard(&_lock);if (filesystem::exists(_dir_path_name)){return;}try{filesystem::create_directories(_dir_path_name);}catch (const filesystem::filesystem_error &e){std::cerr << e.what() << "\r\n";}}void SyncLog(const string &logmessage) override{LockGuard lockguard(&_lock);string target = _dir_path_name;target += "/";target += _filename;ofstream out(target.c_str(), ios::app);if (!out.is_open()){return;}out << logmessage << "\n"; // out.writeout.close();}~FileLogStrategy(){}private:string _dir_path_name;string _filename;Mutex _lock;
};// 网络刷新
// 1. 定制刷新策略
// 2. 构建完整的日志
class Logger
{
public:Logger(){}void EnableConsoleLogStrategy(){_strategy = make_unique<ConsoleLogStrategy>();}void EnableFileLogStrategy(){_strategy = make_unique<FileLogStrategy>();}// 形成一条完整日志的方式class LogMessage{public:LogMessage(LogLevel level, string &filename, int line, Logger &logger): _curr_time(GetCurrentTime()),_level(level),_pid(getpid()),_filename(filename),_line(line),_logger(logger){std::stringstream ss;ss << "[" << _curr_time << "] "<< "[" << Level2String(_level) << "] "<< "[" << _pid << "] "<< "[" << _filename << "] "<< "[" << _line << "]"<< " - ";_loginfo = ss.str();}template<class T>LogMessage& operator <<(const T&info){stringstream ss;ss<<info;_loginfo+=ss.str();return *this;}~LogMessage(){if(_logger._strategy){_logger._strategy->SyncLog(_loginfo);}}private:std::string _curr_time; // 日志时间LogLevel _level;        // 日志等级pid_t _pid;std::string _filename;int _line;string _loginfo; // 一条合并完成的,完整的日志信息Logger &_logger; // 提供刷新策略的具体做法};LogMessage operator()(LogLevel level, string filename, int line){return LogMessage(level, filename, line, *this);}~Logger(){}private:unique_ptr<LogStrategy> _strategy;
};Logger logger;
#define LOG(level) logger(level, __FILE__, __LINE__)
#define EnableConsoleLogStrategy() logger.EnableConsoleLogStrategy()
#define EnableFileLogStrategy() logger.EnableFileLogStrategy()

Mutex.hpp

#pragma once
#include <iostream>
#include <mutex>
#include <pthread.h>class Mutex
{
public:Mutex(){pthread_mutex_init(&_lock, nullptr);}void Lock(){pthread_mutex_lock(&_lock);}void Unlock(){pthread_mutex_unlock(&_lock);}pthread_mutex_t *Get(){return &_lock;}~Mutex(){pthread_mutex_destroy(&_lock);}private:pthread_mutex_t _lock;
};class LockGuard
{
public:LockGuard(Mutex*_mutex):_mutexp(_mutex){_mutexp->Lock();}~LockGuard(){_mutexp->Unlock();}
private:Mutex *_mutexp;
};

现在我们来写客户端和用户端,先看服务端,我们将分为一下几个步骤进行编写,

server部分:

1)初始化部分,包含创建套接字(socket函数),填充IP和PORT,并将其绑定(bind函数)

2)运行部分,首先可以设置一个运行属性(_isruuning),之后搞一个死循环(实际上软件都是死循环的),之后由于我们要进行数据传递,那就要用到recvfrom函数,看这个函数的参数,我们就知道要先设置一个缓冲区buffer(别忘了初始化置零),但是该函数返回的是字节数,我们的工作还没有完成,我们还需要拿到ip和端口号,这就要用到ntohs函数和inet_ntoa函数,之后将多余的缓冲区空间置零就好,之后进行打印输出(LOG自定义函数),最后进行发送(sendto函数)就好啦~之后在退出之后记得将_isrunning属性置为false

3)停止部分,就直接将_isrunning属性置为false就好,没别的了

server.hpp

#pragma once#include <iostream>
#include <string>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <strings.h>
#include <cstdlib>
#include "Logger.hpp"static const int gdefaultsockfd = -1;
class UdpServer
{
public:UdpServer(uint16_t port): _port(port), _socketfd(gdefaultsockfd), _isrunning(false){}void Init(){// 1. 创建socket fd_socketfd = socket(AF_INET, SOCK_DGRAM, 0);if (_socketfd < 0){LOG(LogLevel::FATAL) << "create socket error";exit(1);}LOG(LogLevel::INFO) << "create socket success : " << _socketfd;// 2. bind// 2.1: 填充IP和Port// 我们有没有实现,把socket和file关联起来呢??没有!!!struct sockaddr_in local;bzero(&local, sizeof(local));local.sin_family = AF_INET;                // IPV4地址,UDP协议local.sin_port = htons(_port);             // 端口号,大端序,网络字节序local.sin_addr.s_addr = htonl(INADDR_ANY); // 任意IPbind// 2.2 和socketfd进行bindint n = bind(_socketfd, (struct sockaddr *)&local, sizeof(local));if (n < 0){LOG(LogLevel::FATAL) << "bind socket error";exit(2);}LOG(LogLevel::INFO) << "bind socket success : " << _socketfd;}void Start(){// 所有的服务器都是死循环_isrunning = true;while (_isrunning){char buffer[1024];buffer[0] = 0; // 清空缓冲区struct sockaddr_in peer;socklen_t len = sizeof(peer);// 1.读取数据ssize_t n = recvfrom(_socketfd, buffer, sizeof(buffer), 0, (struct sockaddr *)&peer, &len);if (n > 0){// client是谁啊??ip和端口给我uint16_t clientport = ntohs(peer.sin_port);string clientip = inet_ntoa(peer.sin_addr);buffer[n] = 0; // n是返回的字节数LOG(LogLevel::DEBUG) << "[" << clientip<< ":" << clientport << "]# " << buffer;string echo_string="server echo#";echo_string+=buffer;sendto(_socketfd,echo_string.c_str(),echo_string.size(),0,(struct sockaddr*)&peer,len);}}_isrunning=false;}void Stop(){_isrunning=false;}~UdpServer() {}private:int _socketfd;uint16_t _port; // 端口号,将IP和端口号绑定在一起就是套接字bool _isrunning;
};

完成上述部分,就代表我们完成了hpp,之后.cc应该就好写了,直接调用.hpp的库函数就可以了,就是注意一下可以加一个检测,即如果用户传递参数个数错误进行报错,因为我们这个server要传两个参数,实现这个就用main函数自带的argc参数就好

server.cc

#include"server.hpp"
#include<iostream>
#include<memory>void Usage(string proc)
{cerr<<"Usage:"<<proc<<"localport"<<endl;
}
int main(int argc,char* argv[])
{if(argc!=2)    //保证输入的是两个命令行参数{Usage(argv[0]);exit(0);}uint16_t port=stoi(argv[1]);//将字符串转换为整数EnableConsoleLogStrategy();unique_ptr<UdpServer> usvr=make_unique<UdpServer>(port);usvr->Init();usvr->Start();return 0;
}

client部分:

直接写一个main函数得了,还是先检测传参个数,这里要传三个,分别为./udp_client server_ip server_port 完成这一步之后,从argv[1]中读取serverip,从argv[2]中读取serverport,之后还是创建套接字(socket函数),创建之后对server的各个成员变量进行初始化和赋值,之后就是cliient的死循环,假设我们已经写好了内容,姑且将其称为line,之后我们将这个line进行sendto,send到server那里,之后我们再recvfrom读就好了(此处我们是想进行回显,才这样做的,即我发了什么,对方收到后又会回显回来

client.cc

#include <iostream>
#include <string>
#include <sys/types.h>
#include <sys/socket.h>
#include<cstring>
#include <netinet/in.h>
#include <arpa/inet.h>
using namespace std;void Usage(string proc)
{cerr << "Usage:" << proc << "serverport" << endl;
}
// ./udp_client server_ip server_port
int main(int argc, char *argv[])
{if (argc != 3){Usage(argv[0]);exit(0);}string serverip = argv[1];uint16_t serverport = stoi(argv[2]);int sockfd = socket(AF_INET,SOCK_DGRAM,0);if(sockfd<0){cout << "create socket errror" << endl;return 0;}struct sockaddr_in server;memset(&server,0,sizeof(server));server.sin_family=AF_INET;server.sin_port=htons(serverport);server.sin_addr.s_addr=inet_addr(serverip.c_str());while(true){cout<< "Please Enter@ ";string line;getline(cin,line);//写sendto(sockfd,line.c_str(),line.size(),0,(struct sockaddr*)&server,sizeof(server));//读struct sockaddr_in temp;socklen_t len=sizeof(temp);char buffer[1024];int m=recvfrom(sockfd,buffer,sizeof(buffer),0,(struct sockaddr*)&temp,&len);if(m>0){buffer[m]=0;cout<<buffer<<endl;}}return 0;
}

Makefile

.PHONY:all
all:udpclient udpserver
udpclient:udpclient.ccg++ -o $@ $^ -std=c++17
udpserver:udpserver.ccg++ -o $@ $^ -std=c++17.PHONY:clean
clean:rm -f udpserver udpclient

我们运行一下:

完成!!

二、英文词典服务器

Logger.hpp和Mutex.hpp和前面都是一样的,这里我就不再重新写了~

我们这里要做一个可以查询英文释义的词典,那么我们先讨论一下如何查找的问题,假设现在我们已经有了词典:dict.txt,如下所示:

dict.txt

apple: 苹果
banana: 香蕉
cat: 猫
dog: 狗
book: 书
pen: 笔
happy: 快乐的
sad: 悲伤的
run: 跑
jump: 跳
teacher: 老师
student: 学生
car: 汽车
bus: 公交车
love: 爱
hate: 恨
hello: 你好
goodbye: 再见
summer: 夏天
winter: 冬天
: 冬天
spring:
:world: 世界

(我故意写错了几个,以便于处理异常情况)

好,我们是这样处理这个词典的:初始化部分,先通过路径传参将字典对象进行初始化,之后就可以进行我们的翻译功能了,假设现在我们在浏览dict.txt,此时读到了一个单词,我们要做的就是先找到其分隔符,并将其一分为二成两个字符串,一部分为英文,另一部分为中文,那么我们要做的就是将这两个捆绑在一起,之后再插入我们的哈希表(unordered_map)中。待dict.txt浏览完成之后,将其关闭即可。

dictionary.hpp:

#pragma once#include <iostream>
#include <string>
#include <fstream>
#include <unordered_map>
#include "Logger.hpp"
using namespace std;
static const string sep = ": ";
class Dictionary
{
public:Dictionary(const string&path):_path(path){LOG(LogLevel::INFO)<<"construct Dictionary obj";LoadConf();}string Translate(const string&word,const string&whoip,uint16_t whoport){(void)whoip;(void)whoport;auto iter=_dict.find(word);if(iter==_dict.end()){return "unkown";}return iter->first+iter->second;}~Dictionary(){}
private:string _path;unordered_map<string,string> _dict;void LoadConf(){ifstream in(_path);   //从文件中读取数据if(!in.is_open()){LOG(LogLevel::ERROR)<<"open file error:"<<_path;return;}string line;while(getline(in,line)){LOG(LogLevel::DEBUG)<<"load dict message: "<<line;//dog: 狗auto pos =line.find(sep);   //找到分隔符if(pos==string::npos)     //防止这种情况出现    dog:{LOG(LogLevel::WARNING)<<"format error:"<<line;continue;    //查单词软件也要做成死循环,不能写成break或者exit}string word=line.substr(0,pos);    //取出单词部分  [)string value=line.substr(pos+sep.size());if(word.empty()||value.empty()){LOG(LogLevel::WARNING)<<"format error, word or value is empty: " << line;continue;}_dict.insert(make_pair(word,value));   //将单词和翻译捆绑到一起}in.close();    //读取文件之后一定一定要记得close}
};

dictserver.hpp:

大体逻辑跟上一个差不多,就是缓冲区那里我们填入单词就好了,之后可以设置一个回调函数,方便我们传参,代码如下:

#pragma once#include <iostream>
#include <string>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <strings.h>
#include <cstdlib>
#include <functional>
#include "Logger.hpp"
using namespace std;
using callback_t = function<string(const string &word, const string &whoip, uint16_t whoport)>;
static const int gdefaultsockfd = -1;
class DictServer
{
public:DictServer(uint16_t port, callback_t cb): _port(port), _sockfd(gdefaultsockfd), _isrunning(false), _cb(cb){}void Init(){// 1. 创建socket fd_sockfd = socket(AF_INET, SOCK_DGRAM, 0);if (_sockfd < 0){LOG(LogLevel::FATAL) << "create socket error";exit(1);}LOG(LogLevel::INFO) << "create socket success:" << _sockfd;// 2. bind// 2.1: 填充IP和Port// 我们有没有实现,把socket和file关联起来呢??没有!!!struct sockaddr_in local;bzero(&local, sizeof(local));local.sin_family = AF_INET;local.sin_port = htons(_port);// 什么叫做任意IP bind? 不明确具体IP,只要是发给我对应的主机,对应的port// 我都能收到!local.sin_addr.s_addr = htonl(INADDR_ANY); // 任意IPbind// 2.2 和socketfd进行bindint n = bind(_sockfd, (struct sockaddr *)&local, sizeof(local));if (n < 0){LOG(LogLevel::FATAL) << "bind socket error";exit(2);}LOG(LogLevel::INFO) << "bind socket success : " << _sockfd;}void Start(){_isrunning = true;while (_isrunning){char buffer[1024];buffer[0] = 0;struct sockaddr_in peer;socklen_t len = sizeof(peer);// 1. 读取数据ssize_t n = recvfrom(_sockfd, buffer, sizeof(buffer), 0,(struct sockaddr *)&peer, &len);if (n > 0){buffer[n] = 0;// client是谁啊??ip和端口给我!uint16_t clientport = ntohs(peer.sin_port);string clientip = inet_ntoa(peer.sin_addr);string word = buffer;LOG(LogLevel::DEBUG) << "用户查找: " << word;// 回调!string result = _cb(word, clientip, clientport);sendto(_sockfd, result.c_str(), result.size(), 0, (struct sockaddr *)&peer, len);}}_isrunning = false;}void Stop(){_isrunning = false;}~DictServer(){}
private:int _sockfd;uint16_t _port;callback_t _cb;bool _isrunning;
};

这一这一部分写好后,我们将两个.cc文件改造一下之后就好啦~

改造dictserver.cc文件:

还是先进行参数个数检测,之后将dict.txt传进去,以便于unordered_map插入数据,之后进行类的实例化,这里可以用lambda表达式来完成,之后调用类内成员函数就ok了~

dictserver.cc:

#include "Dictionary.hpp"
#include "DictServer.hpp"
#include <iostream>
#include <memory>
using namespace std;
void Usage(std::string proc)
{std::cerr << "Usage: " << proc << " localport" << std::endl;
}int main(int argc,char* argv[])
{if(argc!=2){Usage(argv[0]);exit(0);}EnableConsoleLogStrategy();uint16_t port=stoi(argv[1]);Dictionary dict("./dict.txt");unique_ptr<DictServer> usvr=make_unique<DictServer>(port,[&dict](const string&word,const string &whoip,uint16_t whoport)->string{return dict.Translate(word,whoip,whoport);});usvr->Init();usvr->Start();return 0;
}

dictclient.cc变化差别不大,直接给出了

dictclient.cc:

#include<iostream>
#include <string>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <cstring>
using namespace std;void Usage(string proc)
{cerr<< "Usage: " << proc << " serverip serverport" << endl;
}//  ./udp_client server_ip server_port
int main(int argc,char*argv[])
{if(argc!=3){Usage(argv[0]);exit(0);}string serverip=argv[1];uint16_t serverport=stoi(argv[2]);int sockfd=socket(AF_INET,SOCK_DGRAM,0);if(sockfd<0){cout<<"create socket errror" <<endl;return 0;}struct sockaddr_in server;memset(&server,0,sizeof(server));server.sin_family=AF_INET;server.sin_port=htons(serverport);server.sin_addr.s_addr=inet_addr(serverip.c_str());while(true){cout<< "Please Enter@ ";string line;getline(cin,line);//写sendto(sockfd,line.c_str(),line.size(),0,(struct sockaddr*)&server,sizeof(server));//读struct sockaddr_in temp;socklen_t len=sizeof(temp);char buffer[1024];int m=recvfrom(sockfd,buffer,sizeof(buffer),0,(struct sockaddr*)&temp,&len);if(m>0){buffer[m]=0;cout<<buffer<<endl;}}return 0;
}

makefile:

.PHONY:all
all:DictClient DictServer
DictClient:DictClient.ccg++ -o $@ $^ -std=c++17
DictServer:DictServer.ccg++ -o $@ $^ -std=c++17.PHONY:clean
clean:rm -f DictServer DictClient

运行一下:

结果如下:

三、简单聊天室

我们现在的任务室完成简单聊天室的设计,既然是聊天室,那就说明不能只是一问一答,可能要有多个线程同时在上面运行,所以我们把前面的一些文件直接cp过来就好,这里还是直接给出:

thread.hpp

#ifndef __THREAD_HPP__
#define __THREAD_HPP__#include <iostream>
#include <string>
#include <pthread.h>
#include <unistd.h>
#include <functional>
#include <sys/syscall.h> /* For SYS_xxx definitions */
#include "Logger.hpp"#define get_lwp_id() syscall(SYS_gettid)
using namespace std;
using func_t = function<void(const string &name)>;
const string threadnamedefault = "None-Name";class Thread
{
public:Thread(func_t func,const string& name=threadnamedefault):_name(name),_func(func),_isrunning(false){LOG(LogLevel::INFO)<<_name<<" create thread obj success";}static void *start_route(void *args)   //让线程开始运行{Thread *self=static_cast<Thread*>(args);self->_isrunning=true;self->_lwpid=get_lwp_id();self->_func(self->_name);pthread_exit((void*)0);}void Start(){int n=pthread_create(&_tid,nullptr,start_route,this);      //???为什么传递this指针if(n==0){LOG(LogLevel::INFO)<<_name<<"running success";}}void Stop(){int n=pthread_cancel(_tid);// 太简单粗暴了(void)n;}// 检测线程结束并且回收的功能void Join(){if(!_isrunning)return;int n=pthread_join(_tid,nullptr);if(n==0){LOG(LogLevel::INFO)<<_name<<" pthread_join success";}}~Thread(){}private:bool _isrunning;pthread_t _tid;pid_t _lwpid;string _name;func_t _func;
};#endif

ThreadPool.hpp

#pragma once
#include <iostream>
#include <cstdio>
#include <queue>
#include <vector>
#include <unistd.h>
#include "Mutex.hpp"
#include "Cond.hpp"
#include "thread.hpp"
using namespace std;
// 单例线程池 - 懒汉模式
const static int defaultthreadnum = 3; // for debugtemplate <class T>
class ThreadPool
{
public:void Start(){if (_isrunning)return;_isrunning = true;for (auto &t : _threads){t.Start();}LOG(LogLevel::INFO) << "thread pool running success";}void Stop(){if (!_isrunning)return;_isrunning = false;if (_wait_thread_num)_cond.NotifyAll();}void Wait(){for (auto &t : _threads){t.Join();}LOG(LogLevel::INFO) << "thread pool wait success";}void Enqueue(const T &t){if (!_isrunning)return;{LockGuard lockguard(&_lock);_q.push(t);if (_wait_thread_num > 0)_cond.NotifyOne();}}// debugstatic std::string ToHex(ThreadPool<T> *addr){char buffer[64];snprintf(buffer, sizeof(buffer), "%p", addr);return buffer;}// 获取单例 ??static ThreadPool<T> *GetInstance(){// A, B, c{// 线程安全,提高效率式的获取单例if(!_instance){LockGuard lockguard(&_singleton_lock);if(!_instance){_instance=new ThreadPool<T>();LOG(LogLevel::DEBUG) << "线程池单例首次被使用,创建并初始化, addr: " <<ToHex(_instance);_instance->Start();}}}return _instance;}~ThreadPool(){}
private:// 任务队列std::queue<T> _q; // 整体使用的临界资源// 多个线程vector<Thread> _threads; // 1. 创建线程对象 2. 让线程对象启动int _threadnum;int _wait_thread_num;// 保护机制Mutex _lock;Cond _cond;// 其他属性bool _isrunning;// 单例中静态指针static ThreadPool<T> *_instance;static Mutex _singleton_lock;bool QueueIsEmpty(){return _q.empty();}void Routine(const string &name){while (1){T t;LockGuard lockguard(&_lock);while (QueueIsEmpty() && _isrunning){_wait_thread_num++;_cond.Wait(_lock);_wait_thread_num--;}if (!_isrunning && QueueIsEmpty()){LOG(LogLevel::INFO) << " 线程池退出 && 任务队列为空, " << name << " 退出";break;}// 队列中一定有任务了!, 但是// 1. 线程池退出 -- 消耗历史// 2. 线程池没有退出 -- 正常工作t = _q.front();_q.pop();t();}}ThreadPool(int threadnum = defaultthreadnum): _threadnum(threadnum), _isrunning(false), _wait_thread_num(0){for (int i = 0; i < _threadnum; i++){// 方法1:// auto f = std::bind(hello, this);// 方法2string name = "thread-" + to_string(i + 1);_threads.emplace_back([this](const string &name){ this->Routine(name); }, name);// Thread t([this](){//     this->hello();// }, name);// _threads.push_back(st::move(t));}LOG(LogLevel::INFO) << "thread pool obj create success";}ThreadPool<T> &operator=(const ThreadPool<T> &) = delete;ThreadPool(const ThreadPool<T> &) = delete;
};
// 静态成员变量需要在类外进行定义和初始化
template <class T>
ThreadPool<T> *ThreadPool<T>::_instance = nullptr;
template <class T>
Mutex ThreadPool<T>::_singleton_lock;

多线程还需要条件变量,把cond.hpp也拉过来

cond.hpp

#pragma once
#include<pthread.h>
#include<iostream>
#include"Mutex.hpp"class Cond
{
public:Cond(){pthread_cond_init(&_cond,nullptr);}void Wait(Mutex &lock){int n=pthread_cond_wait(&_cond,lock.Get());}void NotifyOne()   //激活一个线程{int n=pthread_cond_signal(&_cond);(void)n;}void NotifyAll(){int n=pthread_cond_broadcast(&_cond);(void)n;}~Cond(){pthread_cond_destroy(&_cond);}
private:pthread_cond_t _cond;
};

mutex.hpp

#pragma once
#include <iostream>
#include <mutex>
#include <pthread.h>class Mutex
{
public:Mutex(){pthread_mutex_init(&_lock, nullptr);}void Lock(){pthread_mutex_lock(&_lock);}void Unlock(){pthread_mutex_unlock(&_lock);}pthread_mutex_t *Get(){return &_lock;}~Mutex(){pthread_mutex_destroy(&_lock);}private:pthread_mutex_t _lock;
};class LockGuard
{
public:LockGuard(Mutex*_mutex):_mutexp(_mutex){_mutexp->Lock();}~LockGuard(){_mutexp->Unlock();}
private:Mutex *_mutexp;
};

Logger.hpp

#pragma once#include <iostream>
#include <string>
#include <filesystem> // C++17 文件操作
#include <fstream>
#include <ctime>
#include <unistd.h>
#include <memory>
#include <sstream>
#include "Mutex.hpp"using namespace std;
// 规定出场景的日志等级
enum class LogLevel
{DEBUG,INFO,WARNING,ERROR,FATAL
};string Level2String(LogLevel level)
{switch (level){case LogLevel::DEBUG:return "DEBUG";case LogLevel::INFO:return "Info";case LogLevel::WARNING:return "Warning";case LogLevel::ERROR:return "Error";case LogLevel::FATAL:return "Fatal";default:return "Unknown";}
}string GetCurrentTime()
{// 获取时间戳time_t currtime = time(nullptr);// 把时间戳转换成为20XX-08-04 12:27:03struct tm currtm;localtime_r(&currtime, &currtm);// 转换成为字符串char timebuffer[64];snprintf(timebuffer, sizeof(timebuffer), "%4d-%02d-%02d %02d:%02d:%02d",currtm.tm_year + 1900,currtm.tm_mon + 1,currtm.tm_mday,currtm.tm_hour,currtm.tm_min,currtm.tm_sec);return timebuffer;
}
// 基类方法
class LogStrategy
{
public:virtual ~LogStrategy() = default;virtual void SyncLog(const string &logmessage) = 0;
};
// 显示器刷新
class ConsoleLogStrategy : public LogStrategy
{
public:~ConsoleLogStrategy(){}void SyncLog(const string &logmessage) override{LockGuard lockguard(&_lock);cout << logmessage << endl;}private:Mutex _lock;
};const std::string logdefaultdir = "log";
const static std::string logfilename = "test.log";// 文件刷新
class FileLogStrategy : public LogStrategy
{
public:FileLogStrategy(const string &dir = logdefaultdir,const string &filename = logfilename): _dir_path_name(dir), _filename(filename){LockGuard lockguard(&_lock);if (filesystem::exists(_dir_path_name)){return;}try{filesystem::create_directories(_dir_path_name);}catch (const filesystem::filesystem_error &e){std::cerr << e.what() << "\r\n";}}void SyncLog(const string &logmessage) override{LockGuard lockguard(&_lock);string target = _dir_path_name;target += "/";target += _filename;ofstream out(target.c_str(), ios::app);if (!out.is_open()){return;}out << logmessage << "\n"; // out.writeout.close();}~FileLogStrategy(){}private:string _dir_path_name;string _filename;Mutex _lock;
};// 网络刷新
// 1. 定制刷新策略
// 2. 构建完整的日志
class Logger
{
public:Logger(){}void EnableConsoleLogStrategy(){_strategy = make_unique<ConsoleLogStrategy>();}void EnableFileLogStrategy(){_strategy = make_unique<FileLogStrategy>();}// 形成一条完整日志的方式class LogMessage{public:LogMessage(LogLevel level, string &filename, int line, Logger &logger): _curr_time(GetCurrentTime()),_level(level),_pid(getpid()),_filename(filename),_line(line),_logger(logger){std::stringstream ss;ss << "[" << _curr_time << "] "<< "[" << Level2String(_level) << "] "<< "[" << _pid << "] "<< "[" << _filename << "] "<< "[" << _line << "]"<< " - ";_loginfo = ss.str();}template<class T>LogMessage& operator <<(const T&info){stringstream ss;ss<<info;_loginfo+=ss.str();return *this;}~LogMessage(){if(_logger._strategy){_logger._strategy->SyncLog(_loginfo);}}private:std::string _curr_time; // 日志时间LogLevel _level;        // 日志等级pid_t _pid;std::string _filename;int _line;string _loginfo; // 一条合并完成的,完整的日志信息Logger &_logger; // 提供刷新策略的具体做法};LogMessage operator()(LogLevel level, string filename, int line){return LogMessage(level, filename, line, *this);}~Logger(){}private:unique_ptr<LogStrategy> _strategy;
};Logger logger;
#define LOG(level) logger(level, __FILE__, __LINE__)
#define EnableConsoleLogStrategy() logger.EnableConsoleLogStrategy()
#define EnableFileLogStrategy() logger.EnableFileLogStrategy()

好,现在进入主体部分

为了方便期间,我打算将基本的网络操作函数诸如主机转网络等等函数进行封装,重新形成一个类,以便于我们后续的调用,这个没啥好说的,直接给出

InetAddr.hpp

#pragma once// 这个类,描述client socket信息的类
// 方便我们后续用它来管理客户端#include<iostream>
#include<string>
#include<cstring>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#define Conv(addr) ((struct sockaddr*)&addr)
using namespace std;
class InetAddr
{
public:InetAddr(const struct sockaddr_in &addr):_addr(addr){Net2Host();}InetAddr(uint16_t port,const string&ip="0.0.0.0"):_port(port),_ip(ip){Host2Net();}string Ip(){return _ip;}uint16_t Port(){return _port;}struct sockaddr* Addr(){return Conv(_addr);}socklen_t Length(){return sizeof(_addr);}string ToString(){return _ip+"-"+to_string(_port);}bool operator==(const InetAddr &addr){return (_ip==addr._ip&&_port==addr._port);}~InetAddr(){}
private:struct sockaddr_in _addr;    // 网络风格地址string _ip;         // 主机风格地址uint16_t _port;     //端口号void Net2Host(){_port=ntohs(_addr.sin_port);_ip=inet_ntoa(_addr.sin_addr);  //将 IPv4 地址转换成点分十进制字符串格式}void Host2Net(){memset(&_addr,0,sizeof(_addr));_addr.sin_family=AF_INET;_addr.sin_port=htons(_port);_addr.sin_addr.s_addr=inet_addr(_ip.c_str());}};

接下来我们在写一下服务端,其主要作用包括

1)初始化服务(包括套接字的创建以及将本地端口号和ip进行绑定)

2)服务器运行,这里要去我们在一个死循环中设置数组缓冲区,以便存放客户的消息,之后我们要recvfrom读取消息,之后将数组缓冲区的字符内容传给新的变量message,进而通过回调函数完成任务

3)服务器停止,就是_isrunning=false就好

ChatServer.hpp

#pragma once#include <iostream>
#include <string>
#include <cstdlib>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <strings.h>
#include <functional>
#include "Logger.hpp"
#include "InetAddr.hpp"
using namespace std;
using callback_t = function<void(int sockfd, string message, InetAddr addr)>;static const int gdefaultsockfd = -1;
class ChateServer
{
public:ChateServer(uint16_t port,callback_t cb):_port(port),_sockfd(gdefaultsockfd),_isrunning(false),_cb(cb){}void Init(){// 1. 创建socket fd_sockfd=socket(AF_INET,SOCK_DGRAM,0);if(_sockfd<0){LOG(LogLevel::FATAL)<<"create socket error";exit(1);}LOG(LogLevel::INFO) << "create socket success : " << _sockfd;InetAddr local(_port);   //本地端口号// 2.2 和socketfd进行bindint n=bind(_sockfd,local.Addr(),local.Length());if(n<0){LOG(LogLevel::FATAL)<<"bind socket error";exit(2);}LOG(LogLevel::INFO)<< "bind socket success : " << _sockfd; }void Start(){// 所有的服务器都是死循环_isrunning=true;while(_isrunning){char buffer[1024];buffer[0]=0;// 清空缓冲区struct sockaddr_in peer;socklen_t len=sizeof(peer);// 1. 读取数据ssize_t n=recvfrom(_sockfd,buffer,sizeof(buffer),0,(struct sockaddr*)&peer,&len);if(n>0){// 约定: 聊天消息buffer[n]=0;//得到对应的client是谁?InetAddr clientaddr(peer);LOG(LogLevel::DEBUG)<< "get a client info # " <<clientaddr.Ip()<<"-"<<clientaddr.Port()<<":"<<buffer;string message=buffer;// 回调!_cb(_sockfd,message,clientaddr);}}_isrunning=false;}void Stop(){_isrunning=false;}~ChateServer(){}private:int _sockfd;uint16_t _port;callback_t _cb;bool _isrunning;
};

之后我们要创造服务器实例化对象之前,还有实现一个类似于路由转发的功能,要不怎么给别人发消息呢?所以我们再定义一个Route.hpp文件

这个文件的作用很简单,就是发送数据,增减用户个数,要做到增加用户,就要根据传过来的addr判断这个用户是否已经存在,为此可以写一个函数进行判断,如果不存在就增加,这里可以用vector来管理,比较方便,之后删除用户,可以用设置一个关键字,这里就设为字符串"QUIT",之后找到其addr后erase就好,最后一部分就是发送,直接sendto函数就行了

Route.hpp

#pragma once#include <string>
#include <vector>
#include <iostream>
#include "InetAddr.hpp"
#include"Logger.hpp"using namespace std;// 实现一个路由协议类,以便我们进行数据交换
class Route
{
private:bool IsExists(const InetAddr &addr){//寻找这个addr是否存在for(auto &user:_online_user){if(user==addr){return true;}}return false;}void AddUser(const InetAddr&addr){if(!IsExists(addr))_online_user.push_back(addr);}void DeleteUser(const string &message,const InetAddr& addr){// 权宜之计, 自定义协议部分if(message=="QUIT"){auto iter=_online_user.begin();for(;iter!=_online_user.end();iter++){if(*iter==addr){_online_user.erase(iter);break;//防止迭代器失效}}}}void SendMessageeeToAll(int sockfd,string&message,InetAddr&addr){for(auto &user:_online_user){LOG(LogLevel::DEBUG) << "route [" << message << "] to : " << user.ToString();string info =addr.ToString();info +="#";info+=message;// XXXX-PORT# 你好sendto(sockfd,info.c_str(),info.size(),0,user.Addr(),user.Length());}}
public:Route(){}void RouteMessageToAll(int sockfd,string &message,InetAddr&addr){AddUser(addr);// 我们就一定或有在线用户列表SendMessageeeToAll(sockfd,message,addr);DeleteUser(message,addr);}~Route(){}private:// 临界资源// 方法1:加锁vector<InetAddr> _online_user; // 在线用户// 方法2:// // 锁 + 拷贝// std::vector<InetAddr> _send_list;// 方法3:// std::queue<std::string> _message_queue;
};

之后我们再把服务器的.cc文件写了吧

这个.cc文件要实现的作用是1.消息转发功能 2.为了支持多人聊天,所以我们要创建一个多线程对象 3.最后我们再创建一个服务器对象

首先是消息转发功能,可以用一个智能指针来写,并创建出线程池对象,最后用lambda表达式来处理服务器对象,这里详细说一下:代码如下

首先,我们make_unique就是在将ChatServer实例化,需要我们传递两个参数进去,这里把ChatServer的构造列表也放上来吧~

我们惊奇的发现第二个参数居然是回调函数,那就再把回调函数拿过来看一看吧~

我们发现回调函数里面又有三个参数,所以lambda表达式里面我们就要这三个参数列表,但是我们还是要访问外部变量呀,所以捕获列表也要传,那传什么呢?我们干的是多线程的聊天,那肯定是一个变量为指向转发服务的智能指针r,另一个就是线程池对象tp了,最后就是函数体部分,我们可以使用c++11中std::bind函数,将RouteMessageToAll这个函数进行参数绑定   绑定了r.get() sockfd message, addr 给RouteMessageToAll函数,至于为什么还要传一个r.get(),原因是bind函数还有一个this指针,r.get()是给这个传递它所管理的对象的原始指针的,之后访问usvr里的函数即可完成内容

ServerMain.cc

#include"ThreadPool.hpp"
#include"Route.hpp"
#include"ChatServer.hpp"
#include<memory>
#include<iostream>void Usage(string proc)
{cerr<<"Usage:"<<proc<<"localport"<<endl;
}using task_t =function<void()>;// ./udp_server serverport
int main(int argc,char* argv[])
{if(argc!=2){Usage(argv[0]);exit(0);}EnableConsoleLogStrategy();uint16_t port=stoi(argv[1]);// 1. 消息转发功能unique_ptr<Route> r=make_unique<Route>();// 2. 线程池对象auto tp=ThreadPool<task_t>::GetInstance();// 3. 服务器对象            unique_ptr<ChateServer> usvr=make_unique<ChateServer>(port,[&r,&tp](int sockfd,string message,InetAddr addr){task_t task=bind(&Route::RouteMessageToAll,r.get(),sockfd,message,addr);tp->Enqueue(task);});usvr->Init();usvr->Start();return 0;
}

下面写客户端

我们是要仿照聊天窗,所以既要能发出来消息,同时又要能接收到消息,为此我们可以设置如下几个函数,初始化函数,这个不多说了,就是创建套接字,之后,

接收消息的函数,跟server.hpp差不多,设置一个字符数组缓冲区,以及一个sockaddr_in的结构体,之后recvfrom函数,接收完了cout<<buffer就可以了。之后,

发送消息的函数,也是设置sockaddr_in server的结构,并对其内部成员进行初始化赋值,之后sendto向server写就好了,之后就是main函数,还是先检测传的参数个数是否正确,之后调用初始化函数,并通过函数方法进行thread类的默认构造,最后别忘了记得进程阻塞,否则主线程会直接退出的,你就会收获一个core dumped!!!

ChatClient.cc

#include<iostream>
#include<string>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <cstring>
#include <thread>using namespace std;int sockfd=-1;
uint16_t serverport;
string serverip;
void Usage(string proc)
{cerr<<"Usage:"<<proc<<" serverip serverport"<<endl;
}void InitClient(const string&serverip,uint16_t serberport)
{sockfd=socket(AF_INET,SOCK_DGRAM,0);if(sockfd<0){cout << "create socket errror" <<endl;}
}
void recever()
{while(true){struct sockaddr_in temp;socklen_t len=sizeof(temp);char buffer[1024];int m=recvfrom(sockfd,buffer,sizeof(buffer),0,(struct sockaddr*)&temp,&len);if(m>0){buffer[m]=0;cerr << buffer << endl;}}
}void sender()
{struct sockaddr_in server;memset(&server,0,sizeof(server));server.sin_family=AF_INET;server.sin_port=htons(serverport);server.sin_addr.s_addr=inet_addr(serverip.c_str());while(true){cout<<"Please Enter@ ";string line;getline(cin,line);//写sendto(sockfd,line.c_str(),line.size(),0,(struct sockaddr*)&server,sizeof(server));    }
}
// ./udp_client server_ip server_port
int main(int argc,char* argv[])
{if(argc!=3){Usage(argv[0]);exit(0);}serverip=argv[1];serverport=stoi(argv[2]);InitClient(serverip,serverport);thread trecv(recever);  //通过函数方法进行类的默认构造thread tsend(sender);trecv.join();tsend.join();  //不join主线程直接退出了return 0;
}

makefile文件

.PHONY:all
all:chatclient servermain
chatclient:ChatClient.ccg++ -o $@ $^ -std=c++17 -g
servermain:ServerMain.ccg++ -o $@ $^ -std=c++17
.PHONY:clean
clean:rm -f chatclient servermain

好现在我们运行一下:

大家可以自己感受一下,这样看起来不是很直观甚至有点乱()

http://www.dtcms.com/a/473880.html

相关文章:

  • [Power BI] CALCULATETABLE函数
  • 3494. 酿造药水需要的最少总时间
  • 沐风老师3DMAX科研绘图插件DNA生成器使用方法详解
  • 宁波做网站gs什么是网络营销的职能
  • AI编程工具(Cursor/Copilot/灵码/文心一言/Claude Code/Trae)AI编程辅助工具全方位比较
  • FastGPT入门实战
  • 数据结构笔试核心考点
  • 用python做购物网站万网搜官网
  • 创建qq网站如何做网站流量分析报表
  • Docker实战:从基础镜像到Nginx定制
  • 什么是NoSQL?
  • 北京网站建设公司代理备份整个网站
  • 宁夏做网站建设公司私人订制与定制
  • 在 Ubuntu 下开发鸿蒙应用:理解系统的最佳入口
  • RabbitMQ四种交换机详解
  • 几种最常见的病毒/恶意软件类型
  • PHP计算过去一定时间段内日期范围函数
  • 怎么看网站是什么程序做的产品推广的目的和意义
  • 摄像头软件参数调试详解与实战
  • DB-GPT:AI原生数据应用开发框架解析
  • 论文笔记(九十三)ManipulationNet: Benchmarking
  • AIX 服务器 CPU 长期 90%:从 nmon 画像到 DataStage 僵尸进程的定位与清理
  • 10_基础策略编程实现
  • 服装网站建设前景分析网站 不备案
  • 克隆网站模板网站正在建设中 模板
  • 【完整源码+数据集+部署教程】 葡萄病害检测系统源码和数据集:改进yolo11-CAA-HSFPN
  • deepseekmine2.2.0发布,本地知识库,秒级上传与检索文件,免费试用
  • JavaSE
  • 基于数据挖掘的银行贷款审批预测系统
  • 加大网站建设力度上海十大互联网公司