TCP_transfer/README.md
2024-09-12 18:35:50 +08:00

20 KiB
Raw Permalink Blame History

面试专用贴

网络编程TCPdemo

tcpgetfiles tcppullfiles分别为上传和下载模块

demo中使用的框架类函数定义和实现在EL文件夹中

socket通讯类定义

// socket通讯的客户端类
class ctcpclient
{
private:
    int  m_connfd;    // 客户端的socket.
    string m_ip;        // 服务端的ip地址。
    int  m_port;        // 服务端通讯的端口。
public:
    ctcpclient(): m_connfd(-1),m_port(0) { }  // 构造函数。

    // 向服务端发起连接请求。
    // ip服务端的ip地址。
    // port服务端通讯的端口。
    // 返回值true-成功false-失败。
    bool connect(const string &ip,const int port);

    // 接收对端发送过来的数据。
    // buffer存放接收数据缓冲区。
    // ibuflen: 打算接收数据的大小。
    // itimeout等待数据的超时时间-1-不等待0-无限等待;>0-等待的秒数。
    // 返回值true-成功false-失败失败有两种情况1等待超时2socket连接已不可用。
    bool read(string &buffer,const int itimeout=0);                           // 接收文本数据。
    bool read(void *buffer,const int ibuflen,const int itimeout=0);   // 接收二进制数据。

    // 向对端发送数据。
    // buffer待发送数据缓冲区。
    // ibuflen待发送数据的大小。
    // 返回值true-成功false-失败如果失败表示socket连接已不可用。
    bool write(const string &buffer);                          // 发送文本数据。
    bool write(const void *buffer,const int ibuflen);   // 发送二进制数据。

    // 断开与服务端的连接
    void close();

    ~ctcpclient();  // 析构函数自动关闭socket释放资源。
};

// socket通讯的服务端类
class ctcpserver
{
private:
    int m_socklen;                                // 结构体struct sockaddr_in的大小。
    struct sockaddr_in m_clientaddr;   // 客户端的地址信息。
    struct sockaddr_in m_servaddr;     // 服务端的地址信息。
    int  m_listenfd;                               // 服务端用于监听的socket。
    int  m_connfd;                                // 客户端连接上来的socket。
public:
    ctcpserver():m_listenfd(-1),m_connfd(-1) {}  // 构造函数。

    // 服务端初始化。
    // port指定服务端用于监听的端口。
    // 返回值true-成功false-失败一般情况下只要port设置正确没有被占用初始化都会成功。
    bool initserver(const unsigned int port,const int backlog=5); 

    // 从已连接队列中获取一个客户端连接,如果已连接队列为空,将阻塞等待。
    // 返回值true-成功的获取了一个客户端连接false-失败如果accept失败可以重新accept。
    bool accept();

    // 获取客户端的ip地址。
    // 返回值客户端的ip地址如"192.168.1.100"。
    char *getip();

    // 接收对端发送过来的数据。
    // buffer存放接收数据的缓冲区。
    // ibuflen: 打算接收数据的大小。
    // itimeout等待数据的超时时间-1-不等待0-无限等待;>0-等待的秒数。
    // 返回值true-成功false-失败失败有两种情况1等待超时2socket连接已不可用。
    bool read(string &buffer,const int itimeout=0);                           // 接收文本数据。
    bool read(void *buffer,const int ibuflen,const int itimeout=0);   // 接收二进制数据。

    // 向对端发送数据。
    // buffer待发送数据缓冲区。
    // ibuflen待发送数据的大小。
    // 返回值true-成功false-失败如果失败表示socket连接已不可用。
    bool write(const string &buffer);                          // 发送文本数据。
    bool write(const void *buffer,const int ibuflen);   // 发送二进制数据。

    // 关闭监听的socket即m_listenfd常用于多进程服务程序的子进程代码中。
    void closelisten();

    // 关闭客户端的socket即m_connfd常用于多进程服务程序的父进程代码中。
    void closeclient();

    ~ctcpserver();  // 析构函数自动关闭socket释放资源。
};

socket通讯类实现

