Cifer

四月 29, 2018

关于模板

一定要合理使用模板, 一定要优先保证可读性…

关于指针

个人觉得, 能少用指针就少用, 优先级如下:

引用 > 智能指针 > 指针

关于 const

引入 const 有意义吗? 变量是否应该改变应该由代码确保, 而不需要编译器确保. 相比与 const 带来的安全性, 它带来了更多的麻烦:

  1. const 类型的变量不能传入接受非 const 类型参数的函数, 这要求我们定义函数时参数类型最好都定义成 const 的. (c++ primer 5th, p192)

Cifer

四月 27, 2018

acceptor 一般使用方法:

using boost::asio::ip::tcp;

boost::asio::io_context     io_context;
boost::system::error_code   ec;

tcp::acceptor   acceptor(io_context)
tcp::socket     socket(io_context);
tcp::endpoint   ep_listen(tcp::v4(), 13000);
tcp::endpoint   ep_peer;

acceptor.open(ep.protocol(), ec);
acceptor.bind(ep, ec);
acceptor.listen(5, ec);
acceptor.accept(socket, ep_peer, ec);

...

这其实是有些繁琐的, 实际上 acceptor 的构造函数还有这个:

basic_socket_acceptor(
    boost::asio::io_context & io_context,
    const endpoint_type & endpoint,
    bool reuse_addr = true);

这里我们可以传入一个 endpoint 作为第二个参数, 提供了这个 endpoint 参数的话,
acceptor 会同时帮我们做 open, bind, listen 三件事,
所以上述代码可以写成如下这么短:

using boost::asio::ip::tcp;

boost::asio::io_context     io_context;
boost::system::error_code   ec;

tcp::endpoint   ep;
tcp::acceptor   acceptor(io_context, tcp::endpoint(tcp::v4(), 13000))
tcp::socket     socket(io_context);

acceptor.accept(socket, ep, ec);

...

Daniel Larmer 列传

  • 2013 年, 全职挺进区块链行业. 在 bitcointalk 上找到了 Adam Levine 和 Charles Hoskinson 一起开发了 bitshares
  • 2014 年发布 Bitshares 第一版, 沿用了比特币的数据库技术和 UTXO 思想, 导致性能不好, 不能跑什么实际应用
  • 2015, 首次提出, 从 “So how does Delegated Proof of Stake work?” 这段开始介绍了 DPoS 的机制
  • 2015 年 1 月, 发表对 Nothing at Stake 问题的看法: http://bytemaster.github.io/article/2015/01/08/Nothing-at-Stake-Nothing-to-Fear/
  • 2015 年 6 月, 发布了石墨烯工具组, 在此基础上建立 Bitshares 2.0, 这个架构的一个特点是东西全 load 进内存, 出块时间 3s
  • 2016 以前, 在 http://bytemaster.github.io/ 上发文, 后来在 steemit 上, 再后来在 medium 上.
  • 2017 年, dan 发表文章称 PoW 没有解决拜占庭将军问题 (实际也确实如此), https://steemit.com/blockchain/@dantheman/the-problem-with-byzantine-generals
  • 2018 年 1 月, dan 录了两段视频介绍 DPoS/PBFT: https://twitter.com/go_eos/status/956607047449137153, 网上有文字翻译.
  • 2018 年 2 月, Ivan 对 BM 做了一次采访, 内容很全, 讲述了自己的生平, bitshares/steem 的创建, 还讨论了以太坊和 EOS, 还有 DPoS 的起源: https://twitter.com/IvanOnTech/status/960908134628896768. 网上能找到中文文字版.
  • 2018 年 4 月, 巨蟹在巴比特直播, 表示如果时光倒流, 希望 BM 不要离开 http://www.8btc.com/20180412
  • 2018 年 5 月 5 日, 有媒体对巨蟹做了一次独家专访, 谈到了巨蟹的公开市场操作 https://mp.weixin.qq.com/s/h3twkhoOSmGRv1aBJZ0SfQ

    以上信息来源于互联网

Graphene 源码阅读 – RPC 篇 – 通信协议与服务端实现

从现在开始我们进入一个新的篇章: RPC 篇, 这个篇章会包含客户端 (钱包, UI 等) 与节点间的通信细则, API 分类, 服务端的实现等内容, 最后会挑几个主要的 API 讲一下.

本文是 RPC 篇的第一章, 我们就先来介绍一下整体的通信机制和服务端 (节点) 实现.

通信协议

Bitshares 提供两种通信方式: http 和 websocket. 这俩最大的区别就是 websocket 是双向通信, 客户端和服务段都能主动向对方发送消息; 而 http 则只能由客户端主动发送消息. Websocket 的双向通信特性能够满足一些对实时性需求较高的应用.

Websocket 和 http 如此不同, 但却又难解难分, websocket 是在 http 之后出现的, 它复用了 http 的传输层以及协议端口, 并且握手过程也是使用 http 消息格式实现的, 只不过在 http 头部添加了几个新的 headers, 当服务端检测到 websocket 的 headers 时, 就会知道这是个 websocket 连接, 从而与传统的 http 请求过程区分开来.

刚说了 websocket 复用了 http 的传输层, http 的传输层可以是未加密的 tcp, 也可以是加密过的 tls, 那么 websocket 自然也可以用这两种传输层协议.

\ http websocket
tcp http:// ws://
tls https:// wss://

关于 websocket 协议的细则可以自行 google 一下, 这里就不再敖述了.

消息格式

不管是 websocket 还是 http, 客户端与节点通信时的消息体都是 json 格式, 一个典型的请求体内容如下:

{
 "jsonrpc": "2.0",
 "id": 1
 "method": "get_accounts",
 "params": [["1.2.0", "1.2.1"]],
}

其中 id 是自增的, jsonrpc 目前固定是 2.0. methodparams 不用过多解释一看便知其意. 返回体会因请求不同而不同, 但当然也是标准的 json 格式, 而且一般会包含 errno, errmsg 这样的通用字段, 不再贴出.

服务端实现

服务端的实现借助了 websocketapp 库, 这个库能够帮助我们方便的开发 websocket 服务端程序, 不但如此, 它也支持对普通 http 消息的处理, 因为前面说了 websocket 和 http 使用共同的传输层和端口, websocket 协议也只是在握手阶段使用 http 的消息格式, 所以 websocketapp 很容易区分客户端发来的是 websocket 消息还是普通的 http 消息, 相应的做不同的处理, 为此 websocketapp 提供了两个回调接口: on_message 和 on_http, 应用程序可以注册这两个回调方法. 当收到 websocket 消息时, on_message 会被调用; 而收到普通 http 消息时, on_http 会被调用.

