Webserver笔记

三个主要的类EventLoop、Channel、Poller

EventLoop
// 如果当前线程就是创建此EventLoop的线程,就调用callback,否则就放入等待执行函数区
void EventLoop::runInLoop(Functor&& cb) {
  if (isInLoopThread())
    cb();
  else
    queueInLoop(std::move(cb));
}

// 将此函数放入等待执行函数区,如果当前是跨线程or正在调用等待的函数,则唤醒事件循环
void EventLoop::queueInLoop(Functor&& cb) {
  {
    MutexLockGuard lock(mutex_);
    pendingFunctors_.emplace_back(std::move(cb));
  }

  if (!isInLoopThread() || callingPendingFunctors_) wakeup();
}

// 关键函数,首先从poll中取出所有的事件,并依次处理,再执行等待的函数,再处理超时的函数
void EventLoop::loop() {
  assert(!looping_);
  assert(isInLoopThread());
  looping_ = true;
  quit_ = false;
  // LOG_TRACE << "EventLoop " << this << " start looping";
  std::vector<SP_Channel> ret;
  while (!quit_) {
    // cout << "doing" << endl;
    ret.clear();
    ret = poller_->poll();
    eventHandling_ = true;
    for (auto& it : ret) it->handleEvents();
    eventHandling_ = false;
    doPendingFunctors();
    poller_->handleExpired();
  }
  looping_ = false;
}

wakeup函数是手动触发写事件,对应的是wakeupFd_描述符

Channel
 private:
  typedef std::function<void()> CallBack;
  EventLoop *loop_;
  int fd_;
  __uint32_t events_;
  __uint32_t revents_;
  __uint32_t lastEvents_;

  // 方便找到上层持有该Channel的对象
  std::weak_ptr<HttpData> holder_;
  
每个Channel持有一个文件描述符fd,正在监听(感兴趣的)事件,实际发生的事件,以及各个事件的回调函数的Function对象

总的来说,Channel就是对fd事件的封装,包括注册它的事件以及回调。EventLoop通过调用Channel::handleEvent()来执行Channel的读写事件。Channel::handleEvent()的实现也很简单,就是比较以已经发生的事件(由Poller返回),来调用对应的回调函数(读、写、错误)
Poller
class Epoll {
 public:
  Epoll();
  ~Epoll();
  void epoll_add(SP_Channel request, int timeout);
  void epoll_mod(SP_Channel request, int timeout);
  void epoll_del(SP_Channel request);
  std::vector<std::shared_ptr<Channel>> poll();
  std::vector<std::shared_ptr<Channel>> getEventsRequest(int events_num);
  void add_timer(std::shared_ptr<Channel> request_data, int timeout);
  int getEpollFd() { return epollFd_; }
  void handleExpired();

 private:
  static const int MAXFDS = 100000;
  int epollFd_;
  std::vector<epoll_event> events_;
  std::shared_ptr<Channel> fd2chan_[MAXFDS];
  std::shared_ptr<HttpData> fd2http_[MAXFDS];
  TimerManager timerManager_;
};

Poller的主要成员变量就三个
    epollFd_:epoll_create方法返回的epoll句柄
    events_:存放epoll_wait()方法返回的活动事件。是一个vector
    fd2chan_:存放每个Channel

// 分发处理函数
std::vector<SP_Channel> Epoll::getEventsRequest(int events_num) {
  std::vector<SP_Channel> req_data;
  for (int i = 0; i < events_num; ++i) {
    // 获取有事件产生的描述符
    int fd = events_[i].data.fd;

    SP_Channel cur_req = fd2chan_[fd];

    if (cur_req) {
      cur_req->setRevents(events_[i].events);
      cur_req->setEvents(0);
      // 加入线程池之前将Timer和request分离
      // cur_req->seperateTimer();
      req_data.push_back(cur_req);
    } else {
      LOG << "SP cur_req is invalid";
    }
  }
  return req_data;
}

上述函数最关键,其余的函数无非就是epoll add 、mod、del的封装

Server类

Server.cpp

这里处理新连接,感觉应该先把while放在开头。。。while循环结束以后,还得acceptChannel_->setEvents(EPOLLIN | EPOLLET);是因为,使用的是epoll边沿模式,只有事件状态改变了才会有通知!!!所以得重新设置一下,不然再有新连接都不会通知了。

