muduo源码分析之实现TCP网络库(连接的接收和关闭)

EventLoopChannelPoller三个类中完成对一般描述符、事件循环(poll)的封装。实现了Reactor的基本功能,接下来则需要将网络套接字描述符、I/O函数、等进行封装。

1.传统的TcpServer

在进行封装之前需要明确我们需要封装的内容有哪些?复习最简单的TcpServer大概需要经历如下步骤:

listenfd= socket(AF_INET,SOCK_STREAM,0);
strcut sockaddr_in servaddr;
bind(listenfd,(SA*)&servaddr,sizeof(servaddr));
listen(listenfd,LISTENQ);
struct pollfd fds[1024];
fds[0].fd=listenfd;
fds[0].events=POLLIN;
while(1)
{
    ret=poll(fds,fds.size(),timeout);
    //handle_event
    if(fds[0].revents&POLLIN)
    {
    //new connection
        connectedfd = accept(listenfd,clienAddr,sizeof(clienAddr));
    //add connectedfd into POLL
    }
}

这是一个比较典型的reactor TcpServer,poll调用的部分已经封装,目前来看至少需要封装的内容有:
1.网络套接字描述符fd
2.描述地址结构体strcut sockaddr_in
3.socket相关的系统调用

2.socket操作封装

Endian.h

封装了字节序转换函数(全局函数,位于muduo::net::sockets名称空间中)。

SocketsOps.h/ SocketsOps.cc

封装了socket相关系统调用(全局函数,位于muduo::net::sockets名称空间中)。

Socket.h/Socket.cc(Socket类)

用RAII方法封装socket file descriptor,包含操作:listenbindaccept这些操作将调用上述封装的内容。

InetAddress.h/InetAddress.cc(InetAddress类)

网际地址sockaddr_in封装

3.使用Acceptor类封装监听描述符

这个类用于处理accept调用,事实上是对监听套接字Listenfd的封装。

Acceptor::Acceptor(EventLoop* loop, const InetAddress& listenAddr)
  : loop_(loop),
    acceptSocket_(sockets::createNonblockingOrDie()),//create socket
    acceptChannel_(loop, acceptSocket_.fd()),//Channel construct
    listenning_(false)
{
  acceptSocket_.setReuseAddr(true);//不用等待Time_Wait状态结束
  acceptSocket_.bindAddress(listenAddr);//bind
  acceptChannel_.setReadCallback(
      boost::bind(&Acceptor::handleRead, this));
}

从Acceptor的构造函数可以看出它包含了Socket对象,也就是说它本质上是一个描述符。
通过Channel类型的对象,设置了当Acceptor所管理的描述符可读时,进行所执行的回调handleRead
handleRead函数:

void Acceptor::handleRead()
{
  loop_->assertInLoopThread();
  InetAddress peerAddr(0);
  //FIXME loop until no more
  int connfd = acceptSocket_.accept(&peerAddr);//accpet
  if (connfd >= 0) {
    if (newConnectionCallback_) {
      newConnectionCallback_(connfd, peerAddr);//用户回调
    } else {
      sockets::close(connfd);
    }
  }
}

当listenfd可读时,说明有新连接,这个时候调用accept创建已连接套接字,并且执行相应的用户回调。
到目前为止,完成了对监听套接字的简单封装,这个封装是不完全的,因为事实上,当有了TcpServer的概念后,监听套接字对于用户来说应该是不可见的。

不过在对TcpServer进行封装之前,可以测试Acceptor的功能:

void newConnection(int sockfd, const muduo::InetAddress& peerAddr)
{
  printf("newConnection(): accepted a new connection from %s\n",
         peerAddr.toHostPort().c_str());
  ::write(sockfd, "How are you?\n", 13);
  muduo::sockets::close(sockfd);
}

int main()
{
  printf("main(): pid = %d\n", getpid());

  muduo::InetAddress listenAddr(9981);

  muduo::EventLoop loop;

  muduo::Acceptor acceptor(&loop, listenAddr);

  acceptor.setNewConnectionCallback(newConnection);

  acceptor.listen();

  loop.loop();
}

4.TcpServer接收新的连接

看了上述最后一个例子我们发现,监听套接字的封装暴露给了用户。所以设计的TcpServer类就需要对Acceptor再封装一层了。

TcpServer::TcpServer(EventLoop* loop, const InetAddress& listenAddr)
  : loop_(CHECK_NOTNULL(loop)),
    name_(listenAddr.toHostPort()),//
    acceptor_(new Acceptor(loop, listenAddr)),//Accept的封装
    started_(false),
    nextConnId_(1)//记录连接数,当有新连接的时候会自增
{
  acceptor_->setNewConnectionCallback(
      boost::bind(&TcpServer::newConnection, this, _1, _2));
}

我们看在之前的Acceptor中,只是在main中设置了用户回调,向已连接套接字write了一段文本,便close掉了,对已连接套接字并没有处理。
现在在Acceptor的回调中设置TcpServer::newConnection