除了 on_message 和 on_http 之外还有一个重要的回调是 on_connection, 它代表着有新的客户端连接过来.

Bitshares 代码中当然是实现了这三个回调的, 下面我们就来看一下服务端启动的主要流程以及对上面三个回调的实现.

服务流程

在节点启动时, 会调用 application::startup() 方法, 而这个方法的最后一个工作就是启动 RPC server, 这在 reset_websocket_server() 方法中去做:

// 代码 1.1

 277 void application_impl::reset_websocket_server()
 278 { try {
 279    if( !_options->count("rpc-endpoint") )
 280       return;
 281
 282    _websocket_server = std::make_shared<fc::http::websocket_server>();
 283    _websocket_server->on_connection( std::bind(&application_impl::new_connection, this, std::placeholders::_1) );
 284
 285    ilog("Configured websocket rpc to listen on ${ip}", ("ip",_options->at("rpc-endpoint").as<string>()));
 286    _websocket_server->listen( fc::ip::endpoint::from_string(_options->at("rpc-endpoint").as<string>()) );
 287    _websocket_server->start_accept();
 288 } FC_CAPTURE_AND_RETHROW() }

这个方法很简单, 首先直接实例化了 _websocket_server 对象, 这个对象的类型是 fc::http::websocket_server, 它又是属于 fc 库的一部分, 然而这不重要, 这里不需要再了解 fc 库中对应的代码了. 实际上 fc::http::websocket_server 就是对前面我们说的 websocketapp 库的封装, 我们可以把 fc::http::websocket_server 就看做是 websocketapp.

那么可以看到紧接着就是注册了 on_connection 回调, 然后就是 listen, accept, 多么熟悉的套接字编程套路, websocket 服务端就这么愉快的启起来了~

我知道你要问什么, 怎么没看见注册 on_message 和 on_http 回调呢? 对了, 看到注册 on_connection 回调用的 application_impl::new_connection 方法了吗, on_message 和 on_http 实际上就是在这个方法里注册的:

