網(wǎng)絡(luò)音頻點(diǎn)播軟件的設(shè)計(jì)與開發(fā)實(shí)驗(yàn)
網(wǎng)絡(luò)音頻點(diǎn)播軟件的設(shè)計(jì)與開發(fā)實(shí)驗(yàn)一、實(shí)驗(yàn)?zāi)康恼莆栈赟ocket?的C/S編程的方法掌握?Windows平臺Socket?網(wǎng)絡(luò)應(yīng)用程序的開發(fā)方法掌握?Windows平臺多線程網(wǎng)絡(luò)程序的開發(fā)方法二、實(shí)驗(yàn)
網(wǎng)絡(luò)音頻點(diǎn)播軟件的設(shè)計(jì)與開發(fā)實(shí)驗(yàn)
一、實(shí)驗(yàn)?zāi)康?/p>
掌握基于Socket?的C/S編程的方法
掌握?Windows平臺Socket?網(wǎng)絡(luò)應(yīng)用程序的開發(fā)方法
掌握?Windows平臺多線程網(wǎng)絡(luò)程序的開發(fā)方法
二、實(shí)驗(yàn)內(nèi)容
在?Windows2000平臺下,使用Microsoft?Visual?C 6.0,基于Socket?開發(fā)網(wǎng)絡(luò)音頻 點(diǎn)播程序,服務(wù)器端能夠捕捉音頻流并發(fā)送到需要點(diǎn)播的客戶端,客戶端接收音頻流 后播放。不同客戶端之間可以互相發(fā)送文本。
三、實(shí)驗(yàn)原理?
1.Winsock 概述
在?Win32平臺上?Winsock是訪問網(wǎng)絡(luò)層協(xié)議的首選接口。而且在每個(gè)Windows 平 臺上,Winsock 都以不同形式存在著。Winsock?與Linux 的Socket?一樣,是網(wǎng)絡(luò)編程接 口, 而不是協(xié)議。Winsock ?是Unix 的Berkeley(BSD)套接字的基礎(chǔ)上發(fā)展起來的,Winsock??
有多個(gè)版本,從?Windows95、WinNt4?開始,系統(tǒng)就內(nèi)置了?Winsock1.1, 后來到了?Windows98、windows2000,它內(nèi)置的?Winsock?DLL已更新為?Winsock2.2。Winsock1.1?有?2?種?I/O?方,2?種?I/O?模型,到了?Winsock2.2,則有了?2?種?I/O?方式,5?種?I/O?模型。 另外,Winsock2.2?對?Socket?進(jìn)行了很多擴(kuò)充與改進(jìn),如重疊?I/O?模型、服務(wù)質(zhì)量控制 等。Winsock 的版本是向前兼容的,也就是說,使用Winsock1.1編程接口的應(yīng)用程序, 可以在?Winsock2.2的計(jì)算機(jī)上運(yùn)行。?
2.Winsock 編程基礎(chǔ)?
Winsock 與Linux 的?socket?編程是基本一致的,Linux ?的?socket?編程的原理和方法, 在?Windows下依然適用。當(dāng)然?Winsock有了更多的擴(kuò)展。?
(1)Winsock的初始化和釋放
每個(gè)?Winsock應(yīng)用都必須加載?Winsock?Dll的相應(yīng)版本。如果調(diào)用Winsock 之前沒 有加載?Winsock庫,這個(gè)函數(shù)就會返回錯(cuò)誤,錯(cuò)誤信息是?WSANOTINITIALISED。加 載?Winsock庫是通過調(diào)用?WSAStartup函數(shù)實(shí)現(xiàn)的,這個(gè)函數(shù)定義為:?
int?WSAStartup(?
WORD?wVersionRequested,?
LPWSADA TA?lpWSAData?
)??
參數(shù)w?VersionRequested 指定加載的?Winsock?庫的版本,高位字節(jié)指定副版本,低 位字節(jié)指定主版本??梢允褂煤闙AKEWORD(X,Y)方便地指定合適的版本。?
lpWSAData 是一個(gè)與加載庫版本有關(guān)的信息, 在函數(shù)調(diào)用后系統(tǒng)會填充這個(gè)結(jié)構(gòu), 以獲得相應(yīng)的?Winsock庫的信息.WSADA TA 結(jié)構(gòu)聲明為:?
typedef?struct?WSAData?{?
WORD?wVersion??
WORD?wHighV ersion:?
char?szDescription [WSADESCRIPTION_LEN 1];?
char?szSystemStatus[WSASYS_STATUS_LEN 1]??
unsigned?short?iMaxSockets??
unsigned?short?iMaxUdpDg??
char?FAR??*?lpVendorInfo??
}WSADATA,??*LPWSADATA?
,在?Winsock應(yīng)用程序結(jié)束網(wǎng)絡(luò)程序后,需要釋放?Winsock?DLL的資源,釋放函 數(shù)為:?
int?WSACleanup?(void)?
(2)?Winsock的流套接字編程
下圖是使用?Winsock流套接字時(shí)服務(wù)器與客戶端的交互過程?
(3)Winsock編程接口支持庫?
Winsock 支持庫與?Winsock的版本有關(guān),在使用不同版本的?Winsock開發(fā)程序時(shí), 需要注意使用?Winsock庫,下表列出了不同版本的?Winsock編程接口的支持庫。?
(1)名字解析
依用戶看來,IP?
地址是不容易記憶的。在指定機(jī)器時(shí),大多數(shù)人喜歡用一個(gè)易記 的、友好的主機(jī)名而不是IP 地址。與此類似的是網(wǎng)絡(luò)域名,大家在訪問網(wǎng)頁時(shí),喜歡 輸入的是服務(wù)器的域名地址如www.sina.com.cn , 而不是一個(gè)難記的IP 地址。?
Winsock 套接字提供了支持函數(shù),可以將主機(jī)名/域名解析為IP 地址。這個(gè)函數(shù)定 義為:struct?hostent?FAR?*gethostbyname(const?char?FAR?*name)??
該函數(shù)傳入域名字符串, 返回一個(gè)結(jié)構(gòu)hostent, 這個(gè)結(jié)構(gòu)包含了名字解析結(jié)構(gòu)信息:?struct?hosten{?
char?FAR?*????????h_name??
char?FAR?*?FAR?*??h_aliases:?
short?????????????h_addrtype?
,short?????????????h_length??
char?FAR?*?FAR?*?h_addr_list??
}??
h_name?是正式的主機(jī)名或者域名,h_aliases?是一個(gè)由備用名字組成的空中止數(shù)組,?h_addrtype?返回地址家族,h_length表示解析的地址字段長度,h_addr_list?是解析后地 址數(shù)組。一般情況下應(yīng)當(dāng)使用數(shù)組中的第一個(gè)地址,但如果返回的地址多于一個(gè),可 以考慮使用其它地址。?
(2)查詢錯(cuò)誤碼
對編寫程序而言,錯(cuò)誤的查詢和控制是十分重要的,不能因?yàn)橐粋€(gè)小錯(cuò)誤導(dǎo)致網(wǎng) 絡(luò)程序的崩潰。對于?Winsock?來說,返回錯(cuò)誤是常見的,但是在大多數(shù)情況下,這些 錯(cuò)誤都是無關(guān)緊要的,通信仍可以繼續(xù)在套接字上進(jìn)行。
不成功的?Winsock?接口函數(shù)返回的最常見的值是?SOCKER_ERROR?它的常量值被 定義為-1。如果需要查詢錯(cuò)誤的具體情況,可以調(diào)用函數(shù)?WSAGetLastError獲得錯(cuò)誤 代碼,了解錯(cuò)誤的詳細(xì)信息,這個(gè)函數(shù)定義為:?
int?WSAGetLastError(void)??
函數(shù)返回的錯(cuò)誤碼都是以預(yù)先定義的常量值,可以在相關(guān)的幫助或者?winsock?的 頭文件中找到它們的含義。?
4、Windows?多線程?
(1)線程的概念
為了了解線程的概念,必須先了解一下進(jìn)程的概念。
一個(gè)進(jìn)程通常定義為程序的一個(gè)實(shí)例。在?Win32中,進(jìn)程占據(jù)4GB 的地址空間。 為了讓進(jìn)程完成一些工作,進(jìn)程必須至少占有一個(gè)線程,所以線程是描述進(jìn)程內(nèi)的執(zhí) 行,正是線程負(fù)責(zé)執(zhí)行包含在進(jìn)程的地址空間中的代碼。實(shí)際上,單個(gè)進(jìn)程可以包含 幾個(gè)線程,它們可以同時(shí)執(zhí)行進(jìn)程地址空間中的代碼。為了做到這一點(diǎn),每個(gè)線程有 自己的一組CPU 寄存器和堆棧。
每個(gè)進(jìn)程至少有一個(gè)線程在執(zhí)行其地址空間中的代碼,為了運(yùn)行所有這些線程, 操作系統(tǒng)為每個(gè)獨(dú)立線程安排一些CPU 時(shí)間, 操作系統(tǒng)以輪轉(zhuǎn)方式向線程提供時(shí)間片。 創(chuàng)建一個(gè)?Win32?進(jìn)程時(shí),它的第一個(gè)線程稱為主線程,由系統(tǒng)自動生成,然后再由這 個(gè)主線程生成額外的線程,這些線程又可生成更多的線程。?
(2)編寫線程函數(shù)
所有線程必須從一個(gè)指定的函數(shù)開始執(zhí)行,該函數(shù)稱為線程函數(shù),它必須具有下 列原型:?
DWORD?WINAPI?YourThreadFunc(?PVOID?lpvThreadParm)??
該函數(shù)輸入一個(gè)LPVOID 類型的參數(shù),可以是一個(gè)DWORD 型的整數(shù),也可以是 一個(gè)指向一個(gè)緩沖區(qū)的指針,返回一個(gè)DWORD 型的值。?
(3)創(chuàng)建一個(gè)線程
一個(gè)進(jìn)程的主線程是由操作系統(tǒng)自動生成的,如果要讓一個(gè)主線程創(chuàng)建額外的線 程,可以調(diào)用CreateThread 函數(shù),這個(gè)函數(shù)聲明為:?
HANDLE?CreateThread(?
LPSECURITY_ATTRIBUTES?lpThreadAttributes,?????????//SD?
DWORD?dwStackSize,???????????????????????????????//initial?stack?size?
LPTHREAD_START_ROUTINE?lpStartAddress,??????????//thread?function?
LPVOID?lpParameter,?//thread?argument?
DWORD?dwCreationFlags,????????????????????????????//creation?option
,LPDWORD?lpThreadId???????????????????????????????//threa?identifier?
)??
其中,lpThreadAttributes 參數(shù)為一個(gè)指向SECURITY_ATTRIBUTES 結(jié)構(gòu)的指針。 如果想讓對象為缺省安全屬性,可以傳一個(gè)NULL ;參數(shù)?lpStartAddress用來表示新線 程開始執(zhí)行時(shí)代碼所在函數(shù)的地址,即為線程函數(shù)。lpParameter?為傳入線程函數(shù)的參 數(shù),dwCreationFlags?參數(shù)指定控制線程創(chuàng)建的附加標(biāo)志,可以取兩種值。如果該參數(shù) 為?0,線程就會立即開始執(zhí)行,如果該參數(shù)為?CREA TE_SUSPENDED,則系統(tǒng)產(chǎn)生線 程后,掛起該線程。最后一個(gè)參數(shù)?lpThreadId?是一個(gè)?DWORD?類型的地址,返回賦給 新線程的ID 值。?
CreateThread 函數(shù)參數(shù)較多,但在常見的使用中,這些參數(shù)可以取默認(rèn)值,如:?DWORD?dwThreadId??
HANDLE?hThread=CreateThread(NULL,0,ServiceThread,param?,0,&dwThreadId)??
(4)終止線程
如果某些線程調(diào)用了ExitThread 函數(shù),就可以終止自己。?
VOID?ExitThread(?
DWORD?dwExitCode??????????????????????????//exit?code?for?this?thread?
)??
這個(gè)函數(shù)為調(diào)用該函數(shù)的線程設(shè)置了退出碼dwExitCode 后,就終止該線程。調(diào)用?TerminateThread 函數(shù)也可以終止線程:?
BOOL?TerminateThread(?
HANDLE?hThread,???????????????????????//handle?to?thread?
DWORD?dwExitCode??????????????????????????//exit?code?
):?
該函數(shù)用來結(jié)束由hThread 參數(shù)指定的線程,并把dwExitCode 設(shè)成該線程的退出 碼。當(dāng)某個(gè)線程不再響應(yīng)時(shí),就可以用其它線程調(diào)用該函數(shù)來終止這個(gè)不響應(yīng)的線程?
(5)掛起及恢復(fù)線程
在線程被創(chuàng)建后的運(yùn)行過程中,可以將線程掛起,線程在保存當(dāng)前的運(yùn)行環(huán)境后 進(jìn)入睡眠狀態(tài),不再占用?CPU ;然后程序可以某個(gè)時(shí)刻“喚醒”這個(gè)線程,恢復(fù)運(yùn)行 環(huán)境,然后繼續(xù)運(yùn)行。掛起和恢復(fù)的函數(shù)分別為:?
DWORD?SuspendThread(HANDLE?hThread)??
DWORD?ResumeThread(HANDLE?hThread)??
四、實(shí)驗(yàn)步驟?
1、需求分析
這是一個(gè)網(wǎng)絡(luò)音頻點(diǎn)播的工程,服務(wù)器端能夠捕捉音頻流并發(fā)送到需要點(diǎn)播的客 戶端,客戶端接收音頻流后播放。不同客戶端之間可以互相發(fā)送文本。顯然程序包含 兩個(gè)程序:服務(wù)器和客戶端,它們需要實(shí)現(xiàn)的功能為:
(1)服務(wù)器?
●音頻的捕捉:使用音頻編程接口捕捉服務(wù)器正在播放的音頻。
●音頻數(shù)據(jù)的緩存:保存捕捉的音頻數(shù)據(jù),并在合適的時(shí)刻交給網(wǎng)絡(luò)模塊發(fā)送。 ●音頻數(shù)據(jù)的發(fā)送:發(fā)送音頻數(shù)據(jù)流到客戶端。
●多客戶的支持和管理:應(yīng)該能夠支持多個(gè)客戶同時(shí)接收網(wǎng)絡(luò)音頻,并能夠監(jiān)控和 管理多個(gè)用戶。
●文本接收和發(fā)送功能:接收到一個(gè)客戶端發(fā)送的文本后,將文本信息轉(zhuǎn)發(fā)給相應(yīng) 的客戶端。
,(2)客戶端
●音頻數(shù)據(jù)的接收。
●音頻數(shù)據(jù)的緩存。
●音頻數(shù)據(jù)的播放。
●文本接收和發(fā)送功能。?
2.程序的設(shè)計(jì)
這是一個(gè)標(biāo)準(zhǔn)的客戶/服務(wù)器程序,使用socket 套接字編程接口實(shí)現(xiàn)網(wǎng)絡(luò)功能。
(1) 套接字類型
由于這個(gè)工程是音頻和文本的傳輸,對可靠性要求較高,同時(shí)音頻數(shù)據(jù)的播放對 及時(shí)性和傳輸效率要求較高,所以應(yīng)當(dāng)使用面向連接的流式套接字來實(shí)現(xiàn)網(wǎng)絡(luò)數(shù)據(jù)的 傳輸。
(2) 服務(wù)器模式
音頻的傳輸不需要服務(wù)器接收客戶端的信息,所以不用考慮服務(wù)器模式。而客戶 端的文本通信需要服務(wù)器來轉(zhuǎn)發(fā),這種文本通信具有并發(fā)的特點(diǎn),所以應(yīng)當(dāng)采用并發(fā) 服務(wù)器模式,而且由于傳輸?shù)男畔⒘坎皇呛艽?,所以可以考慮采用select?函數(shù)監(jiān)聽多客 戶端的設(shè)計(jì)方法,結(jié)構(gòu)如下圖所示。
(3)socket?類封裝
本實(shí)驗(yàn)項(xiàng)目中兩種數(shù)據(jù)需要網(wǎng)絡(luò)傳輸:音頻數(shù)據(jù)和文本數(shù)據(jù)。而對于socket?來說, 它并不認(rèn)為這兩種數(shù)據(jù)有什么不同。都是數(shù)據(jù)流,只需要發(fā)送而已。Socket?的初始化、 連接和接收發(fā)送等功能都與數(shù)據(jù)無關(guān)。所以應(yīng)當(dāng)使用類來封裝socket ,實(shí)現(xiàn)基本的網(wǎng)絡(luò) 功能,并使用類的繼承或者對象組合擴(kuò)展它的功能,實(shí)現(xiàn)文本和音頻流的傳輸。Socket?類可以在服務(wù)器程序和客戶端程序中使用。?
(4)音頻捕捉和播放
音頻捕捉和播放并不是本實(shí)驗(yàn)的重點(diǎn),可以使用很多種方法來實(shí)現(xiàn)這個(gè)功能???/p>
以使用面向?qū)ο蟮木幊谭椒▽⒁纛l捕捉和播放的實(shí)現(xiàn)細(xì)節(jié)封裝起來,并提供統(tǒng)一的使 用接口,供其它功能模塊使用。
本實(shí)驗(yàn)提供了一個(gè)音頻捕捉和播放模塊的例子,在實(shí)驗(yàn)?FTP?服務(wù)器的目錄下,它 使用了?DirectX?技術(shù),調(diào)用?DirectSoundCapture?和?DirectSound?接口分別實(shí)現(xiàn)音頻的捕 捉和播放,并提供了相應(yīng)的接口。?
(5)服務(wù)器設(shè)計(jì)
服務(wù)器依據(jù)的需求,應(yīng)當(dāng)包含以下模塊:
●音頻捕捉模塊:可以開始和中止音頻的捕捉,并提供定時(shí)獲取音頻流數(shù)據(jù)接口。 ●數(shù)據(jù)發(fā)送模塊: 可以將數(shù)據(jù)發(fā)送到指定的客戶端。 支持多個(gè)和單個(gè)客戶端的發(fā)送。 ●客戶端服務(wù)模塊:接收客戶端的網(wǎng)絡(luò)數(shù)據(jù),并在解析客戶端請求后交給相應(yīng)模塊 處理。
●客戶維護(hù)和管理模塊:維護(hù)客戶列表。
●控制臺模塊:監(jiān)視服務(wù)器運(yùn)行狀況、日值記錄和控制服務(wù)器。
各個(gè)模塊間的關(guān)系如下圖所示。?
(6)客戶端設(shè)計(jì)
客戶端依據(jù)需求,就當(dāng)包含以下模塊:
●音頻播放模塊。
●數(shù)據(jù)接收模塊。
●文本發(fā)送模塊。
客戶端的結(jié)構(gòu)圖請自己繪出。?
3、開發(fā)實(shí)現(xiàn)
由于工程較大,開發(fā)實(shí)現(xiàn)的具體步驟將不再敘述,下面介紹一些需要注意的問題。?
(1)C/S通信協(xié)議
在客戶端和服務(wù)器間進(jìn)行通信時(shí),惟一的方式是發(fā)送和接收數(shù)據(jù)。而數(shù)據(jù)又可能 有多種數(shù)據(jù)類型,如音頻數(shù)據(jù)、文本數(shù)據(jù)、控制信息等。服務(wù)器和客戶端必須為不同 的數(shù)據(jù)類型定義不同的數(shù)據(jù)傳輸格式,以便服務(wù)器和客戶端之間能夠正常的“通話” , 否則它們接收到的數(shù)據(jù)就只能是一堆無法理解的密文。
常見的設(shè)計(jì)方法是定義服務(wù)器和客戶端通信協(xié)議,為每種數(shù)據(jù)類型和操作規(guī)定詳 細(xì)的解釋和行為。如是單純發(fā)送文本數(shù)據(jù)時(shí),可以在發(fā)送的文本數(shù)據(jù)前添加字段的長
度。這是一種簡單的形式。在本實(shí)驗(yàn)中上,由于涉及復(fù)雜的數(shù)據(jù)類型,所以通信協(xié)議 也相對復(fù)雜,能夠支持全部數(shù)據(jù)類型。
的長度,最后是實(shí)際要發(fā)送的數(shù)據(jù)。?
(2)發(fā)送數(shù)據(jù)
在使用socket?編程接口發(fā)送數(shù)據(jù)時(shí),使用send?函數(shù),如:?
char?buf[1024*4]??
int?r=send(sockfd,buf,1024*4,0)??
對于上面的代碼,返回值的表示實(shí)際發(fā)送的數(shù)據(jù),在正常情況發(fā)送情況下,有可 能小于發(fā)送緩沖區(qū)的大小。對于TCP 來說,一個(gè)主要的原因是窗口大小的問題,接收 端會對窗口大小進(jìn)行調(diào)整,指出它可以接收多少數(shù)據(jù),如果有大量數(shù)據(jù)涌入接收端, 它就會減少窗口大小甚至設(shè)置窗口為0.?
在上面的例子中,如果一次只能發(fā)送1024字節(jié)的數(shù)據(jù),則發(fā)送就不完整,所以應(yīng) 當(dāng)處理這種情況,常見的方式是循環(huán)發(fā)送緩沖區(qū)中的數(shù)據(jù),直至全部發(fā)出,如:?
char?buf[1024*]??
int?toal_bytes=1024*4??
int?send_bytes=0?
while?(sendbytes {? r=send(sockfd,buf? ?send_bytes,total_bytes?–?sendbytes,0)?? if(r<0)? return?r???????????????//發(fā)生錯(cuò)誤? send_bytes? ?=r?? }? return?0?? (3)音頻捕捉設(shè)置 需要正確設(shè)置音頻的捕捉源,由于實(shí)驗(yàn)是捕捉聲卡發(fā)出的聲音,所以需要設(shè)置捕 捉源為相應(yīng)類型。 在Windows 的錄音控制中(控制面版?聲音和音頻控制?音頻?音量)?選中“Wave?Out?Mix”(或波形輸出混音) 下的“選擇 ”復(fù)選框,這個(gè)參數(shù)在不同的聲卡 中可能有細(xì)微差別。? (4)運(yùn)行程序 在一臺計(jì)算機(jī)上運(yùn)行服務(wù)器程序,在運(yùn)行之前首先開始播放音頻。在其它幾臺計(jì) 算機(jī)上運(yùn)行客戶端程序,并連接到服務(wù)器,收聽網(wǎng)絡(luò)點(diǎn)播的音頻 思考題? 1.?Winsock1.1?版本的應(yīng)用程序是否可以與?Winsock2.2?的應(yīng)用程序進(jìn)行正常的通信? 請說明原因。? 2.?在文本數(shù)據(jù)傳輸?shù)姆?wù)器模式中使用并發(fā)服務(wù)器模式,可以采用?select?函數(shù)監(jiān)聽多 客戶端的設(shè)計(jì)方法,也可以采用一個(gè)線程對應(yīng)一個(gè)客戶端的方法并發(fā)服務(wù)器,這兩 種方法哪一種更適合這個(gè)實(shí)驗(yàn)程序,為什么?? 3.?在調(diào)用recv 接收數(shù)據(jù)時(shí),接收到數(shù)據(jù)并不一定就是發(fā)送方發(fā)出的數(shù)據(jù)大小,而是可 能大于或者小于這個(gè)值,如何在編程中處理這個(gè)問題?