《MFC网络编程》学习日记3
Posted chenyibin1995
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了《MFC网络编程》学习日记3相关的知识,希望对你有一定的参考价值。
一个基于UDP的网络通迅程序:
整个例子有两个程序组成,一个发送数据,另一个程序接收数据后,完成绘图工
作。
本文程序运行效果图如下:
一、发送程序
首先建立数据发送程序( Fason) 。一共发送四个数据:圆心的 X坐标、圆心的 Y
坐标、圆半径 R、圆的颜色。它的实现主要如下:
void CFasonDlg::OnSend()
{
UpdateData(TRUE);
CString m_Getstring;
this->m_Cob.GetLBText(m_Cob.GetCurSel(),m_Getstring);
if(m_Getstring==_T("Red"))
yuan1.color=1;
if(m_Getstring==_T("Green"))
yuan1.color=2;
if(m_Getstring==_T("Blue"))
yuan1.color=3;
yuan1.x=m_x;
yuan1.y=m_y;
yuan1.r=m_r;
p=&yuan1;
CSocket m_hSocket;
m_hSocket.Create(2330,SOCK_DGRAM);
m_hSocket.SendTo( p,sizeof(yuan1),3550,"127.0.0.1");//用结构体发送。
m_x=0;
m_y=0;
m_r=0;
m_hSocket.Close();
UpdateData(FALSE);
}
发送数据时有一个技巧,就是所有数据放在一个结构体中来发送,不用一个一个
发送。但是不能在结构体中包含CString等可以变长的数据类型。
二、接收程序
下面我们看一下接收数据端的程序(Jieshou )。如下: void
CDASocket::OnReceive(int nErrorCode)
{
char buff[256];
int ret=0;
ret=Receive(buff,256);
if(ret==ERROR)
{
TRACE("ERROR!");
}
else
m_pDoc->Presscessding(buff);
CAsyncSocket::OnReceive(nErrorCode);
}
三、数据处理与显示
我们设计程序时,数据通常在文档中处理,显示工作在视图中完成。当整个程序
较小时,这感觉不到有何好处,但整个程序较大时,程序的条理就会较清晰,易
读性好。所以一定要时时养成良好的设计习惯。数据在文档中处理如下:
void CJieshouDoc::Presscessding(char* lbuff)
{
buff=(struct yuan*)lbuff;
p.x=buff->x;
p.y=buff->y;
p.r=buff->r;
p.color=buff->color;
UpdateAllViews(NULL);
}
在视图中的显示如下:
void CJieshouView::OnDraw(CDC* pDC)
{
CJieshouDoc* pDoc = GetDocument();
ASSERT_VALID(pDoc);
x=pDoc->p.x;
y=pDoc->p.y;
r=pDoc->p.r;
if(pDoc->p.color==1)
pDC->SelectObject(new CBrush(RGB(255,0,0)));
if(pDoc->p.color==2)
pDC->SelectObject(new CBrush(RGB(0,255,0)));
if(pDoc->p.color==3)
pDC->SelectObject(new CBrush(RGB(0,0,255)));
pDC->Ellipse(x-r,y-r,x+r,y+r);
}
五、再论基于WinSocket的网络通信实现:
VC++中,MFC编程支持两种利用WindowsSockets进行网络通信的编程模式,
这两种模式即为用CAsyncSocket类和派生于CAsyncSocket 的CSocket类。
CAsyncSocket类封装了 WindowsSocketsAPI函数, 提供了较低层的与Windows
Sockets对话接口, 一般适合于有相当水平的网络编程基础者使用, 可方便地进行
底层的网络事件通知及信息回叫控制等操作。
CSocket 派生于 CAsyncSocket,它继承了父类中一些常用易懂得的 Windows
Sockets API函数,并对 CAsyncSocket中底层的较难控制的一些 API函数或成员
函数进行了处理, 它通过 MFCCArchive 对象进行信息的接发操作, 使得网络传输
如同使用MFC的文档连载协议(Serialization protocol),简捷易用。同时它支
持模块化的后台信息处理,解决了 CAsyncSocket中较难克服的多线程处理。
Serialization讲解
创建一个具有序列化能力的类
class CMessageChar : public CObject
{
public:
CMessageChar();
virtual ~CMessageChar(){};
//重载串行化的函数,实现将消息串行化
virtual void Serialize(CArchive& ar);
CString m_strText;
};
CMessageChar::CMessageChar()
{ m_strText = "xnbbbb";}
void CMessageChar::Serialize(CArchive& ar)
{
if (ar.IsStoring())
{ ar << m_strText; }
else
{ ar >> m_strText; }
}
写两个序列化的例子
void CSerialTestDlg::OnButWrite()
{
CFile file;
char buf[512];
file.Open("e:\\\\demo.txt",CFile::modeCreate | CFile::modeWrite);
CArchive aSessionOut(&file,CArchive::store,512,buf);
CMessageChar aa;
aa.Serialize(aSessionOut);
aSessionOut.Close( );
file.Close();
}
void CSerialTestDlg::OnButRead()
{
CFile file;
char buf[512];
file.Open("e:\\\\demo.txt",CFile::modeRead );
CArchive aSessionIn(&file,CArchive::load,512,buf);
CMessageChar aa;
aa.Serialize(aSessionIn);
aSessionIn.Close( );
file.Close();
}
Socket的建立与使用操作
作为服务器,需要有一专用于接听的 Socket,以便于随时接收从客户发送来
的信息, CSocket类的Listen本身支持多线程,故而不必为其开辟新的线程。建
立Windows Sockets的操作步骤见表1。
服务器Socket的建立与使用
在服务器应用程序中声名Socket全局变量, 并使其处于听等状态以等待客户
端与其建立连接:
CListeningSocket m_pSocket;
m_pSocket = new CListeningSocket(this);
if (m_pSocket->Create(Dialog.m_nPort+700))
{ if (m_pSocket->Listen())
return TRUE;
}
当接到客户端的主动连接时或接到数据时,调用文档类接收新客户的处理函
数:
void CListeningSocket::OnAccept(int nErrorCode)
{
CSocket::OnAccept(nErrorCode);
m_pDoc->ProcessPendingAccept();
//调用文档类成员函数,以生成新的对话线程
}
服务器 Socket的网上数据发送与接收操作:
void CClientSocket::SendMsg(CMsg pMsg)
{
if (m_pArchiveOut != NULL)
{ pMsg->Serialize(m_pArchiveOut);
//调用按协议自定义信息类(CMsg)的函数
m_pArchiveOut->Flush();
//Serialize进行网上信息发送操作
}
}
void CClientSocket::ReceiveMsg(CMsg pMsg)
{
pMsg->Serialize(m_pArchiveIn);
} //调用按协议自定义信息类(CMsg)的函数
//Serialize进行网上信息接收操作
客户端建立连接与通信实现
新客户与服务器端建立连接:
BOOL CChatDoc::ConnectSocket(LPCTSTR lpszHandle,
LPCTSTR lpszAddress, UINT nPort)
{ m_strHandle = lpszHandle;
m_pSocket = new CChatSocket(this);
if (!m_pSocket->Create())
{...} // 创建新 Socket失败处理
while (!m_pSocket->Connect(lpszAddress, nPort + 700))
{...} //与服务器建立连接失败后的处理
m_pFile = new CSocketFile(m_pSocket);
m_pArchiveIn = new CArchive(m_pFile,CArchive::load);
m_pArchiveOut = new CArchive(m_pFile,CArchive::store);
// 建立通信流对象
return TRUE;
}
* 客户端从流中读入网上信息:
void CChatDoc::ReceiveMsg()
{ CMsg msg;
TRY { msg.Serialize( m_pArchiveIn);
//调用按协议自定义信息类(CMsg)的函数
while(!msg.m_msgList.IsEmpty()) //Serialize进行网上信息接收操作
{ ...
CATCH(CFileException, e) {... } END—CATCH // 出错处理
if (msg.m_bClose)
{...} // 关闭连接的善后处理
}
* 客户端通过流进行网上发送信息:
void CChatDoc::SendMsg(CString& strText,int index,int i)
{
if (m_pArchiveOut != NULL)
{ CMsg msg;
msg=AssembleMsg(index,i);
// 装配信息
try { msg->Serialize(m_pArchiveOut);
//调用按协议自定义信息类(CMsg)的函数
m_pArchiveOut->Flush();
// Serialize进行网上信息发送操作
}
catch(CFileException, e)
{ ...... } // 出错处理
}
}
表1
六、 深入 CSocket 编程之阻塞和非阻塞模式
这里的阻塞和非阻塞的概念仅适用于 Server 端 socket 程序。 socket 意为
套接字,它与 Socket 不同,请注意首字母的大小写。
客户端与服务端的通信简单来讲:服务端 socket 负责监听,应答,接收和
发送消息,而客户端 socket 只是连接,应答,接收,发送消息。此外,如果你
对于采用 CSocket 类编写 Client/Server 网络程序的原理不是很了解,请先查
询一下( 详见:参考书籍和在线帮助 ) 。
在此之前, 有必要先讲述一下: 网络传输服务提供者, ws2_32.dll , socket 事
件 和 socket window 。
1、概念
网络传输服务提供者(网络传输服务进程) , Socket 事件, Socket Window
网络传输服务提供者 ( transport service provider )是以 DLL 的形式
存在的, 在 windows 操作系统启动时由服务进程 svchost.exe 加载。 当 socket
被创建时,调用 API 函数 Socket (在 ws2_32.dll 中) , Socket 函数会传递
三个参数 : 地址族,套接字类型 ( 注 2 ) 和协议,这三个参数决定了是由哪一
个类型的 网络传输服务提供者 来启动网络传输服务功能。所有的网络通信正是
由网络传输服务提供者完成 , 这里将 网络传输服务提供者 称为 网络传输服务
进程 更有助于理解,因为前文已提到 网络传输服务提供者 是由 svchost.exe
服务进程所加载的。
下图描述了网络应用程序、 CSocket ( WSock32.dll )、 Socket
API(ws2_32.dll) 和 网络传输服务进程 之间的接口层次关系:
当 Client 端 socket 与 Server 端 socket 相互通信时, 两端均会触发 socket
事件。这里仅简要说明两个 socket 事件:
FD_CONNECT: 连接事件 , 通常 Client 端 socket 调用 socket API 函数
Connect 时所触发,这个事件发生在 Client 端。
FD_ACCEPT :正在引入的连接事件,通常 Server 端 socket 正在接收来自
Client 端 socket 连接时触发,这个事件发生在 Server 端。
网络传输服务进程 将 socket 事件 保存至 socket 的事件队列中。此外,
网络传输服务进程 还会向 socket window 发送消息 WM_SOCKET_NOTIFY , 通知
有 socket 事件 产生,见下文对 socket window 的详细说明。
调用 CSocket::Create 函数后, socket 被创建。 socket 创建过程中调用
CAsyncSocket::AttachHandle(SOCKET hSocket, CAsyncSocket* pSocket, BOOL
bDead) 。该函数的作用是:
将 socket 实例句柄和 socket 指针添加至 当前模块状态 ( 注 1 )的
一个映射表变量 m_pmapSocketHandle 中。
在 AttachHandle 过程中,会 new 一个 CSocketWnd 实例 ( 基于 CWnd
派生 ) ,这里将这个实例称之为 socket window ,进一步理解为它是存放所有
sockets 的消息池 ( window 消息) ,请仔细查看,这里 socket 后多加了一个
s ,表示创建的多个 socket 将共享一个 消息池 。
当 Client 端 socket 与 Server 端相互通信时 , 此时 网络传输服务进程
向 socket window 发送消息 WM_SOCKET_NOTIFY ,需要说明的是 CSocketWnd 窗
口句柄保存在 当前模块状态 的 m_hSocketWindow 变量中。
2、阻塞模式
阻塞模式下 Server 端与 Client 端之间的通信处于同步状态下。 在 Server
端直接实例化 CSocket 类,调用 Create 方法创建 socket ,然后调用方法
Listen 开始侦听,最后用一个 while 循环阻塞调用 Accept 函数用于等待来自
Client 端的连接, 如果这个 socket 在主线程( 主程序)中运行, 这将导致主线
程的阻塞。因此,需要创建一个新的线程以运行 socket 服务。
调试跟踪至 CSocket::Accept 函数源码:
while(!Accept(...))
{
// The socket is marked as nonblocking and no connections are present to be accepted.
if (GetLastError() == WSAEWOULDBLOCK)
PumpMessage(FD_ACCEPT);
else
return FALSE;
}
它不断调用 CAsyncSocket::Accept ( CSocket 派生自 CAsyncSocket 类)
判断 Server 端 socket 的事件队列中是否存在正在引入的连接事件 -
FD_ACCEPT (见 1 ) ,换句话说,就是判断是否有来自 Client 端 socket 的连
接请求。
如果当前 Server 端 socket 的事件队列中存在正在引入的连接事件,
Accept 返回一个非 0 值。否则, Accept 返回 0,此时调用 GetLastError 将
返回错误代码 WSAEWOULDBLOCK ,表示队列中无任何连接请求。注意到在循环体
内有一句代码: PumpMessage(FD_ACCEPT);
PumpMessage 作为一个消息泵使得 socket window 中的消息能够维持在活动
状态。 实际跟踪进入 PumpMessage 中, 发现这个消息泵与 Accept 函数的调用并
不相关,它只是使很少的 socket window 消息(典型的是 WM_PAINT 窗口重绘消
息)处于活动状态,而绝大部分的 socket window 消息被阻塞,被阻塞的消息中
含有 WM_SOCKET_NOTIFY。
很显然, 如果没有来自 Client 端 socket 的连接请求, CSocket 就会不断
调用 Accept 产生循环阻塞,直到有来自 Client 端 socket 的连接请求而解除
阻塞。
阻塞解除后,表示 Server 端 socket 和 Client 端 socket 已成功连接,
Server 端与 Client 端彼此相互调用 Send 和 Receive 方法开始通信。
3、非阻塞模式
在非阻塞模式下 利用 socket 事件 的消息机制, Server 端与 Client 端
之间的通信处于异步状态下。
通常需要从 CSocket 类派生一个新类, 派生新类的目的是重载 socket 事件
的消息函数,然后在 socket 事件 的消息函数中添入合适的代码以完成 Client
端与 Server 端之间的通信, 与阻塞模式相比, 非阻塞模式无需创建一个新线程。
这里将讨论当 Server 端 socket 事件 - FD_ACCEPT 被触发后,该事件的
处理函数 OnAccept 是如何进一步被触发的。 其它事件的处理函数如 OnConnect,
OnReceive 等的触发方式与此类似。
在 1 中已提到 Client/Server 端通信时, Server 端 socket 正在接收来自
Client 端 socket 连接请求,这将会触发 FD_ACCEPT 事件,同时 Server 端的
网络传输服务进程 向 Server 端的 socket window (CSocketWnd )发送事件通
知消息 WM_SOCKET_NOTIFY , 通知有 FD_ACCEPT 事件产生 , CSocketWnd 在收到
事件通知消息后,调用消息处理函数 OnSocketNotify:
LRESULT CSocketWnd::OnSocketNotify(WPARAM wParam, LPARAM lParam)
{
CSocket::AuxQueueAdd(WM_SOCKET_NOTIFY, wParam, lParam);
CSocket::ProcessAuxQueue();
return 0L ;
}
消息参数 wParam 是 socket 的句柄, lParam 是 socket 事件 。 这里稍作
解释一下, CSocketWnd 类是作为 CSocket 类的 友元类 ,这意味着它可以访问
CSocket 类中的保护和私有成员函数和变量, AuxQueueAdd 和 ProcessAuxQueue
是 CSocket 类的静态成员函数,如果你对友元不熟悉,请迅速找本有关 C++ 书
看一下友元的使用方法吧!
ProcessAuxQueue 是实质处理 socket 事件的函数, 在该函数中有这样一句代码:
CAsyncSocket* pSocket = CAsyncSocket::LookupHandle((SOCKET)wParam,
TRUE);
其实也就是由 socket 句柄得到发送事件通知消息的 socket 指针
pSocket:从 m_pmapSocketHandle 中查找(见 1 ) !
最后, WSAGETSELECTEVENT(lParam) 会取出事件类型, 在一个简单的 switch
语句中判断事件类型并调用事件处理函数。在这里,事件类型是 FD_ACCEPT , 当
然就调用 pSocket->OnAccept !
结束语
Server 端 socket 处于阻塞调用模式下,它必须在一个新创建的线程中工
作,防止主线程被阻塞。
当有多个 Client 端 socket 与 Server 端 socket 连接及通信时, Server
端采用阻塞模式就显得不适合了,应该采用非阻塞模式 , 利用 socket 事件 的
消息机制来接受多个 Client 端 socket 的连接请求并进行通信。
在非阻塞模式下,利用 CSocketWnd 作为所有 sockets 的消息池,是实现
socket 事件 的消息机制的关键技术。 文中存在用词不妥和可能存在的技术问题,
请大家原谅,也请批评指正,谢谢!
注:
当前模块状态——用于保存当前线程和模块状态的一个结构,可以通过
AfxGetThreadModule() 获得。 AFX_MODULE_THREAD_STATE 在 CSocket 重新定义
为 _AFX_SOCK_THREAD_STATE 。
socket 类型——在 TCP/IP 协议中, Client/Server 网络程序采用 TCP 协议:
即 socket 类型为 SOCK_STREAM ,它是可靠的连接方式。在这里不采用 UDP 协
议:即 socket 类型为 SOCK_DGRAM ,它是不可靠的连接方式。
以上是关于《MFC网络编程》学习日记3的主要内容,如果未能解决你的问题,请参考以下文章