第3章 网络应用

来源:互联网 发布:升级7本数据 编辑:程序博客网 时间:2024/05/17 07:51

DirectShow实务精选 (样章)

陆其明 著


第3章 网络应用

3.1 网络编程基础

多媒体技术与网络技术的结合,使得网络生活变得多姿多彩。从此,网络生活很迷人;网络改变了和改变着人们原本的生活方式。姑且认为DirectShow是单机的多媒体技术,一旦融合了网络技术,DirectShow更显现了它强大的生命力。本章将着重介绍DirectShow技术在网络方面的应用。

网络编程,当然要用到Windows Socket(套接字)技术。Socket相关的操作由一系列API函数来完成,比如socket、bind、listen、connect、accept、send、sendto、recv、recvfrom等。调用这些API函数有一定的先后次序,有些函数的参数还比较复杂,对于开发者来说,不是很好用。于是,微软的MFC提供了两个类:CAsyncSocket和CSocket,极大地方便了Socket功能的使用。这两个类的继承关系如图3.1。

图3.1 MFC Socket类的继承关系

CAsyncSocket类在较低层次上封装了Windows Socket API,并且通过内建一个(隐藏的)窗口,实现了适合Windows应用的异步机制(Windows Socket API默认情况下工作在阻塞模式,不方便直接在消息驱动的Windows程序上使用)。CSocket类从CAsyncSocket类派生,进一步简化了Socket功能的应用。不过很遗憾,正因为这两个类都内建了一个窗口,它们并不是线程安全的(thread-safe);如果要在多线程环境下应用Socket功能,建议自行封装Socket API函数。

使用Socket传输数据主要有两种方式:TCP传输和UDP传输。(OSI参考模型将网络通信分成7个层次,从低往上依次为物理层、数据链路层、网络层、传输层、会话层、表示层、应用层;TCP和UDP均是传输层的协议。)下面,就分别来介绍这两种数据传输方式。

提示:本章在介绍网络通信双方的时候,会使用两组关键词:服务器-客户机和本地端-远程端。其中,服务器-客户机是根据角色来界定的;而本地端-远程端是一个相对概念,依据不同的参照物,可以分别表示不同的角色。比如以服务器为参照物,可以称服务器为本地端,称客户机为远程端;而如果以客户机为参照物,可以称客户机为本地端,称服务器为远程端。

3.1.1 TCP传输

TCP,Transfer Control Protocol的缩写(传输控制协议),是一种面向连接的网络传输协议。TCP协议的特点是,支持多数据流操作,提供流控和错误控制,甚至能完成对乱序到达报文的重新排序等。因此,TCP提供了可靠的应用数据传输服务。

通信双方使用TCP传输的一般过程参考如图3.2。

图3.2 TCP通信的一般过程

本节将要实现一个TCP传输的演示程序TCPDemo,它包括服务器和客户机两个部分。它们的程序界面如图3.3。


图3.3 TCP传输演示程序界面

TCPDemo的演示过程如下:

(1)将服务器和客户机两部分程序都运行起来(此时服务器已经启动了侦听客户机连接请求的子线程,侦听端口号为10028)。
(2)在客户机程序界面上输入服务器的IP地址(如果服务器和客户机运行在同一台机器上,IP地址可以指定为127.0.0.1)、侦听端口号(因为服务器在10028端口上侦听,这里也应该指定为10028)。
(3)点击客户机程序界面上的“Connect”按钮,向服务器发送Socket连接请求。
(4)服务器侦听到有客户机的连接请求后便接受它(于是在两个程序之间就建立了一条可靠的Socket连接)。然后服务器会向客户机发送两次字符串数据。
(5)客户机接收到数据后,弹出两次如图3.4的消息框。

图3.4 TCP传输客户机接收到数据后显示的消息框

提示:TCPDemo为什么使用10028作为TCP通信的端口号?因为TCP数据包的TCP头结构中,使用了16位的域来表示一个端口号。因此,有65536个可能的端口号。不过,0-1023是周知口(众所周知的端口,比如80是超文本传输协议http的端口,25是简单邮件传输协议smtp的端口,20和21是文件传输协议ftp的端口等),比1023大的端口号通常被称为高端口号。应用程序一般使用高端口号提供自己的通信服务。TCPDemo使用10028端口是偶然的,只要比1023大就可以了。

TCPDemo在具体实现时,设计了一个CTCPListener类专门用于服务器对特定TCP端口的侦听。另外,设计了一个CStreamSocket类专门用于TCP数据的传输。CStreamSocket作为基类,服务器程序从它派生出另一个类CSocketSender专门用于数据的发送,客户机程序从它派生出CSocketReceiver类专门用于数据的接收。这些类的继承结构如图3.5。


图3.5 TCPDemo的类继承结构

提示:关于CMsgStation和CMsgReceiver两个类的功能介绍,请读者另行参考本书的“2.4.1 一种不错的设计模式”。

//
// CTCPListener.h
//

#ifndef __H_CTCPListener__
#define __H_CTCPListener__

#include "CMsgStation.h"

class CTCPListener : public CMsgStation
{
protected:
SOCKET mListener; // 用于侦听的Socket
SOCKET mAccepted; // 用于与远程端建立连接的Socket
WORD mListenPort; // 侦听端口号
BOOL mIsListening; // 是否正在侦听的标记
HANDLE mLsnThread; // 侦听线程
public:
CTCPListener();
virtual ~CTCPListener();
public:
// 设置/得到侦听的端口号
void SetListenPort(WORD inPort);
WORD GetListenPort(void);
// 创建/销毁用于侦听的Socket
BOOL Create(void);
void DeleteListener(void);
// 销毁服务器与客户机建立连接的Socket
void DeleteAccepted(void);
// 启动/停止侦听线程
BOOL StartListening(void);
void StopListening(void);
// 得到服务器与客户机建立连接的Socket(用于数据传输)
SOCKET GetAccepted(void);
private:
BOOL Accept(void); // 接受远程端的连接请求
static DWORD WINAPI ListeningThrd(void *pParam); // 侦听线程执行体
};

#endif // __H_CTCPListener__

//
// CTCPListener.cpp
//
#include "stdafx.h"
#include "CTCPListener.h"
#include "Netdefs.h"

