基于Windows套接字的网络编程(MFC)

1.预备知识

1.计算机网络

计算机网络是相互连接的独立自主的计算机的集合。网络把许多计算机连接在一起,而互连网则把许多网络通过路由器连接在一起。与网络相连的计算机常称为主机。其示意图如下:
在这里插入图片描述

1.IP地址

  • IP网络中每台主机都必须有一个惟一的IP地址;
  • IP地址是一个逻辑地址;
  • 因特网上的IP地址具有全球唯一性;
  • 32位,4个字节,常用点分十进制的格式表示,例如:192.168.0.16

2.协议

  • 为进行网络中的数据交换(通信)而建立的规则、标准或约定。(=语义+语法+规则)
  • 不同层具有各自不同的协议。

3.网络的状况

  • 多种通信媒介——有线、无线……
  • 不同种类的设备——通用、专用……
  • 不同的操作系统——Unix、Windows ……
  • 不同的应用环境——固定、移动……

它们互相交织,形成了非常复杂的系统应用环境。

4.计算机网络层次模型

OSI的七层协议体系结构的概念清楚,理论也较完整,但它既复杂又不实用。TCP/IP体系结构则不同,但它现在却得到了非常广泛的应用。它起源于美国国防部高级研究规划署(DARPA)的一项研究计划——实现若干台主机的相互通信。现在TCP/IP已成为Internet上通信的工业标准。

TCP/IP是一个四层的体系结构,它包含应用层、运输层、网际层和网络接口层(用网际层这个名字是强调这一层是为了解决不同网络的互连问题)。不过从实质上讲,TCP/IP只有最上面的三层,因为最下面的网络接口层并没有什么具体内容。因此在学习计算机网络的原理时往往采取折中的办法,即综合OSI和TCP/IP的优点,采用一种只有五层协议的体系结构,这样既简洁又能将概念阐述清楚"。有时为了方便,也可把最底下两层称为网络接口层。
在这里插入图片描述

  • 应用层:远程登录协议Telnet、文件传输协议FTP、 超文本传输协议HTTP、域名服务DNS、简单邮件传输协议SMTP、邮局协议POP3等。
  • 传输层:传输控制协议TCP、用户数据报协议UDP。
    • TCP:面向连接的可靠的传输协议。
    • UDP:是无连接的,不可靠的传输协议。
  • 网络层:网际协议IP、Internet互联网控制报文协议ICMP、Internet组管理协议IGMP。

Windows的网络通信建立在TCP/IP协议的基础上,TCP/IP协议族包含一系列构成互联网基础结构的网络协议。TCP/IP字面上代表两个重要协议:

  • TCP:传输控制协议

  • IP:网际协议

  • IP协议(互联层)

    • 是一个路由协议,负责IP寻址、数据包的分片和重组等任务
  • TCP协议(传输层)

    • 提供一对一有连接的通信服务
    • 提供连接的确认,数据包发送/接收顺序的控制,出错重传等机制
    • 保证数据在传输中的正确性
  • HTTP协议(应用层)

    • 用于传送Web网页文件

5.端口

按照OSI七层模型的描述,传输层提供进程(应用程序)通信的能力。为 了标识通信实体中进行通信的进程(应用程序),TCP/IP协议提出了协议端口(protocol port,简称端口)的概念。

端口是一种抽象的软件结构(包括一些数据结构和I/O缓冲区)。应用程序通过系统调用与某端口建立连接(binding)后,传输层传给该端口的数据都被相应的进程所接收,相应进程发给传输层的数据都通过该端口输出。

端口用一个整数型标识符来表示,即端口号。端口号跟协议相关,TCP/IP传输层的两个协议TCP和UDP是完全独立的两个软件模块,因此各自的端口号也相互独立。

端口使用一个16位的数字来表示,它的范围是0~65535,1024以下的端口号保留给预定义的服务。例如:http使用80端口。

6.客户机/服务器模式

在TCP/IP网络应用中,通信的两个进程间相互作用的主要模式是客户机/服务器模式(client/server),即客户向服务器提出请求,服务器接收到请求后,提供相应的服务。

