[muduo网络库]——muduo库Socket类,Acceptor类,Connector类

《Linux多线程服务端编程-使用muduo C++网络库》中给出的简化muduo库,白色是用户可见模块,灰色是用户不可见模块,我们剖析的[Muduo网络库]:三大核心库Channel类、Poller/EpollPoller类以及EventLoop类,我们接下来继续看muduo库中的其他类,这一篇,我们先来介绍灰色的Socket类,Acceptor类,Connector类,这些我们随后剖析其它类的基础
在这里插入图片描述

一 Socket类

1.1 Socket类概述

Socket类实际上就是封装套接字socket fd,请参考游双《Linux高性能服务器编程》第五章

1.2 重要成员变量

  • const int sockfd_:服务器监听套接字

1.3重要成员函数

  • 绑定服务端IP端口 bindAddress(const InetAddress& localaddr)
void Socket::bindAddress(const InetAddress& addr)
{
  sockets::bindOrDie(sockfd_, addr.getSockAddr());
}
void sockets::bindOrDie(int sockfd, const struct sockaddr* addr)
{
  int ret = ::bind(sockfd, addr, static_cast<socklen_t>(sizeof(struct sockaddr_in6)));
  if (ret < 0)
  {
    LOG_SYSFATAL << "sockets::bindOrDie";
  }
}

调用底层bind来绑定服务器IP端口

  • 监听listen()
void Socket::listen()
{
  sockets::listenOrDie(sockfd_);
}
void sockets::listenOrDie(int sockfd)
{
  int ret = ::listen(sockfd, SOMAXCONN);
  if (ret < 0)
  {
    LOG_SYSFATAL << "sockets::listenOrDie";
  }
}
  • 接受连接accept()
int Socket::accept(InetAddress* peeraddr)
{
  struct sockaddr_in6 addr;
  memZero(&addr, sizeof addr);
  int connfd = sockets::accept(sockfd_, &addr);
  if (connfd >= 0)
  {
    peeraddr->setSockAddrInet6(addr);//把客户端地址传出去
  }
  return connfd;
}

调用底层accept接受新的客户端的连接请求

  • 关闭写端
void Socket::shutdownWrite()
{
  sockets::shutdownWrite(sockfd_);
}
void sockets::shutdownWrite(int sockfd)
{
  if (::shutdown(sockfd, SHUT_WR) < 0)
  {
    LOG_SYSERR << "sockets::shutdownWrite";
  }
}
  • 调用setsockopt来设置一些socket选项
  void setTcpNoDelay(bool on);
  void setReuseAddr(bool on);
  void setReusePort(bool on);
  void setKeepAlive(bool on);
  1. setTcpNoDelay禁用 Nagle 算法,避免连续发包出现延迟,这对编写低延迟网络服务很重要
  2. SOL_SOCKET 是协议级别,表示我们正在设置套接字级的选。SO_REUSEADDR 设置了一个套接字的地址复用选项。用于允许套接字在关闭后立即重新使用其地址。这在进行服务器编程时特别有用,因为在服务器重启或关闭后,它通常需要立即重新绑定到相同的地址和端口。
  3. SO_REUSEPORT 设置了一个套接字的端口复用选项。用于允许多个套接字绑定到同一个端口上。
  4. SO_KEEPALIVE 用于启用或禁用 “Keep-Alive” 功能。启用这个功能后,如果套接字在一段时间内没有活动,操作系统会发送一个数据包来检查连接是否仍然有效。

二 Acceptor类

2.1 Acceptor类概述

用于接受TCP连接, 它是TcpServer的成员, 生命期由后者控制。Acceptor类用于创建套接字socket,接受新客户端连接并分发连接给SubReactor(SubEventLoop)