#ifdef _DEBUG
#define new DEBUG_NEW
#undef THIS_FILE
static char THIS_FILE[] = __FILE__;
#endif
//////////////////////////////////////////////////////////////////////////////
CTCPListener::CTCPListener()
{
// 参数初始化
mListener = INVALID_SOCKET;
mAccepted = INVALID_SOCKET;
// 默认在10028端口上侦听
mListenPort = 10028;
mLsnThread = NULL;
mIsListening = FALSE;
}
CTCPListener::~CTCPListener()
{
// 销毁Socket
DeleteAccepted();
DeleteListener();
// 停止侦听线程
StopListening();
}
// 设置侦听端口号
void CTCPListener::SetListenPort(WORD inPort)
{
mListenPort = inPort;
}
// 得到侦听端口号
WORD CTCPListener::GetListenPort(void)
{
return mListenPort;
}
// 创建用于侦听的Socket
BOOL CTCPListener::Create(void)
{
DeleteListener(); // 销毁侦听Socket

int val = 0;
BOOL pass = FALSE;
// 创建一个TCP传输的Socket
mListener = socket(PF_INET, SOCK_STREAM, IPPROTO_TCP);
if (mListener != INVALID_SOCKET)
{
// 在Socket上进行参数设置
BOOL sopt = TRUE;
setsockopt(mListener, IPPROTO_TCP, TCP_NODELAY,
(char *)&sopt, sizeof(BOOL));
// 在销毁Socket时不必等待未发送完的数据完全发送出去
setsockopt(mListener, SOL_SOCKET, SO_DONTLINGER,
(char *)&sopt, sizeof(BOOL));
// 绑定Socket到指定的侦听端口
SOCKADDR_IN addr;
memset(&addr, 0, sizeof(SOCKADDR_IN));
addr.sin_family = AF_INET;
addr.sin_addr.s_addr = htonl(INADDR_ANY);
addr.sin_port = htons(mListenPort);
val = bind(mListener, (struct sockaddr*) &addr, sizeof(addr));
pass = (val != SOCKET_ERROR);
}
if (pass)
{
// 将Socket置于侦听状态
val = listen(mListener, SOMAXCONN);
pass = (val != SOCKET_ERROR);
}
if (!pass)
{
DeleteListener();
}
return pass;
}
// 销毁用于侦听的Socket
void CTCPListener::DeleteListener(void)
{
if (mListener != INVALID_SOCKET)
{
closesocket(mListener);
mListener = INVALID_SOCKET;
}
}
// 销毁服务器与客户机建立连接的Socket
void CTCPListener::DeleteAccepted(void)
{
if (mAccepted != INVALID_SOCKET)
{
closesocket(mAccepted);
mAccepted = INVALID_SOCKET;
}
}
// 启动侦听线程(因为用于接受连接请求的accept函数调用时会阻塞)
BOOL CTCPListener::StartListening(void)
{
// 如果侦听Socket没有创建,则创建它
if (mListener == INVALID_SOCKET)
{
Create();
}
if (mListener != INVALID_SOCKET)
{
if (mIsListening)
{
return TRUE;
}
// 启动侦听线程
DWORD threadID = 0;
mLsnThread = CreateThread(NULL, 0, ListeningThrd,
this, 0, &threadID);
return (mLsnThread != NULL);
}
return FALSE;
}
// 停止侦听线程
void CTCPListener::StopListening(void)
{
if (mListener != INVALID_SOCKET && mIsListening)
{
// 销毁侦听Socket,于是accept将脱离阻塞状态
DeleteListener();
// 等待侦听线程完全退出
if (mLsnThread != NULL)
{
WaitForSingleObject(mLsnThread, INFINITE);
mLsnThread = NULL;
}
}
}
// 接受远程端的连接请求(创建一个新的Socket用于与远程端建立一条连接)
BOOL CTCPListener::Accept(void)
{
if (mListener != INVALID_SOCKET)
{
SOCKADDR_IN saddr;
int len = sizeof(SOCKADDR_IN);
// 侦听远程端的连接请求(如果没有连接请求,这个函数将阻塞)
SOCKET accepted = accept(mListener, (SOCKADDR *)&saddr, &len);
if (accepted == INVALID_SOCKET)
{
return FALSE;
}
// 注意:目前仅支持建立一条Socket连接!
// 在建立新的连接之前将以前的连接断开
DeleteAccepted();
// 保存与远程端建立连接的Socket
mAccepted = accepted;
// 在Socket上设置一些参数
BOOL sopt = TRUE;
setsockopt(mAccepted, IPPROTO_TCP, TCP_NODELAY,
(char *)&sopt, sizeof(BOOL));
setsockopt(mAccepted, SOL_SOCKET, SO_DONTLINGER,
(char *)&sopt, sizeof(BOOL));
return TRUE;
}
return FALSE;
}
// 当与远程端连接的Socket取出之后,保存该Socket的变量置为无效
// 取出的Socket由取出者负责销毁
SOCKET CTCPListener::GetAccepted(void)
{
SOCKET ret = mAccepted;
mAccepted = INVALID_SOCKET;
return ret;
}
// 侦听线程的函数执行体
DWORD WINAPI CTCPListener::ListeningThrd(void *pParam)
{
ASSERT(pParam);
// 获得侦听对象指针
CTCPListener * pListen = (CTCPListener *) pParam;
pListen->mIsListening = TRUE;
while (pListen->mIsListening)
{
// 开始侦听(如果没有远程端发送连接请求,这个函数将阻塞)
if (!pListen->Accept())
{
pListen->mIsListening = FALSE;
break;
}
else
{
// const long cNewSocketAccepted = 6688;
// 发送给上层观察者一个自定义消息cNewSocketAccepted,
// 表示一条Socket连接已经建立(可以用它进行数据传输了!)
pListen->Broadcast(cNewSocketAccepted);
}
}

return 1;
}

//
// CStreamSocket.h
//

#ifndef __H_CStreamSocket__
#define __H_CStreamSocket__

class CStreamSocket
{
protected:
SOCKET mSocket; // 用于数据发送或接收的Socket
BOOL mIsConnected; // Socket是否已经建立了连接的标记
BOOL mIsReceiving; // 使用独立的线程进行数据接收
HANDLE mRcvThread;
BOOL mIsSending; // 使用独立的线程进行数据发送
HANDLE mSndThread;
public:
CStreamSocket();
virtual ~CStreamSocket();
public:
BOOL Attach(SOCKET inSocket); // 关联一个Socket
void Detach(void); // 销毁Socket
// 向指定IP地址、端口号的机器发送连接请求
BOOL ConnectTo(const char * inTarget, WORD inPort);
BOOL IsConnected(void) { return mIsConnected; };
// 用于数据接收的控制函数
BOOL StartReceiving(void);
void StopReceiving(void);
BOOL IsReceiving(void) { return mIsReceiving; };
// 用于数据发送的控制函数
BOOL StartSending(void);
void StopSending(void);
BOOL IsSending(void) { return mIsSending; };
protected:
static DWORD WINAPI ReceivingThrd(void * pParam); // 接收线程执行体
static DWORD WINAPI SendingThrd(void * pParam); // 发送线程执行体
// 接收/发送数据循环过程(虚函数,供子类定制)
virtual void ReceivingLoop(void);
virtual void SendingLoop(void);
};

#endif // __H_CStreamSocket__

//
// CStreamSocket.cpp
//

#include "stdafx.h"
#include "CStreamSocket.h"
#include "UNetwork.h"

#ifdef _DEBUG
#define new DEBUG_NEW
#undef THIS_FILE
static char THIS_FILE[] = __FILE__;
#endif
//////////////////////////////////////////////////////////////////////////////
CStreamSocket::CStreamSocket()
{
// 参数初始化
mSocket = INVALID_SOCKET;
mIsConnected = FALSE;
mIsReceiving = FALSE;
mIsSending = FALSE;
mRcvThread = NULL;
mSndThread = NULL;
}
// 销毁Socket,停止发送/接收线程
CStreamSocket::~CStreamSocket()
{
Detach();
StopSending();
StopReceiving();
}
// 关联一个Socket到本包装对象
BOOL CStreamSocket::Attach(SOCKET inSocket)
{
// 如果已经包装了一个Socket,则返回一个错误值
if (mSocket != INVALID_SOCKET)
{
return FALSE;
}
// 保存Socket句柄
mSocket = inSocket;
mIsConnected = TRUE;
return TRUE;
}
// 销毁Socket
void CStreamSocket::Detach(void)
{
if (mSocket != INVALID_SOCKET)
{
closesocket(mSocket);
mSocket = INVALID_SOCKET;
mIsConnected = FALSE;
}
}
// 向指定IP地址、端口号的机器发送连接请求
BOOL CStreamSocket::ConnectTo(const char * inTarget, WORD inPort)
{
if (mIsConnected)
{
return TRUE;
}
// 首先创建一个TCP传输的Socket
mSocket = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
if (mSocket != INVALID_SOCKET)
{
// 在成功创建的Socket上调整参数
BOOL sopt = TRUE;
setsockopt(mSocket, IPPROTO_TCP, TCP_NODELAY,
(char *)&sopt, sizeof(BOOL));
setsockopt(mSocket, SOL_SOCKET, SO_DONTLINGER,
(char *)&sopt, sizeof(BOOL));
// 向服务器发送连接请求
SOCKADDR_IN saddr;
memset(&saddr, 0, sizeof(SOCKADDR_IN));
saddr.sin_addr.S_un.S_addr = inet_addr(inTarget);
saddr.sin_family = AF_INET;
saddr.sin_port = htons((WORD)inPort);
if (connect(mSocket, (SOCKADDR *)&saddr, sizeof(SOCKADDR_IN)) != 0)
{
// 跟踪Socket错误
#ifdef _DEBUG
UNetwork::DumpSocketError();
#endif
// 如果连接失败,则销毁刚才创建的Socket
Detach();
return FALSE;
}
mIsConnected = TRUE;
return TRUE;
}
return FALSE;
}
// 启动数据接收线程(因为Socket数据接收函数调用时会阻塞)
BOOL CStreamSocket::StartReceiving(void)
{
if (mSocket != INVALID_SOCKET)
{
if (mIsReceiving)
{
return TRUE;
}
DWORD threadID = 0;
mRcvThread = CreateThread(NULL, 0, ReceivingThrd,
this, 0, &threadID);
return (mRcvThread != NULL);
}
return FALSE;
}
// 停止数据接收线程
void CStreamSocket::StopReceiving(void)
{
if (mIsReceiving)
{
// 销毁Socket,使接收函数失败或脱离阻塞
Detach();
// 等待数据接收线程的完全退出
if (mRcvThread != NULL)
{
WaitForSingleObject(mRcvThread, INFINITE);
mRcvThread = NULL;
}
}
}
// 启动数据发送线程(以提高数据发送的效率)
BOOL CStreamSocket::StartSending(void)
{
if (mSocket != INVALID_SOCKET)
{
if (mIsSending)
{
return TRUE;
}
DWORD threadID = 0;
mSndThread = CreateThread(NULL, 0, SendingThrd,
this, 0, &threadID);
return (mSndThread != NULL);
}
return FALSE;
}
// 停止数据发送线程
void CStreamSocket::StopSending(void)
{
if (mIsSending)
{
// 销毁Socket,使发送函数失败或脱离阻塞
Detach();
if (mSndThread != NULL)
{
// 等待数据发送线程的完全退出
WaitForSingleObject(mSndThread, INFINITE);
mSndThread = NULL;
}
}
}
// 数据接收线程的函数执行体
DWORD WINAPI CStreamSocket::ReceivingThrd(void * pParam)
{
CStreamSocket * pSock = (CStreamSocket *) pParam;
if (pSock)
{
pSock->mIsReceiving = TRUE;
// 执行接收循环
pSock->ReceivingLoop();
return 1;
}
return 0;
}
// 数据发送线程的函数执行体
DWORD WINAPI CStreamSocket::SendingThrd(void * pParam)
{
CStreamSocket * pSock = (CStreamSocket *) pParam;
if (pSock)
{
pSock->mIsSending = TRUE;
// 执行发送循环
pSock->SendingLoop();
return 1;
}
return 0;
}
// 虚函数,供子类定制实际的数据接收(循环)过程
void CStreamSocket::ReceivingLoop(void)
{
}
// 虚函数,供子类定制实际的数据发送(循环)过程
void CStreamSocket::SendingLoop(void)
{
}