客户机/服务器模式在操作过程中采取的是主动请求的方式。首先服务器方要先启动,并根据请求提供相应的服务:

  1. 打开一个通信通道并告知本地主机,它愿意在某一地址和端口上接收客户请求。
  2. 等待客户请求到达该端口。
  3. 接收到服务请求,处理该请求并发送应答信号,同时要激活一个新的进程(或线程)来处理这个客户请求。新进程(或线程) 处理此客户请求,并不需要对其它请求作出应答。服务完成后,关闭此新进程与客户的通信链路,并终止。
  4. 返回第二步,等待另一客户请求。
  5. 关闭服务器。

客户方:

  1. 打开一个通信通道,并连接到服务器所在主机的特定端口。
  2. 向服务器发服务请求报文,等待并接收应答;继续提出请求。
  3. 请求结束后关闭通信通道并终止。

在这里插入图片描述

7.Windows Sockets通信机制

Windows Sockets通信的基础是套接字(Socket)。套接字是一种网络编程接口,其英文的字面意思为插座、插口,可以形象地将套接字理解为应用程序与网络协议之间的插口,也就是编程接口。

套接字在TCP/IP模型中位于传输层之上,主要针对TCP、UDP协议进行抽象,不涉及应用层协议。

套接字是网络通信的端点。例如,在网络中,主机H1上的一个套接字端点(endpoint)可以和主机H2上的另一个套接字端点进行通信。IP地址和端口号可以唯一确定一个套接字。

在这里插入图片描述
套接字的概念最初是由BSD Unix操作系统所实现的。Microsoft将Unix套接字中的大部分函数移植到Windows操作系统,形成了Windows套接字。Windows套接字针对Windows操作系统的消息驱动机制,对原有的Unix套接字进行了扩展,定义了一部分新的函数。

2.在MFC中使用Windows套接字

使用MFC提供的Windows套接字功能可以利用面向对象的概念进行网络编程,比直接调用Win32 API更方便、更直观。

MFC中与套接字功能有关的类包括:

  • CAsyncSocket类:对套接字API进行了较低级别的封装。
  • CSocket类:对套接字API进行了较高级别的封装,是CAsyncSocket类的派生类。

1.CAsyncSocket

虽然对Windows Sockets API的封装级别较低,但该类为网络通信程序的开发提供了很大的灵活性,可以对一些有关网络协议的具体选项参数进行设定。

CAsyncSocket类只将套接字的通知消息改进为C++语言中的可重载的消息处理函数,没有像CSocket那样引入文件、文档、串行化等其他额外的概念,这使编程变得更加简洁方便。

1.套接字的种类
  • 流套接字(stream socket)
    • 使用TCP协议进行通信
    • 具有TCP协议所拥有的各种特征
      • 面向连接的、可靠的数据流传输服务
      • 数据包不会出现丢失、重复、乱序等现象
  • 数据报套接字(datagram socket)
    • 使用UDP协议进行通信
    • 具有UDP协议所拥有的各种特征
      • 面向非连接的、不可靠的用户数据报传输服务
      • 数据包可能出现丢失、重复、乱序等现象

指定套接字的种类:

  • Create成员函数缺省创建的是流套接字SOCK_STREAM
  • 也可以根据需要指明创建数据报套接字SOCK_DGRAM
  • 流套接字和数据报套接字在收发数据时所调用的函数有所不同
    • 流套接字调用成员函数SendReceive
    • 数据报套接字调用成员函数SendToReceiveFrom
2.套接字的工作模式

套接字在使用时分为两种模式:

  • 阻塞式
  • 非阻塞式

阻塞模式也称为同步模式,非阻塞模式也称为异步模式。

阻塞模式
  • 在阻塞模式下,套接字函数要一直等到全部操作完成后才返回
  • 例如,在建立连接时
    • 函数必须等到连接完全建立好为止
    • 调用函数的线程在这期间被挂起
    • 程序看起来好像停止了响应

以阻塞模式执行套接字函数,可能会出现某个函数的执行等待很长时间的情况。必须考虑建立多个线程来执行每个套接字函数,程序编写起来比较繁琐。

