ROS初始化分析

ROS初始化分析

  • ROS通信模型
  • ros::init()
    • network::init()
    • master::init()
    • this_node::init()
    • file_log::init()
    • param::init()
  • ros::NodeHandle()
  • ros::start()

ROS通信模型

ROS节点构成了一个计算图(computation graph),它是一个p2p的网络结构,节点之间的拓扑结构为mesh(网状)。ROS节点之间的通讯中,节点通过socket来实现建立连接,传输数据。ROS与master节点的通信则通过远程过程调用实现。ROS 中远程过程调用采用XML-RPC 实现。远程调用负责管理节点对计算图中信息的获取与更改,还有一些全局的设置, 但RPC不直接支持数据的流传输。数据的传输是通过 TCPROS与 UDPROS实现的。

每个ROS节点运行一个XML-RPC server,ROS的通讯模块在ros_comm中。整个通信模型如下:
ROS通信模型
上图中蓝线表示XML-RPC通信,黑线表示socket通信。每个ROS节点都拥有IP地址、端口号和XML-RPC服务器,IP地址和端口号用来标识不同的节点。如果ROS节点在同一台主机上,那么IP地址可能是相同的。

发布者节点通过一个名为registerPublisher的远程调用(XML-RPC),注册它们的话题和消息类型到主节点。同样地,订阅者通过registerSubscriber远程调用,注册到主节点,这样主节点就获知了双方的URI、话题名称、消息类型。

订阅者注册时,主节点会回复注册同一话题的所有发布者,然后订阅者就可以通过socket直接与发布者通信,通信基于TCPROS协议传输消息,这就跟主节点无关了。当又有新的发布者注册同一个话题到主节点时,主节点执行publisherUpdate()给订阅者,回复一个新的发布者列表。

发布者和订阅者通信是基于TCPROS协议,过程与TCP三次握手类似。订阅者与发布者建立第一次连接,订阅者传输topic信息给发布者,然后再根据发布者返回的topic信息,建立第二次连接,发布者开始传输具体的数据。通信过程使用socket实现。

ros::init()

ros::init有三种重载形式,不过最后都会调用最简单的一种形式。

void init(const M_string& remappings, const std::string& name, uint32_t options)
{
  if (!g_atexit_registered)
  {
    g_atexit_registered = true;   // 是否已注册终止函数
    // 注册终止函数,在调用exit()或终止main函数时调用
    atexit(atexitCallback);
  }

  if (!g_global_queue)
  {
    g_global_queue.reset(new CallbackQueue);
  }
  //上面做了一些预处理,主要部分在下面:
  if (!g_initialized)   // 若未初始化
  {
    g_init_options = options;
    g_ok = true;

    ROSCONSOLE_AUTOINIT; //在console.h中的一段宏定义:Initializes the rosconsole library. 
    // Disable SIGPIPE
#ifndef WIN32     // 如果不是windows系统,则执行
    signal(SIGPIPE, SIG_IGN);
#endif
    // 重点是5个init
    network::init(remappings);//初始化网络,实现在network.cpp中
    master::init(remappings); //初始化master节点
    // name namespace is initialized by this_node
    this_node::init(name, remappings, options); //初始化当前节点
    file_log::init(remappings); //初始化日志文件
    param::init(remappings); //初始化ROS参数服务器
    g_initialized = true; //置上初始化标记
  }
}

g_global_queue的定义是CallbackQueuePtr g_global_queue,是一个boost共享指针:typedef boost::shared_ptr<CallbackQueue> CallbackQueuePtr。

network::init()

network::init函数主要就是给g_host和g_tcpros_server_port赋值。

