Mudo分析及总结(八)TcpCliet和Connector

一、上下文类关系图

在这里插入图片描述
Tcp的上下文类关系图如上所示。

  • TcpClient,相对于TcpServer的,作为一个客户端,TcpClient具备TcpConnection断开之后重连的功能。
  • EventLoop,IO处理线程。
  • Connector,负责客户端的发起连接任务,连接建立好之后就和Connector没关系了。
  • TcoConnection,当Connector连接上服务端之后,就将连接Socket交由TcoConnection进行维护。
二、模块分析
2.1 Connector

主动发起连接比被动接受连接要复杂些,一方面是错误处理麻烦,另一方面是要考虑重试。Muduo封装了Connector class 用于客户端的发起连接,它只负责建立socket,创建好的socket交由TcpConnection进行维护管理。
Connector的设计难点:

  • socket是一次性的,一旦出错(比如对方拒绝连接),就无法恢复,只能关闭重来。但Connector是可以反复使用的,因此每次尝试连接都要使用新的socket文件描述符和新的Channel对象。
  • Connect连接时处理的错误代码与accept(2)不同,EAGAIN是真的错误,表明本机ephemeral port暂时用完,要关闭socket再延期重试。“正在连接”的返回码是EINPROGRESS。另外,即使出现socket可写,也不一定意味着连接已经成功建立,还需要用getsockopt(sockfd,SOL_SOCKET,SO_ERROR,…)再次确认下。
  • 重试的间隔应该逐渐延长,例如0.5s、1s、2s、4s,直至30s,即back-off。
  • 要处理自连接的情况。
2.1.1源码分析

Connector只对外开放了四个接口。

  • Connector::setNewConnectionCallback(),连接建立好之后调用此回调。
  • Connector::start(),开始连接服务端。
  • Connector::restart(),重新开始连接服务端。
  • Connector::stop(),停止对服务端的连接。

Connector.h

class Connector : boost::noncopyable,
                  public boost::enable_shared_from_this<Connector>
{
 public:
  typedef boost::function<void (int sockfd)> NewConnectionCallback;

  Connector(EventLoop* loop, const InetAddress& serverAddr);
  ~Connector();

  //新连接回调
  void setNewConnectionCallback(const NewConnectionCallback& cb)
  { newConnectionCallback_ = cb; }

  void start();  // can be called in any thread
  void restart();  // must be called in loop thread
  void stop();  // can be called in any thread

  //服务端地址
  const InetAddress& serverAddress() const { return serverAddr_; }

 private:

  //连接三种状态	
  enum States { kDisconnected, kConnecting, kConnected };
  //最大重连时间30s
  static const int kMaxRetryDelayMs = 30*1000;
  //初始重连时间500ms
  static const int kInitRetryDelayMs = 500;

  //当前连接状态
  void setState(States s) { state_ = s; }

  //开始连接
  void startInLoop();
  //停止连接
  void stopInLoop();
  //连接
  void connect();
  //正在连接
  void connecting(int sockfd);
  //写处理调用
  void handleWrite();
  //出错调用
  void handleError();
  //重试
  void retry(int sockfd);
  //移除和释放Channel
  int removeAndResetChannel();
  //释放Channel
  void resetChannel();

  //IO处理服务
  EventLoop* loop_;
  //服务端地址
  InetAddress serverAddr_;
  //连接状态位
  bool connect_; // atomic
  States state_;  // FIXME: use atomic variable
  //用于连接服务端的Channel
  boost::scoped_ptr<Channel> channel_;
  //连接成功以后要调用的回调
  NewConnectionCallback newConnectionCallback_;
  //重连间隔
  int retryDelayMs_; 
};

Connector.cc

const int Connector::kMaxRetryDelayMs;

//Connector初始化流程
//a、传入IO服务线程。
//b、传入服务端地址。
//c、连接动作置位false。
//d、当前连接状态为断开。
//e、重连时间初始化为500ms。
Connector::Connector(EventLoop* loop, const InetAddress& serverAddr)
  : loop_(loop),
    serverAddr_(serverAddr),
    connect_(false),
    state_(kDisconnected),
    retryDelayMs_(kInitRetryDelayMs)
{
  LOG_DEBUG << "ctor[" << this << "]";
}

//连接释放
Connector::~Connector()
{
  LOG_DEBUG << "dtor[" << this << "]";
  assert(!channel_);
}

//开始连接
void Connector::start()
{
  connect_ = true;
  loop_->runInLoop(boost::bind(&Connector::startInLoop, this)); // FIXME: unsafe
}