void Server::handNewConn() {
  struct sockaddr_in client_addr;
  memset(&client_addr, 0, sizeof(struct sockaddr_in));
  socklen_t client_addr_len = sizeof(client_addr);
  int accept_fd = 0;
  while ((accept_fd = accept(listenFd_, (struct sockaddr *)&client_addr,
                             &client_addr_len)) > 0) {
    EventLoop *loop = eventLoopThreadPool_->getNextLoop();
    LOG << "New connection from " << inet_ntoa(client_addr.sin_addr) << ":"
        << ntohs(client_addr.sin_port);
    // cout << "new connection" << endl;
    // cout << inet_ntoa(client_addr.sin_addr) << endl;
    // cout << ntohs(client_addr.sin_port) << endl;
    /*
    // TCP的保活机制默认是关闭的
    int optval = 0;
    socklen_t len_optval = 4;
    getsockopt(accept_fd, SOL_SOCKET,  SO_KEEPALIVE, &optval, &len_optval);
    cout << "optval ==" << optval << endl;
    */
    // 限制服务器的最大并发连接数
    if (accept_fd >= MAXFDS) {
      close(accept_fd);
      continue;
    }
    // 设为非阻塞模式
    if (setSocketNonBlocking(accept_fd) < 0) {
      LOG << "Set non block failed!";
      // perror("Set non block failed!");
      return;
    }

    setSocketNodelay(accept_fd);
    // setSocketNoLinger(accept_fd);

    shared_ptr<HttpData> req_info(new HttpData(loop, accept_fd));
    req_info->getChannel()->setHolder(req_info);
    loop->queueInLoop(std::bind(&HttpData::newEvent, req_info));
  }
  acceptChannel_->setEvents(EPOLLIN | EPOLLET);
}

LogStream.h

// 返回data_ char数组的数据末尾地址
const char* end() const { return data_ + sizeof data_; }

Logging.cpp

//定义一个 struct timeval 类型的变量 tv,用于存储当前的时间信息。
//定义一个 time_t 类型的变量 time,用于存储当前的时间戳。
//定义一个字符数组 str_t,用于存储格式化后的时间字符串。
//调用 gettimeofday 函数,获取当前的时间信息,并将其存储在 tv 变量中。
//将 tv 变量中的时间戳赋值给 time 变量。
//调用 localtime 函数,将 time 变量转换为本地时间,并将结果存储在 p_time 变量中。
//调用 strftime 函数,将 p_time 变量中的本地时间格式化为一个字符串,并将其存储在 str_t 数组中。
//将格式化后的时间字符串添加到日志信息中,并将其存储在 stream_ 变量中。
void Logger::Impl::formatTime()
{
    struct timeval tv;
    time_t time;
    char str_t[26] = {0};
    gettimeofday (&tv, NULL);
    time = tv.tv_sec;
    struct tm* p_time = localtime(&time);   
    strftime(str_t, 26, "%Y-%m-%d %H:%M:%S\n", p_time);
    stream_ << str_t;
}

FileUtil.cpp

//stream:一个指向 FILE 类型的指针,表示要设置缓冲区的文件流。
//buf:一个指向字符数组的指针,表示用户提供的缓冲区。
//size:一个 size_t 类型的变量,表示缓冲区的大小。
//当你调用 setbuffer 函数时,文件流将使用用户提供的缓冲区进行 I/O 操作。这样,在进行文件读写时
//数据首先被存储在缓冲区,然后再从缓冲区写入文件或从文件读取到缓冲区。这种方式可以减少实时读写操作的开销,提高性能。
AppendFile::AppendFile(string filename) : fp_(fopen(filename.c_str(), "ae")) {
  // 用户提供缓冲区
  setbuffer(fp_, buffer_, sizeof buffer_);
}

HttpData.cpp

mime这个map在h和cpp中都声明了,感觉cpp中的声明是没有必要的?
std::unordered_map<std::string, std::string> MimeType::mime;  // 16行

Timer这个文件包含两个类

TimerNode类主要维护一个http请求和超时时间,以及是否删除等等。
TimerManager类包含一个优先级队列,用来存放TimerNode对象
// 表示队列存放的数据类型是SPTimerNode,std::deque<SPTimerNode>表示队列的实现容器是deque,TimerCmp代表比较器
std::priority_queue<SPTimerNode, std::deque<SPTimerNode>, TimerCmp> timerNodeQueue;

void TimerNode::update(int timeout) 更新超时时间

 // 这个函数的注释有点看不懂,我感觉就是被删除或者无效了,直接从堆里面删了啊
void TimerManager::handleExpiredEvent() {
  // MutexLockGuard locker(lock);
  while (!timerNodeQueue.empty()) {
    SPTimerNode ptimer_now = timerNodeQueue.top();
    if (ptimer_now->isDeleted())
      timerNodeQueue.pop();
    else if (ptimer_now->isValid() == false)
      timerNodeQueue.pop();
    else
      break;
  }
}