// 代码 1.2

 245 void application_impl::new_connection( const fc::http::websocket_connection_ptr& c )
 246 {
 247    auto wsc = std::make_shared<fc::rpc::websocket_api_connection>(*c, GRAPHENE_NET_MAX_NESTED_OBJECTS);
 248    auto login = std::make_shared<graphene::app::login_api>( std::ref(*_self) );
 249    login->enable_api("database_api");
 250
 251    wsc->register_api(login->database());
 252    wsc->register_api(fc::api<graphene::app::login_api>(login));
 253    c->set_session_data( wsc );
….
….

// 代码 1.3

 10 websocket_api_connection::websocket_api_connection( fc::http::websocket_connection& c, uint32_t max_depth )
 11    : api_connection(max_depth),_connection(c)
 12 {
 13    _rpc_state.add_method( "call", [this]( const variants& args ) -> variant
 14    {
 15       FC_ASSERT( args.size() == 3 && args[2].is_array() );
…
…
…
 48    } );
 49
 50    _connection.on_message_handler( [&]( const std::string& msg ){ on_message(msg,true); } );
 51    _connection.on_http_handler( [&]( const std::string& msg ){ return on_message(msg,false); } );
 52    _connection.closed.connect( [this](){ closed(); } );
 53 }

application_impl::new_connection 的参数 fc::http::websocket_connection_ptr 这个类型又是对 websocketapp 的封装, 不难理解, 我们直接认为它就是 websocketapp 传过来的对这个新连接的上下文描述就好, 紧接着实例化了一个 fc::rpc::websocket_api_connection 对象并且把这个上下文传了进去, fc::rpc::websocket_api_connection 的构造函数在代码 1.3, 可以看到在构造函数最后它注册了 on_message 和 on_http 的 handler, 而这两个 handlers 实际上是调用的同一个方法: on_message, 注意这里这个 on_message 可是 websocket_api_connection::on_message.

到这里为止, 我们就知道该如何 track 各种请求在服务期短的处理了, 新连接的处理就看 application_impl::new_connection, 来了请求怎么处理就看 websocket_api_connection::on_message, 当然对 websocket 来说还有一个过程就是服务器端主动发消息给客户端的过程, 这部分感兴趣可以自己研究一下, 提示一下: fc::http::websocket_connection::send_message 方法.

后记

本文最后引出了 on_connection 和 on_message 这两个重要的回调, 下篇文章将会简单介绍 on_connection 实现, 然后从 on_message 展开介绍一下各类 api 们, 以及从请求体到这些 api 们的映射机制.

Graphene 源码阅读 – 番外篇 – 出块判断逻辑

见证人节点起来之后, 会周期行的检查是否轮到自己出块了, 判断自己是否能出块的逻辑在 maybe_produce_block()方法中, 这里面的第一步就是判断自己是不是和网络同步了, 同步了的话就继续下面的判断, 没有同步的话就直接返回 not_synced 错误告诉外面不要出块.

211 block_production_condition::block_production_condition_enum witness_plugin::maybe_produce_block( fc::limited_mutable_variant_object& capture )
212 {
213    chain::database& db = database();
214    fc::time_point now_fine = fc::time_point::now();
215    fc::time_point_sec now = now_fine + fc::microseconds( 500000 );
216
217    // If the next block production opportunity is in the present or future, we're synced.
218    if( !_production_enabled )
219    {
220       if( db.get_slot_time(1) >= now )
221          _production_enabled = true;
222       else
223          return block_production_condition::not_synced;
224    }

正常情况下, 第一次启动时 _production_enabled 的值是 false, 因此这段逻辑确保了在同步到最新块之前本节点肯定不会出块的, 否则就分叉了; 而一旦达到过一次同步状态, _production_enabled 就会置为 true, 我搜索了全部代码, 程序运行周期中没有其它地方会将 _production_enabled 再置为 false 了, 而且 maybe_produce_block() 方法中后面也没有对节点同步状态进行检查的逻辑了, 这就意味着只要见证人节点曾经同步到最新过, 并且始终没有宕机没有重启, 那么就算后来节点不再处于同步状态, 照样也能出块?

这个行为我诈一看感觉不太合理, 按理说节点如果没同步的话那肯定也得直接返回 not_synced 不能让出块呀! 所以 maybe_produce_block() 这段逻辑得改改, 于是我计划写个 issue 然后提个 patch, 修改方案已经想好了, head_block_time() + block_interval()now 比较, 如果前者大就说明节点已经同步到最新状态了, 否则就说明节点显然已经落后了 (这也是上述代码中 db.get_slot_time() 方法的逻辑). 这样一来上面这段代码可以改成类似如下这样就可以了:

211 block_production_condition::block_production_condition_enum witness_plugin::maybe_produce_block( fc::limited_mutable_variant_object& capture )
212 {
213    chain::database& db = database();
214    fc::time_point now_fine = fc::time_point::now();
215    fc::time_point_sec now = now_fine + fc::microseconds( 500000 );
216
217    // If the next block production opportunity is in the present or future, we're synced.
218    if( db.get_slot_time(1) < now )
219       return block_production_condition::not_synced;

本来 issue 都已经写完了, 然而在写完的时候我才意识到自己错了, 如果按照上面的说法做的话, 可能会导致从某个时间起所有见证人都不再出块, 用一个简单的例子说明. 假设有 A, B, C 三个见证人, 他们先按照 A -> B -> C 的顺序每人出了一个块, 然后顺序变成 B -> A -> C, 所以这时轮到 B 出块了, 然而不幸的是 B miss 了, 于是到 A, A 这时判断最新块的时间加上出块间隔发现小于当前时间, 按照上面的策略, 这个认为自己 out of sync 了, 所以 A 也不会出块, 到 C 时也是一样的情况, C 也认为自己 out of sync 于是 C 也不出块. 于是.. 就达到了一个没有节点出块的局面…

所以说 _production_enabled 实际上正是避免了出现这个局面, 只是它的名字可能有点让人困惑, 实际上它就是一个标识, 标识着当节点第一次达到了与全网同步的状态时, 它就初步具备了出块资格.

也许改叫 once-synced 之类的名字会好些 😛

(PS: 这篇的内容貌似已经有点涉及 DPoS 啦)

Graphene 源码阅读 – 数据库篇 – 区块数据管理

在 bitshares 中除了对象数据需要落盘之外, 网络上接收到的区块数据也是需要落盘的, 区块数据的管理依赖的就是 chain::block_database 模块.

chain::block_database

这个模块听起来好像很复杂 – 毕竟管理着区块数据的落盘与加载, 但实际上非常简单. 它维护了两个文件流: _blocks 存储具体区块数据, _block_num_to_pos 则是索引数据. 当我们知道了区块 id 或者区块号时, 就可以从 _block_num_to_pos 查询出此区块在 _blocks 中的偏移和大小, 进而从 _blocks 中取出整个块数据.

区块数据的内存模型

区块数据在内存中由 chain::signed_block 类型表示, 但是与对象在内存中会挂到红黑树上不同, 区块数据从其他节点收到并 apply 完后就会序列化到磁盘并在内存中释放.

区块数据与区块索引的序列化与反序列化

读写 _blocks 文件流时会涉及到区块数据的序列化与反序列化, 这里依赖的也是 对象序列化 中介绍的 fc::raw::pack/fc::raw::unpack, 不再敖述.

而读写 _block_num_to_pos 流时关于区块索引的序列化反序列化就简单了, 它没有使用 fc::raw::pack/fc::raw::unpack, 而是简单粗暴的强转成字符序列:

// 代码 6.1

 78 void block_database::store( const block_id_type& _id, const signed_block& b )
 79 {
 80    block_id_type id = _id;
 81    if( id == block_id_type() )
 82    {
 83       id = b.id();
 84       elog( "id argument of block_database::store() was not initialized for block ${id}", ("id", id) );
 85    }
 86    _block_num_to_pos.seekp( sizeof( index_entry ) * int64_t(block_header::num_from_id(id)) );
 87    index_entry e;
 88    _blocks.seekp( 0, _blocks.end );
 89    auto vec = fc::raw::pack( b );
 90    e.block_pos  = _blocks.tellp();
 91    e.block_size = vec.size();
 92    e.block_id   = id;
 93    _blocks.write( vec.data(), vec.size() );
 94    _block_num_to_pos.write( (char*)&e, sizeof(e) );
 95 }

反序列过程实际上面说过了, 就是拿区块 id 或者区块号从 _block_num_to_pos 查询出此区块在 _blocks 中的偏移和大小, 然后从 _blocks 中取出整个块数据:

// 代码 6.2

184       index_entry e;
185       int64_t index_pos = sizeof(e) * int64_t(block_num);
186       _block_num_to_pos.seekg( 0, _block_num_to_pos.end );
187       if ( _block_num_to_pos.tellg() <= index_pos )
188          return {};
189
190       _block_num_to_pos.seekg( index_pos, _block_num_to_pos.beg );
191       _block_num_to_pos.read( (char*)&e, sizeof(e) );
192
193       vector<char> data( e.block_size );
194       _blocks.seekg( e.block_pos );
195       _blocks.read( data.data(), e.block_size );
196       auto result = fc::raw::unpack<signed_block>(data);
197       FC_ASSERT( result.id() == e.block_id );
198       return result;

chain::database 初始化

如果说 db::object_database 是管理对象数据, chain::block_database 管理区块数据, 那么 chain::database 就是管理 db::object_databasechain::block_database 了. 从结构上来讲 chain::database 的内容应该单独放在一篇文章里, 但是由于 block_database 模块比较简单, 篇幅太少, 而 chain::database 的内容有比较多, 估计下一篇一篇介绍不完, 所以这里就暂且先把 chain::database 的初始化部分挪到这里来. 也能让我们先对 chain::database 有个大概的认识.

chain::database 的初始化涉及到两个过程, 一个是节点实例被构造时, 同时也会构造 chain::database 实例, 而在 chain::database 构造时就会执行我们前面章节提到过的 initialize_indexes(), add_index<>() 等过程. 对应下面这条链路:

node = new app::application() => new detail::application_impl(this) => _chain_db(std::make_shared<chain::database>()

chain::database() => initialize_indexes()
                 => add_index<>()

其次就是在节点 startup 时, 会调用 chain::database::open() 方法, 这里面包含了 chain::database 初始化阶段的主要工作内容.

首先, 读取 witness_node_data_dir/blockchain/db_version 文件, 比较一下数据库版本和当前运行的版本的程序的数据库版本是否一致, 如果版本不一致或者这个文件不存在, 就会先清空对象库, 然后写入当前的版本号.

// 代码 6.3
143       bool wipe_object_db = false;
144       if( !fc::exists( data_dir / "db_version" ) )
145          wipe_object_db = true;
146       else
147       {
148          std::string version_string;
149          fc::read_file_contents( data_dir / "db_version", version_string );
150          wipe_object_db = ( version_string != db_version );
151       }
152       if( wipe_object_db ) {
153           ilog("Wiping object_database due to missing or wrong version");
154           object_database::wipe( data_dir );
155           std::ofstream version_file( (data_dir / "db_version").generic_string().c_str(),
156                                       std::ios::out | std::ios::binary | std::ios::trunc );
157           version_file.write( db_version.c_str(), db_version.size() );
158           version_file.close();
159       }

然后就是调用 object_databaseblock_database 的 open() 方法, 到这里我们看到了 chain::database 确实是操控 object_databaseblock_database 的 “上游” 模块.

// 代码 6.4

161       object_database::open(data_dir);
162
163       _block_id_to_block.open(data_dir / "database" / "block_num_to_block");

接下来, 如果这是节点的第一次启动 (说明从 object_database 中加载的对象中没有找到 global_property_object) 的话, 就要初始化创世状态, 而创世信息从里来呢? 请参考 Genesis 创世信息生成.

// 代码 6.5

165       if( !find(global_property_id_type()) )
166          init_genesis(genesis_loader());

创世过程的主要工作包括创建一些特殊和初始账户, 以及一些核心资产, 这些信息不用经过广播直接在本地出块, 因为所有其他节点也要执行要全同样的过程.

指的一提的是, 创世信息中的账户我本以为都是链上的公共账户或者委员会特殊账户之类的, 但没想到里面还有 9w 多的正常会员账户, 这些会员账户可谓是 bitshares 共链的 “创世居民”.

后记

区块中保存了整条链的原始记录, 只要区块数据正确完好, 我们就能从这些数据中 replay 出一条一模一样的链.

好了, 本文就到此为止. 感谢阅读.

Graphene 源码 – 番外篇 – 默认的 Genesis 创世信息

Genesis 生成

egenesis 最前面的 e 代表 embedded, 意思是说创世信息嵌入代码中, 它有两个 full 和 brief 两个版本, full 版本包含所有创世信息, brief 只包含这些创世信息的 hash (创世信息的 hash 也是 chain_id).

full 和 brief 的生成靠的都是下面这段 make 脚本.

// libraries/egenesis/CMakeLists.txt

 22 MESSAGE( STATUS "egenesis: " ${GRAPHENE_EGENESIS_JSON} )
 23
 24 if( GRAPHENE_EGENESIS_JSON )
 25    list( APPEND embed_genesis_args --genesis-json "${GRAPHENE_EGENESIS_JSON}" )
 26 endif( GRAPHENE_EGENESIS_JSON )
 27
 28 MESSAGE( STATUS "embed_genesis_args: " ${embed_genesis_args} )
 29
 30 add_custom_command(
 31    OUTPUT
 32       "${CMAKE_CURRENT_BINARY_DIR}/egenesis_brief.cpp"
 33       "${CMAKE_CURRENT_BINARY_DIR}/egenesis_full.cpp"
 34    WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}
 35    COMMAND embed_genesis ${embed_genesis_args}
 36    DEPENDS
 37       "${GRAPHENE_EGENESIS_JSON}"
 38       "${CMAKE_CURRENT_SOURCE_DIR}/egenesis_brief.cpp.tmpl"
 39       "${CMAKE_CURRENT_SOURCE_DIR}/egenesis_full.cpp.tmpl"
 40       embed_genesis )

其中 GRAPHENE_EGENESIS_JSON 在 bitshares-core 根目录被设置为 genesis.json:

// ./CMakeLists.txt

26:set(GRAPHENE_EGENESIS_JSON "${CMAKE_CURRENT_SOURCE_DIR}/genesis.json" )

没错, 就是 bitshares-core 源码根目录的那个 genesis.json 文件.

egenesis 相关的代码都位于 libraries/egenesis/ 目录. 上面 libraries/egenesis/CMakeLists.txt 里的 embed_genesis 这个命令的源码实际就是 libraries/egenesis/embed_genesis.cpp; embed_genesis_args 参数展开就是 —genesis-json genesis.json; embed_genesis 程序会用 egenesis_full.cpp.tmpl, egenesis_brief.cpp.tmpl 这两个模板 egenesis_full.cpp, egenesis_brief.cpp 两个文件.

引用

上面说了 full 和 brief 两个 genesis 源码, 他们在构建时会分别生成 libgraphene_egenesis_full.alibgraphene_egenesis_brief.a, 这俩库里面实现的方法都是 <egenesis/egenesis.hpp> 中定义的方法, 如果同时链接这两个库势必会造成符号重定义错误. 所以这俩只会链接一个.

仍然以 witness 为例, 在 witness_node/CMakeLists.txt 中我们可以看到, 链接时实际上链的是 full 版本:

// witness_node/CMakeLists.txt

 15 PRIVATE graphene_app graphene_delayed_node graphene_account_history graphene_elasticsearch graphene_market_history graphene_witness graphene_chain graphene_debug_witness graphen    e_egenesis_full graphene_snapshot fc ${CMAKE_DL_LIBS} ${PLATFORM_SPECIFIC_LIBS} )

参考

https://github.com/cryptonomex/graphene/wiki/egenesis

嵌入式哈系表的实现

这篇文章是一个例子, 重点在于阐述嵌入式哈系表结构, 而不是针对不同键类型进行哈希的方法.

关于哈希冲突的解决, 我们使用链表存储冲突的节点, 这在一些书里被称为 “Separate Chaning” 方法.

由于在链表中插入以及删除节点需要更新节点的前继和后继的指针, 所以为了方便的从链表中插入以及删除节点, hlist_node_t 结构中定义两个指针, 分别指向前继和后继.

typedef struct hlist_node_s {
    struct hlist_node_s *prev;
    struct hlist_node_s *next;
} hlist_node_t;

hlist_t 结构中的 heads 是一个数组, 每个数组元素都只是一个头节点, 头节点不会嵌入到别的结构体中.

typedef struct xhash {
    hlist_node_t    *heads;
    int             size;
} xhash_t;

void hlist_node_init(hlist_node_t *node)
{
    node->next = node->prev = node;
}

xhash_t *xhash_create(int size)
{
    xhash_t     *hash;
    int         i;

    /* TODO Get next prime bigger than *size* */
    /* size = next_prime(size);*/

    hash = calloc(1, sizeof *hash);
    if (!hash)
        return NULL;

    hash->heads = calloc(size, sizeof *hash->heads);
    if (!hash->heads) {
        free(hash);
        return NULL;
    }

    for (i = 0 ; i < size ; ++i)
        hlist_node_init(&hash->heads[i]);

    hash->size = size;

    return hash;
}

为了方便链表的遍历, 我们还需要定义如下几个宏, 其中 offsetof 宏一般 C 标准库会为我们定义, 并且 C 库的定义会考虑到不同系统上的差异. 为了完整的阐述我们的实现, 这里我还是将它 “bare” 的定义写了出来.

#define offsetof(type, member)              \
    ({                                      \
        type s;                             \
        (char *)(&s.member) - (char *)(&s); \
    })

#define container_of(node, type, member)                \
    ((type *)((char *)(node) - offsetof(type, member)))

#define hlist_for_each(head, node)                      \
    for ((node) = (head)->next ;                        \
            (node) != (head) ; (node) = (node)->next)

#define hlist_for_each_safe(head, node, next)           \
    for ((node) = (head)->next, (next) = (node)->next ; \
            (node) != (head) ;                          \
            (node) = (next), (next) = (node)->next)

void hlist_add_head(hlist_node_t *head, hlist_node_t *new)
{
    head->next->prev = new;
    new->next = head->next;
    head->next = new;
    new->prev = head;
}

void hlist_add_tail(hlist_node_t *head, hlist_node_t *new)
{
    head->prev->next = new;
    new->prev = head->prev;
    head->prev = new;
    new->next = head;
}

void hlist_del(hlist_node_t *node)
{
    node->next->prev = node->prev;
    node->prev->next = node->next;
}

如前所说, 为方便起见我们仅实现整数类型的键, 哈系函数采用简单的取模运算. 其它类型的键的哈系后面只要进一步扩展就可以了, 这里不再多说.

void xhash_int_add(xhash_t *hash, int key, hlist_node_t *node)
{
    key %= hash->size;
    hlist_add_head(&hash->heads[key], node);
}

void xhash_int_del(xhash_t *hash, int key, hlist_node_t *node)
{
    key %= hash->size;
    hlist_del(&hash->heads[key], node);
}

/** return hlist head */
hlist_node_t *xhash_int_find(xhash_t *hash, int key)
{
    key %= hash->size;
    return &hash->heads[key];
}

xhash_destroy 方法中, 要不要帮助用户将所有嵌入的节点从链表中移除是个问题, 可能比较人性化的方法是帮助用户移除. 但假如节点嵌入的结构体是用户动态申请的, 然后用户调用 xhash_destroy 之前就把结构体释放了呢? 这样节点成员所处的内存就是无效的了, 我们的 xhash_destroy 方法就很可能会崩溃. 所以这活儿还是不干了.

void xhash_destroy(xhash_t *hash)
{
    /*hlist_node_t    *node, *next;
    int             i;*/

    /* TODO
     * whether we should del all node from corresponding list? what if user remove
     * the node member from embedded struct?
     */
    /*
    for (i = 0 ; i < size ; ++i) {
        hlist_for_each_safe(&hash->heads[i], node, next) {
            hlist_del(node);
            /* restore to initial state */
            hlist_node_init(node);
        }

    }
    */

    free(hash->heads);
    free(hash);
}

