IT数码 购物 网址 头条 软件 日历 阅读 图书馆
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
图片批量下载器
↓批量下载图片,美女图库↓
图片自动播放器
↓图片自动播放器↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁
 
   -> 网络协议 -> UDP/IP 事件选择模型 -> 正文阅读

[网络协议]UDP/IP 事件选择模型

事件选择模型核心:

为每一个socket创建一个事件对象,将socket和对应的事件对象绑定到一起,并投递给操作系统,由操作系统帮我们进行监视,当对应的socket有响应,该对应的事件会被置成有信号,我们获取该信号,进行分类处理。

UDP/IP 事件选择模型:

UDP/IP 是面向非连接的,不可靠的,基于数据报的传输层协议;

对于服务端只有一个socket(服务端socket),直接创建一个事件对象,将该socket和对应的事件对象进行绑定,并投递给系统,之后等待信号,获取信号,之后进行分类处理。

事件选择模型的逻辑:

1. 创建一个事件对象:

WSAEVENT WSAAPI WSACreateEvent();

2. 绑定并投递

  int WSAAPI WSAEventSelect
  (	// 函数功能:给事件绑上socket与对应的操作,并投递给操作系统
  		SOCKET   s,				// 被绑定的socket 最终,每个socket都会被绑定一个事件
  		WSAEVENT hEventObject,	// 事件对象 逻辑,就是讲参数1与参数2绑定在一起
  		long     lNetworkEvents	// 具体的事件	FD_READ FD_WRITE ...
  );

3. 循环等待事件产生信号

  DWORD WSAAPI WSAWaitForMultipleEvents(	// 作用:等待事件产生信号
  DWORD          cEvents,		// 事件个数
  const WSAEVENT *lphEvents,	// 事件列表
  BOOL           fWaitAll,		// 事件等待方式
  DWORD          dwTimeout,		// 超时间隔
  BOOL           fAlertable		// 这里填FALSE
  );

4. 获取信号,进行分类处理

int WSAAPI WSAEnumNetworkEvents(	// 获取事件类型,并将事件上的信号重置
  SOCKET             s,					// 对应的socket
  WSAEVENT           hEventObject,		// 对应的事件
  LPWSANETWORKEVENTS lpNetworkEvents	// 触发的事件类型 声明一个结构体指针
);

服务端代码:

#define _WINSOCK_DEPRECATED_NO_WARNINGS
#include <stdio.h>
#include <stdlib.h>
#include <winsock2.h>

#pragma comment(lib, "ws2_32.lib")

/*
	UDP/IP (User Datagram Protocol/Internet Protocol) 用户数据报协议
	TCP/IP 协议的特点:
		面向连接的,可靠的,基于字节流的传输层协议。
	UDP/IP 协议的特点:
		面向非连接的,不可靠的,基于数据报的传输层协议。
	1.基本C/S模型中服务端代码:
		UDP/IP 与 TCP/IP 的区别:
			UDP/IP:
				是面向非连接的,不用进行listen(进行监听)、不进行accept(接收连接)
				与客户端进行收发消息调用的是 recvfrom、sendto函数
			TCP/IP:
				是面向连接的,进行listen(进行监听)、进行accept(接收连接 本质创建客户端socket)
				与客户端进行收发消息调用的是 recv、send函数
	2.select模型服务端代码:
			UDP/IP:
				UDP/IP协议的C/S模型不存在傻等的问题,而select模型只是能让recvfrom更灵活一些
					select 代码逻辑:
						1.所有的socket装进一个集合FD_SET UDP只有一个socket即将服务端的socket装进集合中就可以了
						2.通过select函数,检测中的socket集合中那个有响应了,就是检测有没有消息来了,
						3.有消息了,则进行处理 调用 recvfrom
			TCP/IP:
				TCP/IP协议的C/S模型中accept、recv存在傻等问题,select模型是为了解决accept、recv傻等问题,
					select 代码逻辑:
						1.每个客户端都有socket,服务器也有自己的socket,将所有的socket装进一个数据结构里,即数组 fd_set
						2.通过select函数,遍历1中socket数组,当某个socket有响应,select就会通过其参数/返回值反馈出来,之后做相应的处理
						3.处理有响应的socket,
							判断检测到是的服务端socket,有客户端来链接了,调用accept(创建客户端socket)
							判断检测到的是客户端socket,客户端请求通信,调用send或者recv进行发收消息

windows处理用户行为的两种方式:
消息机制:
	核心:消息队列
		处理过程:
			所有的用户操作,比如鼠标,按下键盘键,点击软件的按钮... 等,
			所有的操作均是依次按顺序被记录,装进一个队列中
		特点:
			消息队列由操作系统维护,我们把消息取出,然后进行分类处理

事件机制:
	核心:事件集合
		处理过程:
			根据需求,为用户的特定操作绑定一个事件,事件由我们自己调用API创建,需要多少创建多少
			将事件投递系统,系统就帮我们进行监视着,创建事件不能无限创建,创建多了,系统就会卡
			如果操作发生了,比如用户按鼠标,该事件会被置成有信号
			直接获取有事件的信号,进行处理
		特点:
			所有的事件都是我们自定义的,系统只是我们置成有无信号

	3.事件选择模型服务端代码:
		UDP/IP事件选择:
			事件选择模型是为每一个socket绑定一个事件对象,(而UDP/IP服务端代码只有一个socket)
			并投递给操作系统,系统为我们进行监视着,当有客户端发来了消息,该事件就会被置成有信号的,
			那么我们获取该有信号的事件,进行处理
				代码逻辑:
					第一步:创建一个事件对象(变量)	WSACreateEvent
					第二步:将事件与socket绑定到一起,并指定监视类型(recvfrom,sendto)并投递给系统	WSAEventSelect
					第三步:循环等待事件产生信号	WSAWatiForMultipleEvents	该函数等待过程将所在线程挂起,释放CPU时间
					第四步:有信号的话,就进行分类处理		WSAEnumNetworkEvents
		
		TCP/IP事件选择:
			整体逻辑跟UDP/IP事件选择模型一致,注意的是TCP/IP是面向连接的,要进行accept处理
				代码逻辑:
					第一步:创建一个事件对象(变量)	WSACreateEvent
					第二步:为每一个事件对象绑定一个socket以及操作accept、read、close...,并投递给系统
					第三步:查看事件是否有信号		WSAWaitForMultipleEvents
					第四步:有信号的话就进行分类处理	WSAEnumNetworkEvents
*/
// 声明变量接收创建事件对象
HANDLE nCreEvn;

