1. 程式人生 > >Socket封裝之聊天程序(三)

Socket封裝之聊天程序(三)

pro eat iter fine else if 通過 ext 51cto string

  今天,完成一下epoll的封裝。

類圖

  首先,還是畫下類圖,初步設計一下。
技術分享圖片
  具體函數,我們下面詳解。

epoll封裝

EpollBase類

CEpollBase.h:

class CEpollBase
{
public:
    CEpollBase(int max_events);
    virtual ~CEpollBase();

    bool Create(int size);
    bool AddEvent(int fd,int events);
    bool ModEvent(int fd,int events);
    bool DelEvent(int fd,int events);
    bool Wait(int timeout = -1);    //監聽一次事件的發生 
    virtual void onEvent() = 0;
    void Start();
    void Stop();

protected:
    struct epoll_event *m_rlt_events;
    struct epoll_event m_event;
    int m_nEvent;
    int m_epfd;
private:

    int m_max_events;
    bool isRun;

};

  在之前,我們監聽事件的發生並進行處理,是放在while(1)循環裏的,但是,真正項目裏是不能存在死循環的,所以,這裏的Wait()就是用來監聽一次事件的發生,通過標誌isRun的值來決定是否一直監聽(即Start(),Stop())。純虛函數onEvent()用來處理發生的事件,意味著該類是不能實例化的,並且子類都要重寫。

CEpollBase.cpp:

CEpollBase::CEpollBase( int max_events )
{
    m_max_events = max_events;
    m_rlt_events = new struct epoll_event[max_events];
    memset(m_rlt_events,0,max_events * sizeof(struct epoll_event));
    isRun = true;
}

CEpollBase::~CEpollBase()
{
    delete []m_rlt_events;
    m_rlt_events = NULL;
}

bool CEpollBase::Create( int size )
{
    m_epfd = epoll_create(size);
    if (m_epfd == -1)
    {
        return false;
    }
    return true;
}

bool CEpollBase::AddEvent( int fd,int events )
{
    m_event.data.fd = fd;
    m_event.events = events ;   
    if ( epoll_ctl(m_epfd,EPOLL_CTL_ADD,fd,&m_event) == -1)
    {
        return false;
    }
    return true;
}

bool CEpollBase::ModEvent(int fd,int events)
{
    m_event.data.fd = fd;
    m_event.events = events ;   
    if ( epoll_ctl(m_epfd,EPOLL_CTL_MOD,fd,&m_event) == -1)
    {
        return false;
    }
    return true;
}

bool CEpollBase::DelEvent( int fd,int events )
{
    m_event.data.fd = fd;
    m_event.events = events ;   
    if ( epoll_ctl(m_epfd,EPOLL_CTL_DEL,fd,&m_event) == -1)
    {
        return false;
    }
    return true;
}

bool CEpollBase::Wait( int timeout /*= -1*/ )
{
    m_nEvent = epoll_wait(m_epfd,m_rlt_events,m_max_events,timeout);     //阻塞
    if(m_nEvent == -1)
    {
        perror("epoll_wait");
        return false;
    }
    else if(m_nEvent == 0)
    {
        printf("time out.");
        return true;
    }
    else
    {
        //有事件發生,立即處理
        onEvent();
    }
}   

void CEpollBase::Start()
{
    while(isRun)
    {
        Wait();
    }
}

void CEpollBase::Stop()
{
    isRun = false;
}

ServerEpoll類

  客戶端類應該是作為epoll類的成員的,所以,我們需要編寫一個通用的服務器處理事件的類,這個就是CServerEpoll類。
CServerEpoll.h:

class CServerEpoll:public CEpollBase
{
public:
    CServerEpoll(char *ip,unsigned short port,int max_events,int backlog,int size,int type);
    CServerEpoll(CAddress &addr,int max_events,int backlog,int size,int type);