void Connector::startInLoop()
{
  loop_->assertInLoopThread();
  assert(state_ == kDisconnected);
  if (connect_)
  {
    connect();
  }
  else
  {
    LOG_DEBUG << "do not connect";
  }
}

void Connector::stop()
{
  connect_ = false;
  loop_->runInLoop(boost::bind(&Connector::stopInLoop, this)); // FIXME: unsafe
  // FIXME: cancel timer
}

void Connector::stopInLoop()
{
  loop_->assertInLoopThread();
  if (state_ == kConnecting)
  {
    setState(kDisconnected);
    int sockfd = removeAndResetChannel();
    retry(sockfd);
  }
}

//连接,要处理三种状态。
//a、正在连接过程。
//b、重连。
//c、连接失败。
void Connector::connect()
{
  //创建非阻塞套接字
  int sockfd = sockets::createNonblockingOrDie();
  //发起连接
  int ret = sockets::connect(sockfd, serverAddr_.getSockAddrInet());
  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;

    //余下状态直接关闭socket,不再进行连接
    case EACCES: //没权限
    case EPERM: //操作不允许
    case EAFNOSUPPORT: //地址族不被协议支持
    case EALREADY://操作已存在
    case EBADF: //错误文件描述符
    case EFAULT: //地址错误
    case ENOTSOCK: //非套接字上操作
      LOG_SYSERR << "connect error in Connector::startInLoop " << savedErrno;
      sockets::close(sockfd);
      break;

    default:
      LOG_SYSERR << "Unexpected error in Connector::startInLoop " << savedErrno;
      sockets::close(sockfd);
      // connectErrorCallback_();
      break;
  }
}

//重新开始连接
void Connector::restart()
{
  loop_->assertInLoopThread();
  setState(kDisconnected);
  retryDelayMs_ = kInitRetryDelayMs;
  connect_ = true;
  startInLoop();
}

//正在连接的处理。
//这里给正在连接的socket创建一个Channel去处理。
void Connector::connecting(int sockfd)
{
  setState(kConnecting);
  assert(!channel_);
  channel_.reset(new Channel(loop_, sockfd));
  channel_->setWriteCallback(
      boost::bind(&Connector::handleWrite, this)); // FIXME: unsafe
  channel_->setErrorCallback(
      boost::bind(&Connector::handleError, this)); // FIXME: unsafe

  // channel_->tie(shared_from_this()); is not working,
  // as channel_ is not managed by shared_ptr
  channel_->enableWriting();
}

//释放用于连接的Channel。
int Connector::removeAndResetChannel()
{
  channel_->disableAll();
  channel_->remove();
  int sockfd = channel_->fd();
  // Can't reset channel_ here, because we are inside Channel::handleEvent
  loop_->queueInLoop(boost::bind(&Connector::resetChannel, this)); // FIXME: unsafe
  return sockfd;
}

 //释放Channel
void Connector::resetChannel()
{
  channel_.reset();
}

//这里为什么要对错误码做判断?
//连接成功客户端会收到写事件。
//a、释放掉用于连接的Channel。
//b、有异常就重连。
//c、连接成功,检查下是否是自连接。(分析下什么是自连接。)
//d、正常的话就连接成功了。
//e、如果当前不是连接状态,就关闭此Socket
void Connector::handleWrite()
{
  LOG_TRACE << "Connector::handleWrite " << state_;

  if (state_ == kConnecting)
  {
    //释放掉用于创建连接的Channel
    int sockfd = removeAndResetChannel();
    int err = sockets::getSocketError(sockfd);
    if (err)
    {
      LOG_WARN << "Connector::handleWrite - SO_ERROR = "
               << err << " " << strerror_tl(err);
      retry(sockfd);
    }
    else if (sockets::isSelfConnect(sockfd))
    {
      LOG_WARN << "Connector::handleWrite - Self connect";
      retry(sockfd);
    }
    else
    {
      setState(kConnected);
      if (connect_)
      {
        newConnectionCallback_(sockfd);
      }
      else
      {
        sockets::close(sockfd);
      }
    }
  }
  else
  {
    // what happened?
    assert(state_ == kDisconnected);
  }
}

//出现错误时,
//a、重置用于连接的Channel。
//b、重连。
void Connector::handleError()
{
  LOG_ERROR << "Connector::handleError";
  assert(state_ == kConnecting);

  int sockfd = removeAndResetChannel();
  int err = sockets::getSocketError(sockfd);
  LOG_TRACE << "SO_ERROR = " << err << " " << strerror_tl(err);
  retry(sockfd);
}

