色哟哟视频在线观看-色哟哟视频在线-色哟哟欧美15最新在线-色哟哟免费在线观看-国产l精品国产亚洲区在线观看-国产l精品国产亚洲区久久

0
  • 聊天消息
  • 系統(tǒng)消息
  • 評(píng)論與回復(fù)
登錄后你可以
  • 下載海量資料
  • 學(xué)習(xí)在線課程
  • 觀看技術(shù)視頻
  • 寫文章/發(fā)帖/加入社區(qū)
會(huì)員中心
創(chuàng)作中心

完善資料讓更多小伙伴認(rèn)識(shí)你,還能領(lǐng)取20積分哦,立即完善>

3天內(nèi)不再提示

ROS機(jī)器人操作系統(tǒng)的實(shí)現(xiàn)原理

STM32嵌入式開(kāi)發(fā) ? 來(lái)源:STM32嵌入式開(kāi)發(fā) ? 2024-04-27 02:39 ? 次閱讀

本文介紹ROS機(jī)器人操作系統(tǒng)(Robot Operating System)的實(shí)現(xiàn)原理,從最底層分析ROS代碼是如何實(shí)現(xiàn)的。

1、序列化

通信的內(nèi)容(也就是消息message)序列化是通信的基礎(chǔ),所以我們先研究序列化。 盡管筆者從事機(jī)器人學(xué)習(xí)和研發(fā)很長(zhǎng)時(shí)間了,但是在研究ROS的過(guò)程中,“序列化”這個(gè)詞還是這輩子第一次聽(tīng)到。 所以可想而知很多人在看到“把一個(gè)消息序列化”這樣的描述時(shí)是如何一臉懵逼。 但其實(shí)序列化是一個(gè)比較常見(jiàn)的概念,你雖然不知道它但一定接觸過(guò)它。 下面我們先介紹“序列化”的一些常識(shí),然后解釋ROS里的序列化是怎么做的?

1.1什么是序列化? “序列化”(Serialization )的意思是將一個(gè)對(duì)象轉(zhuǎn)化為字節(jié)流。 這里說(shuō)的對(duì)象可以理解為“面向?qū)ο蟆崩锏哪莻€(gè)對(duì)象,具體的就是存儲(chǔ)在內(nèi)存中的對(duì)象數(shù)據(jù)。 與之相反的過(guò)程是“反序列化”(Deserialization )。 雖然掛著機(jī)器人的羊頭,但是后面的介紹全部是計(jì)算機(jī)知識(shí),跟機(jī)器人一丁點(diǎn)關(guān)系都沒(méi)有,序列化就是一個(gè)純粹的計(jì)算機(jī)概念。 序列化的英文Serialize就有把一個(gè)東西變成一串連續(xù)的東西之意。 形象的描述,數(shù)據(jù)對(duì)象是一團(tuán)面,序列化就是將面團(tuán)拉成一根面條,反序列化就將面條捏回面團(tuán)。 另一個(gè)形象的類比是我們?cè)趯?duì)話或者打電話時(shí),一個(gè)人的思想轉(zhuǎn)換成一維的語(yǔ)音,然后在另一個(gè)人的頭腦里重新變成結(jié)構(gòu)化的思想,這也是一種序列化。

11ad290a-fc76-11ee-a297-92fbcf53809c.jpg

面對(duì)序列化,很多人心中可能會(huì)有很多疑問(wèn)。 首先,為什么要序列化?或者更具體的說(shuō),既然對(duì)象的信息本來(lái)就是以字節(jié)的形式儲(chǔ)存在內(nèi)存中,那為什么要多此一舉把一些字節(jié)數(shù)據(jù)轉(zhuǎn)換成另一種形式的、一維的、連續(xù)的字節(jié)數(shù)據(jù)呢? 如果我們的程序在內(nèi)存中存儲(chǔ)了一個(gè)數(shù)字,比如25。那要怎么傳遞25這個(gè)數(shù)字給別的程序節(jié)點(diǎn)或者把這個(gè)數(shù)字永久存儲(chǔ)起來(lái)呢? 很簡(jiǎn)單,直接傳遞25這個(gè)數(shù)字(的字節(jié)表示,即0X19,當(dāng)然最終會(huì)變成二進(jìn)制表示11001以高低電平傳輸存儲(chǔ))或者直接把這個(gè)數(shù)字(的字節(jié)表示)寫進(jìn)硬盤里即可。 所以,對(duì)于本來(lái)就是連續(xù)的、一維的、一連串的數(shù)據(jù)(例如字符串),序列化并不需要做太多東西,其本質(zhì)是就是由內(nèi)存向其它地方拷貝數(shù)據(jù)而已。 所以,如果你在一個(gè)序列化庫(kù)里看到memcpy函數(shù)不用覺(jué)得奇怪,因?yàn)槟阒佬蛄谢畹讓硬贿^(guò)就是在操作內(nèi)存數(shù)據(jù)而已(還有些庫(kù)使用了流的ostream.rdbuf()->sputn函數(shù))。

可是實(shí)際程序操作的對(duì)象很少是這么簡(jiǎn)單的形式,大多數(shù)時(shí)候我們面對(duì)的是包含不同數(shù)據(jù)類型(int、double、string)的復(fù)雜數(shù)據(jù)結(jié)構(gòu)(比如vector、list),它們很可能在內(nèi)存中是不連續(xù)存儲(chǔ)的而是分散在各處。比如ROS的很多消息都包含向量。 數(shù)據(jù)中還有各種指針和引用。而且,如果數(shù)據(jù)要在運(yùn)行于不同架構(gòu)的計(jì)算機(jī)之上的、由不同編程語(yǔ)言所編寫的節(jié)點(diǎn)程序之間傳遞,那問(wèn)題就更復(fù)雜了,它們的字節(jié)順序endianness規(guī)定有可能不一樣,基本數(shù)據(jù)類型(比如int)的長(zhǎng)度也不一樣(有的int是4個(gè)字節(jié)、有的是8個(gè)字節(jié))。 這些都不是通過(guò)簡(jiǎn)單地、原封不動(dòng)地復(fù)制粘貼原始數(shù)據(jù)就能解決的。這時(shí)候就需要序列化和反序列化了。 所以在程序之間需要通信時(shí)(ROS恰好就是這種情況),或者希望保存程序的中間運(yùn)算結(jié)果時(shí),序列化就登場(chǎng)了。 另外,在某種程度上,序列化還起到統(tǒng)一標(biāo)準(zhǔn)的作用。

11be5a90-fc76-11ee-a297-92fbcf53809c.jpg

我們把被序列化的東西叫object(對(duì)象),它可以是任意的數(shù)據(jù)結(jié)構(gòu)或者對(duì)象:結(jié)構(gòu)體、數(shù)組、類的實(shí)例等等。 把序列化后得到的東西叫archive,它既可以是人類可讀的文本形式,也可以是二進(jìn)制形式。 前者比如JSON和XML,這兩個(gè)是網(wǎng)絡(luò)應(yīng)用里最常用的序列化格式,通過(guò)記事本就能打開(kāi)閱讀; 后者就是原始的二進(jìn)制文件,比如后綴名是bin的文件,人類是沒(méi)辦法直接閱讀一堆的0101或者0XC9D23E72的。 序列化算是一個(gè)比較常用的功能,所以大多數(shù)編程語(yǔ)言(比如C++PythonJava等)都會(huì)附帶用于序列化的庫(kù),不需要你再去造輪子。 以C++為例,雖然標(biāo)準(zhǔn)STL庫(kù)沒(méi)有提供序列化功能,但是第三方庫(kù)Boost提供了[ 2 ]谷歌的protobuf也是一個(gè)序列化庫(kù),還有Fast-CDR,以及不太知名的Cereal,Java自帶序列化函數(shù),python可以使用第三方的pickle模塊實(shí)現(xiàn)。 總之,序列化沒(méi)有什么神秘的,用戶可以看看這些開(kāi)源的序列化庫(kù)代碼,或者自己寫個(gè)小程序試試簡(jiǎn)單數(shù)據(jù)的序列化,例如這個(gè)例子,或者這個(gè),有助于更好地理解ROS中的實(shí)現(xiàn)。

1.2ROS中的序列化實(shí)現(xiàn)

理解了序列化,再回到ROS。我們發(fā)現(xiàn),ROS沒(méi)有采用第三方的序列化工具,而是選擇自己實(shí)現(xiàn),代碼在roscpp_core項(xiàng)目下的roscpp_serialization中,見(jiàn)下圖。這個(gè)功能涉及的代碼量不是很多。 為什么ROS不使用現(xiàn)成的序列化工具或者庫(kù)呢?可能ROS誕生的時(shí)候(2007年),有些序列化庫(kù)可能還不存在(protobuf誕生于2008年),更有可能是ROS的創(chuàng)造者認(rèn)為當(dāng)時(shí)沒(méi)有合適的工具。

