muduo库的框架剖析及总结

muduo库介绍 muduo库是一个多线程服务器开发库 muduo 作者陈硕,现在在美国加州硅谷某互联网大公司工作,从事大规模分布式的可靠系统工程。这个库是作者多年工作的总结,可以说大家学通了这个库,找一份Linux服务器开发的工作是没问题的,因为这个库是教大家Linux环境下多线程服务器开发的正规做法,这个库是用的现在C++技术,大量使用了智能指针,bind、function这些技术。 作者还出了一本书《Linux多线程服务端编程:使用muduo C++网络库》,但是这本书不适合初学者,适合具有一定网络编程经验的开发者 阅读详情
有些人觉得我的博客口水话很多,嗯,我个人考虑了一下,可能我写东西确实没有那么高大上,口水话很多,但是我个人确实非常喜欢这种看着非常亲切的风格,其实我觉得还好,只要你能读懂我的博客就行!!!有问题和意见可以提,有问题可以相互探讨、留言,甚至在留言板骂我都行,对我个人也是一种鼓励吧,好了还是太多废话了!!!


我前面有说过把Muduo库分成大致四个模块,那么我们今天把Channel模块和Eventloop这两个模块给搞定吧


Channel我们前面说过了它是一个事件处理器,看看它的对象

  boost::weak_ptr<void> tie_;
  bool tied_;
  bool eventHandling_;
  bool addedToLoop_;
  //读处理的回调
  ReadEventCallback readCallback_;
  EventCallback writeCallback_;
  EventCallback closeCallback_;
  EventCallback errorCallback_;

再看看它的构造函数:

Channel::Channel(EventLoop* loop, int fd__)
  : loop_(loop),
    fd_(fd__),
    events_(0),
    revents_(0),
    index_(-1),
    logHup_(true),
    tied_(false),
    eventHandling_(false),
    addedToLoop_(false)
{
}

我们前面知道Acceptor和TcpConnection都用了Channel的setReadCallback,而这个函数呢就是相当于设置了Channel中的ReadBack回调函数,而这个ReadBack函数的作用是在Epoll一旦触发了就绪事件的回调函数

 void setReadCallback(const ReadEventCallback& cb)
  { readCallback_ = cb; }

不过在这里强调的Channel则内的主要函数则不是这个 
我们这里第一个应该强调的函数应该是: 
Channel::handleEvent

//调用底下的处理事件
void Channel::handleEvent(Timestamp receiveTime)
{
  boost::shared_ptr<void> guard;
  if (tied_)
  {
    guard = tie_.lock();
    if (guard)
    {
      handleEventWithGuard(receiveTime);
    }
  }
  else
  {
    handleEventWithGuard(receiveTime);
  }
}

而它的底层调用的是: 
Channel::handleEventWithGuard

