为了正常的体验网站,请在浏览器设置里面开启Javascript功能!

网络编程基础

2017-10-14 45页 doc 109KB 17阅读

用户头像

is_180829

暂无简介

举报
网络编程基础网络编程基础 网络编程基础网络编程基础 VC编程-网络编程3.1 网络编程基础 多媒体技术与网络技术的结合,使得网络生活变得多姿多彩。从此,网络生活很迷人;网络改变了和改变着人们原本的生活方式。姑且认为DirectShow是单机的多媒体技术,一旦融合了网络技术,DirectShow更显现了它强大的生命力。本章将着重介绍DirectShow技术在网络方面的应用。 网络编程,当然要用到Windows Socket(套接字)技术。Socket相关的操作由一系列API函数来完成,比如socket、bind、listen、con...
网络编程基础
网络编程基础 网络编程基础网络编程基础 VC编程-网络编程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; // 用于侦听的 SocketSOCKET mAccepted; // 用于与远程端建立连接的SocketWORD mListenPort; // 侦听 端口号BOOL mIsListening; // 是否正在侦听的标记HANDLE mLsnThread; // 侦听线程 public:CTCPListener();virtual ~CTCPListener();public:// 设置/得到侦听的端口号 void SetListenPort(WORD inPort); WORD GetListenPort(void);// 创建/销毁用于侦听的 SocketBOOL Create(void);void DeleteListener(void);// 销毁服务器与客户机建立连接的 Socketvoid 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_FILEstatic char THIS_FILE[] = __FILE__;#endif///////////////////////////////////////////////////////////////// /////////////CTCPListener::CTCPListener(){// 参数初始化mListener = INVALID_SOCKET;mAccepted = INVALID_SOCKET;// 默认在10028端口上侦听mListenPort = 10028;mLsnThread = NULL;mIsListening = FALSE;}CTCPListener::~CTCPListener(){// 销 毁SocketDeleteAccepted();DeleteListener();// 停止侦听线程StopListening();}// 设 置侦听端口号void CTCPListener::SetListenPort(WORD inPort){mListenPort = inPort;}// 得到侦听端口号WORD CTCPListener::GetListenPort(void){return mListenPort;}// 创建用于侦听的SocketBOOL CTCPListener::Create(void){DeleteListener(); // 销毁侦听Socket int val = 0;BOOL pass = FALSE;// 创建一个TCP传输的SocketmListener = 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;}// 销毁用于侦听的 Socketvoid CTCPListener::DeleteListener(void){if (mListener != INVALID_SOCKET){closesocket(mListener);mListener = INVALID_SOCKET;}}// 销毁服务器 与客户机建立连接的Socketvoid 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();// 保存与远程端建立连接的 SocketmAccepted = 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; // 用于数据发送或接收的SocketBOOL mIsConnected; // Socket是否已经建立了连接的标记BOOL mIsReceiving; // 使用独立的 线程进行数据接收HANDLE mRcvThread;BOOL mIsSending; // 使用独立的线程进行数据发送 HANDLE mSndThread;public:CStreamSocket();virtual ~CStreamSocket();public:BOOL Attach(SOCKET inSocket); // 关联一个Socketvoid 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_FILEstatic 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;}// 销毁Socketvoid 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传输的SocketmSocket = 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 _DEBUGUNetwork::DumpSocketError();#endif// 如果连接失败,则销毁刚才创建的 SocketDetach();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 Win32m_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 dialogSetIcon(m_hIcon, TRUE); // Set big iconSetIcon(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){// 获取建立连接的 SocketmNetSender.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 Win32m_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 dialogSetIcon(m_hIcon, TRUE); // Set big iconSetIcon(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-endianDEC Alpha 所有 little-endianHP-PA NT little-endianHP-PA UNIX big-endianSUN SPARC 所有 big-endianMIPS NT little-endianMIPS UNIX big-endianPowerPC NT little-endianPowerPC 非NT big-endianRS/6000 UNIX big-endianMotorola 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; // 用于接收的SocketSOCKET mSckSender; // 用于发送的SocketDWORD 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);// 创建/销毁用于发送的 SocketBOOL CreateSender(void);void DeleteSender(void);// 创建/销毁用于接收的 SocketBOOL 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_FILEstatic char THIS_FILE[] = __FILE__;#endif /////////////////////////////////////////////////////////////////////////////CUDPManager::CUDPManager(){// 参数初始化mSckReceiver = INVALID_SOCKET;mSckSender = INVALID_SOCKET;mTargetIP = 0x7f000001; // 127.0.0.1mTargetPort = 10080;mLocalPort = 10080;mIsReceiving = FALSE;mRcvThread = NULL;}CUDPManager::~CUDPManager(){// 销 毁所有使用过的SocketDeleteSender();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;}// 创建用于发送的 SocketBOOL CUDPManager::CreateSender(void){DeleteSender();// 创建一个UDP传输的 SocketmSckSender = socket(AF_INET, SOCK_DGRAM, 0);if (mSckSender != INVALID_SOCKET){return TRUE;}return FALSE;}// 销毁用于发送的Socketvoid CUDPManager::DeleteSender(void){if (mSckSender != INVALID_SOCKET){closesocket(mSckSender);mSckSender = INVALID_SOCKET;}}// 创建用于 接收的SocketBOOL CUDPManager::CreateReceiver(void){DeleteReceiver();// 创建一个 UDP传输的SocketmSckReceiver = 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;}// 销毁用于接收的 Socketvoid 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传输的SocketSOCKET 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 _DEBUGUNetwork::DumpSocketError();#endif}// 发送完成后销毁该 Socketclosesocket(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 dialogSetIcon(m_hIcon, TRUE); // Set big iconSetIcon(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);// 设置服务器接收数据用的端口号, 然后创建接收用的SocketmUDPManager.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 dialogSetIcon(m_hIcon, TRUE); // Set big iconSetIcon(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);// 创建接收用 的Socketif (mUDPManager.CreateReceiver()){// 启动数据接收线程 mUDPManager.StartReceiving();}// 创建发送用的SocketmUDPManager.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; // 接收用的SocketSOCKET mMulticaster; // 组播发送用的SocketDWORD 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);// 创建/销毁组播用的 SocketBOOL CreateMulticaster(void);void DeleteMulticaster(void);// 创建/销毁接收 用的SocketBOOL 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_FILEstatic char THIS_FILE[] = __FILE__;#endif /////////////////////////////////////////////////////////////////////////////CMulticastAdmin::CMulticastAdmin(){// 参数初始化mSckReceiver = INVALID_SOCKET;mMulticaster = INVALID_SOCKET;// Multicast IP: from 224.0.0.0 to 239.255.255.255mMulticastIP = 0xef080808; // 239.8.8.8mMulticastPort = 10018;mIsReceiving = FALSE;mRcvThread = NULL;}CMulticastAdmin::~CMulticastAdmin(){// 销毁所有使用过的 SocketDeleteMulticaster();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;}// 创建组播发送用 的SocketBOOL CMulticastAdmin::CreateMulticaster(void){DeleteMulticaster();// 初 始化Socket函数库:使用2.2版本的WinSock DLL WSADATA data;int ret = WSAStartup(0x0202, &data);if (ret != 0){WSACleanup();return FALSE;}// 创建一 个使用UDP协议传输的SocketmMulticaster = 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;}// 销毁组播发送用的Socketvoid CMulticastAdmin::DeleteMulticaster(void){if (mMulticaster != INVALID_SOCKET){closesocket(mMulticaster);mMulticaster = INVALID_SOCKET;WSACleanup();}}// 创建接收用的Socket(需要加入指定的组播组~)BOOL CMulticastAdmin::CreateReceiver(void){DeleteReceiver();// 初始化Socket函数库:使 用2.2版本的WinSock DLLWSADATA data;int ret = WSAStartup(0x0202, &data);if (ret != 0){WSACleanup();return FALSE;}// 创建一个使用UDP协议传输的 SocketmSckReceiver = 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;}// 销毁接收用的Socketvoid 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 Win32m_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 dialogSetIcon(m_hIcon, TRUE); // Set big iconSetIcon(m_hIcon, FALSE); // Set small icon // 在界面上显示组播组IP地址、端口号等信息 mStaticInfo.SetWindowText("Multicast IP: 239.8.8.8 Port: 10018");mMulticaster.SetMulticastIP("239.8.8.8");// 创建组播发送 用的SocketmMulticaster.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 dialogSetIcon(m_hIcon, TRUE); // Set big iconSetIcon(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地址,创建 接收用的SocketmReceiver.SetMulticastIP("239.8.8.8");if (mReceiver.CreateReceiver()){// 启动数据接收线程mReceiver.StartReceiving();} return TRUE; // return TRUE unless you set the focus to a control} 提示:本书配套光盘的SourceCodes/Chapter03/MulticastDemo目录下提供了组播传输演示 程序的完整实现。其中,MulticastServer为服务器程序,MulticastClient为客户机程序。 打开WsClientServer.dsw可以同时浏览MulticastServer和MulticastClient两个项目。
/
本文档为【网络编程基础】,请使用软件OFFICE或WPS软件打开。作品中的文字与图均可以修改和编辑, 图片更改请在作品中右键图片并更换,文字修改请直接点击文字进行修改,也可以新增和删除文档中的内容。
[版权声明] 本站所有资料为用户分享产生,若发现您的权利被侵害,请联系客服邮件isharekefu@iask.cn,我们尽快处理。 本作品所展示的图片、画像、字体、音乐的版权可能需版权方额外授权,请谨慎使用。 网站提供的党政主题相关内容(国旗、国徽、党徽..)目的在于配合国家政策宣传,仅限个人学习分享使用,禁止用于任何广告和商用目的。

历史搜索

    清空历史搜索