1.2.1serialization.h 核心的函數(shù)都在serialization.h里,簡(jiǎn)而言之,里面使用了C語(yǔ)言標(biāo)準(zhǔn)庫(kù)的memcpy函數(shù)把消息拷貝到流中。 下面來(lái)看一下具體的實(shí)現(xiàn)。 序列化功能的特點(diǎn)是要處理很多種數(shù)據(jù)類型,針對(duì)每種具體的類型都要實(shí)現(xiàn)相應(yīng)的序列化函數(shù)。 為了盡量減少代碼量,ROS使用了模板的概念,所以代碼里有一堆的template。 從后往前梳理,先看Stream這個(gè)結(jié)構(gòu)體吧。在C++里結(jié)構(gòu)體和類基本沒(méi)什么區(qū)別,結(jié)構(gòu)體里也可以定義函數(shù)。 Stream翻譯為流,流是一個(gè)計(jì)算機(jī)中的抽象概念,前面我們提到過(guò)字節(jié)流,它是什么意思呢? 在需要傳輸數(shù)據(jù)的時(shí)候,我們可以把數(shù)據(jù)想象成傳送帶上連續(xù)排列的一個(gè)個(gè)被傳送的物體,它們就是一個(gè)流。 更形象的,可以想象磁帶或者圖靈機(jī)里連續(xù)的紙帶。在文件讀寫、使用串口、網(wǎng)絡(luò)Socket通信等領(lǐng)域,流經(jīng)常被使用。例如我們常用的輸入輸出流:

cout<<"helllo"; 由于使用很多,流的概念也在演變。想了解更多可以看這里。

struct Stream
{
 // Returns a pointer to the current position of the stream
 inline uint8_t* getData() { return data_; }
 // Advances the stream, checking bounds, and returns a pointer to the position before it was advanced.
 // 	hrows StreamOverrunException if len would take this stream past the end of its buffer
 ROS_FORCE_INLINE uint8_t* advance(uint32_t len)
{
  uint8_t* old_data = data_;
  data_ += len;
  if (data_ > end_)
  {
   // Throwing directly here causes a significant speed hit due to the extra code generated for the throw statement
   throwStreamOverrun();
  }
  return old_data;
 }
 // Returns the amount of space left in the stream
 inline uint32_t getLength() { return static_cast(end_ - data_); }
 
protected:
 Stream(uint8_t* _data, uint32_t _count) : data_(_data), end_(_data + _count) {}


private:
 uint8_t* data_;
 uint8_t* end_;
};
注釋表明Stream是個(gè)基類,輸入輸出流IStream和OStream都繼承自它。 Stream的成員變量data_是個(gè)指針,指向序列化的字節(jié)流開(kāi)始的位置,它的類型是uint8_t。 在Ubuntu系統(tǒng)中,uint8_t的定義是typedef unsigned char uint8_t; 所以u(píng)int8_t就是一個(gè)字節(jié),可以用size_of()函數(shù)檢驗(yàn)。data_指向的空間就是保存字節(jié)流的。

輸出流類OStream用來(lái)序列化一個(gè)對(duì)象,它引用了serialize函數(shù),如下。

struct OStream : public Stream
{
 static const StreamType stream_type = stream_types::Output;
 OStream(uint8_t* data, uint32_t count) : Stream(data, count) {}
 /* Serialize an item to this output stream*/
 template
 ROS_FORCE_INLINE void next(const T& t)
{
  serialize(*this, t);
 }
 template
 ROS_FORCE_INLINE OStream& operator<<(const T& t)
 ?{
 ? ?serialize(*this, t);
 ? ?return *this;
 ?}
};

輸入流類IStream用來(lái)反序列化一個(gè)字節(jié)流,它引用了deserialize函數(shù),如下。

struct ROSCPP_SERIALIZATION_DECL IStream : public Stream
{
 static const StreamType stream_type = stream_types::Input;
 IStream(uint8_t* data, uint32_t count) : Stream(data, count) {}
 /* Deserialize an item from this input stream */
 template
 ROS_FORCE_INLINE void next(T& t)
{
  deserialize(*this, t);
 }
 template
 ROS_FORCE_INLINE IStream& operator>>(T& t)
 {
  deserialize(*this, t);
  return *this;
 }
};

自然,serialize函數(shù)和deserialize函數(shù)就是改變數(shù)據(jù)形式的地方,它們的定義在比較靠前的地方。它們都接收兩個(gè)模板,都是內(nèi)聯(lián)函數(shù),然后里面沒(méi)什么東西,只是又調(diào)用了Serializer類的成員函數(shù)write和read。所以,serialize和deserialize函數(shù)就是個(gè)二道販子。

// Serialize an object. Stream here should normally be a ros::OStream
template
inline void serialize(Stream& stream, const T& t)
{
 Serializer::write(stream, t);
}
// Deserialize an object. Stream here should normally be a ros::IStream
template
inline void deserialize(Stream& stream, T& t)
{
 Serializer::read(stream, t);
}
所以,我們來(lái)分析Serializer類,如下。我們發(fā)現(xiàn),write和read函數(shù)又調(diào)用了類型里的serialize函數(shù)和deserialize函數(shù)。 頭別暈,這里的serialize和deserialize函數(shù)跟上面的同名函數(shù)不是一回事。 注釋中說(shuō):“Specializing the Serializer class is the only thing you need to do to get the ROS serialization system to work with a type”(要想讓ROS的序列化功能適用于其它的某個(gè)類型,你唯一需要做的就是特化這個(gè)Serializer類)。

這就涉及到的另一個(gè)知識(shí)點(diǎn)——模板特化(template specialization)。

template struct Serializer
{
 // Write an object to the stream. Normally the stream passed in here will be a ros::OStream
 template
 inline static void write(Stream& stream, typename boost::call_traits::param_type t)
{
  t.serialize(stream.getData(), 0);
 }
  // Read an object from the stream. Normally the stream passed in here will be a ros::IStream
 template
 inline static void read(Stream& stream, typename boost::call_traits::reference t)
{
  t.deserialize(stream.getData());
 }
 // Determine the serialized length of an object.
 inline static uint32_t serializedLength(typename boost::call_traits::param_type t)
{
  return t.serializationLength();
 }
};
接著又定義了一個(gè)帶參數(shù)的宏函數(shù)ROS_CREATE_SIMPLE_SERIALIZER(Type),然后把這個(gè)宏作用到了ROS中的10種基本數(shù)據(jù)類型,分別是:uint8_t, int8_t, uint16_t, int16_t, uint32_t, int32_t, uint64_t, int64_t, float, double。 說(shuō)明這10種數(shù)據(jù)類型的處理方式都是類似的。看到這里大家應(yīng)該明白了,write和read函數(shù)都使用了memcpy函數(shù)進(jìn)行數(shù)據(jù)的移動(dòng)。 注意宏定義中的template<>語(yǔ)句,這正是模板特化的標(biāo)志,關(guān)鍵詞template后面跟一對(duì)尖括號(hào)。


關(guān)于模板特化可以看這里。

#define ROS_CREATE_SIMPLE_SERIALIZER(Type) 
 template<> struct Serializer 
 { 
  template inline static void write(Stream& stream, const Type v) 
{ 
   memcpy(stream.advance(sizeof(v)), &v, sizeof(v) ); 
  } 
  template inline static void read(Stream& stream, Type& v) 
{ 
   memcpy(&v, stream.advance(sizeof(v)), sizeof(v) ); 
  } 
  inline static uint32_t serializedLength(const Type&) 
{ 
   return sizeof(Type); 
  } 
};
ROS_CREATE_SIMPLE_SERIALIZER(uint8_t)
ROS_CREATE_SIMPLE_SERIALIZER(int8_t)
ROS_CREATE_SIMPLE_SERIALIZER(uint16_t)
ROS_CREATE_SIMPLE_SERIALIZER(int16_t)
ROS_CREATE_SIMPLE_SERIALIZER(uint32_t)
ROS_CREATE_SIMPLE_SERIALIZER(int32_t)
ROS_CREATE_SIMPLE_SERIALIZER(uint64_t)
ROS_CREATE_SIMPLE_SERIALIZER(int64_t)
ROS_CREATE_SIMPLE_SERIALIZER(float)
ROS_CREATE_SIMPLE_SERIALIZER(double)
對(duì)于其它類型的數(shù)據(jù),例如bool、std::string、std::vector、ros::Time、ros::Duration、boost::array等等,它們各自的處理方式有細(xì)微的不同,所以不再用上面的宏函數(shù),而是用模板特化的方式每種單獨(dú)定義,這也是為什么serialization.h這個(gè)文件這么冗長(zhǎng)。 對(duì)于int、double這種單個(gè)元素的數(shù)據(jù),直接用上面特化的Serializer類中的memcpy函數(shù)實(shí)現(xiàn)序列化。 對(duì)于vector、array這種多個(gè)元素的數(shù)據(jù)類型怎么辦呢?方法是分成幾種情況,對(duì)于固定長(zhǎng)度簡(jiǎn)單類型的(fixed-size simple types),還是用各自特化的Serializer類中的memcpy函數(shù)實(shí)現(xiàn),沒(méi)啥太大區(qū)別。
對(duì)于固定但是類型不簡(jiǎn)單的(fixed-size non-simple types)或者既不固定也不簡(jiǎn)單的(non-fixed-size, non-simple types)或者固定但是不簡(jiǎn)單的(fixed-size, non-simple types),用for循環(huán)遍歷,一個(gè)元素一個(gè)元素的單獨(dú)處理。 那怎么判斷一個(gè)數(shù)據(jù)是不是固定是不是簡(jiǎn)單呢?這是在roscpp_traits文件夾中的message_traits.h完成的。 其中采用了萃取Type Traits,這是相對(duì)高級(jí)一點(diǎn)的編程技巧了,筆者也不太懂。 對(duì)序列化的介紹暫時(shí)就到這里了,有一些細(xì)節(jié)還沒(méi)講,等筆者看懂了再補(bǔ)。

2、消息訂閱發(fā)布

