网络音频点播软件的设计与开发实验
网络音频点播软件的设计与开发实验一、实验目的掌握基于Socket 的C/S编程的方法掌握 Windows平台Socket 网络应用程序的开发方法掌握 Windows平台多线程网络程序的开发方法二、实验
网络音频点播软件的设计与开发实验
一、实验目的
掌握基于Socket 的C/S编程的方法
掌握 Windows平台Socket 网络应用程序的开发方法
掌握 Windows平台多线程网络程序的开发方法
二、实验内容
在 Windows2000平台下,使用Microsoft Visual C 6.0,基于Socket 开发网络音频 点播程序,服务器端能够捕捉音频流并发送到需要点播的客户端,客户端接收音频流 后播放。不同客户端之间可以互相发送文本。
三、实验原理
1.Winsock 概述
在 Win32平台上 Winsock是访问网络层协议的首选接口。而且在每个Windows 平 台上,Winsock 都以不同形式存在着。Winsock 与Linux 的Socket 一样,是网络编程接 口, 而不是协议。Winsock 是Unix 的Berkeley(BSD)套接字的基础上发展起来的,Winsock
有多个版本,从 Windows95、WinNt4 开始,系统就内置了 Winsock1.1, 后来到了 Windows98、windows2000,它内置的 Winsock DLL已更新为 Winsock2.2。Winsock1.1 有 2 种 I/O 方,2 种 I/O 模型,到了 Winsock2.2,则有了 2 种 I/O 方式,5 种 I/O 模型。 另外,Winsock2.2 对 Socket 进行了很多扩充与改进,如重叠 I/O 模型、服务质量控制 等。Winsock 的版本是向前兼容的,也就是说,使用Winsock1.1编程接口的应用程序, 可以在 Winsock2.2的计算机上运行。
2.Winsock 编程基础
Winsock 与Linux 的 socket 编程是基本一致的,Linux 的 socket 编程的原理和方法, 在 Windows下依然适用。当然 Winsock有了更多的扩展。
(1)Winsock的初始化和释放
每个 Winsock应用都必须加载 Winsock Dll的相应版本。如果调用Winsock 之前没 有加载 Winsock库,这个函数就会返回错误,错误信息是 WSANOTINITIALISED。加 载 Winsock库是通过调用 WSAStartup函数实现的,这个函数定义为:
int WSAStartup(
WORD wVersionRequested,
LPWSADA TA lpWSAData
);
参数w VersionRequested 指定加载的 Winsock 库的版本,高位字节指定副版本,低 位字节指定主版本。可以使用宏MAKEWORD(X,Y)方便地指定合适的版本。
lpWSAData 是一个与加载库版本有关的信息, 在函数调用后系统会填充这个结构, 以获得相应的 Winsock库的信息.WSADA TA 结构声明为:
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应用程序结束网络程序后,需要释放 Winsock DLL的资源,释放函 数为:
int WSACleanup (void)
(2) Winsock的流套接字编程
下图是使用 Winsock流套接字时服务器与客户端的交互过程
(3)Winsock编程接口支持库
Winsock 支持库与 Winsock的版本有关,在使用不同版本的 Winsock开发程序时, 需要注意使用 Winsock库,下表列出了不同版本的 Winsock编程接口的支持库。

(1)名字解析
依用户看来,IP