下面我们用一个例子测试一下我们的实现:

typedef struct xy_s {
    int             key;
    int             x;
    int             y;
    hlist_node_t    node;
} xy_t;

int xy_print(xhash_t *hash)
{
    int             idx;
    hlist_node_t    *node;
    xy_t            *xy;

    if (!hash) return -1;

    for (idx = 0 ; idx < hash->size ; ++idx) {
        printf("bucket[%d]: ", idx);
        hlist_for_each(&hash->heads[idx], node) {
            xy = container_of(node, xy_t, node);
            printf("%d, ", xy->key);
        }

        printf("\n");
    }

    return 0;
}

int main()
{
    xhash_t     *hash;
    xy_t        *xy;
    int         i;

    assert((hash = xhash_create(13)) != NULL);

    printf("\ninsert 0 to 99 nodes \n\n");

    for (i = 0 ; i < 100 ; ++i) {
        xy = calloc(1, sizeof *xy);
        xy->key = i;
        xy->x = i * 3;
        xy->y = i * 4;
        hlist_node_init(&xy->node);
        xhash_int_add(hash, xy->key, &xy->node);
    }

    xy_print(hash);

    printf("\ndelete 99\n\n");

    xhash_int_del(hash, 99, &xy->node);
    xy_print(hash);

    return 0;
}

