muduo 网络库数据流分析

news2024/11/13 9:44:04

最近自己实现了一个 Tiny_WebServer 服务器,是一个半同步半反应堆的模式,具体可以看我 github 上面的描述。但是春招实习二面被面试官表示项目太简单了,疯狂被怼分布式、集群等知识,故想进一步重构项目,无奈我实现的 Tiny_WebServer 中业务逻辑和网络Socket层之间耦合性太强,再基于源项目重构的话其实跟重写没有区别了,并且我没有好的设计思路。这个时候找到了陈硕大神的 muduo 网络库,正好可以满足我的需要,解耦网络层与业务层,故拜读大神的源码设计,越往读越觉得自己上面的 Tiny_WebServer 是个什么垃圾[捂脸] 。以下内容适合对网络编程有一定基础的看,并且对 muduo 网络库稍微了解下。

从简单的 EchoServer 代码入手,分析 muduo 网络库数据流

#include "muduo/net/TcpServer.h"
#include "muduo/base/Logging.h"

class EchoServer
{
 public:
  EchoServer(muduo::net::EventLoop* loop,
             const muduo::net::InetAddress& listenAddr);

  void start();  // calls server_.start();

 private:
  void onConnection(const muduo::net::TcpConnectionPtr& conn);

  void onMessage(const muduo::net::TcpConnectionPtr& conn,
                 muduo::net::Buffer* buf,
                 muduo::Timestamp time);

  muduo::net::TcpServer server_;
};

using std::placeholders::_1;
using std::placeholders::_2;
using std::placeholders::_3;

// using namespace muduo;
// using namespace muduo::net;

EchoServer::EchoServer(muduo::net::EventLoop* loop,
                       const muduo::net::InetAddress& listenAddr)
  : server_(loop, listenAddr, "EchoServer")
{
  server_.setConnectionCallback(
      std::bind(&EchoServer::onConnection, this, _1));
  server_.setMessageCallback(
      std::bind(&EchoServer::onMessage, this, _1, _2, _3));
}

void EchoServer::start()
{
  server_.start();
}

void EchoServer::onConnection(const muduo::net::TcpConnectionPtr& conn)
{
  LOG_INFO << "EchoServer - " << conn->peerAddress().toIpPort() << " -> "
           << conn->localAddress().toIpPort() << " is "
           << (conn->connected() ? "UP" : "DOWN");
}

void EchoServer::onMessage(const muduo::net::TcpConnectionPtr& conn,
                           muduo::net::Buffer* buf,
                           muduo::Timestamp time)
{
  muduo::string msg(buf->retrieveAllAsString());
  LOG_INFO << conn->name() << " echo " << msg.size() << " bytes, "
           << "data received at " << time.toString();
  conn->send(msg);
}

int main()
{
  LOG_INFO << "pid = " << getpid();
  muduo::net::EventLoop loop;
  muduo::net::InetAddress listenAddr(2007);
  EchoServer server(&loop, listenAddr);
  server.start();
  loop.loop();
}

一、服务端接收客户端连接数据流分析

一、EventLoop

首先看到主函数 main() 中定义的 EventLoop ,这是 muduo 网络库的核心知识,它的核心是个事件循环 Loop 用于响应计时器和 IO 事件。main() 函数中的 loop 定义可以理解为是一个 main_loop 用于处理新客户端的连接响应。用户一行代码,再看看封装了那些处理,首先当然是要看 EventLoop 的构造函数。

EventLoop::EventLoop()
  : looping_(false),
    quit_(false),
    eventHandling_(false),
    callingPendingFunctors_(false),
    iteration_(0),
    threadId_(CurrentThread::tid()),
    poller_(Poller::newDefaultPoller(this)),
    timerQueue_(new TimerQueue(this)),
    wakeupFd_(createEventfd()),
    wakeupChannel_(new Channel(this, wakeupFd_)),
    currentActiveChannel_(NULL)
{
  LOG_DEBUG << "EventLoop created " << this << " in thread " << threadId_;
  if (t_loopInThisThread)
  {
    LOG_FATAL << "Another EventLoop " << t_loopInThisThread
              << " exists in this thread " << threadId_;
  }
  else
  {
    t_loopInThisThread = this;
  }
  wakeupChannel_->setReadCallback(
      std::bind(&EventLoop::handleRead, this));
  // we are always reading the wakeupfd
  wakeupChannel_->enableReading();
}

