MFC网络编程2——异步套接字

 从上一节(MFC网络编程1——网络基础及套接字 )中,我们了解了网络的部分基础知识以及套接字的使用,这一节,我们学习异步套接字的使用。
 Windows套接字在两种模式下执行I/O操作,阻塞模式和非阻塞模式。在阻塞模式下,在I/O操作完成前,执行操作的Winsock函数会一直等待下去,不会立即返回,例如,程序中调用了recvfrom函数后,如果这时网络上没有数据传送过来,该函数就会阻塞程序的执行,从而导致调用线程暂停运行。
 在非阻塞模式下,Winsock函数无论如何都会立即返回,在该函数执行的操作完成之后,系统会采用某种方式将操作结果通知给调用线程。在很多情况下,阻塞方式会影响应用程序的性能,所以有时需要采用非阻塞方式实现网络应用程序。Windows Sockets为了支持Windows消息驱动机制,使应用程序开发者能够方便的处理网络通信,它对网络事件采用了基于消息的异步存取策略。Windows Sockets的异步选择函数WSAAsyncSelect提供了消息机制的网络事件选择,当使用它登记的网络事件发生时,Windows应用程序相应的窗口函数将收到一个消息,消息中指示了发生的网络事件,以及与该事件相关的一些信息。
 因此,可以针对不同的网络事件进行登记,例如,如果登记一个网络读取事件,一旦有数据到来,就会触发这个事件,操作系统就会通过一个消息来通知调用线程,后者就可以在相应的消息响应函数中接受到这个数据。因为是在该数据到来之后,操作系统发出的通知,所以这时肯定能够接收到数据。采用异步套接字能够有效地提高应用程序的性能。
 这一节,我们利用异步套接字完成一个聊天室程序,如下图所示。项目工程下载链接为:
 https://download.csdn.net/download/mary288267/88344113
MFC网络编程2——异步套接字_第1张图片

1 相关函数

 Windows Socket规范提供了一组基于Berkeley套接字函数的扩展函数。这些扩展函数在实现Socket功能的基础上,还允许基于消息或函数进行处理,处理异步网络事件,开启重叠I/O功能。除了WSAStartup()函数和WSACleanup()函数外,编写Socket程序可以不使用这些扩展API函数,但是建议使用这些扩展函数以保持与Windows编程模式一致。
 下面,为了使用异步套接字进行编程,我们将介绍几个比较重要的扩展函数,这些扩展函数均是以WSA开头。

1.1 WSAStartup

 WSAStartup函数将初始化进程使用的Winsock DLL(WS2_32.DLL),原型如下

int WSAAPI WSAStartup(
[in] WORD wVersionRequested,
[out] LPWSADATA lpWSAData
);

参数
 [in] wVersionRequested: 调用方可以使用的最高版本的 Windows 套接字规范。 高序字节指定次要版本号;低序字节指定主版本号。
 [out] lpWSAData: 指向 WSADATA 数据结构的指针,用于接收 Windows 套接字实现的细节。

1.2 WSACleanup

 WSACleanup函数将终止程序对套接字库Winsock 2 DLL(WS2_32.DLL)的使用,该函数的原型声明如下。

int WSAAPI WSACleanup();

1.3 WSASocket

 Winsock库中的扩展函数WSASocket将创建套接字,其原型声明如下:

SOCKET WSAAPI WSASocketW(
[in] int af,
[in] int type,
[in] int protocol,
[in] LPWSAPROTOCOL_INFOW lpProtocolInfo,
[in] GROUP g,
[in] DWORD dwFlags
);

 参数
 [in] af:地址族。对于TCP/IP协议的套接字,只能是AF_INIET。
 [in] type:套接字的类型规范,具体如下表所示。在 Windows 套接字 1.1 中,唯一可能的套接字类型是 SOCK_DGRAM 和 SOCK_STREAM。SOCK_STREAM产生流式套接字;SOCK_DGRAM产生数据报套接字。