编译运行以上程序, 输出的信息如下:

insert 0 to 99 nodes 

bucket[0]: 91, 78, 65, 52, 39, 26, 13, 0, 
bucket[1]: 92, 79, 66, 53, 40, 27, 14, 1, 
bucket[2]: 93, 80, 67, 54, 41, 28, 15, 2, 
bucket[3]: 94, 81, 68, 55, 42, 29, 16, 3, 
bucket[4]: 95, 82, 69, 56, 43, 30, 17, 4, 
bucket[5]: 96, 83, 70, 57, 44, 31, 18, 5, 
bucket[6]: 97, 84, 71, 58, 45, 32, 19, 6, 
bucket[7]: 98, 85, 72, 59, 46, 33, 20, 7, 
bucket[8]: 99, 86, 73, 60, 47, 34, 21, 8, 
bucket[9]: 87, 74, 61, 48, 35, 22, 9, 
bucket[10]: 88, 75, 62, 49, 36, 23, 10, 
bucket[11]: 89, 76, 63, 50, 37, 24, 11, 
bucket[12]: 90, 77, 64, 51, 38, 25, 12, 

delete 99

bucket[0]: 91, 78, 65, 52, 39, 26, 13, 0, 
bucket[1]: 92, 79, 66, 53, 40, 27, 14, 1, 
bucket[2]: 93, 80, 67, 54, 41, 28, 15, 2, 
bucket[3]: 94, 81, 68, 55, 42, 29, 16, 3, 
bucket[4]: 95, 82, 69, 56, 43, 30, 17, 4, 
bucket[5]: 96, 83, 70, 57, 44, 31, 18, 5, 
bucket[6]: 97, 84, 71, 58, 45, 32, 19, 6, 
bucket[7]: 85, 72, 59, 46, 33, 20, 7, 
bucket[8]: 86, 73, 60, 47, 34, 21, 8, 
bucket[9]: 87, 74, 61, 48, 35, 22, 9, 
bucket[10]: 88, 75, 62, 49, 36, 23, 10, 
bucket[11]: 89, 76, 63, 50, 37, 24, 11, 
bucket[12]: 90, 77, 64, 51, 38, 25, 12, 

LoxiGen 与 Indigo 项目介绍

