热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

EOS测试插件:txn_test_gen_plugin.cpp

txn_test_gen_plugin插件这个插件是官方开发用来测试块打包交易量的,这种方式由于是直接系统内部调用来模拟transaction,没有中间通讯的损耗,因此效率是非常高

txn_test_gen_plugin 插件

这个插件是官方开发用来测试块打包交易量的,这种方式由于是直接系统内部调用来模拟transaction,没有中间通讯的损耗,因此效率是非常高的,官方称通过这个插件测试到了8000的tps结果,而就我的测试结果来讲,没有这么恐怖,但也能到2000了,熟不知,其他的测试手段,例如cleos,eosjs可能只有百级的量。下面,我们一同来研究一下这个插件是如何实现以上功能的,过程中,我们也会思考EOS插件的架构体系,以及实现方法。通过本文的学习,如果有好的想法,我们也可以自己开发一个功能强大的插件pr给eos,为EOS社区做出我们自己的贡献。

关于txn_test_gen_plugin插件的使用,非常易于上手,本文不做分析,这方面可以直接参考官方文档。

插件的整体架构

插件代码整体结构中,我们上面介绍的核心功能的实现函数都是包含在一个结构体struct txn_test_gen_plugin_impl中。剩余的其他代码都是对插件本身的通讯进行描述,包括如何调用,如何响应等,以及整个插件的生命周期的控制:

  • set_program_options,设置参数的阶段,是最开始的阶段,内容只设置了txn-reference-block-lag的值,默认是0,-1代表最新头区块。
  • plugin_initialize,这一时期就把包含核心功能的结构体txn_test_gen_plugin_impl加载到程序运行时内存中了,同时初始化标志位txn_reference_block_lag为txn-reference-block-lag的值。
  • plugin_startup,我们通过基础插件http_plugin的支持获得了http接口的能力,这一时期,就暴露出来本插件的对外接口。
  • plugin_shutdown,调用stop_generation函数,重置标志位running为false,计时器关闭,打印关闭提示日志。

下面是对外暴露的三个接口之一的stop_generation函数的源码:

void stop_generation() { if(!running) throw fc::exception(fc::invalid_operation_exception_code); timer.cancel(); running = false; ilog("Stopping transaction generation test"); }

接下来,我们主要集中精力在结构体txn_test_gen_plugin_impl上,研究路线是以剩余两个接口分别为入口进行逐一分析。

create_test_accounts 接口

关于这个接口,调用方法是

curl --data-binary '["eosio", "5KQwrPbwdL6PhXujxW37FSSQZ1JiwsST4cqQzDeyXtP79zkvFD3"]' http://localhost:8888/v1/txn_test_gen/create_test_accounts

传入的参数是eosio以及其私钥。我们进入到函数create_test_accounts中去分析源码。

准备知识

首先,整个函数涉及到的所有transaction都是打包存入到一个vector集合std::vector中去。

trxs是一个事务集,它包含很多的trx,而其中每一个trx包含一个actions集合vector

一、准备账户

trxs的第一个trx,内容为账户创建:

  • 定义3个账户:txn.test.a,txn.test.b, txn.test.t
  • 辅助功能:controller& cc = app().get_plugin().chain();,通过cc可以随时调用本地区块链上的任意信息。
  • 通过fc::crypto::private_key::regenerate函数分别生成他们的私钥,要传入生成秘钥的seed。
  • 通过私钥直接调用get_public_key()即可获得公钥
  • 设置每个账户的owner和active权限对应的公钥,一般来讲他们是相同的
  • 账户的创建者均为我们外部调用create_test_accounts接口时传入的账户eosio,注意:eosio的私钥是通过字符串传入的,要通过fc::crypto::private_key转换成私钥对象
  • 将每一个账户的创建组装好成为一个action,存入trx的actions集合中去。
  • trx的actions成员已经设置完毕,完成剩余trx的组装工作,包括
    • expiration,通过cc获得当前头区块的时间,加上延迟时间,这里是30s,fc::seconds(30)
    • reference_block,值为通过cc获取当前的头区块,意思为本transaction的引用区块,所有的信息是引用的这个区块为头区块的环境
    • sign,签名,使用的是创建者eosio的私钥对象,上面我们已经准备好了,签名的数据是data的摘要
      • 当前trx的actions中的元素的data并不是如文首的transaction中的data的加密串的结构,而是明文的,这里的加密是数字摘要技术,感兴趣的朋友可以去《应用密码学初探》进行了解。
      • 摘要的源码函数是:sig_digest(chain_id, context_free_data),其中参数使用到了chain_id,而context_free_data就是上面提到的明文data内容,所以它是要与链id一起做数字摘要的(这一点我在使用eosjs尝试自己做摘要的时候并未想到)