base类

noncopyable类

通过将拷贝构造函数和赋值运算符声明为私有成员函数并不提供实现,noncopyable 类禁止了派生类的对象进行拷贝构造和赋值操作。这样可以确保派生类的对象在使用过程中不会意外地进行拷贝或赋值,从而避免潜在的错误。

class noncopyable {
 protected:
  noncopyable() {}
  ~noncopyable() {}

 private:
  noncopyable(const noncopyable&);
  const noncopyable& operator=(const noncopyable&);
};
Util.cpp
主要是对读写函数进行了封装,可以保证数据能够被读写完。

// 这段代码是用来处理 SIGPIPE 信号的函数。在 Unix/Linux 系统中,当向一个已关闭的 socket 进行写操作时,系统会发送 SIGPIPE 信号给进程,通知它发生了管道破裂(Broken Pipe)的情况。默认情况下,如果进程没有对 SIGPIPE 信号进行处理,那么进程会被终止。
void handle_for_sigpipe() {
  struct sigaction sa;
  memset(&sa, '\0', sizeof(sa));
  sa.sa_handler = SIG_IGN;
  sa.sa_flags = 0;
  if (sigaction(SIGPIPE, &sa, NULL)) return;
}

// 设置 sa 结构体的 sa_handler 字段为 SIG_IGN,表示忽略 SIGPIPE 信号。 
// 调用 sigaction 函数,将 SIGPIPE 信号的处理方式设置为忽略,即当进程接收到 SIGPIPE 信号时不做任何处理。


// 将socket文件描述符设置成非阻塞
int setSocketNonBlocking(int fd) {
  int flag = fcntl(fd, F_GETFL, 0);
  if (flag == -1) return -1;

  flag |= O_NONBLOCK;
  if (fcntl(fd, F_SETFL, flag) == -1) return -1;
  return 0;
}

// 设置套接字的 SO_LINGER 选项,控制套接字关闭时的行为。SO_LINGER 选项允许应用程序控制在关闭套接字时的行为,特别是在还有数据未发送完毕的情况下。这样设置的作用是在关闭套接字时,如果还有未发送完的数据,套接字会最多等待指定的超时时间(30 秒),然后强制关闭连接并丢弃未发送完的数据。这样可以确保连接的及时关闭,避免连接处于半关闭状态或者等待数据发送完毕而导致的延迟。
void setSocketNoLinger(int fd) {
  struct linger linger_;
  linger_.l_onoff = 1;
  linger_.l_linger = 30;
  setsockopt(fd, SOL_SOCKET, SO_LINGER, (const char *)&linger_,
             sizeof(linger_));
}

日志

FileUtil提供了AppendFile类,将文件指针和缓冲区关联起来,可向缓冲区中append数据,也可以flush到实际的文件中

LogFile类,自带一个锁,和一个AppendFile类,提供缓冲区append能力,每append1024次,就flush到磁盘一次

LogStream自带一个FixedBuffer类,FixedBuffer其实就是一个缓冲区类,模板指定大小,提供一些追加操作;提供链式调用,提供向缓冲区中追加str、char、整型、浮点数等 前端

AsyncLogging就是启动一个线程函数,专门将缓冲区写入到日志系统中,但是不懂为什么要在线程函数中latch_.countDown();一次?

Logging是最上层的类,每次调用LOG<<的时候,就会生成一个Logger类,Logger类里面有一个AsyncLogging静态变量,每次析构的时候,就会启动这个类

总结

1、由于doPendingFunctors()调用的Functor可能再调用queueInLoop(cb),这时queueInLoop()就必须wakeup(),否则这些新加的
cb就不能被及时调用了。muduo这里没有反复执行doPendingFunctors()直到pendingFunctors_为空,这是有意的,否则IO线程有可能陷入死循
环,无法处理IO事件。为什么是这样,能解释一下吗?

在 Muduo 中,doPendingFunctors() 函数用于执行队列中的待处理函数。当 IO 线程在处理事件时,可能会有新的回调函数被添加到队列中。为了确保这些新添加的回调函数能够被及时执行,Muduo 在每次处理完一个事件后,都会调用 doPendingFunctors() 函数来执行队列中的待处理函数。然而,如果在执行 doPendingFunctors() 函数时,新的回调函数又被添加到队列中,那么就有可能导致 IO 线程陷入死循环,无法处理 IO 事件。为了避免这种情况,Muduo 在每次调用 doPendingFunctors() 函数时,只会执行队列中的一定数量的回调函数。这个数量由 kPendingFunctorCount 常量定义,通常设置为 16。