//
// CSocketSender.h
//

#ifndef __H_CSocketSender__
#define __H_CSocketSender__

#include "CStreamSocket.h"

class CSocketSender : public CStreamSocket
{
public:
CSocketSender();
virtual ~CSocketSender();
protected:
virtual void SendingLoop(void); // 定制数据发送过程
};

#endif // __H_CSocketSender__

//
// CSocketSender.cpp
//
// 服务器程序定制的数据发送过程
void CSocketSender::SendingLoop(void)
{
char buf[1024]; // 发送数据使用的缓存
int bytes = 0;
// 定义一个字符串作为发送的数据内容
char str[] = "HQ Tech, Make Technology Easy!";
// 发送数据的总长度 = 字符串长度 + 头信息长度
int len = strlen(str) + sizeof(Net_Header);
// 在数据内容之前加上一个自定义头信息(用以说明数据内容的长度)
Net_Header * pHeader = (Net_Header *) buf;
pHeader->pack_size = strlen(str);
pHeader->my_hton(); // 字节顺序转换!
// 将欲发送的数据内容和头信息整合
strcpy(buf+sizeof(Net_Header), str);
// 作为演示,将上述定义的字符串数据发送两次
int counter = 2;
while (mIsSending)
{
// 使用Socket进行一次数据发送
bytes = send(mSocket, buf, len, 0);
if (bytes == SOCKET_ERROR)
{
Detach();
mIsSending = FALSE;
break;
}
// 当完成两次发送后断开Socket连接,结束发送线程
if (--counter == 0)
{
Detach();
mIsSending = FALSE;
break;
}
}
}

//
// CSocketReceiver.h
//

#ifndef __H_CSocketReceiver__
#define __H_CSocketReceiver__

#include "CStreamSocket.h"

class CSocketReceiver : public CStreamSocket
{
public:
CSocketReceiver();
virtual ~CSocketReceiver();
protected:
virtual void ReceivingLoop(void); // 定制数据接收过程
};

#endif // __H_CSocketReceiver__

//
// CSocketReceiver.cpp
//
// 客户机程序定制的数据接收过程
void CSocketReceiver::ReceivingLoop(void)
{
// 接收数据使用的缓存
char buf[1024];
int bytes = 0;
Net_Header * pHeader = (Net_Header *) buf;

while (mIsReceiving)
{
// 首先接收一个头信息(头信息内包含了随后的有效数据长度)
bytes = recv(mSocket, buf, sizeof(Net_Header), 0);
if (bytes == SOCKET_ERROR || bytes == 0)
{
Detach();
mIsReceiving = FALSE;
break;
}

pHeader->my_ntoh(); // 字节顺序转换!
// 继续读取后续的有效数据(即一个字符串内容)
bytes = recv(mSocket, buf, pHeader->pack_size, 0);
if (bytes == SOCKET_ERROR || bytes == 0)
{
Detach();
mIsReceiving = FALSE;
break;
}

buf[bytes] = '/0';
// 弹出一个消息框显示接收到的字符串内容
CString msg = "Received content:/n";
AfxMessageBox(msg + buf);
}
}

那么,TCPDemo是怎么来使用CTCPListener、CStreamSocket、CSocketSender、CSocketReceiver这几个类的呢?先来看服务器程序TCPServer。这是一个基于对话框的MFC程序。它在对话框类CTCPServerDlg中定义了两个成员:一个是CTCPListener类的实例,一个是CSocketSender类的实例。前者用于侦听客户机的连接请求,后者负责实际的Socket数据发送。然后,在主对话框的初始化函数中创建用于侦听的Socket,并启动侦听线程。当有客户机发出连接请求,并且服务器成功接受后,就启动数据发送线程真正开始数据的发送。

// TCPServerDlg.h : header file
//
class CTCPServerDlg : public CDialog, public CMsgReceiver
{
public:
CTCPServerDlg(CWnd* pParent = NULL);
protected:
HICON m_hIcon;

CTCPListener mListener; // 用于侦听客户机的连接请求
CSocketSender mNetSender; // 用于数据发送
// 自定义消息的处理函数
virtual bool ReceiveMessage(MessageT inMessage, void * ioParam,
void * ioParam2);
// 其它成员定义(省略)
// ……
};

// TCPServerDlg.cpp : implementation file
//
CTCPServerDlg::CTCPServerDlg(CWnd* pParent /*=NULL*/)
: CDialog(CTCPServerDlg::IDD, pParent)
{
//{{AFX_DATA_INIT(CTCPServerDlg)
mHostPort = 10028;
//}}AFX_DATA_INIT
// Note that LoadIcon does not require a subsequent DestroyIcon in Win32
m_hIcon = AfxGetApp()->LoadIcon(IDR_MAINFRAME);
}
// 主对话框的初始化函数
BOOL CTCPServerDlg::OnInitDialog()
{
CDialog::OnInitDialog();

// Set the icon for this dialog. The framework does this automatically
// when the application's main window is not a dialog
SetIcon(m_hIcon, TRUE); // Set big icon
SetIcon(m_hIcon, FALSE); // Set small icon

// 获取本地机器的IP地址、机器名,并在界面上显示
char hostName[100];
char hostIP[50];
if (UNetwork::GetHostInfo(hostIP, hostName))
{
mEditHostName.SetWindowText(hostName);
mEditHostIP.SetWindowText(hostIP);
}
// 主界面对象是mListener对象的观察者(因为它想获得Socket连接建立的通知)
mListener.AddMsgReceiver(this);
// 设置侦听端口号
mListener.SetListenPort(mHostPort);
// 创建侦听Socket,成功后启动一个侦听线程
if (mListener.Create())
{
mListener.StartListening();
}

return TRUE; // return TRUE unless you set the focus to a control
}
// 当接收到Socket连接已经建立的通知后,启动数据发送线程向客户机发送数据
bool CTCPServerDlg::ReceiveMessage(MessageT inMessage, void * ioParam, void * ioParam2)
{
if (inMessage == cNewSocketAccepted)
{
// 获取建立连接的Socket
mNetSender.Attach(mListener.GetAccepted());
// 启动数据发送线程
mNetSender.StartSending();
return true;
}

return CMsgReceiver::ReceiveMessage(inMessage, ioParam, ioParam2);
}

提示:使用MFC开发Socket程序时,一般要包含afxsock.h头文件(可以加在stdafx.h文件中)。程序运行之前,还要调用AfxSocketInit函数进行Socket函数库的初始化,实现如下:

//
// TCPServer.cpp
//
BOOL CTCPServerApp::InitInstance()
{
// --- Socket函数库的初始化 ---
// AfxSocketInit内部调用WSAStartup函数,
// 并且能够保证在程序退出之前自动调用WSACleanup函数!
if (!AfxSocketInit())
{
AfxMessageBox("Socket initializing failded!");
return FALSE;
}

// 创建主对话框
CTCPServerDlg dlg;
m_pMainWnd = &dlg;
int nResponse = dlg.DoModal();
if (nResponse == IDOK)
{
}
else if (nResponse == IDCANCEL)
{
}
return FALSE;
}

再来看客户机程序TCPClient的实现。这也是一个基于对话框的MFC程序。它在对话框类CTCPClientDlg中定义了一个是CSocketReceiver类的实例,专门用于向服务器发出连接请求,以及接收服务器发送过来的数据。