// 1.打开网络库 2.校验版本号
void OpenAndCheckWSAStartup();

// 3.创建服务端socket
SOCKET Sock_Server();

// 全局声明要创建 创建服务端socket
SOCKET socketServer;

// 4.绑定地址与端口号
void Bind(SOCKET sock);

// 5.实现事件选择模型核心代码
void Event_Select(SOCKET sock);

// 监视窗口回调函数
BOOL WINAPI fun(DWORD dwCtrlType);

int main()
{
	// 监视关闭窗口按钮
	SetConsoleCtrlHandler(fun, TRUE);

	// 1.打开网络库 2.校验版本号
	OpenAndCheckWSAStartup();

	// 3.创建服务端socket
	socketServer = Sock_Server();

	// 4.绑定地址与端口号
	Bind(socketServer);

	// 5.核心事件选择模型
	Event_Select(socketServer);

	//关闭事件对象
	WSACloseEvent(nCreEvn);
	//关闭socket
	closesocket(socketServer);
	//关闭网络库
	WSACleanup();
	system("pause");
	return 0;
}

// 1.打开网络库 2.校验版本号
void OpenAndCheckWSAStartup()
{
	// 定义WSAStartup函数中参数1 所需的版本号
	WORD wVersionRequestd = MAKEWORD(2, 2);	// 设置2.2版本号
	// 声明WSAStartup函数中参数1 所需的结构体
	WSADATA WSAData;
	// 打开网络库
	int nRet = WSAStartup(wVersionRequestd, &WSAData);
	if (0 != nRet)
	{
		//出错了
		int a = WSAGetLastError();
		printf("网络库打开失败 %d\n", a);
		return;
	}
	// 检验版本号
	if (2 != HIBYTE(WSAData.wVersion) || 2 != LOBYTE(WSAData.wVersion))
	{
		printf("校验版本号错误\n");
		// 关闭网络库
		WSACleanup();
		return;
	}
}
// 3.创建服务端socket
SOCKET Sock_Server()
{
	// 创建服务端 socket
	SOCKET sock_server = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP);
	// 判断服务端socket是否成功创建
	if (INVALID_SOCKET == sock_server)
	{
		//出错了
		int a = WSAGetLastError();
		printf("创建服务端socket失败,获取的错误码为 %d\n", a);
		//关闭网络库
		WSACleanup();
		return 0;
	}
	return sock_server;
}
// 4.绑定地址与端口号
void Bind(SOCKET sock)
{
	// 声明bind函数参数2所需的 结构体
	struct sockaddr_in sockMsg;
	// 给结构体成员赋值
	sockMsg.sin_family = AF_INET;
	sockMsg.sin_port = htons(12345);
	sockMsg.sin_addr.S_un.S_addr = inet_addr("127.0.0.1");
	// 绑定地址与端口号
	int nRet = bind(sock, (struct sockaddr*)&sockMsg, sizeof(sockMsg));
	// 判断是否成功绑定地址与端口号
	if (SOCKET_ERROR == nRet)
	{
		//出错了
		int a = WSAGetLastError();
		printf("绑定地址与端口号失败,获取的错误码为 %d\n", a);
		//关闭socket
		closesocket(sock);
		//关闭网络库
		WSACleanup();
		return;
	}
}
// 5.实现事件选择模型核心代码
void Event_Select(SOCKET sock)
{
/*int WSAAPI WSAEventSelect
  (	// 函数功能:给事件绑上socket与对应的操作,并投递给操作系统
  		SOCKET   s,				// 被绑定的socket 最终,每个socket都会被绑定一个事件
  		WSAEVENT hEventObject,	// 事件对象 逻辑,就是讲参数1与参数2绑定在一起
  		long     lNetworkEvents	// 具体的事件	FD_READ FD_WRITE ...
  );
*/
/* DWORD WSAAPI WSAWaitForMultipleEvents(	// 作用:等待事件产生信号
  DWORD          cEvents,		// 事件个数
  const WSAEVENT *lphEvents,	// 事件列表
  BOOL           fWaitAll,		// 事件等待方式
  DWORD          dwTimeout,		// 超时间隔
  BOOL           fAlertable		// 这里填FALSE
*/
/*int WSAAPI WSAEnumNetworkEvents(	// 获取事件类型,并将事件上的信号重置
  SOCKET             s,					// 对应的socket
  WSAEVENT           hEventObject,		// 对应的事件
  LPWSANETWORKEVENTS lpNetworkEvents	// 触发的事件类型 声明一个结构体指针
);*/


	// 1.创建服务端socket对应的事件对象
	nCreEvn = WSACreateEvent();
	// 判断是否成功创建事件对象
	if (WSA_INVALID_EVENT == nCreEvn)
	{
		// 出错了
		int a = WSAGetLastError();
		printf("创建事件对象失败,获取的错误码为: %d\n", a);
		// 关闭socket
		closesocket(sock);
		// 关闭网络库
		WSACleanup();
		return;
	}
	// 2.将socket和对应的事件对象绑定到一起,并投递给系统
	int nEvnSel = WSAEventSelect(sock, nCreEvn, FD_READ | FD_WRITE);
	// 判断是否成功绑定并投递
	if (SOCKET_ERROR == nEvnSel)
	{
		// 出错了
		int a = WSAGetLastError();
		printf("绑定并投递失败,获取的错误码为: %d\n", a);
		// 关闭socket
		closesocket(sock);
		// 关闭事件对象
		WSACloseEvent(nCreEvn);
		// 关闭网络库
		WSACleanup();
		return;
	}
	// 3.循环等待事件
	while (1)
	{
		// 等待事件
		DWORD nWaiFor = WSAWaitForMultipleEvents(1, &nCreEvn, FALSE, WSA_INFINITE, FALSE);
		// 判断是否等待事件
		if (WSA_WAIT_FAILED == nWaiFor)
		{
			// 没有等待到事件
			int a = WSAGetLastError();
			printf("没有等待到事件,出现的错误码为: %d\n", a);
			// 直接退出函数
			break;
		}
		// 声明WSAEnumNetworkEvents函数中参数3所需的结构体
		WSANETWORKEVENTS NetworkEvents;
		// 4.等到了事件,进行列举事件
		int nEnm = WSAEnumNetworkEvents(sock, nCreEvn, &NetworkEvents);
		// 判断是否成功列举事件
		if (SOCKET_ERROR == nEnm)
		{
			//出错了
			int a = WSAGetLastError();
			printf("列举事件失败,获取的错误码为: %d\n", a);
			//函数调用失败
			continue;
		}
		// 5.成功列举事件,进行分类处理 FD_READ  FD_WRITE
		// 处理FD_READ
		if (NetworkEvents.lNetworkEvents & FD_READ)
		{
			// 通过列举函数参数3的结构体成员变量进行判断是否进行接收消息
			if (0 == NetworkEvents.iErrorCode[FD_READ_BIT])
			{
				// 声明发送对方(客户端)的结构体对象
				// 声明recvfrom函数中参数5所需的对方结构体对象
				struct sockaddr sockClient;
				// 获取对方结构体大小
				int len = sizeof(sockClient);
				// 声明recvfrom函数中参数2所需的 接收数据的缓存区字节数组
				char strBuf[548] = { 0 };
				int nRecv = recvfrom(sock, strBuf, 548, 0, &sockClient, &len);
				// 判断是否成功接收数据
				if (SOCKET_ERROR == nRecv)
				{
					//出错了
					int a = WSAGetLastError();
					printf("接收数据失败,获取的错误码为: %d\n", a);
				}
				// 打印数据
				printf("%s\n", strBuf);

				// 进行发送数据(给客户端发送数据)
				int nSend = sendto(sock, "send ok", sizeof("send ok"), 0, &sockClient, len);
				// 判断是否成功发送数据
				if (SOCKET_ERROR == nSend)
				{
					//出错了
					int a = WSAGetLastError();
					printf("发送数据失败,获取的错误码为: %d\n", a);
				}
			}
		}
		// 处理FD_WRITE
		if (NetworkEvents.lNetworkEvents & FD_WRITE)
		{
			// 通过列举函数参数3的结构体成员变量进行判断是否成功发送sendto
			if (0 == NetworkEvents.iErrorCode[FD_WRITE_BIT])
			{
				printf("成功发送\n");
			}
		}
	}
}
// 监视窗口回调函数
BOOL WINAPI fun(DWORD dwCtrlType)
{
	switch (dwCtrlType)
	{
	case CTRL_CLOSE_EVENT:
		// 关闭socket
		closesocket(socketServer);
		// 关闭对应的事件
		WSACloseEvent(nCreEvn);
		// 清理网络库
		WSACleanup();
		break;
	}
	return TRUE;
}