地址是不容易记忆的。在指定机器时,大多数人喜欢用一个易记 的、友好的主机名而不是IP 地址。与此类似的是网络域名,大家在访问网页时,喜欢 输入的是服务器的域名地址如www.sina.com.cn , 而不是一个难记的IP 地址。
Winsock 套接字提供了支持函数,可以将主机名/域名解析为IP 地址。这个函数定 义为:struct hostent FAR *gethostbyname(const char FAR *name);
该函数传入域名字符串, 返回一个结构hostent, 这个结构包含了名字解析结构信息: 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 是正式的主机名或者域名,h_aliases 是一个由备用名字组成的空中止数组, h_addrtype 返回地址家族,h_length表示解析的地址字段长度,h_addr_list 是解析后地 址数组。一般情况下应当使用数组中的第一个地址,但如果返回的地址多于一个,可 以考虑使用其它地址。
(2)查询错误码
对编写程序而言,错误的查询和控制是十分重要的,不能因为一个小错误导致网 络程序的崩溃。对于 Winsock 来说,返回错误是常见的,但是在大多数情况下,这些 错误都是无关紧要的,通信仍可以继续在套接字上进行。
不成功的 Winsock 接口函数返回的最常见的值是 SOCKER_ERROR 它的常量值被 定义为-1。如果需要查询错误的具体情况,可以调用函数 WSAGetLastError获得错误 代码,了解错误的详细信息,这个函数定义为:
int WSAGetLastError(void);
函数返回的错误码都是以预先定义的常量值,可以在相关的帮助或者 winsock 的 头文件中找到它们的含义。
4、Windows 多线程
(1)线程的概念
为了了解线程的概念,必须先了解一下进程的概念。
一个进程通常定义为程序的一个实例。在 Win32中,进程占据4GB 的地址空间。 为了让进程完成一些工作,进程必须至少占有一个线程,所以线程是描述进程内的执 行,正是线程负责执行包含在进程的地址空间中的代码。实际上,单个进程可以包含 几个线程,它们可以同时执行进程地址空间中的代码。为了做到这一点,每个线程有 自己的一组CPU 寄存器和堆栈。
每个进程至少有一个线程在执行其地址空间中的代码,为了运行所有这些线程, 操作系统为每个独立线程安排一些CPU 时间, 操作系统以轮转方式向线程提供时间片。 创建一个 Win32 进程时,它的第一个线程称为主线程,由系统自动生成,然后再由这 个主线程生成额外的线程,这些线程又可生成更多的线程。
(2)编写线程函数
所有线程必须从一个指定的函数开始执行,该函数称为线程函数,它必须具有下 列原型:
DWORD WINAPI YourThreadFunc(;PVOID lpvThreadParm);
该函数输入一个LPVOID 类型的参数,可以是一个DWORD 型的整数,也可以是 一个指向一个缓冲区的指针,返回一个DWORD 型的值。
(3)创建一个线程
一个进程的主线程是由操作系统自动生成的,如果要让一个主线程创建额外的线 程,可以调用CreateThread 函数,这个函数声明为:
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 参数为一个指向SECURITY_ATTRIBUTES 结构的指针。 如果想让对象为缺省安全属性,可以传一个NULL ;参数 lpStartAddress用来表示新线 程开始执行时代码所在函数的地址,即为线程函数。lpParameter 为传入线程函数的参 数,dwCreationFlags 参数指定控制线程创建的附加标志,可以取两种值。如果该参数 为 0,线程就会立即开始执行,如果该参数为 CREA TE_SUSPENDED,则系统产生线 程后,挂起该线程。最后一个参数 lpThreadId 是一个 DWORD 类型的地址,返回赋给 新线程的ID 值。
CreateThread 函数参数较多,但在常见的使用中,这些参数可以取默认值,如: DWORD dwThreadId;
HANDLE hThread=CreateThread(NULL,0,ServiceThread,param ,0,&dwThreadId);
(4)终止线程
如果某些线程调用了ExitThread 函数,就可以终止自己。
VOID ExitThread(
DWORD dwExitCode //exit code for this thread
);
这个函数为调用该函数的线程设置了退出码dwExitCode 后,就终止该线程。调用 TerminateThread 函数也可以终止线程:
BOOL TerminateThread(
HANDLE hThread, //handle to thread
DWORD dwExitCode //exit code
):
该函数用来结束由hThread 参数指定的线程,并把dwExitCode 设成该线程的退出 码。当某个线程不再响应时,就可以用其它线程调用该函数来终止这个不响应的线程
(5)挂起及恢复线程
在线程被创建后的运行过程中,可以将线程挂起,线程在保存当前的运行环境后 进入睡眠状态,不再占用 CPU ;然后程序可以某个时刻“唤醒”这个线程,恢复运行 环境,然后继续运行。挂起和恢复的函数分别为:
DWORD SuspendThread(HANDLE hThread);
DWORD ResumeThread(HANDLE hThread);
四、实验步骤
1、需求分析
这是一个网络音频点播的工程,服务器端能够捕捉音频流并发送到需要点播的客 户端,客户端接收音频流后播放。不同客户端之间可以互相发送文本。显然程序包含 两个程序:服务器和客户端,它们需要实现的功能为:
(1)服务器
●音频的捕捉:使用音频编程接口捕捉服务器正在播放的音频。
●音频数据的缓存:保存捕捉的音频数据,并在合适的时刻交给网络模块发送。 ●音频数据的发送:发送音频数据流到客户端。
●多客户的支持和管理:应该能够支持多个客户同时接收网络音频,并能够监控和 管理多个用户。
●文本接收和发送功能:接收到一个客户端发送的文本后,将文本信息转发给相应 的客户端。
,(2)客户端
●音频数据的接收。
●音频数据的缓存。
●音频数据的播放。
●文本接收和发送功能。
2.程序的设计
这是一个标准的客户/服务器程序,使用socket 套接字编程接口实现网络功能。
(1) 套接字类型
由于这个工程是音频和文本的传输,对可靠性要求较高,同时音频数据的播放对 及时性和传输效率要求较高,所以应当使用面向连接的流式套接字来实现网络数据的 传输。
(2) 服务器模式
音频的传输不需要服务器接收客户端的信息,所以不用考虑服务器模式。而客户 端的文本通信需要服务器来转发,这种文本通信具有并发的特点,所以应当采用并发 服务器模式,而且由于传输的信息量不是很大,所以可以考虑采用select 函数监听多客 户端的设计方法,结构如下图所示。
(3)socket 类封装
本实验项目中两种数据需要网络传输:音频数据和文本数据。而对于socket 来说, 它并不认为这两种数据有什么不同。都是数据流,只需要发送而已。Socket 的初始化、 连接和接收发送等功能都与数据无关。所以应当使用类来封装socket ,实现基本的网络 功能,并使用类的继承或者对象组合扩展它的功能,实现文本和音频流的传输。Socket 类可以在服务器程序和客户端程序中使用。
(4)音频捕捉和播放
音频捕捉和播放并不是本实验的重点,可以使用很多种方法来实现这个功能。可