void init(const M_string& remappings) //该函数在init.cpp中被调用
{
  //模块1
  M_string::const_iterator it = remappings.find("__hostname");
  if (it != remappings.end())
  {
    g_host = it->second;
  }
  else
  {
    it = remappings.find("__ip");
    if (it != remappings.end())
    {
      g_host = it->second;
    }
  }
  //模块2
  it = remappings.find("__tcpros_server_port");
  if (it != remappings.end())
  {
    try
    { // 尝试将对应元素的值(std::string)转化成uint16_t类型,g_tcpros_server_port初始值为0
      g_tcpros_server_port = boost::lexical_cast<uint16_t>(it->second);
    }
    catch (boost::bad_lexical_cast & )        // 如果上述类型转化发生异常,捕捉
    {
      throw ros::InvalidPortException("__tcpros_server_port [" + it->second + "] was not specified as a number within the 0-65535 range");
    }
  }
  //模块3,如果上面未能赋值,调用函数determineHost赋值
  if (g_host.empty())
  {
    g_host = determineHost();
  }
}

boost::lexical_cast用于string和数值之间的转换比如:将一个字符串”712”转换成整数712。

最后,如果之前未能给g_host成功赋值,会调用determineHost函数,它依次获取环境变量ROS_HOSTNAME和ROS_IP。如果没有设置这两个环境变量,就调用gethostname函数获取主机名,但不能取localhost。如果这样也不能获取到,会返回127.0.0.1。

master::init()

master::init处理master节点。

void init(const M_string& remappings)
{
  M_string::const_iterator it = remappings.find("__master");
  if (it != remappings.end())
  {
    g_uri = it->second;
  }
  //如果g_uri没有被赋值(即刚刚没找到相应节点)
  if (g_uri.empty())
  {
    char *master_uri_env = NULL;
    #ifdef _MSC_VER     // 根据编译器使用不同函数
      _dupenv_s(&master_uri_env, NULL, "ROS_MASTER_URI");
    #else
      master_uri_env = getenv("ROS_MASTER_URI");
    #endif

    if (!master_uri_env)
    {
      ROS_FATAL( "ROS_MASTER_URI is not defined in the environment. Either " \
                 "type the following or (preferrably) add this to your " \
                 "~/.bashrc file in order set up your " \
                 "local machine as a ROS master:\n\n" \
                 "export ROS_MASTER_URI=http://localhost:11311\n\n" \
                 "then, type 'roscore' in another shell to actually launch " \
                 "the master program.");
      ROS_BREAK();
    }
    g_uri = master_uri_env;

#ifdef _MSC_VER
    free(master_uri_env);
#endif
  }

  //对g_uri进行解析,把g_uri中去掉协议部分赋值给g_host,并将端口赋值给g_port。
  if (!network::splitURI(g_uri, g_host, g_port))
  {
    ROS_FATAL( "Couldn't parse the master URI [%s] into a host:port pair.", g_uri.c_str());
    ROS_BREAK();
  }
}

如果没有获得环境变量ROS_MASTER_URI,那么节点报错退出。最后把值赋给g_uri,splitURI函数是一个字符串的处理函数。 该函数使得rosmaster能够在系统中查找到节点,并且能够获得IP与端口号进行通信。rosmaster掌握了各节点的文本信息,包括名称、话题名,以及它们的端口号、URL,用线程池收集,用字典保存,开启XML-RPC服务器进行话题的传送。

this_node::init()

this_node::init初始化name命令空间。

void init(const std::string& name, const M_string& remappings, uint32_t options)
{
  ThisNode::instance().init(name, remappings, options);
}
// ThisNode是一个类,instance()是单例模式
static ThisNode& instance()
{
	static ThisNode singleton;
	return singleton;
}