// TCPClientDlg.h : header file
//
class CTCPClientDlg : public CDialog
{
public:
CTCPClientDlg(CWnd* pParent = NULL);
protected:
HICON m_hIcon;
CSocketReceiver mNetReceiver; // 用于数据接收
// 其它成员定义(省略)
// ……
};

// TCPClientDlg.cpp : implementation file
//
CTCPClientDlg::CTCPClientDlg(CWnd* pParent /*=NULL*/)
: CDialog(CTCPClientDlg::IDD, pParent)
{
//{{AFX_DATA_INIT(CTCPClientDlg)
mTargetIP = _T("127.0.0.1");
mTargetPort = 10028;
//}}AFX_DATA_INIT
// Note that LoadIcon does not require a subsequent DestroyIcon in Win32
m_hIcon = AfxGetApp()->LoadIcon(IDR_MAINFRAME);
}
// 主对话框的初始化函数
BOOL CTCPClientDlg::OnInitDialog()
{
CDialog::OnInitDialog();

// Set the icon for this dialog. The framework does this automatically
// when the application's main window is not a dialog
SetIcon(m_hIcon, TRUE); // Set big icon
SetIcon(m_hIcon, FALSE); // Set small icon

// 获取本地机器的IP地址、机器名,并在界面上显示
char hostName[100];
char hostIP[50];
if (UNetwork::GetHostInfo(hostIP, hostName))
{
mEditHostName.SetWindowText(hostName);
mEditHostIP.SetWindowText(hostIP);
}

return TRUE; // return TRUE unless you set the focus to a control
}
// 界面上的“Connect”按钮的响应函数
void CTCPClientDlg::OnButtonConnect()
{
// 从界面上获取最新的数据
UpdateData(TRUE);
// 向指定IP地址、端口号的服务器发出连接请求
if (mNetReceiver.ConnectTo(mTargetIP, mTargetPort))
{
// 连接成功后启动一个子线程用于数据接收
mNetReceiver.StartReceiving();
}
else
{
// 弹出连接失败的消息框
CString msg;
msg.Format("Connecting to %s:%d failed!", mTargetIP, mTargetPort);
AfxMessageBox(msg);
}
}

TCPServer和TCPClient两个程序的整个交互过程如图3.6。

图3.6 TCPServer和TCPClient的交互过程

值得注意的是,TCP传输的是一种字节流数据。在应用程序中,有时需要对这些数据进行一些控制,或者获得一些说明。于是,TCPServer和TCPClient两个程序在进行TCP通信的时候,定义了一个简单的应用协议,即TCPServer发出的数据总是使用一个“负载头+负载数据”的结构。其中,负载数据是真正需要传输的数据内容,而负载头是对负载数据的一个说明,指示了负载数据的实际长度。这个负载头的定义如下:

//
// Netdefs.h
//
struct Net_Header
{
unsigned long pack_size; // 使用一个无符号整型变量,说明负载数据的长度

// 将本结构的变量值,从主机字节顺序转换成网络字节顺序
void my_hton(void)
{
pack_size = htonl(pack_size);
};
// 将本结构的变量值,从网络字节顺序转换成主机字节顺序
void my_ntoh(void)
{
pack_size = ntohl(pack_size);
};
};


小知识:字节顺序

很少有人关心字节顺序(Byte Ordering),因为它真的很少用到。何为字节顺序呢?让我们先来看一个例子,假设现在有一个WORD类型的变量,它的值为0x7788,那么它在内存中是怎么存放的呢?

图3.7 两种字节顺序

事实上,对于不同的CPU、不同的操作系统,图3.7中的两种字节顺序都是可能的。如果像图3.7左边那样:高字节在前,低字节在后,则这种字节顺序称作为big-endian;如果像图3.7右边那样:低字节在前,高字节在后,则这种字节顺序称作为little-endian。

表3.1 常见的CPU、操作系统上使用的字节顺序

CPU 操作系统 字节顺序
x86 (Intel、AMD等) 所有 little-endian
DEC Alpha 所有 little-endian
HP-PA NT little-endian
HP-PA UNIX big-endian
SUN SPARC 所有 big-endian
MIPS NT little-endian
MIPS UNIX big-endian
PowerPC NT little-endian
PowerPC 非NT big-endian
RS/6000 UNIX big-endian
Motorola m68k 所有 big-endian

一般来说,我们不用关心字节顺序问题,除非要涉及到跨平台的通信和资源共享,比如本章将要介绍的网络编程(网络传输协议TCP/IP采用的是big-endian)。假设现在要在使用不同字节顺序的机器之间传输和交换数据,那该怎么办呢?(同样的数据,不同的机器可能有不同的理解,岂不是有悖初衷!)有两种方法,一种是全部转换成文本来传输,另一种是双方都按照某一方的字节顺序来传输(这时就有一个不同字节顺序之间的相互转换问题)。

Socket编程中经常采用第二种方法。整个传输过程如下:发送端将本机的数据转换成网络的字节顺序(调用API函数htonl或htons),然后发送;接收端收到网络数据后,先将数据转换成本机的字节顺序(调用API函数ntohl或ntohs),然后再进行其它操作——如此就能保证“会议精神”在通信双方的正确传达了!这个过程中用到的几个API函数:ntohl、htonl、ntohs、htons,名字都差不多,很难区分。但是如果知道了它们的来历,问题也就不存在了:n是network,网络的意思;h是host,本地主机的意思。ntohl,就是将32位的u_long类型的数据从网络字节顺序转换成本机字节顺序(htonl的字节顺序转换过程与ntohl相反);ntohs,就是将16位的u_short类型的数据从网络字节顺序转换成本机字节顺序(htons的字节顺序转换过程与ntohs相反)。

最后还有一个小问题:如何知道本机的字节顺序呢?有个很简单的方法,如下:
BOOL IsLittleEndian(void)
{
WORD wValue = 0x5678;
return (*((BYTE*)&wValue) == 0x78);
}

另外,TCPServer程序和TCPClient程序在实现时都用到了一个工具类UNetwork。这个类实现了两个静态成员函数:GetHostInfo和DumpSocketError。前者用于获取本地主机的IP地址、机器名等信息,后者用于程序调试时跟踪Socket错误。特别是DumpSocketError函数,非常实用。因为Socket程序的调试一般都比较麻烦,这时DumpSocketError函数就能将整型Socket错误码转换成容易理解的字符串说明形式输出,非常方便!

//
// UNetwork.cpp
//
// 获取本地主机的IP地址和机器名
BOOL UNetwork::GetHostInfo(char * outIP, char * outName)
{
char name[300];
// 获取主机名
if (gethostname(name, 300) == 0)
{
if (outName)
{
strcpy(outName, name);
}
// 获取主机的IP地址
PHOSTENT hostinfo;
if ((hostinfo = gethostbyname(name)) != NULL)
{
LPCSTR pIP = inet_ntoa (*(struct in_addr *)*hostinfo->h_addr_list);
strcpy(outIP, pIP);
return TRUE;
}
}
return FALSE;
}
// 将整型的Socket错误码转换成字符串说明形式输出
void UNetwork::DumpSocketError(void)
{
switch (WSAGetLastError())
{
case WSANOTINITIALISED:
TRACE("A successful WSAStartup call must occur before using this function. ");
break;
case WSAENETDOWN:
TRACE("The network subsystem has failed. ");
break;
case WSAEACCES:
TRACE("The requested address is a broadcast address, but the appropriate flag was not set. Call setsockopt with the SO_BROADCAST parameter to allow the use of the broadcast address. ");
break;
case WSAEINVAL:
TRACE("An unknown flag was specified, or MSG_OOB was specified for a socket with SO_OOBINLINE enabled. ");
break;
case WSAEINTR:
TRACE("A blocking Windows Sockets 1.1 call was canceled through WSACancelBlockingCall. ");
break;
case WSAEINPROGRESS:
TRACE("A blocking Windows Sockets 1.1 call is in progress, or the service provider is still processing a callback function. ");
break;
case WSAEFAULT:
TRACE("The buf or to parameters are not part of the user address space, or the tolen parameter is too small. ");
break;
case WSAENETRESET:
TRACE("The connection has been broken due to keep-alive activity detecting a failure while the operation was in progress. ");
break;
case WSAENOBUFS:
TRACE("No buffer space is available. ");
break;
case WSAENOTCONN:
TRACE("The socket is not connected (connection-oriented sockets only). ");
break;
case WSAENOTSOCK:
TRACE("The descriptor is not a socket. ");
break;
case WSAEOPNOTSUPP:
TRACE("MSG_OOB was specified, but the socket is not stream-style such as type SOCK_STREAM, OOB data is not supported in the communication domain associated with this socket, or the socket is unidirectional and supports only receive operations. ");
break;
case WSAESHUTDOWN:
TRACE("The socket has been shut down; it is not possible to sendto on a socket after shutdown has been invoked with how set to SD_SEND or SD_BOTH. ");
break;
case WSAEWOULDBLOCK:
TRACE("The socket is marked as nonblocking and the requested operation would block. ");
break;
case WSAEMSGSIZE:
TRACE("The socket is message oriented, and the message is larger than the maximum supported by the underlying transport. ");
break;
case WSAEHOSTUNREACH:
TRACE("The remote host cannot be reached from this host at this time. ");
break;
case WSAECONNABORTED:
TRACE("The virtual circuit was terminated due to a time-out or other failure. The application should close the socket as it is no longer usable. ");
break;
case WSAECONNRESET:
TRACE("The virtual circuit was reset by the remote side executing a hard or abortive close. For UPD sockets, the remote host was unable to deliver a previously sent UDP datagram and responded with a /"Port Unreachable/" ICMP packet. The application should close the socket as it is no longer usable. ");
break;
case WSAEADDRNOTAVAIL:
TRACE("The remote address is not a valid address, for example, ADDR_ANY. ");
break;
case WSAEAFNOSUPPORT:
TRACE("Addresses in the specified family cannot be used with this socket. ");
break;
case WSAEDESTADDRREQ:
TRACE("A destination address is required. ");
break;
case WSAENETUNREACH:
TRACE("The network cannot be reached from this host at this time. ");
break;
case WSAETIMEDOUT:
TRACE("The connection has been dropped, because of a network failure or because the system on the other end went down without notice. ");
break;
default:
TRACE("Unknown socket error. ");
break;
}
}