可以看到 EventLoop() 的构造函数中,除了像 looping_、quit_等标记的定义,还构造 poller_ 、timerQueue_、Channel_ ,这些就可以先不关注代码细节,只需要知道 poller_ 就是用于 I/O 多路复用监听就绪事件的;timerQueue_ 定时器用于处理长时间未活跃连接;Channel_ 辅助poller_响应执行就绪事件的回调函数。注意的是一个线程有且只有一个 EventLoop 。

这里有个设计思想非常棒,就是新增的 wakeupFd_ 和它对应的 wakeupChannel_ ,可以实现随时唤醒阻塞在 poller 上的线程,再执行当前 Eventloop 中需要处理的回调函数。wakeupChannel_->setReadCallback();wakeupChannel_->enableReading();构造函数中的这两行代码,就让当前 poller_ 会一直监听 wakeupfd 上的可读事件,达到随时唤醒 loop 线程的目的。

好了,现在我们可以知道的是第一行代码 muduo::net::EventLoop loop; 大概就是定义了一个 poller_ 等待用户往里面注册要监听的就绪事件。main() 中的第二行代码muduo::net::InetAddress listenAddr(2007); 就比较通俗易懂了,InetAddress 类的作用就是方便定义或者获取套接字的 IP:port 等。

二、TcpServer

main() 中的第三行代码EchoServer server(&loop, listenAddr); 这个牵扯到的知识就非常多了,还是一样,我们首先看 EchoServer 的构造函数内的内容。

EchoServer::EchoServer(muduo::net::EventLoop* loop,
                       const muduo::net::InetAddress& listenAddr)
  : server_(loop, listenAddr, "EchoServer")
{
  server_.setConnectionCallback(
      std::bind(&EchoServer::onConnection, this, _1));
  server_.setMessageCallback(
      std::bind(&EchoServer::onMessage, this, _1, _2, _3));
}

变量 server_ 的构造牵扯到的知识非常丰富 muduo::net::TcpServer server_; ,它是属于 TcpServer 类,再看 TcpServer 的构造函数。

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)
{
  acceptor_->setNewConnectionCallback(
      std::bind(&TcpServer::newConnection, this, _1, _2));
}

loop_ 还是主线程main()第一行代码创建的 EventLoop ,acceptor 看到是不是特别熟悉,它就是对系统调用 ::accept 的封装,用于连接新的客户端请求。EventLoopThreadPool 是一个 EventLoop 池,建立连接之后的每个 TcpConnection 必须归某个 EventLoop 管理,连接描述符的所有的 IO 会转移到这个 loop 线程,one loop per thread 说的就是在这里。

三、TcpConnection

我们接着看建立连接成功之后执行的回调函数acceptor_->setNewConnectionCallback(std::bind(&TcpServer::newConnection, this, _1, _2));。这里我直接在代码上添加注释了,保证每行都给你看明白。

