Socket封装之聊天程序(一)

﹥>﹥吖頭↗ 提交于 2019-11-26 04:27:39

  之前使用IPC编写过聊天程序,但是这样仅能在同一台计算机上进行聊天;要使得在不同的计算机(不同的IP+端口)上也能进行通信,就需要用到socket编程。前面说到,要处理多客户端的响应问题,需要I/O复用,即调用select或者epoll。通常我们使用epoll函数,以下例子也是。
  接下来,我们需要封装一个地址类。为什么要封装这样一个类呢?
  在前面的练习中,我们可以看到,在socket规程中,需要反复用到struct sockaddr_in 这个地址,包括以下的bind绑定过程也是经常出现的,而且这些方法其实都是系统函数,我们并不希望每次都直接使用,不进繁琐难记,而且可读性差。所以,我们需要封装一个地址类CAdress,将这些步骤在函数内部实现。
Socket封装之聊天程序(一)
  


  
编写CAdress类
  
Socket封装之聊天程序(一)
CAdress.h:

#ifndef _ADRESS_H_
#define _ADRESS_H_
#include <netinet/in.h> //sockaddr_in
#include <arpa/inet.h>

class CAdress
{
public:
    CAdress(char *ip,unsigned short port);
    CAdress();
    ~CAdress();

    void setIP(char *ip);
    void setPort(unsigned short port);

    char *getIP();
    unsigned short getPort();
    struct sockaddr *getAddr();
    socklen_t getAddrLen();
    socklen_t *getAddrLenPtr();

private:
    struct sockaddr_in m_addr;
    socklen_t m_addrLen;
};
#endif

CAdress.cpp:

#include "adress.h"

CAdress::CAdress( char *ip,unsigned short port )
{
    m_addr.sin_family = AF_INET;
    m_addr.sin_port = htons(port);
    m_addr.sin_addr.s_addr = inet_addr(ip);
    m_addrLen = sizeof(struct sockaddr_in);
}

CAdress::CAdress()
{
    m_addrLen = sizeof(struct sockaddr_in);
}

CAdress::~CAdress()
{

}

void CAdress::setIP( char *ip )
{
    m_addr.sin_addr.s_addr = inet_addr(ip);
}

void CAdress::setPort( unsigned short port )
{
    m_addr.sin_port = htons(port);
}

char * CAdress::getIP()
{
    return inet_ntoa(m_addr.sin_addr);
}

unsigned short CAdress::getPort()
{
    return ntohs(m_addr.sin_port);
}

struct sockaddr * CAdress::getAddr()
{
    return (struct sockaddr *)&m_addr;
}

socklen_t CAdress::getAddrLen()
{
    return m_addrLen ;
}

socklen_t *CAdress::getAddrLenPtr()
{
    return &m_addrLen ;
}

编写主程序
common.h:

#include <string.h>
#include <signal.h>
#include <sys/epoll.h>
#include <unistd.h>
#include <list>
#include <stdlib.h>
#include <map>
#include <string>
#include <signal.h>
#include <iostream>

#include "adress.h"
#include <netinet/in.h> //sockaddr_in
#include <arpa/inet.h>

using namespace std;

//包头
typedef struct pack_head
{
    char type;  //1-登录包 2-聊天包
    int size;   //包体的长度
}PK_HEAD;

//登录包
typedef struct pack_login
{
    char name[10];
    char pwd[8];
}PK_LOGIN;

//消息包
typedef struct pack_chat
{
    char fromName[10];
    char toName[10];
    char msg[100];
}PK_CHAT;

#define HEAD_SIZE   sizeof(PK_HEAD)
#define LOGIN_SIZE  sizeof(PK_LOGIN)
#define CHAT_SIZE   sizeof(PK_CHAT)

#define LOGIN_OK    1
#define LOGIN_FAIL  0

#endif

server.cpp:

#include "common.h"

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

