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

 //服务器绑定。一旦为某种协议创建了套接字,就必须将套接字绑定到一个已知地址上。使用bind函数

void WSAClientSocket::bind(Host &h)
{
 struct sockaddr_in localAddr;

 if ((sockNum = socket (PF_INET, SOCK_STREAM, IPPROTO_TCP)) == -1)
  throw SockException("Can`t open socket");

 setBlocking(false);
 setReuse(true);

 memset(&localAddr,0,sizeof(localAddr));
 localAddr.sin_family = AF_INET;
 localAddr.sin_port = htons(h.port);
 localAddr.sin_addr.s_addr = INADDR_ANY;

 if( ::bind (sockNum, (sockaddr *)&localAddr, sizeof(localAddr)) == -1)
  throw SockException("Can`t bind socket");

//接下来要做的,是将套接字置入监听模式。bind函数的作用只是将套接字和指定的地址关联在一起。指示套接字等待连接传入的API是listen

 if (::listen(sockNum,SOMAXCONN))
  throw SockException("Can`t listen",WSAGetLastError());

 host = h;
}

 //现在我们已做好了接受客户机连接的准备,通过ACCEPT函数来完成

ClientSocket *WSAClientSocket::accept()
{

 int fromSize = sizeof(sockaddr_in);
 sockaddr_in from;

 int conSock = ::accept(sockNum,(sockaddr *)&from,&fromSize);


 if (conSock ==  INVALID_SOCKET)
  return NULL;

 
    WSAClientSocket *cs = new WSAClientSocket();
 cs->sockNum = conSock;

 cs->host.port = from.sin_port;
 cs->host.ip = from.sin_addr.S_un.S_un_b.s_b1<<24 |
      from.sin_addr.S_un.S_un_b.s_b2<<16 |
      from.sin_addr.S_un.S_un_b.s_b3<<8 |
      from.sin_addr.S_un.S_un_b.s_b4;


 cs->setBlocking(false);
#ifdef DISABLE_NAGLE
 cs->setNagle(false);
#endif

 return cs;
}

 //关闭套接字

void WSAClientSocket::close()
{
 if (sockNum)
 {
  shutdown(sockNum,SD_SEND);

  setReadTimeout(2000);
  try
  {
   //char c;
   //while (readUpto(&c,1)!=0);
   //readUpto(&c,1);
  }catch(StreamException &) {}

  if (closesocket(sockNum))
   LOG_ERROR("closesocket() error");


  sockNum=0;
 }
}

//客户端连接

 void WSAClientSocket::connect()
{
 if (::connect(sockNum,(struct sockaddr *)&remoteAddr,sizeof(remoteAddr)) == SOCKET_ERROR)
  checkTimeout(false,true);

}

//发送数据

void WSAClientSocket::write(const void *p, int l)
{
 while (l)
 {
  int r = send(sockNum, (char *)p, l, 0);
  if (r == SOCKET_ERROR)
  {
   checkTimeout(false,true); 
  }
  else if (r == 0)
  {
   throw SockException("Closed on write");
  }
  else
  if (r > 0)
  {
   stats.add(Stats::BYTESOUT,r);
   if (host.localIP())
    stats.add(Stats::LOCALBYTESOUT,r);

   updateTotals(0,r);
   l -= r;
   p = (char *)p+r;
  }
 }
}

//接收数据

int WSAClientSocket::read(void *p, int l)
{
 int bytesRead=0;
 while (l)
 {
  int r = recv(sockNum, (char *)p, l, 0);
  if (r == SOCKET_ERROR)
  {
   // non-blocking sockets always fall through to here
   checkTimeout(true,false);

  }else if (r == 0)
  {
   throw EOFException("Closed on read");

  }else
  {
   stats.add(Stats::BYTESIN,r);
   if (host.localIP())
    stats.add(Stats::LOCALBYTESIN,r);
   updateTotals(r,0);
   bytesRead += r;
   l -= r;
   p = (char *)p+r;
  }
 }
 return bytesRead;
}

十九。Peercast的命令行使用方式 

Peercast也可以从命令行启动,有如下几种参数选择