void TcpServer::newConnection(int sockfd, const InetAddress& peerAddr)
{
  loop_->assertInLoopThread();
  // 从EventLoop线程池中,取出一个EventLoop管理TcpConnection对象,getNextLoop()是采用的轮流处理的方式
  EventLoop* ioLoop = threadPool_->getNextLoop();
  char buf[64];
  snprintf(buf, sizeof buf, "-%s#%d", ipPort_.c_str(), nextConnId_);
  ++nextConnId_;
  // 设置连接对象名称, 包含 TcpServer 名称 + ip地址 + 端口号 + 目前是第几个连接号
  // 作为 connectionsmap 的key , 运行期间确保唯一性
  string connName = name_ + buf;

  LOG_INFO << "TcpServer::newConnection [" << name_
           << "] - new connection [" << connName
           << "] from " << peerAddr.toIpPort();
  InetAddress localAddr(sockets::getLocalAddr(sockfd));
  // FIXME poll with zero timeout to double confirm the new connection
  // FIXME use make_shared if necessary
  // 新建 TcpConnection 对象,加入 connections_ 中
  TcpConnectionPtr conn(new TcpConnection(ioLoop,
                                          connName,
                                          sockfd,
                                          localAddr,
                                          peerAddr));
  connections_[connName] = conn;
  // 设置各种回调函数,注意看这里的回调函数,就是用户设置的回调函数,也就是本例中 EchoServer 设置的回调函数
  conn->setConnectionCallback(connectionCallback_); 		// 新客户端连接回调
  conn->setMessageCallback(messageCallback_);				// 接收到客户端消息,再处理发送消息给客户端的回调
  conn->setWriteCompleteCallback(writeCompleteCallback_);	// 发送完消息的回调
  conn->setCloseCallback(									// 关闭连接回调
      std::bind(&TcpServer::removeConnection, this, _1)); // FIXME: unsafe
  // 初始化建立连接之后的设置 
  ioLoop->runInLoop(std::bind(&TcpConnection::connectEstablished, conn));
}

看完上面的代码,整体思路应该相对来说清楚了不少叭,在捋捋就是:

  1. 在这个示例中 TcpServer 是整个 muduo 库中唯一要跟用户交互的类,用户设置服务器监听到相应就绪事件后的处理执行方式。
  2. 陈硕作者提出的 one loop per thread + thread pool 模型也在该代码中得到了体现,每个线程最多有一个 EventLoop,每个 TcpConnection 必须归某个 EventLoop 管理,所有的 IO 会转移到这个线程。TcpConnection 所在的线程由其所属的 EventLoop 决定,新到的连接会按 round-robin 方式分配到线程池中。
  3. 简单的说,就是这个新建立好的连接归 EventLoopPool 中的某个 EventLoop 负责管理了,也就是代码中的 ioLoop 。

还没有完,还有一个非常巧妙的设计,我们再看TcpConnection的类定义和它的构造函数.

class TcpConnection : noncopyable,
                      public std::enable_shared_from_this<TcpConnection>
{
	...............
}

TcpConnection::TcpConnection(EventLoop* loop,
                             const string& nameArg,
                             int sockfd,
                             const InetAddress& localAddr,
                             const InetAddress& peerAddr)
  : loop_(CHECK_NOTNULL(loop)),
    name_(nameArg),
    state_(kConnecting),
    reading_(true),
    socket_(new Socket(sockfd)),
    channel_(new Channel(loop, sockfd)),
    localAddr_(localAddr),
    peerAddr_(peerAddr),
    highWaterMark_(64*1024*1024)
{
  channel_->setReadCallback(
      std::bind(&TcpConnection::handleRead, this, _1));
  channel_->setWriteCallback(
      std::bind(&TcpConnection::handleWrite, this));
  channel_->setCloseCallback(
      std::bind(&TcpConnection::handleClose, this));
  channel_->setErrorCallback(
      std::bind(&TcpConnection::handleError, this));
  LOG_DEBUG << "TcpConnection::ctor[" <<  name_ << "] at " << this
            << " fd=" << sockfd;
  socket_->setKeepAlive(true);
}

TcpConnection 这里引入了 enable_shared_from_this,它可以在对象内部获得一个指向自己的 shared_ptr。这样,当回调函数需要持续使用该对象时,可以通过 shared_from_this 方法获取对象的 shared_ptr,从而确保对象在持续使用时不会被提前销毁。第一次见,很骚的一个操作,目的就是为了回调函数完整执行。

再看看newConnection中最后一行代码ioLoop->runInLoop(std::bind(&TcpConnection::connectEstablished, conn));这行代码所带来的效果,它会执行一个回调函数,也就是下面这个。