2.1ROS的本質(zhì) 如果問(wèn)ROS的本質(zhì)是什么,或者用一句話概括ROS的核心功能。那么,筆者認(rèn)為ROS就是個(gè)通信庫(kù),讓不同的程序節(jié)點(diǎn)能夠相互對(duì)話。 很多文章和書籍在介紹ROS是什么的時(shí)候,經(jīng)常使用“ROS是一個(gè)通信框架”這種描述。 但是筆者認(rèn)為這種描述并不是太合適。“框架”是個(gè)對(duì)初學(xué)者非常不友好的抽象詞匯,用一個(gè)更抽象難懂的概念去解釋一個(gè)本來(lái)就不清楚的概念,對(duì)初學(xué)者起不到任何幫助。 而且筆者嚴(yán)重懷疑絕大多數(shù)作者能對(duì)機(jī)器人的本質(zhì)或者軟件框架能有什么太深的理解,他們的見(jiàn)解不會(huì)比你我深刻多少。 既然提到本質(zhì),那我們就深入到最基本的問(wèn)題。 在接觸無(wú)窮的細(xì)節(jié)之前,我們不妨先做一個(gè)哲學(xué)層面的思考。 那就是,為什么ROS要解決通信問(wèn)題? 機(jī)器人涉及的東西千千萬(wàn)萬(wàn),機(jī)械電子、軟件、人工智能無(wú)所不包,為什么底層的設(shè)計(jì)是一套用來(lái)通信的程序而不是別的東西。 到目前為止,我還沒(méi)有看到有人討論過(guò)這個(gè)問(wèn)題。這要回到機(jī)器人或者智能的本質(zhì)。 當(dāng)我們?cè)谡務(wù)摍C(jī)器人的時(shí)候,最首要的問(wèn)題不是硬件設(shè)計(jì),而是對(duì)信息的處理。

一個(gè)機(jī)器人需要哪些信息,信息從何而來(lái),如何傳遞,又被誰(shuí)使用,這些才是最重要的問(wèn)題。 人類飛不鳥(niǎo),游不過(guò)魚,跑不過(guò)馬,力不如牛,為什么卻自稱萬(wàn)物之靈呢。 因?yàn)槿擞写竽X,而且人類大腦處理的信息更多更復(fù)雜。 拋開(kāi)物質(zhì),從信息的角度看,人與動(dòng)物、與機(jī)器人存在很多相似的地方。 機(jī)器人由許多功能模塊組成,它們之間需要協(xié)作才能形成一個(gè)有用的整體,機(jī)器人與機(jī)器人之間也需要協(xié)作才能形成一個(gè)有用的系統(tǒng),要協(xié)作就離不開(kāi)通信。 需要什么樣的信息以及信息從何而來(lái)不是ROS首先關(guān)心的,因?yàn)檫@取決于機(jī)器人的應(yīng)用場(chǎng)景。 因此,ROS首先要解決的是通信的問(wèn)題,即如何建立通信、用什么方式通信、通信的格式是什么等等一系列具體問(wèn)題。 帶著這些問(wèn)題,我們看看ROS是如何設(shè)計(jì)的。 2.2客戶端庫(kù) 實(shí)現(xiàn)通信的代碼在ros_comm包中,如下。 其中clients文件夾一共有127個(gè)文件,看來(lái)是最大的包了。 現(xiàn)在我們來(lái)到了ROS最核心的地帶。

11dd6944-fc76-11ee-a297-92fbcf53809c.png

客戶端這個(gè)名詞出現(xiàn)的有些突然,一個(gè)機(jī)器人操作系統(tǒng)里為什么需要客戶端。 原因是,節(jié)點(diǎn)與主節(jié)點(diǎn)master之間的關(guān)系是client/server,這時(shí)每個(gè)節(jié)點(diǎn)都是一個(gè)客戶端(client),而master自然就是服務(wù)器端(server)。 那客戶端庫(kù)(client libraries)是干什么的?就是為實(shí)現(xiàn)節(jié)點(diǎn)之間通信的。 雖然整個(gè)文件夾中包含的文件眾多,但是我們?nèi)绻凑找欢ǖ拿}絡(luò)來(lái)分析就不會(huì)眼花繚亂。 節(jié)點(diǎn)之間最主要的通信方式就是基于消息的。為了實(shí)現(xiàn)這個(gè)目的,需要三個(gè)步驟,如下。 弄明白這三個(gè)步驟就明白R(shí)OS的工作方式了。這三個(gè)步驟看起來(lái)是比較合乎邏輯的,并不奇怪。

消息的發(fā)布者和訂閱者(即消息的接收方)建立連接; 發(fā)布者向話題發(fā)布消息,訂閱者在話題上接收消息,將消息保存在回調(diào)函數(shù)隊(duì)列中; 調(diào)用回調(diào)函數(shù)隊(duì)列中的回調(diào)函數(shù)處理消息。 2.2.1一個(gè)節(jié)點(diǎn)的誕生 在建立連接之前,首先要有節(jié)點(diǎn)。 節(jié)點(diǎn)就是一個(gè)獨(dú)立的程序,它運(yùn)行起來(lái)后就是一個(gè)普通的進(jìn)程,與計(jì)算機(jī)中其它的進(jìn)程并沒(méi)有太大區(qū)別。 一個(gè)問(wèn)題是:ROS中為什么把一個(gè)獨(dú)立的程序稱為“節(jié)點(diǎn)” 這是因?yàn)镽OS沿用了計(jì)算機(jī)網(wǎng)絡(luò)中“節(jié)點(diǎn)”的概念。 在一個(gè)網(wǎng)絡(luò)中,例如互聯(lián)網(wǎng),每一個(gè)上網(wǎng)的計(jì)算機(jī)就是一個(gè)節(jié)點(diǎn)。前面我們看到的客戶端、服務(wù)器這樣的稱呼,也是從計(jì)算機(jī)網(wǎng)絡(luò)中借用的。 下面來(lái)看一下節(jié)點(diǎn)是如何誕生的。我們?cè)诘谝淮问褂肦OS時(shí),一般都會(huì)照著官方教程編寫一個(gè)talker和一個(gè)listener節(jié)點(diǎn),以熟悉ROS的使用方法。

我們以talker為例,它的部分代碼如下。

#include "ros/ros.h"
int main(int argc, char **argv)
{
  /* You must call one of the versions of ros::init() before using any other part of the ROS system. */
  ros::init(argc, argv, "talker");
  ros::NodeHandle n;
main函數(shù)里首先調(diào)用了init()函數(shù)初始化一個(gè)節(jié)點(diǎn),該函數(shù)的定義在init.cpp文件中。 當(dāng)我們的程序運(yùn)行到init()函數(shù)時(shí),一個(gè)節(jié)點(diǎn)就呱呱墜地了。 而且在出生的同時(shí)我們還順道給他起好了名字,也就是"talker"。 名字是隨便起的,但是起名是必須的。 我們進(jìn)入init()函數(shù)里看看它做了什么,代碼如下,看上去還是挺復(fù)雜的。它初始化了一個(gè)叫g(shù)_global_queue的數(shù)據(jù),它的類型是CallbackQueuePtr。 這是個(gè)相當(dāng)重要的類,叫“回調(diào)隊(duì)列”,后面還會(huì)見(jiàn)到它。init()函數(shù)還調(diào)用了network、master、this_node、file_log、param這幾個(gè)命名空間里的init初始化函數(shù)各自實(shí)現(xiàn)一些變量的初始化,這些變量都以g開(kāi)頭,例如g_host、g_uri,用來(lái)表明它們是全局變量。 其中,network::init完成節(jié)點(diǎn)主機(jī)名、IP地址等的初始化,master::init獲取master的URI、主機(jī)號(hào)和端口號(hào)。

this_node::init定義節(jié)點(diǎn)的命名空間和節(jié)點(diǎn)的名字,沒(méi)錯(cuò),把我們給節(jié)點(diǎn)起的名字就存儲(chǔ)在這里。file_log::init初始化日志文件的路徑。

void init(const M_string& remappings, const std::string& name, uint32_t options)
{
 if (!g_atexit_registered) {
  g_atexit_registered = true;
  atexit(atexitCallback);
 }
 if (!g_global_queue) {
  g_global_queue.reset(new CallbackQueue);
 }
 if (!g_initialized) {
  g_init_options = options;
  g_ok = true;
  ROSCONSOLE_AUTOINIT;
  // Disable SIGPIPE
#ifndef WIN32
  signal(SIGPIPE, SIG_IGN);
#else
  WSADATA wsaData;
  WSAStartup(MAKEWORD(2, 0), &wsaData);
#endif
  check_ipv6_environment();
  network::init(remappings);
  master::init(remappings);
  // names:: namespace is initialized by this_node
  this_node::init(name, remappings, options);
  file_log::init(remappings);
  param::init(remappings);
  g_initialized = true;
 }
}
完成初始化以后,就進(jìn)入下一步ros::NodeHandle n定義句柄。 我們?cè)龠M(jìn)入node_handle.cpp文件,發(fā)現(xiàn)構(gòu)造函數(shù)NodeHandle::NodeHandle調(diào)用了自己的construct函數(shù)。然后,順藤摸瓜找到construct函數(shù),它里面又調(diào)用了ros::start()函數(shù)。 沒(méi)錯(cuò),我們又繞回到了init.cpp文件。 ros::start()函數(shù)主要實(shí)例化了幾個(gè)重要的類,如下。 完成實(shí)例化后馬上又調(diào)用了各自的start()函數(shù),啟動(dòng)相應(yīng)的動(dòng)作。 這些都做完了以后就可以發(fā)布或訂閱消息了。

一個(gè)節(jié)點(diǎn)的故事暫時(shí)就到這了。

