很多时候,要朝对端发请求, 那么你需要的是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是一个总的预期时间, 做递减更新更符合逻辑。