// 主要实现在这里
void ThisNode::init(const std::string& name, const M_string& remappings, uint32_t options)
{
  char *ns_env = NULL;
#ifdef _MSC_VER
  _dupenv_s(&ns_env, NULL, "ROS_NAMESPACE");
#else
  ns_env = getenv("ROS_NAMESPACE");
#endif

  if (ns_env)	//如果获得环境变量
  {
    namespace_ = ns_env;    // namespace_是成员变量
#ifdef _MSC_VER
    free(ns_env);
#endif
  }
  if (name.empty()) {	// 节点名不能为空
    throw InvalidNameException("The node name must not be empty");
  }

  name_ = name;     // 节点名赋值,name_也是类成员变量,初始值为"empty"

  bool disable_anon = false;
  //在输入参数remappings查找键为"__name"的项
  M_string::const_iterator it = remappings.find("__name");
  if (it != remappings.end())
  {
    name_ = it->second;
    disable_anon = true;
  }
  //在输入参数remappings查找键为"__ns"的项
  it = remappings.find("__ns");
  if (it != remappings.end())
  {
    namespace_ = it->second;
  }
  // 这里可以看出ROS_NAMESPACE不是必要的
  if (namespace_.empty())
  {
    namespace_ = "/";
  }
  // 如果在上面赋值为 / ,最终就是 /,否则就是 /+namespace
  namespace_ = (namespace_ == "/")
    ? std::string("/") 
    : ("/" + namespace_)
    ;

  std::string error;
  // 对命名空间进行验证,这是个字符串处理函数
  // 检查首字符,首字符只能是~ / 或 alpha,逐个检查name中的每个字符是否为合法字符
  if (!names::validate(namespace_, error))
  {
    std::stringstream ss;
    ss << "Namespace [" << namespace_ << "] is invalid: " << error;
    throw InvalidNameException(ss.str());
  }

  // names must be initialized here, because it requires the namespace to already be known so that it can properly resolve names.
  // It must be done before we resolve g_name, because otherwise the name will not get remapped.
  // 将remappings映射为g_remappings和g_unresolved_remappings两个变量
  names::init(remappings);
  // 节点名不能含有 / 和 ~
  if (name_.find("/") != std::string::npos)
  {
    throw InvalidNodeNameException(name_, "node names cannot contain /");
  }
  if (name_.find("~") != std::string::npos)
  {
    throw InvalidNodeNameException(name_, "node names cannot contain ~");
  }
  // 简单的格式化操作
  name_ = names::resolve(namespace_, name_);
  // 如果初始化时的options选择的时匿名节点,那么在节点名后加时间戳,单位是纳秒
  if (options & init_options::AnonymousName && !disable_anon)
  {
    char buf[200];
    // ros::WallTime::now()返回当前时间的纳秒表示
    snprintf(buf, sizeof(buf), "_%llu", (unsigned long long)WallTime::now().toNSec());
    name_ += buf;
  }
  //把节点和名字联系起来
  ros::console::setFixedFilterToken("node", name_);
}

这个函数主要是对节点的名字进行了注册,在name这个命名空间内有一组映射g_remappings,包含了所有注册的节点名。

file_log::init()

file_log::init利用节点的名称来创造日志文件,确定日志存放目录和日志名称。

void init(const M_string& remappings)
{
  std::string log_file_name;
  M_string::const_iterator it = remappings.find("__log");
  //在remappings中找到键为"__log"的项
  if (it != remappings.end())
  {
    log_file_name = it->second;		//如果找到了,将对应的值赋值给log_file_name
  }

  {
    // Log filename can be specified on the command line through __log
    // If it's been set, don't create our own name
    if (log_file_name.empty())
    {
      // Setup the logfile appender
      // Can't do this in rosconsole because the node name is not known
      pid_t pid = getpid();		//获取当前进程号
      std::string ros_log_env;
      //获取"ROS_LOG_DIR"
      if ( get_environment_variable(ros_log_env, "ROS_LOG_DIR"))
      {
        log_file_name = ros_log_env + std::string("/");
      }
      else		//如果不存在"ROS_LOG_DIR"这个环境变量
      {			//获取"ROS_HOME"的环境变量值
        if ( get_environment_variable(ros_log_env, "ROS_HOME"))
        {
          log_file_name = ros_log_env + std::string("/log/");
        }
        else		//如果不存在环境变量"ROS_HOME"
        {
          // 如果没有设置以上环境变量,日志最终放在 ~/.ros/log
          if( get_environment_variable(ros_log_env, "HOME") )
          {
            std::string dotros = ros_log_env + std::string("/.ros/");
            fs::create_directory(dotros);
            log_file_name = dotros + "log/";
            fs::create_directory(log_file_name);
          }
        }
      }

      // log_file_name是完整路径,这里是取 文件名=节点名_
      for (size_t i = 1; i < this_node::getName().length(); i++)
      {
        if (!isalnum(this_node::getName()[i]))
        {
          log_file_name += '_';
        }
        else
        {
          log_file_name += this_node::getName()[i];
        }
      }
      // 变成了  节点名_pid_log
      char pid_str[100];
      snprintf(pid_str, sizeof(pid_str), "%d", pid);  //将pid以整形变量的形式写入pid_str
      log_file_name += std::string("_") + std::string(pid_str) + std::string(".log");
    }
    // 赋值
    log_file_name = fs::system_complete(log_file_name).string();
    g_log_directory = fs::path(log_file_name).parent_path().string();
  }
}