Peercast:正常方式启动

Peercast -inifile:启动,并设置配置文件参数

Peercast -kill:启动后立即关闭

Peercast -url:按照频道的URL地址启动Peercast并播放相应电台

Peercast -multi:以非互斥方式启动

具体实现如下:

int APIENTRY WinMain(HINSTANCE hInstance,
                     HINSTANCE hPrevInstance,
                     LPSTR     lpCmdLine,
                     int       nCmdShow)
{
 char tmpURL[8192];
 tmpURL[0]=0;
 char *chanURL=NULL;   //频道地址


 iniFileName.set(".\\peercast.ini");

 // off by default now
 showGUI = false;  //初始化时不打开图形窗口

 //根据命令行参数进行处理
 if (strlen(lpCmdLine) > 0)
 {
  char *p;
  if ((p = strstr(lpCmdLine,"-inifile"))!=NULL) 
   iniFileName.setFromString(p+8);

  if (strstr(lpCmdLine,"-zen")) 
   showGUI = false;

  if (strstr(lpCmdLine,"-multi")) 
   allowMulti = true;

  if (strstr(lpCmdLine,"-kill")) 
   killMe = true;

  if ((p = strstr(lpCmdLine,"-url"))!=NULL)
  {
   p+=4;
   while (*p)
   {
    if (*p=='"')
    {
     p++;
     break;
    }    
    if (*p != ' ')
     break;
    p++;
   }
   if (*p)
    strncpy(tmpURL,p,sizeof(tmpURL)-1);
  }
 }

 // get current path
 {
  exePath = iniFileName;
  char *s = exePath.cstr();
  char *end = NULL;
  while (*s)
  {
   if (*s++ == '\\')
    end = s;
  }
  if (end)
   *end = 0;
 }

 
 if (strnicmp(tmpURL,"peercast://",11)==0)
 {
  if (strnicmp(tmpURL+11,"pls/",4)==0)
   chanURL = tmpURL+11+4;
  else
   chanURL = tmpURL+11;
  showGUI = false;
 }

二十。Sys.h源代码分析 

String类:完成字符串的一些定义和操作

Random类:可调用next方法返回随机数

Sys类:提供一些系统功能,如线程操作、返回随机数、返回时

WEvent类:

WLock类:对临界区操作的封装,用于线程同步

ThreadInfo类:线程信息

二一。Peercast的日志实现

这里以输出DEBUG信息为例子。

输出日志函数为LOG_DEBUG。具体使用方法为LOG_DEBUG("Play request: %s",pc->lpData);

void LOG_DEBUG(const char *fmt,...)
{
 if (servMgr)
 {
  if ((servMgr->showLog & (1<<LogBuffer::T_DEBUG)) && (!servMgr->pauseLog))
  {
   va_list ap;
     va_start(ap, fmt);
   ADDLOG(fmt,ap,LogBuffer::T_DEBUG);
      va_end(ap); 
  }
 }
}

ADDLOG函数

void ADDLOG(const char *fmt,va_list ap,LogBuffer::TYPE type)
{
 if(sys)
 {
  const int MAX_LINELEN = 1024;

  char str[MAX_LINELEN+1];
  vsnprintf(str,MAX_LINELEN-1,fmt,ap);
  str[MAX_LINELEN-1]=0;

  if (type != LogBuffer::T_NONE)
   sys->logBuf->write(str,type);

  peercastApp->printLog(type,str);
 }
}


void APICALL MyPeercastApp ::printLog(LogBuffer::TYPE t, const char *str)
{
 ADDLOG(str,logID,true,NULL,t);
 if (logFile.isOpen())
 {
  logFile.writeLine(str);
  logFile.flush();
 }
}

二十二。用Peercast广播视频文件(WMV格式) 

1.安装Windows media encoder

2.新建会话中选择自定义会话

3.源来自选择文件,在文件名中点击浏览选择要广播的视频(WMV)

4.在输出中选择自编码器拉传递,端口号填8080

5.点击应用

6.在Peercast的Broadcast页面中URL填入http://localhost:8080,其他项根据你的选择设置,然后点击Create Relay

7.在WIndows media encoder中菜单中选择控制->开始编码

8.若广播建立成功,在Relays页面中可以看见刚刚建立的频道,点击Play即可播放

二十三。Peercast收听电台的源代码流程分析 

以收听JOKV-FM(TEST)为例,在YP上点击Play,则其URL地址为
peercast://pls/25838B9F1EAE27079B793C9FBA0E4156?tip=222.148.187.176:7144


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;

// 解析连接参数,str表示相应的频道URL,例65051E037A7A2A3433090065051E037A?tip=211.132.83.9:7144
// 从URL中解析频道的相关信息以初始化info

void ServMgr::procConnectArgs(char *str,ChanInfo &info)
{
 char arg[512];
 char curr[256];

 //使args等于?后面的字符串,即tip=211.132.83.9:7144
 char *args = strstr(str,"?");
 if (args)
  *args++=0;

 info.initNameID(str);

 if (args)
 {
  //nextCGIarg分解字符串,把"tip"保存到curr中,"211.132.83.9"保存到arg中
  while (args=nextCGIarg(args,curr,arg))
  {
   LOG_DEBUG("cmd: %s, arg: %s",curr,arg);

   if (strcmp(curr,"sip")==0)
   // sip - add network connection to client with channel
   {
    Host h;
    h.fromStrName(arg,DEFAULT_PORT);
    if (addOutgoing(h,servMgr->networkID,true))
     LOG_NETWORK("Added connection: %s",arg);

   }else if (strcmp(curr,"pip")==0)
   // pip - add private network connection to client with channel
   {
    Host h;
    h.fromStrName(arg,DEFAULT_PORT);
    if (addOutgoing(h,info.id,true))
     LOG_NETWORK("Added private connection: %s",arg);
   }else if (strcmp(curr,"ip")==0)
   // ip - add hit
   {
    Host h;
    h.fromStrName(arg,DEFAULT_PORT);
    ChanHit hit;
    hit.init();
    hit.host = h; 
    hit.rhost[0] = h;
    hit.rhost[1].init();
    hit.chanID = info.id;
    hit.recv = true;

    chanMgr->addHit(hit);
   }else if (strcmp(curr,"tip")==0)
   // tip - add tracker hit
   {
    Host h;
    h.fromStrName(arg,DEFAULT_PORT);
    chanMgr->addHit(h,info.id,true);
   }


  }
 }
}

根据info中的信息寻找和播放频道


void ChanMgr::findAndPlayChannel(ChanInfo &info, bool keep)
{
 ChanFindInfo *cfi = new ChanFindInfo;
 cfi->info = info;
 cfi->keep = keep;
 cfi->func = findAndPlayChannelProc;


 sys->startThread(cfi);
}

启动线程

THREAD_PROC findAndPlayChannelProc(ThreadInfo *th)
{
 ChanFindInfo *cfi = (ChanFindInfo *)th;

 ChanInfo info;
 info = cfi->info;


 Channel *ch = chanMgr->findChannelByNameID(info);

 chanMgr->currFindAndPlayChannel = info.id;

 if (!ch)
  ch = chanMgr->findAndRelay(info);

 if (ch)
 {
  // check that a different channel hasn`t be selected already.
  if (chanMgr->currFindAndPlayChannel.isSame(ch->info.id))
   chanMgr->playChannel(ch->info);

  if (cfi->keep)
   ch->stayConnected = cfi->keep;
 }

 delete cfi;
 return 0;
}

创建频道

// 寻找和转播相应频道
Channel *ChanMgr::findAndRelay(ChanInfo &info)
{
 char idStr[64];
 info.id.toStr(idStr);
 LOG_CHANNEL("Searching for: %s (%s)",idStr,info.name.cstr());
 peercastApp->notifyMessage(ServMgr::NT_PEERCAST,"Finding channel...");


 Channel *c = NULL;

 c = findChannelByNameID(info);

 //如果当前没有转播该频道,则新创建一个频道
 if (!c)
 {
  c = chanMgr->createChannel(info,NULL);
  if (c)
  {
   c->setStatus(Channel::S_SEARCHING);   
   c->startGet();
  }
 }

 for(int i=0; i<600; i++) // search for 1 minute.
 {

  c = findChannelByNameID(info);

  if (!c)
  {
   peercastApp->notifyMessage(ServMgr::NT_PEERCAST,"Channel not found");
   return NULL;
  }

  
  if (c->isPlaying() && (c->info.contentType!=ChanInfo::T_UNKNOWN))
   break;

  sys->sleep(100);
 }

 return c;
}

创建频道

Channel *ChanMgr::createChannel(ChanInfo &info, const char *mount)
{
 lock.on();

 Channel *nc=NULL;

 nc = new Channel();

 //将新建的频道加入频道列表
 nc->next = channel;
 channel = nc;


 nc->info = info;
 nc->info.lastPlayStart = 0;
 nc->info.lastPlayEnd = 0;
 nc->info.status = ChanInfo::S_UNKNOWN;
 if (mount)
  nc->mount.set(mount);
 nc->setStatus(Channel::S_WAIT);
 nc->type = Channel::T_ALLOCATED;
 nc->info.createdTime = sys->getTime();

 LOG_CHANNEL("New channel created");

 lock.off();
 return nc;


开始获取数据,即新创建一个Source并调用startStream进行实际传输
void Channel::startGet()
{
 srcType = SRC_PEERCAST;
 type = T_RELAY;
 info.srcProtocol = ChanInfo::SP_PCP;


 sourceData = new PeercastSource();

 startStream();
}

启动传输线程
void Channel::startStream()
{
 thread.data = this;
 thread.func = stream;
 if (!sys->startThread(&thread))
  reset();
}

进行实际的流传输,调用ChannelSource::stream函数
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);

  LOG_CHANNEL("Channel stopped");

  if (!ch->stayConnected)
  {
   break;
  }else
  {
   if (!ch->info.lastPlayEnd)
    ch->info.lastPlayEnd = sys->getTime();

   unsigned int diff = (sys->getTime()-ch->info.lastPlayEnd) + 5;

   LOG_DEBUG("Channel sleeping for %d seconds",diff);
   for(unsigned int i=0; i<diff; i++)
   {
    if (!thread->active || peercastInst->isQuitting)
     break;
    sys->sleep(1000); 
   }
  }
 }

 ch->endThread();

 return 0;

}