类型 含义
SOCK_STREAM 一种套接字类型,它通过 OOB 数据传输机制提供排序的可靠双向基于连接的字节流。 此套接字类型使用 Internet 地址系列 (AF_INET 或AF_INET6) 的传输控制协议 (TCP) 。
SOCK_DGRAM 支持数据报的套接字类型,这些数据报是固定 (通常较小) 最大长度的无连接、不可靠的缓冲区。 此套接字类型对 Internet 地址系列 (AF_INET 或AF_INET6) 使用用户数据报协议 (UDP) 。
SOCK_RAW 一种套接字类型,它提供允许应用程序操作下一层协议标头的原始套接字。 若要操作 IPv4 标头,必须在套接字上设置 IP_HDRINCL 套接字选项。 若要操作 IPv6 标头,必须在套接字上设置 IPV6_HDRINCL 套接字选项。
SOCK_RDM 提供可靠消息数据报的套接字类型。 此类型的一个示例是 Windows 中的实用常规多播 (PGM) 多播协议实现,通常称为 可靠的多播编程。仅当安装了可靠多播协议时,才支持此 类型值。
SOCK_SEQPACKET 提供基于数据报的伪流数据包的套接字类型。

 [in] protocol:要使用的协议。如果指定值 0,那么系统就会根据地址格式和套接字的类别,自动选择一个合适的协议。
 [in] lpProtocolInfo:指向 WSAPROTOCOL_INFO 结构的指针,该结构定义要创建的套接字的特征。 如果此参数为 NULL,则WinSock2.DLL使用前三个参数来决定使用哪一个服务提供者。如果此参数不为NULL,则套接字绑定到与指定的结构WSAPROTOCOL_INFO相关的提供者。
 [in] g:保留。
 [in] dwFlags:一组用于指定其他套接字属性的标志。

1.4 WSAAsyncSelect

 WSAAsyncSelect函数为指定的套接字请求基于Windows消息的网络事件通知,并自动将该套接字设置为非阻塞模式。

int WSAAPI WSAAsyncSelect(
[in] SOCKET s,
[in] HWND hWnd,
[in] u_int wMsg,
[in] long lEvent
);

 参数
 [in] s:标识需要事件通知的套接字的描述符。
 [in] hWnd:标识在发生网络事件时接收消息的窗口的句柄。
 [in] wMsg:发生网络事件时要接收的消息。
 [in] lEvent:位掩码,指定应用程序感兴趣的网络事件的组合。
 指定应用程序感兴趣的网络事件,该参数可以是下表中列出的值之一,并且可以用位或操作构造多个事件。

含义
FD_READ 设置为接收读取就绪通知。
FD_WRITE 想要接收准备写入的通知。
FD_OOB 想要接收有关 OOB 数据到达的通知。
FD_ACCEPT 想要接收传入连接的通知。
FD_CONNECT 想要接收已完成连接或多点联接操作的通知。
FD_CLOSE 想要接收套接字关闭的通知。
FD_QOS 想要接收套接字服务质量 (QoS) 更改的通知。
FD_GROUP_QOS 想要接收有关套接字组服务质量 (QoS) 更改的通知, (保留以供将来用于套接字组) 。 保留。
FD_ROUTING_INTERFACE_CHANGE 想要接收指定目标 () 的路由接口更改通知。
FD_ADDRESS_LIST_CHANGE 想要接收套接字协议系列的本地地址列表更改通知。

1.5 WSARecvFrom

 WSARecvFrom函数接收数据报类型的数据,并保存数据发送方的地址。

int WSARecvFrom(
SOCKET s,
LPWSABUF lpBuffers,
DWORD dwBufferCount,
LPDWORD lpNumberOfBytesRecvd,
LPDWORD lpFlags,
struct sockaddr FAR* lpFrom,
LPINT lpFromlen,
LPWSAOVERLAPPED lpOverlapped,
LPWSAOVERLAPPED_COMPLETION_ROUTINE lpCompletionRoutine
);

 参数:
 s:标识套接字的描述符。
 lpBuffers:一个指向WSABUF结构数组的指针。每个WSABUF结构包含缓冲区的指针和缓冲区的大小。
 dwBufferCount:lpBuffers数组中WSABUF结构的数目。
 lpNumberOfBytesRecvd:如果接收操作立即完成,则为一个指向所接收数据字节数的指针。
 lpFlags:一个指向标志位的指针。
 lpFrom:(可选)指针,指向重叠操作完成后存放源地址的缓冲区。
 lpFromlen:指向from缓冲区大小的指针,仅当指定了lpFrom才需要。
 lpOverlapped:指向WSAOVERLAPPED结构的指针(对于非重叠套接口则忽略)。
 lpCompletionRoutine:一个指向接收操作完成后调用的完成例程的指针。(对于非重叠套接口则忽略)。

