Peercast简介、分析及常见问题处理 (一)

 一。简介 
    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)&copy); 

相信你会找到自己的解决方案的

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);

}

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Peercast注释版是我在做本科毕业设计时分析Peercast的一些成果,当时阅读代码的时候做了注释,然后写了一些分析文章。本来想发到自己的博客上保留一份记录的,不想后来竟成了很多人参考Peercast的资料。这点上我是有些惭愧的,因为很多不成熟的想法和不正确的观点可能会误导大家,也希望大家能够分辨吧。本来想形成一份比较完整的注释版,但后来由于毕业以及转变研究方向等因素已经没有继续做下去了。不过核心的代码我想应该还是比较清晰的,大家可以参考一下。我把Peercast注释版发到CSDN的资源上,大家可以访问http://download.csdn.net/hicsdn/bbisonic进行下载,以后我就不回复索要代码的留言和邮件了。也是很抱歉,有半年的时间没更新过此BLOG了,对于有些我没注意到留言或邮件而漏发代码的朋友们,在此说声SORRY了。<br><br>大家在看代码前最好先看看Peercast源码分析文章,里面的<源码编译方法>可以告诉你如何编译Peercast源代码。<看源代码前必读>可以告诉你项目的组织结构以及各源码目录各自完成什么功能,可以让你对整体有个大概的了解。<Peercast整体架构分析>让你能大致了解一下Peercast的架构及设计思想。<阅读Peercast源码的一些经验>是我阅读Peercast源码的一些体会,希望对你能有所帮助。<Peercast服务器端代码执行流程>解释服务器启动的一些关键代码,<Peercast播放模块分析>解释Peercast如何调用播放器实现媒体播放的。以上是一些比较重要的文章,其他文章可以有选择性地看看。其他的话就看源码注释吧。<br><br>这份源码注释有很多不足之处,是我学习过程中的一个成果,大家请见谅。如果它能给你阅读 Peercast的过程中带来一些帮助的话,我将感到非常荣幸。<br>特此声明。<br><br>王浩聪<br>2007.12.11<br>

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值