1.1. 시작에 앞서서 ¶
늦게 이 글을 올린 것에 대해 미안하다.
이 문서는 내가 아르바이트를 하면서 번역과 작성한 문서이다.
지금의 스펙에서는 라우팅 부분의 맴과 캐쉬와 쉐어에서 많은 부분 수정이 가해졌다.
대부분의 P2P 프로그램들이 누텔라의 방법을 기본으로 작성 되었으며 지금의 유동적
IP를 갖는 환경에서는 이를 뛰어 넘는 방법은 없을 것으로 보여진다.
후에 IPv6가 나온다면 각각의 시스템이 아닌 어플리케이션에 IP를 붙일 수 있게 되어진다고
읽은 적이 있다. 이 때 되면 보다 획기적인 P2P 프로그램이 나올 것으로 보인다.
이 문서는 내가 아르바이트를 하면서 번역과 작성한 문서이다.
지금의 스펙에서는 라우팅 부분의 맴과 캐쉬와 쉐어에서 많은 부분 수정이 가해졌다.
대부분의 P2P 프로그램들이 누텔라의 방법을 기본으로 작성 되었으며 지금의 유동적
IP를 갖는 환경에서는 이를 뛰어 넘는 방법은 없을 것으로 보여진다.
후에 IPv6가 나온다면 각각의 시스템이 아닌 어플리케이션에 IP를 붙일 수 있게 되어진다고
읽은 적이 있다. 이 때 되면 보다 획기적인 P2P 프로그램이 나올 것으로 보인다.
아래의 글은 가볍게 읽어 주시길....
1.2. 주인장이 생각하는 가장 이상적인 P2P란 ¶
그누텔라는 확실히 매력적이고 또한 이상적인 순수 P2P라는 생각이 든다. 하지만 P2P란 많은 부분
희생(?)이 필요하다. 여기서 희생이란 불필요하지만 쓸 수 밖이 없는 대역폭을 말한다.
내가 찾고자 하는 파일이 누가 가지고 있는 지 알고 있다면 굳이 P2P는 필요하지 않을 것이다.
희생(?)이 필요하다. 여기서 희생이란 불필요하지만 쓸 수 밖이 없는 대역폭을 말한다.
내가 찾고자 하는 파일이 누가 가지고 있는 지 알고 있다면 굳이 P2P는 필요하지 않을 것이다.
하지만 이러한 희생을 줄이는 것이 보다 좋을 것이라는 생각이 든다. 그런 점에서 내가 생각하는 가장
이상적인 P2P는 e-Donkey라고 생각 되어진다. 물론 지금의 e-Donkey는 아니다. 내가 생각하는 부분으로
고쳐져야 겠지. 하지만 지금의 e-Donkey처럼 개인이 서버를 가질 수 있고 또한 이 서버를 가지고 찾는
다면 불필요한 대역폭은 줄어들 것이고 분산된 서버를 하나의 순수 P2P로 묶고 서버에서 클라이언트의
노드들을 수정해 준다면 가장 이상적이지 않을까? 라고 한 때 무신히 고민했다. 잡소리 이제 그만!!!!
이상적인 P2P는 e-Donkey라고 생각 되어진다. 물론 지금의 e-Donkey는 아니다. 내가 생각하는 부분으로
고쳐져야 겠지. 하지만 지금의 e-Donkey처럼 개인이 서버를 가질 수 있고 또한 이 서버를 가지고 찾는
다면 불필요한 대역폭은 줄어들 것이고 분산된 서버를 하나의 순수 P2P로 묶고 서버에서 클라이언트의
노드들을 수정해 준다면 가장 이상적이지 않을까? 라고 한 때 무신히 고민했다. 잡소리 이제 그만!!!!
1.3. The Gnutella Protocol Document ¶
The Gnutella Protocol Document
1. Protocol Specification
1.1 Gnutella란 Peer to Peer 의 분산 모델로서 Mesh구조의 네트워크망을 가지며
Query전송을 통해 네트워크내 파일의 검색과 다운로드를 가능하게 한다.
1.2 프로토콜 정의5개의 Descriptor를 사용하고 TCP/IP 프로토콜과 ASCII Code를 기반으로
이용하여 통신이 이루어진다.
- 네트워크망 초기 진입 -
- Note
Note 1 : 모든 데이터 필드는 특별한 말이 없으면 리틀 엔디언
Note 2 : IP 주소는 IPv4 형식으로 쓰인다.
Note 3 : Protocol은 Header와 Payload로 구성
- 헤더
Descriptor ID (16 byte): 네트워크 상의 고유 식별자
Payload Descriptor (2 byte): 명령어의 식별자
TTL (1 byte): 네트워크의 Servent를 지날 때마다 감소하는 수 /
Payload Length (4 byte): Header 다음에 따라오는 Descriptor 의 길이
이용하여 통신이 이루어진다.
- 네트워크망 초기 진입 -
~cpp Connection String : GNUTELLA CONNECTION/<version>/nn
~cpp Response Connection : GNUTELLA OKnn
- Note
Note 1 : 모든 데이터 필드는 특별한 말이 없으면 리틀 엔디언
Note 2 : IP 주소는 IPv4 형식으로 쓰인다.
Note 3 : Protocol은 Header와 Payload로 구성
- 헤더
Descriptor ID (16 byte): 네트워크 상의 고유 식별자
Payload Descriptor (2 byte): 명령어의 식별자
TTL (1 byte): 네트워크의 Servent를 지날 때마다 감소하는 수 /
네트워크 부하를 줄이기 위해 쓰임
Hops (1 byte): 지나간 Servent수 Payload Length (4 byte): Header 다음에 따라오는 Descriptor 의 길이
Data가 스트림이기 때문에 공백이나 Pad Byte가 따라오지않는다.
Gnutella 프로토콜의 경우 synch를 맞추는 특별한 byte가 존재하지 않아
형식에 틀린 Descriptor는 폐기된다.0 15 16 17 18 19 22
- 디스크립터 (Descriptor)
표1)
색인 | 설명 |
ping | 네트워크상의 호스트를 찾을 때 쓰인다. Payload가 없기 때문에 header의 Payload_Length = 0x00000000 로 설정된다. |
pong | Ping을 받으면 주소와 기타 정보를 포함해 응답한다.Port / IP_Address / Num Of Files Shared / Num of KB Shared** IP_Address - Big endian |
query | 네트워크상에서 검색에 쓰이고 검색 Minimum Speed ( 응답의 최소 속도 ), Search Criteria 검색 조건 |
queryHit | 검색 Query 조건과 일치한 경우 QueryHit로 응답한다. Num Of Hits 조건에 일치하는 Query의 결과 수 Port / IP_Address (Big-endian) / Speed / Result Set File Index ( 파일 번호 ) File Size ( 파일 크기 )File Name ( 파일 이 / 더블 널로 끝남 ) Servent Identifier 응답하는 Servent의 고유 식별자 Push 에 쓰인다. |
push | 방화벽이 설치된 Servent와의 통신을 위한 DescriptorServent Identifier / File Index / IP_Address(Big-endian)/Port |
1.3 명령어 라우팅
- Pong 은 Ping이 왔던 같은 길을 따라 전송된다. 만약 DescriptorID가 n
인 Pong을 받았는 데 Descriptor ID가 n인 Ping 보지 못했다면 Ping을 보
낸 Servent는 네트워크 상에서 제거된 것을 의미한다.
- QueryHit와 Query -
- Push/QueryHit -
- 전달 되었던 Ping과 Query를 제외하고 모든 Ping과 Query는 연결 된
모든 Servent에 전송된다.
- TTL/Hop은 다른 Servent로 전송되기 전에 변경되며 후에 TTL이
0이면 다른 Servent로 전송되지 않는다.
- 전에 전송된 Descriptor는 다시 전송되지 않는다. 네트워크 부하 줄임
1.4 파일 다운로드
QueryHit 명령어를 받으면 파일을 초기화 하고 다운로드를 시작한다.
이는 Gnutella 네트워크를 이용하는 것이 아니라 기존의 HTTP 프로토콜
을 이용하여 직접 파일이 전송된다.
GET/get/<File Index>/<File Name>/HTTP/1.0rn
Connection:Keep-Alivern
Range:bytes=0-rn
User-Agent:Gnutellarn3
rn
Range가 파일의 이어받기가 가능하게 함.
<File Index>는 파일 번호이고 이는 QueryHit Result에 포함된 내용이다.
이와같은 HTTP 헤더를 받으면 서버는 다음과 같은 헤더를 보내준다.
HTTP 200 OKrn
Server:Gnutellarn
Content-type:application/binaryrn
Content-length:435678rn
rn
바로 다음에 데이터가 Content-length만큼 따라오게 된다.
1.5 방화벽이 설치된 Servents
방화벽으로 인해 직접 연결이 불가능한 경우 Push Descriptor를 보내고 라우팅을 통해 받은
QueryHit에 대한 응답으로 새로운 TCP/IP 연결이 생긴
다.
새롭게 연결 된 후
GIV<File Index>:<Severnt Identifier>/<File Name>nn 를 보내 파일
을 요청하면
GET/get/<File Index>/<File Name>/HTTP1.0rn
Connection:Keep-Alivern
Range:bytes=0-rn
User-Agent:Gnutellarn3
rn
과 같은 HTTP GET request형식으로 응답을 하며 파일을 전송한다.
그러나 Push Descriptor조차 보내지 못하면 파일전송은 불가능하다.
1.6 확장된 Gnutella 프로토콜
기존의 Gnutella가 다른 프로그램(BearShare) 에 의해 서비스 되면서
확장된 프로토콜 이 필요하게 되었다. 이를 Triler라고 하며 QueryHit 의
ResultSet 마지막 더블널 과 Servent ID사이에 들어간다.
- Trailer
VendorCode(3byte) OpenDataSize(1byte) OpenData(2byte) PrivateData(n)
2.1 Data Structure
3. Gnucleus Technical DescriptionFile Name : Packet.Cpp
Common IP / ExIP / Node / NodeEx
GnutellaPacket packet_Header / packet_Ping / packet_Pong
2.2 Class Hierarchal DiagramCommon IP / ExIP / Node / NodeEx
GnutellaPacket packet_Header / packet_Ping / packet_Pong
Gnuclues는 Gnutella 프로젝트 중 OpenSoure로 실제 인터페이스 부분이 열악하다.
하지만 Gnucleus의 Core 코드 부분의 Docunment가 가장 잘 나와있고 실제로
Compile / Execute 가능한 Code를 손에 얻을 수 있는 프로그램이다. 물론
지금 상태는 버전의 호환성으로 인해 Gnucleus node에 실제 노드에 접속하는 것이
어렵지만 같은 버전 사이의 접속은 가능하므로 인터페이스 부분을 수정한다면
보다 좋은 프로그램으로 변형할 수 있을 것 같다.
또한 Entica에서 필요로하는 Search / MultiThreadDownloader를 지원하며
또한 가장 기본적인 기능을 구현하여 불필요한 소스코드가 적다는 장점도 있다.
이런 이유로 다른 몇몇 Gnutella 프로그램도 Gnucleus를 기반으로 작성 되어졌다.
4. Note하지만 Gnucleus의 Core 코드 부분의 Docunment가 가장 잘 나와있고 실제로
Compile / Execute 가능한 Code를 손에 얻을 수 있는 프로그램이다. 물론
지금 상태는 버전의 호환성으로 인해 Gnucleus node에 실제 노드에 접속하는 것이
어렵지만 같은 버전 사이의 접속은 가능하므로 인터페이스 부분을 수정한다면
보다 좋은 프로그램으로 변형할 수 있을 것 같다.
또한 Entica에서 필요로하는 Search / MultiThreadDownloader를 지원하며
또한 가장 기본적인 기능을 구현하여 불필요한 소스코드가 적다는 장점도 있다.
이런 이유로 다른 몇몇 Gnutella 프로그램도 Gnucleus를 기반으로 작성 되어졌다.
servent : server 와 client 의 합성어
little endian byte : 작은 쪽 (바이트 열에서 가장 작은 값)이 먼저 저장되는 순서
descriptor : 패킷이 갖는 의미
payload : 패킷 갖는 데이타
5. 참고 URL
http://www.gnucleus.com/ (Gnucleus 프로그램)
http://www.sourceforge.net/ (Gnutella Clone 프로그램)
1.4. 기타 코드에 관한 설명 ¶
Gnucleus에서 다운로드 받는 방법에 대한 설명
파일을 선택하고 그 그룹의 결과값을 RelayDownload 함수의 전달인자로 보낸다.
그러면 View 에 Download에 관한 페이지를 업데이트하고 DownloadShell의 생성자가 실행이 된다.
~cpp void CSearchResults::OnButtonDownload() { std::list<ResultGroup>::iterator itGroup; POSITION pos = m_lstResults.GetFirstSelectedItemPosition(); while(pos) { int nItem = m_lstResults.GetNextSelectedItem(pos); ResultGroup* pGroup = (ResultGroup*) m_lstResults.GetItem(nItem); if(pGroup) RelayDownload(*pGroup); } }에서 다운로드 버튼을 누르면 결과 그룹중에서 pos를 기억하고 이 pos를 이용해 다운받을
파일을 선택하고 그 그룹의 결과값을 RelayDownload 함수의 전달인자로 보낸다.
~cpp void CSearchResults::RelayDownload(ResultGroup &Item) 에서는 CGnuDownloadShell* Download = new CGnuDownloadShell(m_pComm);에서와 같이 새로운 다운로드 쉘을 만들고
~cpp m_pComm->m_DownloadList.push_back(Download);와 같이 m_DownloadList에 Download 객체를 삽입하고 CGnuControl에서 제어하게 만든다.
~cpp Download->m_Name = Item.Name; Download->m_Search = ReSearch; Download->m_FileLength = Item.Size; Download->m_AvgSpeed = Item.AvgSpeed;같이 초기화를 한다.
~cpp // Add hosts to the download list for(int i = 0; i < Item.ResultList.size(); i++) Download->AddHost( Item.ResultList[i] );처럼 DownloadShell에 AddHost함수를 통해 Item의 모든 Host를 다운로드 에 넣는다.
그러면 View 에 Download에 관한 페이지를 업데이트하고 DownloadShell의 생성자가 실행이 된다.
CGnuControl에서의 void CGnuControl::ManageDownloads()에 의해 제어된다.
void CGnuDownloadShell::Start() 로 다운로드가 시작이 되고 실제적인 다운로드는 CGnuDownload에서 이루어지면 쉘에서는 Timer()에서 CheckCompletion()로 완료 되었는 지 확인을 하게 되고 AttachChunk2Chunk 와 AttachChunk2Partial로 부분부분 완료된 Chunk들을 결합해 주는 역활을 하게 된다.
실제적으로 하나의 Host마다 CGnuDownload 클래스를 갖게 되며 데이타를 받는 소켓이 된다m_StartPos가 받기 시작하는 Chunk의 시작을 나타내며 ReadyFile()에서는 전의 받던 파일이 있는 지 조사후에 File을 연다.
StartDownload() 함수에서 다운로드를 시작하며 GetStartPos() 를 통해 Chunk가 다운로드가 시작할 m_pChunk->m_StartPos의 위치를 알게 한다.
SendRequest() 에서 HTTP/GET 형식으로 헤더를 보내 받고자 하는 데이타를 요청한다
OnReceive(int nErrorCode) 에서 Content-length 만큼의 버퍼 데이타를 받아 청크와 연결 시킨다.
이어 받기를 할때에는 파일의 끝에서 -4096만큼 얻고 m_Verification 블럭의 4096크기 만큼 비교를 한 후에 이어받기를 시작한다.
새로운 청크를 만드는 조건은 이미 완료된 청크의 남은 부분이 EmptySize > 16384 보다 커야
한다는 것이다.
소스는 다음과 같고
생각하고 m_pChunk = NULL로 만든다
void CGnuDownloadShell::Start() 로 다운로드가 시작이 되고 실제적인 다운로드는 CGnuDownload에서 이루어지면 쉘에서는 Timer()에서 CheckCompletion()로 완료 되었는 지 확인을 하게 되고 AttachChunk2Chunk 와 AttachChunk2Partial로 부분부분 완료된 Chunk들을 결합해 주는 역활을 하게 된다.
실제적으로 하나의 Host마다 CGnuDownload 클래스를 갖게 되며 데이타를 받는 소켓이 된다m_StartPos가 받기 시작하는 Chunk의 시작을 나타내며 ReadyFile()에서는 전의 받던 파일이 있는 지 조사후에 File을 연다.
StartDownload() 함수에서 다운로드를 시작하며 GetStartPos() 를 통해 Chunk가 다운로드가 시작할 m_pChunk->m_StartPos의 위치를 알게 한다.
~cpp m_StartPos = m_pShell->m_BytesCompleted + ((m_pShell->m_FileLength - m_pShell->m_BytesCompleted) / 2); m_pChunk->StartPos = m_StartPos; m_pChunk->FileLength = m_pShell->m_FileLength - m_StartPos;공식으로 Chunk의 시작위치를 구한다.
SendRequest() 에서 HTTP/GET 형식으로 헤더를 보내 받고자 하는 데이타를 요청한다
OnReceive(int nErrorCode) 에서 Content-length 만큼의 버퍼 데이타를 받아 청크와 연결 시킨다.
이어 받기를 할때에는 파일의 끝에서 -4096만큼 얻고 m_Verification 블럭의 4096크기 만큼 비교를 한 후에 이어받기를 시작한다.
새로운 청크를 만드는 조건은 이미 완료된 청크의 남은 부분이 EmptySize > 16384 보다 커야
한다는 것이다.
소스는 다음과 같고
~cpp // Reset m_pChunk because this can be called multiple times if(m_pChunk) { bool destroy = true; for(int i = 0; i < m_pShell->m_ChunkList.size(); i++) if(m_pShell->m_ChunkList[i] == m_pChunk) destroy = false; if(destroy) delete m_pChunk; m_pChunk = NULL; }부분은 소켓의 청크중에 지금의 청크가 없다면 지금 받은 청크는 끝마친 것으로
생각하고 m_pChunk = NULL로 만든다
Gnucleus에서 프로토콜 통신
GnuCreateGuid(&Guid) // DescriptorID 생성
라우팅시 연결된 모든 nodeList에서 key->Origin를 찾아내어 key->Origin를 제외한 모든 node에 받은 pong 또는 queryHit를 전달
- Search
라우팅시 연결된 모든 nodeList에서 key->Origin를 찾아내어 key->Origin를 제외한 모든 node에 받은 pong 또는 queryHit를 전달
- Search
~cpp int length = 25 + m_Search.GetLength() + 1; pNode->SendPacket(m_Packet, length, PACKET_QUERY, true);- Result Set
~cpp // Extract results from the packet while(HitsLeft > 0 && NextPos < Length - 16) { Result Item; memcpy(&TempX, &Packet[NextPos], 4); Item.FileIndex = makeD( flipX(TempX)); memcpy(&TempX, &Packet[NextPos + 4], 4); Item.Size = makeD( flipX(TempX)); Item.Host = QueryHit->Host; Item.Port = QueryHit->Port; Item.Speed = makeD( flipX(QueryHit->Speed)); Item.Firewall = Firewall; Item.Busy = Busy; Item.Stable = Stable; Item.ActualSpeed = ActualSpeed; if(ExtendedPacket) Item.Vendor = Vendor; Item.Origin = Log->Origin; memcpy(&Item.PushID, &Packet[Length - 16], 16); Item.Distance = Log->Header->Hops; // Get Filename for(i = NextPos + 8; Packet[i] != 0; i++) if(i < Length - 16) Item.Name += (char) Packet[i]; else break; // Pass any data between double null while(Packet[++i] != 0) if(i > Length - 16) break; Item.NameLower = Item.Name; Item.NameLower.MakeLower(); Item.Icon = m_pDoc->GetIconIndex(Item.NameLower); m_WholeList.push_back(Item); // Screen Item to user's preferences if(Inspect(Item)) { m_CurrentList.push_back(Item); m_tabResults->UpdateList( AddtoGroup(Item) ); } // Check for end of reply packet if(i + 1>= Length - 16) HitsLeft = 0; else { HitsLeft--; NextPos = i + 1; } }패킷의 packet_QueryHit 에서 ResultSet을 추출하여 Item Vector에 넣어준다.
~cpp void CGnuNode::Recieve_Ping(packet_Ping* Ping, int nLength) key_Value* key = m_pComm->m_TableRouting.FindValue(&Ping->Header.Guid);통해 받았던 핑인지 검사하고 if(key == NULL) 받았던 핑이 아니라 새로운 핑이라면 m_pComm->m_TableRouting.Insert(&Ping->Header.Guid, this) 처럼 라우팅 테이블에 넣고 Pong을 보내준다.
방화벽이 설치된 경우의 자세한 설명
그리고 PUSH를 Route PUSH를 통해 보내 그누텔라 default 포트가 아닌 열 수 있는 포트로 직접연결을 한다.
만약 상대방 서버가 FireWall에 있다면 QueryHit중의 bool Firewall;필드가 True가 되므로 이에 대한 응답으로
~cpp UINT AttemptPort = pPrefs->m_ForcedPort ? pPrefs->m_ForcedPort : pPrefs->m_LocalPort; AttemptPort += rand() % 99 + 0;Firewall에 있을 경우 이런 방법으로 포트를 열지 못하면 랜덤한 포트를 부여 ForcedPort로 접속 Attempts < 3 만큼 시도를 한다.
그리고 PUSH를 Route PUSH를 통해 보내 그누텔라 default 포트가 아닌 열 수 있는 포트로 직접연결을 한다.
만약 상대방 서버가 FireWall에 있다면 QueryHit중의 bool Firewall;필드가 True가 되므로 이에 대한 응답으로
~cpp Item.Host = QueryHit->Host; Item.Port = QueryHit->Port;
에 해당하는 Port로 Download를 한다.