bool ctcpclient::connect(const string &ip,const int port)
{
    // 如果已连接到服务端,则断开,这种处理方法没有特别的原因,不要纠结。
    if (m_connfd!=-1) { ::close(m_connfd); m_connfd=-1; }
 
    // 忽略SIGPIPE信号防止程序异常退出。
    // 如果send到一个disconnected socket上内核就会发出SIGPIPE信号。这个信号
    // 的缺省处理方法是终止进程,大多数时候这都不是我们期望的。我们重新定义这
    // 个信号的处理方法,大多数情况是直接屏蔽它。
    signal(SIGPIPE,SIG_IGN);   

    m_ip=ip;
    m_port=port;

    struct hostent* h;
    struct sockaddr_in servaddr;

    if ( (m_connfd = socket(AF_INET,SOCK_STREAM,0) ) < 0) return false;

    if ( !(h = gethostbyname(m_ip.c_str())) )
    {
        ::close(m_connfd);  m_connfd=-1; return false;
    }

    memset(&servaddr,0,sizeof(servaddr));
    servaddr.sin_family = AF_INET;
    servaddr.sin_port = htons(m_port);  // 指定服务端的通讯端口
    memcpy(&servaddr.sin_addr,h->h_addr,h->h_length);

    if (::connect(m_connfd, (struct sockaddr *)&servaddr,sizeof(servaddr)) != 0)
    {
        ::close(m_connfd);  m_connfd=-1; return false;
    }

    return true;
}

void ctcpclient::close()
{
    if (m_connfd >= 0) ::close(m_connfd); 

    m_connfd=-1;
    m_port=0;
}

ctcpclient::~ctcpclient()
{
    close();
}

bool ctcpserver::initserver(const unsigned int port,const int backlog)
{
    // 如果服务端的socket>0关掉它这种处理方法没有特别的原因不要纠结。
    if (m_listenfd > 0) { ::close(m_listenfd); m_listenfd=-1; }

    if ( (m_listenfd = socket(AF_INET,SOCK_STREAM,0))<=0) return false;

    // 忽略SIGPIPE信号防止程序异常退出。
    // 如果往已关闭的socket继续写数据会产生SIGPIPE信号它的缺省行为是终止程序所以要忽略它。
    signal(SIGPIPE,SIG_IGN);   

    // 打开SO_REUSEADDR选项当服务端连接处于TIME_WAIT状态时可以再次启动服务器
    // 否则bind()可能会不成功Address already in use。
    int opt = 1; 
    setsockopt(m_listenfd,SOL_SOCKET,SO_REUSEADDR,&opt,sizeof(opt));    

    memset(&m_servaddr,0,sizeof(m_servaddr));
    m_servaddr.sin_family = AF_INET;
    m_servaddr.sin_addr.s_addr = htonl(INADDR_ANY);   // 任意ip地址。
    m_servaddr.sin_port = htons(port);
    if (bind(m_listenfd,(struct sockaddr *)&m_servaddr,sizeof(m_servaddr)) != 0 )
    {
        closelisten(); return false;
    }

    if (listen(m_listenfd,backlog) != 0 )
    {
        closelisten(); return false;
    }

    return true;
}

bool ctcpserver::accept()
{
    if (m_listenfd==-1) return false;

    int m_socklen = sizeof(struct sockaddr_in);
    if ((m_connfd=::accept(m_listenfd,(struct sockaddr *)&m_clientaddr,(socklen_t*)&m_socklen)) < 0)
        return false;

    return true;
}

char *ctcpserver::getip()
{
    return(inet_ntoa(m_clientaddr.sin_addr));
}

bool ctcpserver::read(void *buffer,const int ibuflen,const int itimeout)   // 接收二进制数据。
{
    if (m_connfd==-1) return false;

    return(tcpread(m_connfd,buffer,ibuflen,itimeout));
}

bool ctcpserver::read(string &buffer,const int itimeout)  // 接收文本数据。
{
    if (m_connfd==-1) return false;

    return(tcpread(m_connfd,buffer,itimeout));
}

bool ctcpclient::read(void *buffer,const int ibuflen,const int itimeout)   // 接收二进制数据。
{
    if (m_connfd==-1) return false;

    return(tcpread(m_connfd,buffer,ibuflen,itimeout));
}

bool ctcpclient::read(string &buffer,const int itimeout)  // 接收文本数据。
{
    if (m_connfd==-1) return false;

    return(tcpread(m_connfd,buffer,itimeout));
}