//对各种事件的一个处理
void Channel::handleEventWithGuard(Timestamp receiveTime)
{
  eventHandling_ = true;
  LOG_TRACE << reventsToString();
  if ((revents_ & POLLHUP) && !(revents_ & POLLIN))
  {
    if (logHup_)
    {
      LOG_WARN << "Channel::handle_event() POLLHUP";
    }
    if (closeCallback_) closeCallback_();
  }

  if (revents_ & POLLNVAL)
  {
    LOG_WARN << "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;
}

其实从这里我们能看出来这个函数其实是对当有就绪事件发生时判断事件是可读还是可写时间,判断完后调用一个可读事件函数或者一个可写事件函数,这是Channel中的最重要的一个函数,当然还有另一个重要的函数:void enableReading() 
在前面的TcpConnection中的TcpConnection::connectEstablished()我们调用过它,还有前面Acceptor中的Acceptor::listen()中我们也调用过它,我们这里就直接告诉它的功能了吧,就是起到一个注册事件的功能,我们来看一下它的代码:

void enableReading() { events_ |= kReadEvent; update(); }

里面有个update():

void Channel::update()
{
  addedToLoop_ = true;
  loop_->updateChannel(this);
}

我们可以看到它调用了Eventloop中的updateChannel(),虽然我们还没说到Eventloop,但是我们先走进去继续往下看吧,等后面说完Eventloop再来把之前的坑给补上:

//添加事件
void EventLoop::updateChannel(Channel* channel)
{
  assert(channel->ownerLoop() == this);
  assertInLoopThread();
  poller_->updateChannel(channel);
}

我们看到它又调用了一个poller**底下的updateChannel()(我想吐槽为什么封装那么多),我们现在这里先不细说**poller,因为它是一个抽象类,它会供我们去选择epoll或者poll,我后面并没有分析poll的部分,而只是分析了epoll,直接进入epoll看就好咯:

//注册事件
void EPollPoller::updateChannel(Channel* channel)
{
  Poller::assertInLoopThread();
  LOG_TRACE << "fd = " << channel->fd() << " events = " << channel->events();
  const int index = channel->index();
  if (index == kNew || index == kDeleted)
  {
    // a new one, add with EPOLL_CTL_ADD
    int fd = channel->fd();
    if (index == kNew)
    {
      assert(channels_.find(fd) == channels_.end());
      channels_[fd] = channel;
    }
    else // index == kDeleted
    {
      assert(channels_.find(fd) != channels_.end());
      assert(channels_[fd] == channel);
    }
    channel->set_index(kAdded);
    update(EPOLL_CTL_ADD, channel);
  }
  else
  {
    // update existing one with EPOLL_CTL_MOD/DEL
    int fd = channel->fd();
    (void)fd;
    assert(channels_.find(fd) != channels_.end());
    assert(channels_[fd] == channel);
    assert(index == kAdded);
    if (channel->isNoneEvent())
    {
      update(EPOLL_CTL_DEL, channel);
      channel->set_index(kDeleted);
    }
    else
    {
      update(EPOLL_CTL_MOD, channel);
    }
  }
}

最后还是调用了一个EPollPoller::update():

//注册删除事件核心
void EPollPoller::update(int operation, Channel* channel)
{
  struct epoll_event event;
  bzero(&event, sizeof event);
  event.events = channel->events();
  event.data.ptr = channel;
  int fd = channel->fd();
  //核心
  if (::epoll_ctl(epollfd_, operation, fd, &event) < 0)
  {
    if (operation == EPOLL_CTL_DEL)
    {
      LOG_SYSERR << "epoll_ctl op=" << operation << " fd=" << fd;
    }
    else
    {
      LOG_SYSFATAL << "epoll_ctl op=" << operation << " fd=" << fd;
    }
  }
}

当然,在这里你就能看到它实际上调用了epoll_ctl,所以无论是注册事件还是删除事件其实底层都是调用了这个函数,啊,这里说得有些深入Eventloop中了,待会说到这个地方,会把这里的一些坑给补上,这里我就要说明一点就是void enableReading()是一个注册事件的函数,而它底层调用指向Eventloop中的updateChannel(),我们一直往下深入就会发现实际上它调用了epoll_ctl


/*****************************************************/ 
好吧Channel也说得差不多了,该说Eventloop这个家伙了,所谓的Eventloop在这里就类似于一个事件分流器,我们把我们所谓的一些事件封装成功能类,比如TcpConnection和Acceptor,当然这些事件有它们的文件描述符,OK!这个时候我们的Channel就相当于一个事件处理器,它把对于这个事件的一些方法收集在一起,当有就绪事件发生的时候,就会回调这些方法,我们把我们事件的文件描述符传入到Channel中,再将Channel放入到Eventloop中,那么剩下的事情就由Eventloop去处理吧


我们来看一下Eventloop这个类,它实际上是独立于TcpServer而存在,它,当然,我们只能说我们在结构上可以暂时这么说,因为当你建立一个Eventloop是独立创建然后传入指针进入TcpServer中的,而不是TcpServer创建好的,还是老规矩,老看一下它的成员变量:

bool looping_; /* atomic */
  bool quit_; /* atomic and shared between threads, okay on x86, I guess. */
  bool eventHandling_; /* atomic */
  bool callingPendingFunctors_; /* atomic */
  int64_t iteration_;
  const pid_t threadId_;
  Timestamp pollReturnTime_;//时间戳
  boost::scoped_ptr<Poller> poller_;//底层的poll或者epoll
  boost::scoped_ptr<TimerQueue> timerQueue_;//时间队列

typedef std::vector<Channel*> ChannelList;
  ChannelList activeChannels_;//就绪事件
  Channel* currentActiveChannel_;//当前就绪事件

构造函数:

EventLoop::EventLoop()
  : looping_(false),
    quit_(false),
    eventHandling_(false),
    callingPendingFunctors_(false),
    iteration_(0),
    threadId_(CurrentThread::tid()),
    poller_(Poller::newDefaultPoller(this)),//选择poll或者epoll
    timerQueue_(new TimerQueue(this)),//epoll_wait的时间列表
    wakeupFd_(createEventfd()),//线程间通信需要的fd
    wakeupChannel_(new Channel(this, wakeupFd_)),
    currentActiveChannel_(NULL)
{
    //...貌似应该和ioloop有关,跟主线程和线程之间的响应有关
  wakeupChannel_->setReadCallback(
      boost::bind(&EventLoop::handleRead, this));
  // we are always reading the wakeupfd
  wakeupChannel_->enableReading();
}

好吧,其实它的重点是这个: 
poller_(Poller::newDefaultPoller(this))

Poller* Poller::newDefaultPoller(EventLoop* loop)
{
  if (::getenv("MUDUO_USE_POLL"))
  {
    return new PollPoller(loop);
  }
  else
  {
    return new EPollPoller(loop);
  }
}

之前我也说过,它是对I/O复用,poll和epoll的一个选择,我所选择的是epoll 
epoll的构造函数:

EPollPoller::EPollPoller(EventLoop* loop)
  : Poller(loop),
  //构造一个epoll
    epollfd_(::epoll_create1(EPOLL_CLOEXEC)),
    events_(kInitEventListSize)

我们能看到它调用了epoll_create,创建一个epoll事件队列


我们再转回Eventloop中: 
我们要知道Eventloop最重要的一个函数就是loop(),我们应该分析的是这个函数:

void EventLoop::loop()
{
  assert(!looping_);
  assertInLoopThread();
  looping_ = true;
  quit_ = false;  // FIXME: what if someone calls quit() before loop() ?
  LOG_TRACE << "EventLoop " << this << " start looping";

//quit这个其实和线程有点关系,当然你要了解更多详细的请去看陈硕的书= =!!
  while (!quit_)
  {
      //前面其实我们有看到这就是一个就是就绪事件列表哦
    activeChannels_.clear();//删除事件列表所有元素
    pollReturnTime_ = poller_->poll(kPollTimeMs, &activeChannels_);//!!!最重要的函数(我们可以看到这里它把activeChannels_传了进去)
    ++iteration_;
    if (Logger::logLevel() <= Logger::TRACE)
    {
      printActiveChannels();
    }
    // TODO sort channel by priority
    eventHandling_ = true;
    for (ChannelList::iterator it = activeChannels_.begin();
    //把传进去的activeChannels_的就绪事件提取出来
        it != activeChannels_.end(); ++it)
    {
      currentActiveChannel_ = *it;
      //根据就绪事件处理事件
      currentActiveChannel_->handleEvent(pollReturnTime_);
    }
    currentActiveChannel_ = NULL;
    eventHandling_ = false;
    doPendingFunctors();
  }

  LOG_TRACE << "EventLoop " << this << " stop looping";
  looping_ = false;
}

好吧poll这个函数比较重要,当然它是调用了EPollPoller::poll(),好吧来看看:

Timestamp EPollPoller::poll(int timeoutMs, ChannelList* activeChannels)
{
//我们看到这里调用了epoll_wait,这里当然就是监听就绪事件的文件描述符了
  int numEvents = ::epoll_wait(epollfd_,
                               &*events_.begin(),
                               static_cast<int>(events_.size()),
                               timeoutMs);
  int savedErrno = errno;
  Timestamp now(Timestamp::now());
  if (numEvents > 0)
  {
    LOG_TRACE << numEvents << " events happended";
    //将就绪事件放入就绪队列拉到Eventloop中统一处理
    fillActiveChannels(numEvents, activeChannels);
    if (implicit_cast<size_t>(numEvents) == events_.size())
    {
      events_.resize(events_.size()*2);
    }
  }
  else if (numEvents == 0)
  {
    LOG_TRACE << " nothing happended";
  }
  else
  {
    // error happens, log uncommon ones
    if (savedErrno != EINTR)
    {
      errno = savedErrno;
      LOG_SYSERR << "EPollPoller::poll()";
    }
  }
  return now;
}

fillActiveChannels(numEvents, activeChannels)这个得看一下:

//将就绪事件放到activeChannels中
void EPollPoller::fillActiveChannels(int numEvents,
                                     ChannelList* activeChannels) const
{
  assert(implicit_cast<size_t>(numEvents) <= events_.size());
  for (int i = 0; i < numEvents; ++i)
  {
  //将就绪事件取出来放入到就绪事件列表中
    Channel* channel = static_cast<Channel*>(events_[i].data.ptr);
#ifndef NDEBUG
    int fd = channel->fd();
    ChannelMap::const_iterator it = channels_.find(fd);
    assert(it != channels_.end());
    assert(it->second == channel);
#endif
    channel->set_revents(events_[i].events);
    activeChannels->push_back(channel);
  }
}

其实可以看到这就是一个提取就绪事件的一个过程,我们把就绪事件放入到activeChannels中,在Eventloop中再把从epoll中提取到的事件提取出来 ,并调用了currentActiveChannel_的handleEvent,而我们在分析Channel的时候我们已经分析了handleEvent的作用,那么实际上这里就相当于有就绪事件然后返回调用它的回调函数了,嗯,就是这么一个过程!!! 
/*****************************************************/


这篇是有点长!!!









构建高效分布式系统:基于C++、JsonCpp与Muduo库实现的RPC通信框架深度剖析 本文通过结合C++、JsonCpp与Muduo库,展示了如何构建一个高效、可扩展的分布式RPC通信框架。我们详细介绍了服务端与客户端的实现过程,以及如何利用这些库来实现高效的网络通信。通过本文的学习,读者可以掌握基于C++和现代网络库构建分布式RPC系统的基本方法,并为实际项目中的分布式系统开发提供有力支持。 阅读详情

相关推荐

基于C++11重写muduo库

邮箱:2877944240@qq.com 源码:基于C++11重写muduo库 如有转载,请标明出处,谢谢! 文章目录预备知识muduo库简介muduo库的优点muduo和libeventone loop per process言归正传muduo的模型设计Reactor模型muduo库的Reactor模型one (evnet)loop per threadC++11重写muduo核心类代码muduo库的核心类代码辅助类NoncopyableTimestampLoggersocketInetAddres

weixin_51311212的博客 3650

TCP网络编程库——Muduo库

Muduo库的运用

m0_74246469的博客 2104

别再手动填序号了!用Python脚本在Arcgis属性表里实现分组自动编号(附完整代码)

本文详细介绍了如何利用Python脚本在Arcgis属性表中实现分组自动编号,告别低效的手工操作。通过核心代码解析和实战教程,帮助GIS工程师快速掌握自动化编号技术,提升工作效率并降低错误率。特别适合城市规划、国土调查等需要处理大量分组数据的场景。

weixin_42532473的博客 309

muduo库的模拟实现——muduo库的介绍

本文是muduo库模拟实现项目记录的第一篇文章,简单介绍了一下muduo库的背景知识以及基本结构。

写博客记录分享学习笔记 1895

RAD Studio 12安装全攻略:从下载到破解InterBase的完整流程(附KeyPatch使用技巧)

本文提供RAD Studio 12的完整安装指南,包括系统要求、安装步骤及InterBase数据库的高效配置方法。详细介绍了如何使用KeyPatch工具进行许可证处理,并分享了IDE优化和开发技巧,帮助开发者快速上手这一强大的跨平台开发工具。

q6r7s8t9的博客 953

Android14_MTK调试LCD屏功能

android14 MTK平台调试LCD屏

m0_37786046的博客 1473

epoll_ctl的概念和使用案例

epoll_ctl是Linux epoll机制的核心函数,用于管理epoll实例监控的文件描述符(FD)。通过EPOLL_CTL_ADD/MOD/DEL操作,它可以动态添加、修改或删除FD及其监控事件(如可读EPOLLIN、可写EPOLLOUT)。该函数配合epoll_event结构体使用,支持边缘触发(ET)和水平触发(LT)模式,是实现高性能网络编程(如高并发服务器)的关键。使用时需注意错误处理、资源释放及触发模式的选择,典型应用包括TCP服务器的连接监控与数据读写处理。

weixin_42108533的博客 988

Windows10完美驱动Apple Studio Display:从雷电接口到亮度调节全攻略

本文详细介绍了如何在Windows10系统上完美驱动Apple Studio Display,涵盖雷电接口兼容性、亮度调节及色彩管理等关键问题。通过实测对比不同转接方案和显卡兼容性,推荐使用贝尔金VR线和NVIDIA GTX-1660显卡,并提供了BootCamp驱动安装和亮度调节的终极解决方案,帮助用户实现最佳显示效果。

elastic6hunter的博客 1123

C++Muduo网络库:简介及使用

Muduo网络库:底层实质上为Linux的epoll + pthread线程池,且依赖boost库。 muduo的网络设计核心为一个线程一个事件循环,有一个main Reactor负载accept连接,然后把连接分发到某个sub Reactor(采用轮询的方式来选择sub Reactor),该连接的所用操作都在那个sub Reactor所处的线程中完成。多个连接可能被分派到多个线程中,以充分利用CPU,Reactor poll的大小是固定的,根据CPU的数目确定。如果有过多的耗费CPU I/O的计算任务,可

Rain的博客 1万+

广告推荐算法 - 学习笔记(持续更新中)

本篇博客,是我用来记录学习广告推荐算法的一些笔记和总结。王喆:"深度"学习计算广告计算广告系统的核心目标是通过数据、算法和技术,在合适的场景下,将最相关的广告精准推送给目标用户,同时实现广告主媒体平台和用户之间的多方利益最大化。广告主:需求方核心目标:通过精准投放广告,最大化转化(如点击、购买)并控制成本,提升投资回报率(ROI)。关键行为:广告创建,定向设置,预算与出价策略,效果监控媒体平台:供给方核心目标:最大化广告收入,同时保持用户体验以避免用户流失。

qq_39748832的博客 2201

ABB AC500 系列 PLC 与上位机iFix 的通讯配置

在 软件中,有 3 种方式可以进入 IP config tool 的配置界面 双击 IP_settings,点击 IP config tool,即可进入 IP config tool 界面点击菜单栏的 Tool→IP config如果单独安装了 ABB IP Configration,则可以通过开始→程序→ABB→ABB IPConfigration 启动 IP 配置工具启动 IP config tool 后,即可进入 IP 配置界面。

xdpcxq的专栏 4032

Muduo库的框架剖析及总结(一)

话说回来呢,前段时间又重新看了一遍Muduo库,嗯哼,把原来的框架又大概走了一遍,嗯对,仅仅是框架,还有说到最精华的部分,我想说以前我是发过一篇对Muduo库的线程池的剖析(虽然说剖析的一般吧 = =!!!,但是毕竟是自己的心血啊),Muduo库的内存池也是Muduo库精华的一部分,当然,还有一个精华的部分我认为是Muduo库的Buffer缓冲区,它运用了环形缓冲区以及vector制

Inform 1455

《QT+PCL》点云的点选与框选

【代码】《QT+PCL》点云的点选与框选。

阁楼 3603

以基于Reactor模式的高并发EchoServer为例剖析muduo库框架

前言 在讲解基于muduo库的高并发echo服务器之前,我们先来回顾一下我们一般编写基于Reactor模式的高并发服务器的基本流程。 muduo的EchoServer其实也是基于上述流程,只不过进行了一定程度地封装,本质上仍是Reactor模式。muduo的example中的echo服务器代码展示的是只有一个reactor的服务器,muduo其实支持one loop per t...

一步一步往上爬 956

android 蓝牙通讯demo

android 下蓝牙通讯demo,实现了发现蓝牙,使能蓝牙,通过蓝牙发送消息。

VC中sqlite的使用

在VC中对sqlite的使用,实现数据库大批数据的快速插入,读取数据库中的数据在ListCtrl中显示

Muduo库的框架剖析及总结(三)

有些人觉得我的博客口水话很多,嗯,我个人考虑了一下,可能我写东西确实没有那么高大上,口水话很多,但是我个人确实非常喜欢这种看着非常亲切的风格,其实我觉得还好,只要你能读懂我的博客就行!!!有问题和意见可以提,有问题可以相互探讨、留言,甚至在留言板骂我都行,对我个人也是一种鼓励吧,好了还是太多废话了!!! 我前面有说过把Muduo库分成大致四个模块,那么我们今天把Channel模块和Eventlo

Inform 992

老版QQ原生表情gif动态

这是老版的QQ表情,不过全是动态的的,Windows照片查看器是不会动的,可以右击用浏览器打开查看,再说一遍是老版的,只是分享给宁愿用老版也要动态的人。

Muduo库的框架剖析及总结(二)

我觉得可能由于我可能要分析完这几个大的模块可能并非易事,要仔细的分析也是绝壁很困难,我写的文章带有很多的口水话,一方面我是希望用通俗的语言去解释这些东西大家能更加好的去理解,另一面可以让我自己更加方便在日后看的时候不至于蒙了 上次我们已经介绍完了TcpServer这个大类,诶这个大类啊还真是一个非常犀利的一个类呢,小小一个类把服务器改做的事情的接口全部都封装得完美无缺,那么现在我觉得应该介绍一下

Inform 725

Devexpress16.2.6破解工具

Devexpress16.2.6破解工具Devexpress16.2.6破解工具Devexpress16.2.6破解工具Devexpress16.2.6破解工具

Muduo网络库解析--网络模块(1)

Muduo网络库的网络基础模块剖析并重写,InetAddress、Socket、Channel

2404_87273268的博客 924

RPC 框架项目剖析

本文用于梳理一个 rpc项目的实现细节,此项目基于cpp语言 大概三千行左右,用于学习目的。

m0_74358683的博客 877

Muduo网络库解析--网络模块(2)

我们对Muduo网络库的核心网络模块中的Socket、InetAddress以及Acceptor进行了解析。这节我们将对剩余的核心网络模块中的TcpConnection以及TcpServer进行解析。

2404_87273268的博客 1427

Json-Rpc框架(项目设计 —— 服务端模块功能详细介绍)

Server模块:作为服务端的整合模块,它基于以上各个模块构建而成,提供统一的服务端接口和启动逻辑。Server模块负责初始化各个子模块,并协调它们之间的协作,确保服务端能够正常运行并提供所需的服务。

Yawesh的博客 1447

长文梳理Muduo库核心代码及优秀编程细节剖析

长文梳理Muduo库核心代码(Multi-Reactor架构代码)及优秀编程细节思想剖析。

T_Solotov的博客 1万+

[muduo网络库]——muduo网络库精华总结(剖析muduo网络库核心部分、设计思想)

muduo 是由陈硕大佬个人开发的 TCP 网络库,本专栏使用c++11重构了Muduo库中核心的Multi-Reactor架构,剖析大佬核心的代码设计思想,希望能够理清自己的思路,也希望能够帮助到大家。

m0_73537205的博客 2826

muduo的http库剖析

一:http协议     首先看一下http request: request line + header + body (header分为普通报头,请求报头与实体报头)header与body之间有一空行(CRLF)     请求方法有:GET、POST、HEAD、PUT、DELETE等     协议版本:1.0、1.1     详细知识参见这篇博客:http必知必会,

FreeeLinux's blog 2959

[muduo网络库]——muduo库TcpConnection类,万字总结(剖析muduo网络库核心部分、设计思想)

muduo 是由陈硕大佬个人开发的 TCP 网络库,本专栏使用c++11重构了Muduo库中核心的Multi-Reactor架构,剖析大佬核心的代码设计思想,希望能够理清自己的思路,也希望能够帮助到大家。

m0_73537205的博客 4202

[muduo网络库]——muduo库的Reactor模型(剖析muduo网络库核心部分、设计思想)

muduo 是由陈硕大佬个人开发的 TCP 网络库,本专栏使用c++11重构了Muduo库中核心的Multi-Reactor架构,剖析大佬核心的代码设计思想,希望能够理清自己的思路,也希望能够帮助到大家。

m0_73537205的博客 2032
上一篇: 设计模式之--简单工厂模式
下一篇: muduo库的EpollPoller剖析
FreeeLinux
博客等级 码龄11年 421粉丝 356原创
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

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

抵扣说明:

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

余额充值