TopicManager::instance()->start();
ServiceManager::instance()->start();
ConnectionManager::instance()->start();
PollManager::instance()->start();
XMLRPCManager::instance()->start();
2.2.1XMLRPC是什么? 關(guān)于ROS節(jié)點(diǎn)建立連接的技術(shù)細(xì)節(jié),官方文檔說(shuō)的非常簡(jiǎn)單,在這里ROS Technical Overview。沒(méi)有基礎(chǔ)的同學(xué)看這個(gè)介紹必然還是不懂。 在ROS中,節(jié)點(diǎn)與節(jié)點(diǎn)之間的通信依靠節(jié)點(diǎn)管理器(master)牽線搭橋。 master像一個(gè)中介,它介紹節(jié)點(diǎn)們互相認(rèn)識(shí)。一旦節(jié)點(diǎn)們認(rèn)識(shí)了以后,master就完成自己的任務(wù)了,它就不再摻和了。 這也是為什么你啟動(dòng)節(jié)點(diǎn)后再殺死m(xù)aster,節(jié)點(diǎn)之間的通信依然保持正常的原因。 使用過(guò)電驢和迅雷而且研究過(guò)BitTorrent的同學(xué)對(duì)master的工作方式應(yīng)該很熟悉,master就相當(dāng)于Tracker服務(wù)器,它存儲(chǔ)著其它節(jié)點(diǎn)的信息。 我們每次下載之前都會(huì)查詢Tracker服務(wù)器,找到有電影資源的節(jié)點(diǎn),然后就可以與它們建立連接并開(kāi)始下載電影了。 那么master是怎么給節(jié)點(diǎn)牽線搭橋的呢?ROS使用了一種叫XMLRPC的方式實(shí)現(xiàn)這個(gè)功能。 XMLRPC中的RPC的意思是遠(yuǎn)程過(guò)程調(diào)用(Remote Procedure Call)。 簡(jiǎn)單來(lái)說(shuō),遠(yuǎn)程過(guò)程調(diào)用的意思就是一個(gè)計(jì)算機(jī)中的程序(在我們這就是節(jié)點(diǎn)啦)可以調(diào)用另一個(gè)計(jì)算機(jī)中的函數(shù),只要這兩個(gè)計(jì)算機(jī)在一個(gè)網(wǎng)絡(luò)中。 這是一種聽(tīng)上去很高大上的功能,它能讓節(jié)點(diǎn)去訪問(wèn)網(wǎng)絡(luò)中另一臺(tái)計(jì)算機(jī)上的程序資源。 XMLRPC中的XML我們?cè)?.1節(jié)講消息序列化時(shí)提到了,它就是一種數(shù)據(jù)表示方式而已。 所以合起來(lái),XMLRPC的意思就是把由XML表示的數(shù)據(jù)發(fā)送給其它計(jì)算機(jī)上的程序運(yùn)行。 運(yùn)行后返回的結(jié)果仍然以XML格式返回回來(lái),然后我們通過(guò)解析它(還原回純粹的數(shù)據(jù))就能干別的事了。

想了解更多XMLRPC的細(xì)節(jié)可以看這個(gè)XML-RPC:概述。

舉個(gè)例子,一個(gè)XMLRPC請(qǐng)求是下面這個(gè)樣子的。因?yàn)閄MLRPC是基于HTTP協(xié)議的,所以下面的就是個(gè)標(biāo)準(zhǔn)的HTTP報(bào)文。

POST / HTTP/1.1
User-Agent: XMLRPC++ 0.7
Host: localhost:11311
Content-Type: text/xml
Content-length: 78




  circleArea
   
     
      2.41
     
   
如果你沒(méi)學(xué)過(guò)HTTP協(xié)議,看上面的語(yǔ)句可能會(huì)感到陌生。《圖解HTTP》這本小書可以讓你快速入門。 HTTP報(bào)文比較簡(jiǎn)單,它分兩部分,前半部分是頭部,后半部分是主體。 頭部和主體之間用空行分開(kāi),這都是HTTP協(xié)議規(guī)定的標(biāo)準(zhǔn)。 上面主體部分的格式就是XML,見(jiàn)的多了你就熟悉了。 所以,XMLRPC傳遞的消息其實(shí)就是主體部分是XML格式的HTTP報(bào)文而已,沒(méi)什么神秘的。 對(duì)應(yīng)客戶端一個(gè)XMLRPC請(qǐng)求,服務(wù)器端會(huì)執(zhí)行它并返回一個(gè)響應(yīng),它也是一個(gè)HTTP報(bào)文,如下。

它的結(jié)構(gòu)和請(qǐng)求一樣,不再解釋了。所以,XMLRPC跟我們上網(wǎng)瀏覽網(wǎng)頁(yè)的過(guò)程其實(shí)差不多。

HTTP/1.1 200 OK
Date: Sat, 06 Oct 2001 2304 GMT
Server: Apache.1.3.12 (Unix)
Connection: close
Content-Type: text/xml
Content-Length: 124




  
   
     18.24668429131
   
  
2.2.2ROS中XMLRPC的實(shí)現(xiàn) 上面的例子解釋了XMLRPC是什么?下面我們看看ROS是如何實(shí)現(xiàn)XMLRPC的。 ROS使用的XMLRPC介紹在這里: http://wiki.ros.org/xmlrpcpp。這次ROS的創(chuàng)作者沒(méi)有從零開(kāi)始造輪子,而是在一個(gè)已有的XMLRPC庫(kù)的基礎(chǔ)上改造的。 XMLRPC的C++代碼在下載后的ros_comm-noetic-develutilitiesxmlrpcpp路徑下。 還好,整個(gè)工程不算太大。XMLRPC分成客戶端和服務(wù)器端兩大部分。

咱們先看客戶端,主要代碼在XmlRpcClient.cpp文件里。

擒賊先擒王,XmlRpcClient.cpp文件中最核心的函數(shù)就是execute,用于執(zhí)行遠(yuǎn)程調(diào)用,代碼如下。

// Execute the named procedure on the remote server.
// Params should be an array of the arguments for the method.
// Returns true if the request was sent and a result received (although the result might be a fault).
bool XmlRpcClient::execute(const char* method, XmlRpcValue const& params, XmlRpcValue& result)
{
 XmlRpcUtil::log(1, "XmlRpcClient: method %s (_connectionState %s).", method, connectionStateStr(_connectionState));


 // This is not a thread-safe operation, if you want to do multithreading, use separate
 // clients for each thread. If you want to protect yourself from multiple threads
 // accessing the same client, replace this code with a real mutex.
 if (_executing)
  return false;


 _executing = true;
 ClearFlagOnExit cf(_executing);


 _sendAttempts = 0;
 _isFault = false;


 if ( ! setupConnection())
  return false;


 if ( ! generateRequest(method, params))
  return false;


 result.clear();
 double msTime = -1.0;  // Process until exit is called
 _disp.work(msTime);


 if (_connectionState != IDLE || ! parseResponse(result)) {
  _header = "";
  return false;
 }


 // close() if server does not supports HTTP1.1
 // otherwise, reusing the socket to write leads to a SIGPIPE because
 // the remote server could shut down the corresponding socket.
 if (_header.find("HTTP/1.1 200 OK", 0, 15) != 0) {
  close();
 }


 XmlRpcUtil::log(1, "XmlRpcClient: method %s completed.", method);
 _header = "";
 _response = "";
 return true;
}
它首先調(diào)用setupConnection()函數(shù)與服務(wù)器端建立連接。 連接成功后,調(diào)用generateRequest()函數(shù)生成發(fā)送請(qǐng)求報(bào)文。

XMLRPC請(qǐng)求報(bào)文的頭部又交給generateHeader()函數(shù)做了,代碼如下。

// Prepend http headers
std::string XmlRpcClient::generateHeader(size_t length) const
{
 std::string header = 
  "POST " + _uri + " HTTP/1.1
"
  "User-Agent: ";
 header += XMLRPC_VERSION;
 header += "
Host: ";
 header += _host;


 char buff[40];
 std::snprintf(buff,40,":%d
", _port);


 header += buff;
 header += "Content-Type: text/xml
Content-length: ";


 std::snprintf(buff,40,"%zu

", length);
 return header + buff;
}

主體部分則先將遠(yuǎn)程調(diào)用的方法和參數(shù)變成XML格式,generateRequest()函數(shù)再將頭部和主體組合成完整的報(bào)文,如下:

std::string header = generateHeader(body.length());
_request = header + body;
把報(bào)文發(fā)給服務(wù)器后,就開(kāi)始靜靜地等待。 一旦接收到服務(wù)器返回的報(bào)文后,就調(diào)用parseResponse函數(shù)解析報(bào)文數(shù)據(jù),也就是把XML格式變成純凈的數(shù)據(jù)格式。 我們發(fā)現(xiàn),XMLRPC使用了socket功能實(shí)現(xiàn)客戶端和服務(wù)器通信。 我們搜索socket這個(gè)單詞,發(fā)現(xiàn)它原始的意思是插座,如下。這非常形象,建立連接實(shí)現(xiàn)通信就像把插頭插入插座。 雖說(shuō)XMLRPC也是ROS的一部分,但它畢竟只是一個(gè)基礎(chǔ)功能,我們會(huì)用即可,暫時(shí)不去探究其實(shí)現(xiàn)細(xì)節(jié), 所以對(duì)它的分析到此為止。下面我們來(lái)看節(jié)點(diǎn)是如何調(diào)用XMLRPC的。 2.2.3節(jié)點(diǎn)間通過(guò)XMLRPC建立連接 在一個(gè)節(jié)點(diǎn)剛啟動(dòng)的時(shí)候,它并不知道其它節(jié)點(diǎn)的存在,更不知道它們?cè)诮徽勈裁矗?dāng)然也就談不上通信。 所以,它要先與master對(duì)話查詢其它節(jié)點(diǎn)的狀態(tài),然后再與其它節(jié)點(diǎn)通信。 而節(jié)點(diǎn)與master對(duì)話使用的就是XMLRPC。 從這一點(diǎn)來(lái)看,master叫節(jié)點(diǎn)管理器確實(shí)名副其實(shí),它是一個(gè)大管家,給剛出生的節(jié)點(diǎn)提供服務(wù)。 下面我們以兩個(gè)節(jié)點(diǎn):talker和listener為例,介紹其通過(guò)XMLRPC建立通信連接的過(guò)程,如下圖所示。