void TcpConnection::connectEstablished()
{
  loop_->assertInLoopThread();
  assert(state_ == kConnecting);
  setState(kConnected);
  // 把 TcpConnection 绑定到 channel 上, 避免循环引用故 channel 中用的是 weak_ptr<void> 如下面代码所示
  channel_->tie(shared_from_this());
  // channel 置于可读状态中,也就是把 poller_ 设置为一直监听可读事件
  channel_->enableReading();
  // 至此,连接成功建立,执行连接建立成功的回调函数
  connectionCallback_(shared_from_this());
}

void Channel::tie(const std::shared_ptr<void>& obj)
{
  tie_ = obj; // std::weak_ptr<void> tie_;
  tied_ = true;
}
// 为什么要 tie_ 的这么复杂?我看源代码得出的最终结论就是,确保回调函数成功执行,而不会出现执行的时候出现 TcpConnection 析构,保证安全。
// handleEventWithGuard 就行具体的执行回调函数的地方,再执行前 weak_ptr::lock可将weak_ptr提升为shared_ptr, 引用计数+1 ,执行完毕又 -1 , 太棒了。
void Channel::handleEvent(Timestamp receiveTime)
{
  std::shared_ptr<void> guard;
  if (tied_)
  {
    guard = tie_.lock();
    if (guard)
    {
      handleEventWithGuard(receiveTime);
    }
  }
  else
  {
    handleEventWithGuard(receiveTime);
  }
}

好了至此,连接部分介绍完毕,channel_->enableReading(); 使得 EventLoop 会一直监听该文件描述符上的读就绪事件,我总结为以下一张图,经典的 Reactor 模型。
在这里插入图片描述

二、服务端与客户端读写数据流分析

建立完连接时TcpConnection::connectEstablished()中的这行代码channel_->enableReading();,是服务端让 poller 监听连接 fd 上的可读就绪事件,也就是当客户端发送数据过来会触发EventLoop 中的死循环的 epoll_wait 的代码,也就是如下代码所示 。

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

  while (!quit_)
  {
    activeChannels_.clear();
    // 监听所有通道就绪事件, 会阻塞当前线程, 所有就绪事件对应通道会填入activeChannels_
    pollReturnTime_ = poller_->poll(kPollTimeMs, &activeChannels_);
    ++iteration_;
    if (Logger::logLevel() <= Logger::TRACE)
    {
      printActiveChannels();
    }
    // TODO sort channel by priority
    eventHandling_ = true;
    // 处理所有的就绪事件
    for (Channel* channel : activeChannels_)
    {
      currentActiveChannel_ = channel;
      currentActiveChannel_->handleEvent(pollReturnTime_);
    }
    currentActiveChannel_ = NULL;
    eventHandling_ = false;
    // 处理其他线程要求执行的回调函数任务,如果有的话
    doPendingFunctors();
  }

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

可以看到当前线程会阻塞在 poller_->poll(kPollTimeMs, &activeChannels_); 上等待唤醒,唤醒之后的代码就是将所有就绪事件对应通道会填入activeChannels_,如下所示。

Timestamp EPollPoller::poll(int timeoutMs, ChannelList* activeChannels)
{
  LOG_TRACE << "fd total count " << channels_.size();
  int numEvents = ::epoll_wait(epollfd_,
                               &*events_.begin(),
                               static_cast<int>(events_.size()),
                               timeoutMs);
  ....
  ....
  if (numEvents > 0)
  {
    LOG_TRACE << numEvents << " events happened";
    fillActiveChannels(numEvents, activeChannels);
     ....
	 ....
  }
  ....
  ....
  return now;
}

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);
  	....
  	....
    channel->set_revents(events_[i].events);
    activeChannels->push_back(channel);
  }
}

然后执行 activeChannels_中的就绪事件任务。

void Channel::handleEvent(Timestamp receiveTime)
{
  std::shared_ptr<void> guard;
  if (tied_)
  {
    guard = tie_.lock();
    if (guard)
    {
      handleEventWithGuard(receiveTime);
    }
  }
  else
  {
    handleEventWithGuard(receiveTime);
  }
}