Indigo 是由 Big Switch 开发, 现在托管在 Floodlight 组织下的 OpenFlow agent 开源实现. OpenFlow 控制器的开源实现有很多, 像是 Ryu, ODL, POX 等等, 但是 agent 方面的开源实现可能相对少一些, Indigo 是一个非常精巧的实现, 但是似乎网上 SDN 相关的中文社区对其介绍的文档却很少, 本文将对 Indigo 及其所依附的 LoxiGen 项目做一个简单的介绍.

关于 LOXI 与 Loxigen

LOXI (Logical OpenFlow eXtensible Interface), 是一种描述 OpenFlow 协议的逻辑语言.

LoxiGen 项目能够解读 LOXI 语言, 进而用来生成各种编程语言的 OpenFlow 协议库. 所以说 LoxiGen 实际上是一个 “编译器” 项目, 可想而知 LoxiGen 包含一个前端用来解析 LOXI 语言, 以及包含各种编程语言的后端来生成这些编程语言的代码. 目前包含 Java, Python 以及 C 语言的后端, 生成的 C 版本的协议库叫做 LOCI, Java 版本的叫做 OpenFlowJ, Python 版本的叫做 pyloxi.

LoxiGen is a tool that generate OpenFlow protocol library for a number of languages. It is composed of a frontend that parses wire protocol descriptions and a backend for each supported language (currently C, Python and Java, with an auto-generated wireshark dissector in Lua on the way).

LoxiGen 项目组维护了一个快照, 定期的生成各种语言的协议库, 并将其放在 loxigen-artifacts 仓库下, 这样对于使用者来说只需要定期从 loxigen-artifacts 仓库拿现成的就行了, 连构建都不需要自己构建.

Indigo 项目源码构成

Indigo 项目 (https://github.com/floodlight/indigo) 为我们提供了一些与平台无关的基础功能, 包括:

  • IO 复用与定时器管理框架: 提供了通用的 socket 回调注册机制与定时器回调处理机制, 让我们能够在单一线程中同时处理多个 sockets 的读写以及定时器的回调事件.
  • OpenFlow 连接管理: 维护每一条与控制器之间的连接, 包括连接的建立, 心跳维持, 消息收发以及连接的终止.
  • OpenFlow 状态管理: 包括与控制器之间的各种消息的处理以及交换机状态的上报都会在这里处理
  • 配置模块: 提供了平台无关的配置接口

另外 Indigo 中有一些平台相关的功能是需要厂商自己来实现的, 这包括:

  • Forwarding 模块: 负责实现将控制器下发的流表下发到交换机芯片中的接口
  • Port Manager 模块: 负责实现端口管理的接口

芯片厂商需要负责实现后两个模块, 一般来讲就是将这两个模块中 indigo 所定义的接口用自己芯片的 sdk 来实现. 关于 Indigo 适配有一个很好的例子就是 Broadcom 的 OF-DPA, 我们可以从 Broadcom 的 OF-DPA 项目仓库中获得实现了 Forwarding 和 Port Manager 模块之后的 indigo 代码.

OF-DPA 项目托管在 https://github.com/Broadcom-Switch/of-dpa/, 其对 Indigo 项目做了些许代码上的改动, 从这个地址获得 OF-DPA 的代码, indigo 的代码位于 ofagent/ 目录下:

ofagent/
    application/
    indigo/
    ofdpadriver/

其中 indigo/ 目录中就是 indigo 的源码; ofdpadriver/ 是 OF-DPA 实现好的 Forwarding 和 Port manager 模块的代码, 这两部分会调用芯片 sdk; application/ 目录中只有一个源文件: ofagent.c, 这是一个示例程序, 向我们展示了如何初始化以及启动 indigo.

在 indigo 的项目代码也就是 indigo/ 目录下, 又有两个子目录: modules/submodules/, 其中 modules/ 里面是 indigo 自身的代码, submodules/ 里是 indigo 所用到的非自身的代码, 目前有 bigcode, infra, loxigen-artifacts.

bigcode 和 infra 都是 Floodlight 开发的通用工具库, 另外你可能注意到了 Indigo 使用了 loxigen-artifacts 中的代码. 下面就看一下 indigo 具体是如何使用 loxigen 的.

Indigo 与 LoxiGen

Indigo 使用了 LoxiGen 项目生成的 LOCI 协议库, 并且是直接把源代码拿来使用的. 一开始将其放在 indigo/modules/loci/ 目录下, Indigo 的维护者从 loxigen-artifacts 获得最新的代码, 然后再将其与 modules/loci/ 下原本的 loxigen 项目代码合并或者直接替换. 因为这样每次都需要维护者手动去 pull loxigen-artifacts, 然后与 Indigo 的 codebase 合并, 后来估计是 Indigo 的维护者也觉得这样太笨了, 于是就干脆不往 modules/loci/ 里面合了, 而是直接借助了 git submodule 机制, 把 loxigen-artifacts 用 git pull 拉下来放到了 submodules/loxigen-artifacts/, 然后 modules/loci/make.mk 做了如下的改动, 直接指向 submodules/loxigen-artifacts/.

-loci_INCLUDES := -I $(dir $(lastword $(MAKEFILE_LIST)))inc
-loci_INTERNAL_INCLUDES := -I $(dir $(lastword $(MAKEFILE_LIST)))src
+LOCI := $(SUBMODULE_LOXIGEN_ARTIFACTS)/loci

+loci_INCLUDES := -I $(LOCI)/inc
+loci_INTERNAL_INCLUDES := -I $(LOCI)/src

+LIBRARY := loci
+loci_SUBDIR := $(LOCI)/src
+include $(BUILDER)/lib.mk

这样一来, 编译 indigo 项目时就会直接从 submodules/loxigen-artifacts/ 处获取 loxigen 的源码.

(End)

记一次 Indigo 与 Ryu 的连接建立问题

之前在研究 Indigo 和 Ryu 的衔接时碰到一个 OpenFlow 连接建立失败的问题, 特此记录下, 希望能够供别人参考.

在了解这个问题之前, 我们先回顾一下 OpenFlow 连接建立过程.

OpenFlow 协议的连接建立过程

根据 OpenFlow 协议标准的陈述我们能够知道, 交换机和控制器之间使用 TCP (或者 SSL) 传输协议, 交换机必须能够主动发起连接 (实际应用中, 连接一般都是都由交换机主动发起), 另外就是所有的 OpenFlow 消息, 都要用网络序 (大端序) 发送 (参见 OpenFlow Spec v1.3 以及 v1.4 的第 7 章)

TCP 连接的建立我们很熟悉了, 就是典型的三此握手过程. 在 TCP 连接建立以后, 交换机和控制器双方在 TCP 连接建立后需要立即发送给 OF_HELLO 消息给对方, 并且 OF_HELLO 必须是双发发送给对方的第一个消息, OF_HELLO 消息同时起到协商 OpenFlow 版本的功能.

当双方都收到了对方的 OF_HELLO 消息并且两边都共同支持一个最小版本, OpenFlow 连接就成功建立了, 接下来控制器就可以向交换机发送其它的消息, 比如一般第一次要发送的就是 OFPT_FEATURES_REQUEST 消息.

遇到的问题

在这部分工作中基于 Ryu 框架我写了个简单的 Ryu 小程序令它与 Indigo 通信, 但是发现似乎连接建立都不成功, 好在 indigo 项目的错误日志部分做的很好, 我打开了 verbose 级别的日志, 发现连接建立过程中 indigo 在收取 OF_HELLO 这个消息失败了, 结合代码发现是在读 socket 时发生了 EAGAIN (Resource temporary unvailable) 错误. 熟悉 Linux 开发的人都知道这个错误还有另一个名字叫做 EWOULDBLOCK, 其含义是指我去读一个非阻塞的 socket, 这个 socket 本来是被期望可读的, 但是实际读它的时候发现并没有数据.

这就有点奇怪了, 控制器发送 OF_HELLO 消息给交换机, 然而交换机读这个消息时却发生了这样的系统错误. 原本我怀疑是我的交换机系统环境有问题, 于是我写了一个最基本的 tcp socket server 和一个 tcp socket client, 让 tcp socket server 运行在控制器主机并监听控制器的标准端口 6653, tcp socket client 则跑在我的交换机系统上, 发现这两者是能够正常收发包的, 而且, 我按照 OpenFlow 协议组了一个 OF_HELLO 消息, 从 tcp socket server 发送给 tcp socket client, 我的 socket client 也能够正常的接收这个消息, 并没有出现 EAGAIN 错误.

所以说交换机系统环境是没有问题的, 可是为什么 indigo 的代码里收不到这个 OF_HELLO 消息呢? 看来必须得窥探一下 Ryu 发过来的这个 OF_HELLO 消息是什么样的.

问题的分析

为了确定 Ryu 发过来的 OF_HELLO 消息没问题, 用 tcpdump 抓包看一下两者通信的细节, 下面就是交换机和控制器连接建立过程中抓取的数据包:

09:59:26.578677 ethertype IPv4 (0x0800), length 74: 10.0.0.1.33430 > 10.0.0.2.6653: Flags [S], seq 992465816, win 43690, options [mss 65495,sackOK,TS val 15346082 ecr 0,nop,wscale 7], length 0
        0x0000:  4510 003c fa3a 4000 4006 426f 7f00 0001  E..<.:@.@.Bo....
        0x0010:  7f00 0001 8296 19fd 3b27 d398 0000 0000  ........;'......
        0x0020:  a002 aaaa fe30 0000 0204 ffd7 0402 080a  .....0..........
        0x0030:  00ea 29a2 0000 0000 0103 0307            ..).........