    void onEvent();
    virtual void onChat(char acbuf[],int fd);

protected:
private:
    CTcpServer m_server;
    map<int,CTcpClient> m_clientMap;    //<fd,CTcpClient>存放客戶端的地址信息
};

CServerEpoll.cpp:

CServerEpoll::CServerEpoll( char *ip,unsigned short port,int max_events,int backlog,int size,int type = tcp_sock)
:CEpollBase(max_events)
{
    m_server.SetAddr(ip,port);
    m_server.Socket(type);
    m_server.Bind();
    m_server.Listen(backlog);

    Create(size);
    AddEvent(m_server.GetFd(),EPOLLIN);
}

CServerEpoll::CServerEpoll( CAddress &addr ,int max_events,int backlog,int size,int type = tcp_sock)
:CEpollBase(max_events)
{
    m_server.SetAddr(addr);
    m_server.Socket(type);
    m_server.Bind();
    m_server.Listen(backlog);

    Create(size);
    AddEvent(m_server.GetFd(),EPOLLIN);
}

void CServerEpoll::onEvent()
{
    int ret;
    char acbuf[1024] = "";
    CTcpClient conn;

    //有事件發生,立即處理
    for(int i = 0; i < m_nEvent; i++)
    {
        //如果是 sockfd
        if( m_rlt_events[i].data.fd == m_server.GetFd() )
        {
            conn = m_server.Accept();
            //添加到事件集合
            AddEvent(conn.GetFd(),EPOLLIN);
            printf("client ip:%s ,port:%u connect.\n",conn.GetAddr().GetIP(),conn.GetAddr().GetPort());
            //添加到客戶端鏈表當中
            m_clientMap.insert(pair<int,CTcpClient> (conn.GetFd(), conn));
        }
        else    //否則 connfd 
        {
            ret = read(m_rlt_events[i].data.fd,acbuf,100);
            if( ret == 0) //客戶端退出
            {
                close(m_rlt_events[i].data.fd);
                //從事件集合裏刪除
                DelEvent(m_rlt_events[i].data.fd, EPOLLIN);
                //從客戶端鏈表中刪除
                map<int,CTcpClient>::iterator it;
                for (it = m_clientMap.begin() ; it != m_clientMap.end(); it++)
                {
                    if (it->first == m_rlt_events[i].data.fd)
                    {
                        m_clientMap.erase(it->first);
                        break;
                    }
                }
                printf("client ip:%s ,port:%u disconnect.\n\n",it->second.GetAddr().GetIP(),it->second.GetAddr().GetPort());
            }
            else
            {   
                onChat(acbuf,m_rlt_events[i].data.fd);
            }

        }

    }
}

void CServerEpoll::onChat( char acbuf[],int fd )
{
    //做解包的處理
    write(fd,acbuf,1024);

}

  這裏的onData()可以是純虛函數,也可以是虛函數,看自己設計。這個函數是針對不同項目服務器不同處理數據的。在這個項目中,我們還需要一個類:ChatServerEpoll類,該項目中用來處理聊天事件的服務器程序,也就是在這個類中,我們重寫onData(),在該函數中處理:登錄、聊天、用戶列表等等的功能。

ChatServerEpoll類

CChatServerEpoll.h:

class CChatServerEpoll:public CServerEpoll
{
public:
    CChatServerEpoll(char *ip,unsigned short port,int max_events,int backlog,int size,int type);
    CChatServerEpoll(CAddress &addr,int max_events,int backlog,int size,int type);

    void onChat(char acbuf[],int fd);
protected:
private:
    map<string,int> m_userMap;  //<用戶名,文件描述符>
};

CChatServerEpoll.cpp:

CChatServerEpoll::CChatServerEpoll( char *ip,unsigned short port,int max_events,int backlog,int size,int type = tcp_sock)
:CServerEpoll(ip,port,max_events,backlog,size,type)
{

}

CChatServerEpoll::CChatServerEpoll( CAddress &addr,int max_events,int backlog,int size,int type = tcp_sock)
:CServerEpoll(addr,max_events,backlog,size,type)
{

}