提示:本书配套光盘的SourceCodes/Chapter03/TCPDemo目录下提供了TCP传输演示程序的完整实现。其中,TCPServer为服务器程序,TCPClient为客户机程序。打开WsClientServer.dsw可以同时浏览TCPServer和TCPClient两个项目。

3.1.2 UDP传输

UDP,User Datagram Protocol的缩写(用户数据报协议),是一种无连接的网络传输协议。UDP协议提供的是一种基本的、低延时的称为数据报的传输服务。UDP传输没有像TCP传输一样需要预先建立一条连接;UDP没有计时机制、流控或拥塞管理机制,由于某种原因造成丢失的数据报也不会被重传。因此,UDP提供的是一种不可靠的应用数据传输服务。

提示:TCP与UDP之间的主要差别在于可靠性。但也不是说,因为UDP是一种不可靠的传输协议而一无用处。在一个良好的网络环境下(比如局域网内),使用UDP传输数据还是相当可靠的,而且效率比较高。UDP(比TCP)更适合于与时间相关的应用数据的传输。

本节将要实现一个UDP传输的演示程序UDPDemo,它包括服务器和客户机两个部分。它们的程序界面如图3.8。


图3.8 UDP传输演示程序界面

UDPDemo的演示过程如下:

(1)将服务器和客户机两部分程序都运行起来(此时服务器启动了用于接收10025端口数据的子线程,而客户机也启动了用于接收10026端口数据的子线程)。
(2)在客户机程序界面上输入服务器的IP地址(如果服务器和客户机运行在同一台机器上,IP地址可以指定为127.0.0.1)、数据接收的端口号(因为服务器在10025端口上接收数据,这里也应该指定为10025)。
(3)在客户机程序界面上编辑欲发送消息的内容,然后点击“Send”按钮,向服务器发出UDP数据。
(4)服务器接收到客户机发送过来的数据后就弹出如图3.9左边的消息框。随后,服务器向客户机发送一个反馈消息。
(5)客户机接收到服务器发送过来的反馈数据后也弹出一个消息框,如图3.9右边的那个。

图3.9 UDP传输过程中显示的一对消息框

UDPDemo在具体实现时,设计了一个CUDPManager类专门负责UDP数据的发送和接收(数据接收使用一个独立的子线程)。CUDPManager类的定义和实现如下:

//
// CUDPManager.h
//

#ifndef __H_CUDPManager__
#define __H_CUDPManager__

class CUDPManager
{
private:
SOCKET mSckReceiver; // 用于接收的Socket
SOCKET mSckSender; // 用于发送的Socket
DWORD mTargetIP; // 远程端IP地址(使用主机字节顺序)
WORD mTargetPort; // 远程端口号
WORD mLocalPort; // 本地端口号
BOOL mIsReceiving; // 正在接收数据的标记
HANDLE mRcvThread; // 数据接收线程句柄
public:
CUDPManager();
~CUDPManager();
// 设置/获取远程端的IP地址
void SetTargetIP(DWORD inIP);
DWORD GetTargetIP(void);
void SetTargetIP(const char * inIP);
void GetTargetIP(char * outIP);
// 设置/获取远程端口号
void SetTargetPort(WORD inPort);
WORD GetTargetPort(void);
// 设置/获取本地端口号
void SetLocalPort(WORD inPort);
WORD GetLocalPort(void);
// 创建/销毁用于发送的Socket
BOOL CreateSender(void);
void DeleteSender(void);
// 创建/销毁用于接收的Socket
BOOL CreateReceiver(void);
void DeleteReceiver(void);
// 使用UDP协议发送数据的两个函数
BOOL Send(const char * inBuffer, long inLength);
BOOL SendTo(const char * inBuffer, long inLength,
DWORD inIP, WORD inPort);
// 启动/停止数据接收线程
BOOL StartReceiving(void);
void StopReceiving(void);
private:
void ReceivingLoop(void); // 数据接收循环过程
static DWORD WINAPI ReceivingThrd(void * pParam); // 接收线程执行体
};

#endif // __H_CUDPManager__

//
// CUDPManager.cpp
//

#include "stdafx.h"
#include "CUDPManager.h"
#include "UNetwork.h"

#ifdef _DEBUG
#define new DEBUG_NEW
#undef THIS_FILE
static char THIS_FILE[] = __FILE__;
#endif