这一部分的源码展示如下:

name newaccountA("txn.test.a"); name newaccountB("txn.test.b"); name newaccountC("txn.test.t"); name creator(init_name); abi_def currency_abi_def = fc::json::from_string(eosio_token_abi).as(); controller& cc = app().get_plugin().chain(); auto chainid = app().get_plugin().get_chain_id(); fc::crypto::private_key txn_test_receiver_A_priv_key = fc::crypto::private_key::regenerate(fc::sha256(std::string(64, 'a'))); fc::crypto::private_key txn_test_receiver_B_priv_key = fc::crypto::private_key::regenerate(fc::sha256(std::string(64, 'b'))); fc::crypto::private_key txn_test_receiver_C_priv_key = fc::crypto::private_key::regenerate(fc::sha256(std::string(64, 'c'))); fc::crypto::public_key txn_text_receiver_A_pub_key = txn_test_receiver_A_priv_key.get_public_key(); fc::crypto::public_key txn_text_receiver_B_pub_key = txn_test_receiver_B_priv_key.get_public_key(); fc::crypto::public_key txn_text_receiver_C_pub_key = txn_test_receiver_C_priv_key.get_public_key(); fc::crypto::private_key creator_priv_key = fc::crypto::private_key(init_priv_key); //create some test accounts { signed_transaction trx; //create "A" account { auto owner_auth = eosio::chain::authority{1, { {txn_text_receiver_A_pub_key, 1}}, {}}; auto active_auth = eosio::chain::authority{1, { {txn_text_receiver_A_pub_key, 1}}, {}}; trx.actions.emplace_back(vector{ {creator,"active"}}, newaccount{creator, newaccountA, owner_auth, active_auth}); } //create "B" account { auto owner_auth = eosio::chain::authority{1, { {txn_text_receiver_B_pub_key, 1}}, {}}; auto active_auth = eosio::chain::authority{1, { {txn_text_receiver_B_pub_key, 1}}, {}}; trx.actions.emplace_back(vector{ {creator,"active"}}, newaccount{creator, newaccountB, owner_auth, active_auth}); } //create "txn.test.t" account { auto owner_auth = eosio::chain::authority{1, { {txn_text_receiver_C_pub_key, 1}}, {}}; auto active_auth = eosio::chain::authority{1, { {txn_text_receiver_C_pub_key, 1}}, {}}; trx.actions.emplace_back(vector{ {creator,"active"}}, newaccount{creator, newaccountC, owner_auth, active_auth}); } trx.expiration = cc.head_block_time() + fc::seconds(30); trx.set_reference_block(cc.head_block_id()); trx.sign(creator_priv_key, chainid); trxs.emplace_back(std::move(trx)); }

二、token相关

trxs的第二个trx,内容为token创建和issue,为账户转账为之后的测试做准备

  • 为账户txn.test.t设置eosio.token合约,之前在操作cleos set contract的时候可以通过打印结果发现,是有setcode和setabi两个步骤的。
    • setcode handler:
      • 设置handler的账户为txn.test.t
      • 将wasm设置为handler的code,wasm是通过eosio.token合约的eosio_token_wast文件获取的,vector wasm = wast_to_wasm(std::string(eosio_token_wast))
      • 将handler加上相关权限组装成action装入trx的actions集合中。
    • setabi handler:
      • 设置handler的账户为txn.test.t
      • 设置handler的abi,将文件eosio_token_abi(json格式的)转成json转储为abi_def结构,然后通过fc::raw::pack操作将结果赋值给abi
      • 将handler加上相关权限组装成action装入trx的actions集合中。
  • 使用账户txn.test.t创建token,标志位CUR,总发行量十亿,装成action装入trx的actions集合中。
  • issue CUR 给txn.test.t 600枚CUR,装成action装入trx的actions集合中。
  • 从txn.test.t转账给txn.test.a 200枚CUR,装成action装入trx的actions集合中。
  • 从txn.test.t转账给txn.test.b 200枚CUR,装成action装入trx的actions集合中。
  • trx的actions成员已经设置完毕,完成剩余trx的组装工作(同上),这里只介绍不同的部分
    • max_net_usage_words,指定了网络资源的最大使用限制为5000个词。