bool ctcpserver::write(const void *buffer,const int ibuflen)  // 发送二进制数据。
{
    if (m_connfd==-1) return false;

    return(tcpwrite(m_connfd,(char*)buffer,ibuflen));
}

bool ctcpserver::write(const string &buffer)
{
    if (m_connfd==-1) return false;

    return(tcpwrite(m_connfd,buffer));
}

bool ctcpclient::write(const void *buffer,const int ibuflen)
{
    if (m_connfd==-1) return false;

    return(tcpwrite(m_connfd,(char*)buffer,ibuflen));
}

bool ctcpclient::write(const string &buffer)
{
    if (m_connfd==-1) return false;

    return(tcpwrite(m_connfd,buffer));
}

void ctcpserver::closelisten()
{
    if (m_listenfd >= 0)
    {
        ::close(m_listenfd); m_listenfd=-1;
    }
}

void ctcpserver::closeclient()
{
    if (m_connfd >= 0)
    {
        ::close(m_connfd); m_connfd=-1; 
    }
}

ctcpserver::~ctcpserver()
{
    closelisten(); closeclient();
}

bool tcpread(const int sockfd,void *buffer,const int ibuflen,const int itimeout)    // 接收二进制数据。
{
    if (sockfd==-1) return false;

    // 如果itimeout>0表示需要等待itimeout秒如果itimeout秒后还没有数据到达返回false。
    if (itimeout>0)
    {
        struct pollfd fds;
        fds.fd=sockfd;
        fds.events=POLLIN;
        if ( poll(&fds,1,itimeout*1000) <= 0 ) return false;
    }

    // 如果itimeout==-1表示不等待立即判断socket的缓冲区中是否有数据如果没有返回false。
    if (itimeout==-1)
    {
        struct pollfd fds;
        fds.fd=sockfd;
        fds.events=POLLIN;
        if ( poll(&fds,1,0) <= 0 ) return false;
    }

    // 读取报文内容。
    if (readn(sockfd,(char*)buffer,ibuflen) == false) return false;

    return true;
}

日志类定义

// 日志文件
class clogfile
{
    ofstream fout;                       // 日志文件对象。
    string   m_filename;              // 日志文件名,建议采用绝对路径。
    ios::openmode m_mode;      // 日志文件的打开模式。
    bool     m_backup;                // 是否自动切换日志。
    int        m_maxsize;               // 当日志文件的大小超过本参数时,自动切换日志。
    bool     m_enbuffer;              // 是否启用文件缓冲区。
    spinlock_mutex m_splock;    // 自旋锁,用于多线程程序中给写日志的操作加锁。

public:
    // 构造函数日志文件的大小缺省100M。
    clogfile(int maxsize=100):m_maxsize(maxsize){}

    // 打开日志文件。
    // filename日志文件名建议采用绝对路径如果文件名中的目录不存在就先创建目录。
    // openmode日志文件的打开模式缺省值是ios::app。
    // bbackup是否自动切换备份true-切换false-不切换在多进程的服务程序中如果多个进程共用一个日志文件bbackup必须为false。
    // benbuffer是否启用文件缓冲机制true-启用false-不启用,如果启用缓冲区,那么写进日志文件中的内容不会立即写入文件,缺省是不启用。
    // 注意,在多进程的程序中,多个进程往同一日志文件写入大量的日志时,可能会出现小混乱,但是,多线程不会。
    // 1多个进程往同一日志文件写入大量的日志时可能会出现小混乱这个问题并不严重可以容忍
    // 2只有同时写大量日志时才会出现混乱在实际开发中这种情况不多见。
    // 3如果业务无法容忍可以用信号量加锁。
    bool open(const string &filename,const ios::openmode mode=ios::app,const bool bbackup=true,const bool benbuffer=false);

    // 把日志内容以文本的方式格式化输出到日志文件,并且,在日志内容前面写入时间。
    template< typename... Args >
    bool write(const char* fmt, Args... args) 
    {
        if (fout.is_open()==false) return false;

        backup();                   // 判断是否需要切换日志文件。

        m_splock.lock();        // 加锁。
        fout << ltime1() << " " << sformat(fmt,args...);      // 把当前时间和日志内容写入日志文件。
        m_splock.unlock();    // 解锁。

        return fout.good();
    }

