Linux下電騾aMule Kademlia網絡構建分析I

(如下分析基於ubuntu aMule 2.3.1進行。) node

aMule代碼的下載和編譯

爲了能儘可能縮短aMule代碼的下載、編譯及編譯運行所依賴的環境的創建所耗費的時間,並儘快啓動對於它的研究學習,而直接使用了ubuntu的代碼下載及編譯工具。具體的代碼下載及編譯方法以下: shell

apt-get source amule
sudo apt-get build-dep amule
cd amule-2.3.1
dpkg-buildpackage -rfakeroot -uc -b

Kademlia網絡的啓動

首先來看一下aMule的Kademlia網絡啓動的UI操做。 bootstrap

步驟一:點擊左上角的那個啓動按鈕,會彈出以下這樣的一個dialog: ubuntu

步驟二:點擊對話框中的「是(Y)」按鈕,就會啓動一個線程來下在dat文件,並彈出以下的一個對話框: 網絡

接着咱們看一下aMule的代碼,來了解一下這樣一個過程的具體實現。從何處入手呢?我想那個URL的定義應該是個不錯的入口。搜索那個URL在何處定義。在amule-2.3.1/src/Preferences.cpp中有以下的幾行: app

s_MiscList.push_back( new Cfg_Bool( wxT("/eMule/DropSlowSources"), s_DropSlowSources, false ) );

s_MiscList.push_back( new Cfg_Str(  wxT("/eMule/KadNodesUrl"),s_KadURL, wxT("http://download.tuxfamily.org/technosalad/utils/nodes.dat") ) );
s_MiscList.push_back( new Cfg_Str(  wxT("/eMule/Ed2kServersUrl"),s_Ed2kURL, wxT("http://gruk.org/server.met.gz") ) );

不難理解,那個URL是某個preference item的默認值,而那個item的key正如上面的代碼所示的那樣,爲"/eMule/KadNodesUrl",那個item的值保存在靜態變量s_KadURL中。 dom

搜索"/eMule/KadNodesUrl",搜不到任何引用的地方。 tcp

再來搜s_KadURL。能夠看到在amule-2.3.1/src/Preferences.h文件中有以下的幾行: 函數

// server.met and nodes.dat urls
	static const wxString& GetKadNodesUrl() { return s_KadURL; }
	static void SetKadNodesUrl(const wxString& url) { s_KadURL = url; }
setter/getter函數。那就接着搜引用了這兩個函數的地方。引用了GetKadNodesUrl()的,在amule-2.3.1/src/ServerWnd.cpp的CServerWnd::CServerWnd(wxWindow* pParent /*=NULL*/, int splitter_pos)構造函數中:

CastChild( ID_SRV_SPLITTER, wxSplitterWindow )->SetSashGravity(0.5f);
	CastChild( IDC_NODESLISTURL, wxTextCtrl )->SetValue(thePrefs::GetKadNodesUrl());
	CastChild( IDC_SERVERLISTURL, wxTextCtrl )->SetValue(thePrefs::GetEd2kServersUrl());
而後是SetKadNodesUrl(),其中一個引用到該函數的地方爲amule-2.3.1/src/KadDlg.cpp:
void CKadDlg::OnBnClickedUpdateNodeList(wxCommandEvent& WXUNUSED(evt))
{
	if ( wxMessageBox( wxString(_("Are you sure you want to download a new nodes.dat file?\n")) +
						_("Doing so will remove your current nodes and restart Kademlia connection.")
					, _("Continue?"), wxICON_EXCLAMATION | wxYES_NO, this) == wxYES ) {
		wxString strURL = ((wxTextCtrl*)FindWindowById( IDC_NODESLISTURL ))->GetValue();

		thePrefs::SetKadNodesUrl(strURL);
		theApp->UpdateNotesDat(strURL);
	}
}

看看wxMessageBox中的那段文字,是多麼的親切啊。而這個OnBnClickedUpdateNodeList()是經過一個表,而被註冊爲事件的處理函數的: 工具