void Channel::handleEventWithGuard(Timestamp receiveTime)
{
  eventHandling_ = true;
  LOG_TRACE << reventsToString();
  if ((revents_ & POLLHUP) && !(revents_ & POLLIN))
  {
     .... 
    if (closeCallback_) closeCallback_();
  }
  .... 
  if (revents_ & (POLLERR | POLLNVAL))
  {
    if (errorCallback_) errorCallback_();
  }
  if (revents_ & (POLLIN | POLLPRI | POLLRDHUP))
  {
    if (readCallback_) readCallback_(receiveTime);
  }
  if (revents_ & POLLOUT)
  {
    if (writeCallback_) writeCallback_();
  }
  eventHandling_ = false;
}

还有一部分就是其他线程加入的就绪事件处理,可以通过wakeup唤醒线程,调用 doPendingFunctors();处理。

void EventLoop::doPendingFunctors()
{
  std::vector<Functor> functors;
  callingPendingFunctors_ = true;
   // 为什么要交互? 减少持有锁的时间,提高并发
  {
  MutexLockGuard lock(mutex_);
  functors.swap(pendingFunctors_);
  }

  for (const Functor& functor : functors)
  {
    functor();
  }
  callingPendingFunctors_ = false;
}

总结:这部分的数据流从整体上好理解,但是数据的代码实现比较细节,陈硕大神的代码非常优美,建议看源码,Poller;Channel;EventLoop 之间的来回调用。

三、服务端与客户端断开数据流分析

1. 客户端关闭连接,服务端被动关闭连接,read() == 0 触发关闭逻辑

客户端关闭连接,服务端的 read() 函数会返回0,然后再开始走 handleClose() 的逻辑,也就是这一部分,当 n==0开始的逻辑。

void TcpConnection::handleRead(Timestamp receiveTime)
{
  loop_->assertInLoopThread();
  int savedErrno = 0;
  ssize_t n = inputBuffer_.readFd(channel_->fd(), &savedErrno);
  if (n > 0)
  {
    messageCallback_(shared_from_this(), &inputBuffer_, receiveTime);
  }
  else if (n == 0)//客户端断开连接
  {
    handleClose();
  }
  else
  {
    errno = savedErrno;
    LOG_SYSERR << "TcpConnection::handleRead";
    handleError();
  }
}

再看看 handleClose() 函数,做了那些工作

void TcpConnection::handleClose()
{
  loop_->assertInLoopThread();
  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);
   // 让 epoll 停止监听所有fd的所有事件(读写事件),做法就是把 event 事件置空
  channel_->disableAll();

  TcpConnectionPtr guardThis(shared_from_this());
  //执行关闭连接的回调,用户设置的
  connectionCallback_(guardThis);
   //关闭连接的回调,muduo 定义的
  // must be the last line
  closeCallback_(guardThis);
}

要想知道 close_callback_ 做了什么,需要回到 TcpServer 在构造 TcpConnection 时做了那些设置工作

void TcpServer::newConnection(int sockfd, const InetAddress& peerAddr)
{
  loop_->assertInLoopThread();
  EventLoop* ioLoop = threadPool_->getNextLoop();
  ...
  ...
  ...
  // close_callback_ 执行的就是这里设置的回调函数 
  conn->setCloseCallback(
      std::bind(&TcpServer::removeConnection, this, _1)); // FIXME: unsafe
  ioLoop->runInLoop(std::bind(&TcpConnection::connectEstablished, conn));
}
// 发现这两个函数,其实是套娃函数回调调用,为什么要这样套娃呢?因为要保证线程安全,如果是 io_loop 直接回调过来执行,那么 TcpServer 中的资源就不是 main_loop 独占了,存在线程不安全
void TcpServer::removeConnection(const TcpConnectionPtr& conn)
{
  // FIXME: unsafe
  loop_->runInLoop(std::bind(&TcpServer::removeConnectionInLoop, this, conn));
}

