栏目分类:
子分类:
返回
名师互学网用户登录
快速导航关闭
当前搜索
当前分类
子分类
实用工具
热门搜索
名师互学网 > IT > 软件开发 > 后端开发 > C/C++/C#

基于WSAAsyncSelect模型的服务端和客户端设计(MFC)

C/C++/C# 更新时间: 发布时间: IT归档 最新发布 模块sitemap 名妆网 法律咨询 聚返吧 英语巴士网 伯小乐 网商动力

基于WSAAsyncSelect模型的服务端和客户端设计(MFC)

目录
  • 1 效果展示
    • 1.1 服务器和客户端界面展示
    • 1.2 一对一之间通信的功能展示
    • 1.3 一对多之间通信的功能展示
  • 2 知识预备
    • 2.1 WSAAsyncSelect模型介绍
    • 2.2 WSAAsyncSelect模型的过程图
    • 2.3 与SELECT模型比较
    • 2.4 套接字WSAAsyncSelect模型实现
    • 2.5 网络事件种类
    • 2.6 WSAAsyncSelect模型的优势和不足
  • 3 代码展示
    • 3.1 服务端
      • 3.1.1 定义变量
      • 3.1.2 初始化界面
      • 3.1.3 启动服务器
      • 3.1.4 消息响应
      • 3.1.5 发送消息
      • 3.1.6 断开
    • 3.2 客户端
      • 3.2.1 连接服务器
      • 3.2.2 消息响应
      • 3.2.3 发送消息
  • 4 总结与思考
  • 5 源码下载

1 效果展示 1.1 服务器和客户端界面展示

1.2 一对一之间通信的功能展示

步骤:

  1. 启动服务器进行监听
  2. 客户端再连接服务器端
  3. 服务器与客户端互发消息
1.3 一对多之间通信的功能展示

步骤与前面一样

2 知识预备 2.1 WSAAsyncSelect模型介绍

    WSAAsyncSelect模型是Windows Sockets的一个异步I/O模型。利用该模型应用程序可以在一个套接字上,接收以Windows消息为基础的网络事件。Windows Sockets应用程序在创建套接字后,调用WSAAsyncSelect()函数注册感兴趣的网络事件。当该事件发生时Windows窗口收到消息,然后应用程序就可以对接收到的网络事件进行处理。

2.2 WSAAsyncSelect模型的过程图

    WSAAsyncSelect模型是非阻塞的。如图所示, Windows Sockets应用程序在调用recv)函数接收数据之前,调用WSAAsyncSelect()函数注册网络事件。WSAAsyncSelect()函数立即返回,线程继续运行。当系统中数据准备好时,向应用程序发送消息。应用程序接收到这个消息后,调用recv()函数接收数据。

2.3 与SELECT模型比较

SELECT模型介绍

    WSAAsyncSelect模型与Select模型的相同点是,他们都可以对Windows套接字应用程序所使用的多个套接字进行有效的管理。
但WSAAsyncSelect模型与Select模型相比存在以下不同。

  • WSAAsyncSelect模型是异步的。在应用程序中调用WSAAsyncSelect()函数,通知系统感兴趣的网络事件,该函数立即返回,应用程序继续运行。
  • 发生网络事件时,应用程序得到通知的方式不同。select()函数返回时,说明某个或者某些套接字满足可读可写的条件,应用程序需要使用FD_ISSET宏,判断套接字是否存在于可读可写集合中。而对于WSAAsyncSelect模型来说,当网络事件发生时,系统向应用程序发送消息。
  • WSAAsyncSelect模型应用在基于消息的Windows环境下,使用该模型时必须创建窗口.而Select模型广泛应用在Unix系统和Windows系统,使用该模型不需要创建窗口。
  • 应用程序中调用WSAAsyncSelect()函数后,自动将套接字设置为非阻塞模式。而应用程序中调用select()函数后,并不能改变该套接字的工作方式。
2.4 套接字WSAAsyncSelect模型实现

WSAAsyncSelect()函数功能是请求当网络事件发生时为套接字发送消息。该函数声明如下:

int WSAAsyncSelect(
  SOCKET s,
  HWND hWnd,
  unsigned int wMsg,
  long lEvent
  );
  • s:需要事件通知的套接字。
  • hWnd:当网络事件发生时接收消息的窗口句柄。
  • wMsg:当网络事件发生时窗口收到的消息。
  • lEvent:应用程序感兴趣的网络事件集合。

当应用程序中调用该函数后,自动将套接字设置为非阻塞模式。通常,应用程序声明的消息要比 Windows的WM_USER值大,以避免该消息与Windows预定义消息发生混淆。

