将本节点加入Kademlia网络

连接请求的发起

aMule在启动的时候,会起一些定时器,以便于定期的执行一些任务。其中比较重要的就是core_timer,相关code如下(amule-2.3.1/src/amule-gui.cpp):

// Create the Core timercore_timer = new CTimer(this,ID_CORE_TIMER_EVENT);if (!core_timer) {AddLogLineCS(_("Fatal Error: Failed to create Core Timer"));OnExit();}// Start the Core and Gui timers// Note: wxTimer can be off by more than 10% !!!// In addition to the systematic error introduced by wxTimer, we are losing// timer cycles due to high CPU load.  I've observed about 0.5% random loss of cycles under// low load, and more than 6% lost cycles with heavy download traffic and/or other tasks// in the system, such as a video player or a VMware virtual machine.// The upload queue process loop has now been rewritten to compensate for timer errors.// When adding functionality, assume that the timer is only approximately correct;// for measurements, always use the system clock [::GetTickCount()].core_timer->Start(CORE_TIMER_PERIOD);

wxWidgets的定时器,定期的产生一些事件,具体的事件在Timer创建时传入,而定时器的周期则在Start()时传入。在amule-2.3.1/src/amule.h中可以看到CORE_TIMER_PERIOD的定义为100,也就是说定时器的周期是100ms。

在amule-2.3.1/src/amule-gui.cpp的EventTable中,可以看到事件将由CamuleGuiApp::OnCoreTimer()处理:

// Core timerEVT_MULE_TIMER(ID_CORE_TIMER_EVENT, CamuleGuiApp::OnCoreTimer)

在CamuleApp::OnCoreTimer()函数(amule-2.3.1/src/amule.cpp)中,会执行Kademlia::CKademlia::Process():