int main()
{   
    int sockfd;
    int connfd;

    int reuse = 0;
    int epfd; 
    int nEvent = 0;
    struct epoll_event event = {0};
    struct epoll_event rtlEvents[MAX_EVENTS] = {0};
    char acbuf[100] = "";
    int ret;

    PK_HEAD head = {0};     //包头
    PK_LOGIN login ={0};    //登录包
    PK_CHAT chat = {0};     //聊天包
    map<string,int> userMap;    //<文件描述符,用户名>
    map<string,int>::iterator it;
    int reply;              //登录应答包。 1-成功 0-失败

    //1.socket()
    sockfd = socket(PF_INET,SOCK_STREAM,0);
    if(sockfd == -1)
    {
        perror("socket");
        return -1;
    }

    //2.bind()
    char ip[20] = "192.168.159.6";
    CAdress addr(ip,1234);
    ret = bind(sockfd,addr.getAddr(),addr.getAddrLen());
    if(ret == -1)
    {
        perror("bind");
        return -1;
    }

    //3.listen()
    ret = listen(sockfd,MAX_LISTEN_SIZE);
    if(ret == -1)
    {
        perror("listen");
        return -1;
    }

    //4.epoll初始化
    epfd = epoll_create(MAX_EPOLL_SIZE);    //创建
    event.data.fd = sockfd;
    event.events = EPOLLIN ;    
    epoll_ctl(epfd,EPOLL_CTL_ADD,sockfd,&event);    //添加sockfd

    CAdress connAddr;
    //5.通信
    while(1)
    {
        nEvent = epoll_wait(epfd,rtlEvents,MAX_EVENTS,-1);   //阻塞
        if(nEvent == -1)
        {
            perror("epoll_wait");
            return -1;
        }
        else if(nEvent == 0)
        {
            printf("time out.");
        }
        else
        {
            //有事件发生,立即处理
            for(int i = 0; i < nEvent; i++)
            {
                //如果是 sockfd
                if( rtlEvents[i].data.fd == sockfd )
                {
                    connfd = accept(sockfd,connAddr.getAddr(),connAddr.getAddrLenPtr());
                    //添加到事件集合
                    event.data.fd = connfd;
                    event.events = EPOLLIN;  
                    epoll_ctl(epfd,EPOLL_CTL_ADD,connfd,&event);
                    printf("client ip:%s ,port:%u connect.\n",connAddr.getIP(),connAddr.getPort());
                }
                else    //否则 connfd 
                {
                    ret = read(rtlEvents[i].data.fd,acbuf,100);
                    if( ret == 0) //客户端退出
                    {
                        close(rtlEvents[i].data.fd);
                        //从集合里删除
                        epoll_ctl(epfd,EPOLL_CTL_DEL,rtlEvents[i].data.fd,rtlEvents);
                        //从用户列表删除
                        string username;
                        for (it = userMap.begin(); it != userMap.end(); it++)
                        {
                            if (it->second == rtlEvents[i].data.fd)
                            {
                                username = it->first;
                                userMap.erase(it);
                                break;
                            }
                        }
                        printf("client ip:%s ,port:%u disconnect.\n",connAddr.getIP(),connAddr.getPort());
                        cout<<"client "<<username<<" exit."<<endl;
                    }
                    else
                    {   
                        //解包
                        memset(&head,0,sizeof(head));
                        memcpy(&head,acbuf,HEAD_SIZE);

                        switch(head.type)
                        {
                        case 1:
                            memset(&login,0,sizeof(login));
                            memcpy(&login,acbuf + HEAD_SIZE,LOGIN_SIZE);
                            //通过connfd,区分不同客户端
                            //如果重复登录,失败,让前一个账号下线 ; 如果登录成功,服务器要发送一个应答包给客户端。
                            if ( (it = userMap.find(login.name)) != userMap.end())
                            {
                                reply = LOGIN_FAIL;
                                memset(acbuf,0,100);
                                head.size = 4;
                                memcpy(acbuf,&head,HEAD_SIZE);
                                memcpy(acbuf + HEAD_SIZE , &reply , 4);
                                write(it->second,acbuf,HEAD_SIZE + 4);  //登录失败应答包
                                printf("client %s relogin.\n",login.name);
                            }
                            else
                            {
                                printf("client %s login.\n",login.name);
                            }
                            reply = LOGIN_OK;
                            memcpy(acbuf + HEAD_SIZE , &reply , 4);
                            write(rtlEvents[i].data.fd,acbuf,HEAD_SIZE + 4);    //登录成功应答包
                            userMap.insert(pair<string,int>(login.name,rtlEvents[i].data.fd));

                            break;

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

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

                            break;

                        }

                    }
                }

            }
        }

    }

    return 0;
}

运行结果
Socket封装之聊天程序(一)
  至此,我们完成了简单的聊天功能,接下来我们将进一步学习,如何封装socket,并逐步完善功能。

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!