通俗来说,就是执行额外任务的时候,可能会不断的有额外任务加进来,如果每次都要把额外任务执行完才进行下一步的话,IO线程就不能及时处理新的回调函数了,只能一直在执行doPendingFunctors()函数。
2、为什么quit的时候,如果是其他线程调用quit函数,必须调用wakeup去唤醒IO线程呢?
void EventLoop::quit() {
  quit_ = true;
  if (!isInLoopThread()) {
    wakeup();
  }
}


在 Muduo 中,EventLoop::quit() 函数用于停止事件循环。当调用 quit() 函数时,事件循环会在下一个事件处理完成后终止。在这种情况下,如果 IO 线程调用了 quit() 函数,那么 IO 线程会在处理完当前事件后终止事件循环,不需要调用 wakeup() 函数来唤醒 IO 线程。

这是因为,当 IO 线程调用 quit() 函数时,它已经在事件循环中,并且已经处理了一个事件。在这种情况下,IO 线程会在处理完当前事件后,自然地进入下一个事件处理,然后发现事件循环已经终止,从而退出事件循环。因此,在这种情况下,不需要调用 wakeup() 函数来唤醒 IO 线程。

然而,如果在其他线程中调用 quit() 函数,那么需要调用 wakeup() 函数来唤醒 IO 线程,以确保 IO 线程能够及时终止事件循环。这是因为在其他线程中调用 quit() 函数时,IO 线程可能正在等待事件,而不是处理事件。在这种情况下,调用 wakeup() 函数可以唤醒 IO 线程,使其立即处理 quit() 函数,从而终止事件循环。

Epoll常见事件

  1. EPOLLIN:用于监听文件描述符上的可读事件。当文件描述符上有数据可读时,epoll 会触发这个事件。这通常用于接收网络数据或从其他文件描述符读取数据。
  2. EPOLLOUT:用于监听文件描述符上的可写事件。当文件描述符可以写入数据时,epoll 会触发这个事件。这通常用于发送网络数据或向其他文件描述符写入数据。
  3. EPOLLET:表示使用边缘触发模式。边缘触发模式与默认的水平触发模式不同,它只在状态发生变化时触发事件。这可以提高事件处理的效率,但需要更谨慎地处理事件。
  4. EPOLLPRI:表示有紧急数据可读。这个事件通常用于带外数据的处理,例如TCP的紧急指针。当文件描述符上有紧急数据可读时,epoll 会触发这个事件。
  5. EPOLLERR:表示发生了错误。当文件描述符上发生错误时,epoll 会触发这个事件。这可能是由于文件描述符关闭、连接中断或其他错误导致的。当 EPOLLERR 事件触发时,你需要检查文件描述符的错误状态,并采取相应的措施。
  6. EPOLLHUP:表示文件描述符挂起。当文件描述符的另一端关闭连接时,epoll 会触发这个事件。这意味着你可能需要关闭文件描述符并清理相关资源。当 EPOLLHUP 事件触发时,你需要检查文件描述符的状态,并采取相应的措施。
  7. EPOLLONESHOT:表示仅触发一次事件。当使用这个标志时,epoll 只会触发一次事件,然后需要重新注册事件才能再次触发。这可以避免不必要的事件触发,提高效率。当你需要在特定条件下仅处理一次事件时,可以使用 EPOLLONESHOT 标志。
  8. EPOLLRDHUP表示对方已经关闭了写端的连接,但读端仍然是开放的,这时可以继续读取数据
3、epoll在边沿模式下,必须要将套接字设置为非阻塞模式,但是,这样就会引发另外的一个bug,在非阻塞模式下,循环地将读缓冲区数据读到本地内存中,当缓冲区数据被读完了,调用的read()/recv()函数还会继续从缓冲区中读数据,此时函数调用就失败了,返回-1,对应的全局变量 errno 值为 EAGAIN 或者 EWOULDBLOCK如果打印错误信息会得到如下的信息:Resource temporarily unavailable

4、访问根页面的时候,浏览器总是会自动的访问favicon.ico这个图标,通过get请求区获取,这是浏览器的默认行为!!!>

疑问

  • 为什么HttpData.cpp中处理连接的函数,有这么一段

                if ((events_ & EPOLLIN) && (events_ & EPOLLOUT)) {
                    events_ = __uint32_t(0);
                    events_ |= EPOLLOUT;
                }
                // events_ |= (EPOLLET | EPOLLONESHOT);
                events_ |= EPOLLET;
    

    为什么要置为0,然后设置边沿模式?

  • void Thread::start() 执行的时候,会不会出现latch_.wait()的时候已经为null了?