非阻塞模式
  • BSD Unix是命令行方式的系统
    • 套接字适合以阻塞模式工作
  • Windows是消息驱动的系统
    • 套接字适合以非阻塞模式工作
    • Windows为所有的套接字函数提供了非阻塞模式的版本
  • 在非阻塞模式下
    • 一个套接字函数被调用后会立即返回
    • 即使它执行的操作还没有全部完成
  • 当函数最终完成所执行的操作时
    • Windows通过发送消息的方式通知程序
    • 该模式适合Windows的消息驱动体系

Windows套接字接口建议程序员使用非阻塞模式进行网络通信编程。一个CAsyncSocket类对象默认地工作在非阻塞模式(即异步模式)下,这就是CAsyncSocket(Asyncronization Socket)这个类名称的来源。

3.一般使用步骤
服务器客户
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()

说明:

  1. 客户与服务器端都要先构造一个CAsyncSocket对象,然后使用该对象的Create()成员函数来创建底层的SOCKET句柄。服务器要绑定到特定的端口。
  2. 服务器端的套接口对象,使用CAsyncSocket::Listen成员函数,设置为开始监听状态,一旦受到来自客户机的连接请求,就调用CAsyncSocket::Accept成员函数来接收它。
  • 客户端的套接口对象,应使用CAsyncSocket::Connect成员函数,将它连接到服务器端的套接口对象。建立连接后,双方就可以按照应用层协议交换数据了。
  • Accept将一个新的空CAsyncSocket对象作为参数,在调用Accept之前必须构造这个对象。客户端套接口的连接是通过该对象建立的,如果这个套接口对象退出,连接也就关闭。这个新的套接口对象,不要调用Create来创建它的底层套接口。
  1. 调用CAsyncSocket对象的其它成员函数,如SendReceive,执行与其它套接口对象的通信。这些成员函数与Winsock API函数在形式和用法上基本是一致的。
  2. 关闭并清除CAsyncSocket对象。如果在堆栈上创建了套接口对象,当包含此对象的函数退出时,会调用该类的析构函数(用于释放分配给对象的存储空间)清除该对象。在清除该对象之前,析构函数会调用该对象的Close成员函数。如果在堆上使用new操作符创建了套接口对象,可先调用Close成员函数关闭它,再使用delete操作符清除这个对象。
4.创建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框架自动调用通知函数,可以在套接字被通知的时候来优化套接字的行为。

5.客户端套接字对象请求连接到服务器端套接字对象

在服务器端套接字对象进入监听状态之后,客户端可以调用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()成员函数获得的错误代码。

6.服务器接受客户机的连接请求

在服务器端,使用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地址的整型指针,用来返回所连接的客户端套接口的网络地址。若lpSockAddrlpSockAddrLen中有一个取默认值NULL,则不返回任何信息;lpSockAddrLen参数为指向客户套接口地址长度的整型指针。调用时,是SOCKADDR结构的长度;返回时,是lpSockAddr所指地址的实际长度,以字节为单位。

Accept()的执行过程:首先从监听套接口的待决连接队列中取出一个连接请求;然后使用与监听套接口相同的属性创建一个新的底层套接口,将他绑定到rConnectSocket参数的套接口对象上,并用它来与客户建立连接。

rConnectSocket套接口对象不能用来接收更多的连接,仅仅用来和连接的客户套接口对象交换数据。而原来的套接口仍保持打开和监听状态。lpSockAddr参数返回时填充请求连接的套接口的地址。

7.发送与接收流式数据

当服务器和客户机建立了连接以后,就可以在服务器端的连接套接字对象和客户端的套接字对象之间传输数据了。

对于流式套接字对象,使用CAsyncSocket类的Send/Receive成员函数向流式套接字发送/接收数据。

Send成员函数发送数据
virtual int Send(const void* lpBuf,  int nBufLen, int nFlags = 0);
  • lpBuf是一个指向发送缓冲区的指针,该缓冲区中存放了要发送的数据;
  • nBufLen是发送缓冲区lpBuf中的数据长度(字节);
  • nFlags指定发送方式。取值:
    • 0按正常方式发送数据;
    • 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指定接收方式。取值:
    • 0接收正常数据;
    • MSG_OOB接收带外数据;
    • MSG_PEEK将数据复制到所提供的接收端缓冲区内,但是没有从系统缓冲区中将数据删除。

