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

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