Linux多线程服务端编程学习(七):TCP客户端的设计

本文介绍了Linux多线程服务端编程中TCP客户端的设计,包括Connector类和TcpClient类。Connector用于发起连接,处理连接错误,如EINTR、EAGAIN等,并进行重试。TcpClient利用Connector实现与服务器的连接,支持断线重连功能,确保客户端能适应服务端的动态重启。

前文

一般是由客户端主动发起连接,而主动发起连接就比被动接受连接要复杂一些,一方面是错误处理麻烦,另一方面是要考虑重试。发起连接的基本方式是调用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();

成员函数的功能从名字可以看出,非常明显,分别是启动连接,断开连接,停止连接。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值