前文
一般是由客户端主动发起连接,而主动发起连接就比被动接受连接要复杂一些,一方面是错误处理麻烦,另一方面是要考虑重试。发起连接的基本方式是调用connect,当socket变得可写时,表明连接建立完毕,但其中要处理各种错误,要判断连接是否建立成功。
因此,实现中我们将其封装为Connector类,将其作为TCP客户端TcpClient类的成员,需要注意的几个地方是:
- 用于建立连接的socket是一次性的,一旦出错,就无法恢复,只能关闭重来。但Connector是可以反复使用的,因此每次尝试连接都要使用新的socket和新的channel。
- 错误代码与accept不同,EAGAIN是真的错误,表明本机的临时端口号暂时用完,要关闭socket再延期重试。
- 即便出现socket可写,也不一定意味着连接已成功建立,还需要用
getsockopt
再次确认一下。 - 重试的间隔应该逐渐延长,例如0.5s、1s、2s、4s,直至30s。
- 要处理自连接,处理的办法是断开连接再重试
Connector类
connector的构造函数为:
Connector(EventLoop* loop, const InetAddress& serverAddr);
其核心成员函数为
void start();
void startInLoop();
void connect();
void connecting(int sockfd);
void handleWrite();
void handleError();
void retry(int sockfd);
void restart();
void stop();
void stopInLoop();
其中start函数会调用线程安全的startInLoop函数,startInLoop则调用connect尝试连接。
void Connector::connect()
{
int sockfd = sockets::createNonblockingOrDie(serverAddr_.family());
int ret = sockets::connect(sockfd, serverAddr_.getSockAddr());
int savedErrno = (ret == 0) ? 0 : errno;
switch (savedErrno)
{
case 0:
case EINPROGRESS:
case EINTR:
case EISCONN:
connecting(sockfd);
break;
case EAGAIN:
case EADDRINUSE:
case EADDRNOTAVAIL:
case ECONNREFUSED:
case ENETUNREACH:
retry(sockfd);
break;
case EACCES:
case EPERM:
case EAFNOSUPPORT:
case EALREADY:
case EBADF:
case EFAULT:
case ENOTSOCK:
LOG_ERROR << "connect error in Connector::startInLoop " << savedErrno;
sockets::close(sockfd);
break;
default:
LOG_ERROR << "Unexpected error in Connector::startInLoop " << savedErrno;
sockets::close(sockfd);
break;
}
}
在Connector::connect()中会调用connect(2)尝试连接,并处理各种类型的错误。我们分析下错误类型EINTR。
EINTR:在我的上一篇文章非阻塞IO中,我提到如何处理被中断的connect。如果connect被中断并且不由内核自动重启,那么它将返回EINTR,我们不能再次调用connect等待未连接的连接完成,这样会导致返回EADDRINUSE错误。而应该使用IO多路复用技术(如epoll),连接建立成功时,epoll返回套接字可写,连接建立失败时,套接字既可读又可写。
对于EAGAIN、ECONNREFUSED、ENETUNREACH等错误类型,我们应该关闭套接字,并尝试重连。
根据不同的错误类型,将分别调用Connector::connecting()和Connector::retry()。
void Connector::connecting(int sockfd)
{
setState(Connecting);
assert(!channel_);
channel_.reset(new Channel(loop_, sockfd));
channel_->setWriteCallback(std::bind(&Connector::handleWrite, this));
channel_->setErrorCallback(std::bind(&Connector::handleError, this));
channel_->enableWriting();
}
connecting()等待连接描述符变得可写,并设置其相应的回调函数handleWrite和handleError。
若客户端发起连接,根据连接的情况,epoll会处理相应的事件,若事件为EPOLLOUT,则调用handleWrite回调函数:
void Connector::handleWrite()
{
LOG_TRACE << "Connector::handleWrite " << state_;
if (state_ == Connecting)
{
//一次连接,channel_只使用一次,用于设置IO回调。
int sockfd = removeAndResetChannel();
int err = sockets::getSocketError(sockfd);
if (err)
{
LOG_WARN << "Connector::handleWrite - SO_ERROR = "
<< err << " " << strerror(err);
retry(sockfd);
}
else if (sockets::isSelfConnect(sockfd))
{
LOG_WARN << "Connector::handleWrite - Self connect";
retry(sockfd);
}
else
{
setState(Connected);
if (connect_)
{
newConnectionCallback_(sockfd);
}
else
{
sockets::close(sockfd);
}
}
}
else
{
assert(state_ == Disconnected);
}
}
在handleWrite中,当套接字变得可写时,需要调用getsockopt来检查套接字中是否有待处理的错误。若有错误,则调用retry尝试重连。同时,也需要处理自连接的情况。若连接成功,则调用newConnectionCallback_回调函数,该回调函数会创建新连接,设置连接的回调函数。
void Connector::retry(int sockfd)
{
sockets::close(sockfd);
setState(Disconnected);
if (connect_)
{
LOG_INFO << "Connector::retry - Retry connecting to " << serverAddr_.toIpPort()
<< " in " << retryDelayMs_ << " milliseconds. ";
loop_->runAfter(retryDelayMs_/1000.0,
std::bind(&Connector::startInLoop, shared_from_this()));
retryDelayMs_ = std::min(retryDelayMs_ * 2, MaxRetryDelayMs);
}
else
LOG_DEBUG << "do not connect";
}
当出现连接错误时,就会调用retry尝试重连,retry中会关闭套接字,并调用EventLoop::runAfter尝试延时调用startInLoop重连。
Connector::stop()通过设置标志connect_ = false
来实现中断连接。
TCP客户端TcpClient
有了Connector来处理主动发起连接时各种麻烦事,我们可以着手实现TCP客户端TcpClient类了,它的代码与TcpServer比较相似,不同的是,TcpClient只管理一个TcpConnection。
TcpClient具备TcpConnection断开之后重连的功能,加上Connector具备反复尝试连接的功能,因此客户端和服务端的启动顺序无关紧要。在客户端运行期间服务端可以重启,客户端也会自动重连。
TcpClient的构造函数为:
TcpClient(EventLoop* loop, const InetAddress& serverAddr,
const string& nameArg);
loop为TcpClient的反应器,serverAddr为需要连接的服务器地址,nameArg为TcpClient的名字。
核心的成员函数为:
void connect();
void disconnect();
void stop();
成员函数的功能从名字可以看出,非常明显,分别是启动连接,断开连接,停止连接。