(评论)
进行实际的流传输,调用ChannelSource::stream函数 
这里错了,没有进行流传输,只是将流读到了channel rawData中了,还没有广播呢

转播相同频道的主机称为一个ChanHit 
Peercast刚启动连接的时候会去找8个ChanHit,然后选择一个最好的ChanHit进行传输,所以不像BT那样是多点传输。 
流传输通过HTTP协议进行,确实是按顺序接收的。

二十三。Peercast整体架构分析 

现在的P2P流媒体主要有两种架构:

1.基于树的架构。这是由流媒体的多播演化而来的,也就是播放同一频道的节点组成一棵树,提供广播的源节点为这棵树的根。每个节点可以为下层几个节点提供数据。但这种架构仍然会对上层的结点造成太大的负担,而且在节点动态加入和退出的情况下树不易维护。另外还存在传输延迟问题,所以树的高度不能太大。

2.基于图(MESH)的架构。通过邻居发现寻找相关的节点。这种架构可以实现完全非中心化。

Peercast采用的是基于图的架构。所有Peercast节点都在同一网络中,而且一个Peercast可以同时转播多个频道。由于Peercast集合了客户端和服务器功能,所以一个Peercast可以同时是广播者、转播者和收听者。

Peercast网络架构可分为三层。

第一层是YP。
YP(yp.peercast.org)从广播者中收集频道信息,是整个网络的根。