void TcpServer::removeConnectionInLoop(const TcpConnectionPtr& conn)
{
  // 保证线程安全调用
  loop_->assertInLoopThread();
  LOG_INFO << "TcpServer::removeConnectionInLoop [" << name_
           << "] - connection " << conn->name();
  // 总的 TcpServer 中的 connections_ 移除
  size_t n = connections_.erase(conn->name());
  (void)n;
  assert(n == 1);
  EventLoop* ioLoop = conn->getLoop();
  // 再从 ioLoop 中的删除 epoll 监听的连接描述符
  ioLoop->queueInLoop(
      std::bind(&TcpConnection::connectDestroyed, conn));
}
void TcpConnection::connectDestroyed()
{
  loop_->assertInLoopThread();
  if (state_ == kConnected)
  {
    setState(kDisconnected);
    channel_->disableAll();
    connectionCallback_(shared_from_this());
  }
  channel_->remove();
}
void Channel::remove()
{
  assert(isNoneEvent());
  addedToLoop_ = false;
  loop_->removeChannel(this);
}
void EventLoop::removeChannel(Channel* channel)
{
  assert(channel->ownerLoop() == this);
  assertInLoopThread();
  if (eventHandling_)
  {
    assert(currentActiveChannel_ == channel ||
        std::find(activeChannels_.begin(), activeChannels_.end(), channel) == activeChannels_.end());
  }
  poller_->removeChannel(channel);
}
void EPollPoller::removeChannel(Channel* channel)
{
  Poller::assertInLoopThread();
  int fd = channel->fd();
  LOG_TRACE << "fd = " << fd;
  assert(channels_.find(fd) != channels_.end());
  assert(channels_[fd] == channel);
  assert(channel->isNoneEvent());
  int index = channel->index();
  assert(index == kAdded || index == kDeleted);
  size_t n = channels_.erase(fd);
  (void)n;
  assert(n == 1);

  if (index == kAdded)
  {
  	// 执行 epoll 的删除
    update(EPOLL_CTL_DEL, channel);
  }
  channel->set_index(kNew);
}

从上面的回调流程大概就是:

  1. io_loop 监听到 read() 就绪,执行 handleRead() ,结果 read() 返回 0 ,客户端主动关闭连接了,故再执行 handleClose;
  2. handleClose() 执行在构造 TcpConnection 的时候用户定义的回调函数和 muduo 库定义的关闭连接回调函数removeConnection()
  3. TcpServer 首先会删除对应的连接资源,然后再让负责该连接的 EventLoop 执行它的删除步骤
  4. EventLoop -> io_loop 首先会调用对应的 channel->remove() 函数,然后再通知 epoller 删除步骤。
  5. 最后这里非常棒的设计还是,所有的 TcpConnection 都是 shared_ptr 负责析构的,所以当删除步骤都执行完毕了,引用计数为 0,执行析构关闭 close(fd) ;(TcpConnection 中的 socket 是 unique_ptr 负责,TcpConnection 析构 -> socket 析构)

2. 服务端主动发起关闭连接

服务端主动关闭客户端连接的函数如下所示,其实最后还是调用上面分析到的 handleClose(); ,和 read() == 0 一样的数据流走向。

void TcpConnection::forceClose()
{
  // FIXME: use compare and swap
  if (state_ == kConnected || state_ == kDisconnecting)
  {
    setState(kDisconnecting);
    loop_->queueInLoop(std::bind(&TcpConnection::forceCloseInLoop, shared_from_this()));
  }
}

void TcpConnection::forceCloseWithDelay(double seconds)
{
  if (state_ == kConnected || state_ == kDisconnecting)
  {
    setState(kDisconnecting);
    loop_->runAfter(
        seconds,
        makeWeakCallback(shared_from_this(),
                         &TcpConnection::forceClose));  // not forceCloseInLoop to avoid race condition
  }
}

void TcpConnection::forceCloseInLoop()
{
  loop_->assertInLoopThread();
  if (state_ == kConnected || state_ == kDisconnecting)
  {
    // as if we received 0 byte in handleRead();
    handleClose();
  }
}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/481148.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Anaconda Prompt安装pytorch