客户端代码:

#define _WINSOCK_DEPRECATED_NO_WARNINGS

#include <stdio.h>
#include <stdlib.h>
#include <WinSock2.h>

#pragma comment(lib, "ws2_32.lib")

/*
	UDP/IP 基本C/S模型 客户端:
		与 UDP/IP 基本C/S模型 服务端代码的区别:
		代码基本一致,只有bind 函数有区别
		服务端需要进行绑定地址与端口号,因为服务端是一对多
		客户端只需要创建服务端地址与端口结构体,与服务端发送数据报时,
		  直接传递的是创建的结构体就可以了,不用进行绑定,因为服务端的ip与端口号固定不变的	

*/
// 1.打开网络库 2.校验版本号
void OpenAndCheckWSAStartup();

// 3.创建服务端socket
SOCKET Sock_Server();

// 全局声明要创建 创建客户端socket
SOCKET socketClient;

// 4.与客户端进行发收数据
void SendToAndRecvFrom(SOCKET sock);

int main()
{
	// 1.打开网络库 2.校验版本号
	OpenAndCheckWSAStartup();

	// 3.创建服务端socket
	SOCKET socketClient = Sock_Server();

	// 4.与客户端进行发收数据
	SendToAndRecvFrom(socketClient);

	//释放socket 
	closesocket(socketClient);
	//清理网络库
	WSACleanup();
	system("pause");
	return 0;
}