void CChatServerEpoll::onChat( char acbuf[],int fd )
{
    PK_HEAD head = {0};     //包頭
    PK_LOGIN login ={0};    //登錄包
    PK_CHAT chat = {0};     //聊天包
    int reply;              //登錄應答包。 1-成功 0-失敗
    PK_USERLIST userlist = {0}; //用戶列表包

    map<string,int>::iterator it;

    //解包
    memset(&head,0,sizeof(head));
    memcpy(&head,acbuf,HEAD_SIZE);

    for(int i = 0; i < m_nEvent; i++)
    {
        switch(head.type)
        {
        case 1: //登錄或退出
            memset(&login,0,sizeof(login));
            memcpy(&login,acbuf + HEAD_SIZE,LOGIN_SIZE);

            if(login.isOnline)
            {
                //通過connfd區分不同客戶端
                reply = LOGIN_OK;
                memcpy(acbuf + HEAD_SIZE , &reply , 4);
                write(m_rlt_events[i].data.fd,acbuf,HEAD_SIZE + 4); //登錄成功應答包
                m_userMap.insert(pair<string,int>(login.name,m_rlt_events[i].data.fd));
                printf("client %s login.\n\n",login.name);
            }
            else
            {
                m_userMap.erase(login.name);
                printf("client %s exit.\n",login.name);
            }

            break;

        case 2:     //聊天
            memset(&chat,0,CHAT_SIZE);
            memcpy(&chat,acbuf + HEAD_SIZE,CHAT_SIZE);
            if(strcmp(chat.toName,"all") == 0)
            {
                //群聊
                for (it = m_userMap.begin(); it != m_userMap.end(); it++)
                {
                    //轉發消息
                    if (it->second != m_rlt_events[i].data.fd)
                    {
                        write(it->second, acbuf, HEAD_SIZE + CHAT_SIZE);
                    }

                }
            }
            else
            {
                //私聊
                if ( (it = m_userMap.find(chat.toName)) != m_userMap.end()) //找到了
                {
                    //轉發消息
                    write(it->second, acbuf, HEAD_SIZE + CHAT_SIZE);
                }
                else    //用戶不存在
                {
                    memset(&chat.msg,0,100);
                    strcpy(chat.msg,"the acccount is not exist.");
                    memcpy(acbuf + HEAD_SIZE, &chat, CHAT_SIZE);
                    write(m_rlt_events[i].data.fd, acbuf, HEAD_SIZE + CHAT_SIZE);
                }
            }
            break;

        case 3:
            memset(&userlist,0,USERLIST_SIZE);
            int j = 0;
            for (it = m_userMap.begin();j<MAX_USERS && it!= m_userMap.end(); it++,j++)
            {
                strncpy(userlist.userName[10*j],it->first.c_str(),10);
            }

            memcpy(acbuf + HEAD_SIZE, &userlist, USERLIST_SIZE);
            write(m_rlt_events[i].data.fd, acbuf, HEAD_SIZE + USERLIST_SIZE);

            break;

        }
    }

}

功能測試

  編寫好類之後,我們就可以修改之前的服務器端的代碼了,客戶端不變。
server.cpp:

#include "common.h"
#include "ChatServerEpoll.h"

#define MAX_LISTEN_SIZE 10
#define MAX_EPOLL_SIZE 1000
#define MAX_EVENTS 20

int main()
{   
    char ip[20] = "192.168.159.6";
    unsigned short port = 1234;
    SOCKET_TYPE type = tcp_sock;
    CChatServerEpoll ser_epoll(ip,port,MAX_EVENTS,MAX_LISTEN_SIZE,MAX_EPOLL_SIZE,type);
    ser_epoll.Start();

    return 0;
}

  是不是簡化很多了。
  到這裏為止,代碼已經基本完善。編譯的時候,可以分兩個文件夾。完整的代碼已經上傳。

Socket封裝之聊天程序(三)