详解Anaconda安装pytorch的全过程 1.首先切换Anaconda的镜像地址&#xff0c;切换的原因我想大家应该明白&#x1f60a; 在anaconda prompt中输入以下四行命令 conda config --add channels https://mirrors.tuna.tsinghua.edu.cn/anaconda/pkgs/free/conda config --add ch…

医学图像分割之Attention U-Net

目录 一、背景 二、问题 三、解决问题 四、Attention U-Net网络结构 简单总结Attention U-Net的操作&#xff1a;增强目标区域的特征值&#xff0c;抑制背景区域的目标值。抑制也就是设为了0。 一、背景 为了捕获到足够大的、可接受的范围和语义上下文信息&#xff0c;在标…

Anaconda安装教程

最新Anaconda3安装教程 1.Anaconda3下载 官网下载地址 缺点&#xff1a; 下载速度比较慢&#xff0c;对速度有要求的小伙伴往下看 通过清华镜像加速的方式下载比较快 清华镜像加速地址 2.Anaconda3安装 双击安装包&#xff0c;点击next 点击 I agree 选择使用的用户&am…

攻防世界-Crypto-不仅仅是Morse

题目描述&#xff1a;题目太长就不拷贝了&#xff0c;总之&#xff0c;就是对以下字符进行解密 --/.-/-.--/..--.-/-..././..--.-/..../.-/...-/./..--.-/.-/-./---/-/...././.-./..--.-/-.././-.-./---/-.././..../..../..../..../.-/.-/.-/.-/.-/-.../.-/.-/-.../-.../-.../…

QML应用动画(Applying Animations)

目录 一 扩展可点击图像元素版本2&#xff08;ClickableImage Version2&#xff09; 1 第一个火箭 2 第二个火箭 3 第三个火箭 动画可以通过以下几种方式来应用&#xff1a; 属性动画 - 在元素完整加载后自动运行&#xff1b; 属性动作 - 当属性值改变时自动运行&#xf…

通讯录的实现(静态入手版)

&#x1f349;博客主页&#xff1a;阿博历练记 &#x1f4d6;文章专栏&#xff1a;c语言&#xff08;初阶与进阶&#xff09; &#x1f381;代码仓库&#xff1a;阿博编程日记 &#x1f339;欢迎关注&#xff1a;欢迎友友们点赞收藏关注哦 文章目录 &#x1f36d;前言&#x1f…

python学习之【类和对象】

前言 五一快乐&#xff01; 上一篇文章python学习——【第八弹】中&#xff0c;给大家介绍了python中的函数&#xff0c;这篇文章接着学习python中的类和对象。 我们知道&#xff0c;python中一切皆对象。在这篇文章开始之前&#xff0c;我们先了解一下编程界的两大阵营——面…

[渗透教程]-004-长城防火墙GFW的原理

文章目录 1. baidu.com 请求过程2. GFW原理2.1 GFW拦截方法1:DNS渲染2.2 通过IP黑名单2.3 VPN阻断1. baidu.com 请求过程 家庭的路由器具备了交换机的功能.域名–>ip,优先检测本地的缓存,没有的话就查找DNS服务器,传输层对应该层的数据进行封装增加了端口的信息,网络层对传输…

[230502]英语阅读长难句分析|共6个

&#x1f363;五月份第二篇笔记&#x1f363; 40&#xff1a;0/3 41&#xff1a; 3/3 目录 题目 40-1 &#xff08;1&#xff09;句子结构分析 &#xff08;2&#xff09;生词 &#xff08;3&#xff09;原题 40-2 &#xff08;1&#xff09;句子结构分析 &#xff08;2&#…

2022年度项目管理软件排名揭晓:哪些软件在市场中脱颖而出?

在项目管理软件的选择过程中&#xff0c;用户会倾向于参考一些软件排名来辅助自己进行选择。软件排名方面推荐参考G2&#xff0c;一个国外的靠谱软件评测网站&#xff0c;类似于软件版的“大众点评”&#xff0c;软件评价来自于真实用户&#xff0c;网站通过多维度的算法&#…

springboot3+react18+ts实现一个点赞功能

