tars源码漫谈第15篇------tc_clientsocket.h/tc_clientsocket.cpp(socket的客户端操作封装)

      很多时候,要朝对端发请求, 那么你需要的是tc_clientsocket.h/tc_clientsocket.cpp. 之前说过, 在tars中, 几乎没看到select/poll,  因为epoll遮挡了它们的光芒, 虽然在client端编程中完全可以用select/poll.

 

    在tc_clientsocket.h中有#include "util/tc_http.h", 但没有看到与http相关的代码, 所以暂时忽略http. 而TC_EndpointParse_Exception之类的异常类, 不必说, 都一个德性。

 

      看看TC_Endpoint类, 首先, 来看看tars配置文件中endpoint的格式:

<tars.tarsconfig.ConfigObjAdapter>  
	allow  
	endpoint=tcp -h 192.168.2.131 -p 10001 -t 60000  
	handlegroup=tars.tarsconfig.ConfigObjAdapter  
	maxconns=10240  
	protocol=tars  
	queuecap=10000  
	queuetimeout=60000  
	servant=tars.tarsconfig.ConfigObj  
	shmcap=0  
	shmkey=0  
	threads=10  
</tars.tarsconfig.ConfigObjAdapter>  

      而TC_Endpoint类就是对“tcp -h 192.168.2.131 -p 10001 -t 60000”的解析, 从而获取如下信息:

    /**
     * @brief  字符串形式的端口
     * tcp:SOCK_STREAM
     *
     * udp:SOCK_DGRAM
     *
     * -h: ip
     *
     * -p: 端口
     *
     * -t: 超时时间, 毫秒
     *
     * -p 和 -t可以省略, -t默认10s
     *
     * tcp -h 127.0.0.1 -p 2345 -t 10000
     *
     * @param desc
     */

      所以, TC_Endpoint不必多说。

 

      再看TC_ClientSocket,  这个一个客户端socket虚基类, 它不会有具体的cpp实现, 将来是要被tcp client和udp client相关类继承进而实现的:

/**
* @brief 客户端socket相关操作基类
*/
class TC_ClientSocket
{
public:

    /**
    *  @brief 构造函数
     */
    TC_ClientSocket() : _port(0),_timeout(3000) {}

    /**
     * @brief 析够函数
     */
    virtual ~TC_ClientSocket(){}

    /**
    * @brief 构造函数
    * @param sIP      服务器IP
    * @param iPort    端口, port为0时:表示本地套接字此时ip为文件路径
    * @param iTimeout 超时时间, 毫秒
    */
    TC_ClientSocket(const string &sIp, int iPort, int iTimeout) { init(sIp, iPort, iTimeout); }

    /**
    * @brief 初始化函数
    * @param sIP      服务器IP
    * @param iPort    端口, port为0时:表示本地套接字此时ip为文件路径
    * @param iTimeout 超时时间, 毫秒
    */
    void init(const string &sIp, int iPort, int iTimeout)
    {
        _socket.close();
        _ip         = sIp;
        _port       = iPort;
        _timeout    = iTimeout;
    }

    /**
    * @brief 发送到服务器
    * @param sSendBuffer 发送buffer
    * @param iSendLen    发送buffer的长度
    * @return            int 0 成功,<0 失败
    */
    virtual int send(const char *sSendBuffer, size_t iSendLen) = 0;

    /**
    * @brief 从服务器返回不超过iRecvLen的字节
    * @param sRecvBuffer 接收buffer
    * @param iRecvLen    指定接收多少个字符才返回,输出接收数据的长度
    * @return            int 0 成功,<0 失败
    */
    virtual int recv(char *sRecvBuffer, size_t &iRecvLen) = 0;

