当前位置: 首页 > news >正文

Linux下电骡aMule Kademlia网络构建分析3

将本节点加入Kademlia网络

连接请求的发起

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

// Create the Core timer
	core_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 timer
	EVT_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 second
		msPrev1 = 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 map
	m_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 function
			if (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 entries
	m_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 count
				CKademlia::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 firewalled
			packetdata.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 requester
    for (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 ourself
        CKademlia::GetPrefs()->StatsIncUDPFirewalledNodes(udpFirewalled);
        CKademlia::GetPrefs()->StatsIncTCPFirewalledNodes(tcpFirewalled);
    }

    if (!udpFirewalled) {    // do not add (or update) UDP firewalled sources to our routing table
        return 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, validReceiverKey
	wxASSERT(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);
	}
#endif
	AddLogLineNS(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 IP
	SendMyDetails(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 validate
		DebugSend(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 workaround
		SendLegacyChallenge(ip, port, contactID);
	}

	// Check if firewalled
	if (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 wish
		if (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 client
			AddDebugLogLineN(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 time
			DebugSend(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 need
		SendLegacyChallenge(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 firewalled
	if (CKademlia::GetPrefs()->GetRecheckIP()) {
		FirewalledCheck(ip, port, senderKey, contactVersion);
	}
}

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

大体如此。

Done。

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

相关文章:

  • C# List集合基础操作
  • bootstrap-表格-响应式表格
  • Linux下的ls 常用命令
  • jquery ui autocomplete 自动补充完成 测试在ie下 点击显示列表框 有时候需要多次点击才能选取值...
  • mysql 误删root
  • 数据库查询
  • 黑马程序员——Java基础语法---数组
  • Reinhold就Jigsaw投票一事向JCP提交公开信
  • eclipse快捷键调试总结 -转--快捷键大全
  • git基本操作
  • bootstrap(4)关于下拉菜单的功能
  • ASP.NET 5探险(8):利用中间件、TagHelper来在MVC 6中实现Captcha
  • 【c语言】统计一个数字在排序数组中出现的次数
  • CentOS 下安装testlink
  • 历史记录
  • co.js - 让异步代码同步化
  • export和import的用法总结
  • vue+element后台管理系统,从后端获取路由表,并正常渲染
  • Windows Containers 大冒险: 容器网络
  • yii2中session跨域名的问题
  • 从输入URL到页面加载发生了什么
  • 技术发展面试
  • 深度学习在携程攻略社区的应用
  • 数据库写操作弃用“SELECT ... FOR UPDATE”解决方案
  • 吴恩达Deep Learning课程练习题参考答案——R语言版
  • MiKTeX could not find the script engine ‘perl.exe‘ which is required to execute ‘latexmk‘.
  • ionic异常记录
  • PostgreSQL 快速给指定表每个字段创建索引 - 1
  • RDS-Mysql 物理备份恢复到本地数据库上
  • 宾利慕尚创始人典藏版国内首秀,2025年前实现全系车型电动化 | 2019上海车展 ...
  • #Linux(Source Insight安装及工程建立)
  • (day6) 319. 灯泡开关
  • (非本人原创)史记·柴静列传(r4笔记第65天)
  • (附源码)ssm考生评分系统 毕业设计 071114
  • (附源码)小程序 交通违法举报系统 毕业设计 242045
  • (南京观海微电子)——COF介绍
  • (原創) X61用戶,小心你的上蓋!! (NB) (ThinkPad) (X61)
  • (原創) 如何使用ISO C++讀寫BMP圖檔? (C/C++) (Image Processing)
  • .apk 成为历史!
  • .chm格式文件如何阅读
  • .NET C#版本和.NET版本以及VS版本的对应关系
  • .net core webapi Startup 注入ConfigurePrimaryHttpMessageHandler
  • .NET Core日志内容详解,详解不同日志级别的区别和有关日志记录的实用工具和第三方库详解与示例
  • .NET 读取 JSON格式的数据
  • .NET业务框架的构建
  • /var/log/cvslog 太大
  • @RequestParam @RequestBody @PathVariable 等参数绑定注解详解
  • [ JavaScript ] JSON方法
  • [ web基础篇 ] Burp Suite 爆破 Basic 认证密码
  • [2009][note]构成理想导体超材料的有源THz欺骗表面等离子激元开关——
  • [BZOJ 4598][Sdoi2016]模式字符串
  • [BZOJ] 2427: [HAOI2010]软件安装
  • [G-CS-MR.PS02] 機巧之形2: Ruler Circle
  • [HJ56 完全数计算]
  • [JS7] 显示从0到99的100个数字