对于一个CAsyncSocket套接字对象,当有数据到达它的接收队列时,会激发FD_READ事件,套接字会得到已经有数据到达的事件通知。MFC框架会自动调用这个套接字对象的OnReceive事件处理函数。一般编程者会重载这个函数,在其中调用Receive成员函数来接收数据。在应用程序将数据取走之前,套接字接收的数据将一直保留在套接字的缓冲区中。

8.关闭套接字
virtual void Close();

该函数用于关闭套接口,释放与套接口有关的系统资源。Close( )函数是在对象被删除时,由CAsyncSocket的析构函数自动调用的。Close( )函数的行为,取决于套接口的SO_LINGERSO_DONTLINGER选项。

9.其它成员函数
BOOL Listen( int nConnectionBacklog = 5 );

用于连接的监听,调用成功时返回一个非0值。参数nConnectionBacklog的取值区间为1~5,默认值为5,它指出正在等待连接的最大队列长度。

virtual void OnAccept( int nErrorCode );

该函数是一个需要重载的回调函数,当一个套接口可能需要与另一端建立连接时,可以调用此函数处理相应的消息。参数nErrorCode指出最新的错误代码。

virtual void OnConnect( int nErrorCode );

该函数是一个需要重载的回调函数,当一个套接口成功建立连接或连接失败时,可以调用此函数处理相应的消息。

2.基于TCP(面向连接)的socket编程

1.服务器端
  1. 创建套接字(socket)。
  2. 将套接字绑定到一个本地地址和端口上(bind)。
  3. 将套接字设为监听模式,准备接收客户请求(listen)。
  4. 等待客户请求到来;当请求到来后,接受连接请求,返回一个新的对应于此次连接的套接字(accept)。
  5. 用返回的套接字和客户端进行通信(send/receive)。
  6. 返回第4步,等待另一客户请求。
  7. 关闭套接字。
2.客户端
  1. 创建套接字(socket)。
  2. 向服务器发出连接请求(connect)。
  3. 和服务器端进行通信(send/receive)。
  4. 关闭套接字。

2.实验目的

掌握MFC的网络编程方法和技巧,对套接字类CAsyncSocket有详细了解和掌握。

3.实验内容

创建一个简单的实现客户端和服务器端对话的网络聊天程序类似下图:

在这里插入图片描述

4.代码实现

1.服务器端

1.项目创建

首先利用向导创建项目,选择“基于对话框”后,在高级选项中选择“windows套接字”如下,点击“完成”创建项目。

在这里插入图片描述

2.界面编写

分别加入列表框、文本框以及按钮如下:

在这里插入图片描述
需要修改的属性有:

  • 列表框的Sort改为FalseHorizontal Scroll改为True
  • 按钮的Disabled改为TrueCaption改为发送

此外分别为3个控件添加变量为:m_LogCtrl(列表框)、m_MsgCtrl(文本框)和m_SendCtrl(按钮)。

3.定义套接字类

CSocket编程模型知道,服务器端需要两种套接字,一个用来侦听连接请求,一个用来与请求连接的套接字建立连接。因此,利用类向导为程序添加两个CSocket派生类:CServSocketCRecvSocket

定义好了以后,在对话框类的头文件里加入套接字类的声明:

#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"

4.初始化对话框

重写对话框的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
}

5.接受连接请求

为了实现消息的读写,首先在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);
}

6.接收信息

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);
}

7.发送信息

为按钮控件添加事件处理程序:

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("发送的消息不能为空!"));
}

2.客户端

项目创建界面编写与服务器端类似,略。

1.定义套接字类

首先类似服务器端添加一个套接字类CClientSocket,并添加对话框变量及其相应构造。

类似CRecvSocket,添加3个数据成员:

CSocketFile *m_File;
CArchive *m_ArIn;
CArchive *m_ArOut;

2.初始化对话框

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
}

3.接收消息

重写CClientSocketOnReceive

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);
}

4.发送信息

为按钮添加事件处理程序:

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("消息为空!"));
}

5.运行结果

首先启动服务器:

在这里插入图片描述
再启动客户端:

在这里插入图片描述
这时服务器也连接成功了:

