计算机网络是相互连接的独立自主的计算机的集合。网络把许多计算机连接在一起,而互连网则把许多网络通过路由器连接在一起。与网络相连的计算机常称为主机。其示意图如下:
它们互相交织,形成了非常复杂的系统应用环境。
OSI的七层协议体系结构的概念清楚,理论也较完整,但它既复杂又不实用。TCP/IP体系结构则不同,但它现在却得到了非常广泛的应用。它起源于美国国防部高级研究规划署(DARPA)的一项研究计划——实现若干台主机的相互通信。现在TCP/IP已成为Internet上通信的工业标准。
TCP/IP是一个四层的体系结构,它包含应用层、运输层、网际层和网络接口层(用网际层这个名字是强调这一层是为了解决不同网络的互连问题)。不过从实质上讲,TCP/IP只有最上面的三层,因为最下面的网络接口层并没有什么具体内容。因此在学习计算机网络的原理时往往采取折中的办法,即综合OSI和TCP/IP的优点,采用一种只有五层协议的体系结构,这样既简洁又能将概念阐述清楚"。有时为了方便,也可把最底下两层称为网络接口层。
Windows的网络通信建立在TCP/IP协议的基础上,TCP/IP协议族包含一系列构成互联网基础结构的网络协议。TCP/IP字面上代表两个重要协议:
TCP:传输控制协议
IP:网际协议
IP协议(互联层)
TCP协议(传输层)
HTTP协议(应用层)
按照OSI七层模型的描述,传输层提供进程(应用程序)通信的能力。为 了标识通信实体中进行通信的进程(应用程序),TCP/IP协议提出了协议端口(protocol port,简称端口)的概念。
端口是一种抽象的软件结构(包括一些数据结构和I/O缓冲区)。应用程序通过系统调用与某端口建立连接(binding)后,传输层传给该端口的数据都被相应的进程所接收,相应进程发给传输层的数据都通过该端口输出。
端口用一个整数型标识符来表示,即端口号。端口号跟协议相关,TCP/IP传输层的两个协议TCP和UDP是完全独立的两个软件模块,因此各自的端口号也相互独立。
端口使用一个16位的数字来表示,它的范围是0~65535,1024以下的端口号保留给预定义的服务。例如:http使用80端口。
在TCP/IP网络应用中,通信的两个进程间相互作用的主要模式是客户机/服务器模式(client/server),即客户向服务器提出请求,服务器接收到请求后,提供相应的服务。
客户机/服务器模式在操作过程中采取的是主动请求的方式。首先服务器方要先启动,并根据请求提供相应的服务:
客户方:
Windows Sockets通信的基础是套接字(Socket)。套接字是一种网络编程接口,其英文的字面意思为插座、插口,可以形象地将套接字理解为应用程序与网络协议之间的插口,也就是编程接口。
套接字在TCP/IP模型中位于传输层之上,主要针对TCP、UDP协议进行抽象,不涉及应用层协议。
套接字是网络通信的端点。例如,在网络中,主机H1上的一个套接字端点(endpoint)可以和主机H2上的另一个套接字端点进行通信。IP地址和端口号可以唯一确定一个套接字。
套接字的概念最初是由BSD Unix操作系统所实现的。Microsoft将Unix套接字中的大部分函数移植到Windows操作系统,形成了Windows套接字。Windows套接字针对Windows操作系统的消息驱动机制,对原有的Unix套接字进行了扩展,定义了一部分新的函数。
使用MFC提供的Windows套接字功能可以利用面向对象的概念进行网络编程,比直接调用Win32 API更方便、更直观。
MFC中与套接字功能有关的类包括:
CAsyncSocket
类:对套接字API进行了较低级别的封装。CSocket
类:对套接字API进行了较高级别的封装,是CAsyncSocket
类的派生类。CAsyncSocket
类虽然对Windows Sockets API的封装级别较低,但该类为网络通信程序的开发提供了很大的灵活性,可以对一些有关网络协议的具体选项参数进行设定。
CAsyncSocket
类只将套接字的通知消息改进为C++语言中的可重载的消息处理函数,没有像CSocket
那样引入文件、文档、串行化等其他额外的概念,这使编程变得更加简洁方便。
指定套接字的种类:
SOCK_STREAM
SOCK_DGRAM
Send
和Receive
SendTo
和ReceiveFrom
套接字在使用时分为两种模式:
阻塞模式也称为同步模式,非阻塞模式也称为异步模式。
以阻塞模式执行套接字函数,可能会出现某个函数的执行等待很长时间的情况。必须考虑建立多个线程来执行每个套接字函数,程序编写起来比较繁琐。
Windows套接字接口建议程序员使用非阻塞模式进行网络通信编程。一个CAsyncSocket
类对象默认地工作在非阻塞模式(即异步模式)下,这就是CAsyncSocket
(Asyncronization Socket)这个类名称的来源。
服务器 | 客户 | |
---|---|---|
1 | 创建一个套接字CAsyncSocket sockSrv | 创建一个套接字CAsyncSocket sockClient |
2 | 创建底层套接字,获取其句柄,绑定到指定端口sockSrv.Create(nPort) | 创建底层套接字,获取其句柄,使用默认参数sockClient.Create() |
3 | 启动监听,时刻准备接收连接请求sockSrv.Listen() | |
4 | 请求连接到服务器sockClient.Connect(strAddr,nPort) | |
5 | 构造一个新的空套接口CAsyncSocket sockRecv 接收连接 sockSrv.Accept(sockRecv) | |
6 | 接收数据,发送数据sockRecv.Receive(Buff,nLen);sockRecv.Send(Buff,nLen); | 发送数据,接收数据sockClient.Send(Buff,nLen);sockClient.Receive(Buff,nLen); |
8 | 关闭套接口对象sockRecv.Close() | 关闭套接口对象sockClientt.Close() |
说明:
CAsyncSocket
对象,然后使用该对象的Create()
成员函数来创建底层的SOCKET
句柄。服务器要绑定到特定的端口。CAsyncSocket::Listen
成员函数,设置为开始监听状态,一旦受到来自客户机的连接请求,就调用CAsyncSocket::Accept
成员函数来接收它。CAsyncSocket::Connect
成员函数,将它连接到服务器端的套接口对象。建立连接后,双方就可以按照应用层协议交换数据了。Accept
将一个新的空CAsyncSocket
对象作为参数,在调用Accept
之前必须构造这个对象。客户端套接口的连接是通过该对象建立的,如果这个套接口对象退出,连接也就关闭。这个新的套接口对象,不要调用Create
来创建它的底层套接口。CAsyncSocket
对象的其它成员函数,如Send
和Receive
,执行与其它套接口对象的通信。这些成员函数与Winsock API函数在形式和用法上基本是一致的。CAsyncSocket
对象。如果在堆栈上创建了套接口对象,当包含此对象的函数退出时,会调用该类的析构函数(用于释放分配给对象的存储空间)清除该对象。在清除该对象之前,析构函数会调用该对象的Close
成员函数。如果在堆上使用new
操作符创建了套接口对象,可先调用Close
成员函数关闭它,再使用delete
操作符清除这个对象。CAsyncSocket
类对象CAsyncSocket
类对象称为异步套接字对象。创建异步套接字对象分为两个步骤:
CAsyncSocket
的构造函数,构造一个CAsyncSocket
对象。构造函数没有带参数,所以它只创建一个新的空套接口对象;Create
成员函数,再创建该对象的底层的套接字数据结构,并绑定它的地址。有两种使用方法:
CAsyncSocket aa;
aa.Create(/*……*/);
这种方式,直接定义了CAsyncSocket
类的变量。编译时,会隐式地调用该类的构造函数。在堆栈上创建该类对象实例。使用对象实例变量调用该类的成员变量或成员函数时,要用.
操作符。
CAsyncSocket* Pa;
Pa = new CAsyncSocket;
Pa->Create(/*……*/);
这种方式,先定义异步套接口类型的指针变量,再显式调用该类的构造函数。在堆上创建该类对象实例,并将指向该对象实例的指针返回给套接口指针变量。使用对象实例指针变量调用该类成员时,要用->
操作符。
通过调用CAsyncSocket
类的Create()
成员函数,创建该对象的底层套接字句柄,决定套接字对象的具体特性。
Create()
函数的调用格式为:
BOOL Create( UINT nSocketPort = 0,
Int nSocketType = SOCK_STREAM,
Long Levent = FD_READ | FD_WRITE | FD_OOB | FD_ACCEPT | FD_CONNECT | FD_CLOSE,
LPCTSTR lpszSocketAddress = NULL
);
参数nSocketPort
指定一个分配给套接口的端口号,默认值为0,说明由系统给该套接口分配一个端口号。服务器程序应该明确地分配一个端口号(熟知端口号)。客户程序中可以使用默认的0值。
参数nSocketType
指定是流式(SOCK_STREAM
)还是数据报式(SOCK_DGRAM
)套接口。 SOCK_STREAM
是默认值。
参数LEvent
,长整型,指定一个将为此CasyncSocket
对象生成通知消息的套接口事件。默认情况下,所有套接口事件都会生成通知消息。
参数lpszSocketAddress
字符串指针,为套接口指定一个网络地址。该地址可以以主机名或点分十进制的形式给定,如“ftp.microsoft” 或“128.56.22.8”。其默认值为NULL
,表示套接口的地址将限定为本地默认的IP地址。
CAsyncSocket
类接受处理的消息事件Create
成员函数中参数Levent
指定将为CAsyncSocket
对象生成通知消息的套接口事件。
其中,参数Levent
可以选用的六个符号常量在winsock.h文件中定义:
#define FD_READ 0x01
#define FD_WRITE 0x02
#define FD_OOB 0x04
#define FD_ACCEPT 0x08
#define FD_CONNECT 0x10
#define FD_CLOSE 0x20
FD_READ
事件通知:通知有数据可读。当一个套接口对象的数据输入缓冲区收到了数据时产生此事件,并通知该套接口对象:可以调用Receive
成员函数来接收数据。FD_WRITE
事件通知:通知可以写数据。当一个套接口对象的数据输出缓冲区已经发送出去时,输出缓冲区空时产生此事件,并通知该套接口对象:可以调用Send
成员函数向外发送数据。FD_ACCEPT
事件通知:通知监听套接字有连接请求可以接受。当客户端的连接请求到达服务器时,即客户端的连接请求已经进入服务器监听套接口的接收缓冲区时,发生该事件,并通知监听套接口对象,告诉它可以调用Accept成员函数来接收待决的连接请求。这个事件仅对流式套接口有效,并且发生在服务器端。FD_CONNECT
事件通知:通知请求连接的套接字,连接的要求已被处理。当客户端的连接请求已经被处理时产生该事件。该事件仅对流式套接口有效,发生在客户端。有两种情况:
FD_CLOSE
事件通知:通知套接字已关闭。FD_OOB
事件通知:通知将有带外数据到达。在afxSock.h文件中的CAsyncSocket
类的声明中,定义了与这六个网络事件对应的事件处理函数。
virtual void OnReceive(int nErrorCode);//对应FD_READ事件
virtual void OnSend(int nErrorCode);//对应FD_WRITE事件
virtual void OnAccept(int nErrorCode);//对应FD_ACCEPT事件
virtual void OnConnect(int nErrorCode);//对应FD_CONNECT事件
virtual void OnClose(int nErrorCode);//对应 FD_CLOSE事件
virtual void OnOutOfBandData(int nErrorCode);//对应 FD_OOB事件
当某个网络事件发生时,MFC框架会自动调用套接字对象的对应事件处理函数。相当给了套接字对象一个通知,告诉它某个重要的事件已经发生。所以也称之为套接字类的通知函数(notification functions)或回调函数(callback functions)。
套接口对象的回调函数定义都有Virtual,说明他们是可以重载的。一般不直接使用CAsyncSocket
类,而是从它们派生出自己的套接口类。然后在派生出的类中,对这些虚拟函数进行重载,加入对网络事件处理的特定代码。如果从CAsyncSocket
类派生了套接字类,则必须重载所感兴趣的那些网络事件对应的通知函数。MFC框架自动调用通知函数,可以在套接字被通知的时候来优化套接字的行为。
在服务器端套接字对象进入监听状态之后,客户端可以调用CAsyncSocket
类的Connect()
成员函数,向服务器发出一个连接请求。
Connect()
成员函数的两种格式:
BOOL Connect( LPCTSTR lpszHostAddress, UINT nHostPort );
lpszHostAddress
指定服务器地址的字符串,可以使用域名(ftp.microsoft.com)或IP地址(128.56.22.8);lpSockAddrLen
给出lpSockAddr
结构变量中地址的长度,以字节为单位。
BOOL Connect( const SOCKADDR* lpSockAddr, int nSockAddrLen );
该函数用于建立与远程套接口的连接。
两种格式的返回值都是BOOL型。
TRUE
(非0值),说明服务器接收了客户的连接请求,调用成功,连接已建立。FLASE
(0),说明发生了错误,或者服务器不能立即响应,函数就返回。如果调用成功或者发生了WSAEWOULDBLOCK
错误,当调用结束返回时,都会发生FD_CONNECT
事件,MFC框架会自动调用客户端套接字的OnConnect()
事件处理函数,并将错误代码作为参数传送给它。
OnConnect()
原型调用格式如下:
virtual void OnConnect( int nErrorCode );
nErrorCode
是调用Connect()
成员函数获得的错误代码。
在服务器端,使用CAsyncSocket
流式套接字对象,一般按照以下步骤来接收客户端套接字对象的连接请求。服务器应用程序必须首先创建一个CAsyncSocket
流式套接字对象,并调用它的Create
成员函数创建底层套接字句柄。这个套接字对象专门用来监听来自客户机的连接请求(监听套接字对象)。
调用监听套接字对象的Listen
成员函数,使监听套接字对象开始监听来自客户端的连接请求。调用格式如下:
BOOL Listen( int nConnectionBacklog = 5);
当Listen
函数确认并接纳了一个来自客户端的连接请求后,会触发FD_ACCEPT
事件。MFC框架会自动调用监听套接字的OnAccept
事件处理函数,其格式为:
virtual void OnAccept( int nErrorCode );
创建一个新的空的套接字对象,不需要使用它的Create
函数来创建底层套接字句柄。这个套接字专门用来与客户端连接,并进行数据的传输。一般称它为连接套接字,并作为参数传递给下一步的Accept
成员函数。
调用监听套接字对象的Accept
成员函数,该函数用于在一个套接口上接收连接请求。调用格式:
virtual BOOL Accept( CAsyncSocket& rConnectedSocket, SOCKADDR* lpSockAddr=NULL, int* lpSockAddrLen=NULL );
rConnectedSocket
参数是一个空的新的CAsyncSocket
对象,专门用于与客户端建立连接并交换数据,就是上一步创建的连接套接口对象,必须在调用Accept
之前创建,但不需要调用Create()
产生该对象的底层套接口句柄。在Accept
执行过程中,会自动创建,并绑定到该对象;lpSockAddr
参数为指向SOCKADDR
地址的整型指针,用来返回所连接的客户端套接口的网络地址。若lpSockAddr
或lpSockAddrLen
中有一个取默认值NULL
,则不返回任何信息;lpSockAddrLen
参数为指向客户套接口地址长度的整型指针。调用时,是SOCKADDR
结构的长度;返回时,是lpSockAddr
所指地址的实际长度,以字节为单位。
Accept()
的执行过程:首先从监听套接口的待决连接队列中取出一个连接请求;然后使用与监听套接口相同的属性创建一个新的底层套接口,将他绑定到rConnectSocket
参数的套接口对象上,并用它来与客户建立连接。
rConnectSocket
套接口对象不能用来接收更多的连接,仅仅用来和连接的客户套接口对象交换数据。而原来的套接口仍保持打开和监听状态。lpSockAddr
参数返回时填充请求连接的套接口的地址。
当服务器和客户机建立了连接以后,就可以在服务器端的连接套接字对象和客户端的套接字对象之间传输数据了。
对于流式套接字对象,使用CAsyncSocket
类的Send/Receive
成员函数向流式套接字发送/接收数据。
Send
成员函数发送数据virtual int Send(const void* lpBuf, int nBufLen, int nFlags = 0);
lpBuf
是一个指向发送缓冲区的指针,该缓冲区中存放了要发送的数据;nBufLen
是发送缓冲区lpBuf
中的数据长度(字节);nFlags
指定发送方式。取值:
MSG_DONTROUTE
无需路由选择;MSG_OOB
按带外数据发送。Send
函数用于向当前套接口已经建立连接的套接口发送数据。对于一个CAsyncSocket
套接字对象,当它的发送缓冲区为空时,会激发FD_WRITE
事件。套接字会得到这个通知,MFC框架会自动调用这个套接字对象的OnSend
事件处理函数。一般编程者会重载这个函数,在其中调用Send
成员函数来发送数据。
Receive
成员函数接收数据Virtual int Receive(Void* lpBuf,Int nBufLen, Int nFlags=0);
该函数用于获得已经与当前套接口建立连接的远程套接口发送的数据,该数据存入lpBuf
缓冲区中。
lpBuf
是一个指向接收缓冲区的指针,该缓冲区用来存放要发送的数据;nBufLen
给出接收缓冲区lpBuf
中数据的长度,以字节为单位;nFlags
指定接收方式。取值:
MSG_OOB
接收带外数据;MSG_PEEK
将数据复制到所提供的接收端缓冲区内,但是没有从系统缓冲区中将数据删除。对于一个CAsyncSocket
套接字对象,当有数据到达它的接收队列时,会激发FD_READ
事件,套接字会得到已经有数据到达的事件通知。MFC框架会自动调用这个套接字对象的OnReceive
事件处理函数。一般编程者会重载这个函数,在其中调用Receive
成员函数来接收数据。在应用程序将数据取走之前,套接字接收的数据将一直保留在套接字的缓冲区中。
virtual void Close();
该函数用于关闭套接口,释放与套接口有关的系统资源。Close(?)
函数是在对象被删除时,由CAsyncSocket
的析构函数自动调用的。Close(?)
函数的行为,取决于套接口的SO_LINGER
和SO_DONTLINGER
选项。
BOOL Listen( int nConnectionBacklog = 5 );
用于连接的监听,调用成功时返回一个非0值。参数nConnectionBacklog
的取值区间为1~5,默认值为5,它指出正在等待连接的最大队列长度。
virtual void OnAccept( int nErrorCode );
该函数是一个需要重载的回调函数,当一个套接口可能需要与另一端建立连接时,可以调用此函数处理相应的消息。参数nErrorCode
指出最新的错误代码。
virtual void OnConnect( int nErrorCode );
该函数是一个需要重载的回调函数,当一个套接口成功建立连接或连接失败时,可以调用此函数处理相应的消息。
掌握MFC的网络编程方法和技巧,对套接字类CAsyncSocket
有详细了解和掌握。
创建一个简单的实现客户端和服务器端对话的网络聊天程序类似下图:
首先利用向导创建项目,选择“基于对话框”后,在高级选项中选择“windows套接字”如下,点击“完成”创建项目。
分别加入列表框、文本框以及按钮如下:
需要修改的属性有:
Sort
改为False
,Horizontal Scroll
改为True
。Disabled
改为True
,Caption
改为发送
。此外分别为3个控件添加变量为:m_LogCtrl
(列表框)、m_MsgCtrl
(文本框)和m_SendCtrl
(按钮)。
从CSocket
编程模型知道,服务器端需要两种套接字,一个用来侦听连接请求,一个用来与请求连接的套接字建立连接。因此,利用类向导为程序添加两个CSocket
派生类:CServSocket
和CRecvSocket
。
定义好了以后,在对话框类的头文件里加入套接字类的声明:
#include "ServSocket.h"
#include "RecvSocket.h"
并添加套接字变量:
CServSocket *ServSock;
CRecvSocket *RecvSock;
下面在套接字类里加入对话框类信息。
首先在RecvSocket.h和ServSocket.h的开头加入对话框类的声明:1
class CEx8ServerDlg;
然后在两个套接字类里添加公有对话框类指针数据成员m_Dlg
,并添加相应有参构造:
CRecvSocket::CRecvSocket(CEx8ServerDlg* Dlg)
{
m_Dlg = Dlg;
}
CServSocket::CServSocket(CEx8ServerDlg* Dlg)
{
m_Dlg = Dlg;
}
由于我们在派生类里使用了对话框类,我们需要在对话框类中包含工程文件的头文件:2
#include "Ex8Server.h"
重写对话框的OnInitDialog
函数如下:
BOOL CEx8ServerDlg::OnInitDialog()
{
CDialogEx::OnInitDialog();
// 将“关于...”菜单项添加到系统菜单中。
// IDM_ABOUTBOX 必须在系统命令范围内。
ASSERT((IDM_ABOUTBOX & 0xFFF0) == IDM_ABOUTBOX);
ASSERT(IDM_ABOUTBOX < 0xF000);
CMenu* pSysMenu = GetSystemMenu(FALSE);
if (pSysMenu != NULL)
{
BOOL bNameValid;
CString strAboutMenu;
bNameValid = strAboutMenu.LoadString(IDS_ABOUTBOX);
ASSERT(bNameValid);
if (!strAboutMenu.IsEmpty())
{
pSysMenu->AppendMenu(MF_SEPARATOR);
pSysMenu->AppendMenu(MF_STRING, IDM_ABOUTBOX, strAboutMenu);
}
}
// 设置此对话框的图标。 当应用程序主窗口不是对话框时,框架将自动
// 执行此操作
SetIcon(m_hIcon, TRUE); // 设置大图标
SetIcon(m_hIcon, FALSE); // 设置小图标
if (ServSock = new CServSocket(this))
if (ServSock->Create(9547))
{
m_LogCtrl.AddString(TEXT("等待连接......"));
ServSock->Listen();
}
else
{
m_LogCtrl.AddString(TEXT("初始化失败,请重新启动程序!"));
delete ServSock;
}
else
m_LogCtrl.AddString(TEXT("初始化失败,请重新启动程序!"));
return TRUE; // 除非将焦点设置到控件,否则返回 TRUE
}
为了实现消息的读写,首先在CRecvSocket
中添加3个成员:
CSocketFile *m_File;
CArchive *m_ArIn;
CArchive *m_ArOut;
然后在ServSocket.cpp中添加头文件:
#include "RecvSocket.h"
接着重写OnAccept
函数:
void CServSocket::OnAccept(int nErrorCode)
{
CRecvSocket *tempSock;
if (tempSock = new CRecvSocket(this->m_Dlg))
if (Accept(*tempSock))
{
tempSock->m_File = new CSocketFile(tempSock);
tempSock->m_ArIn = new CArchive(tempSock->m_File, CArchive::load);
tempSock->m_ArOut = new CArchive(tempSock->m_File, CArchive::store);
m_Dlg->RecvSock = tempSock;
tempSock = NULL;
m_Dlg->m_LogCtrl.AddString(TEXT("连接成功,可以开始传递消息"));
m_Dlg->m_SendCtrl.EnableWindow(true);
}
else
{
m_Dlg->m_LogCtrl.AddString(TEXT("客户端当前的连接尝试失败"));
delete tempSock;
}
else
m_Dlg->m_LogCtrl.AddString(TEXT("连接套接字初始化失败"));
CSocket::OnAccept(nErrorCode);
}
在CRecvSocket
类中重写OnReceive
函数:
void CRecvSocket::OnReceive(int nErrorCode)
{
CString str;
(*m_ArIn) >> str;
m_Dlg->m_LogCtrl.AddString(TEXT("对方发来的信息如下:"));
m_Dlg->m_LogCtrl.AddString(str);
m_Dlg->m_LogCtrl.SetCurSel(m_Dlg->m_LogCtrl.GetCount() - 1);
CSocket::OnReceive(nErrorCode);
}
为按钮控件添加事件处理程序:
void CEx8ServerDlg::OnBnClickedSend()
{
CString str;
m_MsgCtrl.GetWindowText(str);
if (str.GetLength())
{
m_LogCtrl.AddString(TEXT("你发出的信息如下:"));
m_LogCtrl.AddString(str);
m_LogCtrl.SetCurSel(m_LogCtrl.GetCount() - 1);
*(RecvSock->m_ArOut) << str;
RecvSock->m_ArOut->Flush();
}
else
AfxMessageBox(TEXT("发送的消息不能为空!"));
}
项目创建与界面编写与服务器端类似,略。
首先类似服务器端添加一个套接字类CClientSocket
,并添加对话框变量及其相应构造。
类似CRecvSocket
,添加3个数据成员:
CSocketFile *m_File;
CArchive *m_ArIn;
CArchive *m_ArOut;
BOOL CEx8ClientDlg::OnInitDialog()
{
CDialogEx::OnInitDialog();
// 将“关于...”菜单项添加到系统菜单中。
// IDM_ABOUTBOX 必须在系统命令范围内。
ASSERT((IDM_ABOUTBOX & 0xFFF0) == IDM_ABOUTBOX);
ASSERT(IDM_ABOUTBOX < 0xF000);
CMenu* pSysMenu = GetSystemMenu(FALSE);
if (pSysMenu != NULL)
{
BOOL bNameValid;
CString strAboutMenu;
bNameValid = strAboutMenu.LoadString(IDS_ABOUTBOX);
ASSERT(bNameValid);
if (!strAboutMenu.IsEmpty())
{
pSysMenu->AppendMenu(MF_SEPARATOR);
pSysMenu->AppendMenu(MF_STRING, IDM_ABOUTBOX, strAboutMenu);
}
}
// 设置此对话框的图标。 当应用程序主窗口不是对话框时,框架将自动
// 执行此操作
SetIcon(m_hIcon, TRUE); // 设置大图标
SetIcon(m_hIcon, FALSE); // 设置小图标
m_LogCtrl.AddString(TEXT("正在连接……"));
if (ClientSock = new CClientSocket(this))
if (ClientSock->Create())
if (ClientSock->Connect(TEXT("localhost"), 9547))
{
ClientSock->m_File = new CSocketFile(ClientSock);
ClientSock->m_ArIn = new CArchive(ClientSock->m_File, CArchive::load);
ClientSock->m_ArOut = new CArchive(ClientSock->m_File, CArchive::store);
m_LogCtrl.AddString(TEXT("连接成功,可以开始传递消息"));
m_SendCtrl.EnableWindow(true);
}
else
{
m_LogCtrl.AddString(TEXT("连接不成功"));
delete ClientSock;
}
else
{
m_LogCtrl.AddString(TEXT("初始化失败,请重新启动程序"));
delete ClientSock;
}
else
m_LogCtrl.AddString(TEXT("初始化失败,请重新启动程序"));
return TRUE; // 除非将焦点设置到控件,否则返回 TRUE
}
重写CClientSocket
的OnReceive
:
void CClientSocket::OnReceive(int nErrorCode)
{
CString str;
m_Dlg->m_LogCtrl.AddString(TEXT("对方发来消息如下:"));
*m_ArIn >> str;
m_Dlg->m_LogCtrl.AddString(str);
m_Dlg->m_LogCtrl.SetCurSel(m_Dlg->m_LogCtrl.GetCount() - 1);
CSocket::OnReceive(nErrorCode);
}
为按钮添加事件处理程序:
void CEx8ClientDlg::OnBnClickedSend()
{
CString str;
m_MsgCtrl.GetWindowText(str);
if (str.GetLength())
{
m_LogCtrl.AddString(TEXT("你发的信息如下:"));
m_LogCtrl.AddString(str);
m_LogCtrl.SetCurSel(m_LogCtrl.GetCount() - 1);
*(ClientSock->m_ArOut) << str;
ClientSock->m_ArOut->Flush();
}
else
AfxMessageBox(TEXT("消息为空!"));
}
首先启动服务器:
再启动客户端:
这时服务器也连接成功了:
在服务器中输入一条消息:
发送消息:
在客户端输入一条消息并发送:
这个问题是由于派生了套接字类并连接了对话框类导致的,引入工程文件的头文件即可解决问题。
在这个实验中,我学到了如何使用MFC进行简单的网络编程,特别是利用CAsyncSocket
类来实现服务器端和客户端的通信。MFC提供了CAsyncSocket
类作为基础网络编程的工具,它能够处理异步的套接字通信,使得开发网络应用变得相对简单。在实现服务器和客户端的通信过程中,需要派生CAsyncSocket
类来创建自定义的套接字类,分别用于服务器端和客户端。这样的设计使得代码结构清晰,易于维护。在实验中,我学到了如何使用CArchive
和CSocketFile
类来实现消息的序列化和反序列化。这是因为CAsyncSocket
类的Send
和Receive
方法需要传递字符数组,而通过CArchive
可以方便地将复杂的数据结构进行序列化,便于在网络上传输。在网络编程中,错误处理和异常处理是非常重要的。在实验中,我学到了如何通过返回值和错误码来判断网络操作是否成功,并在失败时进行适当的处理。同时,异常处理也是必不可少的,尤其是在动态内存分配等情况下,要注意避免内存泄漏。在服务器端和客户端的交互中,我学到了如何在对话框类和套接字类之间进行信息传递。通过在套接字类中添加对话框类的指针,并在构造函数中进行初始化,可以在套接字操作时方便地更新对话框中的界面。总体而言,这个实验让我更深入地理解了MFC框架下的网络编程,并掌握了一些基本的技巧和方法。网络通信是软件开发中一个重要的方向,通过这个实验,我对网络编程有了更实际的认识。
参考资料:一个使用CSocket类的网络通信实例
代码地址:https://github.com/zsc118/MFC-exercises
这里不能直接包含头文件,否则会出现重定义错误。 ??