//重连的时间翻倍
void Connector::retry(int sockfd)
{
  //关闭当前socket
  sockets::close(sockfd);
  //当前连接设置为断开
  setState(kDisconnected);
  if (connect_)
  {
    LOG_INFO << "Connector::retry - Retry connecting to " << serverAddr_.toIpPort()
             << " in " << retryDelayMs_ << " milliseconds. ";

    //加入到定时器中	
    loop_->runAfter(retryDelayMs_/1000.0,
                    boost::bind(&Connector::startInLoop, shared_from_this()));
    retryDelayMs_ = std::min(retryDelayMs_ * 2, kMaxRetryDelayMs);
  }
  else
  {
    LOG_DEBUG << "do not connect";
  }
2.2 TcpClient

TcpClient是相对于TcpServer来说的,是个客户端。TcpClient用Connector去建立连接。连接建立好以后,用TcpConnection去维护管理连接。每个TcpConnection只管理一个TcpConnection。

  • TcpClient具备TcpConnection断开之后重新连接的功能,加上Conector具备反复尝试连接的功能,因此客户端和服务端的启动顺序无关紧要。可以先启动客户端,一旦服务端启动,半分钟之内即可恢复连接。在客户端运行期间服务端也可以重启,客户端也会自动重连。
  • 连接断开后初次重试的延迟应该有随机性,比方说服务端崩溃,它所有的客户连接同时断开,然后0.5s之后同时发起连接,这样既可能造成SYN丢包,也可能给服务端带来短期的大负载,影响其服务质量。因此每个TcpClient应该等待一段随机的时间(0.5~2s),在重试,避免拥塞。
    (TcpClient目前并未这样做)
  • 发起连接的时候如果发生TCP SYN丢包,那么系统(TCP协议栈)默认重试间隔3s,这期间不会返回错误码,而且这个间隔似乎不容易修改。如果需要缩短时间,可以再用一个定时器,在0.5s或1s之后发起另一次连接。(此处要说明下,默认重试时间间隔不是固定的3s。在Linux下,默认重试次数为5次,重试的间隔时间从1s开始每次都翻售,5次的重试时间间隔为1s, 2s, 4s, 8s, 16s,总共31s,第5次发出后还要等32s都知道第5次也超时了,所以,总共需要 1s + 2s + 4s+ 8s+ 16s + 32s = 2^6 -1 = 63s,TCP才会把断开这个连接。所以像Muduo作者所说,与其等待63s,系统才返回真正的连接不是,还不如应该逻辑层做好处理,设置一个定时器,如果在一定时间没连接上,直接断开当前连接,发起下一次连接)
2.2.1 源码分析

TcpClient.h

class TcpClient : boost::noncopyable
{
 public:
  // TcpClient(EventLoop* loop);
  // TcpClient(EventLoop* loop, const string& host, uint16_t port);
  TcpClient(EventLoop* loop,
            const InetAddress& serverAddr,
            const string& name);
  ~TcpClient();  // force out-line dtor, for scoped_ptr members.

  void connect(); //连接
  void disconnect(); //断开连接
  void stop(); //停止

 //获取连接变量
  TcpConnectionPtr connection() const
  {
    MutexLockGuard lock(mutex_);
    return connection_;
  }

  //重试
  bool retry() const;

  //使能重试状态
  void enableRetry() { retry_ = true; }


//设置链路的连接断开回调
  /// Set connection callback.
  /// Not thread safe.
  void setConnectionCallback(const ConnectionCallback& cb)
  { connectionCallback_ = cb; }

  //设置链路的接受消息回调
  /// Set message callback.
  /// Not thread safe.
  void setMessageCallback(const MessageCallback& cb)
  { messageCallback_ = cb; }

  //设置写完成回调
  /// Set write complete callback.
  /// Not thread safe.
  void setWriteCompleteCallback(const WriteCompleteCallback& cb)
  { writeCompleteCallback_ = cb; }

 private:

  //创建新连接	
  /// Not thread safe, but in loop
  void newConnection(int sockfd);

  //移除连接
  /// Not thread safe, but in loop
  void removeConnection(const TcpConnectionPtr& conn);

  //IO主循环
  EventLoop* loop_;

  //负责客户端连接建立的类
  ConnectorPtr connector_; // avoid revealing Connector

  //客户端名称
  const string name_;

  //三个回调函数
  ConnectionCallback connectionCallback_;
  MessageCallback messageCallback_;
  WriteCompleteCallback writeCompleteCallback_;

  //重试状态
  bool retry_;   // atmoic

  //连接状态
  bool connect_; // atomic
  // always in loop thread

  //下一个连接ID
  int nextConnId_;

  //可修改锁,用于管理TcpConnection
  mutable MutexLock mutex_;

  //连接建立好的类
  TcpConnectionPtr connection_; // @BuardedBy mutex_
};

TcpClient.cc

namespace detail
{

 //移除已经建立好的连接
void removeConnection(EventLoop* loop, const TcpConnectionPtr& conn)
{
  loop->queueInLoop(boost::bind(&TcpConnection::connectDestroyed, conn));
}

//移除负责建立连接的实体
void removeConnector(const ConnectorPtr& connector)
{
  //connector->
}

}
}
}