void TcpServer::newConnection(int sockfd, const InetAddress& peerAddr)
{
    //sockfd 是已连接套接字
  loop_->assertInLoopThread();
  char buf[32];
  snprintf(buf, sizeof buf, "#%d", nextConnId_);
  ++nextConnId_;//连接数+1
  std::string connName = name_ + buf;

  LOG_INFO << "TcpServer::newConnection [" << name_
           << "] - new connection [" << connName
           << "] from " << peerAddr.toHostPort();
  //打印特定消息

  InetAddress localAddr(sockets::getLocalAddr(sockfd));

  //TcpConnection类用来管理已连接套接字
  TcpConnectionPtr conn(
      new TcpConnection(loop_, //EventLoop 
      connName,//ConnectionName
      sockfd, //accepted fd
      localAddr,//
      peerAddr));//

  connections_[connName] = conn;//map
  conn->setConnectionCallback(connectionCallback_);//设置用户回调
  conn->setMessageCallback(messageCallback_);//设置用户回调
  conn->connectEstablished();//在这个函数中,会执行onConnection的用户回调
}

在这个回调中,创建了一个关键的对象,TcpConnection,并用shared_ptr管理,该Class用来管理已连接套接字。

TcpConnection::TcpConnection(EventLoop* loop,
                             const std::string& nameArg,
                             int sockfd,
                             const InetAddress& localAddr,
                             const InetAddress& peerAddr)
  : loop_(CHECK_NOTNULL(loop)),//EventLoop
    name_(nameArg),//
    state_(kConnecting),
    socket_(new Socket(sockfd)),//已连接套接字
    channel_(new Channel(loop, sockfd)),//Channel
    localAddr_(localAddr),//
    peerAddr_(peerAddr)//
{
  LOG_DEBUG << "TcpConnection::ctor[" <<  name_ << "] at " << this
            << " fd=" << sockfd;
  channel_->setReadCallback(
      boost::bind(&TcpConnection::handleRead, this));//当已连接套接字可读时
}

当已连接套接字可读将回调TcpConnection::handleRead

void TcpConnection::handleRead()
{
  char buf[65536];
  ssize_t n = ::read(channel_->fd(), buf, sizeof buf);//read
  messageCallback_(shared_from_this(), buf, n);//onMessage用户回调
  // FIXME: close connection if n == 0
}

busy loop事件

对于运行s05/test8.cc它是一个discard服务,当客户端关闭连接,将进入busy loop,原因在与当客户端断开连接时,客户端发送一个FIN段,服务端返回一个ACK,当服务端TCP收到FIN时,read就返回0(表示EOF),而此时这种状态总是readable,将会不停触发poll返回,就出现busy loop事件了。

5.TcpConnection关闭连接

关闭连接相对于建立连接要麻烦,因为需要考虑TcpConnection的生命周期。

监听套接字可读事件是POLLIN; 已连接套接字正常可读是POLLIN; 正常可写是POLLOUT; 对等方close/shutdown关闭连接,已连接套接字可读是POLLIN | POLLHUP;

在TcpConnection 构造函数中再添加:

// 连接关闭,回调TcpConnection::handleClose
channel_->setCloseCallback(
    boost::bind(&TcpConnection::handleClose, this));
// 发生错误,回调TcpConnection::handleError
channel_->setErrorCallback(
    boost::bind(&TcpConnection::handleError, this));

在 TcpServer::newConnection() 中再添加:

void TcpServer::newConnection(int sockfd, const InetAddress &peerAddr)
{
    .....
    conn->setCloseCallback(
        boost::bind(&TcpServer::removeConnection, this, _1));
}

在TcpConnection::handleRead() 中再添加:

void TcpConnection::handleRead()
{
  char buf[65536];
  ssize_t n = ::read(channel_->fd(), buf, sizeof buf);
  if (n > 0) {
    messageCallback_(shared_from_this(), buf, n);
  } else if (n == 0) {
    handleClose();
  } else {
    handleError();
  }
}

在来看看TcpConnection::handleClose()函数

void TcpConnection::handleClose()
{
  loop_->assertInLoopThread();
  LOG_TRACE << "TcpConnection::handleClose state = " << state_;
  assert(state_ == kConnected);
  // we don't close fd, leave it to dtor, so we can find leaks easily.
  channel_->disableAll();
  // must be the last line
  closeCallback_(shared_from_this());
}

回调来回调去的有点晕,整理一下整个过程。
当已连接套接字发生可读事件,poll返回,将调用调用Channel::handleEvent()处理活动通道,调用TcpConnection::handleRead(),::read() 返回0,进而调用TcpConnection::handleClose()

handleClose()函数中,调用TcpConnection的closeCallback_,这个回调函数是在TcpServer里面设定:

conn->setCloseCallback(
        boost::bind(&TcpServer::removeConnection, this, _1));

进而调用TcpServer::removeConnection

void TcpServer::removeConnection(const TcpConnectionPtr& conn)
{
  loop_->assertInLoopThread();
  LOG_INFO << "TcpServer::removeConnection [" << name_
           << "] - connection " << conn->name();
  size_t n = connections_.erase(conn->name());
  assert(n == 1); (void)n;
  loop_->queueInLoop(
      boost::bind(&TcpConnection::connectDestroyed, conn));
}

最后将调用TcpConnection::connectDestroy

void TcpConnection::connectDestroyed()
{
  loop_->assertInLoopThread();
  assert(state_ == kConnected);
  setState(kDisconnected);
  channel_->disableAll();
  connectionCallback_(shared_from_this());//回调用户onConnection Callback

  loop_->removeChannel(get_pointer(channel_));//Poll不再关注此通道
}

6.参考

1.Linux多线程服务端编程 使用muduo C++网络库
2.http://blog.csdn.net/jnu_simba/article/details/14556387
3.unp1

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值
>