1210938c-fc76-11ee-a297-92fbcf53809c.jpg

1.talker注冊(cè) 假設(shè)我們先啟動(dòng)talker。啟動(dòng)后,它通過(guò)1234端口使用XMLRPC向master注冊(cè)自己的信息,包含所發(fā)布消息的話題名。master會(huì)將talker的注冊(cè)信息加入注冊(cè)列表中; 2.listener注冊(cè) listener啟動(dòng)后,同樣通過(guò)XMLRPC向master注冊(cè)自己的信息,包含需要訂閱的話題名; 3.master進(jìn)行匹配 master根據(jù)listener的訂閱信息從注冊(cè)列表中查找,如果沒(méi)有找到匹配的發(fā)布者,則等待發(fā)布者的加入,如果找到匹配的發(fā)布者信息,則通過(guò)XMLRPC向listener發(fā)送talker的地址信息。 4.listener發(fā)送連接請(qǐng)求 listener接收到master發(fā)回的talker地址信息,嘗試通過(guò)XMLRPC向talker發(fā)送連接請(qǐng)求,傳輸訂閱的話題名、消息類型以及通信協(xié)議(TCP或者UDP); 5.talker確認(rèn)連接請(qǐng)求 talker接收到listener發(fā)送的連接請(qǐng)求后,繼續(xù)通過(guò)XMLRPC向listener確認(rèn)連接信息,其中包含自身的TCP地址信息; 6.listener嘗試與talker建立連接 listener接收到確認(rèn)信息后,使用TCP嘗試與talker建立網(wǎng)絡(luò)連接。 7.talker向listener發(fā)布消息 成功建立連接后,talker開(kāi)始向listener發(fā)送話題消息數(shù)據(jù),master不再參與。 從上面的分析中可以發(fā)現(xiàn),前五個(gè)步驟使用的通信協(xié)議都是XMLRPC,最后發(fā)布數(shù)據(jù)的過(guò)程才使用到TCP。 master只在節(jié)點(diǎn)建立連接的過(guò)程中起作用,但是并不參與節(jié)點(diǎn)之間最終的數(shù)據(jù)傳輸。 節(jié)點(diǎn)在請(qǐng)求建立連接時(shí)會(huì)通過(guò)master.cpp文件中的execute()函數(shù)調(diào)用XMLRPC庫(kù)中的函數(shù)。

我們舉個(gè)例子,加入talker節(jié)點(diǎn)要發(fā)布消息,它會(huì)調(diào)用topic_manager.cpp中的TopicManager::advertise()函數(shù),在函數(shù)中會(huì)調(diào)用execute()函數(shù),該部分代碼如下。

 XmlRpcValue args, result, payload;
 args[0] = this_node::getName();
 args[1] = ops.topic;
 args[2] = ops.datatype;
 args[3] = xmlrpc_manager_->getServerURI();
 master::execute("registerPublisher", args, result, payload, true);
其中,registerPublisher就是一個(gè)遠(yuǎn)程過(guò)程調(diào)用的方法(或者叫函數(shù))。節(jié)點(diǎn)通過(guò)這個(gè)遠(yuǎn)程過(guò)程調(diào)用向master注冊(cè),表示自己要發(fā)布發(fā)消息了。 你可能會(huì)問(wèn),registerPublisher方法在哪里被執(zhí)行了呢?我們來(lái)到ros_comm-noetic-devel ools osmastersrc osmaster路徑下,打開(kāi)master_api.py文件,然后搜索registerPublisher這個(gè)方法,就會(huì)找到對(duì)應(yīng)的代碼,如下。 匆匆掃一眼就知道,它在通知所有訂閱這個(gè)消息的節(jié)點(diǎn),讓它們做好接收消息的準(zhǔn)備。 你可能注意到了,這個(gè)被調(diào)用的XMLRPC是用python語(yǔ)言實(shí)現(xiàn)的。

也就是說(shuō),XMLRPC通信時(shí)只要報(bào)文的格式是一致的,不管C++還是python語(yǔ)言,都可以實(shí)現(xiàn)遠(yuǎn)程調(diào)用的功能。

def registerPublisher(self, caller_id, topic, topic_type, caller_api):
    try:
      self.ps_lock.acquire()
      self.reg_manager.register_publisher(topic, caller_id, caller_api)
      # don't let '*' type squash valid typing
      if topic_type != rosgraph.names.ANYTYPE or not topic in self.topics_types:
        self.topics_types[topic] = topic_type
      pub_uris = self.publishers.get_apis(topic)
      sub_uris = self.subscribers.get_apis(topic)
      self._notify_topic_subscribers(topic, pub_uris, sub_uris)
      mloginfo("+PUB [%s] %s %s",topic, caller_id, caller_api)
      sub_uris = self.subscribers.get_apis(topic)      
    finally:
      self.ps_lock.release()
    return 1, "Registered [%s] as publisher of [%s]"%(caller_id, topic), sub_uris

2.3master是什么?

當(dāng)我們?cè)诿钚兄休斎雛oscore想啟動(dòng)ROS的節(jié)點(diǎn)管理器時(shí),背后到底發(fā)生了什么?我們先用Ubuntu的which命令找找roscore這個(gè)命令在什么地方,發(fā)現(xiàn)它位于/opt/ros/melodic/bin/roscore路徑下,如下圖。再用file命令查看它的屬性,發(fā)現(xiàn)它是一個(gè)Python腳本。

1224d18a-fc76-11ee-a297-92fbcf53809c.jpg

2.3.1roscore腳本 我們回到自己下載的源碼:ros_comm文件夾中,找到roscore文件,它在 os_comm-melodic-devel ools oslaunchscripts路徑下。 雖然它是個(gè)Python腳本,但是卻沒(méi)有.py后綴名。 用記事本打開(kāi)它,迎面第一句話是#!/usr/bin/env python,說(shuō)明這還是一個(gè)python 2版本的腳本。

我們發(fā)現(xiàn)這個(gè)roscore只是個(gè)空殼,真正重要的只有最后一行指令,如下

import roslaunch
roslaunch.main(['roscore', '--core'] + sys.argv[1:])
2.3.2roslaunch模塊 2.3.2.1XMLRPC服務(wù)器如何啟動(dòng)?

roscore調(diào)用了roslaunch.main,我們繼續(xù)追蹤,進(jìn)到ros_comm-noetic-devel ools oslaunchsrc oslaunch文件夾中,發(fā)現(xiàn)有個(gè)__init__.py文件,說(shuō)明這個(gè)文件夾是一個(gè)python包,打開(kāi)__init__.py文件找到def main(argv=sys.argv),這就是roscore調(diào)用的函數(shù)roslaunch.main的實(shí)現(xiàn),如下(這里只保留主要的代碼,不太重要的刪掉了)。

def main(argv=sys.argv):
  options = None
  logger = None
  try:
    from . import rlutil
    parser = _get_optparse()
    
    (options, args) = parser.parse_args(argv[1:])
    args = rlutil.resolve_launch_arguments(args)
    write_pid_file(options.pid_fn, options.core, options.port)
    uuid = rlutil.get_or_generate_uuid(options.run_id, options.wait_for_master)
    configure_logging(uuid)
    # #3088: don't check disk usage on remote machines
    if not options.child_name and not options.skip_log_check:
      rlutil.check_log_disk_usage()


    logger = logging.getLogger('roslaunch')
    logger.info("roslaunch starting with args %s"%str(argv))
    logger.info("roslaunch env is %s"%os.environ)
      
    if options.child_name:
      # 這里沒(méi)執(zhí)行到,就不列出來(lái)了
    else:
      logger.info('starting in server mode')
      # #1491 change terminal name
      if not options.disable_title:
        rlutil.change_terminal_name(args, options.core)
      # Read roslaunch string from stdin when - is passed as launch filename.
      roslaunch_strs = []
      # This is a roslaunch parent, spin up parent server and launch processes.
      # args are the roslaunch files to load
      from . import parent as roslaunch_parent
      # force a port binding spec if we are running a core
      if options.core:
        options.port = options.port or DEFAULT_MASTER_PORT
      p = roslaunch_parent.ROSLaunchParent(uuid, args, roslaunch_strs=roslaunch_strs, is_core=options.core, port=options.port, local_only=options.local_only, verbose=options.verbose, force_screen=options.force_screen, force_log=options.force_log, num_workers=options.num_workers, timeout=options.timeout, master_logger_level=options.master_logger_level, show_summary=not options.no_summary, force_required=options.force_required, sigint_timeout=options.sigint_timeout, sigterm_timeout=options.sigterm_timeout)
      p.start()
      p.spin()
roslaunch.main開(kāi)啟了日志,日志記錄的信息可以幫我們了解main函數(shù)執(zhí)行的順序。


