C++网络:IO复用epoll服务器-附带网络聊天室代码实例

//!
//! C++网络:IO复用epoll服务器-附带网络聊天室代码实例
//!
//! ===== IO复用简介 =====
//! 众所周知,在LINUX中有一切皆文件的说法,将文件视为一种IO流,网络也被抽象为IO流,
//!     网络数据的操作与文件文件操作一致
//! 如socket等进行TCP连接时返回的套接字,对socket的读写都会堵塞线程等待返回,
//!     此时一个线程只能对一个socket进行监控读写,也就是只能对一个IO口进行操作,
//!     如果采用非堵塞的方式就可以对多个IO口进行监控,因为非堵塞下读写IO口会立刻返回,
//!     不管是否成功读写到数据
//! 如果将多个成功连接的socket存储起来,并采用非堵塞模式定时对IO口循环扫描,
//!     这样就可以知道那一个IO口有数据可读,这就是IO复用的基本原理
//! 目前LINUX下IO复用主流的监控方式有select/poll/epoll,其中select/poll为循环扫描所有IO口,
//!     不管该IO口是否有读写操作,在监控的IO口数量多时会拉低扫描速度,通常的监控上限建议是1024个IO口
//! epoll则采用事件触发的方式,即扫描IO口无读写事件时不会将IO可加入到处理队列,
//!     所以epoll不存在监控上限,所以在大数量连接上epoll可以完全取代select/poll
//! ===== IO复用简介 =====
//!
//!
//! ===== epoll简介 =====
//! 事件触发:
//!     epoll的使用特点就是,使用一个epool事件数组放入epoll_wait函数,当有IO口存在可读事件,
//!         epoll_wait函数就会返回,并将事件放入epool事件数组中,
//!         让使用者在循环的对epool事件数组中的IO口进行操作
//! 触发方式:
//!     epoll存在水平触发(LT)/边缘触发(ET),两种触发方式
//!         水平触发:只要epool事件数组中还存在IO口未读完的数据就会让epoll_wait函数返回并进入事件处理循环
//!         边缘触发:epool事件数组中每一次数据变化只会触发一次,不读取数据会一直留在IO口
//!     水平触发是epoll的默认触发模式,边沿触发则属于高速性能模式,两则的性能差距尚未可知,
//!         但边沿触发的读写处理往往更加复杂且容易引发错误
//! ===== epoll简介 =====
//!
//!
//! ===== 任务介绍 =====
//! 使用epoll写一个网络聊天室的服务器:
//!     场景描述:
//!         聊天室在命令行中运行,可以在登陆时输入昵称,有群聊和私聊的功能,
//!             并可以查看所有已经登陆的用户
//! ===== 任务介绍 =====
//!
//!
//! ===== 服务器实现流程 =====
//! 1.将监听socket设置为堵塞模式,保证所有新连接能成功接入服务器
//! 2.将新连接的读写socket设置为非堵塞模式,并在主线程完成读操作
//! 3.TCP的socket是字节流,在应用层会出现粘包问题,需要在应用层实现字节流的发送协议
//!         (该服务器的协议为:[内容长度:内容]模式的拆包协议,发送与接收接口为string)
//! 4.客户端与服务器的传输协议使用结构体转换进行传输
//!         (发送方:struct->string ,网络传输:通过网络发送string到接收方 ,接收方:string->struct)
//! 5.启动线程池,多线程处理拆包协议与回调任务处理函数
//! 6.拆包时在子线程完成,且并为每次拆包都可以拿到一整个包的数据,数据不足时需要存储数据包
//! 7.解析出数据包触发任务回调函数,需要提供channel管道,保证反馈到客户端的封包协议一致
//! 8.提供读写回调函数接口给使用者
//! ===== 服务器实现流程 =====
//!
//!
//! ===== 客户端实现流程 =====
//! 1.客户端保证使用的封包/拆包协议与服务器一致,发送的请求协议与服务器协议一致
//! 2.不需要线程池,但需要启动子线程对IO口进行堵塞读,IO口的堵塞写操作不能与读操作在同一个线程
//! 3.客户端需要提供一个命令处理操作函数,对不同的命令向服务器发出不同请求
//! ===== 客户端实现流程 =====
//!
//!
//! ===== 客户端使用方式 =====
//! 输入命令规则:(三种命令输入方式)
//!          [文字]:直接发送--公开模式群发
//!                  例子:     大家好,我是小黄
//!          ID:[文字]:用冒号分割,前面是登陆ID,后面是发送内容
//!                  例子:     3:你好阿!3号,我是小黄
//!          [show]:查看所有的已登陆用户
//!                  例子:     show
//!                      返回内容:
//!                          <<<< system:
//!                          1:小名
//!                          2:小黄
//!                          3:小虎
//!                           >>>>
//!
//! == 输出显示 ==
//!     <<<< system: [登陆] [ID:3] [昵称:小花] >>>>
//!     <<<< {public: 小黄 <1>} [大家好] >>>>
//!     <<<< {public: 小毛 <2>} [小黄你好阿] >>>>
//!     <<<< {private: 小毛 <2>} [小花,你看到了吗,是新朋友] >>>>
//!     <<<< system: [退出] [ID: 2] [name: 小毛] >>>>
//! == 输出显示 ==
//! ===== 客户端使用方式 =====
//!
//!
//! ===== 代码段展示顺序 =====
//! 1.ux_epoll.h
//! 2.ux_server:main.cpp
//! 3.ux_client:main.cpp
//! ===== 代码段展示顺序 =====
//!
//! 结束语:
//!     该服务器的ux_epoll.h文件实现了一个轻量线程池,简单的TCP拆包协议,
//!         发送数据的string接口,结构体转string等,可以完全满足小数量的简易服务器部署,
//!         且无需任务依赖,只要支持C++11即可携带ux_epoll.h头文件在LINUX环境下到处部署IO复用服务器
//!
//!
//! ux_epoll.h 
//!
#ifndef UX_EPOLL_H
#define UX_EPOLL_H