    /**
    * @brief  定义发送的错误
    */
    enum
    {
        EM_SUCCESS  = 0,          /** EM_SUCCESS:发送成功*/
        EM_SEND     = -1,        /** EM_SEND:发送错误*/
        EM_SELECT   = -2,        /** EM_SELECT:select 错误*/
        EM_TIMEOUT  = -3,        /** EM_TIMEOUT:select超时*/
        EM_RECV     = -4,        /** EM_RECV: 接受错误*/
        EM_CLOSE    = -5,        /**EM_CLOSE: 服务器主动关闭*/
        EM_CONNECT  = -6,        /** EM_CONNECT : 服务器连接失败*/
        EM_SOCKET   = -7        /**EM_SOCKET : SOCKET初始化失败*/
    };

protected:
    /**
     * 套接字句柄
     */
    TC_Socket     _socket;

    /**
     * ip或文件路径
     */
    string        _ip;

    /**
     * 端口或-1:标示是本地套接字
     */
    int         _port;

    /**
     * 超时时间, 毫秒
     */
    int            _timeout;
};

       这样避免和tcp client和udp client的代码重复。

 

       看下TC_TCPClient这个重要的类, 在check socket的时候, 创建了tcp socket, 并进行了connect连接:

int TC_TCPClient::checkSocket()
{
    if(!_socket.isValid())
    {
        try
        {
            if(_port == 0)
            {
                _socket.createSocket(SOCK_STREAM, AF_LOCAL);
            }
            else
            {
                _socket.createSocket(SOCK_STREAM, AF_INET);

            }

            //设置非阻塞模式
            _socket.setblock(false);

            try
            {
                if(_port == 0)
                {
                    _socket.connect(_ip.c_str());
                }
                else
                {
                    _socket.connect(_ip, _port);
                }
            }
            catch(TC_SocketConnect_Exception &ex)
            {
                if(errno != EINPROGRESS)
                {
                    _socket.close();
                    return EM_CONNECT;
                }
            }

            if(errno != EINPROGRESS)
            {
                _socket.close();
                return EM_CONNECT;
            }

            TC_Epoller epoller(false);
            epoller.create(1);
            epoller.add(_socket.getfd(), 0, EPOLLOUT);
            int iRetCode = epoller.wait(_timeout);
            if (iRetCode < 0)
            {
                _socket.close();
                return EM_SELECT;
            }
            else if (iRetCode == 0)
            {
                _socket.close();
                return EM_TIMEOUT;
            }
            else
            {
                for(int i = 0; i < iRetCode; ++i)
                {
                    const epoll_event& ev = epoller.get(i);
                    if (ev.events & EPOLLERR || ev.events & EPOLLHUP)
                    {
                        _socket.close();
                        return EM_CONNECT;
                    }
                    else
                    {
                        int iVal = 0;
                        socklen_t iLen = static_cast<socklen_t>(sizeof(int));
                        if (::getsockopt(_socket.getfd(), SOL_SOCKET, SO_ERROR, reinterpret_cast<char*>(&iVal), &iLen) == -1 || iVal)
                        {
                            _socket.close();
                            return EM_CONNECT;
                        }
                    }
                }
            }

            //设置为阻塞模式
            _socket.setblock(true);
        }
        catch(TC_Socket_Exception &ex)
        {
            _socket.close();
            return EM_SOCKET;
        }
    }
    return EM_SUCCESS;
}

        创建socket, 设置为非阻塞模式, 为什么这么搞? 因为随后要实现超时的connect连接。

       在这里, 看到了我们熟悉的epoll(且上述代码用了LT模式, TC_Epoller epoller(false);就是证据),  创建了epoll管理句柄, 把socket添加到epoll管理句柄中, 然后执行wait(里面实际上是epoll_wait的操作),  当iRetCode>0时候, 说明connect成功了, iRetCode就是“就绪”的socket的个数, 这里的值肯定是1. connect成功后, 把socket设置为阻塞模式, 还原。 这个过程一气呵成, tcp连接通道已经建立。

         注意到, epoll在这里监测的是EPOLLOUT事件, 而不是EPOLLIN, 想想为什么? easy.

         随后是send, 这个很简单。 来看看recv:

int TC_TCPClient::recv(char *sRecvBuffer, size_t &iRecvLen)
{
    int iRet = checkSocket();
    if(iRet < 0)
    {
        return iRet;
    }

    TC_Epoller epoller(false);
    epoller.create(1);
    epoller.add(_socket.getfd(), 0, EPOLLIN);

    int iRetCode = epoller.wait(_timeout);
    if (iRetCode < 0)
    {
        _socket.close();
        return EM_SELECT;
    }
    else if (iRetCode == 0)
    {
        _socket.close();
        return EM_TIMEOUT;
    }

    epoll_event ev  = epoller.get(0);
    if(ev.events & EPOLLIN)
    {
        int iLen = _socket.recv((void*)sRecvBuffer, iRecvLen);
        if (iLen < 0)
        {
            _socket.close();
            return EM_RECV;
        }
        else if (iLen == 0)
        {
            _socket.close();
            return EM_CLOSE;
        }

        iRecvLen = iLen;
        return EM_SUCCESS;
    }
    else
    {
        _socket.close();
    }

    return EM_SELECT;
}

      由于socket是阻塞的, 如果直接调用原生的linux recv函数会阻塞,  所以在TC_TCPClient::recv中, 要先判断socket的可读性, 也就是EPOLLIN事件,  epoll又完成了监测工作, 随后的recv就能顺利读取了, 不会阻塞(因为EPOLLIN表示有数据了)。

       recvBySep是一个很有意思的函数, 一直接收, 直到包含sSep为止, 但要注意, 接收的实际sRecvBuffer很有可能会越过sSep的边界, 包含sSep, 会break出来:

int TC_TCPClient::recvBySep(string &sRecvBuffer, const string &sSep)
{
    sRecvBuffer.clear();

    int iRet = checkSocket();
    if(iRet < 0)
    {
        return iRet;
    }

    TC_Epoller epoller(false);
    epoller.create(1);
    epoller.add(_socket.getfd(), 0, EPOLLIN);

    while(true)
    {
        int iRetCode = epoller.wait(_timeout);
        if (iRetCode < 0)
        {
            _socket.close();
            return EM_SELECT;
        }
        else if (iRetCode == 0)
        {
            _socket.close();
            return EM_TIMEOUT;
        }

        epoll_event ev  = epoller.get(0);
        if(ev.events & EPOLLIN)
        {
            char buffer[LEN_MAXRECV] = "\0";

            int len = _socket.recv((void*)&buffer, sizeof(buffer));
            if (len < 0)
            {
                _socket.close();
                return EM_RECV;
            }
            else if (len == 0)
            {
                _socket.close();
                return EM_CLOSE;
            }

            sRecvBuffer.append(buffer, len);

            if(sRecvBuffer.length() >= sSep.length() 
               && sRecvBuffer.compare(sRecvBuffer.length() - sSep.length(), sSep.length(), sSep) == 0)
            {
                break;
            }
        }
    }

    return EM_SUCCESS;
}

       而如下的recvAll确实太霸道了, 会按LEN_MAXRECV来循环接收, 直到没有可收的数据而超时为止:

int TC_TCPClient::recvAll(string &sRecvBuffer)
{
    sRecvBuffer.clear();

    int iRet = checkSocket();
    if(iRet < 0)
    {
        return iRet;
    }

    TC_Epoller epoller(false);
    epoller.create(1);
    epoller.add(_socket.getfd(), 0, EPOLLIN);

    while(true)
    {
        int iRetCode = epoller.wait(_timeout);
        if (iRetCode < 0)
        {
            _socket.close();
            return EM_SELECT;
        }
        else if (iRetCode == 0)
        {
            _socket.close();
            return EM_TIMEOUT;
        }

        epoll_event ev  = epoller.get(0);
        if(ev.events & EPOLLIN)
        {
            char sTmpBuffer[LEN_MAXRECV] = "\0";

            int len = _socket.recv((void*)sTmpBuffer, LEN_MAXRECV);
            if (len < 0)
            {
                _socket.close();
                return EM_RECV;
            }
            else if (len == 0)
            {
                _socket.close();
                return EM_SUCCESS;
            }

            sRecvBuffer.append(sTmpBuffer, len);
        }
        else
        {
            _socket.close();
            return EM_SELECT;
        }
    }

    return EM_SUCCESS;
}

       recvLength函数是给出了预期, 一定要收到iRecvLen这个长度为止, 否则绝不罢休, 直到超时:

int TC_TCPClient::recvLength(char *sRecvBuffer, size_t iRecvLen)
{
    int iRet = checkSocket();
    if(iRet < 0)
    {
        return iRet;
    }

    size_t iRecvLeft = iRecvLen;
    iRecvLen = 0;

    TC_Epoller epoller(false);
    epoller.create(1);
    epoller.add(_socket.getfd(), 0, EPOLLIN);

    while(iRecvLeft != 0)
    {
        int iRetCode = epoller.wait(_timeout);
        if (iRetCode < 0)
        {
            _socket.close();
            return EM_SELECT;
        }
        else if (iRetCode == 0)
        {
            _socket.close();
            return EM_TIMEOUT;
        }

        epoll_event ev  = epoller.get(0);
        if(ev.events & EPOLLIN)
        {
            int len = _socket.recv((void*)(sRecvBuffer + iRecvLen), iRecvLeft);
            if (len < 0)
            {
                _socket.close();
                return EM_RECV;
            }
            else if (len == 0)
            {
                _socket.close();
                return EM_CLOSE;
            }

            iRecvLeft -= len;
            iRecvLen += len;
        }
        else
        {
            _socket.close();
            return EM_SELECT;
        }
    }

    return EM_SUCCESS;
}

       随后的sendRecv是一个简单的单发单收, 不必说。

       sendRecvBySep是单发, 且收到至少包含sSep为止。

       sendRecvLine是sendRecvBySep的特化(sSep="\r\n"时的特例)

       sendRecvAll无非就是send和recvAll的组合, 并无新意。

 

       tcp说完了, 再来看udp:

       checkSocket不必再说。

       TC_UDPClient::send也很直接, 不说。

       由于udp是数据包传输, 所以TC_UDPClient::recv也很直接地接收了, 不用管包的边界问题, 用epoll来控制超时时间即可:

int TC_UDPClient::recv(char *sRecvBuffer, size_t &iRecvLen)
{
    string sTmpIp;
    uint16_t iTmpPort;

    return recv(sRecvBuffer, iRecvLen, sTmpIp, iTmpPort);
}

int TC_UDPClient::recv(char *sRecvBuffer, size_t &iRecvLen, string &sRemoteIp, uint16_t &iRemotePort)
{
    int iRet = checkSocket();
    if(iRet < 0)
    {
        return iRet;
    }

    TC_Epoller epoller(false);
    epoller.create(1);
    epoller.add(_socket.getfd(), 0, EPOLLIN);
    int iRetCode = epoller.wait(_timeout);
    if (iRetCode < 0)
    {
        return EM_SELECT;
    }
    else if (iRetCode == 0)
    {
        return EM_TIMEOUT;
    }

    epoll_event ev  = epoller.get(0);
    if(ev.events & EPOLLIN)
    {
        iRet = _socket.recvfrom(sRecvBuffer, iRecvLen, sRemoteIp, iRemotePort);
        if(iRet <0 )
        {
            return EM_SEND;
        }

        iRecvLen = iRet;
        return EM_SUCCESS;
    }

    return EM_SELECT;
}

        而最后的sendRecv也很easy,  无需多说。

 

        至此, tcp client和udp client的源码都分析完了, 以后可以直接来套用/调用。 

        说两点自己的想法:

        1. 其实在tcp收包的过程中,在TC_TCPClient类中, 可以搞个函数指针/回调函数, 让上层的业务调用着来定义tcp包的边界。 当然, 如果上层的业务调用着知道要收多少包后, recvLength函数也是能满足需求的。

        2.  上述的循环recv过程, 没有考虑到超时时间的更新问题, 要知道, 对于外部调用者来说,_timeout是一个总的预期时间, 做递减更新更符合逻辑。

 

 

       

 

      

      

 

    

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值