我們?nèi)buntu的.ros/log/路徑下,打開(kāi)roslaunch-ubuntu-52246.log日志文件,內(nèi)容如下。

1232dbc2-fc76-11ee-a297-92fbcf53809c.jpg

通過(guò)閱讀日志我們發(fā)現(xiàn),main函數(shù)首先檢查日志文件夾磁盤占用情況,如果有剩余空間就繼續(xù)往下運(yùn)行。 然后把運(yùn)行roscore的終端的標(biāo)題給改了。 再調(diào)用ROSLaunchParent類中的函數(shù),這大概就是main函數(shù)中最重要的地方了。 ROSLaunchParent類的定義是在同一路徑下的parent.py文件中。為什么叫LaunchParent筆者也不清楚。 先不管它,我們?cè)倏慈罩荆l(fā)現(xiàn)運(yùn)行到了下面這個(gè)函數(shù),它打算啟動(dòng)XMLRPC服務(wù)器端。

所以調(diào)用的順序是:roslaunch\__init__.py文件中的main()函數(shù)調(diào)用parent.pystart()函數(shù),start()函數(shù)調(diào)用自己類中的_start_infrastructure()函數(shù),_start_infrastructure()函數(shù)調(diào)用自己類中的_start_server()函數(shù),_start_server()函數(shù)再調(diào)用server.py中的start函數(shù)。

    def _start_server(self):
        self.logger.info("starting parent XML-RPC server")
        self.server = roslaunch.server.ROSLaunchParentNode(self.config, self.pm)
        self.server.start()

我們?cè)龠M(jìn)到server.py文件中,找到ROSLaunchNode類,里面的start函數(shù)又調(diào)用了父類XmlRpcNode中的start函數(shù)。

class ROSLaunchNode(xmlrpc.XmlRpcNode):
  """
  Base XML-RPC server for roslaunch parent/child processes
  """
  def start(self):
    logger.info("starting roslaunch XML-RPC server")
    super(ROSLaunchNode, self).start()
我們來(lái)到ros_comm-noetic-devel ools osgraphsrc osgraph路徑,找到xmlrpc.py文件。找到class XmlRpcNode(object)類,再進(jìn)入start(self)函數(shù),發(fā)現(xiàn)它調(diào)用了自己類的run函數(shù),run函數(shù)又調(diào)用了自己類中的_run函數(shù),_run函數(shù)又調(diào)用了自己類中的_run_init()函數(shù),在這里才調(diào)用了真正起作用的ThreadingXMLRPCServer類。 因?yàn)閙aster節(jié)點(diǎn)是用python實(shí)現(xiàn)的,所以,需要有python版的XMLRPC庫(kù)。 幸運(yùn)的是,python有現(xiàn)成的XMLRPC庫(kù),叫SimpleXMLRPCServer。SimpleXMLRPCServer已經(jīng)內(nèi)置到python中了,無(wú)需安裝。

所以,ThreadingXMLRPCServer類直接繼承了SimpleXMLRPCServer,如下。

class ThreadingXMLRPCServer(socketserver.ThreadingMixIn, SimpleXMLRPCServer)
2.3.2.2master如何啟動(dòng)? 我們?cè)賮?lái)看看節(jié)點(diǎn)管理器master是如何被啟動(dòng)的,再回到parent.pystart()函數(shù),如下。

我們發(fā)現(xiàn)它啟動(dòng)了XMLRPC服務(wù)器后,接下來(lái)就調(diào)用了_init_runner()函數(shù)

  def start(self, auto_terminate=True):
    self.logger.info("starting roslaunch parent run")
    # load config, start XMLRPC servers and process monitor
    try:
      self._start_infrastructure()
    except:
      self._stop_infrastructure()
    # Initialize the actual runner. 
    self._init_runner()
    # Start the launch
    self.runner.launch()

init_runner()函數(shù)實(shí)例化了ROSLaunchRunner類,這個(gè)類的定義在launch.py里。

    def _init_runner(self):
         self.runner = roslaunch.launch.ROSLaunchRunner(self.run_id, self.config, server_uri=self.server.uri, ...)
實(shí)例化完成后,parent.pystart()函數(shù)就調(diào)用了它的launch()函數(shù)。 我們打開(kāi)launch.py文件,找到launch()函數(shù),發(fā)現(xiàn)它又調(diào)用了自己類中的_setup()函數(shù),而_setup()函數(shù)又調(diào)用了_launch_master()函數(shù)。 看名字就能猜出來(lái),_launch_master()函數(shù)肯定是啟動(dòng)節(jié)點(diǎn)管理器master的,它調(diào)用了create_master_process函數(shù),這個(gè)函數(shù)在nodeprocess.py里。 所以我們打開(kāi)nodeprocess.py,create_master_process函數(shù)使用了LocalProcess類。這個(gè)類繼承自Process類。nodeprocess.py文件引用了python中用于創(chuàng)建新的進(jìn)程的subprocess模塊。 create_master_process函數(shù)實(shí)例化LocalProcess類用的是’rosmaster’,即ros_comm-noetic-devel ools osmaster中的包。 main.py文件中的rosmaster_main函數(shù)使用了master.py中的Master類。

Master類中又用到了master_api.py中的ROSMasterHandler類,這個(gè)類包含所有的XMLRPC服務(wù)器接收的遠(yuǎn)程調(diào)用,一共24個(gè),如下。

shutdown(self, caller_id, msg='')
getUri(self, caller_id)
getPid(self, caller_id)
deleteParam(self, caller_id, key)
setParam(self, caller_id, key, value)
getParam(self, caller_id, key)
searchParam(self, caller_id, key)
subscribeParam(self, caller_id, caller_api, key)
unsubscribeParam(self, caller_id, caller_api, key)
hasParam(self, caller_id, key)
getParamNames(self, caller_id)
param_update_task(self, caller_id, caller_api, param_key, param_value)
_notify_topic_subscribers(self, topic, pub_uris, sub_uris)
registerService(self, caller_id, service, service_api, caller_api)
lookupService(self, caller_id, service)
unregisterService(self, caller_id, service, service_api)
registerSubscriber(self, caller_id, topic, topic_type, caller_api)
unregisterSubscriber(self, caller_id, topic, caller_api)
registerPublisher(self, caller_id, topic, topic_type, caller_api)
unregisterPublisher(self, caller_id, topic, caller_api)
lookupNode(self, caller_id, node_name)
getPublishedTopics(self, caller_id, subgraph)
getTopicTypes(self, caller_id) 
getSystemState(self, caller_id)
2.3.2.1檢查log文件夾占用空間 roslaunch這個(gè)python包還負(fù)責(zé)檢查保存log的文件夾有多大。在ros_comm-noetic-devel ools oslaunchsrc oslaunch\__init__.py文件中的main函數(shù)里,有以下語(yǔ)句。


看名字就知道是干啥的了。

rlutil.check_log_disk_usage()
再打開(kāi)同一路徑下的rlutil.py,發(fā)現(xiàn)它又調(diào)用了rosclean包中的get_disk_usage函數(shù)。 我們發(fā)現(xiàn),這個(gè)函數(shù)里直接寫死了比較的上限:disk_usage > 1073741824,當(dāng)然這樣不太好,應(yīng)該改為可配置的。 數(shù)字1073741824的單位是字節(jié),剛好就是1GB(102 4 3 1024^31024 3byte)。

我們要是想修改log文件夾報(bào)警的上限,直接改這個(gè)值即可。