param::init()

param::init初始化ROS参数服务器。

void init(const M_string& remappings)
{
  M_string::const_iterator it = remappings.begin();
  M_string::const_iterator end = remappings.end();
  for (; it != end; ++it)//依次遍历remappings变量的所有元素
  {
    const std::string& name = it->first;//提取键
    const std::string& param = it->second;//提取值

    if (name.size() < 2)    //跳过键的长度小于2的元素
    {
      continue;
    }
    if (name[0] == '_' && name[1] != '_')//如果键以“__”开头
    {
      //为name赋予一个本地名称,用符号"~"代替“__”
      std::string local_name = "~" + name.substr(1);
      bool success = false;

      try
      {
        int32_t i = boost::lexical_cast<int32_t>(param);    //尝试将param转化成整型
        //将local_name规整化
        ros::param::set(names::resolve(local_name), i);
        success = true;//将成功标志置上
      }
      catch (boost::bad_lexical_cast&)
      {

      }
      if (success)    //如果成功标志已被置上,则越过后续过程
      {
        continue;     //此时,即param成功被转化为整型
      }

      try
      {
        //没能转化为整型,尝试将param转化成浮点型
        double d = boost::lexical_cast<double>(param);
        //将local_name规整化
        ros::param::set(names::resolve(local_name), d);
        success = true;   //将成功标志置上
      }
      catch (boost::bad_lexical_cast&)
      {

      }

      if (success)    //如果成功标志已被置上,则越过后续过程
      {
        continue;    //此时,即param成功被转化为浮点型
      }
      // 处理param为布尔型或其他的情况
      if (param == "true" || param == "True" || param == "TRUE")
      {
        ros::param::set(names::resolve(local_name), true);
      }
      else if (param == "false" || param == "False" || param == "FALSE")
      {
        ros::param::set(names::resolve(local_name), false);
      }
      else
      {
        ros::param::set(names::resolve(local_name), param);
      }
    }
  }

  XMLRPCManager::instance()->bind("paramUpdate", paramUpdateCallback);
}

Parameter是ROS系统运行所定义的全局变量,它由master节点的parameter server基于XML-RPC进行维护。它是全局可见的,因此可以运行时进行修改。

ros::NodeHandle()

ros::NodeHandle的构造函数会调用ros::start()。当最后一个ros::NodeHandle销毁时,将调用ros::shutdown()。如果想自定义节点生存期,可以用这两个函数。检查关闭节点的两种方法是ros::ok()和ros::isShuttingDown()。

NodeHandle类是非常重要的一个类,可以说是ROS程序的核心,发布、订阅就是它完成的。NodeHandle类中有几个私有成员变量:

std::string namespace_;
// 回调接口指针,主要用于advertise, subscribe, advertiseService和createTimer函数
CallbackQueueInterface* callback_queue_;
NodeHandleBackingCollection* collection_;

NodeHandle构造函数:

NodeHandle::NodeHandle(const std::string& ns, const M_string& remappings)
  : namespace_(this_node::getNamespace())
  , callback_queue_(0)	// 空指针
  , collection_(0)		// 空指针
{
  std::string  tilde_resolved_ns;
  // 如果给句柄命名并以'~'开头,它就将节点名加上'/'加在话题名之前
  if (!ns.empty() && ns[0] == '~')
    tilde_resolved_ns = names::resolve(ns);
  else
    tilde_resolved_ns = ns;

  construct(tilde_resolved_ns, true);
  //remappings所对应的是一个维护节点名称字符串的哈希表
  initRemappings(remappings);
}

construct函数:

void NodeHandle::construct(const std::string& ns, bool validate_name)
{
  if (!ros::isInitialized())     // 若没有调用 ros::init,报错: 必须先init()然后创建NodeHandle
  {
    ROS_FATAL("You must call ros::init() before creating the first NodeHandle");
    ROS_BREAK();
  }
  collection_ = new NodeHandleBackingCollection;
  unresolved_namespace_ = ns;
  // if callback_queue_ is nonnull, we are in a non-nullary constructor

  if (validate_name)
    namespace_ = resolveName(ns, true);
  else
      namespace_ = resolveName(ns, true, no_validate());
    
  ok_ = true;
  boost::mutex::scoped_lock    lock(g_nh_refcount_mutex);
   //此参数为全局变量,初始为0
  if (g_nh_refcount == 0 && !ros::isStarted())
  {
    g_node_started_by_nh = true;
    ros::start();	// 启动ros
  }
  ++g_nh_refcount;
}

若此时没有调用ros::init,报严重错误然后终止,所以 ros::init()必须在NodeHandle创建之前。然后创建NodeHandleBackingCollection指针。接下来是一些赋值,g_nh_refcount为初始为0的全局变量,即全局引用计数,如果此时没有启动ros,将调用ros::start(),然后将g_nh_refcount加1。

ros::start()比较复杂,它是ROS架构的核心,也是理解ROS源码的核心。

NodeHandle类的析构函数只调用了destruct():

void NodeHandle::destruct()
{
  delete collection_;

  boost::mutex::scoped_lock lock(g_nh_refcount_mutex);

  --g_nh_refcount;

  if (g_nh_refcount == 0 && g_node_started_by_nh)
  {
    ros::shutdown();
  }
}

析构函数很简单,释放collection_,引用计数减1,如果变成了0,就关闭ROS。

ros::start()

ros::start()函数位于init.cpp中,它初始化了ROS中重要的管理者,这些管理者均为单例模式:

TopicManager::instance()->start();
ServiceManager::instance()->start();
ConnectionManager::instance()->start();
PollManager::instance()->start();
XMLRPCManager::instance()->start();

TopicManager的start函数从其他三个类中得到实例,建立进程间通信需要这三个类的协助。

void TopicManager::start()
{
  boost::mutex::scoped_lock shutdown_lock(shutting_down_mutex_);
  shutting_down_ = false;
 
  poll_manager_ = PollManager::instance();//负责处理发布消息的缓冲队列中的消息
  connection_manager_ = ConnectionManager::instance();//负责建立进程间的网络通信连接
  xmlrpc_manager_ = XMLRPCManager::instance();//负责通知rosmaster
 
  xmlrpc_manager_->bind("publisherUpdate", boost::bind(&TopicManager::pubUpdateCallback, this, _1, _2));
  xmlrpc_manager_->bind("requestTopic", boost::bind(&TopicManager::requestTopicCallback, this, _1, _2));
  xmlrpc_manager_->bind("getBusStats", boost::bind(&TopicManager::getBusStatsCallback, this, _1, _2));
  xmlrpc_manager_->bind("getBusInfo", boost::bind(&TopicManager::getBusInfoCallback, this, _1, _2));
  xmlrpc_manager_->bind("getSubscriptions", boost::bind(&TopicManager::getSubscriptionsCallback, this, _1, _2));
  xmlrpc_manager_->bind("getPublications", boost::bind(&TopicManager::getPublicationsCallback, this, _1, _2));
}

ConnectionManager位于connection_manager.cpp,它实实在在地执行了进程间TCP的通信,是真正的执行者。

void ConnectionManager::start()
{
  poll_manager_ = PollManager::instance();
  poll_conn_ = poll_manager_->addPollThreadListener(boost::bind(&ConnectionManager::removeDroppedConnections, 
								this));
 
  // Bring up the TCP listener socket
  tcpserver_transport_ = boost::make_shared<TransportTCP>(&poll_manager_->getPollSet());
  // 在这个函数中将调用socket中的listen函数开始监听TCP连接请求
  if (!tcpserver_transport_->listen(network::getTCPROSPort(), 
				    MAX_TCPROS_CONN_QUEUE, 
				    boost::bind(&ConnectionManager::tcprosAcceptConnection, this, _1)))
  {
    ROS_FATAL("Listen on port [%d] failed", network::getTCPROSPort());
    ROS_BREAK();
  }
 
  // Bring up the UDP listener socket
  udpserver_transport_ = boost::make_shared<TransportUDP>(&poll_manager_->getPollSet());
  if (!udpserver_transport_->createIncoming(0, true))
  {
    ROS_FATAL("Listen failed");
    ROS_BREAK();
  }
}