#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 

#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 

using namespace std;
using namespace std::placeholders;

/*
//此处代码在真正编译时需打开,但上传至CSDN时却导致下方所有代码被识别为注释,顾将其注释
//===== 日志宏 =====
#define vv(value) "["#value": "<] <<<< "<<__VA_ARGS__<<"\033[0m"<] <<<< "<<__VA_ARGS__<<"\033[0m"<] <<<< "<<__VA_ARGS__<<"\033[0m"<] <<<< "<<__VA_ARGS__<
string to_string(const T& t)
{ ostringstream os; os<
T from_string(const string& str)
{ T t; istringstream iss(str); iss>>t; return t; }
//== 字符串类型转换 ==


//===== 结构体转换string函数 =====
//结构体转string
//      语法解析:(char*)&ct ,由&ct获取结构体地址,在由该地址(char*)转为char*类型的指针
//      根据string构造函数,参数1:char*地址,参数2:长度,可从地址内存中复制二进制内容
template 
static string ct_s(T_ct ct)
{ return string((char*)&ct,sizeof(T_ct)); }

//string转结构体
//      语法解析:*(T_ct*)str.c_str() ,由str.c_str()从string类获取const char*指针,
//      由const char*指针转为T_ct*指针,再*(T_ct*)从指针中获取值,从而返回值
template 
static T_ct st_c(const string &str)
{ T_ct ct = *(T_ct*)str.c_str(); return ct; }
//===== 结构体转换string函数 =====


//===== 线程池 =====
class vpool_th
{
public:
    //创建线程池
    vpool_th(size_t number)
    {
        //准备一个循环函数--给线程池内的线程[等待任务/执行任务]
        auto create_func = [=](){
            while(true)
            {
                std::function task;
                {
                    std::unique_lock lock(_mutex);              //独占锁--获取队列任务
                    while (_tasks.empty() && _run) { _cond.wait(lock); }    //假唤醒--退出且队列为空
                    if(_run == false && _tasks.empty()) { return; }         //等待队列任务完成并退出任务
                    task = std::move(_tasks.front()); _tasks.pop();         //取任务
                }
                task(); //执行从队列获取的任务函数
            }
        };
        for(size_t i = 0;i lock(_mutex); _run = false; }    //这里锁的用处--add_work执行时不给释放
        _cond.notify_all();                                             //唤醒所有线程准备退出
        for(std::thread &worker: _workers) { worker.join(); }           //等待所有线程完成任务后释放
    }

    //加入任务函数
    //  解析: typename std::result_of::type -- 获取外部函数的返回值类型
    template
    auto add_work(Tfunc&& func, Targs&&... args)
        -> std::future::type>
    {
        using ret_type = typename std::result_of::type;                //任务函数的返回类型
        auto pack = std::bind(std::forward(func), std::forward(args)...); //任务函数打包
        auto task = std::make_shared>(pack);             //打包为连接future类
        auto res = task->get_future();                                                  //从future类获取函数返回值
        {
            std::unique_lock lock(_mutex);              //锁住并准备将任务插入队列
            std::function func = [task](){ (*task)(); };    //包装外部任务函数到function
            if(_run) { _tasks.emplace(func); }                      //插入function到队列
        }
        _cond.notify_one(); //通知一个线程去完成任务
        return res;
    }

private:
    bool _run = true;                           //运行标记
    std::vector _workers;          //线程容器
    std::mutex _mutex;                          //线程池锁
    std::queue> _tasks;   //任务队列
    std::condition_variable _cond;              //条件变量
};
//===== 线程池 =====


//===== 数据管道 =====
class channel
{
public:
    channel(int fd) : _fd(fd){}
    int get_fd() const { return _fd; }


    //发送string字符串,带锁
    bool send_msg(const string &msg)
    {
        unique_lock lock(_mutex);
        if(send_msg(_fd,msg,NULL) == false)
        { if(close_cb) {close_cb(_fd);} return false; }
        else return true;
    }
    function close_cb = nullptr;  //发送失败时触发回调--用于服务器

private:
    int _fd;            //连接套接字
    std::mutex _mutex;  //互斥锁--发送

    //指定发送N个字节的数据
    size_t writen(int sock,const void *buf,size_t len) const
    {
        size_t all = len;
        const char *pos = (const char *)buf;
        while (all > 0)
        {
            size_t res = write (sock,pos,all);
            if (res <= 0){ if (errno == EINTR){res = 0;} else{return -1;} }
            pos += res; all -= res;
        }
        return len;
    }

    //发送string字符串
    bool send_msg(int sock,const string &msg,size_t *all)
    {
        size_t len = msg.size();
        string buf;
        buf += string((char*)&len,sizeof(len));
        buf += msg;

        size_t ret = writen(sock,buf.c_str(),buf.size());
        if(all != nullptr) *all = ret;
        return ret != -1u;
    }
};
//===== 数据管道 =====


//===== epoll事件循环 =====
class ux_epoll
{
public:
    //初始化epoll,启动线程池与拆包线程,拆包线程会永久占用一个线程
    ux_epoll()
    {
        _pool = new vpool_th(10);                       //线程池初始化
        _pool->add_work(&ux_epoll::work_parse_th,this); //启动拆包函数线程
    }
    ~ux_epoll() { delete _pool; }

    //! 启动epoll服务器
    //! 返回值:
    //!      0:正常退出
    //!     -1:监听失败
    //!     -2:创建epoll失败 epoll_wait
    //!     -3:epoll_wait失败
    //!     -4:监听套接字加入epoll失败
    //!
    int open_epoll(int port)
    {
        int listensock = init_port(port);//获取套接字--监听
        if(listensock <= 0) { return -1; }

        vlogd("启动epoll成功:" vv(port));

        //创建一个epoll描述符
        //参数1:无效数,不过要求必须大于0
        _fd_epoll = epoll_create(1);
        if(_fd_epoll <= 0) { return -2; }

        if(epoll_add(listensock) != 0) { return -4; }//将监听套接字加入epoll
        struct epoll_event events[_size_event];//存放epool事件的数组

        //epoll进入监听状态
        while (true)
        {
            //等待监视的socket有事件发生 | 参数4设置超时时间,-1为无限制
            //参数1:epoll描述符,参数2:epoll事件数组,
            //      参数3:同时处理的fd数量,参数4:超时(-1则无视超时时间)
            int infds = epoll_wait(_fd_epoll, events, _size_event, -1);
            if (infds < 0) { return -3; }

            //遍历有事件发生的结构数组--事件处理循环
            for (int i = 0; i < infds; i++)
            {
                //EPOLLIN事件:(1)新连接 (2)有数据可读 (3)连接正常关闭
                //(1)新连接,响应套接字等于监听套接字listensock
                if ((events[i].data.fd == listensock)
                        && (events[i].events & EPOLLIN))
                {
                    //建立请求,接收客户端的套接字(accept返回之后双方套接字可通信)
                    struct sockaddr_in client;
                    socklen_t len = sizeof(client);
                    int clientsock = accept(listensock,(struct sockaddr *)&client, &len);
                    set_non_block(clientsock);

                    //新连接进入
                    if (clientsock != -1)
                    {
                        //把新的客户端添加到epoll中
                        if(epoll_add(clientsock) == 0)
                        {
                            //触发新连接回调
                            string str_ip = inet_ntoa(client.sin_addr);
                            if(sock_new) _pool->add_work
                                    (sock_new,make_shared(clientsock),str_ip);
                        }
                        else vlogw("新连接--加入epoll失败");
                    }
                    else vlogw("新连接--接入失败");
                }

                //事件触发:有数据可读,或者连接断开
                else if (events[i].events & EPOLLIN)
                {
                    char buf[_size_buf];
                    memset(buf,0,sizeof(buf));
                    size_t size = read(events[i].data.fd,&buf,sizeof(buf));

                    //发生了错误或socket被对方关闭
                    if(size <= 0)
                    {
                        //把已断开的客户端从epoll中删除
                        if(epoll_del(events[i].data.fd) == 0)
                        {

                            //触发关闭回调
                            int fd = events[i].data.fd;
                            if(sock_close) _pool->add_work
                                    (sock_close,make_shared(fd));
                        }
                        else vlogw("连接断开:epoll删除失败");
                    }
                    else
                    {
                        //子线程解析(需要将字符串复制一份而不是引用,因为地址会反复填充新内容)
                        int fd = events[i].data.fd;
                        add_work_parse(bind(&ux_epoll::parse_buf_th,this,fd,string(buf,size)));

                        //原地拆包解析(无需多线程,但是可能降低IO遍历的能力)
                        //parse_buf(events[i].data.fd,buf,size);
                    }

                }//<<事件触发
            }//<<遍历有事件发生的结构数组
        }//< &pch,const string &ip)>
            sock_new = nullptr;
    //关闭连接
    function &pch)>
            sock_close = nullptr;
    //读取数据
    function &pch,const string &msg)>
            sock_read = nullptr;
protected:
    int _size_event = 1024;     //单次IO扫描最大事件数
    int _size_buf = 4096;       //接收数据缓冲区大小
    int _fd_epoll = 0;          //epoll描述符
    mutex _mutex;               //互斥锁
    vpool_th *_pool;            //线程池
    mutex _mutex_parse;         //互斥锁--用于拆包解析函数
    condition_variable _cond;   //条件变量
    queue> _queue_task;    //解析任务队列
    map _map_save_read;         //存储fd拆包剩余数据

    //设置为非阻塞套接字
    int set_non_block(int fd)
    {
        //将O_NONBLOCK无堵塞选项设置到fd中
        int old_op = fcntl(fd, F_GETFL);
        int new_op = old_op | O_NONBLOCK;
        fcntl(fd,F_SETFL,new_op);
        return old_op;
    }

    //从epoll移除套接字
    int epoll_del(int fd)
    {
        //从epoll移除fd,根据结构体信息
        struct epoll_event ev;
        memset(&ev, 0, sizeof(struct epoll_event));
        ev.events = EPOLLIN;
        ev.data.fd = fd;
        int ret = epoll_ctl(_fd_epoll, EPOLL_CTL_DEL, fd, &ev);
        if(ret != 0) close(fd);
        return ret;
    }

    //套接字加入epoll
    int epoll_add(int fd)
    {
        //将临时结构体加入到epoll--水平触发
        struct epoll_event ev;
        memset(&ev,0,sizeof(struct epoll_event));
        ev.data.fd = fd;
        ev.events = EPOLLIN;
        return epoll_ctl(_fd_epoll,EPOLL_CTL_ADD,fd,&ev);
    }

    //启动子线程执行拆包
    void work_parse_th()
    {
        //拆包子线程,单线程执行,自行运行一个循环防止退出,并永久占用一个线程池中的子线程
        while (true)
        {
            unique_lock lock(_mutex_parse);     //此处单线程启动,无需锁,用于唤醒
            while(_queue_task.empty()){ _cond.wait(lock); } //假唤醒--退出且队列为空

            //取任务并执行
            function task = std::move(_queue_task.front());
            _queue_task.pop();
            task();
        }
    }

    //添加拆包内容
    void add_work_parse(function task)
    { _queue_task.push(task); _cond.notify_one(); }

    //epoll水平触发拆包函数--子线程
    void parse_buf_th(int fd,const string &buf)
    { parse_buf(fd,buf.c_str(),buf.size()); }

    //epoll水平触发拆包函数
    void parse_buf(int fd,const char *buf,size_t size)
    {
        size_t all_len = 0;
        string all_content;

        //查找是否有上次剩余部分
        auto it = _map_save_read.find(fd);
        if(it != _map_save_read.end())
        {
            all_len += it->second.size() + size;
            all_content += it->second + string(buf,size);
        }
        else
        {
            all_len = size;
            all_content = string(buf,size);
        }

        //循环解析,一次读取可能有多个任务包
        bool is_break = false;
        while (true)
        {
            //超过八个字节(判断是否能完整读出头部大小)
            if(all_len > sizeof(all_len))
            {
                //解析出ct_msg结构体的信息--长度
                size_t con_len = *(size_t*)string(all_content.begin(),
                                    all_content.begin()+sizeof(all_len)).c_str();

                //判断目前剩余量是否大于等于一个包的长度
                if((all_len - sizeof(all_len)) >= con_len)
                {
                    //解析的内容
                    string buf_content(all_content,sizeof(all_len),con_len);

                    //保存信息剩余量
                    all_len -= sizeof(all_len) + con_len;
                    all_content = string(all_content.begin() +
                                    sizeof(all_len) + con_len,all_content.end());
                    //触发读回调
                    auto pch = make_shared(fd);
                    pch->close_cb = bind(&ux_epoll::epoll_del,this,_1);
                    if(sock_read) _pool->add_work(sock_read,pch,buf_content);
                }
                else is_break = true;
            }
            else is_break = true;

            if(is_break)
            {
                //如果已经存在则插入剩余容器,不存在则新建,完成插入后退出循环
                if(it != _map_save_read.end())
                    { it->second = all_content; }
                else { _map_save_read.emplace(fd,all_content); }
                break;
            }
        }
    }

    //! 初始化监听端口,返回套接字
    //! 返回值:
    //!     -1:socket打开失败
    //!     -2:bind建立失败
    //!     sock:返回成功,建立的套接字
    //!
    int init_port(int port)
    {
        int sock = socket(AF_INET, SOCK_STREAM, 0); //设置TCP连接模式
        if (sock < 0) { return -1; }

        int opt = 1;
        unsigned int len = sizeof(opt);
        setsockopt(sock, SOL_SOCKET, SO_REUSEADDR, &opt, len); //打开复用
        setsockopt(sock, SOL_SOCKET, SO_KEEPALIVE, &opt, len); //打开心跳

        //设置网络连接模式
        struct sockaddr_in servaddr;
        servaddr.sin_family = AF_INET;				  //TCP协议族
        servaddr.sin_addr.s_addr = htonl(INADDR_ANY); //监听所有
        servaddr.sin_port = htons(port);			  //兼容端口

        //监听或绑定失败返回错误 | listen函数 参数2:正在连接的队列容量
        if (bind(sock, (struct sockaddr *)&servaddr,
                 sizeof(servaddr)) < 0 || listen(sock, _size_event) != 0)
        { close(sock); return -2; }

        return sock;
    }
};
//===== epoll事件循环 =====


//===== 客户端发送协议 =====
class ux_client
{
public:
    ux_client(){}
    ~ux_client() { if(pch != nullptr) { delete pch; pch = nullptr; } }

    int get_fd() { return _fd; }
    bool is_connect() { return _connect; }

    //建立连接
    int open_connect(const string &ip,int port)
    {
        _fd = init_connect(ip,port);
        if(_fd < 0) { if(sock_close){sock_close();} return _fd; }

        //建立发送管道
        pch = new channel(_fd);
        _connect = true;
        if(sock_new) sock_new();

        //建立读取子线程
        thread(&ux_client::read_string_th, this,_fd,sock_read,sock_close).detach();
        return _fd;
    }

    //关闭连接
    int close_connect()
    { _connect = false; return close(_fd); }

    //发送string字符串
    bool send_msg(const string &msg)
    {
        if(_connect)
        {
            bool ok = pch->send_msg(msg);
            if(ok == false)
            { _connect = false; if(sock_close) {sock_close();} }
            return ok;
        }
        else return false;
    }

    function sock_new = nullptr;                    //新连接
    function sock_close = nullptr;                  //关闭连接
    function sock_read = nullptr;  //读取数据

protected:
    int _fd = 0;
    bool _connect = false;
    channel *pch = nullptr;

    //! 网络连接初始化
    //! 返回值:
    //!     -1:socket打开失败
    //!     -2:IP转换失败
    //!     -3:connect连接失败
    //!     sock:返回成功,建立的套接字
    //!
    int init_connect(const string &ip,int port)
    {
        int sock = socket(AF_INET, SOCK_STREAM, 0); //设置TCP连接模式
        if (sock < 0) { return -1; }

        int opt = 1;
        unsigned int len = sizeof(opt);
        setsockopt(sock, SOL_SOCKET, SO_REUSEADDR, &opt, len); //打开复用
        setsockopt(sock, SOL_SOCKET, SO_KEEPALIVE, &opt, len); //打开心跳

        //设置网络连接模式
        struct sockaddr_in servaddr;
        servaddr.sin_family = AF_INET;				  // TCP协议族
        servaddr.sin_port = htons(port);			  //兼容端口

        //IP转换
        if(inet_pton(AF_INET,ip.c_str(), &servaddr.sin_addr) <=0 )
        { return -2; }

        //建立连接
        if(connect(sock,(struct sockaddr*)&servaddr,sizeof(servaddr)) < 0)
        { return -3; }

        return sock;
    }

    //读取反馈信息--线程启动
    void read_string_th(int fd,function read_cb,function close_cb)
    {
        size_t all_len = 0;
        string all_content;
        while(true)
        {
            char buf[1024];
            memset(buf,0,sizeof(buf));
            size_t size = read(fd,&buf,sizeof(buf));
            if(size <= 0) { if(close_cb){close_cb();} return; }

            //加入新内容(可能存在上一次的人剩余信息)
            all_len += size;
            all_content += string(buf,size);

            while(true)
            {
                //超过八个字节(判断是否能完整读出头部大小)
                if(all_len > sizeof(all_len))
                {
                    //解析出ct_msg结构体的信息--长度
                    size_t con_len = *(size_t*)string(all_content,0,sizeof(con_len)).c_str();

                    //判断目前剩余量是否大于等于一个包的长度
                    if((all_len - sizeof(all_len)) >= con_len)
                    {
                        //解析的内容
                        string buf_content(all_content,sizeof(all_len),con_len);
                        if(read_cb) read_cb(buf_content);//解析出完整包后触发回调

                        //存放剩余的内容
                        all_len -= sizeof(all_len) + con_len;
                        all_content = string(all_content.begin() +
                                        sizeof(all_len) + con_len,all_content.end());
                    }
                    else break;
                }
                else break;
            }
        }
    }
};
//===== 客户端发送协议 =====

#endif // UX_EPOLL_H
//!
//! ux_server:main.cpp
//! ===== 服务端代码 =====
//!
#include "ux_epoll.h"
#include 
#include 


//===== 消息处理结构体 =====
//== 请求类型 ==
enum en_transmit
{
    e_login,    //登陆
    e_swap,     //交换
    e_notify,   //通知
};

//== 消息权限 ==
enum en_msg
{
    e_public,   //群发
    e_private,  //私聊
};

//== 服务器解析聊天消息交互协议 ==
struct ct_msg_swap
{
    en_transmit et;     //用于判断是登陆还是发送信息
    en_msg em;          //用于判断是私聊还是群发
    size_t number_to;   //用于判断私聊时的目标ID
    size_t number_from; //用于存储发送者ID
    char name[64];      //发送者的昵称
    char buf[2048];     //存放发送的内容(登陆时:昵称,转发时:信息)
};

//== 登陆信息记录 ==
struct ct_login_id
{
    int fd;                     //存放fd,用于转发信息时建立连接
    string name;                //存放连接用户的昵称
    shared_ptr pch;    //用于发送信息的接口
};
//===== 消息处理结构体 =====


//== 主函数 ==
int main()
{
    mutex mutex_read;                   //读锁
    map map_login;  //登陆的用户索引,用于查找
    size_t count_login = 0;             //分配ID
    ux_epoll server_epoll;              //epoll服务器

    //===== 回调区 =====
    //新连接
    server_epoll.sock_new = [&](const shared_ptr &pch,const string &ip){
        vlogd("sock_new: " vv(pch->get_fd()) vv(ip));
    };

    //读数据--服务器接收到数据
    server_epoll.sock_read = [&](const shared_ptr &pch,const string &msg){
        unique_lock lock(mutex_read); //加锁是因为服务器读数据是多线程读取
        ct_msg_swap ct = st_c(msg); //字符串转结构体,无需引入json即可结构化数据

        //登陆的处理:分配ID,存储fd和昵称,反馈登陆ID,群发登陆用户信息
        if(ct.et == e_login)
        {
            //分配ID
            count_login++;

            //存储fd和昵称
            ct_login_id ct_id;
            ct_id.fd = pch->get_fd();
            ct_id.name = ct.buf;
            ct_id.pch = pch;
            map_login.insert(pair(count_login,ct_id));

            //反馈登陆ID
            ct.et = e_login;
            vlogd("count_login:" vv(to_string(count_login)));
            strncpy(ct.buf,to_string(count_login).c_str(),sizeof(ct.buf));
            pch->send_msg(ct_s(ct)); //结构体转string,并原路发送到客户端

            //群发登陆用户信息
            string content;
            ct.et = e_notify;
            content = "[登陆] [ID:" + to_string(count_login) +"] [昵称:"+ct_id.name+"]";
            strncpy(ct.buf,content.c_str(),sizeof(ct.buf));
            vlogd("群发:" vv(content) vv(ct_id.name) vv(count_login));
            for(const auto &a:map_login)
            { a.second.pch->send_msg(ct_s(ct)); }
        }

        //转发消息的处理:区分群发和私发,获取转发数据接口,转发数据
        else if(ct.et == e_swap)
        {
            if(ct.em == e_public)//群发
            {
                for(const auto &a:map_login)
                { a.second.pch->send_msg(ct_s(ct)); }
            }
            else if(ct.em == e_private)//私发
            {
                //查找并发送
                auto it = map_login.find(ct.number_to);
                if(it != map_login.end())
                    { it->second.pch->send_msg(ct_s(ct)); }
                else
                {
                    strncpy(ct.buf,"信息无法送达--请检查是否合理",sizeof(ct.buf));
                    ct.et = e_notify;
                    pch->send_msg(ct_s(ct));
                    vlogw("== number inexistence ==");
                }
            }
            else { vlogw("== en_msg inexistence =="); }
        }

        //所有用户信息请求:排队ID,通过ID获取昵称,原路反馈信息
        else if(ct.et == e_notify)
        {
            //排队ID
            vector vec;
            for(const auto &a:map_login)
            { vec.push_back(a.first); }
            std::sort(vec.begin(),vec.end());

            //通过ID获取昵称
            string content = "\n";
            for(const auto &a:vec)
            {
                //查找并发送
                auto it = map_login.find(a);
                if(it != map_login.end())
                { content += to_string(it->first) + ":" + it->second.name + "\n"; }
            }

            //原路反馈信息
            ct.et = e_notify;
            strncpy(ct.buf,content.c_str(),sizeof(ct.buf));
            pch->send_msg(ct_s(ct));
        }
        else { vlogw("== en_transmit inexistence =="); }
    };

    //关闭连接--客户端主动关闭
    server_epoll.sock_close = [&](const shared_ptr &pch){
        unique_lock lock(mutex_read);

        //群发通知信息有用户退出
        ct_msg_swap ct;
        memset(&ct,0,sizeof(ct));

        ct.et = e_notify;//通知类型
        for(auto a:map_login)
        {
            if(a.second.fd == pch->get_fd())
            {
                //记录退出信息
                string content = "[退出] [ID: "+to_string(a.first)+"] [昵称: "+a.second.name+"]";
                strncpy(ct.buf,content.c_str(),sizeof(ct.buf));
                map_login.erase(a.first);
                break;
            }
        }

        //群发通知
        for(const auto &a:map_login)
        { a.second.pch->send_msg(ct_s(ct));  }

        vlogd("channel 断开的fd :" vv(pch->get_fd()));
    };
    //===== 回调区 =====

    int ret = server_epoll.open_epoll(5005);    //启动服务器
    vlogd("open_epoll ret: " vv(ret));          //服务器退出

    printf("\n===== end =====\n");
    return 0;
}
//!
//! ux_client:main.cpp
//! ===== 客户端代码 =====
//!
#include "../ux_server/ux_epoll.h"
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 

using namespace std;


//===== stmv =====
//功能:字符串切割,按分隔符将字符串切割到数组
//算法:利用vector生成与字符串一样长的标记位
//      切割算法扫描到切割符时将vector对应标记位置1(切割符占领位)
//      然后将连续0段加入结果数组
//用法示例:
//      [1]
//      string a = "11--22--33";
//      string b = "11--22++33";
//      string c = "11 22 33 44++55--66";
//      vector vec = vts::stmv(a)("--");
//      [ret = 11,22,33]
//      vector vec1 = vts::stmv(b)("--");
//      [ret = 11,22++33]
//      vector vec2 = vts::stmv(c)(" ","++","--");
//      [ret = 11,22,33,44,55,66]
//
struct stmv
{
    string v_str;
    vector vec_flg;
    vector vec_bit;

    stmv(const string &str) : v_str(str) { vec_bit.resize(str.size(),false); }

    template
    vector operator()(const Tarr &...arg) { return push_flg(arg...); }

    //获取切割符
    template vector push_flg()
    { return split_value(v_str,vec_flg); }
    template
    vector push_flg(const string &flg,Tarr ...arg)
    { vec_flg.push_back(flg); return push_flg(arg...); };

    //根据标记切割字符串
    vector split_value(const string &in_str,const vector &in_flg)
    {
        vector vec;

        //标记数循环
        for(size_t iflg=0;iflg0 && (vec_bit[i-1] == true)) str.clear();
                str+=in_str[i];
            }
            else if(i>0 && (vec_bit[i-1] == false)) vec.push_back(str);
        }

        //末尾无状态转跳时加入结果
        if(vec_bit[vec_bit.size()-1] == false)
        { vec.push_back(str); }

        return vec;
    }
};
//===== stmv =====


//===== 消息处理结构体 =====
//== 请求类型 ==
enum en_transmit
{
    e_login,    //登陆
    e_swap,     //交换
    e_notify,   //通知
};

//== 消息权限 ==
enum en_msg
{
    e_public,   //群发
    e_private,  //私聊
};

//== 服务器解析聊天消息交互协议 ==
struct ct_msg_swap
{
    en_transmit et;     //用于判断是登陆还是发送信息
    en_msg em;          //用于判断是私聊还是群发
    size_t number_to;   //用于判断私聊时的目标ID
    size_t number_from; //用于存储发送者ID
    char name[64];      //发送者的昵称
    char buf[2048];     //存放发送的内容(登陆时:昵称,转发时:信息)
};
//===== 消息处理结构体 =====


//===== 发送协议 =====
//size_t readn(int fd, void *buf, size_t len)
//{
//    size_t all = len;
//    char *pos = (char *)buf;
//    while (all > 0)
//    {
//        size_t size = read(fd,pos,all);
//        if (size == -1u)
//        {
//            if (errno == EINTR) size = 0;
//            else return -1;
//        }
//        else if (size == 0) return 0;
//        pos += size;
//        all -= size;
//    }
//    return len - all;
//}

//size_t writen(int sock,const void *buf,size_t len)
//{
//    size_t all = len;
//    const char *pos = (const char *)buf;
//    while (all > 0)
//    {
//        size_t res = write (sock,pos,all);
//        if (res <= 0)
//        {
//            if (errno == EINTR) res = 0;
//            else return -1;
//        }
//        pos += res;
//        all -= res;
//    }
//    return len;
//}
//===== 发送协议 =====


//解析命令内容
string pares_send_cmd(const string &cmd,const string &name,size_t *number)
{
    ct_msg_swap ct_swap;
    memset(&ct_swap,0,sizeof(ct_swap));

    //查询所有用户信息
    if(cmd == "show") { ct_swap.et = e_notify; }
    else
    {
        string content = cmd;
        ct_swap.et = e_swap;
        ct_swap.em = e_public;

        //私聊,使用stmv按照分割副切割出容器,如果容器存在数据则标识为私聊
        vector vec = stmv(cmd)(":");
        if(vec.size() >= 2)
        {
            ct_swap.em = e_private;
            ct_swap.number_to = from_string(vec[0]);
            content = vec[1];
        }

        ct_swap.number_from = *number;
        strncpy(ct_swap.name,name.c_str(),sizeof(ct_swap.buf));
        strncpy(ct_swap.buf,content.c_str(),sizeof(ct_swap.buf));
    }
    return ct_s(ct_swap);
}


int main()
{
    bool is_run = true;
    size_t number = -1;
    ux_client client;

    string name;
    cout<<"please input your name: "<>name;

    client.sock_new = [&](){
        //连接成功并登陆
        ct_msg_swap ct_login;
        memset(&ct_login,0,sizeof(ct_login));
        ct_login.et = e_login;
        strncpy(ct_login.buf,name.c_str(),sizeof(ct_login.buf));
        client.send_msg(ct_s(ct_login)); //发送数据
    };

    client.sock_close = [&](){
        is_run = false;
        cout<<"== sock_close =="<(msg);

        //登陆反馈信息
        if(ct.et == e_login)
        {
            number = from_string(ct.buf);
            if(number != -1u)
            { cout<<"<<<< login ID:"<>>>"<} [%s] >>>>",
                     stc_ms.c_str(),ct.name,ct.number_from,ct.buf);
            cout<>>>",ct.buf);
            cout<>str;
        if(str == "exit") { break; }

        str = pares_send_cmd(str,name,&number); //解析输入命令
        if(client.send_msg(str) == false)
        { cout<<"== send err =="<

你可能感兴趣的:(c++,服务器,网络)