def check_log_disk_usage():
  """
  Check size of log directory. If high, print warning to user
  """
  try:
    d = rospkg.get_log_dir()
    roslaunch.core.printlog("Checking log directory for disk usage. This may take a while.
Press Ctrl-C to interrupt") 
    disk_usage = rosclean.get_disk_usage(d)
    # warn if over a gig
    if disk_usage > 1073741824:
      roslaunch.core.printerrlog("WARNING: disk usage in log directory [%s] is over 1GB.
It's recommended that you use the 'rosclean' command."%d)
    else:
      roslaunch.core.printlog("Done checking log file disk usage. Usage is <1GB.") ? ? ? ? ? ?
 ? ?except:
 ? ? ? ?pass
我們刨根問(wèn)底,追查rosclean.get_disk_usage(d)是如何實(shí)現(xiàn)的。 這個(gè)rosclean包不在ros_comm里面,需要單獨(dú)下載。 打開(kāi)后發(fā)現(xiàn)這個(gè)包還是跨平臺(tái)的,給出了Windows和Linux下的實(shí)現(xiàn)。 如果是Windows系統(tǒng),用os.path.getsize函數(shù)獲取文件的大小,通過(guò)os.walk函數(shù)遍歷所有文件,加起來(lái)就是文件夾的大小。 如果是Linux系統(tǒng),用Linux中的du -sb命令獲取文件夾的大小。哎,搞個(gè)機(jī)器人不僅要學(xué)習(xí)python,還得熟悉Linux,容易嗎?

123e1b54-fc76-11ee-a297-92fbcf53809c.gif

主節(jié)點(diǎn)會(huì)獲取用戶設(shè)置的ROS_MASTER_URI變量中列出的URI地址和端口號(hào)(默認(rèn)為當(dāng)前的本地IP和11311端口號(hào))。 3、時(shí)間 不只是機(jī)器人,在任何一個(gè)系統(tǒng)里,時(shí)間都是一個(gè)繞不開(kāi)的重要話題。下面我們就從萬(wàn)物的起點(diǎn)——時(shí)間開(kāi)始吧。 ROS中定義時(shí)間的程序都在roscpp_core項(xiàng)目下的rostime中,見(jiàn)下圖。 如果細(xì)分一下,時(shí)間其實(shí)有兩種,一種叫“時(shí)刻”,也就是某個(gè)時(shí)間點(diǎn); 一種叫“時(shí)段”或者時(shí)間間隔,也就是兩個(gè)時(shí)刻之間的部分。這兩者的代碼是分開(kāi)實(shí)現(xiàn)的,時(shí)刻是time,時(shí)間間隔是duration。 在Ubuntu中把rostime文件夾中的文件打印出來(lái),發(fā)現(xiàn)確實(shí)有time和duration兩類文件,但是還多了個(gè)rate,如下圖所示。

125a5850-fc76-11ee-a297-92fbcf53809c.png

我們還注意到,里面有 CMakeLists.txt 和 package.xml 兩個(gè)文件,那說(shuō)明rostime本身也是個(gè)ROS的package,可以單獨(dú)編譯。 3.1時(shí)刻time 先看下文件間的依賴關(guān)系。跟時(shí)刻有關(guān)的文件是兩個(gè)time.h文件和一個(gè)time.cpp文件。 time.h給出了類的聲明,而impl ime.h給出了類運(yùn)算符重載的實(shí)現(xiàn),time.cpp給出了其它函數(shù)的實(shí)現(xiàn)。 3.1.1TimeBase基類 首先看time.h文件,它定義了一個(gè)叫TimeBase的類。注釋中說(shuō),TimeBase是個(gè)基類,定義了兩個(gè)成員變量uint32_t sec, nsec,還重載了+ ++、? -?、< <<、> >>、= ==等運(yùn)算符。 成員變量uint32_t sec, nsec其實(shí)就是時(shí)間的秒和納秒兩部分,它們合起來(lái)構(gòu)成一個(gè)完整的時(shí)刻。 至于為啥要分成兩部分而不是用一個(gè)來(lái)定義,可能是考慮到整數(shù)表示精度的問(wèn)題。 因?yàn)?2位整數(shù)最大表示的數(shù)字是2147483647。如果我們要用納秒這個(gè)范圍估計(jì)是不夠的。 你可能會(huì)問(wèn),機(jī)器人系統(tǒng)怎么會(huì)使用到納秒這么高精度的時(shí)間分辨率,畢竟控制器定時(shí)器最高精度可能也只能到微秒? 如果你做過(guò)自動(dòng)駕駛項(xiàng)目,有使用過(guò)GPS和激光雷達(dá)傳感器的經(jīng)驗(yàn),就會(huì)發(fā)現(xiàn)GPS的時(shí)鐘精度就是納秒級(jí)的,它可以同步激光雷達(dá)每個(gè)激光點(diǎn)的時(shí)間戳。 還有,為什么定義TimeBase這個(gè)基類,原因大家很容易就能猜到。 因?yàn)樵诔绦蚶铮瑫r(shí)間本質(zhì)上就是一個(gè)數(shù)字而已,數(shù)字系統(tǒng)的序關(guān)系(能比較大小)和運(yùn)算(加減乘除)也同樣適用于時(shí)間這個(gè)東西。 當(dāng)然,這里只有加減沒(méi)有乘除,因?yàn)闀r(shí)間的乘除沒(méi)有意義。 3.1.2Time類 緊接著TimeBase類的是Time類,它是TimeBase的子類。我們做機(jī)器人應(yīng)用程序開(kāi)發(fā)時(shí)用不到TimeBase基類,但是Time類會(huì)經(jīng)常使用。 3.1.3now()函數(shù)

Time類比TimeBase類多了now()函數(shù),它是我們的老熟人了。在開(kāi)發(fā)應(yīng)用的時(shí)候,我們直接用下面的代碼就能得到當(dāng)前的時(shí)間戳:

ros::Time begin = ros::now(); //獲取當(dāng)前時(shí)間

now()函數(shù)的定義在rostimesrc ime.cpp里,因?yàn)樗艹S煤苤匾P者就把代碼粘貼在這里,如下。

函數(shù)很簡(jiǎn)單,可以看到,如果定義了使用仿真時(shí)間(g_use_sim_time為true),那就使用仿真時(shí)間,否則就使用墻上時(shí)間。

 Time Time::now()
{
  if (!g_initialized)
    throw TimeNotInitializedException();
  if (g_use_sim_time)
   {
    boost::scoped_lock lock(g_sim_time_mutex);
    Time t = g_sim_time;
    return t;
   }
  Time t;
  ros_walltime(t.sec, t.nsec);
  return t;
 }

在ROS里,時(shí)間分成兩類,一種叫仿真時(shí)間,一種叫墻上時(shí)間。 顧名思義,墻上時(shí)間就是實(shí)際的客觀世界的時(shí)間,它一秒一秒地流逝,誰(shuí)都不能改變它,讓它快一點(diǎn)慢一點(diǎn)都不可能,除非你有超能力。 仿真時(shí)間則是可以由你控制的,讓它快它就快。之所以多了一個(gè)仿真時(shí)間,是因?yàn)橛袝r(shí)我們?cè)诜抡鏅C(jī)器人希望可以自己控制時(shí)間,例如為了提高驗(yàn)證算法的效率,讓它按我們的期望速度推進(jìn)。 在使用墻上時(shí)間的情況下,now()函數(shù)調(diào)用了ros_walltime函數(shù),這個(gè)函數(shù)也在rostimesrc ime.cpp里。 剝開(kāi)層層洋蔥皮,最后發(fā)現(xiàn),這個(gè)ros_walltime函數(shù)才是真正調(diào)用操作系統(tǒng)時(shí)間函數(shù)的地方,而且它還是個(gè)跨平臺(tái)的實(shí)現(xiàn)(Windows和Linux)。 如果操作系統(tǒng)是Linux,那它會(huì)使用clock_gettime函數(shù),在筆者使用的Ubuntu 18.04系統(tǒng)中這個(gè)函數(shù)在usrinclude路徑下。 但是萬(wàn)一缺少這個(gè)函數(shù),那么ROS會(huì)使用gettimeofday函數(shù),但是gettimeofday沒(méi)有clock_gettime精確,clock_gettime能提供納秒的精確度。 如果操作系統(tǒng)是Windows,那它會(huì)使用標(biāo)準(zhǔn)庫(kù)STL的chrono庫(kù)獲取當(dāng)前的時(shí)刻,要用這個(gè)庫(kù)只需要引用它的頭文件,所以在time.cpp中引用了#include。 具體使用的函數(shù)就是 std::now().time_since_epoch()。

當(dāng)然,時(shí)間得是秒和納秒的形式,所以用了count方法:

uint64_t now_ns = std::duration_cast
         (std::now().time_since_epoch()).count();
3.1.4WallTime類 后面又接著聲明了WallTime類和SteadyTime類。 3.2時(shí)間間隔duration 時(shí)間間隔duration的定義與實(shí)現(xiàn)跟time時(shí)刻差不多,但是Duration類里的sleep()延時(shí)函數(shù)是在time.cpp里定義的,其中使用了Linux操作系統(tǒng)的nanosleep系統(tǒng)調(diào)用。

這個(gè)系統(tǒng)調(diào)用雖然叫納秒,但實(shí)際能實(shí)現(xiàn)的精度也就是幾十毫秒,即便這樣也比C語(yǔ)言提供的sleep函數(shù)的精度好多了。如果是Windows系統(tǒng)則調(diào)用STL chrono函數(shù):

std::nanoseconds(static_cast(sec * 1e9 + nsec)));
3.3頻率rate 關(guān)于Rate類,聲明注釋中是這么解釋的“Class to help run loops at a desired frequency”,也就是幫助(程序)按照期望的頻率循環(huán)執(zhí)行。

我們看看一個(gè)ROS節(jié)點(diǎn)是怎么使用Rate類的,如下圖所示的例子。

12639d66-fc76-11ee-a297-92fbcf53809c.jpg

首先用Rate的構(gòu)造函數(shù)實(shí)例化一個(gè)對(duì)象loop_rate。調(diào)用的構(gòu)造函數(shù)如下。 可見(jiàn),構(gòu)造函數(shù)使用輸入完成了對(duì)三個(gè)參數(shù)的初始化。 然后在While循環(huán)里調(diào)用sleep()函數(shù)實(shí)現(xiàn)一段時(shí)間的延遲。

既然用到延遲,所以就使用了前面的time類。

Rate::Rate(double frequency)
: start_(Time::now())
, expected_cycle_time_(1.0 / frequency)
, actual_cycle_time_(0.0)
{ }
3.4總結(jié) 至此rostime就解釋清楚了。可以看到,這部分實(shí)現(xiàn)主要是依靠STL標(biāo)準(zhǔn)庫(kù)函數(shù)(比如chrono)、BOOST庫(kù)(date_time)、Linux操作系統(tǒng)的系統(tǒng)調(diào)用以及標(biāo)準(zhǔn)的C函數(shù)。這就是ROS的底層了。 說(shuō)句題外話,在百度Apollo自動(dòng)駕駛項(xiàng)目中也受到了rostime的影響,其cyber ime中的Time、Duration、Rate類的定義方式與rostime中的類似,但是沒(méi)有定義基類,實(shí)現(xiàn)更加簡(jiǎn)單了。

審核編輯:黃飛