2.5 网络事件种类
种类含义
FD_READ欲接收可读的通知
FD_WRITE欲接收可写读的通知
FD_ACCEPT欲接收等待接受连接的通知
FD_CONNECT欲接收一次连接或者多点jion操作完成的通知
FD_OOB欲接收有带外(OOB)数据到达的通知
FD_CLOSE欲接收套接字关闭的通知
FD_QoS欲接收套接字服务质量发生变化的通知
FD_GROUP_QoS欲接收套接字组服务质量发生变化的通知
FD_ROUTING_INTERFACE_CHANGE欲在指定方向上,与路由接口发生变化的通知
FD_ADDRESS_LIST_CHANGE欲接收针对套接字的协议家族,本地地址列表发生变化的通知

调用WSAAsyncSelect()函数如下所示:

WSAAsyncSelect (s,hwnd,WM_SOCKET,FD_CONNECT | FD_READ|ED_CLOSE);

应用程序在一个套接字上成功调用了WSAAsyncSelect之后,会在与hWnd窗口句柄对应的窗口例程中,以Windows消息的形式,接收网络事件通知。
窗口例程通常定义如下:

LRESULT CALLBACK WindowProc( 
     HWND hwnd,
     UINT uMsg,
     WPARAM wParam,
     LPARAM lParam
 );
  • hWnd:窗口句柄。
  • uMsg:消息。对Windows Sockets应用程序来说感兴趣的是在WSAAsyncSelect()函数中,由应用程序定义的消息。
  • wParam:消息参数。在Windows Sockets应用程序中,该参数指明发生网络事件的套接字。
  • lParam:消息参数。在Windows Sockets应用程序中,该参数低字节指明已经发生的网络事件。高字节包含可能出现的错误代码。
2.6 WSAAsyncSelect模型的优势和不足

优势:

  1. 该模型的使用方便了在基于消息的Windows环境下开发套接字的应用程序。开发人员可以像处理其他消息一样对网络事件消息进行处理。
  2. 该模型确保接收所有数据提供了很好的机制。通过注册FD_CLOSE网络事件,从容关闭服务器与客户端的连接保证了数据全部接收。

不足:

  1. 该模型局限在,他基于Windows的消息机制,必须在应用程序中创建窗口。当然,在开发中可以根据具体情况是否显示该窗口。MFC的CSocketWnd类就是用来创建一个不显示的窗口,并在该类中声明接收网络事件消息处理函数。
  2. 由于调用WSAAsyncSelect()函数后,自动将套接字设置为非阻塞状态。当应用程序为接收到网络事件调用相应函数时,未必能够成功返回。这无疑增加了开发人员使用该模型的难度。对于这一点可以从MFC CSocket类的Accept()、Receive()和Send()函数的实现得到验证。
3 代码展示 3.1 服务端 3.1.1 定义变量

在ServerDlg.h里

#define WM_SOCKET WM_USER+1000 //套接字消息
...
public:
	WSADATA wsd;//WSADATA变量
	SOCKET listenSocket;//服务器监听套接字
	SOCKET acceptSocket;//接受客户端连接请求的套接字
	sockaddr_in addr;
	sockaddr_in addr1;
	int n = 0;//客户端数
	SOCKET s[10];//套接字数组
	int flag = 0;
	vector socket_arr[100];//存放acceptsocket
	int count = 0;
	
	CListCtrl m_Listctrl;
	CEdit m_SendText;//发送消息内容
	CButton m_Send;//发送按钮
	CIPAddressCtrl m_Ip;//IP地址
	CEdit m_Port;//端口
	CButton m_Start;//启动
	afx_msg void OnBnClickedButton2();
	afx_msg void OnBnClickedButton1();
	CButton m_Close;//断开
	afx_msg void OnBnClickedButton3();
	CEdit m_num;//选择发送的对象
	void InitSocket();//初始化
...

在ServerDlg.cpp里