09:59:26.578703 ethertype IPv4 (0x0800), length 74: 10.0.0.2.6653 > 10.0.0.1.33430: Flags [S.], seq 710500324, ack 992465817, win 43690, options [mss 65495,sackOK,TS val 15346082 ecr 15346082,nop,wscale 7], length 0
        0x0000:  4500 003c 0000 4000 4006 3cba 7f00 0001  E..<..@.@.<.....
        0x0010:  7f00 0001 19fd 8296 2a59 5fe4 3b27 d399  ........*Y_.;'..
        0x0020:  a012 aaaa fe30 0000 0204 ffd7 0402 080a  .....0..........
        0x0030:  00ea 29a2 00ea 29a2 0103 0307            ..)...).....
09:59:26.578723 ethertype IPv4 (0x0800), length 66: 10.0.0.1.33430 > 10.0.0.2.6653: Flags [.], ack 710500325, win 342, options [nop,nop,TS val 15346082 ecr 15346082], length 0
        0x0000:  4510 0034 fa3b 4000 4006 4276 7f00 0001  E..4.;@.@.Bv....
        0x0010:  7f00 0001 8296 19fd 3b27 d399 2a59 5fe5  ........;'..*Y_.
        0x0020:  8010 0156 fe28 0000 0101 080a 00ea 29a2  ...V.(........).
        0x0030:  00ea 29a2                                ..).
10:10:56.193800 ethertype IPv4 (0x0800), length 76: 10.0.0.1.33430 > 10.0.0.2.6653: Flags [P.], seq 992465817:992465827, ack 710500325, win 342, options [nop,nop,TS val 15518486 ecr 15346082]
        0x0000:  4510 003e fa3c 4000 4006 426b 7f00 0001  E..>.<@.@.Bk....
        0x0010:  7f00 0001 8296 19fd 3b27 d399 2a59 5fe5  ........;'..*Y_.
        0x0020:  8018 0156 fe32 0000 0101 080a 00ec cb16  ...V.2..........
        0x0030:  00ea 29a2 0400 0800 0000 0000            ..).........                  (注: 交换机发给控制器的 OF_HELLO 消息, 即 0400 0800 0000 0000)