前端&#xff1a;vitereact18tsantd 后端&#xff1a;springboot3.0.6mybatisplus 最终效果大致如下&#xff1a; 后端&#xff1a; 引入pom依赖 <dependencies><dependency><groupId>org.springframework.boot</groupId><artifactId>spring…

PMP/高项 03-项目进度管理

项目进度管理 概念 项目进度管理&#xff08;Schedule Management) 项目进度管理又叫项目工期管理&#xff08;Duration Management)或项目的时间管理(Time Management) 是一种为管理项目按时完成项目所需的各个过程 进度管理过程 规划进度管理 定义活动 排列活动顺序 估算活…

jQuery -- 常用API(上)

1. jQuery选择器 1.1 jQuery基础选择器 原生 JS 获取元素方式很多&#xff0c;很杂&#xff0c;而且兼容性情况不一致&#xff0c;因此 jQuery 给我们做了封装&#xff0c;使获取元素统一标准。 语法&#xff1a;$(“选择器”) // 里面选择器直接写 CSS 选择器即可&#xff…

【毕业设计】基于springboot + vue微信小程序商城

目录 前言一、视频展示二、系统介绍三、项目地址四、运行环境五、创新点/亮点六、设计模块①前台②后台 七、系统功能模块结构图八、 准备阶段①使用真实支付②使用模拟支付九、使用说明十、登录后台十一、后台页面展示十二、微信小程序页面展示关于我 前言 【毕业设计】基于s…

IPsec中IKE与ISAKMP过程分析(快速模式-消息2)

IPsec中IKE与ISAKMP过程分析&#xff08;主模式-消息1&#xff09;_搞搞搞高傲的博客-CSDN博客 IPsec中IKE与ISAKMP过程分析&#xff08;主模式-消息2&#xff09;_搞搞搞高傲的博客-CSDN博客 IPsec中IKE与ISAKMP过程分析&#xff08;主模式-消息3&#xff09;_搞搞搞高傲的博客…

数据结构与算法基础(王卓)(31):折半插入排序、希尔排序

目录 折半插入排序 Project 1: 问题&#xff1a;缺少在插入元素之前的移动元素的操作 Project 2:&#xff08;最终成品、结果&#xff09; 希尔排序 Project 1:&#xff08;个人思路&#xff09; 标准答案&#xff1a;&#xff08;PPT答案&#xff09; 解释说明&#xff…

etcd的Watch原理

在 Kubernetes 中&#xff0c;各种各样的控制器实现了 Deployment、StatefulSet、Job 等功能强大的 Workload。控制器的核心思想是监听、比较资源实际状态与期望状态是否一致&#xff0c;若不一致则进行协调工作&#xff0c;使其最终一致。 那么当你修改一个 Deployment 的镜像…

数据结构篇三:双向循环链表

文章目录 前言双向链表的结构功能的解析及实现1. 双向链表的创建2. 创建头节点&#xff08;初始化&#xff09;3. 创建新结点4. 尾插5. 尾删6. 头插7. 头删8. 查找9. 在pos位置前插入10. 删除pos位置的结点11. 销毁 代码实现1.ListNode.h2. ListNode.c3. test.c 总结 前言 前面…

03-WAF绕过-漏洞利用之注入上传跨站等绕过

WAF绕过-漏洞利用之注入上传跨站等绕过 思维导图 一、sql注入绕过 使用sqlmap注入测试绕过 1.绕过cc流量 通过sqlmap对网站进行测试的时候&#xff0c;如果对方有cc流量防护&#xff0c;需要给sqlmap设置一个代理进行注入。 防cc拦截&#xff1a;修改user-agent头代理&…

ADB调试删除手机内置应用

前言 最近手机升级到了鸿蒙3系统&#xff0c;但是内置了两个输入法&#xff0c;我想删掉小艺输入法&#xff0c;于是就有了这篇记录。   本文在B站上ADB调试卸载应用的教程的基础上&#xff0c;去掉了内网穿透相关操作步骤。 前期准备 手机&#xff08;荣耀10青春版&#x…