3.1.2 初始化界面
//初始化
void CServerDlg::InitSocket()
{
	//按钮初始化
	m_Send.EnableWindow(false);
	m_SendText.EnableWindow(false);
	m_Close.EnableWindow(false);
	//m_Listctrl.ShowScrollBar(SB_VERT, TRUE);
	//服务器地址ip和端口初始化
	char * strIP = "127.0.0.1";
	DWORD dwAddress = ntohl(inet_addr(strIP));
	m_Ip.SetAddress(dwAddress);
	m_Port.SetWindowText("8080");
}
3.1.3 启动服务器
//启动服务器
void CServerDlg::OnBnClickedButton2()
{
	CString str, str1;
	BYTE a, b, c, d;
	m_Ip.GetAddress(a, b, c, d);//获取服务器地址
	str.Format(_T("%d.%d.%d.%d"), a, b, c, d);//CString->int
	m_Port.GetWindowText(str1);//获取端口号

	//设置地址
	addr.sin_family = AF_INET;//地址族
	addr.sin_port = htons(_ttoi(str1));//端口号
	addr.sin_addr.S_un.S_addr = inet_addr(CT2CA(str));//IP地址
	//创建套接字
	listenSocket = ::socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
	//绑定套接字
	::bind(listenSocket, (sockaddr*)&addr, sizeof(addr));
	//监听套接字
	::listen(listenSocket, 5);
	//输出
	CString str2, str3;
	str2 = ::inet_ntoa(addr.sin_addr);//返回点分十进制的字符串
	str3.Format("%d", ntohs(addr.sin_port)); //ntohs主要是将网络字节转为主机字节
	m_Listctrl.InsertItem(count++, _T("服务器["+str2+":"+str3+"]已启动监听..."));
	//按钮变化
	m_Start.EnableWindow(false);
	m_Close.EnableWindow(true);
	//设置异步套接字
	::WSAAsyncSelect(listenSocket, this->m_hWnd, WM_SOCKET, FD_ACCEPT | FD_READ | FD_CLOSE);
}
3.1.4 消息响应
//消息响应函数
LRESULT CServerDlg::OnSocket(WPARAM wPARAm, LPARAM lParam) {
	CString str;
	switch (lParam) {
	case FD_ACCEPT:
	{
		//accept函数返回一个新的套接字,同时返回客户端的IP地址,初始化ClientAddr
		int len = sizeof(addr1);
		acceptSocket = ::accept(listenSocket, (sockaddr*)&addr1, &len);
		//添加套接字
		socket_arr->push_back(acceptSocket);
		flag++;
		n = n + 1;//客户端数加一
		CString str1, str2,str3;
		str1 = ::inet_ntoa(addr1.sin_addr);//返回点分十进制的字符串
		str2.Format("%d", ntohs(addr1.sin_port)); //ntohs主要是将网络字节转为主机字节
		str.Format("目前有%d个客户端n", n);
		str3.Format("%d", flag);
		
		m_Listctrl.InsertItem(count++, _T("用户"+str3+"[" + str1 + ":" + str2 + "]已经连接成功"));
		m_Listctrl.InsertItem(count++, _T(str));
		
		//按钮变化
		m_Send.EnableWindow(true);
		m_SendText.EnableWindow(true);
	}
	break;
	case FD_READ:
	{
		//接收消息
		char buf[100] = { '' };
		int ret = 0;
		int i = 0;
		//迭代器遍历
		for (vector::iterator it = socket_arr->begin(); it != socket_arr->end(); it++) {
			ret=::recv(*it, buf, 100, 0);
			SOCKADDR_IN addClient;
			int nLen = sizeof(addClient);
			//获取当前连接的客户端的IP地址和端口号,即初始化addClient
			getpeername(socket_arr->at(i), (sockaddr*)&addClient, &nLen);
			CString str, str1, str2;
			str.Format("%d", i + 1);
			str1 = ::inet_ntoa(addClient.sin_addr);
			str2.Format("%d", ntohs(addClient.sin_port));
			if (ret > 0) {//说明有数据
				m_Listctrl.InsertItem(count++, _T("用户" + str +"["+str1+":"+str2+ "]说:" + buf));
			}
			i++;
		}
		
	}
	break;
	case FD_CLOSE:
	{
		//欲接收客户端关闭情况
		int ret = 0;
		char buf[100] = { '' };
		int i = 0;
		for (vector::iterator it = socket_arr->begin(); it != socket_arr->end(); it++) {
			ret = ::recv(*it, buf, 100, 0);
			if (ret==0){
				//客户端数量减一
				n = n - 1;
				CString str;
				str.Format("目前有%d个客户端n", n);

				SOCKADDR_IN addClient;
				int nLen = sizeof(addClient);
				//获取当前连接的客户端的IP地址和端口号,即初始化addClient
				getpeername(*it, (sockaddr*)&addClient, &nLen);
				CString str1, str2,str3;
				str1 = ::inet_ntoa(addClient.sin_addr);
				str2.Format("%d", ntohs(addClient.sin_port));
				str3.Format("%d", i+1);

				m_Listctrl.InsertItem(count++, _T("用户"+str3+"[" + str1 + ":" + str2 + "]主动关闭"));
				m_Listctrl.InsertItem(count++, _T(str));
				//当没有客户端,按钮变化
				if (!(n > 0)) {
					m_Send.EnableWindow(false);
					m_SendText.EnableWindow(false);
				}
				//删除对应accepsocket
				it = socket_arr->erase(it);
				if (it == socket_arr->end()) {
					break;
				}
			}
		}
	}
	break;
	}
	return 0;
}
3.1.5 发送消息
//发送消息
void CServerDlg::OnBnClickedButton1()
{
	// TODO: 在此添加控件通知处理程序代码
	CString str = "";
	m_SendText.GetWindowText(str);//获取发送消息的内容
	if (str == "") {
		MessageBox(_T("发送消息不能为空"), _T("提示"));
	}
	else {
		CString str1;
		m_num.GetWindowText(str1);//选择客户端进行发送
		if (str1 == "") {
			MessageBox(_T("发送的人不能为空"), _T("提示"));
		}
		else {
			int num = _ttoi(str1);
			if (::send(socket_arr->at(num-1), CT2CA(str), str.GetLength(), 0) != SOCKET_ERROR) {
				m_Listctrl.InsertItem(count++, _T("服务器对用户" + str1 + "说:" + str));
				m_SendText.SetWindowText("");//清空
				m_num.SetWindowText("");
			}
			else {
				MessageBox(_T("发送消息失败"), _T("提示"));
			}
		}
		
	}
}
3.1.6 断开
//断开
void CServerDlg::OnBnClickedButton3()
{
	// TODO: 在此添加控件通知处理程序代码
	closesocket(listenSocket);
	closesocket(acceptSocket);
	WSACleanup();
	m_Send.EnableWindow(false);
	m_SendText.EnableWindow(false);
	m_Start.EnableWindow(true);
	m_Close.EnableWindow(false);
	m_Listctrl.InsertItem(count++, _T("服务器已关闭"));
}
3.2 客户端