    // 重载<<运算符,把日志内容以文本的方式输出到日志文件,不会在日志内容前面写时间。
    // 注意:内容换行用\n不能用endl。
    template<typename T>
    clogfile& operator<<(const T &value)
    {
        m_splock.lock();
        fout << value; 
        m_splock.unlock();

        return *this;
    }
  
private:
    // 如果日志文件的大小超过m_maxsize的值就把当前的日志文件名改为历史日志文件名再创建新的当前日志文件。
    // 备份后的文件会在日志文件名后加上日期时间,如/tmp/log/filetodb.log.20200101123025。
    // 注意,在多进程的程序中,日志文件不可切换,多线的程序中,日志文件可以切换。
    bool backup();
public:
    void close() { fout.close(); }

    ~clogfile() { close(); };
};

日志类实现

bool clogfile::open(const string &filename,const ios::openmode mode,const bool bbackup,const bool benbuffer)
{
    // 如果日志文件是打开的状态,先关闭它。
    if (fout.is_open()) fout.close();

    m_filename=filename;        // 日志文件名。
    m_mode=mode;                 // 打开模式。
    m_backup=bbackup;          // 是否自动备份。
    m_enbuffer=benbuffer;      // 是否启用文件缓冲区。

    newdir(m_filename,true);                              // 如果日志文件的目录不存在,创建它。

    fout.open(m_filename,m_mode);                  // 打开日志文件。

    if (m_enbuffer==false) fout << unitbuf;       // 是否启用文件缓冲区。

    return fout.is_open();
}

bool clogfile::backup()
{
    // 不备份
    if (m_backup == false) return true;

    if (fout.is_open() == false) return false;

    // 如果当前日志文件的大小超过m_maxsize备份日志。
    if (fout.tellp() > m_maxsize*1024*1024)
    {
        m_splock.lock();       // 加锁。

        fout.close();              // 关闭当前日志文件。

        // 拼接备份日志文件名。
        string bak_filename=m_filename+"."+ltime1("yyyymmddhh24miss");

        rename(m_filename.c_str(),bak_filename.c_str());   // 把当前日志文件改名为备份日志文件。

        fout.open(m_filename,m_mode);              // 重新打开当前日志文件。

        if (m_enbuffer==false) fout << unitbuf;   // 判断是否启动文件缓冲区。

        m_splock.unlock();   // 解锁。

        return fout.is_open();
    }

    return true;
}

bool cifile::open(const string &filename,const ios::openmode mode)
{
    // 如果文件是打开的状态,先关闭它。
    if (fin.is_open()) fin.close();

    m_filename=filename;

    fin.open(m_filename,mode);

    return fin.is_open();
}

xml函数族定义

// 解析xml格式字符串的函数族。
// xml格式的字符串的内容如下
// <filename>/tmp/_public.h</filename><mtime>2020-01-01 12:20:35</mtime><size>18348</size>
// <filename>/tmp/_public.cpp</filename><mtime>2020-01-01 10:10:15</mtime><size>50945</size>
// xmlbuffer待解析的xml格式字符串。
// fieldname字段的标签名。
// value传入变量的地址用于存放字段内容支持bool、int、insigned int、long、unsigned long、double和char[]。
// 注意当value参数的数据类型为char []时必须保证value数组的内存足够否则可能发生内存溢出的问题也可以用ilen参数限定获取字段内容的长度ilen的缺省值为0表示不限长度。
// 返回值true-成功如果fieldname参数指定的标签名不存在返回失败。
bool getxmlbuffer(const string &xmlbuffer,const string &fieldname,string &value,const int ilen=0);    
bool getxmlbuffer(const string &xmlbuffer,const string &fieldname,char *value,const int ilen=0);
bool getxmlbuffer(const string &xmlbuffer,const string &fieldname,bool &value);
bool getxmlbuffer(const string &xmlbuffer,const string &fieldname,int  &value);
bool getxmlbuffer(const string &xmlbuffer,const string &fieldname,unsigned int &value);
bool getxmlbuffer(const string &xmlbuffer,const string &fieldname,long &value);
bool getxmlbuffer(const string &xmlbuffer,const string &fieldname,unsigned long &value);
bool getxmlbuffer(const string &xmlbuffer,const string &fieldname,double &value);
bool getxmlbuffer(const string &xmlbuffer,const string &fieldname,float &value);