/////////////////////////////////////////////////////////////////////////////
CUDPManager::CUDPManager()
{
// 参数初始化
mSckReceiver = INVALID_SOCKET;
mSckSender = INVALID_SOCKET;
mTargetIP = 0x7f000001; // 127.0.0.1
mTargetPort = 10080;
mLocalPort = 10080;
mIsReceiving = FALSE;
mRcvThread = NULL;
}
CUDPManager::~CUDPManager()
{
// 销毁所有使用过的Socket
DeleteSender();
DeleteReceiver();
StopReceiving();
}
// 设置远程端的IP地址,参数是DWORD类型
void CUDPManager::SetTargetIP(DWORD inIP)
{
mTargetIP = inIP;
}
// 得到远程端的IP地址
DWORD CUDPManager::GetTargetIP(void)
{
return mTargetIP;
}
// 重载函数:设置远程端的IP地址,参数是字符串类型
void CUDPManager::SetTargetIP(const char * inIP)
{
// 将IP地址从字符串形式转换成DWORD类型(使用主机字节顺序)
mTargetIP = ntohl(inet_addr(inIP));
}
// 重载函数:得到远程端的字符串形式的IP地址
void CUDPManager::GetTargetIP(char * outIP)
{
if (outIP)
{
// 将IP地址从DWORD类型转换成字符串形式
struct in_addr in;
in.S_un.S_addr = htonl(mTargetIP);
char * pStr = inet_ntoa(in);
strcpy(outIP, pStr);
}
}
// 设置远程端口号
void CUDPManager::SetTargetPort(WORD inPort)
{
mTargetPort = inPort;
}
// 得到远程端口号
WORD CUDPManager::GetTargetPort(void)
{
return mTargetPort;
}
// 设置本地端口号
void CUDPManager::SetLocalPort(WORD inPort)
{
mLocalPort = inPort;
}
// 得到本地端口号
WORD CUDPManager::GetLocalPort(void)
{
return mLocalPort;
}
// 创建用于发送的Socket
BOOL CUDPManager::CreateSender(void)
{
DeleteSender();
// 创建一个UDP传输的Socket
mSckSender = socket(AF_INET, SOCK_DGRAM, 0);
if (mSckSender != INVALID_SOCKET)
{
return TRUE;
}
return FALSE;
}
// 销毁用于发送的Socket
void CUDPManager::DeleteSender(void)
{
if (mSckSender != INVALID_SOCKET)
{
closesocket(mSckSender);
mSckSender = INVALID_SOCKET;
}
}
// 创建用于接收的Socket
BOOL CUDPManager::CreateReceiver(void)
{
DeleteReceiver();
// 创建一个UDP传输的Socket
mSckReceiver = socket(AF_INET, SOCK_DGRAM, 0);
if (mSckReceiver != INVALID_SOCKET)
{
// 在Socket上设置参数:允许地址复用
BOOL flag = TRUE;
int ret = setsockopt(mSckReceiver, SOL_SOCKET, SO_REUSEADDR,
(char *) &flag, sizeof(flag));
if (ret == SOCKET_ERROR)
{
DeleteReceiver();
return FALSE;
}
// 将Socket绑定到本地端口号上
SOCKADDR_IN addr;
addr.sin_family = AF_INET;
addr.sin_addr.s_addr = htonl(INADDR_ANY);
addr.sin_port = htons(mLocalPort);
ret = bind(mSckReceiver, (struct sockaddr*) &addr, sizeof(addr));
if (ret == SOCKET_ERROR)
{
DeleteReceiver();
return FALSE;
}
return TRUE;
}
return FALSE;
}
// 销毁用于接收的Socket
void CUDPManager::DeleteReceiver(void)
{
if (mSckReceiver != INVALID_SOCKET)
{
closesocket(mSckReceiver);
mSckReceiver = INVALID_SOCKET;
}
}
// 使用已经创建好的用于发送的Socket发送数据
BOOL CUDPManager::Send(const char * inBuffer, long inLength)
{
SOCKADDR_IN addr;
memset((char *) &addr, 0, sizeof(addr));
addr.sin_family = AF_INET;
addr.sin_addr.s_addr = htonl(mTargetIP);
addr.sin_port = htons(mTargetPort);
// 使用Socket发送数据
int val = sendto(mSckSender, inBuffer, inLength, 0,
(sockaddr *) &addr, sizeof(addr));
return (val != SOCKET_ERROR);
}
// 创建一个新的Socket,并用它将数据发送到指定的IP地址、端口号上
BOOL CUDPManager::SendTo(const char * inBuffer, long inLength, DWORD inIP, WORD inPort)
{
// 创建一个UDP传输的Socket
SOCKET sock = socket(AF_INET, SOCK_DGRAM, 0);
if (sock != INVALID_SOCKET)
{
SOCKADDR_IN addr;
memset((char *) &addr, 0, sizeof(addr));
addr.sin_family = AF_INET;
addr.sin_addr.s_addr = htonl(inIP);
addr.sin_port = htons(inPort);
// 发送数据
int val = sendto(sock, inBuffer, inLength, 0,
(sockaddr *) &addr, sizeof(addr));
if (val == SOCKET_ERROR)
{
// 跟踪Socket错误
#ifdef _DEBUG
UNetwork::DumpSocketError();
#endif
}
// 发送完成后销毁该Socket
closesocket(sock);
return (val != SOCKET_ERROR);
}
return FALSE;
}
// 启动数据接收线程(因为调用recvfrom函数接收UDP数据时会阻塞)
BOOL CUDPManager::StartReceiving(void)
{
if (mSckReceiver == INVALID_SOCKET)
{
CreateReceiver();
}

if (mSckReceiver != INVALID_SOCKET)
{
if (mIsReceiving)
{
return TRUE;
}
DWORD threadID = 0;
mRcvThread = CreateThread(NULL, 0, ReceivingThrd,
this, 0, &threadID);
return (mRcvThread != NULL);
}
return FALSE;
}
// 停止数据接收线程
void CUDPManager::StopReceiving(void)
{
if (mIsReceiving)
{
// 销毁Socket以使接收函数失败或脱离阻塞
DeleteReceiver();
// 等待接收线程完全退出
if (mRcvThread != NULL)
{
WaitForSingleObject(mRcvThread, INFINITE);
mRcvThread = NULL;
}
}
}
// 线程函数执行体:调用本类的ReceivingLoop函数
DWORD WINAPI CUDPManager::ReceivingThrd(void * pParam)
{
ASSERT(pParam);
CUDPManager * pController = (CUDPManager*) pParam;
pController->ReceivingLoop();
return 0;
}
// 数据接收过程
void CUDPManager::ReceivingLoop(void)
{
struct sockaddr_in addr_cli;
int addr_cli_len = sizeof(addr_cli);
char buffer[1024]; // 发送数据缓存
long bytes = 0;

mIsReceiving = TRUE;
while (mIsReceiving)
{
// 等待接收数据
int addr_cli_len = sizeof(addr_cli);
bytes = recvfrom(mSckReceiver, (char *)buffer, 1024, 0,
(LPSOCKADDR) &addr_cli, (int *) &addr_cli_len);
if (bytes == SOCKET_ERROR || bytes == 0)
{
// 如果Socket发送错误或者Socket断开,则跳出循环
mIsReceiving = FALSE;
}
else
{
buffer[bytes] = '/0';
// 获取远程端的IP地址
char * pStr = inet_ntoa(addr_cli.sin_addr);
// 检查标记:是否需要发出反馈消息?
// 作为演示,发送的UDP数据包第一个字节用于指示是否需要反馈:
// 1表示需要反馈,0表示不需反馈
if (buffer[0] == '1')
{
// 向远程端发出一个反馈消息,第一个字节指定为0,
// 表示不再需要远程端反馈,否则通信双方的数据传输永无休止!
CString str = "0Received OK." ;
SendTo(str,str.GetLength()+1,ntohl(inet_addr(pStr)),10026);
}
// 弹出一个对话框显示接收到的数据内容
CString msg;
msg.Format("Receive from %s /nContent:%s", pStr, buffer+1);
AfxMessageBox(msg);
}
}
}

再来看服务器程序UDPServer和客户机程序UDPClient的具体实现。它们都是基于对话框的MFC程序,并且分别在各自的主对话框类中定义了一个CUDPManager类的实例。然后就是在对话框的初始化函数中进行一系列的操作,包括创建UDP传输用的Socket、启动数据接收线程等。

// UDPServerDlg.h : header file
//
class CUDPServerDlg : public CDialog
{
protected:
CUDPManager mUDPManager;
// 其它成员定义(省略)
// ……
};

// UDPServerDlg.cpp : implementation file
//
// 服务器程序主对话框的初始化
BOOL CUDPServerDlg::OnInitDialog()
{
CDialog::OnInitDialog();

// Set the icon for this dialog. The framework does this automatically
// when the application's main window is not a dialog
SetIcon(m_hIcon, TRUE); // Set big icon
SetIcon(m_hIcon, FALSE); // Set small icon

// 获取本地机器的IP地址、机器名,并在界面上显示
char hostName[100];
char hostIP[50];
if (UNetwork::GetHostInfo(hostIP, hostName))
{
mEditHostName.SetWindowText(hostName);
mEditHostIP.SetWindowText(hostIP);
}
// 服务器在10025端口上接收数据
mServerInfo.Format("This is a UDP server, listening to Port 10025.");
UpdateData(FALSE);
// 设置服务器接收数据用的端口号,然后创建接收用的Socket
mUDPManager.SetLocalPort(10025);
if (mUDPManager.CreateReceiver())
{
// 启动数据接收线程
mUDPManager.StartReceiving();
}

return TRUE; // return TRUE unless you set the focus to a control
}

// UDPClientDlg.h : header file
//
class CUDPClientDlg : public CDialog
{
protected:
CUDPManager mUDPManager;
// 其它成员定义(省略)
// ……
};

// UDPClientDlg.cpp : implementation file
//
// 客户机程序主对话框的初始化
BOOL CUDPClientDlg::OnInitDialog()
{
CDialog::OnInitDialog();

// Set the icon for this dialog. The framework does this automatically
// when the application's main window is not a dialog
SetIcon(m_hIcon, TRUE); // Set big icon
SetIcon(m_hIcon, FALSE); // Set small icon

// 获取本地机器的IP地址、机器名,并在界面上显示
char hostName[100];
char hostIP[50];
if (UNetwork::GetHostInfo(hostIP, hostName))
{
mEditHostName.SetWindowText(hostName);
mEditHostIP.SetWindowText(hostIP);
}
// 客户机在10026端口上接收数据
mUDPManager.SetLocalPort(10026);
// 创建接收用的Socket
if (mUDPManager.CreateReceiver())
{
// 启动数据接收线程
mUDPManager.StartReceiving();
}
// 创建发送用的Socket
mUDPManager.CreateSender();

return TRUE; // return TRUE unless you set the focus to a control
}
// 客户机界面上的“Send”按钮的响应函数
void CUDPClientDlg::OnButtonSend()
{
UpdateData(TRUE);
// 设置远程端的IP地址、数据接收端口号
mUDPManager.SetTargetIP(mTargetIP);
mUDPManager.SetTargetPort((WORD)mTargetPort);
// 向远程端发送数据
// 发送数据的第一个字节指定为1,表示要求远程端接收到数据后发回一个反馈消息
mUDPManager.Send("1" + mMsg2Send, mMsg2Send.GetLength() + 1);
}