在这里插入图片描述
在服务器中输入一条消息:

在这里插入图片描述
发送消息:

在这里插入图片描述
在这里插入图片描述
在客户端输入一条消息并发送:

在这里插入图片描述
在这里插入图片描述

6.总结

1.实验中遇到的困难

C2065:未声明的标识符

这个问题是由于派生了套接字类并连接了对话框类导致的,引入工程文件的头文件即可解决问题。

2.心得体会

在这个实验中,我学到了如何使用MFC进行简单的网络编程,特别是利用CAsyncSocket类来实现服务器端和客户端的通信。MFC提供了CAsyncSocket类作为基础网络编程的工具,它能够处理异步的套接字通信,使得开发网络应用变得相对简单。在实现服务器和客户端的通信过程中,需要派生CAsyncSocket类来创建自定义的套接字类,分别用于服务器端和客户端。这样的设计使得代码结构清晰,易于维护。在实验中,我学到了如何使用CArchiveCSocketFile类来实现消息的序列化和反序列化。这是因为CAsyncSocket类的SendReceive方法需要传递字符数组,而通过CArchive可以方便地将复杂的数据结构进行序列化,便于在网络上传输。在网络编程中,错误处理和异常处理是非常重要的。在实验中,我学到了如何通过返回值和错误码来判断网络操作是否成功,并在失败时进行适当的处理。同时,异常处理也是必不可少的,尤其是在动态内存分配等情况下,要注意避免内存泄漏。在服务器端和客户端的交互中,我学到了如何在对话框类和套接字类之间进行信息传递。通过在套接字类中添加对话框类的指针,并在构造函数中进行初始化,可以在套接字操作时方便地更新对话框中的界面。总体而言,这个实验让我更深入地理解了MFC框架下的网络编程,并掌握了一些基本的技巧和方法。网络通信是软件开发中一个重要的方向,通过这个实验,我对网络编程有了更实际的认识。

参考资料:一个使用CSocket类的网络通信实例