1.6 WSASendTo

 WSASendTo函数使用重叠的I/O(如果适用)将数据发送到特定目标。

int WSAAPI WSASendTo(
[in] SOCKET s,
[in] LPWSABUF lpBuffers,
[in] DWORD dwBufferCount,
[out] LPDWORD lpNumberOfBytesSent,
[in] DWORD dwFlags,
[in] const sockaddr *lpTo,
[in] int iTolen,
[in] LPWSAOVERLAPPED lpOverlapped,
[in] LPWSAOVERLAPPED_COMPLETION_ROUTINE lpCompletionRoutine
);

 参数:
 [in] s:标识一个套接字(可能已连接)的描述符。
 [in] lpBuffers:指向 WSABUF 结构数组的指针。 每个 WSABUF 结构都包含指向缓冲区的指针和缓冲区的长度(以字节为单位)
 [in] dwBufferCount:lpBuffers 数组中的 WSABUF 结构数。
 [out] lpNumberOfBytesSent:如果 I/O 操作立即完成,则指向此调用发送的字节数的指针。如果 lpOverlapped 参数不是 NULL,请对此参数使用 NULL,以避免潜在的错误结果。 仅当 lpOverlapped 参数不为 NULL 时,此参数才能为 NULL。
 [in] dwFlags:用于修改 WSASendTo 函数调用行为的标志。
 [in] lpTo:指向 SOCKADDR 结构中目标套接字地址的可选指针。
 [in] iTolen:lpTo 参数中地址的大小(以字节为单位)。
 [in] lpOverlapped:对于未重叠的套接字) , (忽略指向 WSAOVERLAPPED 结构的指针。
 [in] lpCompletionRoutine:类型_In_opt_ LPWSAOVERLAPPED_COMPLETION_ROUTINE 指向完成发送操作时调用的完成例程的指针, (忽略未重叠的套接字) 。

1.7 gethostbyname

 gethostbyname不属于套接字的扩展函数。该函数从主机数据库中获取主机名相对应的IP地址。

struct hostent* FAR gethostbyname(
__in const char* name
);

2 网络聊天室的实现

 下面采用基于消息的异步套接字来实现一个带图形界面的网络聊天室程序。
 首先,新建一个基于对话框的工程,工程取名:ChatAsync。

2.1 设置对话框控件

 该对话框资源上已有的控件全部删除,然后添加一些控件,并设置它们相关的属性,结果下图所示:
MFC网络编程2——异步套接字_第2张图片
 该对话框上各控件的ID及说明如下(按控件在对话框上从上到下、从左到右的顺序)。

控件名称 ID 说明
接收组框 IDC_STATIC 标示作用
接收编辑框 IDC_EDT_RECV 显示所接受的数据
发送组框 IDC_STATIC 标示作用
IP地址控件 IDC_IPADDRESS1 允许用户按照点分十进制格式输入IP地址
发送编辑框 IDC_EDT_SEND 允许用户输入将要发送的内容
发送按钮 IDC_BTN_SEND 单击按钮,就将发送编辑框中的内容发送给聊条的对方
主机名编辑框 IDC_EDT_HOSTNAME 允许用户输入对方主机名

2.2 加载套接字库

 由于AfxSocketInit只能加载1.1版本的套接字库,而本程序需要使用套接字库2.0版本的一些函数,因此调用WSAStartup函数初始化程序所使用的套接字库。套接字库的初始化操作应该放在app类中的InitInstance函数中。

BOOL CChatAsyncApp::InitInstance()
{
	//初始化套接字库
	WORD wVersionRequested;
	WSADATA wsaData;
	int err;
	wVersionRequested = MAKEWORD(2, 2);
	err = WSAStartup(wVersionRequested, &wsaData);
	if (err != 0) {
		return FALSE;
	}

	/* Confirm that the WinSock DLL supports 2.2.*/
	/* Note that if the DLL supports versions greater    */
	/* than 2.2 in addition to 2.2, it will still return */
	/* 2.2 in wVersion since that is the version we      */
	/* requested.                                        */
	if (LOBYTE(wsaData.wVersion) != 2 || HIBYTE(wsaData.wVersion) != 2)
	{
		WSACleanup();
		return FALSE;
	}

......省略
}

 请注意本实现文件需要包含头文件:winsock2.h,为了方便,可以将此头文件放在stdafx.h中;另外,本工程需要链接 ws2_32.lib引入库文件。

2.3 创建并初始化套接字

 接下来创建并初始化套接字,在dialog类中增加一个SOCKET类型的成员变量m_socket,即套接字描述符。然后,在CChatAsyncDlg类中添加下面成员函数的声明:

BOOL InitSocket();

 其实现代码为:

BOOL CChatAsyncDlg::InitSocket()
{
	m_socket = WSASocket(AF_INET, SOCK_DGRAM, 0, NULL, 0, 0);
	if (INVALID_SOCKET == m_socket)
	{
		MessageBox(_T("创建套接字失败!"));
		return FALSE;
	}

	SOCKADDR_IN addrSock;
	addrSock.sin_addr.S_un.S_addr = htonl(INADDR_ANY);
	addrSock.sin_family = AF_INET;
	addrSock.sin_port = htons(6000);

	if (SOCKET_ERROR == bind(m_socket, (SOCKADDR*)&addrSock, sizeof(SOCKADDR)))
	{
		MessageBox(_T("绑定失败!"));
		return FALSE;
	}

	if (SOCKET_ERROR == WSAAsyncSelect(m_socket, m_hWnd, UM_SOCK, FD_READ))
	//if (SOCKET_ERROR == WSAEventSelect(m_socket, m_hWnd, UM_SOCK, FD_READ))
	{
		MessageBox(_T("注册网络读取事件失败!"));
		return FALSE;
	}

	return TRUE;
}

 在实现代码中,首先用WSASocket函数创建了套接字。然后用bind函数将套接字绑定到本地IP地址和端口上(端口号6000)。然后,调用WSAAsyncSelect请求一个基于Windows消息的网络事件通知,该函数的第1个参数就是标识请求网络事件通知的套接字描述符;第2个参数就是该对话框的窗口句柄,即CChatAsyncDlg的m_hWnd成员;第3个参数指定一个自定义消息(UM_SOCK),一旦指定的网络事件发生时,操作系统就会发送该自定义的消息通知调用线程;第4个参数是注册的事件,本例注册了一个读取事件(FD_READ)。这样,一旦有数据到来,就会触发FD_READ事件,系统就会通过UM_SOCK这则消息来通知调用线程,于是,在该消息的响应函数中接受数据,就可以接收到数据了。
 可以在CChatAsyncDlg类的OnInitDialog函数中调用这个函数,以便程序完成套接字的初始化工作。
 在CChatAsyncDlg类的头文件中,定义自定义消息:UM_SOCK,定义代码如下所示:

#define UM_SOCK WM_USER+1

2.4 实现接收端功能

 这里应注意,在注册的事件发生后,操作系统向调用进程发送相应的消息时,还会将该消息相应的信息一起传递给调用进程。事实上,自定义消息的消息映射宏为:

ON_MESSAGE(message, memberFxn )

 消息响应函数(memberFxn )的原型为:

afx_msg LRESULT (CWnd:: * )(WPARAM, LPARAM).

 因此,信息实际上是通过WPARAM和LPARAM传递的。
 在CChatAsyncDlg类的头文件中添加下列代码:


#define UM_SOCK WM_USER+1	//定义自定义消息
class CChatAsyncDlg : public CDialogEx
{
	.......省略
	afx_msg LRESULT OnSock(WPARAM wParam, LPARAM lParam);//声明自定义消息的响应函数
	DECLARE_MESSAGE_MAP()
	.......省略
};

 在实现文件中,增加关于UM_SOCK的消息映射宏

BEGIN_MESSAGE_MAP(CChatAsyncDlg, CDialogEx)
	.......省略
	ON_MESSAGE(UM_SOCK,OnSock)
	.......省略
END_MESSAGE_MAP()

 以及OnSock函数的实现

LRESULT CChatAsyncDlg::OnSock(WPARAM wParam, LPARAM lParam)
{
	switch (LOWORD(lParam))
	{
	case FD_READ:
	{
		CString str;
		CString strTemp;
		WSABUF wsabuf;
		wsabuf.buf = new char[200];
		wsabuf.len = 200;
		DWORD dwRead;
		DWORD dwFlag = 0;
		SOCKADDR_IN addrFrom;
		int len = sizeof(SOCKADDR);
		if (SOCKET_ERROR == WSARecvFrom(m_socket, &wsabuf, 1, &dwRead, &dwFlag,
			(SOCKADDR*)&addrFrom, &len, NULL, NULL))
		{
			MessageBox(_T("接受数据失败!"));
			return 0;
		}

		HOSTENT* pHost;
		pHost = gethostbyaddr((char*)&addrFrom.sin_addr.S_un.S_addr, 4, AF_INET);

		//注意,这里面从char*→CSTring,费了一些工夫,CString构造函数里面不能将char*转为CString,
		//但是赋值函数可以,神奇
		CString psz1;
		//psz1=inet_ntoa(addrFrom.sin_addr);
		psz1 = pHost->h_name;
		CString psz2;
		psz2 = CA2T(wsabuf.buf);
		str.Format(_T("%s说:%s"), psz1, psz2);
		str += _T("\r\n");
		GetDlgItemText(IDC_EDT_RECV, strTemp);
		str += strTemp;
		SetDlgItemText(IDC_EDT_RECV, str);
		break;
	}
	default:
		break;
	}
	return 0;
}

 请注意响应函数中的switch语句,由于基于套接字上请求网络事件通知时,可以同时请求多个网络事件,也就说,不但可以请求FD_READ网络读取事件,还可以同时请求FD_WRITE网络写入事件,所以说需要用switch语句区分各类事件。
 通过读取lParam参数的低位字,就可以知道当前发生的网络事件类型。然后用WSARecvFrom接收数据。

2.5 实现发送端的功能

 主界面上发送按钮的单击消息响应函数为

void CChatAsyncDlg::OnBnClickedBtnSend()
{
	USES_CONVERSION;

	DWORD dwIP;
	CString strSend;
	WSABUF wsaBuf;
	DWORD dwSend;
	int len;
	SOCKADDR_IN addrTo;
	CString sHostName;
	HOSTENT* pHost;
	if (GetDlgItemText(IDC_EDT_HOSTNAME, sHostName), sHostName == _T(""))
	{
		((CIPAddressCtrl*)GetDlgItem(IDC_IPADDRESS1))->GetAddress(dwIP);
		addrTo.sin_addr.S_un.S_addr = htonl(dwIP);
	}
	else
	{
		pHost = gethostbyname(T2A(sHostName));
		if (!pHost)
			return;

		addrTo.sin_addr.S_un.S_addr = *((DWORD*)pHost->h_addr_list[0]);
	}

	addrTo.sin_family = AF_INET;
	addrTo.sin_port = htons(6000);

	//这里面也有个特殊处理,CT2A不可以用,但是T2A可以用,不知为何
	//另外,因为传递的是字节,所有要用strlen计算字节数之后再加1
	GetDlgItemText(IDC_EDT_SEND, strSend);
	len = strSend.GetLength();	
	wsaBuf.buf = T2A(strSend);
	wsaBuf.len = len + 1;
	wsaBuf.len = strlen(wsaBuf.buf) + 1;

	SetDlgItemText(IDC_EDT_SEND, _T(""));
	if (SOCKET_ERROR == WSASendTo(m_socket, &wsaBuf, 1, &dwSend, 0,
		(SOCKADDR*)&addrTo, sizeof(SOCKADDR), NULL, NULL))
	{
		MessageBox(_T("发送数据失败!"));
		return;
	}
}

 在发送数据时,首先要根据用户输入的主机名获取IP地址,这是通过gethostbyname实现的;然后通过WSASendTo将数据发送出去。

2.6 终止套接字库的使用

 在APP类析构时,应该终止对套接字库的使用。

CChatAsyncApp::~CChatAsyncApp()
{
	WSACleanup();
}

 在对话框类析构时,应当关闭套接字。

CChatAsyncDlg::~CChatAsyncDlg()
{
	if (m_socket)
		closesocket(m_socket);	//关闭套接字
}

 以上为本程序的代码实现,本程序是在同一线程中实现了接收和发送端,如果采用阻塞套接字,可能会因为WSARecvFrom函数的阻塞调用而导致线程暂停运行。在编程网络应用程序时,采用异步选择机制可以提高网络应用程序的性能,如果在配合多线程技术,将大大提高所编写的网络应用程序的性能。

你可能感兴趣的:(网络编程,MFC,mfc,网络,c++)