PollManager是用来管理发布器的消息发布的,所利用的是信号与槽的机制,在publish部分使用。

void PollManager::start()
{
  shutting_down_ = false;
  thread_ = boost::thread(&PollManager::threadFunc, this);
}

XMLRPCmanager的start函数中有一个循环在不断轮转,如下所示,它不断地将新注册的话题与发布器信息添加到处理队列中,而xmlrpc存在一个处理器,也在不断发布处理队列的消息给rosmaster。

void XMLRPCManager::serverThreadFunc()
{
  disableAllSignalsInThisThread();
 
  while(!shutting_down_)
  {
    {
      boost::mutex::scoped_lock lock(added_connections_mutex_);
      S_ASyncXMLRPCConnection::iterator it = added_connections_.begin();
      S_ASyncXMLRPCConnection::iterator end = added_connections_.end();
      for (; it != end; ++it)
      {
        (*it)->addToDispatch(server_.get_dispatch());
        connections_.insert(*it);
      }
 
      added_connections_.clear();
    }
   。。。。

nodehandle是客户端,server端是在rosmaster启动时,通过rosgraph的ThreadingXMLRPCServer启动的,这样便实现了rosmaster与nodehandle的联系。

热门文章

暂无图片
编程学习 ·

那些年让我们目瞪口呆的bug

程序员一生与bug奋战&#xff0c;可谓是杀敌无数&#xff0c;见怪不怪了&#xff01;在某知识社交平台中&#xff0c;一个“有哪些让程序员目瞪口呆的bug”的话题引来了6700多万的阅读&#xff0c;可见程序员们对一个话题的敏感度有多高。 1、麻省理工“只能发500英里的邮件” …
暂无图片
编程学习 ·

redis的下载与安装

下载redis wget http://download.redis.io/releases/redis-5.0.0.tar.gz解压redis tar -zxvf redis-5.0.0.tar.gz编译 make安装 make install快链方便进入redis ln -s redis-5.0.0 redis
暂无图片
编程学习 ·

《大话数据结构》第三章学习笔记--线性表(一)

线性表的定义 线性表&#xff1a;零个或多个数据元素的有限序列。 线性表元素的个数n定义为线性表的长度。n为0时&#xff0c;为空表。 在比较复杂的线性表中&#xff0c;一个数据元素可以由若干个数据项组成。 线性表的存储结构 顺序存储结构 可以用C语言中的一维数组来…
暂无图片
编程学习 ·

对象的扩展

文章目录对象的扩展属性的简洁表示法属性名表达式方法的name属性属性的可枚举性和遍历可枚举性属性的遍历super关键字对象的扩展运算符解构赋值扩展运算符AggregateError错误对象对象的扩展 属性的简洁表示法 const foo bar; const baz {foo}; baz // {foo: "bar"…
暂无图片
编程学习 ·

让程序员最头疼的5种编程语言

世界上的编程语言&#xff0c;按照其应用领域&#xff0c;可以粗略地分成三类。 有的语言是多面手&#xff0c;在很多不同的领域都能派上用场。大家学过的编程语言很多都属于这一类&#xff0c;比如说 C&#xff0c;Java&#xff0c; Python。 有的语言专注于某一特定的领域&…
暂无图片
编程学习 ·

写论文注意事项

参考链接 给研究生修改了一篇论文后&#xff0c;该985博导几近崩溃…… 重点分析 摘要与结论几乎重合 这一条是我见过研究生论文中最常出现的事情&#xff0c;很多情况下&#xff0c;他们论文中摘要部分与结论部分重复率超过70%。对于摘要而言&#xff0c;首先要用一小句话引…
暂无图片
编程学习 ·

安卓 串口开发

上图&#xff1a; 上码&#xff1a; 在APP grable添加 // 串口 需要配合在项目build.gradle中的repositories添加 maven {url "https://jitpack.io" }implementation com.github.licheedev.Android-SerialPort-API:serialport:1.0.1implementation com.jakewhart…
暂无图片
编程学习 ·

2021-2027年中国铪市场调研与发展趋势分析报告

2021-2027年中国铪市场调研与发展趋势分析报告 本报告研究中国市场铪的生产、消费及进出口情况&#xff0c;重点关注在中国市场扮演重要角色的全球及本土铪生产商&#xff0c;呈现这些厂商在中国市场的铪销量、收入、价格、毛利率、市场份额等关键指标。此外&#xff0c;针对…
暂无图片
编程学习 ·

Aggressive cows题目翻译

描述&#xff1a; Farmer John has built a new long barn, with N (2 < N < 100,000) stalls.&#xff08;John农民已经新建了一个长畜棚带有N&#xff08;2<N<100000&#xff09;个牛棚&#xff09; The stalls are located along a straight line at positions…
暂无图片
编程学习 ·

剖析组建PMO的6个大坑︱PMO深度实践

随着事业环境因素的不断纷繁演进&#xff0c;项目时代正在悄悄来临。设立项目经理转岗、要求PMP等项目管理证书已是基操&#xff0c;越来越多的组织开始组建PMO团队&#xff0c;大有曾经公司纷纷建造中台的气质&#xff08;当然两者的本质并不相同&#xff0c;只是说明这个趋势…
暂无图片
编程学习 ·

Flowable入门系列文章118 - 进程实例 07

1、获取流程实例的变量 GET运行时/进程实例/ {processInstanceId} /变量/ {变量名} 表1.获取流程实例的变量 - URL参数 参数需要值描述processInstanceId是串将流程实例的id添加到变量中。变量名是串要获取的变量的名称。 表2.获取流程实例的变量 - 响应代码 响应码描述200指…
暂无图片
编程学习 ·

微信每天自动给女[男]朋友发早安和土味情话

微信通知&#xff0c;每天给女朋友发早安、情话、诗句、天气信息等~ 前言 之前逛GitHub的时候发现了一个自动签到的小工具&#xff0c;b站、掘金等都可以&#xff0c;我看了下源码发现也是很简洁&#xff0c;也尝试用了一下&#xff0c;配置也都很简单&#xff0c;主要是他有一…
暂无图片
编程学习 ·

C语言二分查找详解

二分查找是一种知名度很高的查找算法&#xff0c;在对有序数列进行查找时效率远高于传统的顺序查找。 下面这张动图对比了二者的效率差距。 二分查找的基本思想就是通过把目标数和当前数列的中间数进行比较&#xff0c;从而确定目标数是在中间数的左边还是右边&#xff0c;将查…
暂无图片
编程学习 ·

项目经理,你有什么优势吗?

大侠被一个问题问住了&#xff1a;你和别人比&#xff0c;你的优势是什么呢? 大侠听到这个问题后&#xff0c;脱口而出道&#xff1a;“项目管理能力和经验啊。” 听者抬头看了一下大侠&#xff0c;显然听者对大侠的这个回答不是很满意&#xff0c;但也没有继续追问。 大侠回家…
暂无图片
编程学习 ·

nginx的负载均衡和故障转移

#注&#xff1a;proxy_temp_path和proxy_cache_path指定的路径必须在同一分区 proxy_temp_path /data0/proxy_temp_dir; #设置Web缓存区名称为cache_one&#xff0c;内存缓存空间大小为200MB&#xff0c;1天没有被访问的内容自动清除&#xff0c;硬盘缓存空间大小为30GB。 pro…
暂无图片
编程学习 ·

业务逻辑漏洞

身份认证安全 绕过身份认证的几种方法 暴力破解 测试方法∶在没有验证码限制或者一次验证码可以多次使用的地方&#xff0c;可以分为以下几种情况︰ (1)爆破用户名。当输入的用户名不存在时&#xff0c;会显示请输入正确用户名&#xff0c;或者用户名不存在 (2)已知用户名。…