2.2 重要成员变量

  • EventLoop* loop_:监听套接字的fd由哪个EventLoop负责循环监听以及处理相应事件,其实这个EventLoop就是main EventLoop
  • Socket acceptSocket_:服务器监听套接字的文件描述符
  • Channel acceptChannel_:把acceptSocket_及其感兴趣事件和事件对应的处理函数进行封装
  • newConnectionCallback_:非常重要的成员函数,在TcpServer构造函数中通过acceptor_->setNewConnetionCallback(std::bind(&TcpServer::newConnection, this, std::placeholders::_1,std::placeholders::_2));TcpServer::newConnection函数注册给了这个成员变量。这个 TcpServer::newConnection函数的功能是通过轮询EventLoop *ioLoop = threadPool_->getNextLoop();选择一个subEventLoop,并把已经接受的连接分发给这个subEventLoop。
  • bool listening_:是一个标志位
  • int idleFd_:一个空的文件描述符

2.3 重要成员函数

  • 构造函数
Acceptor::Acceptor(EventLoop* loop, const InetAddress& listenAddr, bool reuseport)
  : loop_(loop),
    acceptSocket_(sockets::createNonblockingOrDie(listenAddr.family())),//创建socket
    acceptChannel_(loop, acceptSocket_.fd()),//封装成channel
    listening_(false),
    idleFd_(::open("/dev/null", O_RDONLY | O_CLOEXEC))
{
  assert(idleFd_ >= 0);
  acceptSocket_.setReuseAddr(true); //更改TCP选项
  acceptSocket_.setReusePort(reuseport);
  acceptSocket_.bindAddress(listenAddr); //绑定套接字
  acceptChannel_.setReadCallback(
      std::bind(&Acceptor::handleRead, this));//设置回调函数
}
  1. 要知道AcceptorTcpServer管理的,在构造函数中,首先是TcpServer给的loop,这里实际就是mainloop
  2. 通过createNonblockingOrDie创建 acceptSocket_套接字,一个服务器只创建一个监听socket文件描述符,它在该服务器的生命周期内一直存在,而内核为由每个服务器进程接受的客户端连接自动创建一个已连接socket描述符在accept()函数中,当服务器完成了对某个客户的服务,相应的已连接的socket描述符就被关闭
  3. 将loop和socket封装成Channel
  4. 给listenning_一个初始化
  5. 通过调用Socket中的setReuseAddr、setReusePort更改了一些选项。
  6. 绑定了套接字
  7. 绑定一个回调,用于新用户的连接
  • listen()函数
void Acceptor::listen()
{
  loop_->assertInLoopThread();
  listening_ = true;
  acceptSocket_.listen();
  acceptChannel_.enableReading();
}

实际上调用了Sockert的listen()函数,本质上调用了内核系统的listen(),同时将acceptChannel及其感兴趣事件(可读事件)注册到main EventLoop的Epollpoll中,总结就是让mainloop事件监听器去监听acceptSocket_

  • headleRead()函数
void Acceptor::handleRead()
{
  loop_->assertInLoopThread();
  InetAddress peerAddr;
  //FIXME loop until no more
  int connfd = acceptSocket_.accept(&peerAddr);
  if (connfd >= 0)
  {
    // string hostport = peerAddr.toIpPort();
    // LOG_TRACE << "Accepts of " << hostport;
    if (newConnectionCallback_)
    {
      newConnectionCallback_(connfd, peerAddr);
    }
    else
    {
      sockets::close(connfd);
    }
  }
  else
  {
    LOG_SYSERR << "in Acceptor::handleRead";
    // Read the section named "The special problem of
    // accept()ing when you can't" in libev's doc.
    // By Marc Lehmann, author of libev.
    if (errno == EMFILE)
    {
      ::close(idleFd_);
      idleFd_ = ::accept(acceptSocket_.fd(), NULL, NULL);
      ::close(idleFd_);
      idleFd_ = ::open("/dev/null", O_RDONLY | O_CLOEXEC);
    }
  }
}

建立连接,通过调用Socket的accept函数,底层调用系统的accept函数,返回一个已连接的socket描述字,这样连接就建立起来了,同时内部调用成员变量newConnectionCallback_保存的函数,当mainLoop监听到acceptChannel_上发生了可读事件时(新用户连接事件),就是调用这个handleRead( )方法,内部调用newConnetionCallback_,也就是TcpServer设置的一个回调函数setNewConnetionCallback,绑定了TcpServer::newConnection,通过 轮询算法,选择一个subloop,分发当前的新客户端的Channel,并且绑定了一些回调

