muduo库中TcpServer一次完整的工作流程

模拟单线程情况下muduo库的工作情况

muduo的源代码对于一个初学者来说还是有一些复杂的,其中有很多的回调函数以及交叉的组件,下面我将追踪一次TCP连接过程中发生的事情,不会出现用户态的源码,都是库内部的运行机制。下文笔者将描述一次连接发生的过程,将Channel到加入到loop循环为止。

监听套接字加入loop循环的完整过程

  • 首先创建一个TcpServer对象,在的创建过程中,首先new出来自己的核心组件(Acceptor,loop,connectionMap,threadPool)之后TcpServer会向Acceptor注册一个新连接到来时的Connection回调函数。loop是由用户提供的,并且在最后向Acceptor注册一个回调对象,用于处理:一个新的Client连接到来时该怎么处理。

    TcpServer向Acceptor注册的回调代码主要作用是:当一个新的连接到来时,根据Acceptor创建的可连接描述符和客户的地址,创建一个Connection对象,并且将这个对象加入到TcpServer的ConnectionMap中,由TcpServer来管理上述新建con对象。但是现在监听套接字的事件分发对象Channel还没有加入loop,就先不多提这个新的连接到到来时的处理过程。

    ```

    TcpServer::TcpServer(EventLoop* loop,const InetAddress& listenAddr,const string& nameArg,Option option)

    : loop_(CHECK_NOTNULL(loop)),

    ipPort_(listenAddr.toIpPort()),name_(nameArg),acceptor_(new Acceptor(loop, listenAddr, option == kReusePort)),

    threadPool_(new EventLoopThreadPool(loop, name_)),

    connectionCallback_(defaultConnectionCallback),

    messageCallback_(defaultMessageCallback),

    nextConnId_(1)

    {//上面的loop是用户提供的loop

    acceptor_->setNewConnectionCallback(

    boost::bind(&TcpServer::newConnection, this, _1, _2));//注册给acceptor的回调

    }//将在Acceptor接受新连接的时候

void TcpServer::newConnection(int sockfd, const InetAddress& peerAddr)

{//将本函数注册个acceptor

loop_->assertInLoopThread();//断言是否在IO线程

EventLoop* ioLoop = threadPool_->getNextLoop();//获得线程池中的一个loop

char buf[64];//获得线程池map中的string索引

snprintf(buf, sizeof buf, "-%s#%d", ipPort_.c_str(), nextConnId_);

++nextConnId_;

string connName = name_ + buf;

LOG_INFO << "TcpServer::newConnection [" << name_

<< "] - new connection [" << connName

<< "] from " << peerAddr.toIpPort();

InetAddress localAddr(sockets::getLocalAddr(sockfd));//获得本地的地址,用于构建Connection

// FIXME poll with zero timeout to double confirm the new connection

// FIXME use make_shared if necessary

TcpConnectionPtr conn(new TcpConnection(ioLoop,

connName,

sockfd,

localAddr,

peerAddr));//构建了一个connection

connections_[connName] = conn;//将新构建的con加入server的map中

conn->setConnectionCallback(connectionCallback_);//muduo默认的

conn->setMessageCallback(messageCallback_);//moduo默认的

conn->setWriteCompleteCallback(writeCompleteCallback_);//??

conn->setCloseCallback(

boost::bind(&TcpServer::removeConnection, this, _1)); // FIXME: unsafe

ioLoop->runInLoop(boost::bind(&TcpConnection::connectEstablished, conn));//在某个线程池的loop中加入这个con

}

- 下面接着讲述在TcpServer的构造过程中发生的事情:创建Acceptor对象。TcpServer用unique_ptr持有唯一的指向Acceptor的指针。Acceptor的构造函数完成了一些常见的选项。最后的一个向Acceptor->Channel注册一个回调函数,用于处理:listening可读时(新的连接到来),该怎么办?答案是:当新的连接到来时,创建一个已连接描述符,然后调用TcpServe注册给Acceptor的回调函数,用于处理新的连接。

Acceptor::Acceptor(EventLoop* loop, const InetAddress& listenAddr, bool reuseport)

: loop_(loop),

acceptSocket_(sockets::createNonblockingOrDie(listenAddr.family())),

acceptChannel_(loop, acceptSocket_.fd()),

listenning_(false),

idleFd_(::open("/dev/null", O_RDONLY | O_CLOEXEC))

{

assert(idleFd_ >= 0);

acceptSocket_.setReuseAddr(true);

acceptSocket_.setReusePort(reuseport);

acceptSocket_.bindAddress(listenAddr);

acceptChannel_.setReadCallback(

boost::bind(&Acceptor::handleRead, this));//Channel设置回调,当sockfd可读时掉用设置的回调

}

void Acceptor::handleRead()

{

loop_->assertInLoopThread();//判断是否在IO线程

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);//TcpServer注册的,创建新的con,并且加入TcpServer的ConnectionMap中。

}

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);

}

}

}

- 在上述Acceptor对象的创建过程中,Acceptor会创建一个用于处理监听套接字事件的Channel对象,以下Acceptor的Channel对象的创造过程,很常规的处理过程。

Channel::Channel(EventLoop* loop, int fd_)

: loop(loop),

fd_(fd_),

events(0),

revents_(0),

index_(-1),

logHup_(true),

tied_(false),

eventHandling_(false),

addedToLoop_(false)

{

}

- 到此,在muduo库内部的初始化过程已经基本处理完毕,然后由用户调用TcpServer的setThreadNum()和start()函数。在start()函数中会将打开Acceptor对象linten套接字。

void TcpServer::setThreadNum(int numThreads)

{//设置线程池的开始数目

assert(0 <= numThreads);

threadPool_->setThreadNum(numThreads);

}

void TcpServer::start()

{//TcpServer开始工作

if (started_.getAndSet(1) == 0)//获得原子计数

{

threadPool_->start(threadInitCallback_);//线程池开始工作

assert(!acceptor_->listenning());//打开accepor的监听状态
loop_->runInLoop(
    boost::bind(&Acceptor::listen, get_pointer(acceptor_)));//打开acceptor的listening

}

}

- 打开Acceptor对象的listenfd的详细过程。

void Acceptor::listen()

{

loop_->assertInLoopThread();//判断是否在IO线程

listenning_ = true;//进入监听模式

acceptSocket_.listen();

acceptChannel_.enableReading();//让监听字的channel关注可读事件

}

- 接着使用了Channel对象中的的enableReading()函数,让这个Channel对象关注可读事件。关键在于更新过程,应该是这个流程中最重要的操作。

void enableReading() { events_ |= kReadEvent; update(); }//将关注的事件变为可读,然后更新

- 使用了Channel的更新函数:update()

void Channel::update()

{

addedToLoop_ = true;//更新channel的状态

loop_->updateChannel(this);//调用POLLER的更新功能

}

- EventLoop持有唯一的Poller,也就是说,这个Poller将负责最后的更新过程。如果是新的Channel对象,则在Poller的pollfd数组中增加席位;如果不是新的Channel对象,则更新它目前所发生的事件(将目前发生的事件设置为0)。

void EventLoop::updateChannel(Channel* channel)

{

assert(channel->ownerLoop() == this);//判断channel的LOOP是否是当前的LOOP

assertInLoopThread();//判断是否在IO线程

poller_->updateChannel(channel);//使用POLLER来更新channel

}

- 紧接着使用了Poller的updateChannel函数

void PollPoller::updateChannel(Channel* channel)

{//将channel关注的事件与pollfd同步

Poller::assertInLoopThread();//如果不再loop线程直接退出

LOG_TRACE << "fd = " << channel->fd() << " events = " << channel->events();

if (channel->index() < 0)//获得channel在map中的位置

{

// a new one, add to pollfds_

assert(channels_.find(channel->fd()) == channels_.end());

struct pollfd pfd;//新建一个pfd与channel相关联

pfd.fd = channel->fd();

pfd.events = static_cast

- 至此,调用EventLoop的loop函数,进行loop循环,开始处理事件。

void EventLoop::loop()

{

assert(!looping_);//判断是否在LOOPING

assertInLoopThread();//判断这个函数在LOOP线程调用

looping_ = true;//进入LOOPING状态

quit_ = false; // FIXME: what if someone calls quit() before loop() ?

LOG_TRACE << "EventLoop " << this << " start looping";

while (!quit_)

{

activeChannels_.clear();//将活动线程队列置空

pollReturnTime_ = poller_->poll(kPollTimeMs, &activeChannels_);//获得活动文件描述符的数量,并且获得活动的channel队列

++iteration_;//增加Poll次数

if (Logger::logLevel() <= Logger::TRACE)

{

printActiveChannels();

}

// TODO sort channel by priority

eventHandling_ = true;//事件处理状态

for (ChannelList::iterator it = activeChannels_.begin();

it != activeChannels_.end(); ++it)

{

currentActiveChannel_ = *it;//获得当前活动的事件

currentActiveChannel_->handleEvent(pollReturnTime_);//处理事件,传递一个poll的阻塞时间

}

currentActiveChannel_ = NULL;//将当前活动事件置为空

eventHandling_ = false;//退出事件处理状态

doPendingFunctors();//处理用户在其他线程注册给IO线程的事件

}

LOG_TRACE << "EventLoop " << this << " stop looping";

looping_ = false;//推出LOOPING状态

}

一个监听套接字已经进入循环,如果此时一个新的连接到来又会发生什么事情呢?
###一个新连接到达时的处理过程。
- 此时在loop循环中的监听套接字变得可读,然后便调用一个可读事件的处理对象。首先调用Acceptor注册的handleRead对象,完成连接套接字的创建,其次在handleRead对象的内部调用TcpServer注册给Acceptor的函数对象,用于将新建con对象加入TcpServer的ConnectionMap中去。 

void Channel::handleEvent(Timestamp receiveTime)

{

boost::shared_ptr

void Channel::handleEventWithGuard(Timestamp receiveTime)

{//真正的处理各种事件

eventHandling_ = true;//处理事件状态

LOG_TRACE << reventsToString();

if ((revents_ & POLLHUP) && !(revents_ & POLLIN))

{

if (logHup_)

{

LOG_WARN << "fd = " << fd_ << " Channel::handle_event() POLLHUP";

}

if (closeCallback_) closeCallback_();

}

if (revents_ & POLLNVAL)

{

LOG_WARN << "fd = " << fd_ << " Channel::handle_event() POLLNVAL";

}

if (revents_ & (POLLERR | POLLNVAL))

{

if (errorCallback_) errorCallback_();

}

if (revents_ & (POLLIN | POLLPRI | POLLRDHUP))

{

if (readCallback_) readCallback_(receiveTime);

}

if (revents_ & POLLOUT)

{

if (writeCallback_) writeCallback_();

}

eventHandling_ = false;

}

- 此时,监听套接字处理的时可读事件,调用之前由Acceptor注册的handleRead回调函数

void Acceptor::handleRead()

{

loop_->assertInLoopThread();//判断是否在IO线程

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);

}

}

}

- 在上述函数中又调用,由TcpServer注册给Acceptor的回调函数

void TcpServer::newConnection(int sockfd, const InetAddress& peerAddr)

{//将本函数注册个acceptor

loop_->assertInLoopThread();//断言是否在IO线程

EventLoop* ioLoop = threadPool_->getNextLoop();//获得线程池中的一个loop

char buf[64];//获得线程池map中的string索引

snprintf(buf, sizeof buf, "-%s#%d", ipPort_.c_str(), nextConnId_);

++nextConnId_;

string connName = name_ + buf;

LOG_INFO << "TcpServer::newConnection [" << name_

<< "] - new connection [" << connName

<< "] from " << peerAddr.toIpPort();

InetAddress localAddr(sockets::getLocalAddr(sockfd));//获得本地的地址,用于构建Connection

// FIXME poll with zero timeout to double confirm the new connection

// FIXME use make_shared if necessary

TcpConnectionPtr conn(new TcpConnection(ioLoop,

connName,

sockfd,

localAddr,

peerAddr));//构建了一个connection

connections_[connName] = conn;//将新构建的con加入server的map中

conn->setConnectionCallback(connectionCallback_);//muduo默认的

conn->setMessageCallback(messageCallback_);//moduo默认的

conn->setWriteCompleteCallback(writeCompleteCallback_);//??

conn->setCloseCallback(

boost::bind(&TcpServer::removeConnection, this, _1)); // FIXME: unsafe

ioLoop->runInLoop(boost::bind(&TcpConnection::connectEstablished, conn));//在某个线程池的loop中加入这个con

}

- 上述对象的最后一行,是调用新建的TcpConnection对象的函数,用设置新建的con对象中的channel的关注事件。

void TcpConnection::connectEstablished()

{//建立连接

loop_->assertInLoopThread();//断言是否在IO线程

assert(state_ == kConnecting);//正处于连接建立过程

setState(kConnected);

channel_->tie(shared_from_this());//使channel的tie的指向不为空

channel_->enableReading();//将connection设置为可读的

connectionCallback_(shared_from_this());//用户提供的回调函数,muduo有提供默认的

}

- 至此以后的过程与将listen->channel添加到loop中的过程一样。

void enableReading() { events_ |= kReadEvent; update(); }//将关注的事件变为可读,然后更新

- 使用了Channel的更新函数:update()

void Channel::update()

{

addedToLoop_ = true;//更新channel的状态

loop_->updateChannel(this);//调用POLLER的更新功能

}

- 使用了EventLoop的updateChannel()功能

void EventLoop::updateChannel(Channel* channel)

{

assert(channel->ownerLoop() == this);//判断channel的LOOP是否是当前的LOOP

assertInLoopThread();//判断是否在IO线程

poller_->updateChannel(channel);//使用POLLER来更新channel

}

- 在poller中更新channel

void PollPoller::updateChannel(Channel* channel)

{//将channel关注的事件与pollfd同步

Poller::assertInLoopThread();//如果不再loop线程直接退出

LOG_TRACE << "fd = " << channel->fd() << " events = " << channel->events();

if (channel->index() < 0)//获得channel在map中的位置

{

// a new one, add to pollfds_

assert(channels_.find(channel->fd()) == channels_.end());

struct pollfd pfd;//新建一个pfd与channel相关联

pfd.fd = channel->fd();

pfd.events = static_cast

最后一个连接的channel加入loop循环,新的循环已经开始了。
##模拟单线程情况下muduo库的工作情况
在上篇中,笔者追踪了Connetfd(连接套接字)和Listenfd(监听套接字)的Channel对象加入到loop循环的过程。其中包括了网络连接过程中,muduo会创建的对象。本文将会追踪Connetfd(连接套接字)和Listenfd(监听套接字)从loop循环退出并且销毁,一直到main函数终止的过程。
###连接套接字正常情况下完整的销毁情况(read == 0)
由TcpConnection对象向自己所拥有的Channel对象注册的可读事件结束时,会出现` read == 0 `的情况,此时会直接调用TcpConnection对象的handleClose函数。因为在向Channel对象注册可读事件时,使用了如下的语句:

channel_->setReadCallback(&TcpConnection::handleRead,this);

**this**使得Channel对象可以直接在TcpConnection向它注册的handleClose函数内部使用TcpConnetion的函数。

void TcpConnection::handleRead(Timestamp receiveTime)

{//都是向channel注册的函数

loop_->assertInLoopThread();//断言在loop线程

int savedErrno = 0;//在读取数据之后调用用户提供的回调函数

ssize_t n = inputBuffer_.readFd(channel_->fd(), &savedErrno);

if (n > 0)

{//这个应该时用户提供的处理信息的回调函数

messageCallback_(shared_from_this(), &inputBuffer_, receiveTime);

}

else if (n == 0)

{//读到了0,直接关闭

handleClose();

}

else

{//如果有错误

errno = savedErrno;

LOG_SYSERR << "TcpConnection::handleRead";

handleError();//处理关闭

}

}

void TcpConnection::handleClose()

{//处理关闭事件

loop_->assertInLoopThread();//断言是否在loop线程

LOG_TRACE << "fd = " << channel_->fd() << " state = " << stateToString();

assert(state_ == kConnected || state_ == kDisconnecting);

// we don‘t close fd, leave it to dtor, so we can find leaks easily.

setState(kDisconnected);//设置关闭状态

channel_->disableAll();//不再关注任何事情

TcpConnectionPtr guardThis(shared_from_this());//获得shared_ptr交由tcpsever处理

connectionCallback_(guardThis);//这他妈就是记录一点日志

// must be the last line

closeCallback_(guardThis);

}

在以上的handleClose代码中,首先会设置TcpConnection对象的关闭状态,其次让自己Channel对象不再关注任何事情。
因为TcpConnection在创建时使用了如下语句:

class TcpConnection : boost::noncopyable, public boost::enable_shared_from_this

便可以使用shared_fron_this()获得指向本TcpConnection对象的shared_ptr指针,然后在后续的过程中,对指向本对象的
shared_ptr进行操作,则可以安全的将本对象从其他依赖类中安全的移除。
继续跟踪上述的最后一句,且closeCallback是由TcpServer在创建TcpConnection对象时向它注册的:

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

目的在于在TcpServer的TcpconnectionMap中移除指向指向这个TcpConnection对象的指针。

void TcpServer::removeConnection(const TcpConnectionPtr& conn)

{

// FIXME: unsafe

loop_->runInLoop(boost::bind(&TcpServer::removeConnectionInLoop, this, conn));//注册到loop线程中移除这个con

}

void TcpServer::removeConnectionInLoop(const TcpConnectionPtr& conn)

{

loop_->assertInLoopThread();//断言是否在IO线程

LOG_INFO << "TcpServer::removeConnectionInLoop [" << name_

<< "] - connection " << conn->name();

size_t n = connections_.erase(conn->name());//删除该con

(void)n;

assert(n == 1);

EventLoop* ioLoop = conn->getLoop();//获得线程Loop

ioLoop->queueInLoop(

boost::bind(&TcpConnection::connectDestroyed, conn));//将线程销毁动作添加到loop中去

}

目前的步骤还在于处理TcpConnection对象。

void TcpConnection::connectDestroyed()

{//销毁连接

loop_->assertInLoopThread();//断言是否在loop线程

if (state_ == kConnected)//如果此时处于连接状态

{

setState(kDisconnected);//将状态设置为不可连接状态

channel_->disableAll();//channel不再关注任何事件

connectionCallback_(shared_from_this());//记录作用,好坑的一个作用

}

channel_->remove();//在poller中移除channel

}

TcpConnection对象的声明周期随着将Channel对象移除出loop循环而结束。

void Channel::remove()

{//将channel从loop中移除

assert(isNoneEvent());//判断此时的channel是否没有事件发生

addedToLoop_ = false;//此时没有loop拥有此channel

loop_->removeChannel(this);//调用POLLER的删除功能

}

因为EventLoop对象中的poller对象也持有Channel对象的指针,所以需要将channel对象安全的从poller对象中移除。

void EventLoop::removeChannel(Channel* channel)

{//每次间接的调用的作用就是将需要改动的东西与当前调用的类撇清关系

assert(channel->ownerLoop() == this);

assertInLoopThread();//如果没有在loop线程调用直接退出

if (eventHandling_)//判断是否在事件处理状态。判断当前是否在处理这个将要删除的事件以及活动的事件表中是否有这个事件

{

assert(currentActiveChannel_ == channel ||

std::find(activeChannels_.begin(), activeChannels_.end(), channel) == activeChannels_.end());

}

poller_->removeChannel(channel);//在POLLER中删除这个事件分发表

}

以下时poller对象移除Channel对象的具体操作步骤。

void PollPoller::removeChannel(Channel* channel)

{

Poller::assertInLoopThread();//判断是否在IO线程

LOG_TRACE << "fd = " << channel->fd();

assert(channels_.find(channel->fd()) != channels_.end());

assert(channels_[channel->fd()] == channel);

assert(channel->isNoneEvent());

int idx = channel->index();//获得pfd位置的索引

assert(0 <= idx && idx < static_cast

以上便是一个连接的销毁过程,现在依然让人迷惑的时Channel对象到底被谁持有过?以及TcpConnection对象的生命周期到底在什么时候结束?
###Channel与TcpConnection对象的创建与销毁
####创建
下面,在让我们进入上一篇文章,具体的看看Channel对象的生命期到底是个什么样子?
- 当新连接过来时,由TcpServer创建一个TcpConnection对象,这个对象中包括一个与此连接相关的Channel对象。
- 然后紧接着TcpServer使用创建的TcpConnection对象向Loop中注册事件。此时的控制权回到TcpConnection对象手中。它操作自己的Channel对象更新EventLoop。
- 最后由EventLoop对象去操作自己的Poller更新Poller的Channel队列。
在上述过程中,Channel对象的创建操作有这样的顺序:

TcpServer->TcpConnection->Channel->EventLoop->Poller

TcpConnection对象的创建过程相比于Channel简单的多:

TcpServer->TcpConnection

在TcpServer中创建Connection对象,然后让TcpConnection对象去操作自己的Channel对象,将Channel加入到EventLoop中去,最后由EventLoop操作自己的Poller收尾。总而言之,Channel对象在整个过程中只由Poller和TcpConnection对象持有,销毁时也应该是如此过程。
####销毁
由于Channel是TcpConnection对象的一部分,所以Channel的生命周期一定会比TcpConnection的短。
Channel与TcpConnection对象的销毁基本与上述创建过程相同:

TcpConnection->TcpServer->Channel->EventLoop->Poller

```