提示:本书配套光盘的SourceCodes/Chapter03/UDPDemo目录下提供了UDP传输演示程序的完整实现。其中,UDPServer为服务器程序,UDPClient为客户机程序。打开WsClientServer.dsw可以同时浏览UDPServer和UDPClient两个项目。

3.1.2 IP组播技术

组播技术被认为是WWW技术推广之后出现的最激动人心的网络技术之一。组播是一种允许一个或多个发送者发送单一的数据包到多个接收者的网络技术。组播源把数据包发送到特定的组播组(Multicast Group),而只有加入到该组播组的主机才能接收到这些数据包。组播可以大大的节省网络带宽,因为无论有多少个目标地址,在整个网络的任何一条链路上只传送单一的数据包。单播与组播的数据传送过程区别如图3.10。

图3.10 单播与组播的数据传送过程

单播(Unicast)传输:在发送者和每一接收者之间实现点对点的网络连接。如果一个发送者同时给多个接收者传输相同的数据,则必须相应地将数据包复制成多份后再分别投递。如果有大量主机希望获得数据包的同一份拷贝,将导致发送者负担沉重、延时长、网络拥塞;为保证一定的服务质量需增加硬件和带宽。

组播(Multicast)传输:在发送者和每一接收者之间实现一点对多点的网络连接。如果一个发送者同时给多个接收者传输相同的数据,只需投递一份数据包就可以了。组播提高了数据的传送效率,减少了骨干网络出现拥塞的可能性。

广播(Broadcast)传输:是指在IP子网内广播数据包,所有在子网内部的主机都将收到这些数据包,不管它们是不是否乐于接收。广播的使用范围非常小,只在本地子网内有效,因为路由器通常会封锁广播通信。广播传输会增加非接收者的开销。

目前,使用得最为广泛的组播技术是IP组播技术。IP组播技术是一种为优化使用网络资源而产生的技术,通常用于多点工作方式下的应用程序中,它是标准IP网络层协议技术的一个扩展。从Steve Deering于1989年提出的RFC 1112(“Host Extensions for IP Multicasting”)中的定义可以得知,IP组播的核心思想是:通过一个IP地址向一组主机发送数据(UDP包);发送者仅仅向一个组地址发送信息,接收者只需加入到这个分组就可以接收信息;所有的接收者接收的是同一个数据流;组中成员是动态的,可以根据自己的意愿随时随意加入或退出;每一台主机都可以同时加入到多个组中,每一个组播地址可以在不同的端口或者不同的Socket上有多个数据流,同时许多实际应用可以共享一个组地址。IP组播技术可以有效地避免重复发送可能引起的广播风暴,并且能够突破路由器的限制,将数据包传送到其它网段。

IP地址方案专门为组播划出了一个地址范围,在IPv4中为D类地址,范围是224.0.0.0到239.255.255.255,并将D类地址划分为局部链接组播地址、预留组播地址、管理权限组播地址等,分配如下:
局部链接地址:224.0.0.0~224.0.0.255,用于局域网,路由器不转发此范围内的IP包。  
预留组播地址:224.0.1.0~238.255.255.255,用于全球范围或网络协议。
管理权限地址:239.0.0.0~239.255.255.255,组织内部使用,用于限制组播范围。
从多媒体应用的角度来看,IP组播技术的使用对于网络视频的多点实时传输、网络多点实时监控具有特别重要的意义。本节将实现一个IP组播的演示程序MulticastDemo,它包括服务器和客户机两个部分。它们的程序界面如图3.11。


图3.11 组播传输演示程序界面

MulticastDemo的演示过程如下:

(1)将服务器和客户机两部分程序都运行起来。为了演示组播特性,客户机程序可以运行多个进程,各个进程以PID来区别;它们都加入到同一个组播组239.8.8.8,并且各自启动了一个数据接收线程。
(2)在服务器程序界面上编辑欲发送的消息内容,然后点击“Send”按钮发送数据。
(3)所有运行起来的客户机程序进程都将接收到服务器发出的信息,并且弹出如图3.12的消息框。(假设运行了两个客户机程序进程,它们的PID分别是1608和1716。)

图3.12 组播传输客户机进程接收到数据后显示的消息框

提示:进行组播演示之前,请确认你的机器安装有网卡,并且局域网连接正常。

组播实际上使用了UDP协议进行数据的传输。MulticastDemo在具体实现时,设计了一个CMulticastAdmin类专门负责组播数据的发送和接收(数据接收使用一个独立的子线程)。(CMulticastAdmin类实际上是由CUDPManager 类改写过来的,这两个类的实现非常的类似。)CMulticastAdmin类的定义和实现如下:

//
// CMulticastAdmin.h
//

#ifndef __H_CMulticastAdmin__
#define __H_CMulticastAdmin__

class CMulticastAdmin
{
private:
SOCKET mSckReceiver; // 接收用的Socket
SOCKET mMulticaster; // 组播发送用的Socket
DWORD mMulticastIP; // 组播IP地址(使用主机字节顺序)
WORD mMulticastPort; // 组播的端口号
BOOL mIsReceiving; // 数据正在接收的标记
HANDLE mRcvThread; // 数据接收线程的句柄
public:
CMulticastAdmin();
~CMulticastAdmin();
// 设置/获取组播IP地址
void SetMulticastIP(DWORD inIP);
DWORD GetMulticastIP(void);
void SetMulticastIP(const char * inIP);
void GetMulticastIP(char * outIP);
// 设置/获取组播端口号
void SetMulticastPort(WORD inPort);
WORD GetMulticastPort(void);
// 创建/销毁组播用的Socket
BOOL CreateMulticaster(void);
void DeleteMulticaster(void);
// 创建/销毁接收用的Socket
BOOL CreateReceiver(void);
void DeleteReceiver(void);
// (组播)发送数据
BOOL Multicast(const char * inBuffer, long inLength);
// 启动/停止数据接收线程
BOOL StartReceiving(void);
void StopReceiving(void);
private:
void ReceivingLoop(void); // 数据接收过程
static DWORD WINAPI ReceivingThrd(void * pParam); // 接收线程执行体
};

#endif // __H_CMulticastAdmin__

//
// CMulticastAdmin.cpp
//

#include "stdafx.h"
#include "CMulticastAdmin.h"

#ifdef _DEBUG
#define new DEBUG_NEW
#undef THIS_FILE
static char THIS_FILE[] = __FILE__;
#endif