xml函数族实现

bool getxmlbuffer(const string &xmlbuffer,const string &fieldname,string  &value,const int ilen)
{
    string start="<"+fieldname+">";            // 数据项开始的标签。
    string end="</"+fieldname+">";            // 数据项结束的标签。

    int startp=xmlbuffer.find(start);               // 在xml中查找数据项开始的标签的位置。
    if (startp==string::npos) return false;

    int endp=xmlbuffer.find(end);                 // 在xml中查找数据项结束的标签的位置。
    if (endp==string::npos) return false;

    // 从xml中截取数据项的内容。
    int itmplen=endp-startp-start.length();
    if ( (ilen>0) && (ilen<itmplen) ) itmplen=ilen;
    value=xmlbuffer.substr(startp+start.length(),itmplen);

    return true;
}

bool getxmlbuffer(const string &xmlbuffer,const string &fieldname,char *value,const int len)
{
    if (value==nullptr) return false;

    if (len>0) memset(value,0,len+1);   // 调用者必须保证value的空间足够否则这里会内存溢出。

    string str;
    getxmlbuffer(xmlbuffer,fieldname,str);

    if ( (str.length()<=(unsigned int)len) || (len==0) )
    {
        str.copy(value,str.length());
        value[str.length()]=0;    // string的copy函数不会给C风格字符串的结尾加0。
    }
    else
    {
        str.copy(value,len);
        value[len]=0;
    }

    return true;
}

bool getxmlbuffer(const string &xmlbuffer,const string &fieldname,bool &value)
{
    string str;
    if (getxmlbuffer(xmlbuffer,fieldname,str)==false) return false;

    toupper(str);    // 转换为大写来判断(也可以转换为小写,效果相同)。

    if (str=="TRUE") value=true; 
    else value=false;

    return true;
}

bool getxmlbuffer(const string &xmlbuffer,const string &fieldname,int &value)
{
    string str;

    if (getxmlbuffer(xmlbuffer,fieldname,str)==false) return false;

    try
    {
       value = stoi(picknumber(str,true));  // stoi有异常需要处理异常。
    }
    catch(const std::exception& e)
    {
        return false;
    }

    return true;
}

bool getxmlbuffer(const string &xmlbuffer,const string &fieldname,unsigned int &value)
{
    string str;

    if (getxmlbuffer(xmlbuffer,fieldname,str)==false) return false;

    try
    {
       value = stoi(picknumber(str));  // stoi有异常需要处理异常。不提取符号 + -
    }
    catch(const std::exception& e)
    {
        return false;
    }

    return true;
}

bool getxmlbuffer(const string &xmlbuffer,const string &fieldname,long &value)
{
    string str;

    if (getxmlbuffer(xmlbuffer,fieldname,str)==false) return false;

    try
    {
        value = stol(picknumber(str,true));  // stol有异常需要处理异常。
    }
    catch(const std::exception& e)
    {
        return false;
    }

    return true;
}

bool getxmlbuffer(const string &xmlbuffer,const string &fieldname,unsigned long &value)
{
    string str;

    if (getxmlbuffer(xmlbuffer,fieldname,str)==false) return false;

    try
    {
        value = stoul(picknumber(str));  // stoul有异常需要处理异常。不提取符号 + -
    }
    catch(const std::exception& e)
    {
        return false;
    }

    return true;
}

bool getxmlbuffer(const string &xmlbuffer,const string &fieldname,double &value)
{
    string str;

    if (getxmlbuffer(xmlbuffer,fieldname,str)==false) return false;

    try
    {
        value = stod(picknumber(str,true,true));  // stod有异常需要处理异常。提取符号和小数点。
    }
    catch(const std::exception& e)
    {
        return false;
    }

    return true;
}

bool getxmlbuffer(const string &xmlbuffer,const string &fieldname,float &value)
{
    string str;

    if (getxmlbuffer(xmlbuffer,fieldname,str)==false) return false;

    try
    {
        value = stof(picknumber(str,true,true));  // stof有异常需要处理异常。提取符号和小数点。
    }
    catch(const std::exception& e)
    {
        return false;
    }

    return true;
}