这一部分的源码展示如下:

//set txn.test.t contract to eosio.token & initialize it { signed_transaction trx; vector wasm = wast_to_wasm(std::string(eosio_token_wast)); setcode handler; handler.account = newaccountC; handler.code.assign(wasm.begin(), wasm.end()); trx.actions.emplace_back( vector{ {newaccountC,"active"}}, handler); { setabi handler; handler.account = newaccountC; handler.abi = fc::raw::pack(json::from_string(eosio_token_abi).as()); trx.actions.emplace_back( vector{ {newaccountC,"active"}}, handler); } { action act; act.account = N(txn.test.t); act.name = N(create); act.authorization = vector{ {newaccountC,config::active_name}}; act.data = eosio_token_serializer.variant_to_binary("create", fc::json::from_string("{\"issuer\":\"txn.test.t\",\"maximum_supply\":\"1000000000.0000 CUR\"}}")); trx.actions.push_back(act); } { action act; act.account = N(txn.test.t); act.name = N(issue); act.authorization = vector{ {newaccountC,config::active_name}}; act.data = eosio_token_serializer.variant_to_binary("issue", fc::json::from_string("{\"to\":\"txn.test.t\",\"quantity\":\"600.0000 CUR\",\"memo\":\"\"}")); trx.actions.push_back(act); } { action act; act.account = N(txn.test.t); act.name = N(transfer); act.authorization = vector{ {newaccountC,config::active_name}}; act.data = eosio_token_serializer.variant_to_binary("transfer", fc::json::from_string("{\"from\":\"txn.test.t\",\"to\":\"txn.test.a\",\"quantity\":\"200.0000 CUR\",\"memo\":\"\"}")); trx.actions.push_back(act); } { action act; act.account = N(txn.test.t); act.name = N(transfer); act.authorization = vector{ {newaccountC,config::active_name}}; act.data = eosio_token_serializer.variant_to_binary("transfer", fc::json::from_string("{\"from\":\"txn.test.t\",\"to\":\"txn.test.b\",\"quantity\":\"200.0000 CUR\",\"memo\":\"\"}")); trx.actions.push_back(act); } trx.expiration = cc.head_block_time() + fc::seconds(30); trx.set_reference_block(cc.head_block_id()); trx.max_net_usage_words = 5000; trx.sign(txn_test_receiver_C_priv_key, chainid); trxs.emplace_back(std::move(trx)); }

发起请求

目前trxs集合已经包含了两个trx元素,其中每个trx包含了多个action。下面要将trxs推送到链上执行

  • push_transactions函数,遍历trxs元素,每个trx单独发送push_next_transaction
  • push_next_transaction函数,首先将trx取出通过packed_transaction函数进行组装成post的结构
  • packed_transaction函数,通过set_transaction函数对trx进行摘捡,使用pack_transaction函数进行组装
  • pack_transaction函数,就是调用了一下上面提过的fc::raw::pack操作,然后通过accept_transaction函数向链发起请求
  • accept_transaction函数,是chain_plugin的一个函数,它内部调用了incoming_transaction_async_method异步发起交易请求。

这部分代码比较杂,分为几个部分:

push_transactions函数:

void push_transactions( std::vector&& trxs, const std::function& next ) { auto trxs_copy = std::make_shared>(std::move(trxs)); push_next_transaction(trxs_copy, 0, next); }

push_next_transaction函数:

static void push_next_transaction(const std::shared_ptr>& trxs, size_t index, const std::function& next ) { chain_plugin& cp = app().get_plugin(); cp.accept_transaction( packed_transaction(trxs->at(index)), [=](const fc::static_variant& result){ if (result.contains()) { next(result.get()); } else { if (index + 1 size()) { push_next_transaction(trxs, index + 1, next); } else { next(nullptr); } } }); }

packed_transaction函数,set_transaction函数以及pack_transaction函数的代码都属于本插件源码之外的EOS库源码,由于本身代码量也较少,含义在上面已经完全解释过了,这里不再粘贴源码。

accept_transaction函数也是EOS的库源码

void chain_plugin::accept_transaction(const chain::packed_transaction& trx, next_function next) { my->incoming_transaction_async_method(std::make_shared(trx), false, std::forward(next)); } incoming_transaction_async_method(app().get_method())