/////////////////////////////////////////////////////////////////////////////
CMulticastAdmin::CMulticastAdmin()
{
// 参数初始化
mSckReceiver = INVALID_SOCKET;
mMulticaster = INVALID_SOCKET;
// Multicast IP: from 224.0.0.0 to 239.255.255.255
mMulticastIP = 0xef080808; // 239.8.8.8
mMulticastPort = 10018;
mIsReceiving = FALSE;
mRcvThread = NULL;
}
CMulticastAdmin::~CMulticastAdmin()
{
// 销毁所有使用过的Socket
DeleteMulticaster();
DeleteReceiver();
StopReceiving();
}
// 设置组播IP地址
void CMulticastAdmin::SetMulticastIP(DWORD inIP)
{
mMulticastIP = inIP;
}
// 获取组播IP地址
DWORD CMulticastAdmin::GetMulticastIP(void)
{
return mMulticastIP;
}
// 设置组播IP地址
void CMulticastAdmin::SetMulticastIP(const char * inIP)
{
mMulticastIP = ntohl(inet_addr(inIP));
}
// 获取组播IP地址
void CMulticastAdmin::GetMulticastIP(char * outIP)
{
if (outIP)
{
struct in_addr in;
in.S_un.S_addr = htonl(mMulticastIP);
char * pStr = inet_ntoa(in);
strcpy(outIP, pStr);
}
}
// 设置组播端口号
void CMulticastAdmin::SetMulticastPort(WORD inPort)
{
mMulticastPort = inPort;
}
// 获取组播端口号
WORD CMulticastAdmin::GetMulticastPort(void)
{
return mMulticastPort;
}
// 创建组播发送用的Socket
BOOL CMulticastAdmin::CreateMulticaster(void)
{
DeleteMulticaster();
// 初始化Socket函数库:使用2.2版本的WinSock DLL
WSADATA data;
int ret = WSAStartup(0x0202, &data);
if (ret != 0)
{
WSACleanup();
return FALSE;
}
// 创建一个使用UDP协议传输的Socket
mMulticaster = socket(AF_INET, SOCK_DGRAM, 0);
if (mMulticaster == INVALID_SOCKET)
{
WSACleanup();
return FALSE;
}
// 在Socket上设置参数:允许地址复用
BOOL flag = TRUE;
ret = setsockopt(mMulticaster, SOL_SOCKET, SO_REUSEADDR,
(char *)&flag, sizeof(flag));
// 绑定Socket到组播端口号上
SOCKADDR_IN addr;
ZeroMemory(&addr, sizeof(addr));
addr.sin_family = AF_INET;
addr.sin_addr.s_addr = INADDR_ANY; // 不关心网卡地址
addr.sin_port = htons(mMulticastPort);
ret = bind(mMulticaster, (struct sockaddr*) &addr, sizeof(addr));
return TRUE;
}
// 销毁组播发送用的Socket
void CMulticastAdmin::DeleteMulticaster(void)
{
if (mMulticaster != INVALID_SOCKET)
{
closesocket(mMulticaster);
mMulticaster = INVALID_SOCKET;
WSACleanup();
}
}
// 创建接收用的Socket(需要加入指定的组播组!)
BOOL CMulticastAdmin::CreateReceiver(void)
{
DeleteReceiver();
// 初始化Socket函数库:使用2.2版本的WinSock DLL
WSADATA data;
int ret = WSAStartup(0x0202, &data);
if (ret != 0)
{
WSACleanup();
return FALSE;
}
// 创建一个使用UDP协议传输的Socket
mSckReceiver = socket(AF_INET, SOCK_DGRAM, 0);
if (mSckReceiver == INVALID_SOCKET)
{
WSACleanup();
return FALSE;
}
// 在Socket上设置参数:允许地址复用
BOOL flag = TRUE;
ret = setsockopt(mSckReceiver, SOL_SOCKET, SO_REUSEADDR,
(char *) &flag, sizeof(flag));
// 绑定Socket到组播端口号上
SOCKADDR_IN addr;
ZeroMemory(&addr, sizeof(addr));
addr.sin_family = AF_INET;
addr.sin_addr.s_addr = INADDR_ANY; // 不关心网卡地址
addr.sin_port = htons(mMulticastPort);
ret = bind(mSckReceiver, (struct sockaddr*) &addr, sizeof(addr));
// 将Socket加入到组播组(以便接收组播数据)
struct ip_mreq mreq;
mreq.imr_multiaddr.s_addr = htonl(mMulticastIP);
mreq.imr_interface.s_addr = INADDR_ANY;
ret = setsockopt(mSckReceiver, IPPROTO_IP, IP_ADD_MEMBERSHIP,
(char *) &mreq, sizeof(mreq));
return TRUE;
}
// 销毁接收用的Socket
void CMulticastAdmin::DeleteReceiver(void)
{
if (mSckReceiver != INVALID_SOCKET)
{
closesocket(mSckReceiver);
mSckReceiver = INVALID_SOCKET;
WSACleanup();
}
}
// 组播发送数据
BOOL CMulticastAdmin::Multicast(const char * inBuffer, long inLength)
{
SOCKADDR_IN addr;
memset((char *) &addr, 0, sizeof(addr));
addr.sin_family = AF_INET;
addr.sin_addr.s_addr = htonl(mMulticastIP);
addr.sin_port = htons(mMulticastPort);
// 向组播IP地址、端口号发送数据
int val = sendto(mMulticaster, inBuffer, inLength, 0,
(sockaddr *) &addr, sizeof(addr));
return (val != SOCKET_ERROR);
}
// 启动数据接收线程
BOOL CMulticastAdmin::StartReceiving(void)
{
if (mSckReceiver == INVALID_SOCKET)
{
CreateReceiver();
}

if (mSckReceiver != INVALID_SOCKET)
{
if (mIsReceiving)
{
return TRUE;
}
DWORD threadID = 0;
mRcvThread = CreateThread(NULL, 0, ReceivingThrd,
this, 0, &threadID);
return (mRcvThread != NULL);
}
return FALSE;
}
// 停止数据接收线程
void CMulticastAdmin::StopReceiving(void)
{
if (mIsReceiving)
{
// 销毁接收用的Socket,使接收函数失败或脱离阻塞
DeleteReceiver();
// 等待接收线程完全退出
if (mRcvThread != NULL)
{
WaitForSingleObject(mRcvThread, INFINITE);
mRcvThread = NULL;
}
}
}
// 数据接收线程的执行体(实际调用本类的ReceivingLoop函数)
DWORD WINAPI CMulticastAdmin::ReceivingThrd(void * pParam)
{
ASSERT(pParam);
CMulticastAdmin * pController = (CMulticastAdmin*) pParam;
pController->ReceivingLoop();
return 0;
}
// 数据接收过程
void CMulticastAdmin::ReceivingLoop(void)
{
struct sockaddr_in addr_cli;
int addr_cli_len = sizeof(addr_cli);
char buffer[1024]; // 数据接收缓存
long bytes = 0;

mIsReceiving = TRUE;
while (mIsReceiving)
{
// 等待接收数据
int addr_cli_len = sizeof(addr_cli);
bytes = recvfrom(mSckReceiver, (char *)buffer, 1024, 0,
(LPSOCKADDR) &addr_cli, (int *) &addr_cli_len);
if (bytes == SOCKET_ERROR || bytes == 0)
{
mIsReceiving = FALSE;
}
else
{
// 获取发送者的IP地址
buffer[bytes] = '/0';
char * pStr = inet_ntoa(addr_cli.sin_addr);
// 弹出一个消息框,显示收到的数据内容
CString msg;
msg.Format("Current PID: %d/nReceive from %s /nContent:%s",
GetCurrentProcessId(), pStr, buffer);
AfxMessageBox(msg);
}
}
}

再来看服务器程序MulticastServer和客户机程序MulticastClient的具体实现。它们都是基于对话框的MFC程序,并且分别在各自的主对话框类中定义了一个CMulticastAdmin类的实例。然后就是在对话框的初始化函数中进行一系列的操作,包括创建组播发送/接收用的Socket、启动数据接收线程等。

// MulticastServerDlg.h : header file
//
class CMulticastServerDlg : public CDialog
{
protected:
CMulticastAdmin mMulticaster;
// 其它成员定义(省略)
// ……
};

// MulticastServerDlg.cpp : implementation file
//
CMulticastServerDlg::CMulticastServerDlg(CWnd* pParent /*=NULL*/)
: CDialog(CMulticastServerDlg::IDD, pParent)
{
//{{AFX_DATA_INIT(CMulticastServerDlg)
mMsg = _T("Multicast Message> Hi, DirectShow!");
//}}AFX_DATA_INIT
// Note that LoadIcon does not require a subsequent DestroyIcon in Win32
m_hIcon = AfxGetApp()->LoadIcon(IDR_MAINFRAME);
}
// 组播服务器程序主对话框的初始化
BOOL CMulticastServerDlg::OnInitDialog()
{
CDialog::OnInitDialog();

// Set the icon for this dialog. The framework does this automatically
// when the application's main window is not a dialog
SetIcon(m_hIcon, TRUE); // Set big icon
SetIcon(m_hIcon, FALSE); // Set small icon

// 在界面上显示组播组IP地址、端口号等信息
mStaticInfo.SetWindowText("Multicast IP: 239.8.8.8 Port: 10018");
mMulticaster.SetMulticastIP("239.8.8.8");
// 创建组播发送用的Socket
mMulticaster.CreateMulticaster();

return TRUE; // return TRUE unless you set the focus to a control
}
// 服务器程序界面上的“Send”按钮的响应函数
void CMulticastServerDlg::OnButtonSend()
{
UpdateData();
// 向组播组发送一个消息
mMulticaster.Multicast(mMsg, mMsg.GetLength());
}

// MulticastClientDlg.h : header file
//
class CMulticastClientDlg : public CDialog
{
protected:
CMulticastAdmin mReceiver;
// 其它成员定义(省略)
// ……
};

// MulticastClientDlg.cpp : implementation file
//
// 客户机程序主对话框的初始化
BOOL CMulticastClientDlg::OnInitDialog()
{
CDialog::OnInitDialog();

// Set the icon for this dialog. The framework does this automatically
// when the application's main window is not a dialog
SetIcon(m_hIcon, TRUE); // Set big icon
SetIcon(m_hIcon, FALSE); // Set small icon

// 在界面上显示组播组IP地址、端口号等信息
CString info;
info.Format("Multicast IP: 239.8.8.8 Port: 10018 PID: %d",
GetCurrentProcessId());
mStaticInfo.SetWindowText(info);
// 设置组播组IP地址,创建接收用的Socket
mReceiver.SetMulticastIP("239.8.8.8");
if (mReceiver.CreateReceiver())
{
// 启动数据接收线程
mReceiver.StartReceiving();
}

return TRUE; // return TRUE unless you set the focus to a control
}

原创粉丝点击