第二层是广播者。
广播者向YP发送频道信息,这样YP就能有一个完整的广播者的列表。

第三层是转播者
转播者收听频道,每个广播者维护一份转播者的列表。

P2P流媒体的运行模式
视频/音频输入+编码器+Peercast+播放器

视/音频输入:这是频道的来源,可以是实时事件(电视),也可以是文件(MP3/WMV)
编码器:用于将文件编码成更易于传输的流格式,可以是SHOUTcast DSP和windows media encoder等
播放器:播放编码后传输的文件

Peercast:根据其完成的功能可分为下列几个模块:
1.获取媒体数据:作为广播者读取编码器发送过来的流数据
2.用户界面:以GUI和网页方式提供控制
3.数据传送:在节点间传递控制信息和频道流具体数据
4.节点选择:选择要进行传输的最佳节点
5.缓冲管理:管理流缓冲以实现流媒体下载和播放
6.HTTP服务器。将流数据用HTTP方式送往播放器

节点的加入与退出:

当一个Peercast节点第一次加入Peercast网络时(点击yp.peercast.org中特定频道的PLAY按钮),例如访问地址是peercast://pls/EF49346D72FD05F234D3DA2C33FF3A9C?ip=61.213.94.129:2010 。它会先与广播这个电台的IP(61.213.94.129:2010)建立连接。由于这个广播者是不变的,所以至少会有一个特定的连接。如果广播者是满负荷的,那么这个节点可以通过这个广播者同其他转播同一电台的节点建立连接。
通常会建立8个连接。