start_generation 接口

该接口的调用方法是:

curl --data-binary '["", 20, 20]' http://localhost:8888/v1/txn_test_gen/start_generation

参数列表为:

  • 第一个参数为 salt,一般用于“加盐”加密算法的值,这里我们可以留空。
  • 第二个参数为 period,发送交易的间隔时间,单位为ms,这里是20。
  • 第三个参数为 batch_size,每个发送间隔周期内打包交易的数量,这里也是20。

翻译过来就是:每20ms提交20笔交易。

接下来,以start_generation 函数为入口进行源码分析。

start_generation 函数

  • 校验:
    • period的取值范围为(1, 2500)
    • batch_size的取值范围为(1, 250)
    • batch_size必须是2的倍数,batch_size & 1结果为假0才可以,这是一个位运算,与&,所以batch_size的值转为二进制时末位不能为1,所以就是2的倍数即可。
    • 对标志位running的控制。

这部分代码展示如下:

if(running) throw fc::exception(fc::invalid_operation_exception_code); if(period <1 || period > 2500) throw fc::exception(fc::invalid_operation_exception_code); if(batch_size <1 || batch_size > 250) throw fc::exception(fc::invalid_operation_exception_code); if(batch_size & 1) throw fc::exception(fc::invalid_operation_exception_code); running = true;

  • 定义两个action,分别是:
    • 账户txn.test.a给txn.test.b转账1000枚CUR
    • txn.test.b转给txn.test.a同样1000枚CUR

这部分代码展示如下:

//create the actions here act_a_to_b.account = N(txn.test.t); act_a_to_b.name = N(transfer); act_a_to_b.authorization = vector{ {name("txn.test.a"),config::active_name}}; act_a_to_b.data = eosio_token_serializer.variant_to_binary("transfer", fc::json::from_string(fc::format_string("{\"from\":\"txn.test.a\",\"to\":\"txn.test.b\",\"quantity\":\"1.0000 CUR\",\"memo\":\"${l}\"}", fc::mutable_variant_object()("l", salt)))); act_b_to_a.account = N(txn.test.t); act_b_to_a.name = N(transfer); act_b_to_a.authorization = vector{ {name("txn.test.b"),config::active_name}}; act_b_to_a.data = eosio_token_serializer.variant_to_binary("transfer", fc::json::from_string(fc::format_string("{\"from\":\"txn.test.b\",\"to\":\"txn.test.a\",\"quantity\":\"1.0000 CUR\",\"memo\":\"${l}\"}", fc::mutable_variant_object()("l", salt))));

接下来,是对参数period和batch_size的储存为结构体作用域的变量以供结构体内其他函数调用,然后打印日志,最后调用arm_timer函数。

timer_timeout = period; // timer_timeout是结构体的成员变量 batch = batch_size/2; // batch是结构体的成员变量 ilog("Started transaction test plugin; performing ${p} transactions every ${m}ms", ("p", batch_size)("m", period)); arm_timer(boost::asio::high_resolution_timer::clock_type::now());

arm_timer 函数

从start_generation 函数过来,传入的参数是当前时间now,该函数主要功能是对计时器的初始化操作(计时器与文首的stop_generation函数中的关闭计时器呼应)。具体内容可分为两部分:

  • 设定计时器的过期时间,值为start_generation 接口的参数period与now相加的值,即从现在开始,过period这么久,当前计时器对象timer就过期。
  • 设定计时器的异步定时任务,任务体直接调用send_transaction函数,对函数的返回值进行处理,如果有报错信息(一般是服务中止)则调用stop_generation函数关闭插件。

注意stop_generation函数关闭的是定时任务的无限递归,中止定时任务,停止发送测试交易。但它并没有停止插件服务,我们仍旧可以通过再次请求插件接口启动无限测试交易。

这部分代码如下:

void arm_timer(boost::asio::high_resolution_timer::time_point s) { timer.expires_at(s + std::chrono::milliseconds(timer_timeout)); timer.async_wait([this](const boost::system::error_code& ec) { if(!running || ec) return; send_transaction([this](const fc::exception_ptr& e){ if (e) { elog("pushing transaction failed: ${e}", ("e", e->to_detail_string())); stop_generation(); } else { // 如果没有终止报错,则无限递归调用arm_timer函数,递归时传入的参数代替上面的now是当前timer对象的过期时间,这样在新的递归调用中,timer的创建会以这个时间再加上period,无间隔继续执行。 arm_timer(timer.expires_at()); } }); }); }

send_transaction 函数

这个函数是本插件的核心功能部分,主要是发送测试交易,对transaction的处理,将我们上面start_generation 函数中设置的两个action打包到transaction中去,以及对transaction各项属性的设置。具体步骤为:

  • 声明trxs,并为其设置大小为start_generation 接口中batch_size的值。

std::vector trxs; trxs.reserve(2*batch);

接下来,与上面介绍的create_test_accounts 接口的账户准备过程相同,准备私钥公钥,不多介绍。继续准备trx的参数:

  • nonce,是用来赋值context_free_actions的
  • context_free_actions:官方介绍一大堆,总之就是正常action是需要代价的,要确权,要占用主网资源什么的,所以搞了一个context_free_actions,字面意思就是上下文免费的action,这里权当测试用,填入的数据也是随机nonce组装的。
  • abi_serializer,用来序列化abi的,传入的system_account_name的abi值,它是在这里被赋值,然而是在结构体的作用域中被调用的。
  • reference_block_num的处理,引用区块,上面我们也提到过,而这里面增加了一层判断,是根据标志位txn_reference_block_lag的值来比较,也就是说reference_block_num最后的值是最新区块号减去txn_reference_block_lag的值,但是最小值为0,不可为负数。
  • 通过reference_block_num获得reference_block_id

这部分代码如下:

controller& cc = app().get_plugin().chain(); auto chainid = app().get_plugin().get_chain_id(); fc::crypto::private_key a_priv_key = fc::crypto::private_key::regenerate(fc::sha256(std::string(64, 'a'))); fc::crypto::private_key b_priv_key = fc::crypto::private_key::regenerate(fc::sha256(std::string(64, 'b'))); static uint64_t nOnce= static_cast(fc::time_point::now().sec_since_epoch()) <<32; abi_serializer eosio_serializer(cc.db().find(config::system_account_name)->get_abi()); uint32_t reference_block_num = cc.last_irreversible_block_num(); if (txn_reference_block_lag >= 0) { reference_block_num = cc.head_block_num(); if (reference_block_num <= (uint32_t)txn_reference_block_lag) { reference_block_num = 0; } else { reference_block_num -= (uint32_t)txn_reference_block_lag; } } block_id_type reference_block_id = cc.get_block_id_for_num(reference_block_num);

接下来,就是循环打包trx,我们设置的batch_size好比是20,现在我们已有两个action,每个action对应一个trx,则循环只需要执行10次,每次执行两个trx即可实现,每个trx相关的属性在上一阶段都已准备好。直接看代码吧。

for(unsigned int i = 0; i

最后,执行

push_transactions(std::move(trxs), next);

这个部分与create_test_accounts 接口发起请求的部分一致,这里不再重复展示。

总结

到这里为止,我们已经完全分析透了txn_test_gen_plugin 插件的内容。本文首先从大体上介绍了插件的架构,生命周期,通讯请求与返回。接着介绍了核心结构体的内容,然后以对外接口为入口,沿着一条线将每个功能的实现完整地研究清楚。通过本文的学习,我们对于EOS插件的体系有了初步深刻的理解,同时我们也完全搞清楚了txn_test_gen_plugin 插件的功能,以及它为什么会达到一个比较高的tps的表现。

参考资料

  • EOSIO/eos
  • eos官方文档

推荐阅读
  • 如何查询zone下的表的信息
    本文介绍了如何通过TcaplusDB知识库查询zone下的表的信息。包括请求地址、GET请求参数说明、返回参数说明等内容。通过curl方法发起请求,并提供了请求示例。 ... [详细]
  • 本文介绍了一种轻巧方便的工具——集算器,通过使用集算器可以将文本日志变成结构化数据,然后可以使用SQL式查询。集算器利用集算语言的优点,将日志内容结构化为数据表结构,SPL支持直接对结构化的文件进行SQL查询,不再需要安装配置第三方数据库软件。本文还详细介绍了具体的实施过程。 ... [详细]
  • 本文介绍了一个适用于PHP应用快速接入TRX和TRC20数字资产的开发包,该开发包支持使用自有Tron区块链节点的应用场景,也支持基于Tron官方公共API服务的轻量级部署场景。提供的功能包括生成地址、验证地址、查询余额、交易转账、查询最新区块和查询交易信息等。详细信息可参考tron-php的Github地址:https://github.com/Fenguoz/tron-php。 ... [详细]
  • 本文介绍了如何使用JSONObiect和Gson相关方法实现json数据与kotlin对象的相互转换。首先解释了JSON的概念和数据格式,然后详细介绍了相关API,包括JSONObject和Gson的使用方法。接着讲解了如何将json格式的字符串转换为kotlin对象或List,以及如何将kotlin对象转换为json字符串。最后提到了使用Map封装json对象的特殊情况。文章还对JSON和XML进行了比较,指出了JSON的优势和缺点。 ... [详细]
  • 本文介绍了Codeforces Round #321 (Div. 2)比赛中的问题Kefa and Dishes,通过状压和spfa算法解决了这个问题。给定一个有向图,求在不超过m步的情况下,能获得的最大权值和。点不能重复走。文章详细介绍了问题的题意、解题思路和代码实现。 ... [详细]
  • Android实战——jsoup实现网络爬虫,糗事百科项目的起步
    本文介绍了Android实战中使用jsoup实现网络爬虫的方法,以糗事百科项目为例。对于初学者来说,数据源的缺乏是做项目的最大烦恼之一。本文讲述了如何使用网络爬虫获取数据,并以糗事百科作为练手项目。同时,提到了使用jsoup需要结合前端基础知识,以及如果学过JS的话可以更轻松地使用该框架。 ... [详细]
  • 简述在某个项目中需要分析PHP代码,分离出对应的函数调用(以及源代码对应的位置)。虽然这使用正则也可以实现,但无论从效率还是代码复杂度方面考虑ÿ ... [详细]
  • Spring源码解密之默认标签的解析方式分析
    本文分析了Spring源码解密中默认标签的解析方式。通过对命名空间的判断,区分默认命名空间和自定义命名空间,并采用不同的解析方式。其中,bean标签的解析最为复杂和重要。 ... [详细]
  • 云原生边缘计算之KubeEdge简介及功能特点
    本文介绍了云原生边缘计算中的KubeEdge系统,该系统是一个开源系统,用于将容器化应用程序编排功能扩展到Edge的主机。它基于Kubernetes构建,并为网络应用程序提供基础架构支持。同时,KubeEdge具有离线模式、基于Kubernetes的节点、群集、应用程序和设备管理、资源优化等特点。此外,KubeEdge还支持跨平台工作,在私有、公共和混合云中都可以运行。同时,KubeEdge还提供数据管理和数据分析管道引擎的支持。最后,本文还介绍了KubeEdge系统生成证书的方法。 ... [详细]
  • 本文分享了一个关于在C#中使用异步代码的问题,作者在控制台中运行时代码正常工作,但在Windows窗体中却无法正常工作。作者尝试搜索局域网上的主机,但在窗体中计数器没有减少。文章提供了相关的代码和解决思路。 ... [详细]
  • 本文讨论了使用差分约束系统求解House Man跳跃问题的思路与方法。给定一组不同高度,要求从最低点跳跃到最高点,每次跳跃的距离不超过D,并且不能改变给定的顺序。通过建立差分约束系统,将问题转化为图的建立和查询距离的问题。文章详细介绍了建立约束条件的方法,并使用SPFA算法判环并输出结果。同时还讨论了建边方向和跳跃顺序的关系。 ... [详细]
  • 图像因存在错误而无法显示 ... [详细]
  • 本文介绍了一个React Native新手在尝试将数据发布到服务器时遇到的问题,以及他的React Native代码和服务器端代码。他使用fetch方法将数据发送到服务器,但无法在服务器端读取/获取发布的数据。 ... [详细]
  • 使用eclipse创建一个Java项目的步骤
    本文介绍了使用eclipse创建一个Java项目的步骤,包括启动eclipse、选择New Project命令、在对话框中输入项目名称等。同时还介绍了Java Settings对话框中的一些选项,以及如何修改Java程序的输出目录。 ... [详细]
  • 本文介绍了在go语言中利用(*interface{})(nil)传递参数类型的原理及应用。通过分析Martini框架中的injector类型的声明,解释了values映射表的作用以及parent Injector的含义。同时,讨论了该技术在实际开发中的应用场景。 ... [详细]
author-avatar
摋無赦PK110_147
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有