仅展示连接和发送(依葫芦画瓢)

3.2.1 连接服务器
//客户端连接
void CClientDlg::OnBnClickedButton1()
{
	// TODO: 在此添加控件通知处理程序代码
	CString str, str1;//定义字符串变量
	int port;//定义端口
	BYTE a, b, c, d;
	m_IpAddress.GetAddress(a, b, c, d);//获取服务器地址
	str.Format(_T("%d.%d.%d.%d"), a, b, c, d);
	m_Port.GetWindowText(str1);//获取端口地址

	if (str=="" || str1=="") {
		MessageBox(_T("服务器地址或端口不能为空"), _T("提示"), MB_ICONEXCLAMATION);
	}
	else {
		//初始化套接字动态库
		if (WSAStartup(MAKEWORd(2, 2), &wsd) != 0) {
			MessageBox(_T("WSAStartup error:" + WSAGetLastError()), _T("提示"), MB_ICONEXCLAMATION);
		}

		//创建TCP Socket,流式套接字
		s = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
		if (s == INVALID_SOCKET) {
			MessageBox(_T("create socket error : " + WSAGetLastError()), _T("提示"), MB_ICONEXCLAMATION);
		}
		
		//设置地址
		port=_ttoi(str1);//字符串转整型
		addr.sin_port = ntohs(port);//端口
		addr.sin_addr.S_un.S_addr = inet_addr(CT2CA(str));//网络字节序
		addr.sin_family = AF_INET;//地址族
		//int i = 0;
		if (::connect(s, (sockaddr*)&addr, sizeof(addr))!= SOCKET_ERROR) {
			//设置异步套接字
			::WSAAsyncSelect(s, this->m_hWnd, WM_SOCKET, FD_READ | FD_CLOSE);
			m_ListCtrl.InsertItem(0, _T("连接服务器成功"));
			//设置按钮变化
			m_Close.EnableWindow(true);
			m_Connect.EnableWindow(false);
			m_Port.EnableWindow(false);
			m_IpAddress.EnableWindow(false);
			m_Send.EnableWindow(true);
			m_SendText.EnableWindow(true);
		}
		else {
			m_ListCtrl.InsertItem(0, _T("连接服务器失败,请重试"));
		}
	}
}
3.2.2 消息响应
//消息相应函数
LRESULT CClientDlg::OnSocket(WPARAM wPARAm, LPARAM lParam) {
	char buf[100] = { '' };
	switch (lParam) {
	case FD_READ:
	{
		//接收服务器消息
		::recv(s, buf, 100, 0);
		CString str1, str2;
		str1 = ::inet_ntoa(addr.sin_addr);
		str2.Format("%d", ntohs(addr.sin_port));
		m_ListCtrl.InsertItem(0, _T("服务器[" + str1 + ":" + str2 + "]:" + buf));
	}
	break;
	case FD_CLOSE:
	{
		//欲接收服务器关闭消息
		CString str1, str2;
		str1 = ::inet_ntoa(addr.sin_addr);
		str2.Format("%d", ntohs(addr.sin_port));
		//if (::WSAGetLastError() != WSAECONNRESET) {
			m_ListCtrl.InsertItem(0, _T("服务器[" + str1 + ":" + str2 + "]已关闭"));
			m_Connect.EnableWindow(true);
			m_Send.EnableWindow(false);
			m_SendText.EnableWindow(false);
		//}
		
	}
	break;
	}
	return 0;
}
3.2.3 发送消息
//发送
void CClientDlg::OnBnClickedButton2()
{
	// TODO: 在此添加控件通知处理程序代码
	CString str1;
	m_SendText.GetWindowText(str1);//获取发送消息内容
	if (str1 == "") {
		MessageBox(_T("发送消息不能为空"), _T("提示"));
	}
	else {
		::send(s, CT2CA(str1), str1.GetLength(), 0);
		m_ListCtrl.InsertItem(0, _T("我说:" + str1));
		m_SendText.SetWindowText("");//清空
	}
}
4 总结与思考

    在设计中遇到的一个最主要问题就是如何在服务端显示不同客户端的信息以及接收它们不同的信息。查阅了很多资料,解决方法如下:
1、如何在服务端显示不同的客户端信息?
此代码来源于ServerDlg.cpp中的消息响应函数中

//accept函数返回一个新的套接字,同时返回客户端的IP地址,初始化ClientAddr
int len = sizeof(addr1);
acceptSocket = ::accept(listenSocket, (sockaddr*)&addr1, &len);
//添加套接字
socket_arr->push_back(acceptSocket);
flag++;
n = n + 1;//客户端数加一
...
CString str1, str2,str3;
str1 = ::inet_ntoa(addr1.sin_addr);//返回点分十进制的字符串
str2.Format("%d", ntohs(addr1.sin_port)); //ntohs主要是将网络字节转为主机字节

1、每当接收到一个客户端连接,就把该套接字储存起来,这里我用的是vector容器,当然也可以用数组。
2、inet_ntoa函数主要是返回点分十进制的字符串,ntohs函数主要是是将网络字节转为主机字节

2、如何在服务端接收不同客户端发来的信息?

case FD_READ:
	{
		//接收消息
		char buf[100] = { '' };
		int ret = 0;
		int i = 0;
		//迭代器遍历
		for (vector::iterator it = socket_arr->begin(); it != socket_arr->end(); it++) {
			ret=::recv(*it, buf, 100, 0);
			SOCKADDR_IN addClient;
			int nLen = sizeof(addClient);
			//获取当前连接的客户端的IP地址和端口号,即初始化addClient
			getpeername(socket_arr->at(i), (sockaddr*)&addClient, &nLen);
			CString str, str1, str2;
			str.Format("%d", i + 1);
			str1 = ::inet_ntoa(addClient.sin_addr);
			str2.Format("%d", ntohs(addClient.sin_port));
			if (ret > 0) {//说明有数据
				m_Listctrl.InsertItem(count++, _T("用户" + str +"["+str1+":"+str2+ "]说:" + buf));
			}
			i++;
		}
	}
	break;

这里有一个非常重要的函数是getpeername,它用于获取与某个套接字关联的外地协议地址。
再加上迭代器遍历acceptsocket,就可以循环接收不同客户端发来的消息

那还有一个疑问?反过来服务端给不同客户端发送消息怎么办?

解决办法,给每一个客户端标上flag,在界面添加一个文本框,每次服务端需要选择用户来发送消息,这样就可以根据标号来确定是哪一个acceptsocket了。

CString str1;
m_num.GetWindowText(str1);//选择客户端进行发送
if (str1 == "") {
	MessageBox(_T("发送的人不能为空"), _T("提示"));
}
else {
	int num = _ttoi(str1);
	if (::send(socket_arr->at(num-1), CT2CA(str), str.GetLength(), 0) != SOCKET_ERROR) {
		m_Listctrl.InsertItem(count++, _T("服务器对用户" + str1 + "说:" + str));
		m_SendText.SetWindowText("");//清空
		m_num.SetWindowText("");
	}
	else {
		MessageBox(_T("发送消息失败"), _T("提示"));
	}
}
5 源码下载

源码下载

转载请注明:文章转载自 www.mshxw.com
本文地址:https://www.mshxw.com/it/847798.html
我们一直用心在做
关于我们 文章归档 网站地图 联系我们

版权所有 (c)2021-2022 MSHXW.COM

ICP备案号:晋ICP备2021003244-6号