//TcpClient的实例化流程
//a、传入负责IO的事件循环。
//b、服务端地址。
//c、客户端名称。
//d、检测Loop是否空。
//e、创建负责建立连接的Connector。
//f、赋值客户端名称。
//g、默认连接回调。
//h、默认消息回调。
//i、重试状态false。
//j、进行连接状态true。
//k、下一个连接ID为1。

TcpClient::TcpClient(EventLoop* loop,
                     const InetAddress& serverAddr,
                     const string& name)
  : loop_(CHECK_NOTNULL(loop)),
    connector_(new Connector(loop, serverAddr)),
    name_(name),
    connectionCallback_(defaultConnectionCallback),
    messageCallback_(defaultMessageCallback),
    retry_(false),
    connect_(true),
    nextConnId_(1)
{

  //当连接上服务端以后,调用的回调
  connector_->setNewConnectionCallback(
      boost::bind(&TcpClient::newConnection, this, _1));
  // FIXME setConnectFailedCallback
  LOG_INFO << "TcpClient::TcpClient[" << name_
           << "] - connector " << get_pointer(connector_);
}

TcpClient::~TcpClient()
{
  LOG_INFO << "TcpClient::~TcpClient[" << name_
           << "] - connector " << get_pointer(connector_);
  TcpConnectionPtr conn;
  {
    MutexLockGuard lock(mutex_);
    conn = connection_;
  }

  
  if (conn)
  {
   //如果TcpConnection还在,那就对其资源进行释放。释放资源有
   //a、detail::removeConnection->TcpConnection::connectDestroyed 会将Channel从Poll中移除掉。
   
  
    // FIXME: not 100% safe, if we are in different thread
    CloseCallback cb = boost::bind(&detail::removeConnection, loop_, _1);
    loop_->runInLoop(
        boost::bind(&TcpConnection::setCloseCallback, conn, cb));
  }
  else
  {
    
    //如果TcpConnection还未有值,说明connector正在连接中,
    //这个时候调用connector->stop去停止连接,释放Channel和
    //关闭用于连接的socket。
    connector_->stop();
    // FIXME: HACK
    loop_->runAfter(1, boost::bind(&detail::removeConnector, connector_));
  }
}

//连接时所做的事
//a、将连接状态置位true。
//b、开始连接。
void TcpClient::connect()
{
  // FIXME: check state
  LOG_INFO << "TcpClient::connect[" << name_ << "] - connecting to "
           << connector_->serverAddress().toIpPort();
  connect_ = true;
  connector_->start();
}

//断开连接所做的事
//a、连接状态置位false。
//b、关闭已经存在的连接。
//b-1、如果连接还在,就调用TcpConnection::shutdown关闭本端的写,
//但继续接受服务端已发送过来的数据,即已进入对方的协议栈和在网络中的数据。
void TcpClient::disconnect()
{
  connect_ = false;

  {
    MutexLockGuard lock(mutex_);
    if (connection_)
    {
      connection_->shutdown();
    }
  }
}

//停止客户端
//a、连接状态置位false
//b、释放Connector中的资源(Channel和Socket)
void TcpClient::stop()
{
  connect_ = false;
  connector_->stop();
}

//当客户端调用Connector连接上服务端之后,会销毁掉Connector。
//销毁掉Connector之后,将建立好连接的sockfd给TcpConnection,
//由TcpConnection负责已经建立好连接的维护工作。
void TcpClient::newConnection(int sockfd)
{

  ///1、准备新连接的初始化信息。
  loop_->assertInLoopThread();
  InetAddress peerAddr(sockets::getPeerAddr(sockfd));
  char buf[32];
  snprintf(buf, sizeof buf, ":%s#%d", peerAddr.toIpPort().c_str(), nextConnId_);
  ++nextConnId_;
  string connName = name_ + buf;

  InetAddress localAddr(sockets::getLocalAddr(sockfd));
  // FIXME poll with zero timeout to double confirm the new connection
  // FIXME use make_shared if necessary
  TcpConnectionPtr conn(new TcpConnection(loop_,
                                          connName,
                                          sockfd,
                                          localAddr,
                                          peerAddr));

  ///2、设置连接的回调函数
  conn->setConnectionCallback(connectionCallback_);
  conn->setMessageCallback(messageCallback_);
  conn->setWriteCompleteCallback(writeCompleteCallback_);
  conn->setCloseCallback(
      boost::bind(&TcpClient::removeConnection, this, _1)); // FIXME: unsafe

  ///3、保存新建立的TcpConnection。	  
  {
    MutexLockGuard lock(mutex_);
    connection_ = conn;
  }
  conn->connectEstablished();
}