在这点上是与Gnutella网络不同的,由于必定存在一个广播者,所以不必实现Gnutella中关于节点第一次加入网络的机制(GWebCache),而此时广播者相当于BT软件的一个原始种子。

与本节点转播同一频道的节点(也就是邻居)的信息保存在ChanHitList中,其中ChanHitList是一份ChanHit的链表,每个ChanHit保存一个相关结点的信息。Hit的意思是你想收听的频道的广播者或转播者。

拥塞控制:

当output队列超过50%时进入拥塞控制模式。系统丢弃一些incoming包,并根据包类型和跳树来给outgoing包区分优先值。当output队列降到25%时,系统会关闭拥塞控制模式。

转播时间更久的节点比刚开始转播的节点拥有更高的优先权,这体现在TTL上。刚开始转播的节点广播的包的TTL值为1,而这个TTL值每5分钟会递增1。这样转播时间超过35分钟的结点会有着最高的TTL值7。

节点间的通信:
节点间的通信通过发送和接收控制信息包来实现。Peercast专用协议PCP规定了控制信息包的类型和格式。
ServMgr负责分配、删除和使用servent对象,每个servent对象负责一个具体的连接,而其中包的发送、接收和解析工作由PCP Stream来实现。

传输者的选择:

在建立初始连接之后,节点需要选择一个最佳的节点来传输频道数据。
选择的顺序依次如下:
1.本地转播者
2.邻居转播者
3.本地广播者
4.邻居转播者

比如说如果找到邻居转播者就不用继续往下找,然后从其中选取出最佳的节点作为传输对象,其他作为备用传输者。
这个最佳可从以下三个方面来衡量:


如果这个传输者退出网络,那么必须重新按上次方法选择下一个传输者。
由于备用传输者较多,所以当节点动态退出网络时不会造成太大的影响。


缓冲机制:

流(stream):流在Peercast中是一个非常重要的概念。所谓流,就是字符串的集合。所以无论是包、视/音频数据都可以看做是一个流。

Peercast的缓冲机制是通过ChanPacketBuffer实现的,里面包含有多个ChanPacket,每个ChanPacket封装了实际的数据。

二十四。Peercast中用到的几种数据结构 

链表:Channel类、ChannelHit类均用到了链表数据结构

以Channel类为例

class Channel
{
   Channel* next;
}

ChanMgr维护一份Channel的链表,并完成添加、删除、统计等操作。这里用链表而不用数组是考虑到频道频繁的添加删除操作和空间的节省。

其中channel为头指针,添加新频道采用头插法,统计频道数目采用遍历的方法。

class ChannelMgr
{
   Channel* channel;
}

int ChanMgr::numChannels()
{
 int tot = 0;
 Channel *ch = channel;
 while (ch)
 {
  if (ch->isActive())
   tot++;
  ch = ch->next;
 }
 return tot;
}