代码地址:https://github.com/zsc118/MFC-exercises


  1. 这里不能直接包含头文件,否则会出现重定义错误。 ↩︎

  2. 具体原因参见error C2065: “IDD_DIALOG1”: 未声明的标识符 .↩︎

  • 25
    点赞
  • 19
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
网络编程,当然要用到Windows Socket(套接字)技术。Socket相关的操作由一系列API函数来完成,比如socket、bind、listen、connect、accept、send、sendto、recv、recvfrom等。调用这些API函数有一定的先后次序,有些函数的参数还比较复杂,对于开发者来说,不是很好用。于是,微软的MFC提供了两个类:CAsyncSocket和CSocket,极大地方便了Socket功能的使用。   CAsyncSocket类在较低层次上封装了Windows Socket API,并且通过内建一个(隐藏的)窗口,实现了适合Windows应用的异步机制(Windows Socket API默认情况下工作在阻塞模式,不方便直接在消息驱动的Windows程序上使用)。CSocket类从CAsyncSocket类派生,进一步简化了Socket功能的应用。不过很遗憾,正因为这两个类都内建了一个窗口,它们并不是线程安全的(thread-safe);如果要在多线程环境下应用Socket功能,建议自行封装Socket API函数。 基于TCP的socket编程的服务器端程序流程如下: 1、创建套接字 2、将套接字绑定到一个本地地址和端口号上(bind) 3、将套接字设为监听模式,准备接受客户请求(listen) 4、等待客户请求,请求到来时接受请求,建立链接,并返回 一个新的基于此次通信的套接字(accept) 5、用返回的套接字和客户端进行通信(send、recv) 6、返回,等待另一客户请求 7、关闭套接字 基于TCP的socket编程的客户端程序流程如下: 1、创建套接字 2、向服务器端发送请求(connect) 3、和服务器端进行通信(send、recv) 4、关闭套接字 基于UDP的socket编程的服务器端程序流程如下: 1、创建套接字 2、将套接字绑定到本地地址和端口号上(bind) 3、等待接收数据(recvfrom) 4、关闭套接字 基于UDP的socket编程的客户端程序流程如下: 1、创建套接字 2、和服务器端进行通信(sendto) 3、关闭套接字 异步方式指的是发送方不等接收方响应,便接着发下个数据包的通信方式;而同步指发送方发出数据后,等收到接收方发回的响应,才发下一个数据包的通信方式。   阻塞套接字是指执行此套接字的网络调用时,直到成功才返回,否则一直阻塞在此网络调用上,比如调用recv()函数读取网络缓冲区中的数据,如果没有数据到达,将一直挂在recv()这个函数调用上,直到读到一些数据,此函数调用才返回;而非阻塞套接字是指执行此套接字的网络调用时,不管是否执行成功,都立即返回。比如调用recv()函数读取网络缓冲区中数据,不管是否读到数据都立即返回,而不会一直挂在此函数调用上。在实际Windows网络通信软件开发中,异步非阻塞套接字是用的最多的。平常所说的C/S(客户端/服务器)结构的软件就是异步非阻塞模式的。   对于这些概念,初学者的理解也许只能似是而非,我将用一个最简单的例子说明异步非阻塞Socket的基本原理和工作机制。目的是让初学者不仅对Socket异步非阻塞的概念有个非常透彻的理解,而且也给他们提供一个用Socket开发网络通信应用程序的快速入门方法。操作系统是Windows 98(或NT4.0),开发工具是Visual C++6.0。   MFC提供了一个异步类CAsyncSocket,它封装了异步、非阻塞Socket的基本功能,用它做常用的网络通信软件很方便。但它屏蔽了Socket的异步、非阻塞等概念,开发人员无需了解异步、非阻塞Socket的原理和工作机制。因此,建议初学者学习编网络通信程序时,暂且不要用MFC提供的类,而先用Winsock2 API,这样有助于对异步、非阻塞Socket编程机制的理解。
MFC(Microsoft Foundation Class)是微软公司提供的一个基于WindowsC++应用程序框架。MFC包含了许多类和函数库,可以帮助开发者更轻松地开发Windows应用程序。网络编程MFC框架中的一部分,通过MFC可以实现基于TCP或UDP协议的网络编程MFC中的网络编程主要依靠Windows Socket API来实现。Windows Socket API是Windows操作系统提供的一套网络编程接口,包括了TCP/IP协议栈以及与之相关的函数库和数据结构。MFC封装了Windows Socket API,提供了更加简单易用的网络编程接口。 以下是一个基于MFC的TCP客户端示例代码: ```cpp // 基于MFC的TCP客户端示例代码 #include "stdafx.h" #include "MFCNetworkProgramming.h" #include "MFCNetworkProgrammingDlg.h" #ifdef _DEBUG #define new DEBUG_NEW #endif // CMFCNetworkProgrammingApp BEGIN_MESSAGE_MAP(CMFCNetworkProgrammingApp, CWinApp) ON_COMMAND(ID_HELP, &CWinApp::OnHelp) END_MESSAGE_MAP() // CMFCNetworkProgrammingApp 构造 CMFCNetworkProgrammingApp::CMFCNetworkProgrammingApp() { // TODO: add construction code here, // Place all significant initialization in InitInstance } // 唯一的 CMFCNetworkProgrammingApp 对象 CMFCNetworkProgrammingApp theApp; // CMFCNetworkProgrammingApp 初始化 BOOL CMFCNetworkProgrammingApp::InitInstance() { // 初始化 MFC 和通用控件 AfxEnableControlContainer(); // 创建对话框 CMFCNetworkProgrammingDlg dlg; m_pMainWnd = &dlg; INT_PTR nResponse = dlg.DoModal(); // 删除对话框对象 if (nResponse == IDOK) { // TODO: Place code here to handle when the dialog is // dismissed with OK } else if (nResponse == IDCANCEL) { // TODO: Place code here to handle when the dialog is // dismissed with Cancel } // 删除该应用程序的最上层窗口,除非该应用程序是使用 // Control Panel 来关闭的或者有其他窗口在等待退出。 return FALSE; } ``` 这是一个基本的MFC框架,它创建了一个对话框,并将其作为程序的主窗口。接下来我们需要在对话框中实现TCP客户端的功能。以下是一个基于MFC的TCP客户端示例代码: ```cpp // 基于MFC的TCP客户端示例代码 #include "stdafx.h" #include "MFCNetworkProgramming.h" #include "MFCNetworkProgrammingDlg.h" #ifdef _DEBUG #define new DEBUG_NEW #endif // CMFCNetworkProgrammingDlg 对话框 CMFCNetworkProgrammingDlg::CMFCNetworkProgrammingDlg(CWnd* pParent /*=NULL*/) : CDialog(CMFCNetworkProgrammingDlg::IDD, pParent) { m_hIcon = AfxGetApp()->LoadIcon(IDR_MAINFRAME); } void CMFCNetworkProgrammingDlg::DoDataExchange(CDataExchange* pDX) { CDialog::DoDataExchange(pDX); DDX_Control(pDX, IDC_SERVER_IP, m_serverIP); DDX_Control(pDX, IDC_SERVER_PORT, m_serverPort); DDX_Control(pDX, IDC_DATA, m_data); } BEGIN_MESSAGE_MAP(CMFCNetworkProgrammingDlg, CDialog) ON_WM_PAINT() ON_WM_QUERYDRAGICON() //}}AFX_MSG_MAP ON_BN_CLICKED(IDC_CONNECT, &CMFCNetworkProgrammingDlg::OnBnClickedConnect) ON_BN_CLICKED(IDC_SEND, &CMFCNetworkProgrammingDlg::OnBnClickedSend) END_MESSAGE_MAP() // CMFCNetworkProgrammingDlg 消息处理程序 BOOL CMFCNetworkProgrammingDlg::OnInitDialog() { CDialog::OnInitDialog(); // 设置此对话框的图标。当应用程序主窗口不是对话框时, // 框架将自动执行此操作 SetIcon(m_hIcon, TRUE); // 设置大图标 SetIcon(m_hIcon, FALSE); // 设置小图标 // TODO: Add extra initialization here return TRUE; // 除非将焦点设置到控件,否则返回 TRUE } // 如果添加了最小化按钮,则需要以下代码来绘制该图标。 // 对于使用文档/视图模型的 MFC 应用程序,这将由框架自动完成。 void CMFCNetworkProgrammingDlg::OnPaint() { if (IsIconic()) { CPaintDC dc(this); // 用于绘制的设备上下文 SendMessage(WM_ICONERASEBKGND, reinterpret_cast<WPARAM>(dc.GetSafeHdc()), 0); // 使图标在工作区矩形中居中 int cxIcon = GetSystemMetrics(SM_CXICON); int cyIcon = GetSystemMetrics(SM_CYICON); CRect rect; GetClientRect(&rect); int x = (rect.Width() - cxIcon + 1) / 2; int y = (rect.Height() - cyIcon + 1) / 2; // 绘制图标 dc.DrawIcon(x, y, m_hIcon); } else { CDialog::OnPaint(); } } //当用户拖动最小化窗口时系统调用此函数取得光标 //显示。 HCURSOR CMFCNetworkProgrammingDlg::OnQueryDragIcon() { return static_cast<HCURSOR>(m_hIcon); } void CMFCNetworkProgrammingDlg::OnBnClickedConnect() { // 创建套接字 m_socket.Create(); // 获取服务器地址和端口 CString strIP, strPort; m_serverIP.GetWindowText(strIP); m_serverPort.GetWindowText(strPort); // 连接服务器 if (m_socket.Connect(strIP, _ttoi(strPort)) == FALSE) { AfxMessageBox(_T("连接服务器失败")); } else { AfxMessageBox(_T("连接服务器成功")); } } void CMFCNetworkProgrammingDlg::OnBnClickedSend() { // 获取发送数据 CString strData; m_data.GetWindowText(strData); // 发送数据 if (m_socket.Send(strData, strData.GetLength()) == SOCKET_ERROR) { AfxMessageBox(_T("发送数据失败")); } else { AfxMessageBox(_T("发送数据成功")); } } ``` 以上代码实现了一个基于MFC的TCP客户端,用户可以在界面上输入服务器地址、端口和数据,然后点击连接按钮进行连接,点击发送按钮可以向服务器发送数据。需要注意的是,在实际使用中需要根据具体情况进行相应的修改和完善。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

zsc_118

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值