聲明:本文內(nèi)容及配圖由入駐作者撰寫或者入駐合作網(wǎng)站授權(quán)轉(zhuǎn)載。文章觀點(diǎn)僅代表作者本人,不代表電子發(fā)燒友網(wǎng)立場(chǎng)。文章及其配圖僅供工程師學(xué)習(xí)之用,如有內(nèi)容侵權(quán)或者其他違規(guī)問(wèn)題,請(qǐng)聯(lián)系本站處理。 舉報(bào)投訴
  • 機(jī)器人
    +關(guān)注

    關(guān)注

    211

    文章

    28476

    瀏覽量

    207414
  • 操作系統(tǒng)
    +關(guān)注

    關(guān)注

    37

    文章

    6840

    瀏覽量

    123409
  • 編程語(yǔ)言
    +關(guān)注

    關(guān)注

    10

    文章

    1946

    瀏覽量

    34801
  • 數(shù)據(jù)結(jié)構(gòu)

    關(guān)注

    3

    文章

    573

    瀏覽量

    40149
  • ROS
    ROS
    +關(guān)注

    關(guān)注

    1

    文章

    278

    瀏覽量

    17027

原文標(biāo)題:ROS機(jī)器人操作系統(tǒng)底層代碼實(shí)現(xiàn)原理

文章出處:【微信號(hào):c-stm32,微信公眾號(hào):STM32嵌入式開(kāi)發(fā)】歡迎添加關(guān)注!文章轉(zhuǎn)載請(qǐng)注明出處。

收藏 人收藏

    評(píng)論

    相關(guān)推薦

    ROS機(jī)器人開(kāi)發(fā)更便捷,基于RK3568J+Debian系統(tǒng)發(fā)布!

    本帖最后由 Tronlong創(chuàng)龍科技 于 2024-7-19 17:18 編輯 ROS系統(tǒng)是什么 ROS(Robot Operating System)是一個(gè)適用于機(jī)器人的開(kāi)源的
    發(fā)表于 07-09 11:38

    請(qǐng)問(wèn)能分享ROS機(jī)器人操作系統(tǒng)的一些資料嗎?

    菜鳥(niǎo)想要學(xué)習(xí)ROS機(jī)器人操作系統(tǒng),但是沒(méi)能找到系統(tǒng)的資料,請(qǐng)問(wèn)有哪位大神研究過(guò),可以分享一些資料嗎?跪謝了!!!!!!!!!
    發(fā)表于 02-19 23:14

    創(chuàng)龍TL5728-EasyEVM-A4開(kāi)發(fā)板如何移植ros機(jī)器人操作系統(tǒng)?

    創(chuàng)龍TL5728-EasyEVM-A4開(kāi)發(fā)板如何移植ros機(jī)器人操作系統(tǒng)?
    發(fā)表于 01-02 09:32

    ROS 2 Crystal Clemmys版機(jī)器人操作系統(tǒng)補(bǔ)充說(shuō)明

    ROS 2 Crystal Clemmys版機(jī)器人操作系統(tǒng)補(bǔ)充說(shuō)明
    發(fā)表于 06-10 11:29

    ROS RIKIBOT基礎(chǔ)--使用系列 第一章節(jié)】ROS機(jī)器人硬件系統(tǒng) 精選資料分享

    主控制器ROS控制器采用樹(shù)莓派4B或者Jetson Nano,運(yùn)行運(yùn)行Ubuntu Mate18.04或Ubuntu18.04系統(tǒng),具體型號(hào)有差異。系統(tǒng)安裝有ROS
    發(fā)表于 07-30 06:59

    ROS機(jī)器人開(kāi)發(fā)更便捷,基于RK3568J+Debian系統(tǒng)發(fā)布!

    ROS系統(tǒng)是什么 ROS(Robot Operating System)是一個(gè)適用于機(jī)器人的開(kāi)源的元操作系統(tǒng)。它提供了
    發(fā)表于 11-30 16:01

    ROS是什么?機(jī)器人操作系統(tǒng)ROS的介紹

    ROS 是 Robot Operating System 的縮寫,原本是斯坦福大學(xué)的一個(gè)機(jī)器人項(xiàng)目,后 來(lái)由 WillowGarage 公司發(fā)展,目前由 OSRF(Open Source
    發(fā)表于 09-13 17:23 ?14次下載
    <b class='flag-5'>ROS</b>是什么?<b class='flag-5'>機(jī)器人</b><b class='flag-5'>操作系統(tǒng)</b><b class='flag-5'>ROS</b>的介紹

    ROS的含義與機(jī)器人操作系統(tǒng)ROS的介紹

    Operating System 的縮寫,原本是斯坦福大學(xué)的一個(gè)機(jī)器人項(xiàng)目,后來(lái)由 WillowGarage 公司發(fā)展,目前由 OSRF(Open Source Robotics Foundation,Inc)公司維護(hù)的開(kāi)源項(xiàng)目。 1、首先是一個(gè)操作系統(tǒng) 根據(jù) wiki
    發(fā)表于 09-26 14:08 ?12次下載

    機(jī)器人操作系統(tǒng)ROS)是一種用于機(jī)器人開(kāi)發(fā)的元操作系統(tǒng)

    拋開(kāi)它的名字,ROS并不是一個(gè)真正的操作系統(tǒng)。相反,它是一個(gè)框架和服務(wù)的集合,這些框架和服務(wù)提供了類似操作系統(tǒng)的功能,運(yùn)行在由機(jī)器人硬件支撐的異構(gòu)計(jì)算機(jī)集群之上。
    的頭像 發(fā)表于 10-12 16:53 ?7131次閱讀

    機(jī)器人操作系統(tǒng)ROS詳細(xì)介紹

    機(jī)器人操作系統(tǒng)ROS詳細(xì)介紹 ROS機(jī)器人 操作系統(tǒng),RobotOperatingSyste
    發(fā)表于 11-18 18:09 ?2399次閱讀

    ROS機(jī)器人操作系統(tǒng)實(shí)現(xiàn)原理解析

    首先用Rate的構(gòu)造函數(shù)實(shí)例化一個(gè)對(duì)象loop_rate。調(diào)用的構(gòu)造函數(shù)如下。可見(jiàn),構(gòu)造函數(shù)使用輸入完成了對(duì)三個(gè)參數(shù)的初始化。
    發(fā)表于 01-03 10:56 ?666次閱讀

    ROS機(jī)器人操作系統(tǒng)實(shí)現(xiàn)原理(上)

    本文介紹ROS機(jī)器人操作系統(tǒng)(Robot Operating System)的實(shí)現(xiàn)原理,從最底層分析ROS代碼是如何
    的頭像 發(fā)表于 05-19 17:41 ?1001次閱讀
    <b class='flag-5'>ROS</b><b class='flag-5'>機(jī)器人</b><b class='flag-5'>操作系統(tǒng)</b>的<b class='flag-5'>實(shí)現(xiàn)</b>原理(上)

    ROS機(jī)器人操作系統(tǒng)實(shí)現(xiàn)原理(下)

    本文介紹ROS機(jī)器人操作系統(tǒng)(Robot Operating System)的實(shí)現(xiàn)原理,從最底層分析ROS代碼是如何
    的頭像 發(fā)表于 05-19 17:42 ?1184次閱讀

    ROS機(jī)器人操作系統(tǒng)

    可能很多初學(xué)者聽(tīng)到機(jī)器人操作系統(tǒng),就被“操作系統(tǒng)”幾個(gè)字嚇住了。其實(shí)簡(jiǎn)單點(diǎn)說(shuō),**ROS就是一個(gè)分布式的通信框架,幫助程序進(jìn)程之間更方便地通信。
    的頭像 發(fā)表于 05-19 17:46 ?2216次閱讀
    <b class='flag-5'>ROS</b><b class='flag-5'>機(jī)器人</b><b class='flag-5'>操作系統(tǒng)</b>

    一個(gè)機(jī)器人操作系統(tǒng)(ROS)節(jié)點(diǎn)系統(tǒng)

    電子發(fā)燒友網(wǎng)站提供《一個(gè)機(jī)器人操作系統(tǒng)(ROS)節(jié)點(diǎn)系統(tǒng).zip》資料免費(fèi)下載
    發(fā)表于 07-06 10:49 ?1次下載
    一個(gè)<b class='flag-5'>機(jī)器人</b><b class='flag-5'>操作系統(tǒng)</b>(<b class='flag-5'>ROS</b>)節(jié)點(diǎn)<b class='flag-5'>系統(tǒng)</b>
    主站蜘蛛池模板: 久久精品亚洲热综合一本奇米| 永久免费无码AV国产网站| 亚洲国产日韩欧美高清片a| 99re8在线视频精品| 国产午夜精品理论片免费观看 | 男生J桶进女人P又色又爽又黄| 香蕉尹人综合精品| bbwxxxx交女警| 久久久久久久久久毛片精品美女| 视频在线观看高清免费看| 99久久久久精品国产免费麻豆| 精品久久久久久久久免费影院| 四虎影视库永久免费| 99视频精品国产在线视频| 久久机热免费视频| 亚洲国产日韩a精品乱码| 古代荡女丫鬟高H辣文纯肉| 欧美成人国产| 中文字幕高清在线观看| 国产在线aaa片一区二区99| 色狗综合网| bbbbbxxxxx肥胖| 麻豆啊传媒app黄版破解免费| 亚洲欧美国产旡码专区| 国产超碰人人爱被IOS解锁| 欧美亚洲韩日午夜| 91素人约啪| 久久这里有精品| 亚洲欧美日本久久综合网站| 国产精品久久精品视| 色久天| ai换脸女明星被躁在线观看免费| 老奶奶50p| 一本道高清不卡v免费费| 国产手机在线视频| 视频一区视频二区在线观看| 波多野结衣二区| 女生下面免费看| 97视频在线免费播放| 快播av种子| 中文乱码35页在线观看|