随着,Channel对象从Poller中移除,TcpConnection的生命周期也随之结束。

TcpConnection对象在整个生命周期中只由TcpServer持有,但是TcpConnetion对象中的Channel又由Poller持有,Poller又是EventLoop的唯一成员,所以造成了如此麻烦的清理与创建过程。那如果能将Channel移出TcpConnection对象,那muduo的创建与清理工作会不会轻松很多?

原文地址:https://www.cnblogs.com/ukernel/p/9191104.html

时间: 2024-11-07 15:52:01

muduo库中TcpServer一次完整的工作流程的相关文章

浅析muduo库中的线程设施

muduo是目前我在学习过程中遇到的最具有学习意义的网络库,下文将分析muduo库中的基础设施--Thread和ThreadPool. 首先,介绍在多线程编程中不可缺少的同步措施--Mutex和Condition. Mutex ``` /Mutex.h/ class MutexLock : boost::noncopyable { public: MutexLock() : holder_(0) { MCHECK(pthread_mutex_init(&mutex_, NULL));//MCHE

浅析muduo库中的定时器设施

一个设计良好的定时器在服务端的应用程序上至关重要,muduo定时器的实现陈硕大牛在书中已经详细的谈过,笔者尝试从源码的角度解读定时器的实现,如果理解不对,欢迎指正. 在muduo的定时器系统中,一共由四个类:Timestamp,Timer,TimeId,TimerQueue组成.其中最关键的是Timer和TimerQueue两个类.此文只解释初读时让人非常迷惑的TimerQueue类,这个类是整个定时器设施的核心,其他三个类简介其作用. 其中Timestamp是一个以int64_t表示的微秒级绝

Storm 中什么是-acker,acker工作流程介绍

概述 我们知道storm一个很重要的特性是它能够保证你发出的每条消息都会被完整处理, 完整处理的意思是指: 一个tuple被完全处理的意思是: 这个tuple以及由这个tuple所导致的所有的tuple都被成功处理.而一个tuple会被认为处理失败了如果这个消息在timeout所指定的时间内没有成功处理. 也就是说对于任何一个spout-tuple以及它的所有子孙到底处理成功失败与否我们都会得到通知.关于如果做到这一点的原理,可以看看Twitter Storm如何保证消息不丢失这篇文章.从那篇文

【关于selenium自动化中,Webdriver的原理以及工作流程】

原文地址:https://www.cnblogs.com/imyalost/p/7242747.html#4109245 作者:老 张 1.关于Webdriver 设计模式:按照Server-Client的经典设计模式设计: Server端:即Remote Server(远程服务器),可以是任意的浏览器,当脚本启动浏览器时,该浏览器就是Remote Server,它的职责是等待Client发送请求并做出响应: Client端:我们的测试代码中的一些行为是以HTTP请求的方式发送给被测试浏览器--

muduo库中的核心:std::bind和std::function

最近在读完陈硕大牛的<Linux多线程服务端编程>以及muduo源码后,对其中的一些实现细节有着十分深刻的印象,尤其是使用std::bind和std::function的回调技术.可以说,这两个大杀器简直就是现代C++的"任督二脉",甚至可以解决继承时的虚函数指代不清的问题.在此详细叙述使用std::bind和std::function在C++对象之间的用法,用以配合解决事件驱动的编程模型.笔者才疏学浅,如果解释的不正确希望朋友们不吝赐教. 下面的所有讨论基于对象. std

SharePoint 2013 文档库中PPT转换PDF

原文:SharePoint 2013 文档库中PPT转换PDF 通过使用 PowerPoint Automation Services,可以从 PowerPoint 二进制文件格式 (.ppt) 和 PowerPoint Open XML 文件格式 (.pptx) 转换为其他格式.例如,您可能需要将一批 PowerPoint 97–2003 文件升级到 Open XML 演示文稿文件.您还可以在“编辑”菜单中创建自定义操作,以允许用户按需创建 PDF 版本的演示文稿. 一.效果演示: 1.新建一

BOOST 库中filesyatem 库的学习

/*FileSyatem 库的学习 ------------------------------------------------------------------------------------------------------------库的使用方式 嵌入源码的形式: #define BOOST_SYSTEN_NO_LIB #define BOOST_FILESYSTEM_NO_LIB #include<boost\filesystem.hpp> ----------------

结构体在固件库中的应用

上次介绍了一般结构体的定义以及引用方法,那么接下来将对结构体在官方固件库是如何具体使用的做出简单说明. 结构体指针成员变量引用方法是通过“→”符号来实现,比如要访问student1结构体指针指向的结构体的成员变量name,那么方法是: stuednt1—>name; 如在STM32官方固件库中对端口使用模式结构体定义如下: typedef enum { GPIO_Mode_AIN = 0x0, //模拟输入模式 GPIO_Mode_IN_FLOATING = 0x04, //浮空输入模式 GPI

CAML获取SharePoint文档库中除文件夹外所有文档

方法一: ? 1 2 3 4 <QueryOptions>         <ViewAttributes Scope="Recursive" />     </QueryOptions> </query> 方法二: ? 1 2 3 4 5 <View Scope="RecursiveAll">     <Query>         <Where>...</Where>