//移除连接
void TcpClient::removeConnection(const TcpConnectionPtr& conn)
{
  loop_->assertInLoopThread();
  assert(loop_ == conn->getLoop());

  {

    //释放被管理对象的所有权,若存在。	
    MutexLockGuard lock(mutex_);
    assert(connection_ == conn);
    connection_.reset();
  }

  //销毁TcpConnection
  loop_->queueInLoop(boost::bind(&TcpConnection::connectDestroyed, conn));
  
  if (retry_ && connect_)
  {
    LOG_INFO << "TcpClient::connect[" << name_ << "] - Reconnecting to "
             << connector_->serverAddress().toIpPort();

    //Connector已经被释放了,怎么还能restart?	
    connector_->restart();
  }
}

2.2.1 连接建立

连接建立主要由Connector负责。大致流程如下
在这里插入图片描述
注意点:

Connector建立好连接之后,会释放用于连接的Channel,然后将socket回调给TcpClient,由TcpClient创建TcpConnection去维护这个socket。

2.2.2 连接断开

这里有两层意思:

  1. 正在连接的过程中,停止连接。TcpClinet::connect()发起连接,TcpClient::stop()停止当前正在进行的连接。
  2. 连接建立好以后,断开连接。

第二种的断开连接大致调用方法如下:
TcpClient::disconnect() -> TcpConnection::shutdown() -> TcpConnection::shutdownInLoop() ->
Socket::shutdownWrite()
先关闭本端的写,继续接收对端已经在路上的数据。等收到对端的关闭时,再释放Channel和TcpConnect等和网络操作相关资源。

2.2.3 重连

TcpClient要想自动重连,要调用TcpClient::enableRetry()来触发。
大致流程就是TcpClient收到对端的连接关闭时,会销毁之前用于连接的TcpConnection,重新通过Connector来建立连接连接,剩下的流程就和连接建立流程一样了。

三、总结
Tcp自连接问题:

这里要说下TCP的同时打开概念。两个应用程序同时彼此执行主动打开的情况是有可能的,尽管发生的可能性极小。每一方必须发送SYN,且这些SYN必须传递给对方。交互流程状态图如下:
在这里插入图片描述
而自连接是TCP同时打开的一种特殊情况,要客户端和服务端都跑在同一个网络设备上才会出现且在服务端挂掉,或者服务端本身未启动的情况下即服务端端口未被监听。
出现的原因如下:
一条TCP连接由四元组(源IP、源端口、目的IP、目的端口)来决定,在Muduo连接过程中,源IP、目的IP、目的端口都是确定的,唯一不确定的是源端口。如果此时服务端还未启动,或者服务端异常挂掉了,如果系统选择的源端口与目的端口相同,那么Client和Server(实际上不存在Server这个实体)就是相同的TCP实体。举个列子:

  1. Cient向127.0.0.1:50000发送SYN,进入SYN_SENT状态。
  2. 由于Client已经打开了端口50000,所以不会产生RST报文;相反系统以为50000端口有服务器在监听,就接收了这个SYN报文,并从SYN_SENT状态变为SYN_RCVD状态
  3. 由于TCP状态从SYN_SENT状态变为SYN_RCVD状态,需要发送了SYN+ACK报文。
  4. 参考同时打开的状态图,SYN+ACK报文将TCP状态从SYN_RCVD变为ESTABLISHED
解决办法:

Muduo解决办法是连接建立好以后,判断下连接两端的IP和端口是不是都一样,一样的话就断开重连。

bool sockets::isSelfConnect(int sockfd)
{
  struct sockaddr_in localaddr = getLocalAddr(sockfd);
  struct sockaddr_in peeraddr = getPeerAddr(sockfd);
  return localaddr.sin_port == peeraddr.sin_port
      && localaddr.sin_addr.s_addr == peeraddr.sin_addr.s_addr;
}

参考连接:
http://blog.coderhuo.tech/2018/05/11/tcp_establish_release/

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值