以使用面向对象的编程方法将音频捕捉和播放的实现细节封装起来,并提供统一的使 用接口,供其它功能模块使用。
本实验提供了一个音频捕捉和播放模块的例子,在实验 FTP 服务器的目录下,它 使用了 DirectX 技术,调用 DirectSoundCapture 和 DirectSound 接口分别实现音频的捕 捉和播放,并提供了相应的接口。
(5)服务器设计
服务器依据的需求,应当包含以下模块:
●音频捕捉模块:可以开始和中止音频的捕捉,并提供定时获取音频流数据接口。 ●数据发送模块: 可以将数据发送到指定的客户端。 支持多个和单个客户端的发送。 ●客户端服务模块:接收客户端的网络数据,并在解析客户端请求后交给相应模块 处理。
●客户维护和管理模块:维护客户列表。
●控制台模块:监视服务器运行状况、日值记录和控制服务器。
各个模块间的关系如下图所示。
(6)客户端设计
客户端依据需求,就当包含以下模块:
●音频播放模块。
●数据接收模块。
●文本发送模块。
客户端的结构图请自己绘出。
3、开发实现
由于工程较大,开发实现的具体步骤将不再叙述,下面介绍一些需要注意的问题。
(1)C/S通信协议
在客户端和服务器间进行通信时,惟一的方式是发送和接收数据。而数据又可能 有多种数据类型,如音频数据、文本数据、控制信息等。服务器和客户端必须为不同 的数据类型定义不同的数据传输格式,以便服务器和客户端之间能够正常的“通话” , 否则它们接收到的数据就只能是一堆无法理解的密文。
常见的设计方法是定义服务器和客户端通信协议,为每种数据类型和操作规定详 细的解释和行为。如是单纯发送文本数据时,可以在发送的文本数据前添加字段的长

度。这是一种简单的形式。在本实验中上,由于涉及复杂的数据类型,所以通信协议 也相对复杂,能够支持全部数据类型。

的长度,最后是实际要发送的数据。
(2)发送数据
在使用socket 编程接口发送数据时,使用send 函数,如:
char buf[1024*4];
int r=send(sockfd,buf,1024*4,0);
对于上面的代码,返回值的表示实际发送的数据,在正常情况发送情况下,有可 能小于发送缓冲区的大小。对于TCP 来说,一个主要的原因是窗口大小的问题,接收 端会对窗口大小进行调整,指出它可以接收多少数据,如果有大量数据涌入接收端, 它就会减少窗口大小甚至设置窗口为0.
在上面的例子中,如果一次只能发送1024字节的数据,则发送就不完整,所以应 当处理这种情况,常见的方式是循环发送缓冲区中的数据,直至全部发出,如:
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; //发生错误 send_bytes =r; } return 0; (3)音频捕捉设置 需要正确设置音频的捕捉源,由于实验是捕捉声卡发出的声音,所以需要设置捕 捉源为相应类型。 在Windows 的录音控制中(控制面版®声音和音频控制®音频®音量) 选中“Wave Out Mix”(或波形输出混音) 下的“选择 ”复选框,这个参数在不同的声卡 中可能有细微差别。 (4)运行程序 在一台计算机上运行服务器程序,在运行之前首先开始播放音频。在其它几台计 算机上运行客户端程序,并连接到服务器,收听网络点播的音频 思考题 1. Winsock1.1 版本的应用程序是否可以与 Winsock2.2 的应用程序进行正常的通信? 请说明原因。 2. 在文本数据传输的服务器模式中使用并发服务器模式,可以采用 select 函数监听多 客户端的设计方法,也可以采用一个线程对应一个客户端的方法并发服务器,这两 种方法哪一种更适合这个实验程序,为什么? 3. 在调用recv 接收数据时,接收到数据并不一定就是发送方发出的数据大小,而是可 能大于或者小于这个值,如何在编程中处理这个问题?