循环队列:ChanPacketBuffer

这里考虑到的是频道数据包是先进先出的机制,即先读入的数据先播放

class ChanPacketBuffer
{
   ChanPacket packets[MAX_PACKETS];
   volatile unsigned int lastPos,firstPos,safePos;  
   volatile unsigned int readPos,writePos; 
}

树:XML

XML文件的组织本身就是一个树

class XML
{
public:
    class Node
    {
    public:
     class Attribute
        {
         public:
                int namePos,valuePos;
        };
   }

   Node *child,*parent,*sibling;
}

(若想支持中文,则用记事本打开ui/win32/simple目录下的Simple.rc,将其中字符串 
LANGUAGE LANG_ENGLISH, SUBLANG_ENGLISH_US改为 
LANGUAGE LANG_CHINESE, SUBLANG_CHINESE_SIMPLIFIED。 
重新编译生成即可


二十五。Peercast中“流”概念的分析 

流在Peercast中是一个很重要的概念。
凡是数据从一个地方传输到另一个地方的操作都是流的操作,所以就网络交换数据而言,例如包的发送和接收,都可视为流操作。
Peercast中用Stream类来提供一个界面。
其继承类有FileStream和ClientSocket。

Stream类提供读取和写入各种不同类型数据(ID4类型、字符型、整型、字符串)的操作,例如
    long readLong()
    {
        long v;
        read(&v,4);
        CHECK_ENDIAN4(v);
        return v;
    }


void Stream::writeLine(const char *str)
{
 writeString(str);

 if (writeCRLF)
     write("\r\n",2);
 else
  write("\n",1);
}
其他还有readInt()、readChar()、writeString()等

而这些操作都访问的是read()和write()函数
这里我们看看read()函数的定义
 virtual int read(void *,int)=0;
是个虚函数,而且这个虚函数在基类中并没有实现,而是留在子类中实现的
在FileStream中read()是从文件中读取相应字节的数据,在ClientSocket中read()是在网络中接收相应字节的数据。
在基类中使用一个未来才实现的方法是不是觉得有些不可思议?
这里基类依赖于它的派生类实现。我们通常都知道派生类依赖于基类实现,其实反过来也是可以成立的。
这样做的好处在于提供一个框架,使以后的程序员可以加入代码。
有关这方面的讨论可以参见《C++程序设计语言》设计和编程这一章。

 
IndirectStream(HTTP是其继承类)和AtomStream与Stream是包容关系,例如AtomStream包含Stream类型的对象,并提供各种函数对Stream类型的对象进行操作。
class AtomStream
{
   Stream &io;
}

void AtomStream::writeInt(ID4 id,int d)
{
 io.writeID4(id);
 io.writeInt(4);
 io.writeInt(d);
}

二十六。Peercast的电视直播测试 

硬件设备:服务器+视频采集卡+普通电视

软件设备:Windows XP + Windows Media Encoder 9.0 + Peercast

实现步骤:

1.用视/音频线将电视AV输出连接到视频采集卡上。

2.在Windows media encoder中新建会话选择广播实时事件,设备选项中选取相应的视频采集卡,广播方法中选择自编码器拉传递,广播连接端口选择8080(其他未用端口也行),编码选项选择282Kbps(越高越清晰,有多种选项),282属于较低画质,但是还勉强可以接受的编码率,点击完成。

3.配置Peercast。选择Broadcast,URL中填入http://localhost:8080,TYPE选择WMV,点击Create relay。

4.在控制中选择开始编码。

 

局域网内延时应该在15秒左右。

Peercast由于只是从单点传输,所以传输视频可能会出现一些问题。明天提高码率和访问机器量继续进行测试

 

今天转播的世乒赛哈哈。

中国队果然赢了。

王励勤果然是中国乒乓球队目前的头号人物。

敬请期待明晚7:15分的世乒赛男团直播。

二十七。Peercast的改进分析 

Peercast可以说是一个实验的产品,而不是一个稳定的商业版本。所以无论在稳定性还是在性能上都存在一定的问题。这里提出Peercast存在的一些问题,并考虑相应的改进方法。希望可以为大家改进Peercast提供一些帮助。

先解释几个名词:
父节点:也就是此时你在向别人传输数据。一个节点可以同时是父节点和子节点。
子节点:也就是此时你在接收父节点传输给你的数据。

基于文件传输的流媒体传输:
    Peercast是在Gnutella协议的基础上发展而来的。Gnutella是一个P2P的文件共享系统,可以传输任何类型的文件。Peercast传输的文件是视/音频流,传输方式通过HTTP方式进行。但Peercast没有考虑到流媒体传输的特性,只是简单地用HTTP方式来传输视/音频文件。
    改进:可以考虑采用RTP/RTSP方式来传输流媒体数据

单点传输:
    Peercast每个节点的数据只能来源於单一的节点。这样的好处在于模型简单,缓冲机制容易处理,传输过程中不会产生太多的控制信息。在传输音频时没有任何问题,但是在传输视频数据时,由于视频数据量大,所以会经常出现缓冲很久的情况。
    单点传输还存在的一个问题是如果父节点退出网络,会对子节点造成很大的影响。其实单点传输可以理解为基于转播的传播方式,每个节点向其他结点转播频道。但这样的话无法发挥出一般P2P软件使用的用户越多传输速度越快的优势,这时候结点多只意味着你选择最佳节点的空间大而已,一旦确定一个传输节点,速率可以认为就是固定的。
    改进:采用多点传输的一种称为SwarmStreaming的方式。

节点的动态加入:
    有新节点加入时似乎其他节点也会受到相应的影响,如出现缓冲或声音出错等情况。
    这个问题尚在研究之中。

节点的动态退出:
    由于Peercast只能从一个结点传输数据,那么当你的传输节点退出网络的时候,就无法再连接到另一个传输节点了,而只能选择重新输入频道地址。这是由于Peercast并没有做相应的处理。如果节点动态加入和退出频繁的话,会带来很大的问题。
    改进:在一个节点要退出网络的时候,它先向它的子节点发出要退出的消息,子节点收到这个消息后,根据它的ChanHit再去寻找下一个传输者。如果在读完缓冲前能与另一个节点建立连接,就不会出现中断的情况。

电台发布:
    由于要支持各种各样的数据来源,如静态MP3文件、实时编码,各种不同类型的文件,所以电台发布繁琐。
    改进:可以在Peercast中内置电台发布功能。可以做成类似向导形式,也就是让你一步步地去选择发布的来源、方式、编码率等等。这样可以使软件更易使用。

YP:
    目前大多数的P2P软件都需要一个目录服务器,YP就相当于这样的一个目录服务器。
    也可以把显示频道的功能集成在Peercast软件中,只需要使客户端去读取YP上的频道信息就可以了。不过这样会增加Peercast的CPU使用率,而且由于电台的动态变化比较大,所以可以考虑在这边只显示YP上第一页的电台。

播放器集成:
    其实是否集成播放器各有各的好处。
    不集成播放器的好处在于用户可以使用他们喜欢的媒体播放器去播放相应的文件。
    集成播放器的话用户界面会更友好,可以显示出错和连接信息,而且看起来更像一个完整的软件。
    这个功能可以根据需要而定。

二十八。Peercast播放模块分析 

这里以YP上的JOKV-FM(TEST)为例
当点击YP上的一个频道时,其访问地址为peercast://pls/25838B9F1EAE27079B793C9FBA0E4156?

tip=222.148.187.176:7144
peercast://指的是peercast协议,由于peercast注册了此协议,所以在IE中输入这个地址时会自动启动

peercast并把这个地址传送给peercast
25838B9F1EAE27079B793C9FBA0E4156指的是广播端的ChanID,每广播一个电台peercast会根据相应算法生

成一个ID,这个ID可以唯一标识一个频道。
tip=222.148.187.176:7144表示广播主机的地址和端口号。这样的话本地的peercast会先去和这个主机建

立连接,然后根据这个主机去找8个转播相同频道的主机,选择其中最好的一个作为传输者。
建立连接后,生成播放列表play.pls如下
[playlist]

NumberOfEntries=1
File1=http://localhost:7144/stream/25838B9F1EAE27079B793C9FBA0E4156.ogg
Title1=JOKV-FM(TEST)
Length1=-1
Version=2

然后调用播放器(例如winamp)去播放这个列表,这时winamp访问的url地址为

http://localhost:7144/stream/74F7ECF0508E50E62FD3BEB0624921E4.ogg。即winamp通过http方式从

peercast获得媒体数据并播放,访问主机为本机localhost,端口为7144。
winamp发送的HTTP请求类似如下:
GET /stream/74F7ECF0508E50E62FD3BEB0624921E4.ogg HTTP/1.1
Host: localhost:7144

Peercast有一个servent监听7144端口,并处理发来的HTTP请求。
void Servent::handshakeHTTP(HTTP &http, bool isHTTP)
{
 char *in = http.cmdLine;

 if (http.isRequest("GET /"))
 {
  char *fn = in+4;
     if (strncmp(fn,"/stream/",8)==0)
   triggerChannel(fn+8,ChanInfo::SP_HTTP,isPrivate());
 }
}

下面主要分析Peercast如何把数据送往播放器

// 触发频道,调用processStream传送媒体数据给播放器
void Servent::triggerChannel(char *str, ChanInfo::PROTOCOL proto,bool relay) 
{
 outputProtocol = proto;
 processStream(false,info);
}

// outputProtocol为HTTP协议,调用sendRawChannel发送数据
void Servent::processStream(bool doneHandshake,ChanInfo &chanInfo)
{
 if (outputProtocol == ChanInfo::SP_HTTP)
 {
  sendRawChannel(true,true);
 }
}

// 发送频道数据给播放器
void Servent::sendRawChannel(bool sendHead, bool sendData)
{
 try
 {

  sock->setWriteTimeout(DIRECT_WRITE_TIMEOUT*1000);

  Channel *ch = chanMgr->findChannelByID(chanID);
  if (!ch)
   throw StreamException("Channel not found");

  setStatus(S_CONNECTED);


  //这里进行最重要的数据传输,请特别注意
  LOG_DEBUG("Starting Raw stream of %s at %d",ch->info.name.cstr(),streamPos);

  if (sendHead)
  {
   ch->headPack.writeRaw(*sock);
   streamPos = ch->headPack.pos + ch->headPack.len;
   LOG_DEBUG("Sent %d bytes header ",ch->headPack.len);
  }

  if (sendData)
  {

   unsigned int streamIndex = ch->streamIndex;
   unsigned int connectTime = sys->getTime();
   unsigned int lastWriteTime = connectTime;

   while ((thread.active) && sock->active())
   {
    ch = chanMgr->findChannelByID(chanID);

    if (ch)
    {

     if (streamIndex != ch->streamIndex)
     {
      streamIndex = ch->streamIndex;
      streamPos = ch->headPack.pos;
      LOG_DEBUG("sendRaw got new stream index");
     }

     ChanPacket rawPack;
     if (ch->rawData.findPacket(streamPos,rawPack))
     {
      if (syncPos != rawPack.sync)
       LOG_ERROR("Send skip: %

d",rawPack.sync-syncPos);
      syncPos = rawPack.sync+1;

      if ((rawPack.type == ChanPacket::T_DATA) || 

(rawPack.type == ChanPacket::T_HEAD))
      {
       rawPack.writeRaw(*sock);
       lastWriteTime = sys->getTime();
      }

      if (rawPack.pos < streamPos)
       LOG_DEBUG("raw: skip back %

d",rawPack.pos-streamPos);
      streamPos = rawPack.pos+rawPack.len;
     }
    }

    if ((sys->getTime()-lastWriteTime) > DIRECT_WRITE_TIMEOUT)
     throw TimeoutException();
    
    sys->sleepIdle();
   }
  }
 }catch(StreamException &e)
 {
  LOG_ERROR("Stream channel: %s",e.msg);
 }
}

 

来源:   http://rejoice2008.blog.hexun.com/24609195_d.html

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章