需要注意的是:在muduo库的源码中,实际上还创建了一个空的文件描述符idleFd_

这个思想也是很巧妙的,在调用accept的过程中,如果已用文件描述符过多,accept会返回-1,构造函数中注册的idleFd_就派上用场了。当前文件描述符过多,无法接收新的连接。但是由于我们采用LT模式,如果无法接收,可读事件会一直触发。那么在这个地方的处理机制就是,关掉之前创建的空的idleFd_,然后去accept,这样进行了连接,让这个事件不会一直触发,然后再关掉该文件描述符,重新将它设置为空文件描述符。这样就优雅的解决 EMFIFE 问题

三 Connector类

3.1 Connector类概述

用于发起TCP连接, 它是TcpClient的成员, 生命期由后者控制。它负责主动发起连接,不负责创建socket,只负责连接的建立,和Acceptor类恰好相反。Connector具有重连的功能和停止连接的功能,连接成功建立后返回到TcpClient

3.2 重要成员变量

  • EventLoop *loop_:所属的EventLoop
  • InetAddress serverAddr_: 服务器端的地址
  • States state_:客户端的状态 ,有三种状态
 enum States
      {
        kDisconnected,  //无连接
        kConnecting,    //请求连接
        kConnected       //已连接
      };
  • std::unique_ptr<Channel> channel_:Connector所对应的Channel
  • newConnectionCallback_: 连接成功回调函数
  • int retryDelayMs_:重连间隔时间

与Acceptor 相比少了一个 acceptSocket_ 成员,因为Connector 是创建一个新的sockfd 并connect 它,创建过程如下:Connector::start()–>Connector::startInLoop()–>void Connector::connect()。

3.3 重要成员函数

  • 外部接口函数start()
void Connector::start()
{
  connect_ = true;
  loop_->runInLoop(std::bind(&Connector::startInLoop, this)); // FIXME: unsafe
}
  • startInloop()函数
void Connector::startInLoop()
{
  loop_->assertInLoopThread();
  assert(state_ == kDisconnected);
  if (connect_)//调用前必须connect_为true,start()函数中会这么做
  {
    connect();
  }
  else
  {
    LOG_DEBUG << "do not connect";
  }
}
  • 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_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;
  }
}

这个函数虽然很长,但实际上就是设置套接字为非阻塞,然后底层调用socket的conenct()函数,进而判断errno采取相应的操作

  • errnoEINPROGRESSEINTREISCONN
//如果连接成功
void Connector::connecting(int sockfd)
{
  setState(kConnecting);
  assert(!channel_);
  //Channel与sockfd关联
  channel_.reset(new Channel(loop_, sockfd));
  //设置可写回调函数,这时候如果socket没有错误,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();
}
  • errnoEAGAIN等错误码
  • 说明连接暂时失败,但是仍可能成功,需要重连。调用retry()函数
//重连函数,采用back-off策略重连,也就是退避策略
//也就是重连时间逐渐延长,0.5s,1s,2s,...一直到30s
void Connector::retry(int sockfd)
{
  sockets::close(sockfd);   //先关闭连接
  setState(kDisconnected);  
  if (connect_)
  {
    LOG_INFO << "Connector::retry - Retry connecting to " << serverAddr_.toIpPort()
             << " in " << retryDelayMs_ << " milliseconds. ";
    //隔一段时间后重连,重新启用startInLoop
    loop_->runAfter(retryDelayMs_/1000.0,
                    boost::bind(&Connector::startInLoop, shared_from_this()));
    //间隔时间2倍增长
    retryDelayMs_ = std::min(retryDelayMs_ * 2, kMaxRetryDelayMs);
  }
  else   //超出最大重连时间后,输出连接失败
  {
    LOG_DEBUG << "do not connect";
  }
}
  • 彻底失败,返回errno为EACCES等错误码
    这种情况只能关掉sockfd,因为再怎么试也成功不了的。
 sockets::close(sockfd);
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值