Socket封装之聊天程序

Posted

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Socket封装之聊天程序相关的知识,希望对你有一定的参考价值。

  今天,完成一下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封装之聊天程序的主要内容,如果未能解决你的问题,请参考以下文章

Socket封装之聊天程序

Socket封装之聊天程序

利用java的Socket实现一个简单hello/hi聊天程序

字节码学院之Java socket 控制台聊天室

一个简单的网络聊天程序实现

使用socket.io实现简单的聊天功能