// 1.打开网络库 2.校验版本号
void OpenAndCheckWSAStartup()
{
	// 定义WSAStartup函数中参数1 所需的版本号
	WORD wVersionRequestd = MAKEWORD(2, 2);	// 设置2.2版本号
	// 声明WSAStartup函数中参数1 所需的结构体
	WSADATA WSAData;
	// 打开网络库
	int nRet = WSAStartup(wVersionRequestd, &WSAData);
	if (0 != nRet)
	{
		//出错了
		int a = WSAGetLastError();
		printf("网络库打开失败 %d\n", a);
		return;
	}
	// 检验版本号
	if (2 != HIBYTE(WSAData.wVersion) || 2 != LOBYTE(WSAData.wVersion))
	{
		printf("校验版本号错误\n");
		// 关闭网络库
		WSACleanup();
		return;
	}
}
// 3.创建服务端socket
SOCKET Sock_Server()
{
	// 创建服务端 socket
	SOCKET sock_server = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP);
	// 判断服务端socket是否成功创建
	if (INVALID_SOCKET == sock_server)
	{
		//出错了
		int a = WSAGetLastError();
		printf("创建服务端socket失败,获取的错误码为 %d\n", a);
		//关闭网络库
		WSACleanup();
		return 0;
	}
	return sock_server;
}
// 4.与客户端进行发收数据
void SendToAndRecvFrom(SOCKET sock)
{
	/*int recvfrom(
	  SOCKET   s,
	  char     *buf,
	  int      len,
	  int      flags,
	  sockaddr *from,
	  int      *fromlen
	);
	recvfrom函数:
		作用:得到当前服务端接收到的消息
		本质:复制	将协议缓存区的数据复制黏贴进自定义的buf
		与recv函数一样是进行接收消息,不同的是recv是傻等,recvfrom是死等
	参数1:客户端的socket
		特点:
			TCP是获取指定的,recv函数与客户端是1对1的关系 -- 傻等
			UDP是无差别获取,recvfrom函数与客户端是1对多的关系 -- 死等
	参数2:客户端消息的存储空间,也就是一个字符数组
		广域网:各级路由器上的MTU最小值是576字节
			tcp = 576 - 20(TCP包头) - 20(IP包头) = 536;
			udp = 576 - 8(UDP包头) - 20(IP包头) = 548;
	参数3:想要读取的字节个数, 参数2的字节数
	参数4:数据的读取方式
			0			从协议缓存区取一个数据报,然后就删除掉
			MSG_PEEK	取出数据报,但是协议缓存内不删除,一直残留,导致影响后面的数据读取
			MSG_OOB		带外数据
	参数5:对方的IP地址端口号
			此处结构体为相应的IP地址和端口号 struct sockaddr* sockClient; 填 &sockClient;
	参数6:参数5结构体的大小 int len = sizeof(&sockClient);	填 &len;
	*/
	/*int sendto(
	  SOCKET         s,
	  const char     *buf,
	  int            len,
	  int            flags,
	  const sockaddr *to,
	  int            tolen
	);
	sendto函数:
		作用:向目标发送数据报
		本质:send函数将我们的数据复制黏贴进行系统的协议发送缓冲区,计算机伺机发出去
	参数1:当前客户端的socket(自己的socket)
	参数2:给接收方发送的字节串  UDP统一字节数 548
	参数3:参数2的大小(字节个数 548)
	参数4:填0
	参数5:对方IP地址与端口号结构体
	参数6:参数5的大小

	*/
	// 声明sendto函数中参数5所需的 服务端地址与端口号结构体
	struct sockaddr_in sockServer;
	// 给服务端地址与端口号结构体成员赋值
	sockServer.sin_family = AF_INET;
	sockServer.sin_port = htons(12345);
	sockServer.sin_addr.S_un.S_addr = inet_addr("127.0.0.1");

	// 循环与客户端进行收发消息
	while (1)
	{
		// 定义要发送的字节数组
		char strBuf[548] = { 0 };
		// 手动发送
		scanf_s("%s", strBuf, 548);
		// 设置输入0时,将关闭客户端
		if ('0' == strBuf[0])
		{
			break;
		}

		// 1.发消息
		int nSet = sendto(sock, strBuf, sizeof(strBuf), 0, (struct sockaddr*)&sockServer, sizeof(sockServer));
		// 判断是否成功发送消息
		if (SOCKET_ERROR == nSet)
		{
			// 出错了
			int a = WSAGetLastError();
			printf("发送消息失败,获取的错误码为:%d\n", a);
			continue;
		}

		// 2.收消息
		// 定义recvfrom 函数所需的接收字节数组
		char buf[548] = { 0 };
		// 声明recvfrom函数中参数5所需的 结构体相应的IP地址与端口号
		struct sockaddr sockClient;	// 对方的IP地址与端口号
		// 定义recvfrom函数中参数6所需的 参数5结构体的大小
		int len = sizeof(sockClient);
		int nRet = recvfrom(sock, buf, 548, 0, &sockClient, &len);
		// 判断是否成功接收消息
		if (SOCKET_ERROR == nRet)
		{
			// 出错了
			int a = WSAGetLastError();
			printf("接收消息失败,获取的错误码为:%d\n", a);
			continue;
		}
		// 进行接收消息
		printf("Server say: %s\n", buf);	
	}
}

程序运行结果:

?

?

  网络协议 最新文章
使用Easyswoole 搭建简单的Websoket服务
常见的数据通信方式有哪些?
Openssl 1024bit RSA算法---公私钥获取和处
HTTPS协议的密钥交换流程
《小白WEB安全入门》03. 漏洞篇
HttpRunner4.x 安装与使用
2021-07-04
手写RPC学习笔记
K8S高可用版本部署
mySQL计算IP地址范围
上一篇文章      下一篇文章      查看所有文章
加:2021-08-25 12:33:59  更:2021-08-25 12:35:56 
 
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁

360图书馆 购物 三丰科技 阅读网 日历 万年历 2024年11日历 -2024/11/25 21:41:16-

图片自动播放器
↓图片自动播放器↓
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
图片批量下载器
↓批量下载图片,美女图库↓
  网站联系: qq:121756557 email:121756557@qq.com  IT数码