if (msCur-msPrev1 > 1000) {  // approximately every secondmsPrev1 = msCur;clientcredits->Process();clientlist->Process();// Publish files to server if needed.sharedfiles->Process();if( Kademlia::CKademlia::IsRunning() ) {Kademlia::CKademlia::Process();if(Kademlia::CKademlia::GetPrefs()->HasLostConnection()) {StopKad();clientudp->Close();clientudp->Open();if (thePrefs::Reconnect()) {StartKad();}}}

在Kademlia::CKademlia::Process()(文件amule-2.3.1/src/kademlia/kademlia/Kademlia.cpp)中,主要来关注如下的几行:

if (m_nextSelfLookup <= now) {CSearchManager::FindNode(instance->m_prefs->GetKadID(), true);m_nextSelfLookup = HR2S(4) + now;}

回想 Kademlia 网络在启动的时候,会执行的CKademlia::Start(),其中有这么几行:

// Force a FindNodeComplete within the first 3 minutes.m_nextSelfLookup = time(NULL) + MIN2S(3);

综合来看这两段code,也就是说,在启动之后3分钟,将首次执行 CSearchManager::FindNode(instance->m_prefs->GetKadID(), true) ,而后,则将每隔4个小时执行这个方法一次。

也就意味着,在Kademlia模块启动之后3分钟,将首次搜寻KadID与本节点最接近的节点,然后与它们建立连接,并将它们作为邻居节点。之后则将每隔4个小时执行一次相同的过程。

那我们来看CSearchManager::FindNode()的执行(amule-2.3.1/src/kademlia/kademlia/SearchManager.cpp):

void CSearchManager::FindNode(const CUInt128& id, bool complete)
{// Do a node lookup.CSearch *s = new CSearch;if (complete) {s->SetSearchTypes(CSearch::NODECOMPLETE);} else {s->SetSearchTypes(CSearch::NODE);}s->SetTargetID(id);StartSearch(s);
}。。。。。。bool CSearchManager::StartSearch(CSearch* search)
{// A search object was created, now try to start the search.if (AlreadySearchingFor(search->GetTarget())) {// There was already a search in progress with this target.delete search;return false;}// Add to the search mapm_searches[search->GetTarget()] = search;// Start the search.search->Go();return true;
}

如我们前面在Linux下电骡aMule Kademlia网络构建分析2中看到的,这个请求发出去,能得到的响应也只是一些节点的信息。

那Kademlia网络中一个节点是如何连接到网络中的另一个节点的呢?先回想一下 Linux下电骡aMule Kademlia网络构建分析I 一文,CRoutingZone的初始化函数CRoutingZone::Init()会调用到CRoutingZone::StartTimer(),其中又调用了CKademlia::AddEvent(),如下所示(amule-2.3.1/src/kademlia/routing/RoutingZone.cpp):

void CRoutingZone::StartTimer()
{// Start filling the tree, closest bins first.m_nextBigTimer = time(NULL) + SEC(10);CKademlia::AddEvent(this);
}

再来看CKademlia::AddEvent(),在amule-2.3.1/src/kademlia/kademlia/Kademlia.h中:

static void AddEvent(CRoutingZone *zone) throw()       { m_events[zone] = zone; }

也就是把当前CRoutingZone对象的指针,保存在CKademlia的一个map中,key和value都是该指针。

在定期会被执行的CKademlia::Process()函数中,我们还能看到如下的这样一段code:

for (EventMap::const_iterator it = m_events.begin(); it != m_events.end(); ++it) {CRoutingZone *zone = it->first;if (updateUserFile) {// The EstimateCount function is not made for really small networks, if we are in LAN mode, it is actually// better to assume that all users of the network are in our routing table and use the real count functionif (IsRunningInLANMode()) {tempUsers = zone->GetNumContacts();} else {tempUsers = zone->EstimateCount();}if (maxUsers < tempUsers) {maxUsers = tempUsers;}}if (m_bigTimer <= now) {if (zone->m_nextBigTimer <= now) {if(zone->OnBigTimer()) {zone->m_nextBigTimer = HR2S(1) + now;m_bigTimer = SEC(10) + now;}} else {if (lastContact && (now - lastContact > KADEMLIADISCONNECTDELAY - MIN2S(5))) {if(zone->OnBigTimer()) {zone->m_nextBigTimer = HR2S(1) + now;m_bigTimer = SEC(10) + now;}} }}if (zone->m_nextSmallTimer <= now) {zone->OnSmallTimer();zone->m_nextSmallTimer = MIN2S(1) + now;}}

也就是遍历所有的CRoutingZone对象,并适时地调用一些CRoutingZone对象的需要定时执行的一些方法。其中会调用到 CRoutingZone::OnSmallTimer() 函数,周期大约为1分钟。我们可以具体来看一下这个函数的实现(amule-2.3.1/src/kademlia/routing/RoutingZone.cpp):

void CRoutingZone::OnSmallTimer()
{if (!IsLeaf()) {return;}CContact *c = NULL;time_t now = time(NULL);ContactList entries;// Remove dead entriesm_bin->GetEntries(&entries);for (ContactList::iterator it = entries.begin(); it != entries.end(); ++it) {c = *it;if (c->GetType() == 4) {if ((c->GetExpireTime() > 0) && (c->GetExpireTime() <= now)) {if (!c->InUse()) {m_bin->RemoveContact(c);delete c;}continue;}}if(c->GetExpireTime() == 0) {c->SetExpireTime(now);}}c = m_bin->GetOldest();if (c != NULL) {if (c->GetExpireTime() >= now || c->GetType() == 4) {m_bin->PushToBottom(c);c = NULL;}}if (c != NULL) {c->CheckingType();if (c->GetVersion() >= 6) {DebugSend(Kad2HelloReq, c->GetIPAddress(), c->GetUDPPort());CUInt128 clientID = c->GetClientID();CKademlia::GetUDPListener()->SendMyDetails(KADEMLIA2_HELLO_REQ, c->GetIPAddress(), c->GetUDPPort(), c->GetVersion(), c->GetUDPKey(), &clientID, false);if (c->GetVersion() >= 8) {// FIXME:// This is a bit of a work around for statistic values. Normally we only count values from incoming HELLO_REQs for// the firewalled statistics in order to get numbers from nodes which have us on their routing table,// however if we send a HELLO due to the timer, the remote node won't send a HELLO_REQ itself anymore (but// a HELLO_RES which we don't count), so count those statistics here. This isn't really accurate, but it should// do fair enough. Maybe improve it later for example by putting a flag into the contact and make the answer countCKademlia::GetPrefs()->StatsIncUDPFirewalledNodes(false);CKademlia::GetPrefs()->StatsIncTCPFirewalledNodes(false);}} else if (c->GetVersion() >= 2) {DebugSend(Kad2HelloReq, c->GetIPAddress(), c->GetUDPPort());CKademlia::GetUDPListener()->SendMyDetails(KADEMLIA2_HELLO_REQ, c->GetIPAddress(), c->GetUDPPort(), c->GetVersion(), 0, NULL, false);wxASSERT(c->GetUDPKey() == CKadUDPKey(0));} else {wxFAIL;}}
}

1. 这个函数会首先确保当前CRoutingZone是一个叶子节点。(只有在叶子节点中才会保存其他节点,也就是联系人的信息,这与aMule管理联系人的数据结构设计有关。)

2. 随后会遍历所有的联系人,移除那些当前时间已经过了有效时间,又没在使用的联系人,而对于有效时间为0的联系人,则将有效时间设置为当前时间。

3. 找出最老,同时当前时间又没有超出它的有效时间的一个节点。

4. 调用CKademlia::GetUDPListener()->SendMyDetails()函数,向找到的节点发送一个KADEMLIA2_HELLO_REQ请求,其中会携带有本节点的详细信息。KADEMLIA2_HELLO_REQ请求也就是aMule Kademlia网络的连接请求。

这里可以在看一下CKademliaUDPListener::SendMyDetails()函数,来了解一下具体都会发送本节点的哪些信息(amule-2.3.1/src/kademlia/net/KademliaUDPListener.cpp)

// Used by Kad1.0 and Kad2.0
void CKademliaUDPListener::SendMyDetails(uint8_t opcode, uint32_t ip, uint16_t port, uint8_t kadVersion, const CKadUDPKey& targetKey, const CUInt128* cryptTargetID, bool requestAckPacket)
{CMemFile packetdata;packetdata.WriteUInt128(CKademlia::GetPrefs()->GetKadID());if (kadVersion > 1) {packetdata.WriteUInt16(thePrefs::GetPort());packetdata.WriteUInt8(KADEMLIA_VERSION);// Tag Count.uint8_t tagCount = 0;if (!CKademlia::GetPrefs()->GetUseExternKadPort()) {tagCount++;}if (kadVersion >= 8 && (requestAckPacket || CKademlia::GetPrefs()->GetFirewalled() || CUDPFirewallTester::IsFirewalledUDP(true))) {tagCount++;}packetdata.WriteUInt8(tagCount);if (!CKademlia::GetPrefs()->GetUseExternKadPort()) {packetdata.WriteTag(CTagVarInt(TAG_SOURCEUPORT, CKademlia::GetPrefs()->GetInternKadPort()));}if (kadVersion >= 8 && (requestAckPacket || CKademlia::GetPrefs()->GetFirewalled() || CUDPFirewallTester::IsFirewalledUDP(true))) {// if we're firewalled we send this tag, so the other client doesn't add us to his routing table (if UDP firewalled) and for statistics reasons (TCP firewalled)// 5 - reserved (!)// 1 - requesting HELLO_RES_ACK// 1 - TCP firewalled// 1 - UDP firewalledpacketdata.WriteTag(CTagVarInt(TAG_KADMISCOPTIONS, (uint8_t)((requestAckPacket ? 1 : 0) << 2 |(CKademlia::GetPrefs()->GetFirewalled() ? 1 : 0) << 1 |(CUDPFirewallTester::IsFirewalledUDP(true) ? 1 : 0))));}if (kadVersion >= 6) {if (cryptTargetID == NULL || *cryptTargetID == 0) {AddDebugLogLineN(logClientKadUDP, CFormat(wxT("Sending hello response to crypt enabled Kad Node which provided an empty NodeID: %s (%u)")) % KadIPToString(ip) % kadVersion);SendPacket(packetdata, opcode, ip, port, targetKey, NULL);} else {SendPacket(packetdata, opcode, ip, port, targetKey, cryptTargetID);}} else {SendPacket(packetdata, opcode, ip, port, 0, NULL);wxASSERT(targetKey.IsEmpty());}} else {wxFAIL;}
}

可以看到,主要的信息有,端口号,KAD的版本,以及和TAG有关的一些信息等。CKademliaUDPListener::SendPacket()的执行,如我们前面在 Linux下电骡aMule Kademlia网络构建分析2 中看到的那样,此处不再赘述。

KADEMLIA2_HELLO_REQ消息的处理

连接请求是发出去了,那收到请求的节点又会如何处理这样的请求呢?

在CKademliaUDPListener::ProcessPacket()这个函数里,可以看到这样的一个case(amule-2.3.1/src/kademlia/net/KademliaUDPListener.cpp,更详细的事件传递过程,可以来看 Linux下电骡aMule Kademlia网络构建分析2):

case KADEMLIA2_HELLO_REQ:DebugRecv(Kad2HelloReq, ip, port);Process2HelloRequest(packetData, lenPacket, ip, port, senderKey, validReceiverKey);break;

也就是说,消息会被委托给CKademliaUDPListener::Process2HelloRequest()函数处理,该函数定义如下所示:

// Used only for Kad2.0
bool CKademliaUDPListener::AddContact2(const uint8_t *data, uint32_t lenData, uint32_t ip, uint16_t& port, uint8_t *outVersion, const CKadUDPKey& udpKey, bool& ipVerified, bool update, bool fromHelloReq, bool* outRequestsACK, CUInt128* outContactID)
{if (outRequestsACK != 0) {*outRequestsACK = false;}CMemFile bio(data, lenData);CUInt128 id = bio.ReadUInt128();if (outContactID != NULL) {*outContactID = id;}uint16_t tport = bio.ReadUInt16();uint8_t version = bio.ReadUInt8();if (version == 0) {throw wxString(CFormat(wxT("***NOTE: Received invalid Kademlia2 version (%u) in %s")) % version % wxString::FromAscii(__FUNCTION__));}if (outVersion != NULL) {*outVersion = version;}bool udpFirewalled = false;bool tcpFirewalled = false;uint8_t tags = bio.ReadUInt8();while (tags) {CTag *tag = bio.ReadTag();if (!tag->GetName().Cmp(TAG_SOURCEUPORT)) {if (tag->IsInt() && (uint16_t)tag->GetInt() > 0) {port = tag->GetInt();}} else if (!tag->GetName().Cmp(TAG_KADMISCOPTIONS)) {if (tag->IsInt() && tag->GetInt() > 0) {udpFirewalled = (tag->GetInt() & 0x01) > 0;tcpFirewalled = (tag->GetInt() & 0x02) > 0;if ((tag->GetInt() & 0x04) > 0) {if (outRequestsACK != NULL) {if (version >= 8) {*outRequestsACK = true;}} else {wxFAIL;}}}}delete tag;--tags;}// check if we are waiting for informations (nodeid) about this client and if so inform the requesterfor (FetchNodeIDList::iterator it = m_fetchNodeIDRequests.begin(); it != m_fetchNodeIDRequests.end(); ++it) {if (it->ip == ip && it->tcpPort == tport) {//AddDebugLogLineN(logKadMain, wxT("Result Addcontact: ") + id.ToHexString());uint8_t uchID[16];id.ToByteArray(uchID);it->requester->KadSearchNodeIDByIPResult(KCSR_SUCCEEDED, uchID);m_fetchNodeIDRequests.erase(it);break;}}if (fromHelloReq && version >= 8) {// this is just for statistic calculations. We try to determine the ratio of (UDP) firewalled users,// by counting how many of all nodes which have us in their routing table (our own routing table is supposed// to have no UDP firewalled nodes at all) and support the firewalled tag are firewalled themself.// Obviously this only works if we are not firewalled ourselfCKademlia::GetPrefs()->StatsIncUDPFirewalledNodes(udpFirewalled);CKademlia::GetPrefs()->StatsIncTCPFirewalledNodes(tcpFirewalled);}if (!udpFirewalled) {    // do not add (or update) UDP firewalled sources to our routing tablereturn CKademlia::GetRoutingZone()->Add(id, ip, port, tport, version, udpKey, ipVerified, update, true);} else {AddDebugLogLineN(logKadRouting, wxT("Not adding firewalled client to routing table (") + KadIPToString(ip) + wxT(")"));return false;}
}。。。。。。// KADEMLIA2_HELLO_REQ
// Used in Kad2.0 only
void CKademliaUDPListener::Process2HelloRequest(const uint8_t *packetData, uint32_t lenPacket, uint32_t ip, uint16_t port, const CKadUDPKey& senderKey, bool validReceiverKey)
{DEBUG_ONLY( uint16_t dbgOldUDPPort = port; )uint8_t contactVersion = 0;CUInt128 contactID;bool addedOrUpdated = AddContact2(packetData, lenPacket, ip, port, &contactVersion, senderKey, validReceiverKey, true, true, NULL, &contactID); // might change (udp)port, validReceiverKeywxASSERT(contactVersion >= 2);
#ifdef __DEBUG__if (dbgOldUDPPort != port) {AddDebugLogLineN(logClientKadUDP, CFormat(wxT("KadContact %s uses his internal (%u) instead external (%u) UDP Port")) % KadIPToString(ip) % port % dbgOldUDPPort);}
#endifAddLogLineNS(wxT("") + CFormat(_("KadContact %s uses his UDP Port (%u) to send KADEMLIA2_HELLO_RES.")) % KadIPToString(ip) % port);DebugSend(Kad2HelloRes, ip, port);// if this contact was added or updated (so with other words not filtered or invalid) to our routing table and did not already send a valid// receiver key or is already verified in the routing table, we request an additional ACK package to complete a three-way-handshake and// verify the remote IPSendMyDetails(KADEMLIA2_HELLO_RES, ip, port, contactVersion, senderKey, &contactID, addedOrUpdated && !validReceiverKey);if (addedOrUpdated && !validReceiverKey && contactVersion == 7 && !HasActiveLegacyChallenge(ip)) {// Kad Version 7 doesn't support HELLO_RES_ACK but sender/receiver keys, so send a ping to validateDebugSend(Kad2Ping, ip, port);SendNullPacket(KADEMLIA2_PING, ip, port, senderKey, NULL);
#ifdef __DEBUG__CContact* contact = CKademlia::GetRoutingZone()->GetContact(contactID);if (contact != NULL) {if (contact->GetType() < 2) {AddDebugLogLineN(logKadRouting, wxT("Sending (ping) challenge to a long known contact (should be verified already) - ") + KadIPToString(ip));}} else {wxFAIL;}
#endif} else if (CKademlia::GetPrefs()->FindExternKadPort(false) && contactVersion > 5) { // do we need to find out our extern port?DebugSend(Kad2Ping, ip, port);SendNullPacket(KADEMLIA2_PING, ip, port, senderKey, NULL);}if (addedOrUpdated && !validReceiverKey && contactVersion < 7 && !HasActiveLegacyChallenge(ip)) {// we need to verify this contact but it doesn't support HELLO_RES_ACK nor keys, do a little workaroundSendLegacyChallenge(ip, port, contactID);}// Check if firewalledif (CKademlia::GetPrefs()->GetRecheckIP()) {FirewalledCheck(ip, port, senderKey, contactVersion);}
}

Process2HelloRequest ()函数主要做了两件事情,

1. 调用CKademliaUDPListener::AddContact2()函数,添加联系人。

2. 调用CKademliaUDPListener::SendMyDetails()函数发送本节点的信息,只不过这次是包在一个KADEMLIA2_HELLO_RES消息里的,其它的就与前面发送KADEMLIA2_HELLO_REQ消息的过程一样了。

KADEMLIA2_HELLO_RES消息的处理

连接的目标节点发送了响应消息KADEMLIA2_HELLO_RES,那就再来看一下连接的发起端对于这个消息的处理。

在CKademliaUDPListener::ProcessPacket()这个函数里,可以看到这样的一个case(amule-2.3.1/src/kademlia/net/KademliaUDPListener.cpp,更详细的事件传递过程,可以来看 Linux下电骡aMule Kademlia网络构建分析2):

         case KADEMLIA2_HELLO_RES:DebugRecv(Kad2HelloRes, ip, port);Process2HelloResponse(packetData, lenPacket, ip, port, senderKey, validReceiverKey);break;

也就是说,消息会被委托给CKademliaUDPListener::Process2HelloResponse()函数处理,该函数定义如下所示:

// KADEMLIA2_HELLO_RES
// Used in Kad2.0 only
void CKademliaUDPListener::Process2HelloResponse(const uint8_t *packetData, uint32_t lenPacket, uint32_t ip, uint16_t port, const CKadUDPKey& senderKey, bool validReceiverKey)
{CHECK_TRACKED_PACKET(KADEMLIA2_HELLO_REQ);// Add or Update contact.uint8_t contactVersion;CUInt128 contactID;bool sendACK = false;bool addedOrUpdated = AddContact2(packetData, lenPacket, ip, port, &contactVersion, senderKey, validReceiverKey, true, false, &sendACK, &contactID);if (sendACK) {// the client requested us to send an ACK packet, which proves that we're not a spoofed fake contact// fulfill his wishif (senderKey.IsEmpty()) {// but we don't have a valid sender key - there is no point to reply in this case// most likely a bug in the remote clientAddDebugLogLineN(logClientKadUDP, wxT("Remote client demands ACK, but didn't send any sender key! (sender: ") + KadIPToString(ip) + wxT(")"));} else {CMemFile packet(17);packet.WriteUInt128(CKademlia::GetPrefs()->GetKadID());packet.WriteUInt8(0);    // no tags at this timeDebugSend(Kad2HelloResAck, ip, port);SendPacket(packet, KADEMLIA2_HELLO_RES_ACK, ip, port, senderKey, NULL);}} else if (addedOrUpdated && !validReceiverKey && contactVersion < 7) {// even though this is supposably an answer to a request from us, there are still possibilities to spoof// it, as long as the attacker knows that we would send a HELLO_REQ (which in this case is quite often),// so for old Kad Version which doesn't support keys, we needSendLegacyChallenge(ip, port, contactID);}// do we need to find out our extern port?if (CKademlia::GetPrefs()->FindExternKadPort(false) && contactVersion > 5) {DebugSend(Kad2Ping, ip, port);SendNullPacket(KADEMLIA2_PING, ip, port, senderKey, NULL);}// Check if firewalledif (CKademlia::GetPrefs()->GetRecheckIP()) {FirewalledCheck(ip, port, senderKey, contactVersion);}
}

这个函数做的最主要的事情就是将节点信息添加到本节点的联系人列表里了。然后根据情况,会再发送相应消息回去。

大体如此。

Done。

转载于:https://my.oschina.net/wolfcs/blog/488387

Linux下电骡aMule Kademlia网络构建分析3相关推荐

  1. Linux下电骡aMule Kademlia网络构建分析2

    读代码读到现在,补充一点关于Kademlia网络的理论知识. Kademlia网络的基本原理 Kademlia 是一种结构化的覆盖网络(Structured Overlay Network).所谓覆盖 ...

  2. Linux下电骡aMule Kademlia网络构建分析4

    aMule中联系人的管理 aMule中主要通过CContact,CRoutingBin和CRoutingZone这样几个类来管理它的联系人. CContact表示一个联系人,它包含了与一个联系人有关的 ...

  3. Linux下电骡aMule Kademlia网络构建分析5 —— 资源的发布

    资源发布请求消息的发送 在aMule中,主要用CSharedFileList class来管理共享给其它节点的文件.如我们前面在 Linux下电骡aMule Kademlia网络构建分析3 一文中分析 ...

  4. Linux下电骡aMule Kademlia网络构建分析I

    (以下分析基于ubuntu aMule 2.3.1进行.) aMule代码的下载和编译 为了能尽量缩短aMule代码的下载.编译及编译运行所依赖的环境的建立所耗费的时间,并尽快启动对于它的研究学习,而 ...

  5. Linux下C/C++实现(网络流量分析-NTA)

    网络流量分析(NTA - Network Traffic Analysis) 就是捕捉网络中流动的数据包,并通过查看包内部数据以及进行相关的协议.流量.分析.统计等,协助发现网络运行过程中出现的问题. ...

  6. linux下使用TC模拟弱网络环境

    linux下使用TC模拟弱网络环境 模拟延迟传输简介 netem 与 tc: netem 是 Linux 2.6 及以上内核版本提供的一个网络模拟功能模块.该功能模块可以用来在性能良好的局域网中,模拟 ...

  7. linux sd卡测速工具下载,Linux下3种常用的网络测速工具

    原标题:Linux下3种常用的网络测速工具 不管你用的是什么操作系统,网速都是你非常关心的一个性能指标,毕竟,谁都不想看个视频结果网速卡到你怀疑人生.本文介绍三个 Linux 命令行下的网络测速工具, ...

  8. linux和mac下的电骡 amule 2.2.6下载,aMule Mac版下载_aMule Mac版官方下载-太平洋下载中心...

    aMule Mac版又被称为跨平台版电骡,类似于电驴eMule,支持KAD和eD2k资源的下载,是一款Mac OS平台上的开源免费的P2P文件共享下载软件. 软件截图1 功能介绍 aMule支持KAD ...

  9. linux和mac下的电骡 amule 2.2.6下载,跨平台电骡 aMule 2.2.6

    aMule官方下载:http://www.amule.org/ 源代码:ed2k://|file|aMule-2.2.6.tar.bz2|4722143|34A433C13B6BBC21176A18C ...

最新文章

  1. EF +MYSQL 配置
  2. Jerry文章《浅谈Java和SAP ABAP的静态代理和动态代理,以及ABAP面向切面编程的尝试一文的源代码》
  3. DEDE文章列表加上序号效果
  4. staf工作笔记-对STAX进行扩展(配置并运行官方的Delay实例)
  5. 微信小程序时代,哪些人能赚到第一桶金
  6. 开关电源雷击浪涌整改_高频开关电源的EMC电磁兼容整改问题分析
  7. 美团互助关停:聚焦主业发展 将全额返还会员分摊
  8. 同样是点工,凭什么他拿月薪20k,你却只有10k?
  9. 软件工程小学期经历part1
  10. 频域采样与恢复matlab实验,实验二 时域采样与频域采样及MATLAB程序
  11. 夏新N820/N821 recovery刷入教程附MT6577专用刷机工具SP_Flash_Tool_v3.1308.0.125
  12. 【M1兼容】草图大师mac 英文版 SketchUp 2021 Mac
  13. 《Adaptive Unfolding Total Variation Network for Low-Light Image Enhancement》2021ICCV 弱光增强
  14. 阿里云安装Jdk1.8,So easy!
  15. 治理企业“数据悬河”,阿里云DataWorks全链路数据治理新品发布
  16. 【微软 Azure 认知服务】零基础搭建微软 Azure AI 认知服务实验分享
  17. Jenkins使用问题记录
  18. Pandas 基础(16) - Holidays
  19. 设计模式(10)——策略模式
  20. xsy3320 string

热门文章

  1. 数据结构翻转课堂答疑实录——链表
  2. Codeforces 1203E Boxers(贪心)
  3. 前端传后端,后端传前端,数组与字符串之间的转换。implode和explode
  4. 词云中去重复的词_手把手教你怎么挖掘蓝海词,打造淘宝爆款标题
  5. 清除指定域名下的cookie
  6. 工单系统是什么?工单系统有什么功能?
  7. 前台js实现附件比如word或者pdf的预览
  8. 打开word文档时遇到错误(1)
  9. 推荐系统:如何进行优化!
  10. 城市记忆(4)赫连果城——(白口骝)薄骨律——刁公城