一。簡介
PeerCast.org成立於2002年四月,它是一個非盈利性的站點,提供免費的P2P電臺軟件。這個項目的目標是創建一個容易使用、簡單的和可靠的軟件客戶端,從而使任何人都可以廣播流媒體,而不必耗費昂貴的服務器或者帶寬。
PeerCast爲廣播者提供相當可觀的節省,因爲他們不必提供帶寬給所有的接聽者。一個單獨的56K調制解調器即可用來爲整個網絡廣播一個電臺。
PeerCast是一個健壯的網絡,因爲不存在中心服務器,每個用戶可以是客戶,也可是服務器,也可以是流的廣播者。它爲廣播者提供了匿名服務,因爲要追蹤到源文件流並不是件簡單的事。甚至可能的是,可以讓一個不同國家的客戶收聽廣播,而這個客戶可作爲整個網絡的源。
PeerCast是一個把音頻/視頻服務器和客戶端集合在一起的軟件。你可以通過PeerCast來收聽衆多的網絡電臺,也可以自己廣播。PeerCast頭等的特性是你不需要一個有龐大帶寬的服務器來爲衆多聽衆提供廣播服務,你所需要的只是PeerCast和一個外部的廣播工具。
Peercast和其他P2P文件共享軟件的工作方式大部分相同,除了一點,用戶下載的不是文件而是流。然後這些流實時地與其他用戶進行交換。對於任何連接到網絡的機器來說,沒有任何數據會被存儲到本地機上。
公司可以讓一個PeerCast客戶端爲整個本地網提供音頻流。或者你也可以和你的朋友們在因特網上建立一個私人網絡來收聽音樂。是否直接連接到PeerCast網絡,這取決於你自己的選擇。
二。源代碼文件結構分析
Peercast分爲兩個目錄:
core目錄主要完成核心的操作
ui目錄實現界面
其中ui下面又可分爲html目錄和win32目錄
html目錄主要是一些網頁,用來完成Peercast的相關配置,其中en爲英文版本。
以下就en下的一些文件做一些介紹:
win32目錄下的文件:
Simple.cpp 負責窗口界面的生成及一些相關啓動管理
gui.cpp 完成peercast主窗口(GUI)的一些操作
core目錄:
common目錄:主要完成通用的核心代碼(主要代碼均在此目錄下)
unix目錄:與UNIX相關的一些類
win32目錄:與win32相關的一些類
Wsys.cpp:繼承自Sys,提供基本的win32功能例如開啓線程
Wsocket.cpp:ClientSocket的WINDOWS版本.處理實際的讀寫TCP操作
下面着重介紹一下common目錄,這是整個peercast的核心
Common.h 定義了一些異常處理類和GnuID、GnuIdList和Host類
Channel.cpp: 頻道流類。它們進行客戶之間的實際媒體流傳輸
Gnutella.cpp: GnuPacket是一個Guntella協議,GnuStream是一個流,用來讀取/寫入GunPackets
Html.cpp: Html協議的一些處理
Http.cpp: Http協議的一些處理
Icy.cpp:
IniFile.cpp 配置信息的讀取和修改
Jis\mms\mp3\nsv\ogg:處理不同的流格式,包括讀取HEADER信息等
Pcp.cpp:Peercast的網絡協議
Peercast.cpp: 整個Peercast流程的管理,包括啓動頻道管理器、服務管理器,關閉,登錄日誌等,設置服務器密碼等
RTSP.cpp:只是個實驗,並沒有實際的東西
Servent.cpp:是客戶端之間真正的連接。他們處理handShaking,數據轉輸和GnuPackets的處理。每個servent在連接時有一個socket分配給它,它用這個socket來傳輸所有的數據
Servhs.cpp:處理一些HANDSHAKE相關的東西
ServMgr.cpp: 處理服務器信息的讀取和修改。一個管理類,用來處理多樣的severnt連接
Socket.cpp : ClientSocket是一個通用的socket接口,與OS/HW無關
Stat.cpp: 相關數據的統計
Stream.cpp: 一些流文件的讀取和寫入處理
Sys.cpp:一個對所有系統的基礎類,例如開啓線程,創建socket
url.cpp: 關於URL的一些處理操作
XML.cpp:進行電臺信息的一些XML存取操作
三。如何用Peercast發佈和收聽電臺
如何實時廣播MP3:
所需軟件:Peercast + Winamp + ShoutcastDSP
1.安裝Peercast(下載地址http://www.peercast.org/download.php)
2.安裝Winamp
3.安裝ShoutCastDSP
4.打開Peercast
5.配置ShoutcastDSP。在Winamp中點擊選項->參數,在參數面版左邊選擇插件中的DSP音效,可以在右邊看見Nullsoft SHOUTcast Source DSP,雙擊彈出SHOUTcast窗口。在ouput中的Address填上localhost,Port填7144(與Peercast相同),Password一欄填上在Peercast中設置的密碼(初始爲空)。在Yellowpages選項中可自己設置自己電臺的相關信息。
6.使用Winamp播放你想播放的Mp3文件,在SHOUTcast中點擊Connect
7.若連接成功,Peercast在Relay窗口中顯示你廣播的電臺
8.yp.peercast.org會自動收錄你的電臺地址。
如何收聽:
1.安裝Peercast
2.點擊yp.peercast.org
3.選擇你想收聽的電臺,或者也可以在右上角通過關鍵字進行搜索
4.點擊play,peercast會自動調用默認的播放器進行播放
(相關討論)
Q:不知版主有沒有研究過peercast的root模式?我真的不知道root模式有什麼用.因爲我不用root模式,而用normal,它同樣可以發揮yellow page的作用,只要把另一個peercast上的yp的IP設成這個root下的peercast IP,這個root下的peercast channel信息上,就有另一個peercast廣播頻道源上的信息.所以,root不root,並不重要.只要看YP是誰,就成.YP的作用,還是很明顯,就是提供發現所以以此爲YP的peercast上的頻道源的集成,若是有一個CGI程序對這個集成xml作一個解析,得到channel ID後,就可以做成一個像yp.peercast.org那樣的網站了...
不知版主對上有何高見?歡迎討論...我對源碼研究不多,正在看C++語言呢,不久就要深入源碼分析了,請多指教!
A:按Peercast開發者的解釋,作爲YP的網站確實是要設在root模式下的。
對於如何做一個yp.peercast.org的網站,你的說法是完全正確的。
至於只作爲normal模式能否接收到廣播頻道源的信息,我這兩天做測試看看。
關於源碼的分析,你可以看看我博客上發表的源碼分析文章,希望對你有所幫助並就我的錯誤和改進之處多提意見。
希望我們合作愉快!
Q:請問在PeerCast中的一個問題我最近在研究PeerCast,
我這裏遇到一個問題,我沒有弄明白!
即當我點擊一個網頁裏面的"Play"按鈕,爲什麼PeerCast會收到一個WM_COPYDATA這個消息
這個消息是誰發送的!我自已如何才能做到這一點呢????
A:你可以看看Peercast對收到WM_COPYDATA的處理,也就是播放相應的頻道,換句話說就是實現Play功能
case WM_COPYDATA:
{
COPYDATASTRUCT *pc = (COPYDATASTRUCT *)lParam;
LOG_DEBUG("URL request: %s",pc->lpData);
if (pc->dwData == WM_PLAYCHANNEL)
{
ChanInfo info;
servMgr->procConnectArgs((char *)pc->lpData,info);
chanMgr->findAndPlayChannel(info,false);
}
//sys->callLocalURL((const char *)pc->lpData,servMgr->serverHost.port);
}
break;
再參考Peercast如何發送WM_COPYDATA消息
if (chanURL)
{
COPYDATASTRUCT copy;
copy.dwData = WM_PLAYCHANNEL;
copy.cbData = strlen(chanURL)+1; // plus null term
copy.lpData = chanURL;
SendMessage(oldWin,WM_COPYDATA,NULL,(LPARAM)©);
}
相信你會找到自己的解決方案的
Q:再請教一個問題,peercast的啓動參數有-kill, -multi...但-url是什麼意義,我看了一下源碼,不能確定,版主有沒有知道啊?我覺得好像是用-urlmyform://這種形式,用myform://來取代peercast://的,不知對不對?
A:-url方式是這樣使用的,假設你知道JOKV-FM(TEST)這個電臺的URL地址爲:
peercast://pls/6618AD6E10CD60AF27307AFFF0401345?tip=220.157.200.33:7144peercast -url
那麼使用以下命令peercast -url peercast://pls/6618AD6E10CD60AF27307AFFF0401345?tip=220.157.200.33:7144
也可以直接訪問到這個電臺
四。局域網中的Peercast網絡架設
前面介紹的Peercast發佈和收聽是基於公網的,如果你是在一個小型局域網內,想在局域網內架設一個廣播電臺網絡,那麼該如何設置呢?
1.選擇一臺機器作爲服務器端,安裝Peercast並將其設置工作在root模式下(在setting中修改)。編寫一個動態網頁讀取XML信息,顯示現有的網絡電臺,設置其網頁訪問地址例如yp.cuc.edu.cn。
2.其他機器均作爲客戶端,安裝Peercast,默認設置於normal模式下,並將其中的YP address項改爲服務器端的IP地址。
3.按上文介紹的方式發佈網絡電臺
4.收聽時訪問服務器網頁地址,點擊Play即可播放
(yp.peercast.org就是這樣的一個例子
在另外一臺機器上輸入http://ip/admin?cmd=viewxml可以讀取XML,這意味着你可以通過發送HTTP的GET請求來獲取XML信息)
五。Peercast中關於Gnutella包源碼實現分析
一個Gnutella客戶機通過與另一個當前在網絡中的客戶機建立連接來使自己與網絡相連。
一旦網絡上的另一個客戶機的地址被獲取,一個與該客戶機的TCP/IP連接將被創建,以下的Gnutella連接請求字符串(ASCII編碼)將被髮送:
GNUTELLA CONNECT/<protocol version string>
Peercast定義:static const char *GNU_CONNECT = "GNUTELLA CONNECT/0.6";
一個客戶機願意接受連接的話必須迴應
GNUTELLA OK
Peercast定義:static const char *GNU_OK = "GNUTELLA/0.6 200 OK";
一旦一個客戶機成功連接到網絡上,他與其它客戶機通訊通過發送和接收Gnutella協議描述字。每一個描述符前都有一個以下字節結構的描述頭,如下所示:
Descriptor Header
DescriptorID網絡描述符:16個字節的字符串唯一標示網絡的描述符號。
Payload Descriptor負載描述符:
0x00 = Ping
0x01 = Pong
0x40 = Push
0x80 = Query
0x81 = QueryHit
Peercast定義:
static const int GNU_FUNC_PING = 0;
static const int GNU_FUNC_PONG = 1;
static const int GNU_FUNC_QUERY = 128;
static const int GNU_FUNC_HIT = 129;
static const int GNU_FUNC_PUSH = 64;
TTL生存期:描述字符在刪除前在Gnutella網絡中向前傳遞的次數。每個客戶端在將包向前傳遞前將TTL減一。當TTL等於0,描述符將不再被向前傳遞。
Hops描述符被向前傳遞的次數:作爲一個描述符向前傳遞,頭部的TTL和Hops字必須滿足以下條件:
TTL(0) = TTL(i) + Hops(i)
Payload Length負載長度:表示緊接着頭部後面的描述符部分的長度。下一個描述符頭後的從頭部算起的Payload Length字節數,也就是沒有間隔或保留字在Gnutella的數據流中。
TTL是網絡中唯一的描述過期的機制。客戶機應該仔細檢查收到的描述符的TTL區並必要時減少它的值。濫用TTL區將會導致沒有必要的網絡阻塞和差勁的網絡性能。
Payload Length區是客戶機查找輸入流中下一個描述符的唯一可靠方式。Gnutella協議不提供一個“監視”字符串或任何其它的描述符同步的方式。因此,客戶機應該嚴格保證每一個收到的描述符的Payload Length區的有效性(至少爲固定長度的描述符)。如果一個客戶機不能和輸入的流同步,它應該斷掉與這個輸入流有關的來自發送方的客戶機,不管是產生這個流還是向前傳遞這個流的無效的客戶機。緊接着描述頭的是一個有效裝載包含以下之一的描述符:
Peercast定義一個GnuPacket類來保存包的信息。
class GnuPacket
{
public:
unsigned char func; //描述符類型,包括Ping\Pong\Query\Hit\Push
unsigned char ttl; //生存週期
unsigned char hops; //記錄描述符被傳送的次數
unsigned int len; //數據長度
GnuID id; //描述符ID:由16字節組成,用於唯一標識一個網絡描述符
char data[MAX_DATA]; //實際數據
};
Ping (0x00)
Ping描述符沒有相關的有效裝載和數據長度爲0。一個Ping只是簡單地有一個描述頭表述,它的有效裝載區是0x00和裝載長度區爲0x00000000。
一個客戶機用Ping描述符
Peercast實現:void GnuPacket::initPing(int t)
只需簡單設置ping描述頭表述和TTL值(初始化爲t)及hop值(初始化爲0),並生成校驗ID
Pong (0x01)
Port
Port:同意接收響應的客戶機的端口
IP Address:響應的客戶機的地址(此數據區高位字節在後)
Number of Files Shared:本機共享文件的數量
Number of Kilobytes Shared:本機所有共享文件的空間大小,以K爲單位
Peercast實現:void GnuPacket::initPong(Host &h, bool ownPong, GnuPacket &ping)
data.writeShort(h.port); // 寫入響應的客戶機端口
data.writeLong(SWAP4(h.ip)); // 響應的客戶機的地址
data.writeLong(chanMgr->numChannels()); //本機頻道的數量
data.writeLong(servMgr->totalOutput(false)); // 本機總輸出數據量大小,以K爲單位
Query (0x80)
字節偏移0 1 2 …
Minimum Speed :最小響應速度,響應的客戶機的速度必須在此速度之上( 以K/秒爲單位)
Search criteria:查詢關鍵字,一個零結尾的字符串。這個字符串的最大長度由描述頭的Payload Length負載長度規定。
Peercast實現:void GnuPacket::initFind(const char *str, XML *xml, int maxTTL)
mem.writeShort(0); // 最小響應速度爲0
mem.write((void *)str,slen+1); // 寫入要搜索的字符串
QueryHit (0x81)
Number of Hits:符合搜索條件的結果數
Port:能接受連接的客戶機的端口
IP Address :響應客戶機的地址(此數據區高位字節在後)
Speed :響應客戶機的連線速度(以K/秒爲單位)
Result Set :響應查詢的結果集。其中包含一個Number_of_Hits的部分,其中每個都包含以下結構
File Index:一個數字,由響應的客戶機指定,用來唯一標示響應的文件結果
File Size:與File index相符的文件的大小
File Name:已雙零結尾的與File index相符的文件的名字
Result Set的長度由描述頭的Payload Length負載長度規定。
Servent Identifier:一個16位的字符串用來唯一標示網絡上的客戶機。功能上用來標示客戶機的網絡地址。用在Push指令上。
QueryHit指令只有在收到一個Query指令後響應才發出。一個客戶機只有在它嚴格符合查詢關鍵字時纔對一個Query指令進行響應。
Peercast定義:bool GnuPacket::initHit(Host &h, Channel *ch, GnuPacket *query, bool push, bool busy, bool stable, bool tracker, int maxttl)
mem.writeChar(1); // 能接受連接的客戶機的端口
mem.writeShort(h.port); // 能接受連接的客戶機的端口
mem.writeLong(SWAP4(h.ip)); // 響應客戶機的地址(此數據區高位字節在後)
mem.writeLong(0); // index
mem.writeShort(ch->getBitrate()); // 響應客戶機的連線速度(以K/秒爲單位)
mem.writeShort(ch->localListeners()); // 聽衆數目
Push (0x40)
Servent Identifier:一個16位的字符串用來唯一標示網絡上的客戶機,該客戶機請求下載帶有File_Index的文件。
File Index:下載目標客戶機的文件的唯一標識,初始化的客戶機應該根據返回的QueryHit指令的File_Index中的標識設置。
IP Address:下載帶有File_Index的文件的客戶機的地址(此數據區高位字節在後)
Port:下載帶有File_Index的文件的客戶機的端口
Peercast定義:void GnuPacket::initPush(ChanHit &ch, Host &sh)
data.write(ch.packetID.id,16); //一個16位的字符串用來唯一標示網絡上的客戶機,該客戶機請求下載帶有Channel_Index的文件。
data.writeLong(ch.index); //下載目標客戶機的頻道的唯一標識
data.writeLong(SWAP4(sh.ip)); // 下載帶有Channel_Index的頻道的客戶機的地址(此數據區高位字節在後)
data.writeShort(sh.port); // 下載帶有Channel_Index的頻道的客戶機的端口
六。Peercast的PUSH實現方式
防火牆後的客戶機
並非總是在初始化一個文件下載後都可以與Gnutella客戶機建立直接連接。客戶機可能在防火牆後並不允許通過它的Gnutella端口進入的連接。如果一個直接連接不能建立,客戶機若想下載文件可能會請求共享文件的客戶機採用“推送”方式來代替。一個客戶機可以通過發送一個Push文件推送請求到發送QueryHit請求的客戶機處來實現。作爲Push請求目標的客戶機(在客戶機標誌區標示一個Push的描述符)應該接收Push描述符,嘗試建立一個新的TCP/IP連接到請求客戶機(在Push描述符中標示有IP地址和端口)。如果直接連接不能建立,那麼可能發起Push請求的客戶機自己也在防火牆後。這種情況,文件傳輸將不能進行。
Peercast實現:if (hit.firewalled) strcat(flstr,"Push,");
如果一個直接連接可以從防火牆後的客戶機建立到發起Push請求的客戶機,防火牆後的客戶機應該立刻發送以下的:
GIV :/\n\n 這裏的:和是Push請求頭中的的文件索引和客戶機標示,是本地文件表中文件索引爲的文件。客戶機收到GIV請求頭(Push請求者)應該從頭中取出和並構造一個如下的HTTP GET請求: GET /get/// HTTP/1.0\r\n Connection: Keep-Alive\r\n Range: bytes=0-\r\n User-Agent: Gnutella\r\n3 \r\n 餘下的下載過程和上面所述的“文件下載”內容一致。 可允許的用戶-代理字符串由HTTP標準定義。客戶機開發者不能對這裏使用的值做自己的假定。其中的值“Gnutella”只是用來演示舉例而已。
Peercast實現:s->initGIV(h,c->info.id);
七。如何用peercast實現轉播
如果你想轉播一個在網上已經存在的音頻或視頻流,那麼這個操作非常簡單。
首先在peercast菜單中選擇Advanced->Broadcast
在彈出的頁面中填上相關的信息,最重要的URL項填上已經存在的音視頻流地址,例如http://ccd.zjonline.com.cn/mp3/agtbw.mp3,再填上其他的相關信息
點擊Create Relay,創建成功後你可以在GUI窗口中看到你正在轉播的頻道
八。Stream.h源文件分析
Stream.h包括四個類,分別是Stream、MemoryStream、FileStream、IndirectStream。其中MemoryStream、FileStream、IndirectStream均繼承自Stream類。
流涉及三個基本操作:
可以讀取流。讀取是從流到數據結構(如字節數組)的數據傳輸。
可以寫入流。寫入是從數據結構到流的數據傳輸。
流可以支持查找。查找是對流內的當前位置進行查詢和修改。查找功能取決於流具有的後備存儲區類型。例如,網絡流沒有當前位置的統一概念,因此一般不支持查找。
Stream 是所有流的抽象基類。流是字節序列的抽象概念,例如文件、輸入/輸出設備、內部進程通信管道或者 TCP/IP 套接字。Stream 類及其派生類提供這些不同類型的輸入和輸出的一般視圖,使程序員不必瞭解操作系統和基礎設備的具體細節。
對實施者的說明: 在實現 Stream 的派生類時,必須提供 Read 和 Write 方法的實現。
MemoryStream 類創建這樣的流,該流以內存而不是磁盤或網絡連接作爲支持存儲區。MemoryStream 封裝以字符數組形式存儲的數據,該數組在創建 MemoryStream 對象時被初始化,或者該數組可創建爲空數組。可在內存中直接訪問這些封裝的數據。內存流可降低應用程序中對臨時緩衝區和臨時文件的需要。
使用 FileStream 類對文件系統上的文件進行讀取、寫入、打開和關閉操作
Peercast對於流的封裝與.net framework對於Stream的封裝類似,可參見http://msdn.microsoft.com/library/chs/default.asp?url=/l ...
frlrfSystemIOStreamClassTopic.asp
九。common.h源文件分析
GeneralException類:其中StreamException繼承自GeneralException,而SockException、EOFException、CryptException、TimeoutException均從StreamException繼承
GnuID、GnuIDList、Host類
GnuID是唯一標識GnuPacket的ID號。由16位組成
主要方法有
void generate(unsigned char = 0); //通過隨機數生成Brocast ID號
void encode(class Host *, const char *,const char *,unsigned char); //通過IP地址和其他數據對ID進行重新編碼
GnuIDList維護一個GnuID的鏈表
Host類用於對主機IP地址、端口號的處理
十。IniFile.h源文件分析
peercast.ini的格式如下
[Server]
serverPort = 7144
autoServe = Yes
forceIP =
isRoot = No
maxBitrateOut = 0
maxRelays = 2
maxDirect = 0
IniFile類定義三個字符串變量,currLine,nameStr,valueStr
currentLine對應INI文件中的一行,例如serverPort = 7144
nameStr對應相應的變量名,例如serverPort
valueStr對應相應的變量值,例如7144
寫入INI文件時根據寫入變量值的不同提供幾種寫入方法:
writeSection(const char *name)寫入段,例如writeSection( "Server" )則寫入[Server]
writeIntValue(const char *name, int iv)寫入整型變量,writeIntValue( serverPort, 7144 ),則寫入
serverPort = 7144,其他類似方法還有writeStrValue,writeBoolValue,writeLine等
讀取INI文件時,readNext()每次讀取INI文件中的一行到currLine中,並把相應的變量名和變量值讀取到nameStr和valueStr中
getName()返回變量名,根據變量值類型的不同相應有getIntValue,getStrValue,getBoolValue
這裏用loadSettings的部分代碼解釋一下讀取配置文件的過程
void ServMgr::loadSettings(const char *fn)
{
IniFile iniFile;
if (!iniFile.openReadOnly(fn))
saveSettings(fn);
if (iniFile.openReadOnly(fn))
{
while (iniFile.readNext())
{
// server settings
if (iniFile.isName("serverPort"))
servMgr->serverHost.port = iniFile.getIntValue();
else if (iniFile.isName("autoServe"))
servMgr->autoServe = iniFile.getBoolValue();
else if (iniFile.isName("autoConnect"))
servMgr->autoConnect = iniFile.getBoolValue();
else if (iniFile.isName("icyPassword")) // depreciated
strcpy(servMgr->password,iniFile.getStrValue());
else if (iniFile.isName("forceIP"))
servMgr->forceIP = iniFile.getStrValue();
也就是用readNext()逐行讀取並用IF ELSE語句判斷是否是要讀的變量,直至讀到文件末尾爲止
十一。Peercast接收到GnuPacket的處理過程
對於包是丟棄、廣播或者是繼續路由是通過設置R_TYPE類型來判斷的。
這是在GnuStream類中定義的,其中GnuStream完成收包、發包、處理包等操作
enum R_TYPE
{
R_PROCESS,
R_DEAD,
R_DISCARD, //丟棄
R_ACCEPTED, //接受
R_BROADCAST, //廣播
R_ROUTE, //路由
R_DUPLICATE, /複製
R_BADVERSION,
R_DROP
};
通過GnuStream::R_TYPE processPacket(GnuPacket &, Servent *, GnuID &)這個函數來執行處理進程,下面我們就這個函數進行具體的分析:
接收到包時,首先把TTL值遞減,HOP值遞增
in.ttl--;
in.hops++;
通過讀取包中的func值來判斷命令的類型(Ping\Pong\Query\QueryHit\Push),再執行相應處理
switch(in.func)
{
case GNU_FUNC_PING:
case GNU_FUNC_PONG:
收到Ping消息應返回一個Pong消息,並將消息設置爲廣播方式
GnuPacket pong;
pong.initPong(sh,true,in);
serv->outputPacket(pong,true)) //發出消息
ret = R_BROADCAST;
收到Pong消息應進行判斷,若是對本機發出的Ping消息的回覆,則表明與遠端主機建立連接;若否,則路由返回
if (servMgr->isReplyID(in.id))
{
servMgr->addHost(h,ServHost::T_SERVENT,sys->getTime()); //建立連接
ret = R_ACCEPTED;
}else
ret = R_ROUTE;
收到Query消息,首先應把消息廣播出去.然後若本機存在Query要尋找的頻道,則返回一個QueryHit消息
注意Gnutella中的文件在Peercast中相對應的是一個頻道,因此文件名相對應的是頻道的ID
ret = R_BROADCAST;
numHits = chanMgr->findChannels(info,hits,16); //info是一個頻道信息結構,此時保存着Query消息中尋找的頻道信息
for(int i=0; i<numHits; i++)
{
GnuPacket hit;
if (hit.initHit(sh,hits,&in,push,busy,stable,tracker,in.hops))
serv->outputPacket(hit,true);
}
收到QueryHit消息,則表示頻道已找到,加入收聽.若位於防火牆後面,則加上PUSH消息
if (hit.firewalled) strcat(flstr,"Push,");
readHit(data,hit,in.hops,in.id)
其中readHit()中加入收聽的執行語句爲
if (info.id.isSet())
{
if (!chanMgr->findHitList(info))
chanMgr->addHitList(info);
}
收到Push消息,則新分配servent類,按GIV方式進行傳送
Servent *s = servMgr->allocServent();
if (s)
s->initGIV(h,c->info.id);
十二。Channel.h源代碼分析
ChanInfo類:保存頻道信息
::String name;
GnuID id,bcID;
int bitrate;
TYPE contentType;
PROTOCOL srcProtocol;
unsigned int lastPlayStart,lastPlayEnd;
unsigned int numSkips;
unsigned int createdTime;
STATUS status;
TrackInfo track;
::String desc,genre,url,comment;
Channel類:管理具體的頻道操作
THREAD_PROC Channel::stream(ThreadInfo *thread)
ChannelMgr類,它完成頻道創建、管理、尋找、停止等操作
Channel *ChanMgr::createChannel(ChanInfo &info, const char *mount)
void ChanMgr::findAndPlayChannel(ChanInfo &info, bool keep)
ChannelSource負責具體的流傳輸工作,其中定義一個虛方法stream由子類PeercastSource、ICYSource、URLSource實現
virtual void stream(Channel *) = 0;
ChannelHit:維持一份與你收聽同一個頻道的節點的信息
void ChanHit::pickNearestIP(Host &h)
void ChanHit::initLocal(int numl,int numr,int,int uptm,bool connected,unsigned int oldp,unsigned int newp)
ChannelHitList:維持一份ChannelHit列表
ChanHit *ChanHitList::addHit(ChanHit &h)
十三。通過日誌文件分析Peercast頻道的創建過程
由ChanMgr創建新頻道並加入到當前的頻道列表中
[CHAN] New channel created
Channel *ChanMgr::createChannel(ChanInfo &info, const char *mount)
{
Channel *nc=NULL;
nc = new Channel();
nc->info = info;
nc->next = channel;
channel = nc;
LOG_CHANNEL("New channel created");
return nc;
}
由Channel類啓動頻道,並加入到ChangMgr的Hit列表中
[CHAN] Channel started
THREAD_PROC Channel::stream(ThreadInfo *thread)
{
// thread->lock();
Channel *ch = (Channel *)thread->data;
while (thread->active && !peercastInst->isQuitting)
{
LOG_CHANNEL("Channel started");
ChanHitList *chl = chanMgr->findHitList(ch->info);
if (!chl)
chanMgr->addHitList(ch->info);
ch->sourceData->stream(ch);
由Channel根據頻道信息,創建相應的源
[CHAN] Channel is MP3 - meta: 0
ChannelStream *Channel::createSource()
{
ChannelStream *source=NULL;
switch(info.contentType)
{
case ChanInfo::T_MP3:
LOG_CHANNEL("Channel is MP3 - meta: %d",icyMetaInterval);
source = new MP3Stream();
break;
}
return source;
}
十四。通過日誌文件分析Peercast的HANDSHAKE過程
[DBUG] ShoutCast client
void Servent::handshakeHTTP(HTTP &http, bool isHTTP)
{
LOG_DEBUG("ShoutCast client");
handshakeICY(Channel::SRC_SHOUTCAST,isHTTP);
}
void Servent::handshakeICY(Channel::SRC_TYPE type, bool isHTTP)
{
servMgr->checkFirewall();
//Channel ID用IP地址和速率來編碼以避免重複
info.id = chanMgr->broadcastID;
info.id.encode(NULL,info.name.cstr(),loginMount,info.bitrate);
LOG_DEBUG("Incoming source: %s : %s",info.name.cstr(),ChanInfo::getTypeStr(info.contentType));
c = chanMgr->createChannel(info,loginMount); //創建頻道
}
[DBUG] ICY icy-name:Sonic's radio
[DBUG] ICY icy-genre:Pop
[DBUG] ICY icy-url:http://www.sonic.com
[DBUG] ICY icy-irc:#shoutcast
[DBUG] ICY icy-icq:0
[DBUG] ICY icy-aim:N/A
[DBUG] ICY icy-pub:1
[DBUG] ICY icy-br:48
bool Servent::handshakeStream(ChanInfo &chanInfo)
{
HTTP http(*sock);
if (chanInfo.contentType != ChanInfo::T_MP3)
addMetadata=false;
if (addMetadata && (outputProtocol == ChanInfo::SP_HTTP)) // winamp mp3 metadata check
{
sock->writeLine(ICY_OK);
sock->writeLineF("%s %s",HTTP_HS_SERVER,PCX_AGENT);
sock->writeLineF("icy-name:%s",chanInfo.name.cstr());
sock->writeLineF("icy-br:%d",chanInfo.bitrate);
sock->writeLineF("icy-genre:%s",chanInfo.genre.cstr());
sock->writeLineF("icy-url:%s",chanInfo.url.cstr());
sock->writeLineF("icy-metaint:%d",chanMgr->icyMetaInterval);
sock->writeLineF("%s %s",PCX_HS_CHANNELID,idStr);
sock->writeLineF("%s %s",HTTP_HS_CONTENT,MIME_MP3);
}
}
在開始前我們需要一個有效的IP地址
[DBUG] Checking firewall..
void ServMgr::checkFirewall()
{
Servent::handshakeOutgoingPCP(atom,sock->host,remoteID,agent,true);
}
[DBUG] PCP outgoing waiting for OLEH..
[DBUG] Got new ip: 218.249.186.209:0
[DBUG] Firewall is set to ON
[DBUG] PCP Outgoing handshake complete.
void Servent::handshakeOutgoingPCP(AtomStream &atom, Host &rhost, GnuID &rid, String &agent, bool isTrusted)
{
LOG_DEBUG("PCP outgoing waiting for OLEH..");
LOG_DEBUG("Got new ip: %s",ipstr);
servMgr->serverHost.ip = thisHost.ip;
LOG_DEBUG("PCP Outgoing handshake complete.");
}
十五。用WM_COPYDATA實現進程間通信
進程間通訊的方式有很多,常用的有共享內存、命名管道和匿名管道、發送消息等幾種方法來直接完成,另外還可以通過socket口、配置文件和註冊表等來間接實現進程間數據通訊任務。以上這幾種方法各有優缺點,具體到在進程間進行大數據量數據的快速交換問題上,則可以排除使用配置文件和註冊表的方法;另外,由於管道和socket套接字的使用需要有網卡的支持,因此也可以不予考慮。這樣,可供選擇的通訊方式只剩下共享內存和發送消息兩種。由於數據量比較大,這樣在使用消息進行通訊時就無法通過消息參數將數據直接攜帶到接收方,只能以地址傳送的方式進行。當一個應用程序向另一個應用程序發送數據時將會發出WM_COPYDATA系統消息,因此可以考慮通過向消息隊列插入WM_COPYDATA消息的方法來實現數據在進程間的拷貝。
在使用WM_COPYDATA消息時,由第一個消息參數指定發送窗口的句柄,第二個消息參數則爲一同數據相關的數據結構COPYDATASTRUCT的指針,此結構原形聲明如下:
typedef struct tagCOPYDATASTRUCT {
DWORD dwData;
DWORD cbData;
PVOID lpData;
} COPYDATASTRUCT;
其中,只需將待發送數據的首地址賦予lpData、並由cbData指明數據塊長度即可。消息發出後,接收方程序在WM_COPYDATA消息的響應函數中通過隨消息傳遞進來的第二個參數完成對數據塊的接收。但是在使用WM_COPYDATA消息時,只能用SendMessage()函數發送而不能使用PostMessage(),這兩個函數雖然功能非常相似都是負責向指定的窗口發送消息,但是SendMessage()函數發出消息後不是馬上返回,而是在接收方的消息響應函數處理完之後才能返回,並能夠得到返回結果。在此期間發送方程序將被阻塞,SendMessage()後面的語句不能被繼續執行。而PostMessage()函數在發出消息後馬上返回,其後語句能夠被立即執行,但是無法獲取消息的執行結果。可見,在交換數據量較大的情況下實現數據頻繁而又快速的交換用發送WM_COPYDATA消息的方法也是不合適的,當數據傳輸過於頻繁時將有可能導致數據的丟失。
比之以上幾種進程間通訊方法,共享內存有着明顯的優勢。共享內存是通過直接操作內存映射文件來進行的,而內存映射文件又是進行單機數據共享的最低層機制,前面幾種數據交換方式在低層都是通過內存映射文件來進行的。因此使用共享內存可以以較小的開銷獲取較高的性能,是進行大數據量數據快速交換的最佳方案。
十六。Peercast的http.h源文件分析
定義HTTP消息:
static const char *HTTP_SC_OK = "HTTP/1.0 200 OK";
static const char *HTTP_SC_NOTFOUND = "HTTP/1.0 404 Not Found";
static const char *HTTP_SC_UNAVAILABLE = "HTTP/1.0 503 Service Unavailable";
static const char *HTTP_HS_SERVER = "Server:";
static const char *HTTP_HS_AGENT = "User-Agent:";
static const char *HTTP_HS_CONTENT = "Content-Type:";
static const char *HTTP_HS_HOST = "Host:";
static const char *HTTP_HS_ACCEPT = "Accept:";
static const char *HTTP_HS_LENGTH = "Content-Length:";
static const char *MIME_MP3 = "audio/mpeg";
static const char *MIME_OGG = "application/ogg";
HTTP類
class HTTP : public IndirectStream
{
public:
HTTP(Stream &s)
{
init(&s);
}
void initRequest(const char *r)
{
strcpy(cmdLine,r);
}
void readRequest();
bool isRequest(const char *);
int readResponse();
bool checkResponse(int);
bool nextHeader();
bool isHeader(const char *);
char *getArgStr();
int getArgInt();
void getAuthUserPass(char *, char *);
char cmdLine[8192],*arg;
};
十七。CGI的基本編程要素
CGI的工作流程:
服務器根據客戶端發送的請求方法(GET\POST\HEAD),將信息發送給CGI腳本。CGI腳本進行信息處理並將結果返回給服務器。服務器再對返回結果進行分析,然後發送給客戶端。
CGI腳本的解析流程:
1.判斷方法類型,根據不同類型做相應處理(GET\POST\HEAD)
2.若方法爲GET,則只需往頁面中寫入相應的HTML代碼
3.若方法爲POST,先分析POST信息,再分離NAME和VALUE。然後返回相應值。
十八。wsocket.h源文件分析
WSAClientSocket繼承自ClientSocket,完成對基本WinSock函數的封裝。
ClientSocket只是提供一個接口,具體實現由其繼承類WSAClientSocket(WINDOWS)和UClientSocket (UNIX)實現
這裏先介紹一下Host類:
unsigned int ip; //主機IP
unsigned short port; //主機端口號
unsigned int value;
下面介紹一下WSAClientSocket的具體實現
//初始化,每個Winsock應用都必須加載合適的WinSock DLL版本.加載庫是通過調用WSAStartup函數實現的
void WSAClientSocket::init()
{
WORD wVersionRequested;
WSADATA wsaData;
int err;
wVersionRequested = MAKEWORD( 2, 0 );
err = WSAStartup( wVersionRequested, &wsaData );
if ( err != 0 )
throw SockException("Unable to init sockets");
}
//建立套接字,通過調用socket函數來實現
void WSAClientSocket::open(Host &rh)
{
sockNum = socket (AF_INET, SOCK_STREAM, IPPROTO_TCP);
if (sockNum == INVALID_SOCKET)
throw SockException("Can`t open socket");
setBlocking(false);
#ifdef DISABLE_NAGLE
setNagle(false);
#endif
host = rh;
memset(&remoteAddr,0,sizeof(remoteAddr));
remoteAddr.sin_family = AF_INET;
remoteAddr.sin_port = htons(host.port);
remoteAddr.sin_addr.S_un.S_addr = htonl(host.ip);
}