BEGIN_EVENT_TABLE(CKadDlg, wxPanel)
	EVT_TEXT(ID_NODE_IP1, CKadDlg::OnFieldsChange)
	EVT_TEXT(ID_NODE_IP2, CKadDlg::OnFieldsChange)
	EVT_TEXT(ID_NODE_IP3, CKadDlg::OnFieldsChange)	
	EVT_TEXT(ID_NODE_IP4, CKadDlg::OnFieldsChange)
	EVT_TEXT(ID_NODE_PORT, CKadDlg::OnFieldsChange)

	EVT_TEXT_ENTER(IDC_NODESLISTURL ,CKadDlg::OnBnClickedUpdateNodeList)
	
	EVT_BUTTON(ID_NODECONNECT, CKadDlg::OnBnClickedBootstrapClient)
	EVT_BUTTON(ID_KNOWNNODECONNECT, CKadDlg::OnBnClickedBootstrapKnown)
	EVT_BUTTON(ID_KADDISCONNECT, CKadDlg::OnBnClickedDisconnectKad)
	EVT_BUTTON(ID_UPDATEKADLIST, CKadDlg::OnBnClickedUpdateNodeList)
END_EVENT_TABLE()

總結一下,aMule中Kademlia網絡啓動的UI操做。在APP啓動的時候,初始化全部的UI組件。在amule-2.3.1/src/ServerWnd.cpp中CServerWnd的構造過程當中,建立了一個wxTextCtrl組件,其ID爲IDC_NODESLISTURL,其值被設置爲KadNodesUrl。在步驟一點擊啓動按鈕時,會觸發該按鈕的事件,並執行CKadDlg::OnBnClickedUpdateNodeList()函數,在這個方法中,會彈出一個如咱們步驟一執行以後所看到的那個wxMessageBox。咱們確認後,CKadDlg::OnBnClickedUpdateNodeList()函數經過ID查找到UI組建,並獲取到其值,也就是KadNodesUrl,並從這個URL下載dat文件。

KadNodesDat文件的下在過程又是怎樣的呢?能夠看到在CKadDlg::OnBnClickedUpdateNodeList()函數中有執行到theApp->UpdateNotesDat(strURL),這個函數在amule-2.3.1/src/amule.cpp中定義:

void CamuleApp::UpdateNotesDat(const wxString& url)
{
	wxString strTempFilename(theApp->ConfigDir + wxT("nodes.dat.download"));
		
	CHTTPDownloadThread *downloader = new CHTTPDownloadThread(url, strTempFilename, theApp->ConfigDir + wxT("nodes.dat"), HTTP_NodesDat, true, false);
	downloader->Create();
	downloader->Run();
}
先構造一個臨時文件名,而後經過CHTTPDownloadThread,起一個線程,下載或更新 KadNodesDat文件 (amule-2.3.1/src/HTTPDownload.cpp):
CHTTPDownloadThread::CHTTPDownloadThread(const wxString& url, const wxString& filename, const wxString& oldfilename, HTTP_Download_File file_id,
										bool showDialog, bool checkDownloadNewer)
#ifdef AMULE_DAEMON
	: CMuleThread(wxTHREAD_DETACHED),
#else
	: CMuleThread(showDialog ? wxTHREAD_JOINABLE : wxTHREAD_DETACHED),
#endif
	  m_url(url),
	  m_tempfile(filename),
	  m_result(-1),
	  m_file_id(file_id),
	  m_companion(NULL)
{
	if (showDialog) {
#ifndef AMULE_DAEMON
		CHTTPDownloadDialog* dialog = new CHTTPDownloadDialog(this);
		dialog->Show(true);
		m_companion = dialog;
#endif
	}
	// Get the date on which the original file was last modified
	// Only if it's the same URL we used for the last download and if the file exists.
	if (checkDownloadNewer && thePrefs::GetLastHTTPDownloadURL(file_id) == url) {
		wxFileName origFile = wxFileName(oldfilename);
		if (origFile.FileExists()) {
			AddDebugLogLineN(logHTTP, CFormat(wxT("URL %s matches and file %s exists, only download if newer")) % url % oldfilename);
			m_lastmodified = origFile.GetModificationTime();
		}
	}
	wxMutexLocker lock(s_allThreadsMutex);
	s_allThreads.insert(this);
}