gpt给出的解释为

您提出的问题是关于线程同步中的一个重要考虑:确保在多线程环境中,共享资源(如latch_)的正确使用和生命周期管理。确实,如果startThread先于latch_.wait();执行,并且在runInThread中将latch_置为NULL,然后在latch_.wait();中尝试使用已经被置为NULL的latch_,这将导致未定义的行为,可能包括程序崩溃。

然而,根据您提供的代码,latch_.wait();是在pthread_create成功后立即调用的,而pthread_create是用于创建新线程的。新线程的执行是异步的,这意味着pthread_create会立即返回,并且新线程可能在latch_.wait();之后才开始执行。因此,实际上latch_.wait();会在新线程调用latch_->countDown();之前执行,这是在startThread中的runInThread方法内发生的。

如果pthread_create失败,那么start方法会设置started_为false并删除data,因此不会调用latch_.wait();。

如果pthread_create成功,那么latch_.wait();会阻塞,直到新线程调用latch_->countDown();。在新线程的runInThread方法中,latch_->countDown();是在将latch_置为NULL之前调用的,因此latch_.wait();会在latch_被置为NULL之前返回。

总结来说,根据代码的逻辑,latch_.wait();应该总是在latch_被置为NULL之前执行,因此不会出现尝试使用已经被置为NULL的latch_的情况。这是通过线程的异步执行和latch_的计数器机制来保证的。

实际上在自己测试的时候,这个顺序是不固定的。目前不清楚这里为什么这么写,后面再看看muduo的实现源码。

  • wakeupFd_的作用是?

    其实就是线程间通信
    
    
    事件循环的休眠:在事件驱动的程序中,EventLoop负责处理IO事件,如文件描述符的读写事件。当没有事件发生时,EventLoop会进入休眠状态,以节省资源。
    
    线程间通信:在其他线程需要通知EventLoop处理某些任务或事件时,它们需要一种机制来唤醒EventLoop。
    
    wakeupFd的作用:wakeupFd_就是用于这个目的。它可以被其他线程写入一个字节,从而触发EventLoop从休眠状态唤醒,并处理等待的事件。
    
    实现细节:wakeupFd_通常是一个eventfd(在Linux系统中)。当其他线程需要唤醒EventLoop时,它会向wakeupFd_写入一个字节。EventLoop在休眠时会定期检查wakeupFd_是否有数据可读。一旦检测到数据,它就会从休眠状态唤醒,并处理这些数据。
    
    效率:使用wakeupFd_而不是传统的轮询方法,可以提高效率。因为EventLoop不需要不断地检查是否有事件发生,而是在事件发生时被直接唤醒。
    
    wakeupFd_是EventLoop类中用于线程间通信和唤醒的一个关键机制,允许EventLoop在需要处理事件时从休眠状态快速唤醒。
    
  • Server里面,handNewConn和handThisConn的区别是什么?这里还有一个疑问,**acceptChannel_**有必要设置连接处理函数嘛?因为它绑定的是监听文件描述符,只处理新连接。

    handNewConn 函数用于处理新的连接请求。当有新的客户端连接到达时,handNewConn 函数会被调用。在这个函数中,我们首先接受新的连接,然后创建一个新的 HttpData 对象来处理这个连接。接着,我们将新的连接添加到事件循环中,以便在连接上发生事件时能够正确处理。
    
    handThisConn 函数用于处理已存在的连接上的事件。当已存在的连接上发生读、写或其他事件时,handThisConn 函数会被调用。在这个函数中,我们需要根据事件类型来处理相应的事件,例如读取客户端发送的数据、发送响应给客户端等。
    
    总之,handNewConn 和 handThisConn 的主要区别在于它们处理的事件类型不同。handNewConn 用于处理新的连接请求,而 handThisConn 用于处理已存在的连接上的事件。
    
  • 应该只有主loop唤醒从loop的操作,只涉及到主从loop的通信,不涉及到从loop之间的通信。

流程

main函数中,先创建一个mainloop,然后创建一个server,mainloop为server中的一个变量。然后启动server,开启mainloop的loop循环。在server中会启动线程池,线程池又会开启4个从loop,但是从loop不是通过mainloop一样的方式启动的,是利用线程的方式,在线程threadfunc中启动的,这其中涉及到一些同步、锁相关的东西,重点是countdownlatch。

浏览器输入网址的时候,浏览器首先会建立一个tcp连接,建立以后,会根据网址,自动生成一个http request。server的loop就是mainloop,这个loop会绑定一个acceptchannel,这个channel是专门负责建立连接的,新连接到来的时候,

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值