10:10:56.193822 ethertype IPv4 (0x0800), length 66: 10.0.0.2.6653 > 10.0.0.1.33430: Flags [.], ack 992465827, win 342, options [nop,nop,TS val 15518486 ecr 15518486], length 0
        0x0000:  4500 0034 7647 4000 4006 c67a 7f00 0001  E..4vG@.@..z....
        0x0010:  7f00 0001 19fd 8296 2a59 5fe5 3b27 d3a3  ........*Y_.;'..
        0x0020:  8010 0156 fe28 0000 0101 080a 00ec cb16  ...V.(..........
        0x0030:  00ec cb16                                ....
10:11:01.246773 ethertype IPv4 (0x0800), length 75: 10.0.0.2.6653 > 10.0.0.1.33430: Flags [P.], seq 710500325:710500334, ack 992465827, win 342, options [nop,nop,TS val 15519749 ecr 15518486]
        0x0000:  4500 003d 7648 4000 4006 c670 7f00 0001  E..=vH@.@..p....
        0x0010:  7f00 0001 19fd 8296 2a59 5fe5 3b27 d3a3  ........*Y_.;'..
        0x0020:  8018 0156 fe31 0000 0101 080a 00ec d005  ...V.1..........
        0x0030:  00ec cb16 0400 0008 eed3 0ebf            ............                  (注: 控制器发给交换机的 OF_HELLO 消息, 即 0400 0008 eed3 0ebf)
10:11:01.246800 ethertype IPv4 (0x0800), length 66: 10.0.0.1.33430 > 10.0.0.2.6653: Flags [.], ack 710500334, win 342, options [nop,nop,TS val 15519749 ecr 15519749], length 0
        0x0000:  4510 0034 fa3d 4000 4006 4274 7f00 0001  E..4.=@.@.Bt....
        0x0010:  7f00 0001 8296 19fd 3b27 d3a3 2a59 5fee  ........;'..*Y_.
        0x0020:  8010 0156 fe28 0000 0101 080a 00ec d005  ...V.(..........
        0x0030:  00ec d005                                ....

在这里, 10.0.0.1 是交换机地址, 10.0.0.2 是控制器地址. 其中前三条报文显然就是 tcp 三次握手, 后四条就是两者相互发送的 OF_HELLO 消息, 上面两个括号里的注释是我加的. 在 OpenFlow 协议中, OF_HELLO 消息是一条只有 header 没有 payload 的消息, 所以 OF_HELLO 消息的长度就是 OpenFlow 消息头的长度: 8 字节, 其中第一个字节是 Ryu 与 Indigo 协商好的 OpenFlow 版本号, 这里是 0x04, 代表 OpenFlow v1.3 (注意不是 v1.4), 接下来的一个字节 0x00 表示消息类型是 OF_HELLO, 后四个字节是消息的唯一标识码我们可以不用理会, 中间的两个字节表示整个 OpenFlow 消息的长度, 有意思的地方就在这里.

Ryu 和 Indigo 相互发给对方的 OF_HELLO 消息中, 头部的长度字段字节序不一致, OpenFlow 协议头部的长度字段是两字节, OF_HELLO 消息长度为 8, 即 0x0008, 前面说过在 OpenFlow 协议中要求, 所有的 OpenFlow 消息, 都要用大端序发送. 所以这两个字节用网络序也就是大端序表示应该为 0x0008, 即 MSB 在低字节. 显然, 在这一点上 Ryu 的报文是正确的, indigo 的报文是错误的.

那么 EAGAIN 错误是怎么出现的呢? 既然 indigo 发出消息的长度字段的端序有问题, 可想而知其对于收到的消息的长度字段的端序理解也很可能有问题. 下面这段是 indigo 读取消息的代码逻辑, 为了清晰起见我做了一些删减:

/* read header */
if (READING_HEADER(cxn)) {
    INDIGO_ASSERT(cxn->bytes_needed + cxn->read_bytes ==
                  OF_MESSAGE_HEADER_LENGTH);
    if ((rv = read_from_cxn(cxn)) < 0) {
        return rv;
    }

    msg = (of_message_t)(cxn->read_buffer);
    msg_bytes = of_message_length_get(msg);
    if (msg_bytes < OF_MESSAGE_HEADER_LENGTH) {
        ++ind_cxn_internal_errors;
        return INDIGO_ERROR_PROTOCOL;
    }
    cxn->bytes_needed = msg_bytes - OF_MESSAGE_HEADER_LENGTH;
}

if (cxn->bytes_needed == 0) {
    return INDIGO_ERROR_NONE;
}

/* read the rest */
if ((rv = read_from_cxn(cxn)) < 0) {
    return rv;
}

稍微解释一下这段代码, 在 indigo 的代码中, 与 ryu 通信的 socket 是非阻塞的, 用 poll 系统调用来判断是否可读, indigo 在读 socket 时还有一个字段叫做 bytes_needed 用来表示我想要从 socket 中读多少个字节, read_from_cxn(cxn) 方法会按照 bytes_needed 的值明确读取这些字节数. 当内核第一次通知 indigo 这个 socket 可读时, bytes_needed 字段总是 8, 因为 OpenFlow 的消息头最小就是 8, 小于 8 就是非法的. 读取了 8 字节的头之后, of_message_length_get(msg) 会解析头部中的两字节的长度字段获知整条 OpenFlow 消息的长度, 用它减去头部的长度来获知我还需要读多少字节.

可想而知, 当 indigo 收到 ryu 的 OF_HELLO 时, 它也把 0x0008 处理成了 0x0800, indigo 以为 ryu 给它发送了 8 x 256 = 2048 个字节, 但显然实际上 ryu 只给它发送了 8 字节. 在 indigo 认为后面应该还有 2040 个字节, 于是继续调用 read_from_cxn(cxn), 但实际上 socket 上没有字节可读了, 于是引发 EAGAIN 错误.

问题的解决

综上来看, 问题出在 indigo 对长度字段的解析上, 也就是上面代码段里的 of_message_length_get() 方法中, 我层层看下去发现 of_message_length_get() 方法的调用链是这样的:

of_message_length_get() >> buf_u16_get() >> U16_NTOH()

这几个方法的含义一看名字就知道, 最后的 U16_NTOH() 是个条件编译的宏, 定义如下:

#if __BYTE_ORDER__ == __ORDER_BIG_ENDIAN__ 
#define U16_NTOH(val) (val) 
#else 
#define U16_NTOH(val) (((val) >> 8 |(((val) & 0xff) << 8)) 
#endif 

所以现在一切都清晰了, indigo 没有使用 POSIX 标准的系统调用 htons()/ntohs(), 而是自己定义了一套主机序到网络序的转换方法, 可能是 indigo 想能够编译在更多的平台上而不只是 POSIX 兼容的系统. 只不过这样一来, 我们就需要在编译 indigo 的时候根据我们自己的系统架构来定义 __BYTE_ORDER__ 宏. 在我上面的实验环境中, 交换机系统架构是小端序的, 所以收到网络上的消息时, 对于二字节的长度字段应该做颠倒高低字节的处理, 然而 __BYTE_ORDER____ORDER_BIG_ENDIAN__ 这两个宏我都没定义, 所以在预处理阶段 U16_NTOH(val) 宏就直接被定义成了 (val).

那么解决办法就是, 视系统架构而定, 在编译 indigo 的 CPPFLAGS (或者你可能用的是 CFLAGS) 加上几个宏定义:

小端序系统:

CPPFLAGS += -D__ORDER_BIG_ENDIAN__=0 -D__ORDER_LITTLE_ENDIAN__=1 -D__BYTE_ORDER__=__ORDER_LITTLE_ENDIAN__

大端序系统:

CPPFLAGS += -D__ORDER_BIG_ENDIAN__=0 -D__ORDER_LITTLE_ENDIAN__=1 -D__BYTE_ORDER__=__ORDER_BIG_ENDIAN__

(End)