CMuleThread::ExitCode CHTTPDownloadThread::Entry()
{
	if (TestDestroy()) { 
		return NULL;
	}
	
	wxHTTP* url_handler = NULL;
	
	AddDebugLogLineN(logHTTP, wxT("HTTP download thread started"));
	
	const CProxyData* proxy_data = thePrefs::GetProxyData();
	bool use_proxy = proxy_data != NULL && proxy_data->m_proxyEnable;
讓人不得不感慨,aMule項目真是與wxWidgets綁定的太緊了。在aMule的整個代碼中,對於wxWdigets API的調用真的是無處不在。

KadNodesDat下載以後,aMule對它又是如何處理的呢?這個文件在整個的Kademlia網絡的構建過程當中究竟又起到一個什麼樣的做用呢?

再瞅一眼amule-2.3.1/src/HTTPDownload.cpp文件,能夠看到有一個OnExit() 函數:

void CHTTPDownloadThread::OnExit() 
{
#ifndef AMULE_DAEMON
	if (m_companion) {
		CMuleInternalEvent termEvent(wxEVT_HTTP_SHUTDOWN);
		wxPostEvent(m_companion, termEvent);	
	}
#endif
	
	// Notice the app that the file finished download
	CMuleInternalEvent evt(wxEVT_CORE_FINISHED_HTTP_DOWNLOAD);
	evt.SetInt((int)m_file_id);
	evt.SetExtraLong((long)m_result);
	wxPostEvent(wxTheApp, evt);
	wxMutexLocker lock(s_allThreadsMutex);
	s_allThreads.erase(this);
}
不難判斷,這個函數在文件下載完成以後調用。它產生一個類型爲wxEVT_CORE_FINISHED_HTTP_DOWNLOAD的事件並post出去。而該事件最終將會被傳遞給CamuleApp::OnFinishedHTTPDownload(CMuleInternalEvent& event)(文件amule-2.3.1/src/amule-gui.cpp中):
// HTTPDownload finished
	EVT_MULE_INTERNAL(wxEVT_CORE_FINISHED_HTTP_DOWNLOAD, -1, CamuleGuiApp::OnFinishedHTTPDownload)
而此處引用的CamuleGuiApp::OnFinishedHTTPDownload()函數則在文件amule-2.3.1/src/amule.cpp中定義:

void CamuleApp::OnFinishedHTTPDownload(CMuleInternalEvent& event)
{
	switch (event.GetInt()) {
		case HTTP_IPFilter:
			ipfilter->DownloadFinished(event.GetExtraLong());
			break;
		case HTTP_ServerMet:
			serverlist->DownloadFinished(event.GetExtraLong());
			break;
		case HTTP_ServerMetAuto:
			serverlist->AutoDownloadFinished(event.GetExtraLong());
			break;
		case HTTP_VersionCheck:
			CheckNewVersion(event.GetExtraLong());
			break;
		case HTTP_NodesDat:
			if (event.GetExtraLong() == HTTP_Success) {
				
				wxString file = ConfigDir + wxT("nodes.dat");
				if (wxFileExists(file)) {
					wxRemoveFile(file);
				}

				if ( Kademlia::CKademlia::IsRunning() ) {
					Kademlia::CKademlia::Stop();
				}

				wxRenameFile(file + wxT(".download"),file);
				
				Kademlia::CKademlia::Start();
				theApp->ShowConnectionState();
				
			} else if (event.GetExtraLong() == HTTP_Skipped) {
				AddLogLineN(CFormat(_("Skipped download of %s, because requested file is not newer.")) % wxT("nodes.dat"));
			} else {
				AddLogLineC(_("Failed to download the nodes list."));
			}
			break;
#ifdef ENABLE_IP2COUNTRY
		case HTTP_GeoIP:
			theApp->amuledlg->IP2CountryDownloadFinished(event.GetExtraLong());
			// If we updated, the dialog is already up. Redraw it to show the flags.
			theApp->amuledlg->Refresh();
			break;
#endif
	}
}

回憶一下上面CamuleApp::UpdateNotesDat()中建立CHTTPDownloadThread對象時,傳遞的file_id HTTP_NodesDat。

對於咱們的Kademlia網絡啓動而言,自是主要關注上面CamuleGuiApp::OnFinishedHTTPDownload()函數的case HTTP_NodesDat了。也正是在這個case中,Kademlia相關的一些設施被建立出來。能夠看到,在這個case block中,所作的事情主要爲:

1. 若是HTTP下載沒有成功,則打印出log,並退出。

2. 對於HTTP下載成功的狀況。

(1)、先檢查是否有舊的"nodes.dat"文件,若是有,則移除該文件。

(2)、檢查Kademlia是否正在運行,若是是,則停掉Kademlia。

(3)、將下載到的文件重命名爲"nodes.dat"。

(4)、調用Kademlia::CKademlia::Start(),啓動Kademlia。

(5)、調用theApp->ShowConnectionState(),顯示鏈接狀態。

Kademlia命名空間中的都是和Kademlia網絡有關的東西。總算是找到了Kademlia模塊的入口了。在Kademlia::CKademlia::Start()中初始化整個的Kademlia相關的東西,其實現以下:

在文件amule-2.3.1/src/kademlia/kademlia/Kademlia.h中:

static void Start() 		{ Start(new CPrefs); }
	static void Start(CPrefs *prefs);

在文件amule-2.3.1/src/kademlia/kademlia/Kademlia.cpp中:

void CKademlia::Start(CPrefs *prefs)
{
	if (instance) {
		// If we already have an instance, something is wrong.
		delete prefs;
		wxASSERT(instance->m_running);
		wxASSERT(instance->m_prefs);
		return;
	}

	// Make sure a prefs was passed in..
	if (!prefs) {
		return;
	}

	AddDebugLogLineN(logKadMain, wxT("Starting Kademlia"));

	// Init jump start timer.
	m_nextSearchJumpStart = time(NULL);
	// Force a FindNodeComplete within the first 3 minutes.
	m_nextSelfLookup = time(NULL) + MIN2S(3);
	// Init status timer.
	m_statusUpdate = time(NULL);
	// Init big timer for Zones
	m_bigTimer = time(NULL);
	// First Firewall check is done on connect, init next check.
	m_nextFirewallCheck = time(NULL) + (HR2S(1));
	// Find a buddy after the first 5mins of starting the client.
	// We wait just in case it takes a bit for the client to determine firewall status..
	m_nextFindBuddy = time(NULL) + (MIN2S(5));
	// Init contact consolidate timer;
	m_consolidate = time(NULL) + (MIN2S(45));
	// Look up our extern port
	m_externPortLookup = time(NULL);
	// Init bootstrap time.
	m_bootstrap = 0;
	// Init our random seed.
	srand((uint32_t)time(NULL));
	// Create our Kad objects.
	instance = new CKademlia();
	instance->m_prefs = prefs;
	instance->m_indexed = new CIndexed();
	instance->m_routingZone = new CRoutingZone();
	instance->m_udpListener = new CKademliaUDPListener();
	// Mark Kad as running state.
	m_running = true;
}

在這個函數中,主要作的事就是,1. 初始化了一堆時間;2. 建立了幾個對象:CKademlia、CIndexed、CRoutingZone和CKademliaUDPListener。CKademlia是整個Kademlia網絡的主控類。Kademlia網絡的全部功能,都經過這個class暴露給外部,外部也只經過這個類來訪問Kademlia網絡。這讓人想起了外觀模式(Facade Pattern)。

先來看一下CRoutingZone的建立過程(amule-2.3.1/src/kademlia/routing/RoutingZone.cpp):

CRoutingZone::CRoutingZone()
{
	// Can only create routing zone after prefs
	// Set our KadID for creating the contact tree
	me = CKademlia::GetPrefs()->GetKadID();
	AddLogLineNS(wxT("CRoutingZone KadID: ") + me.ToBinaryString(false));
	// Set the preference file name.
	m_filename = theApp->ConfigDir + wxT("nodes.dat");
	Init(NULL, 0, CUInt128((uint32_t)0));
}

void CRoutingZone::Init(CRoutingZone *super_zone, int level, const CUInt128& zone_index)
{
	// Init all Zone vars
	// Set this zone's parent
	m_superZone = super_zone;
	// Set this zone's level
	m_level = level;
	// Set this zone's CUInt128 index
	m_zoneIndex = zone_index;
	// Mark this zone as having no leafs.
	m_subZones[0] = NULL;
	m_subZones[1] = NULL;
	// Create a new contact bin as this is a leaf.
	m_bin = new CRoutingBin();

	// Set timer so that zones closer to the root are processed earlier.
	m_nextSmallTimer = time(NULL) + m_zoneIndex.Get32BitChunk(3);

	// Start this zone.
	StartTimer();

	// If we are initializing the root node, read in our saved contact list.
	if ((m_superZone == NULL) && (m_filename.Length() > 0)) {
		ReadFile();
	}
}



void CRoutingZone::ReadFile(const wxString& specialNodesdat)
{
	if (m_superZone != NULL || (m_filename.IsEmpty() && specialNodesdat.IsEmpty())) {
		wxFAIL;
		return;
	}

	bool doHaveVerifiedContacts = false;
	// Read in the saved contact list
	try {
		uint32_t numContacts = 0;
		uint32_t validContacts = 0;
		CFile file;
		if (CPath::FileExists(specialNodesdat.IsEmpty() ? m_filename : specialNodesdat) && file.Open(m_filename, CFile::read)) {
			// Get how many contacts in the saved list.
			// NOTE: Older clients put the number of contacts here...
			//       Newer clients always have 0 here to prevent older clients from reading it.
			numContacts = file.ReadUInt32();
			uint32_t fileVersion = 0;
			if (numContacts == 0) {
				if (file.GetLength() >= 8) {
					fileVersion = file.ReadUInt32();
					if (fileVersion == 3) {
						uint32_t bootstrapEdition = file.ReadUInt32();
						if (bootstrapEdition == 1) {
							// this is a special bootstrap-only nodes.dat, handle it in a separate reading function
							ReadBootstrapNodesDat(file);
							file.Close();
							return;
						}
					}
					if (fileVersion >= 1 && fileVersion <= 3) {
						numContacts = file.ReadUInt32();
					}
				}
			} else {
				// Don't read version 0 nodes.dat files, because they can't tell the kad version of the contacts stored.
				AddLogLineC(_("Failed to read nodes.dat file - too old. This version (0) is not supported anymore."));
				numContacts = 0;
			}
			DEBUG_ONLY( unsigned kad1Count = 0; )
			if (numContacts != 0 && numContacts * 25 <= (file.GetLength() - file.GetPosition())) {
				for (uint32_t i = 0; i < numContacts; i++) {
					CUInt128 id = file.ReadUInt128();
					uint32_t ip = file.ReadUInt32();
					uint16_t udpPort = file.ReadUInt16();
					uint16_t tcpPort = file.ReadUInt16();
					uint8_t contactVersion = 0;
					contactVersion = file.ReadUInt8();
					CKadUDPKey kadUDPKey;
					bool verified = false;
					if (fileVersion >= 2) {
						kadUDPKey.ReadFromFile(file);
						verified = file.ReadUInt8() != 0;
						if (verified) {
							doHaveVerifiedContacts = true;
						}
					}
					// IP appears valid
					if (contactVersion > 1) {
						if(IsGoodIPPort(wxUINT32_SWAP_ALWAYS(ip),udpPort)) {
							if (!theApp->ipfilter->IsFiltered(wxUINT32_SWAP_ALWAYS(ip)) &&
							    !(udpPort == 53 && contactVersion <= 5 /*No DNS Port without encryption*/)) {
								// This was not a dead contact, inc counter if add was successful
								if (AddUnfiltered(id, ip, udpPort, tcpPort, contactVersion, kadUDPKey, verified, false, false)) {
									validContacts++;
								}
							}
						}
					} else {
						DEBUG_ONLY( kad1Count++; )
					}
				}
			}
			file.Close();
			AddLogLineN(CFormat(wxPLURAL("Read %u Kad contact", "Read %u Kad contacts", validContacts)) % validContacts);
#ifdef __DEBUG__
			if (kad1Count > 0) {
				AddDebugLogLineN(logKadRouting, CFormat(wxT("Ignored %u kad1 %s in nodes.dat file.")) % kad1Count % (kad1Count > 1 ? wxT("contacts"): wxT("contact")));
			}
#endif
			if (!doHaveVerifiedContacts) {
				AddDebugLogLineN(logKadRouting, wxT("No verified contacts found in nodes.dat - might be an old file version. Setting all contacts verified for this time to speed up Kad bootstrapping."));
				SetAllContactsVerified();
			}
		}
		if (validContacts == 0) {
			AddLogLineC(_("No contacts found, please bootstrap, or download a nodes.dat file."));
		}
	} catch (const CSafeIOException& DEBUG_ONLY(e)) {
		AddDebugLogLineN(logKadRouting, wxT("IO error in CRoutingZone::readFile: ") + e.what());
	}
}




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

爲何要從CRoutingZone對象的建立開始看起呢?主要是由於,在這個class中處理了從網絡下載的"nodes.dat"文件。CRoutingZone建立的主要過程爲,建立了一個CRoutingBin對象,初始化了一些咱們如今看仍是不明覺厲的變量,並解析了從網絡下載的"nodes.dat"文件

此處咱們能夠看一下"nodes.dat"文件的文件結構(ReadFile()中)。這個文件是一個純二進制文件。這個版本的代碼處理了多個版本的文件,不一樣版本的文件也就有着不一樣的文件結構。

先是文件頭的結構,主要分爲以下的幾種:

1. Version 0的文件:文件開頭是一個32位的無符號整型值,表示文件中保存的聯繫人的個數。這個版本的代碼徹底拋棄Version 0的文件不做處理。

2. Version 1, Version 2:文件開頭是一個值爲0的32位無符號整型值。緊隨其後的是一個32位的無符號整型值,表示文件的版本號。再後面是一個32位長的無符號整型值,表示聯繫人的個數。

3. Version 3:文件開頭是一個值爲0的32位無符號整型值。緊隨其後的是一個32位的無符號整型值,表示文件的版本號。再後面的是bootstrap信息,先是bootstrap的版本號

若是bootstrap版本號爲1,則緊隨其後的是Bootstrap節點信息,一直到達文件尾。

若是bootstrap版本號不爲1,則在bootstrap版本號後面跟着的是聯繫人的個數。

4. 其餘版本的文件,不能處理。

而後是聯繫人的結構。Version 1,Version 2,Version 3 bootstrap版本不爲1時,在聯繫人的個數後面都會有一連串的聯繫人信息。聯繫人的信息保存有兩種結構:

1. Version 1:128位也就是16字節的KadID->32位的IP地址->16位的UDP端口號->16位的TCP端口號->8位的聯繫人版本號。

2. Version 二、Version 3128位也就是16字節的KadID->32位的IP地址->16位的UDP端口號->16位的TCP端口號->8位的聯繫人版本號->64位8字節的KadUDPKey,內含4個字節的key和4個字節的IP->8位的Verified。

OK,從文件中解析出了一個個Contact的信息,那以後要怎麼處理呢?在CRoutingZone::ReadFile()中能夠看到,它首先會對Contact作一個過濾。若是一個contact不是dead的,則會爲相應的Contact建立一個CContact對象,並保存起來。

相關文章
相關標籤/搜索